Googleスプレッドシートに並んだ宛先へ、名前や日時を差し込んだメールを1通ずつ送る仕組みを、Google Apps Script(GAS)で作ります。例はサンプル料理教室の「明日のレッスンのリマインド」で、予約一覧から翌日の分だけを拾い、受付の共有アドレス info@ から送り、送ったら行に印を付けます。読み終わると、コードを貼ってトリガーを置き、毎日決まった時刻にリマインドが出る状態まで持っていけます。前提は、Google Workspaceのアカウントがあり、予約一覧のスプレッドシートを編集できることです。GASを触るのが初めてなら、エディタの開き方だけ先に「Google Apps Script(GAS)とは:できること・始め方」で確かめておくと迷いません。
先に決めること
コードより先に、次の4つを決めます。
1つ目は、誰のアカウントで送るかです。GASのメールは、スクリプトを実行したアカウントから出ます。トリガーで動かす場合は、トリガーを作った人のアカウントです。サンプル料理教室では、受付の2人が入れ替わっても止まらないように、代表のアカウントでトリガーを作り、差出人だけを共有アドレスの info@ に見せます。退職や異動があるスタッフのアカウントにトリガーを持たせると、そのアカウントを止めた日から送信も止まります。
2つ目は、差出人をどう見せるかです。個人のアドレスのままか、info@ のような共有アドレスにするかで、使う仕組み(MailAppかGmailAppか)が変わります。返信を受付の2人で見たいなら共有アドレスから送ります。
3つ目は、自動で送り切るか、人が見てから送るかです。毎回同じ文面のリマインドなら自動で構いません。月謝の未納のお知らせのように、1通ずつ事情が違うメールは、下書きまで作って人が確かめてから送る形にします。
4つ目は、1日に何通まで送るかです。GASのメールには1日の宛先数の上限があり、リマインドの数十通なら気にせず済みますが、会員全員へのお知らせを同じ仕組みで送ると当たります。
| 決めること | サンプル料理教室の答え | 決めないと起きること |
|---|---|---|
| 送るアカウント | 代表のアカウントでトリガーを作る | 作った人が退職した日から止まる |
| 差出人の見え方 | info@sample-cooking.example(Googleグループ) | 返信が個人の受信トレイに散る |
| 自動か下書きか | リマインドは自動、未納の連絡は下書き | 誤った文面がそのまま出る |
| 1日の送信数 | 多い日で40通ほど | 上限に当たって途中で止まる |
MailAppとGmailAppの違い
GASでメールを送る仕組みは2つあります。MailAppは「送るだけ」の仕組みで、GmailAppはGmailの受信トレイ・下書き・ラベルまで扱える仕組みです。どちらも sendEmail で送れて、本文の差し込み・HTML・添付は両方でできます。違いは、差出人を別アドレスにできるか、下書きを作れるか、求められる権限の広さです。
| 項目 | MailApp | GmailApp |
|---|---|---|
| できること | メールを送るだけ | 送る・下書きを作る・検索する・ラベルを付ける |
| 求める権限(スコープ) | https://www.googleapis.com/auth/script.send_mail | https://mail.google.com/ |
| 差出人を別アドレスにする | できない(表示名と返信先だけ変えられる) | from で、Gmailに追加済みのエイリアスを指定できる |
| 下書きを作る | できない | createDraft で作れる |
表示名 name | 指定できる | 指定できる |
返信先 replyTo | 指定できる | 指定できる |
返信不可のアドレスから送る noReply | Google Workspaceのアカウントだけ使える | Google Workspaceのアカウントだけ使える |
添付 attachments・HTML htmlBody・本文中の画像 inlineImages | 使える | 使える |
| 今日あと何人に送れるか | MailApp.getRemainingDailyQuota() で分かる | 専用のメソッドは無い(MailApp側で確かめる) |
| 向く場面 | 差出人は自分のままでよい通知 | 共有アドレスから送る・下書きで確認する |
見落としやすいのは権限の広さです。GmailAppはメールの閲覧や削除まで含む広い権限を求めるので、送るだけならMailAppのほうが安心です。サンプル料理教室は差出人を info@ にしたいのでGmailAppを使い、エディタを触れる人を代表と受付のリーダーに絞っています。
送る前の準備
予約一覧の列をそろえる
リマインドで使う予約一覧は、1行1予約で、見出しを1行目に置きます。差し込みに使う列と、送信の記録に使う列を分けておくと、あとでGASを直すときに迷いません。
| 列 | 見出し | 入る値の例 | 使い道 |
|---|---|---|---|
| A | 予約ID | R-20261105-03 | 行の特定 |
| B | レッスン日 | 2026/11/05 | 翌日の分を拾う |
| C | 開始時刻 | 18:30 | 本文に差し込む |
| D | レッスン名 | 季節の和食コース | 本文に差し込む |
| E | 氏名 | 山田 花子 | 本文に差し込む |
| F | メール | hanako@example.com | 宛先 |
| G | 対応状況 | 確定/キャンセル | キャンセルは送らない |
| H | リマインド送信 | 2026/11/04 18:02 | 送ったら日時を入れる |
H列が空の行だけに送り、送ったらすぐ日時を入れます。これが二重送信を防ぐ要です。対応状況はプルダウンにして、書き方の揺れを防ぎます。

