Mihata
仕事効率化(DX)2026.09.12

GASでスプレッドシートからメール自動送信|重複を防ぐ手順

結論:3つの部品でスプレッドシートからのメール自動送信は完成する

Googleスプレッドシートの行を差し込んでメールを自動送信する仕組みは、①差し込み送信のコード ②送信済みフラグ ③時間主導型トリガーの3点だけでできています。ここが揃っていないと、動いても「同じ人に何通も届く」「動いたり動かなかったりする」という運用事故になります。

コードはコピペで動くものを下に全文載せます。あわせて、Apps Script 公式が定める送信上限(個人アカウントは1日100件、Google Workspace は1日1,500件の宛先数。Quotas for Google Services)と、実行時間が1回あたり6分で打ち切られる制限まで、実務で必ずぶつかるところを先に押さえます。

スプレッドシートの関数そのものを整理したい場合は、Googleスプレッドシート関数10選で業務効率化する記事が入口として向いています。本記事は関数ではなく、GAS(Google Apps Script)でのメール送信の実装だけを扱います。

準備:シートの列構成とスクリプトエディタ

差し込みに必要な5列

先に列を決めてしまうほうが、あとの分岐が素直になります。実務でよく使うのは次の5列です。E列の「送信日時」が二重送信を防ぐ心臓部なので、最初から用意しておきます。

見出し

入る値

役割

A

メールアドレス

taro@example.com

宛先。空なら送らない

B

宛名

山田 太郎

本文への差し込み

C

期限

2026/09/30(日付型)

送信条件の判定

D

ステータス

未対応/完了

送信条件の判定

E

送信日時

(空。送ったら自動で入る)

送信済みフラグ

C列は必ず日付型で入れます(セルの表示形式が「書式なしテキスト」だと文字列になり、後述の日付比較がそのまま失敗します)。

スクリプトエディタを開く

対象のスプレッドシートを開き、メニューの「拡張機能」→「Apps Script」を選びます。新しいタブでエディタが開き、そのスプレッドシートに紐づいたスクリプト(コンテナバインド型)が作られます。以降のコードはここに貼り付けます。

最小コードと、MailApp・GmailApp の違い

まず1通だけ送ってみる

いきなり全行ループを書かず、1通だけ送って承認を通すのが早道です。次のコードを貼り、関数を選んで「実行」を押します。初回は権限の承認ダイアログが出るので、許可します。

function sendOne() {
  MailApp.sendEmail('taro@example.com', 'テスト送信', '本文です。');
}

自分宛のアドレスを入れて、実際に届くところまで確認してから次に進みます。ここで届かなければ、差し込みを作り込んでも届きません。

MailApp と GmailApp はどちらを使うか

送るだけなら両方使えますが、役割がはっきり違います。公式リファレンスでは、MailApp はメール送信専用でユーザーの受信トレイにはアクセスできない、GmailApp は下書き・スレッド・ラベルまで触れる、と説明されています。

観点

MailApp

GmailApp

できること

送信のみ

送信+下書き作成・スレッド取得・ラベル操作

受信トレイへのアクセス

不可

要求する権限

狭い(送信のみ)

広い(Gmail全体)

再承認の起きやすさ

起きにくい

起きやすい(公式が明記)

送信済みメールの残り方

送信済みトレイに残らない

送信済みトレイに残る

1日の送信上限

共通(同じ枠を消費)

共通(同じ枠を消費)

実務での判断はシンプルで、「送るだけ」なら MailApp、「送った記録を Gmail に残したい・下書きで止めたい」なら GmailAppです。権限は狭いほど事故が起きにくいので、迷ったら MailApp から始めて、下書き機能が要るようになった時点で GmailApp へ切り替えるのが安全です。

コピペで動く差し込みテンプレート

先ほどの5列構成をそのまま使い、「ステータスが未対応」かつ「期限の前日」の行にだけリマインドを送る全文です。送信したらE列に日時を書き戻します。

const SHEET_NAME = '送信リスト';
// A〜E列(1始まり)
const COL = { email: 1, name: 2, deadline: 3, status: 4, sent: 5 };

