前の記事で、スプレッドシートの一覧からGmailの下書きを作るところまでやりました。
→ スプレッドシートの一覧から、決まった書式のメールを自動で作る【まず動かす版】
あのコードには、弱いところが2つありました。
- 実行を2回押すと、同じ人に同じメールが2通届く
- 文面を変えたいときは、コードそのものを書き替えるしかない
職場で使うと、どちらも困ります。特に1つ目は、送ってしまうと取り消せません。
この記事では、コードを整えて、この2つを直します。あわせて、1日に送れる通数の上限にも備えます。
コードは今回も、そのまま貼るだけで動きます。
できあがるもの
シートは2枚になります。
1枚目は「宛先一覧」。前の記事のシートに、D列「送信日時」とE列「メモ」を足しただけです。

送った行には、送った日時が自動で入ります。日時が入っている行は、次に実行しても飛ばします。これで二重送信が起きません。
2枚目は「文面」。件名と本文をここに書きます。

{名前} と書いたところにB列の名前が、{品名} と書いたところにC列の品名が入ります。文面を変えたいときは、このシートを書き替えるだけです。コードは触りません。
前の記事から変わるところ
| 前の記事(まず動かす版) | この記事(整えたコード版) | |
|---|---|---|
| 二重送信 | 実行するたびに全員へ送る | 送信日時が入った行は飛ばす |
| 文面 | コードの中に書く | 「文面」シートに書く |
| 下書きと送信の切り替え | コードの1行を書き替える | 実行する関数を選ぶだけ |
| 送信の上限 | 考えていない | 足りないときは1通も送らずに止める |
| エラーが出た行 | そこで全体が止まる | その行だけ飛ばして、E列に理由を書く |
宛先一覧シートを用意する
前の記事のシートを、そのまま使えます。新しく作る場合は、前の記事の「シートを用意する」と同じ形で作ってください。
まず、下のタブの名前を「宛先一覧」に変えます。タブの「シート1」をダブルクリックすると、名前を書き替えられます。

名前は1文字でも違うと動きません。コードがこの名前でシートを探しているためです。
次に、D1に「送信日時」、E1に「メモ」と見出しを入れます。D列とE列の2行目から下は、空のままにしておきます。
| 宛先 | 名前 | 品名 | 送信日時 | メモ |
|---|---|---|---|---|
| 自分のメールアドレス | 山田 太郎 | 4月号 | ||
| 自分のメールアドレス | 鈴木 花子 | 4月号 |
前の記事と同じく、テストの間は宛先に自分のメールアドレスだけを入れてください。
文面シートを用意する
下のタブの左にある「+」を押して、シートを1枚足します。名前は「文面」にします。