文面を「文面」シートに置く
件名と本文はコードに書かず、同じスプレッドシートの「文面」シートに置きます。受付がGASのエディタを開かなくても、言い回しや持ち物の案内を直せるようにするためです。A1に件名、A2に本文を書き、差し込む場所は {氏名} のように波かっこで囲みます。
A1: 【サンプル料理教室】明日のレッスンのご案内({レッスン日})
A2: {氏名} 様
サンプル料理教室です。明日のレッスンのご案内です。
日時:{レッスン日} {開始時刻}〜
レッスン:{レッスン名}
持ち物:エプロン、手拭き用のタオル、筆記用具
遅れる場合やお休みの場合は、このメールに返信してご連絡ください。
サンプル料理教室 受付
info@sample-cooking.example

共有アドレスを差出人に追加する
info@ から送るには、代表のGmailに info@ を「送信元のアドレス」として追加します。info@ がGoogleグループのアドレスでも追加できます(グループの作り方は「Gmailを複数人で回す:Googleグループ・共有アドレス・振り分け・署名」)。
- 代表のアカウントでパソコンのGmailを開く
- 右上の歯車アイコンを押し、「すべての設定を表示」を押す
- 「アカウント」タブを開く
- 「名前」の欄の「他のメール アドレスを追加」を押す
- 名前に「サンプル料理教室」、メールアドレスに
info@sample-cooking.exampleを入れ、「次のステップ」を押す - 「確認メールの送信」を押す
- info@ に届いた確認メールを受付が開き、中のリンクを押す
グループのアドレスを追加するときは、グループの「投稿権限」の「投稿」を「ウェブ上の全員」にしておきます。選べない場合は、管理者がグループのオーナーに組織外からのメールの受信を許可する設定を入れます。確認メールはグループに届くので、追加する人がグループのメールを読める状態(グループへの委任アクセスがある状態)にしておきます。追加が済むと、GASの GmailApp.getAliases() の結果に info@ が出てきます。この一覧に出ないアドレスは from に指定できません。