function sendReminders() {
  const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
  const values = sheet.getDataRange().getValues();
  const tz = Session.getScriptTimeZone();
  const today = new Date();
  let count = 0;

  for (let i = 1; i < values.length; i++) {   // 0行目は見出しなので1から
    const row    = values[i];
    const email  = String(row[COL.email - 1]).trim();
    const name   = String(row[COL.name - 1]).trim();
    const dead   = row[COL.deadline - 1];
    const status = String(row[COL.status - 1]).trim();
    const sent   = String(row[COL.sent - 1]).trim();

    if (!email || sent) continue;               // 宛先が空/送信済みは飛ばす
    if (status !== '未対応') continue;           // ステータス条件
    if (daysUntil(dead, today) !== 1) continue; // 期限の前日だけ

    const subject = '【ご確認のお願い】' + Utilities.formatDate(dead, tz, 'M月d日') + 'が期限です';
    const body = [
      name + ' 様',
      '',
      'いつもお世話になっております。',
      Utilities.formatDate(dead, tz, 'yyyy年M月d日') + 'が期限のご対応について、念のためご連絡いたしました。',
      '',
      'よろしくお願いいたします。'
    ].join('\n');

    GmailApp.sendEmail(email, subject, body, { name: '事務局' });

    sheet.getRange(i + 1, COL.sent).setValue(new Date()); // 送信済みフラグ
    SpreadsheetApp.flush();                               // その場でシートに書き戻す
    count++;
  }
  console.log(count + '件送信しました');
}

// 日付を「日」単位に丸めてから引く(時刻が混ざると1日ずれる)
function daysUntil(target, base) {
  if (!(target instanceof Date)) return NaN;
  const a = new Date(target.getFullYear(), target.getMonth(), target.getDate());
  const b = new Date(base.getFullYear(), base.getMonth(), base.getDate());
  return Math.round((a - b) / 86400000);
}

本文は配列に1行ずつ書いて join でつないでいます。文字列の足し算を1本で書くより、あとから文面を直すときに壊しにくいためです。差し込む値を増やしたいときは、COL に列番号を足して同じ形で読み出します。

送信条件の付け方と、二重送信の止め方

条件1:期限の前日だけ送る

ここが最も事故が多い箇所です。スプレッドシートの日付セルはその日の0時00分の Date として渡ってくるのに対し、new Date() は今この瞬間の時刻を持ちます。そのまま引き算すると、同じ日なのに差が「マイナス0.6日」になり、条件から外れます。

上のコードで daysUntil() を別関数にしているのはこのためで、両方を年月日だけの Date に作り直してから引き算しています。「前日」なら1、「当日」なら0、「3日前に予告」なら3で判定できます。

条件2:ステータス列・チェックボックス列で送る

日付ではなく人の操作をきっかけにしたいときは、D列を条件に使います。プルダウン(データの入力規則)で「未対応/対応中/完了」を選ばせ、status !== '未対応' で弾く形です。

チェックボックスで「この行を送る」と指定したい場合は、その列の値が真偽値(true / false)で返るので、if (row[COL.send - 1] !== true) continue; と書きます。文字列比較にすると必ず外れるので注意してください。

送信済みフラグで二重送信を防ぐ

自動送信でいちばん怖いのは、失敗ではなく同じ人に何度も届くことです。原因は主に3つあります。トリガーが二重に登録されている、条件が翌日も成立してしまう、実行が6分で打ち切られて途中から再実行される、のいずれかです。

これらを一括で止めるのが送信済みフラグです。「E列が空でなければ絶対に送らない」「送った直後にE列へ書き戻す」の2点をセットで持たせます。

if (!email || sent) continue;
// ...送信...
sheet.getRange(i + 1, COL.sent).setValue(new Date());
SpreadsheetApp.flush();

SpreadsheetApp.flush() を入れているのがポイントです。GASはシートへの書き込みをまとめて後から反映するため、これが無いまま途中でエラーや時間切れになると、メールは送ったのにフラグだけ残らない状態が生まれます。次の実行で同じ行がもう一度送られるのは、ほぼこのパターンです。

フラグは true ではなく送信日時を入れておくと、あとから「いつ誰に送ったか」を人が目で追えます。台帳としての価値が段違いなので、実務ではこちらを勧めています。