A列は自分用のラベルなので、何を書いても動きには関係しません。大事なのはB1とB2です。
| A | B | |
|---|---|---|
| 1 | 件名 | {品名}のご案内 |
| 2 | 本文 | {名前} 様(改行)いつもお世話になっております。… |
B2の本文には改行を入れます。セルの中で改行するには、Enterではなく Ctrl+Enter を押します。Enterだけだと、下のセルに移ってしまいます。
本文の例です。そのまま使って構いません。
{名前} 様
いつもお世話になっております。
{品名}について、ご案内をお送りいたします。
ご不明な点がありましたら、このメールにご返信ください。
{ と } は半角で打ちます。全角の { } だと、名前が差し込まれずに「{名前}」のまま届きます。
コードを貼る
「拡張機能 → Apps Script」でエディタを開きます。前の記事のコードが残っている場合は、全部消してから貼ってください。
// =====================================
// 設定(ここだけ書き替えれば、ほかは触らなくて済む)
// =====================================
const CONFIG = {
SHEET_LIST: '宛先一覧', // 宛先を並べたシートの名前
SHEET_TEMPLATE: '文面', // 件名と本文を書いたシートの名前
CELL_SUBJECT: 'B1', // 件名を書いたセル
CELL_BODY: 'B2', // 本文を書いたセル
HEADER_ROWS: 1, // 見出しの行数
COLUMN: { // 宛先一覧の列の位置(A列=1、B列=2…)
ADDRESS: 1, // 宛先
NAME: 2, // 名前
ITEM: 3, // 品名
SENT_AT: 4, // 送信日時(送った行にだけ入る)
NOTE: 5 // メモ(エラーが出た行に理由が入る)
}
};
// =====================================
// 手で実行する関数(実行ボタンの横で選ぶ)
// =====================================
/**
* 下書きを作る。送信はしない。
* 送信日時も書き込まないので、何度実行しても大丈夫。
*/
function createDrafts() {
run_(false);
}
/**
* 本当に送信する。送った行には送信日時を書き込み、次からは飛ばす。
*/
function sendMails() {
run_(true);
}
// =====================================
// 部品(コードの中からだけ呼ぶ)
// =====================================
/**
* 全体の流れ。
* @param {boolean} isSend true なら送信、false なら下書き
*/
function run_(isSend) {
try {
const listSheet = getSheet_(CONFIG.SHEET_LIST);
const template = getTemplate_();
const targets = getTargets_(listSheet);
if (targets.length === 0) {
console.log('対象の行がありません(宛先が空か、すべて送信済みです)。');
return;
}
// 送り始めてから上限で止まると、半分だけ送られた状態になる。先に数えて止める
if (isSend) {
checkQuota_(targets.length);
}
let doneCount = 0;
for (const target of targets) {
if (processOne_(listSheet, target, template, isSend)) {
doneCount++;
}
}
const label = isSend ? '送信' : '下書き作成';
console.log(`${label}:${doneCount}件(対象 ${targets.length}件)`);
} catch (error) {
console.error(error.stack);
}
}
/**
* 名前でシートを取り出す。見つからなければ分かる言葉で止める。
* @param {string} sheetName シート名
* @return {Sheet}
*/
function getSheet_(sheetName) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
if (!sheet) {
throw new Error(`シート「${sheetName}」が見つかりません。シートの名前を確認してください。`);
}
return sheet;
}
/**
* 「文面」シートから件名と本文を読む。
* @return {{subject: string, body: string}}
*/
function getTemplate_() {
const sheet = getSheet_(CONFIG.SHEET_TEMPLATE);
const subject = sheet.getRange(CONFIG.CELL_SUBJECT).getValue();
const body = sheet.getRange(CONFIG.CELL_BODY).getValue();
if (!subject || !body) {
throw new Error(`「${CONFIG.SHEET_TEMPLATE}」シートの件名(${CONFIG.CELL_SUBJECT})か本文(${CONFIG.CELL_BODY})が空です。`);
}
return { subject: String(subject), body: String(body) };
}
/**
* 宛先一覧から、これから処理する行だけを集める。
* 見出し行・宛先が空の行・送信日時が入っている行は飛ばす。
* @param {Sheet} sheet 宛先一覧シート
* @return {Object[]} 行番号・宛先・名前・品名の一覧
*/
function getTargets_(sheet) {
const rows = sheet.getDataRange().getValues();
const col = CONFIG.COLUMN;
const targets = [];
// 送信日時を書き戻すために行番号が要るので、番号つきで回す
for (const [index, row] of rows.entries()) {
if (index < CONFIG.HEADER_ROWS) {
continue;
}
// trim() で前後の空白(全角スペースも)を取り除く
const address = String(row[col.ADDRESS - 1]).trim();
if (!address) {
continue;
}
if (row[col.SENT_AT - 1]) {
continue;
}
targets.push({
rowNumber: index + 1,
address: address,
name: row[col.NAME - 1],
item: row[col.ITEM - 1]
});
}
return targets;
}
/**
* 今日あと何件送れるかを確かめ、足りなければ1通も送らずに止める。
* @param {number} count これから送る件数
*/
function checkQuota_(count) {
const remaining = MailApp.getRemainingDailyQuota();
if (count > remaining) {
throw new Error(`今日送れるのはあと${remaining}件です。対象が${count}件あるので、送信を止めました。`);
}
}
/**
* 1行ぶんのメールを作る(または送る)。
* 失敗してもほかの行は続けられるよう、ここで受け止めてメモ欄に書く。
* @return {boolean} うまくいったら true
*/
function processOne_(sheet, target, template, isSend) {
const col = CONFIG.COLUMN;
const subject = fillTemplate_(template.subject, target);
const body = fillTemplate_(template.body, target);
try {
if (isSend) {
GmailApp.sendEmail(target.address, subject, body);
// まとめて最後に書くのではなく、1通送るたびにすぐ書く。
// 途中で止まっても、送った分には印が残る
sheet.getRange(target.rowNumber, col.SENT_AT).setValue(new Date());
sheet.getRange(target.rowNumber, col.NOTE).clearContent();
} else {
GmailApp.createDraft(target.address, subject, body);
}
return true;
} catch (error) {
console.error(`${target.rowNumber}行目(${target.address}):${error.message}`);
sheet.getRange(target.rowNumber, col.NOTE).setValue(`エラー:${error.message}`);
return false;
}
}
/**
* 文面の {名前} {品名} を、その行の値に置き換える。
* @param {string} text 件名または本文
* @param {Object} target その行のデータ
* @return {string}
*/
function fillTemplate_(text, target) {
return text
.replaceAll('{名前}', target.name)
.replaceAll('{品名}', target.item);
}
貼ったら、フロッピーディスクのマークで保存します。