差し込みで送る手順
スクリプトを作る
- 予約一覧のスプレッドシートを開く
- メニューの「拡張機能」を押し、「Apps Script」を押す
- 左上のプロジェクト名を「リマインド送信」に変える
- エディタの
コード.gsの中身を消し、下のコードを貼る - 保存アイコンを押す
// 予約一覧から「明日」のレッスンを拾い、1通ずつリマインドを送る
const SHEET_NAME = '予約一覧';
const TEMPLATE_SHEET = '文面';
const FROM_ALIAS = 'info@sample-cooking.example';
const SENDER_NAME = 'サンプル料理教室';
const HEADER_ROWS = 1;
// 列番号(A列=0)
const C = { id: 0, date: 1, start: 2, lesson: 3, name: 4, email: 5, status: 6, sent: 7 };
function sendReminders() {
const lock = LockService.getScriptLock();
if (!lock.tryLock(30000)) return; // 前の実行が終わっていなければ何もしない
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_NAME);
const tpl = ss.getSheetByName(TEMPLATE_SHEET);
const subjectTpl = tpl.getRange('A1').getValue();
const bodyTpl = tpl.getRange('A2').getValue();
const range = sheet.getDataRange();
const values = range.getValues();
const shown = range.getDisplayValues(); // 時刻は画面の表示どおりに使う
const tz = ss.getSpreadsheetTimeZone();
const tomorrow = Utilities.formatDate(new Date(Date.now() + 24 * 60 * 60 * 1000), tz, 'yyyy/MM/dd');
const aliases = GmailApp.getAliases();
if (aliases.indexOf(FROM_ALIAS) === -1) {
throw new Error('送信元のアドレスに ' + FROM_ALIAS + ' が追加されていません');
}
let sentCount = 0;
for (let i = HEADER_ROWS; i < values.length; i++) {
const row = values[i];
if (row[C.sent] !== '') continue; // 送信済み
if (row[C.status] !== '確定') continue; // キャンセル・未確定は送らない
if (!(row[C.date] instanceof Date)) continue;
const lessonDate = Utilities.formatDate(row[C.date], tz, 'yyyy/MM/dd');
if (lessonDate !== tomorrow) continue;
const to = String(row[C.email]).trim();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(to)) {
sheet.getRange(i + 1, C.sent + 1).setValue('宛先エラー');
continue;
}
const data = {
'氏名': row[C.name],
'レッスン日': lessonDate,
'開始時刻': shown[i][C.start],
'レッスン名': row[C.lesson],
};
GmailApp.sendEmail(to, fill(subjectTpl, data), fill(bodyTpl, data), {
from: FROM_ALIAS,
name: SENDER_NAME,
replyTo: FROM_ALIAS,
});
// 送ったらすぐ印を付ける(途中で止まっても二重に送らない)
sheet.getRange(i + 1, C.sent + 1).setValue(new Date());
SpreadsheetApp.flush();
sentCount++;
}
console.log('送信 ' + sentCount + ' 件');
} finally {
lock.releaseLock();
}
}
// {キー} を値に置き換える
function fill(text, data) {
return String(text).replace(/\{([^}]+)\}/g, function (m, key) {
return (key in data) ? String(data[key]) : m;
});
}
ポイントは3つです。1つ目は、送るたびにH列へ日時を書き、SpreadsheetApp.flush() で確定させていることです。全員に送ってから最後にまとめて書く形にすると、途中でエラーや時間切れで止まったときに、どこまで送ったかが残らず、次の実行で同じ人に2通目が飛びます。書き込みを1行ずつにすると少し遅くなりますが、リマインドの件数なら問題になりません。
2つ目は、LockService で同時に2本走らないようにしていることです。トリガーの重複や手動実行と重なっても、後から来たほうは何もせずに終わります。
3つ目は、開始時刻を getDisplayValues() で読んでいることです。スプレッドシートで時刻だけを入れたセルは、GASで getValues() すると1899年12月30日の日付付きの値として返り、スプレッドシートとスクリプトのタイムゾーンが違うと表示とずれます。画面に出ている「18:30」をそのまま使えば、この問題を避けられます。

初回の承認とテスト
- エディタ上部の関数の選択欄で
sendRemindersを選ぶ - 予約一覧に、明日の日付・対応状況「確定」・メールに自分のアドレスを入れたテスト行を1行作る
- 「実行」を押す
- 「承認が必要です」が出たら「権限を確認」を押し、代表のアカウントを選ぶ
- 求められた権限を確かめ、許可する
- 下の「実行ログ」に「送信 1 件」と出るのを見る
- 自分の受信トレイで、差出人が「サンプル料理教室 <info@sample-cooking.example>」になっているかを見る
- テスト行のH列に日時が入ったのを見て、テスト行を消す
承認の画面では権限を1つずつ選べることがあり、Gmailの権限を外すと送る行でエラーになります。すべて許可します。この手動の1回を通さないとトリガーからは動きません。