こうしたシート起点の自動化は、続けるほど「列が増える」「例外が増える」で読みにくくなっていきます。私たちは、スプレッドシートをそのまま使いながら入力と権限だけを画面に切り出す「スプシ de 社内アプリ」というサービスも行っております。記事の途中で恐縮ですが、シートを捨てずに済む作り方として良い選択肢だと思っておりますので、よろしければ合わせてご覧いただけたら嬉しいです。

時間主導型トリガーの設定手順

エディタでの設定

関数が手動で正しく動くのを確認してから、トリガーを付けます。Apps Script 公式の手順は次のとおりです。

  1. Apps Script プロジェクトを開く
  2. 左側の「トリガー」(時計のアイコン)をクリックする
  3. 右下の「トリガーを追加」をクリックする
  4. 実行する関数に sendReminders を選び、イベントのソースを「時間主導型」、種類を「日付ベースのタイマー」、時刻を「午前8時〜9時」のように選ぶ
  5. 「保存」をクリックする

時刻が「午前8時〜9時」という幅のある選び方なのは仕様です。公式は、午前9時のトリガーを作ると Apps Script が9時〜10時の間の時刻を選び、以後は毎日その時刻を保つと説明しています。分単位で正確に送りたい業務(例:秒単位の締切通知)には向きません。

トリガーは1ユーザー1スクリプトあたり20個までです。テストのたびに追加していくと上限に当たるので、不要なものはその場で削除します。

トリガーは「作った人のアカウント」で動く

見落とされがちですが運用上いちばん重いのがこれです。公式は「インストール可能なトリガーは、常に作成した人のアカウントで実行される」と明記しています。つまり、あなたが設定したトリガーから送られるメールは、誰がシートを触っていてもあなたのアカウントから送信され、あなたの1日の送信枠を消費します

さらに、別のアカウントが設定したトリガーは、他の人からは一覧に表示されません。「誰も設定していないはずのメールが毎朝届く」「担当者が退職した翌日から自動送信が止まった」という相談の多くはこれが原因です。共有で使う仕組みなら、個人アカウントではなく共有用のアカウントで作るか、後述のアプリ化を検討したほうが安全です。

公式のクォータ:1日の送信上限と実行時間

数値は Apps Script 公式の Quotas for Google Services に掲載されているものです。上限は「ユーザーごと」に設定され、最初のリクエストから24時間後にリセットされます。また公式は、これらの値は予告なく変更・削減されうると明記しています(下表は本記事執筆時点の公式値)。

項目

個人アカウント(gmail.com など)

Google Workspace アカウント

1日のメール送信先件数(MailApp など)

100 / 日

1,500 / 日

1日のメール送信先件数(同一ドメイン内)

100 / 日

2,000 / 日

スクリプトの実行時間

6分 / 1回の実行

6分 / 1回の実行

トリガーの合計実行時間

90分 / 日

6時間 / 日

1通あたりの宛先数

50 / 通

50 / 通

添付ファイルの数

250 / 通

250 / 通

添付ファイルの合計サイズ

25 MB / 通

25 MB / 通

本文サイズ

200 KB / 通

400 KB / 通

トリガーの数

20 / ユーザー / スクリプト

20 / ユーザー / スクリプト

数え方は「通数」ではなく「宛先の数」です。CC・BCC もそれぞれ1件として数えられるため、1通に宛先3件を入れれば枠は3件減ります。個人アカウントの100件は、少し配信めいたことをするとすぐ届く水準です。

また、トリガーの合計実行時間は個人アカウントで1日90分しかありません。1分おきのトリガーを回すと、1回30秒かかるだけで枠を使い切ります。頻度は「業務に必要な最小」で設計してください。

残り送信可能数を毎回確認する

公式は、残りの送信可能数を取得する MailApp.getRemainingDailyQuota() を用意しています。ループの前に一度だけ呼んで、足りなければ送らずに止めるのが行儀のよい書き方です。

const remaining = MailApp.getRemainingDailyQuota();
if (remaining < 10) {
  console.log('残り送信可能数が少ないため中止します: ' + remaining);
  return;
}