いちばん上の CONFIG が、設定をまとめた場所です。シート名や列の位置を変えたくなったら、ここだけを書き替えます。CONFIG の考え方は、こちらの記事で説明しています。
→ 動いたGASのコードを、後から直しやすい形に整える【CONFIG・関数分割・try-catch】
まず下書きで確かめる
実行ボタンの右に、実行する関数を選ぶ欄があります。ここで createDrafts を選んでから、実行を押します。

createDrafts は下書きを作るだけで、送信はしません。送信日時も書き込まないので、何度実行しても大丈夫です。
承認画面が出たら、前の記事と同じ手順で許可してください。自分で書いたスクリプトなので、「このアプリはGoogleで確認されていません」と出るのは正常です。
実行ログに「下書き作成:2件(対象 2件)」と出たら成功です。

Gmailの下書きを開いて、宛先・名前・品名・改行を確かめてください。
送信する
下書きに問題がなければ、関数の選択欄を sendMails に切り替えて、実行を押します。

注意:一度送ったメールは取り消せません。 sendMails を押す前に、宛先一覧のA列をもう一度見てください。テスト用の行や、送るつもりのない相手が残っていないか確かめます。
実行ログに「送信:2件(対象 2件)」と出たら、シートに戻ります。D列に送信日時が入っているはずです。
セルには日付だけが表示されますが、時刻も記録されています。D列のセルをクリックすると、上の数式バーに「2026/09/24 22:04:34」のように時刻まで出ます。

