MENU

スプレッドシートの一覧から、決まった書式のメールを自動で送る【整えたコード版】

スプレッドシートの一覧から二重送信せずにメールを送る

前の記事で、スプレッドシートの一覧からGmailの下書きを作るところまでやりました。

→ スプレッドシートの一覧から、決まった書式のメールを自動で作る【まず動かす版】

あのコードには、弱いところが2つありました。

  • 実行を2回押すと、同じ人に同じメールが2通届く
  • 文面を変えたいときは、コードそのものを書き替えるしかない

職場で使うと、どちらも困ります。特に1つ目は、送ってしまうと取り消せません。

この記事では、コードを整えて、この2つを直します。あわせて、1日に送れる通数の上限にも備えます。

コードは今回も、そのまま貼るだけで動きます。


目次

できあがるもの

シートは2枚になります。

1枚目は「宛先一覧」。前の記事のシートに、D列「送信日時」とE列「メモ」を足しただけです。

宛先一覧シート。2行目と3行目のD列に日付が入り、4行目は空欄
送った行にはD列に日時が入ります。4行目はまだ送っていない行です。

送った行には、送った日時が自動で入ります。日時が入っている行は、次に実行しても飛ばします。これで二重送信が起きません。

2枚目は「文面」。件名と本文をここに書きます。

文面シートのB1に件名、B2に複数行の本文を入れたところ
{名前} {品名} のところに、宛先一覧の値が入ります。

{名前} と書いたところにB列の名前が、{品名} と書いたところにC列の品名が入ります。文面を変えたいときは、このシートを書き替えるだけです。コードは触りません。


前の記事から変わるところ

前の記事(まず動かす版)この記事(整えたコード版)
二重送信実行するたびに全員へ送る送信日時が入った行は飛ばす
文面コードの中に書く「文面」シートに書く
下書きと送信の切り替えコードの1行を書き替える実行する関数を選ぶだけ
送信の上限考えていない足りないときは1通も送らずに止める
エラーが出た行そこで全体が止まるその行だけ飛ばして、E列に理由を書く

宛先一覧シートを用意する

前の記事のシートを、そのまま使えます。新しく作る場合は、前の記事の「シートを用意する」と同じ形で作ってください。

まず、下のタブの名前を「宛先一覧」に変えます。タブの「シート1」をダブルクリックすると、名前を書き替えられます。

シートのタブ名を「宛先一覧」に書き替えているところ
タブをダブルクリックすると、名前を書き替えられます。

名前は1文字でも違うと動きません。コードがこの名前でシートを探しているためです。

次に、D1に「送信日時」、E1に「メモ」と見出しを入れます。D列とE列の2行目から下は、空のままにしておきます。

宛先名前品名送信日時メモ
自分のメールアドレス山田 太郎4月号
自分のメールアドレス鈴木 花子4月号

前の記事と同じく、テストの間は宛先に自分のメールアドレスだけを入れてください。


文面シートを用意する

下のタブの左にある「+」を押して、シートを1枚足します。名前は「文面」にします。

「宛先一覧」と「文面」の2つのタブが並んだ画面
「+」でシートを足して、名前を「文面」にしたところ。

A列は自分用のラベルなので、何を書いても動きには関係しません。大事なのはB1とB2です。

AB
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);
}

貼ったら、フロッピーディスクのマークで保存します。

Apps Scriptのエディタにコードを貼り付けて保存した画面
コードを貼って保存し、プロジェクト名を「メール自動送信」にしたところ。

いちばん上の CONFIG が、設定をまとめた場所です。シート名や列の位置を変えたくなったら、ここだけを書き替えます。CONFIG の考え方は、こちらの記事で説明しています。

→ 動いたGASのコードを、後から直しやすい形に整える【CONFIG・関数分割・try-catch】


まず下書きで確かめる

実行ボタンの右に、実行する関数を選ぶ欄があります。ここで createDrafts を選んでから、実行を押します。

関数の選択欄を開くと、createDraftsとsendMailsの2つが並んでいる
選べるのは createDrafts と sendMails の2つだけです。

createDrafts は下書きを作るだけで、送信はしません。送信日時も書き込まないので、何度実行しても大丈夫です。

承認画面が出たら、前の記事と同じ手順で許可してください。自分で書いたスクリプトなので、「このアプリはGoogleで確認されていません」と出るのは正常です。

実行ログに「下書き作成:2件(対象 2件)」と出たら成功です。

実行ログに「下書き作成:2件(対象 2件)」と出ている
下書きが2件できました。

Gmailの下書きを開いて、宛先・名前・品名・改行を確かめてください。


送信する

下書きに問題がなければ、関数の選択欄を sendMails に切り替えて、実行を押します。

関数の選択欄でsendMailsを選んだところ
送るときは sendMails を選びます。

注意:一度送ったメールは取り消せません。 sendMails を押す前に、宛先一覧のA列をもう一度見てください。テスト用の行や、送るつもりのない相手が残っていないか確かめます。

実行ログに「送信:2件(対象 2件)」と出たら、シートに戻ります。D列に送信日時が入っているはずです。

セルには日付だけが表示されますが、時刻も記録されています。D列のセルをクリックすると、上の数式バーに「2026/09/24 22:04:34」のように時刻まで出ます。

送信後、D列に日付が入った宛先一覧シート
送った行のD列に、送信日時が入りました。

もう一度押しても送られないことを確かめる

ここで、わざと 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の画面表示は変わることがあります。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

CAPTCHA


目次