上限に当たると Limit exceeded: Email Recipients Per Day. のような例外でスクリプトごと止まります。止まった時点で送信済みフラグが立っていない行が残るので、翌日の実行で続きから自然に再開できる設計にしておくと、リカバリの手間がほぼゼロになります。

HTML本文・添付ファイル・下書きの使い分け

HTML本文と添付ファイル

4番目の引数(オプション)で指定します。htmlBody を渡すと、HTMLを表示できる環境ではそちらが優先され、表示できない環境では3番目のテキスト本文が使われます。テキスト本文は省略できないので、両方書きます。

const pdf = DriveApp.getFileById('ファイルID').getAs(MimeType.PDF);

GmailApp.sendEmail(email, subject, 'HTMLを表示できない環境向けのテキスト本文です。', {
  htmlBody: '<p>' + name + ' 様</p><p>請求書を添付いたします。</p>',
  attachments: [pdf],
  cc: 'keiri@example.com',
  name: '事務局',
  replyTo: 'info@example.com'
});

添付は Drive のファイルを Blob にして配列で渡します。合計25MBまでという上限があるので、重い資料はファイルそのものではなくDrive の共有リンクを本文に書くほうが確実です(受け取り側のメールボックス容量にも優しくなります)。

差出人名は name、返信先は replyTo で変えられます。自動送信は「誰から来たのか分からない」と開いてもらえないので、この2つはほぼ必ず指定しておくべき項目です。

いきなり送らず下書きにする

金額や宛名が入るメールを、テストなしで本番送信するのは危険です。GmailApp.createDraft() は sendEmail と同じ引数で下書きを作るだけなので、最初の数日はこれで回して、Gmail の下書きを目視で確認するのを勧めています。

const draft = GmailApp.createDraft(email, subject, body, { name: '事務局' });
// 目視で確認したあと、送るときだけ次の行を有効にする
// draft.send();

できあがった下書きは draft.send() でそのまま送れます。つまり、「まず下書き→人が確認→送信」の運用から始めて、慣れたら1行だけ差し替えて全自動にするという移行ができます。MailApp には下書き機能が無いので、この段取りを取るなら GmailApp を選びます。

よくあるエラーと対処

症状・メッセージ

原因

対処

実行時に承認画面が出て進めない

初回実行で権限が未承認

「詳細」→自分のアカウントを選んで許可。自分のスクリプトを自分で承認するので問題ございません

トリガーだけが「承認が必要です」で失敗する

コードを足して必要な権限が増えた

公式の案内どおり、エディタで関数を一度手動実行して承認をやり直す。トリガーは作成者の承認で動く

「このアプリは Google で確認されていません」

機微なスコープを使う未確認のプロジェクト

自分(または自社ドメイン内)で使う分は詳細を開いて続行できる。社外へ配布するなら OAuth クライアントの確認申請が必要

Invalid email / 宛先が空で落ちる

空行・全角スペース・末尾の空白

trim() してから空判定し、空ならスキップ。最終行までループが走る getDataRange() では必ず起きる

条件に合うはずの行が送られない

日付に時刻が混ざって比較がずれた

年月日だけの Date に作り直してから引き算する(本記事の daysUntil)。セルが文字列の場合は日付型に直す

同じ人に何通も届く

トリガーの二重登録/フラグの書き戻し漏れ

トリガー一覧を見て重複を削除。送信直後に setValue() と flush() をセットで実行する

Exceeded maximum execution time

1回6分の実行時間を超えた

1回あたりの件数に上限を設け、未送信の行は次回に回す。送信済みフラグがあれば続きから再開できる

Limit exceeded: Email Recipients Per Day.

1日の送信先件数の上限に到達

getRemainingDailyQuota() で事前に確認し、足りない分は翌日に繰り越す

GASで作らないほうがよい3つの分岐