もう一度押しても送られないことを確かめる
ここで、わざと sendMails をもう一度実行してみてください。
実行ログにこう出て、何も送られずに終わります。
対象の行がありません(宛先が空か、すべて送信済みです)。
D列に日時が入っている行は飛ばすので、何度押しても同じ人に2通届くことはありません。
新しく送りたい相手ができたら、下の行に足して sendMails を実行します。日時が空の行にだけ送られます。
同じ人にもう一度送りたいときは、その行のD列の日時を消してから実行します。
1日に送れる通数の上限
GASから送れるメールには、1日あたりの上限があります。Googleの公式ドキュメント(2026年9月時点)では、次のようになっています。
| アカウントの種類 | 1日に送れる宛先の数 |
|---|---|
| 個人のGoogleアカウント(@gmail.com) | 100 |
| Google Workspace(会社などの有料アカウント) | 1,500 |
数えるのはメールの通数ではなく、宛先の人数です。上限は最初に送ってから24時間でリセットされます。夜中の0時に戻るわけではありません。数字は予告なく変わることがあるので、気になるときは公式ページを見てください。
→ Quotas for Google Services(Google公式・英語)
上限を超えると、その時点でエラーになって止まります。送っている途中で止まると、前半の人には届き、後半の人には届かない状態になります。
そこでこのコードは、送り始める前に「今日あと何件送れるか」を調べます。対象の件数より少なければ、1通も送らずに止めて、ログにこう出します。
Error: 今日送れるのはあと○件です。対象が○件あるので、送信を止めました。
実際に出るとどう見えるかは、コードを一時的に書き替えて確かめました。個人のGmailアカウントで2通送ったあとに試すと、こう出ました。
Error: 今日送れるのはあと98件です。対象が1件あるので、送信を止めました。
100件から、送った2通のぶんが減って98件になっています。
つまずき集
前の記事と同じく、エラーには2種類あります。書き方の間違い(構文エラー)は、保存した時点で画面左下の赤い帯に出ます。動かして初めて分かる問題(実行時エラー)は、実行ログに出ます。
今回のコードは、よくある間違いに気づいたら、日本語で理由を書いて止まるようにしてあります。実行ログに日本語が出たら、まずそれを読んでください。
シートの名前が違う
Error: シート「宛先一覧」が見つかりません。シートの名前を確認してください。
at getSheet_ (コード:84:11)
at run_ (コード:48:23)
at createDrafts (コード:28:3)
実行ログに赤い行で出ます。2行目から下の「at 〜」は、コードのどこで止まったかを示すものです。読み飛ばして構いません。1行目の日本語だけ読めば分かります。
タブの名前が「シート1」のまま、または「宛先一覧 」のように後ろに空白が入っていると、こうなります。タブをダブルクリックして、名前を打ち直してください。
文面が空
Error: 「文面」シートの件名(B1)か本文(B2)が空です。
これも赤い行で出て、下に「at 〜」の行が続きます。
文面をB列ではなくA列に書いてしまった場合も、こうなります。
差し込まれずに {品名} のまま入る
エラーは出ません。実行ログも「下書き作成:1件(対象 1件)」と、うまくいったときと同じ表示です。下書きを開いて初めて、件名が「{品名}のご案内」のままになっていることに気づきます。
かっこが全角になっていると、こうなります。{ と } は半角で打ってください。「名前」「品名」の字が違っている場合(「氏名」など)も、同じように差し込まれません。差し込めるのは {名前} と {品名} の2つだけです。
エラーが出ないので、実行ログだけ見ていると気づけません。送信の前に下書きで確かめる理由の1つです。
E列に「エラー:Invalid email」と入った
実行ログに、赤い行でこう出ます。
4行目(xxxxx@gmail.com):Invalid email: xxxxx@gmail.com
(アドレスの部分は伏せています)
その下には「下書き作成:0件(対象 1件)」と出ます。シートのE列(メモ)にも、同じ理由が書き込まれます。
エラー:Invalid email: xxxxx@gmail.com
「Invalid email」は「メールアドレスとして正しくない」という意味です。この例では、@ が全角の @ になっています。画面ではほとんど見分けがつきません。何行目のどのアドレスかはログに出るので、そのセルを打ち直してください。
前後の空白は自動で取り除くようにしてありますが、途中に入った全角の文字や空白は取り除けません。
その行は送信日時が入らないので、アドレスを直してもう一度 sendMails を実行すれば、その行にだけ送られます。
直したはずなのに、前と同じ結果になる
コードを書き替えたあと、保存していません。画面の上に「変更が保存されていません」と出ていないか見てください。
保存せずに実行すると、直す前のコードが動きます。フロッピーディスクのマーク(または Ctrl+S)で保存して、「ドライブに保存しました」に変わってから実行してください。
run_ や getSheet_ を選びたいのに出てこない
名前の最後に _ が付いている関数は、コードの中から呼ぶための部品です。わざと選択欄に出ないようにしてあります。手で実行するのは createDrafts と sendMails の2つだけです。
まとめ
- 送った行には送信日時が入り、次からは飛ばす。何度押しても二重には送られない
- 文面は「文面」シートで書き替える。
{名前}{品名}に差し込まれる - まず
createDraftsで下書きを確かめてから、sendMailsで送る
このコードの弱いところ
このコードにも、まだ弱いところがあります。
- 送れるのは文字だけのメールです。添付ファイルや、文字の色・太字は付けられません
- 宛先の人数が1日の上限を超える量は、日を分けて送る必要があります
- 毎回、自分でエディタを開いて実行を押す必要があります
3つ目については、このあとの記事で、決まった時刻にGASを自動で動かす方法を扱います。
※2026年9月時点の画面で確かめた内容です。Googleの画面表示は変わることがあります。

コメント