毎日決まった時刻に動かす
- エディタ左の「トリガー」(時計のアイコン)を押す
- 右下の「トリガーを追加」を押す
- 実行する関数に
sendRemindersを選ぶ - イベントのソースに「時間主導型」を選ぶ
- 時間ベースのトリガーのタイプに「日付ベースのタイマー」を選ぶ
- 時刻に「午後6時〜7時」を選ぶ
- 「保存」を押す
時間主導型のトリガーは、指定した1時間の枠の中のどこかで動きます。18時ちょうどには動きません。「前日の夕方に届けばよい」という使い方なら、この幅は問題になりません。トリガーの種類と、時刻の幅の詳しい話は「GASのトリガー:定期実行・フォーム送信時・編集時」にまとめています。

応用
下書きを作って、人が確かめてから送る
月謝の未納の連絡や、レッスンの振替の案内のように、送る前に人が目を通したいメールは、送らずに下書きを作ります。GmailAppの createDraft は sendEmail と同じ引数を取るので、送る行を差し替えるだけです。
// 送らずに下書きを作る。H列には「下書き」と入れる
function createDrafts() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName('未納連絡');
const values = sheet.getDataRange().getValues();
for (let i = 1; i < values.length; i++) {
const [memberId, name, email, month, amount, done] = values[i];
if (done !== '') continue;
const subject = '【サンプル料理教室】' + month + '分の月謝のご確認';
const body = name + ' 様\n\n' + month + '分の月謝 ' + amount + '円 のご入金が、まだこちらで確認できていません。\n'
+ '行き違いでしたら申し訳ありません。ご確認をお願いします。\n\nサンプル料理教室 受付';
GmailApp.createDraft(email, subject, body, {
from: 'info@sample-cooking.example',
name: 'サンプル料理教室',
});
sheet.getRange(i + 1, 6).setValue('下書き');
}
}
下書きは、スクリプトを動かしたアカウントのGmailの「下書き」に並びます。受付はそこを開いて1通ずつ読み、事情のある人だけ文面を直して「送信」を押します。送った人の分はシートのF列を「送信済み」に直す運用にします。下書きを作るアカウントと、確かめて送る人が別の場合は、そのアカウントのGmailを受付が開ける必要があるため、Gmailの委任(「アカウント」タブの「アカウントへのアクセスを許可」→「別のアカウントを追加」。相手は同じ組織のユーザーだけで、管理者が委任を有効にしている必要がある)を使うか、受付本人のアカウントで実行します。
もう1つの形は、送る予定の宛先・件名・本文を「送信予定」シートに書き出し、受付が「OK」の列に印を付けた行だけを送る関数を別に作るやり方です。何十通もあるときは、下書きを開くより表で見たほうが早く済みます。