ここまで書いておいて何ですが、GASが向かない条件ははっきりしています。実務で「作り直し」になるのは、次のどれかを無視して走り出した場合です。

  • 件数が多い:個人アカウントで1日100件、Google Workspace で1日1,500件という宛先上限は、GAS側で工夫しても増やせません。顧客への一斉配信のように件数が読めないものは、最初からメール配信サービスを使うほうが安く済みます。
  • 複数人で運用する:トリガーは作成者のアカウントで動き、他の人の画面には出てきません。担当が変わるたびに設定し直す前提の仕組みは、いずれ必ず止まります。
  • 送信前に承認が要る:「上長が確認してから送る」をGASで表現しようとすると、承認状態を持つ列と、誰が押したかの記録と、差し戻しの導線が要り、シートが一気に読めなくなります。これは業務システムの仕事です。

3つのうち1つでも当てはまるなら、シートを入力台帳として残したまま、送信と承認の部分だけを画面(社内アプリ)に切り出すのが現実的です。判断の材料として、GAS開発を外注するときの費用相場と内製・外注の分かれ目も参考になります。

逆に、受発注のように「メールで届いた内容をシートに転記する」向きの自動化を考えているなら、受発注メールの転記を自動化する手段の比較のほうが近い話をしています。問い合わせへの一次返信を自動化したい場合は、問い合わせメールの自動返信を仕組み化する手順が参考になります。

まとめ

  • コードは差し込み・条件・送信済みフラグの3点。フラグは true ではなく送信日時を入れる
  • 送信直後の setValue() と flush() をセットにしないと、二重送信はいつか必ず起きる
  • 日付条件は年月日だけの Date に丸めてから比較する
  • トリガーは作った人のアカウントで動き、時刻には1時間の幅がある
  • 上限は個人100件/Workspace 1,500件(宛先数・1日)、実行は1回6分まで
  • 最初は createDraft() で下書きにして目視し、慣れたら1行差し替えて自動化する

ここまでの型で作れば、たいていの社内リマインドは安定して回ります。件数・人数・承認のどれかで頭打ちになったときが、仕組みを一段上げるタイミングです。どこで線を引くべきか迷われている段階でも構いませんので、現状のシートを見ながら一緒に考えるお手伝いをしております。

よくある質問

GASでメールを1日に何件まで自動送信できますか?

Apps Script 公式のクォータでは、宛先の数で数えて個人アカウント(gmail.com など)が1日100件、Google Workspace アカウントが1日1,500件です。同一ドメイン内への送信は Workspace で1日2,000件になります。上限はユーザーごとに設定され、最初のリクエストから24時間後にリセットされます。CC・BCC もそれぞれ1件として数えられる点に注意してください。

MailApp と GmailApp はどちらを使うべきですか?

送信するだけなら MailApp を勧めます。公式リファレンスのとおり MailApp は送信専用で受信トレイにアクセスできず、要求する権限が狭いため再承認も起きにくいからです。下書きを作る createDraft や、送信済みトレイに記録を残したい場合は GmailApp を使います。1日の送信上限はどちらを使っても同じ枠を消費します。

同じ人に何度もメールが届いてしまうのはなぜですか?

主な原因は3つで、トリガーが二重に登録されている、送信条件が翌日も成立している、実行が6分で打ち切られて途中から再実行されている、のいずれかです。対策は送信済みフラグ列を作り、値が入っている行は絶対に送らないようにしたうえで、送信した直後に setValue で日時を書き戻し、SpreadsheetApp.flush() で即座にシートへ反映することです。

時間主導型トリガーは指定した時刻ちょうどに動きますか?

動きません。Apps Script 公式は、午前9時のトリガーを作ると9時から10時の間の時刻が選ばれ、以後は毎日その時刻を保つと説明しています。分単位の正確さが必要な業務には向きません。またトリガーは作成した人のアカウントで実行され、別のアカウントが設定したトリガーは他の人の一覧には表示されません。

作ったスクリプトが「承認が必要です」で止まるときはどうすればよいですか?

トリガーは作成した本人の承認で動くため、コードを足して必要な権限が増えると失敗します。公式のトラブルシューティングどおり、スクリプトエディタで対象の関数を一度手動実行して承認ダイアログを出し、許可し直してください。「このアプリは Google で確認されていません」という警告は、自分や自社ドメイン内で使う範囲であれば詳細を開いて続行できます。

まずはお気軽にご相談ください

AI・IT・デザインに関するお悩みやご相談、お見積りのご依頼など、
どんなことでもお気軽にお問い合わせください。

お問い合わせ