HTMLメールにする
リンクや地図、太字の見出しを入れたいときは htmlBody を使います。HTMLが表示できないメールソフトのために、テキストの本文も必ず残します。差し込む値は、そのままHTMLに入れると < や & で表示が崩れるので、置き換えてから入れます。
function escapeHtml(s) {
return String(s).replace(/&/g, '&').replace(/</g, '<')
.replace(/>/g, '>').replace(/"/g, '"');
}
function sendHtmlSample() {
const name = escapeHtml('山田 花子');
const logo = DriveApp.getFileById('YOUR_LOGO_FILE_ID').getBlob();
const html = '<p>' + name + ' 様</p>'
+ '<p>明日 <b>18:30</b> からのレッスンのご案内です。</p>'
+ '<p><img src="cid:logo" width="160"></p>';
GmailApp.sendEmail('hanako@example.com', '明日のレッスンのご案内', 'HTMLが表示できない場合の本文です。', {
from: 'info@sample-cooking.example',
name: 'サンプル料理教室',
htmlBody: html,
inlineImages: { logo: logo },
});
}
本文中の画像は inlineImages に渡し、HTMLでは cid:キー名 で参照します。凝ったデザインは相手のメールソフトで崩れやすいので、段落と少しの強調にとどめます。
添付ファイルを付ける
初回の体験レッスンの人にだけ「持ち物と教室までの道順」のPDFを付ける、といった使い方です。ドライブのファイルを getBlob() で取り出して attachments に渡します。Googleドキュメントをそのまま付けると相手が開けないことがあるので、getAs('application/pdf') でPDFにしてから付けます。
const guide = DriveApp.getFileById('YOUR_GUIDE_DOC_ID').getAs('application/pdf').setName('教室までの道順.pdf');
GmailApp.sendEmail(to, subject, body, {
from: 'info@sample-cooking.example',
name: 'サンプル料理教室',
attachments: [guide],
});
添付は1通につき250個まで、合計25MBまでです(2026年10月時点)。写真が多いなら、共有ドライブのフォルダのリンクを本文に書くほうが上限に当たりません。
返信を受け付けない通知にする
Google Workspaceのアカウントなら、noReply: true で返信不可の汎用アドレスから送れます。付けると replyTo は無視されます。会員連絡は返信で欠席を受けるほうが便利なので、サンプル料理教室では使っていません。
1日に送れる数と、大量に送るとき
GASのメールには、Apps Scriptの割り当てとしての上限があります。Gmailそのものの送信上限とは別の数字で、GASから送るときはGASの側の数字が先に効きます。
| 項目 | 無料のGoogleアカウント | Google Workspace |
|---|---|---|
| 1日のメールの宛先数(GASから) | 100人 | 1,500人 |
| 1日のメールの宛先数(ドメイン内) | 100人 | 2,000人 |
| 1通あたりの宛先数 | 50人 | 50人 |
| 1通の本文の大きさ | 200KB | 400KB |
| 1通の添付の数 | 250個 | 250個 |
| 1通の添付の合計 | 25MB | 25MB |
| 1回の実行時間 | 6分 | 6分 |
2026年10月時点のApps Scriptの割り当てのページの値です。数えるのは「通数」ではなく宛先の数です。上限は利用者ごとで、最初に送ってから24時間で戻ります。午前0時に戻るわけではありません。
試用期間中のGoogle Workspaceは、これより低い上限がかかります。有料に切り替えたあとも、支払いの累計が一定額に達してしばらく経つまで上限が上がりません。導入したばかりの月に会員全員へ一斉に送ろうとして止まるのは、たいていこれです。
今日あと何人に送れるかは、次の1行で確かめられます。大事な送信の前に数を見て、足りなければ翌日に回す処理を入れておくと、途中で止まって半端に送られる事態を避けられます。
function checkQuota() {
console.log('今日あと ' + MailApp.getRemainingDailyQuota() + ' 人に送れます');
}
Gmailそのものにも上限があります。Google Workspaceのユーザーは1日2,000通まで、宛先の合計は1日10,000人まで、外部の宛先は1日3,000人までです(2026年10月時点、管理者ヘルプ「Gmail の送信制限」)。管理者ヘルプは、Apps Scriptを使う場合は割り当てが異なることがあるとしているので、GASの分は上の表の数字で見ます。上限を超えると、そのアカウントは最長で24時間メールを送れなくなります。受付の業務がその日止まるので、上限ぎりぎりでは運用しません。
大量に送るときの代わり
会員全員へのお知らせや、季節のレッスンの案内のような一斉送信は、GASで1通ずつ送るより向く手があります。
| やりたいこと | 向く手段 | 理由 |
|---|---|---|
| 会員100〜数百人に同じお知らせ | Gmailの「メールへの差し込み」 | 画面で差し込みができ、配信停止のリンクが自動で付く |
| 毎週のお知らせを会員に届ける | LINE公式アカウント | 会員がメールより開きやすい。配信の数え方もメールと別 |
| 数千通の宣伝メール | 外部のメール配信サービス | 配信停止の管理・到達率の管理が前提の仕組み |
| 予約や入金に合わせた1通ずつの連絡 | この記事のGAS | 宛先ごとに中身が違い、件数が少ない |
Gmailの「メールへの差し込み」は、Business Standard・Business Plusなど一部のエディションで使える機能で、1日1,500人まで送れます(2026年10月時点)。スプレッドシートを読み込んで @名 のようなタグで差し込めて、配信停止のリンクも自動で付きます。Business Starterでは使えないので、プランを確かめてから選びます。
宣伝のメールは、受け取る人の同意や配信停止の扱いについて特定電子メール法の決まりが絡みます。この記事の仕組みは、予約・出席・請求のように取引に伴う連絡に限って使います。
できないことと、その代わり
| できないこと | 理由 | 代わりの手 |
|---|---|---|
| MailAppで差出人を info@ にする | MailAppには from が無い | GmailAppにして、Gmailに送信元のアドレスを追加する |
| Gmailに追加していないアドレスから送る | from は getAliases() に出るものだけ | 「他のメール アドレスを追加」で先に追加する |
| 決まった分単位の時刻ちょうどに送る | 時間主導型のトリガーは1時間の枠で動く | 時刻が大事なら「特定の日時」のトリガーを毎回作るか、Gmailの「送信日時を設定」で手で予約する |
| 開封されたかを知る | GASのメールに開封の記録は無い | 返信で欠席を受けるなど、反応を返してもらう形にする |
| 1日数千通を確実に届ける | 宛先数の上限と到達率の問題 | メール配信サービスやLINE公式アカウントへ分ける |
| 送ったメールを取り消す | 送信後に戻す仕組みは無い | 確かめたいメールは下書きにしてから送る |
よく詰まるところ
| 症状 | 原因 | 直し方 |
|---|---|---|
| トリガーで動かすと「その操作を実行するには承認が必要です」 | エディタで一度も承認していない、または権限を一部外して許可した | エディタで関数を手動で1回実行し、求められた権限をすべて許可する |
| 「無効なメールアドレス」で止まる | 宛先の前後の空白、全角の「@」、空のセル | trim() してから形を確かめ、おかしい行はH列に「宛先エラー」と書いて飛ばす |
| 同じ人に2通届いた | トリガーが2つある、途中で止まって印が付かなかった | 「トリガー」画面で重複を消す。送るたびに印を書き、LockService を入れる |
from を付けたのに個人のアドレスから出た、またはエラー | 送信元のアドレスに追加していない、確認のリンクを押していない | Gmailの「アカウント」タブで追加と確認を済ませ、getAliases() に出るかを見る |
| 本文の時刻が「18:30」ではなく長い日付の文字列になる | 時刻のセルを getValues() で読み、1899年の日付として扱われた | getDisplayValues() で表示どおりに読む |
| 翌日ではなく当日の分に送られた | スクリプトとスプレッドシートのタイムゾーンが違う | 「プロジェクトの設定」とスプレッドシートの「設定」のタイムゾーンを両方「(GMT+09:00) 東京」にそろえる |
| 「サービスが呼び出された回数が多すぎます」で止まる | 1日の宛先数の上限に達した | MailApp.getRemainingDailyQuota() で残りを見て、翌日に回す。一斉送信は別の手段に分ける |
| 相手の迷惑メールに入る | 独自ドメインの送信ドメイン認証(SPF・DKIM)が不十分、本文がリンクばかり | 管理コンソールでDKIMを有効にし、DNSのSPFを確かめる。本文は短く、リンクは少なく |
| 返信が代表の個人の受信トレイに届く | from も replyTo も付けずに送った | from と replyTo に info@ を入れる |
| 差出人の名前が代表の個人名になる | name を付けていない | name: 'サンプル料理教室' を付ける |
| 受付が文面を直したのに、古い文面で届く | 文面をコードに直書きしている | 文面を「文面」シートに移し、コードは読み込むだけにする |


次にやること
- GASのトリガー:定期実行・フォーム送信時・編集時
- Googleフォームの自動返信メール:標準機能とGASの使い分け
- スプレッドシートで請求書を作り、PDFで送る:GASで月末をまとめる
- GoogleフォームとLINEをつなぐ:回答をLINEで知らせる・LINEから受け付ける
請求書の記事は、ここの差し込みと添付を組み合わせて月謝の請求書をPDFで送る形です。メールよりLINEで届けたい教室はLINEの記事から読みます。