見出し画像

【GASでGmailを自動化する】受信メール振り分け・定型返信・添付保存まで

この記事で得られること

Gmailを毎日使っていると「これ自動化できないかな」と思う作業が出てきませんか。

  • 特定の条件のメールだけスプレッドシートに記録したい

  • 問い合わせメールに定型文で自動返信したい

  • 添付ファイルをGoogleドライブに自動保存したい

  • 毎朝、昨日届いた重要メールのサマリーを送ってほしい

Gmailのフィルタ機能だけでは物足りない。
でも専用ツールを導入するほどじゃない。

そんな隙間を埋めるのがGASです。
GmailAppクラスを使えば検索・読み取り・送信・ラベル操作まで全部GASから制御できます。

この記事では以下の内容をカバーします。

  • GmailAppの基本操作(検索・読み取り・送信)

  • 受信メールをスプレッドシートに記録する実践パターン

  • 自動返信の仕組みと注意点

  • 添付ファイルのGoogleドライブ自動保存

  • 毎朝のメールサマリー配信



前提知識

この記事は以下の知識がある前提で書いています。

  • GASでスプレッドシートの読み書きができる

  • トリガーで定期実行を設定できる


GmailAppの基本

メールを検索する

GASからGmailを操作する起点はGmailApp.search()です。
Gmailの検索演算子がそのまま使えます。

/**
 * Gmailの検索基本パターン
 */
function searchEmails() {
  const query = 'from:example@gmail.com subject:問い合わせ newer_than:7d';
  const threads = GmailApp.search(query, 0, 50); // 0件目から最大50スレッド

  console.log(`${threads.length}件のスレッドが見つかりました`);

  threads.forEach(thread => {
    console.log(`件名: ${thread.getFirstMessageSubject()}`);
    console.log(`最終更新: ${thread.getLastMessageDate()}`);
    console.log(`メッセージ数: ${thread.getMessageCount()}`);
  });
}

重要な概念としてGmailはスレッド単位でメールを管理しています。
search()が返すのはGmailThreadの配列で1スレッドには複数のメッセージが含まれる可能性があります(返信のやり取り等)。

よく使う検索演算子を挙げておきます。

  • from:アドレス— 送信者で絞る

  • to:アドレス — 宛先で絞る

  • subject:キーワード — 件名で絞る

  • has:attachment — 添付ファイルあり

  • is:unread — 未読

  • label:ラベル名 — ラベルで絞る

  • newer_than:7d — 7日以内

  • after:2026/03/01 — 指定日以降

メッセージの内容を取得する

スレッドから個別のメッセージを取り出して送信者・本文・添付などを取得します。

function readMessages() {
  const threads = GmailApp.search('is:unread', 0, 10);

  threads.forEach(thread => {
    const messages = thread.getMessages();

    messages.forEach(message => {
      const from = message.getFrom();           // 送信者
      const to = message.getTo();               // 宛先
      const subject = message.getSubject();     // 件名
      const body = message.getPlainBody();      // プレーンテキスト本文
      const htmlBody = message.getBody();       // HTML本文
      const date = message.getDate();           // 送信日時
      const attachments = message.getAttachments(); // 添付ファイル配列

      console.log(`${date.toISOString()} | ${from} | ${subject}`);
      console.log(`本文(先頭100文字): ${body.substring(0, 100)}`);
      console.log(`添付: ${attachments.length}件`);
    });
  });
}

getPlainBody()とgetBody()の違いを覚えておくと便利です。
HTMLタグが邪魔な場合はgetPlainBody()、リンクや書式情報が必要な場合はgetBody()を使います。

メールを送信する

function sendSimpleEmail() {
  GmailApp.sendEmail(
    'recipient@example.com',
    'テスト件名',
    'テスト本文です。\n\nこれはGASからの自動送信です。'
  );
}

/**
 * オプション付きの送信(HTML本文、CC等)
 */
function sendAdvancedEmail() {
  const htmlBody = `
    <h2>自動通知</h2>
    <p>以下の内容をお知らせします。</p>
    <ul>
      <li>項目1</li>
      <li>項目2</li>
    </ul>
  `;

  GmailApp.sendEmail(
    'recipient@example.com',
    '重要なお知らせ',
    '本文のプレーンテキスト版(HTMLが表示できない環境向け)',
    {
      htmlBody: htmlBody,
      cc: 'cc@example.com',
      name: '自動通知システム',
      replyTo: 'noreply@example.com'
    }
  );
}

送信数の制限に注意

Gmailには1日あたりの送信制限があります。
無料のGoogleアカウントなら1日100受信者まで

Google Workspaceなら1日1,500受信者までです。
1通にCC/BCCを含む場合は受信者数分が消費されます。

この上限を超えるとGASからの送信がエラーになります。
残り送信可能数は以下で確認できます。

function checkRemainingQuota() {
  const remaining = MailApp.getRemainingDailyQuota();
  console.log(`本日の残り送信可能数: ${remaining}受信者`);
}

定期実行するスクリプトでは送信前にこの値を確認しておくと安全です。


実践①:問い合わせメールをスプレッドシートに記録する

問い合わせフォームからのメールを自動でスプレッドシートに転記し、対応漏れを防ぐパターンです。

シートイメージ

コード

/**
 * 問い合わせメールをスプレッドシートに記録する
 */
function recordInquiries() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('問い合わせ');

  // ヘッダーがなければ作成
  if (sheet.getRange('A1').getValue() === '') {
    const headers = ['受信日時', '送信者', '件名', '本文抜粋', 'ステータス', '対応者', 'メッセージID'];
    sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
    SpreadsheetApp.flush(); // getLastRow() が正しい値を返すよう確定させる
  }

  // 既に記録済みのメッセージIDを取得(重複登録防止)
  const lastRow = sheet.getLastRow();
  const recordedIds = new Set();
  if (lastRow >= 2) {
    const ids = sheet.getRange(2, 7, lastRow - 1, 1).getValues().flat();
    ids.forEach(id => recordedIds.add(id));
  }

  // 検索: ラベル「問い合わせ」が付いているメール
  const threads = GmailApp.search('label:問い合わせ newer_than:30d', 0, 100);

  const newRecords = [];

  threads.forEach(thread => {
    thread.getMessages().forEach(message => {
      const messageId = message.getId();

      // 既に記録済みならスキップ
      if (recordedIds.has(messageId)) return;

      const record = [
        message.getDate(),
        message.getFrom(),
        message.getSubject(),
        message.getPlainBody().substring(0, 200).replace(/\n+/g, ' '),
        '未対応',
        '',
        messageId
      ];
      newRecords.push(record);
    });
  });

  if (newRecords.length > 0) {
    sheet.getRange(lastRow + 1, 1, newRecords.length, newRecords[0].length).setValues(newRecords);
    console.log(`${newRecords.length}件の新規問い合わせを記録しました`);
  } else {
    console.log('新規の問い合わせはありません');
  }
}

設計のポイント

メッセージIDで重複を防ぐ
Gmailの各メッセージにはgetId()で取得できる一意のIDがあります。

これをG列に保存しておき次回実行時に突き合わせることで同じメールを二重登録しません。
Setを使った判定は配列検索より圧倒的に速いので大量データでも問題なく動きます。

ラベルで対象を絞る
Gmailのフィルタ機能で「問い合わせフォームからのメール」にラベルを自動付与しておけばGAS側はlabel:問い合わせで検索するだけです。

GmailのフィルタとGASの役割分担を意識すると、コードがシンプルになります。

本文抜粋の整形
substring(0, 200)で先頭200文字に制限しreplace(/\n+/g, ' ')で改行を空白に変換しています。

スプレッドシートに長文が入るとセル表示が崩れるので、この前処理は入れておくべきです。


実践②:自動返信の実装

特定の条件のメールに対して、自動で定型返信を送るパターンです。

/**
 * 問い合わせメールに自動返信する
 */
function autoReplyToInquiries() {
  // 未返信の問い合わせを取得
  const threads = GmailApp.search('label:問い合わせ -label:自動返信済み newer_than:1d', 0, 20);

  const replyTemplate = `お問い合わせありがとうございます。

内容を確認し、2営業日以内にご返信いたします。
しばらくお待ちくださいませ。

---
このメールは自動返信です。`;

  const processedLabel = GmailApp.getUserLabelByName('自動返信済み')
    || GmailApp.createLabel('自動返信済み');

  let replyCount = 0;

  threads.forEach(thread => {
    try {
      // スレッドに返信を追加
      thread.reply(replyTemplate);

      // 処理済みラベルを付ける
      thread.addLabel(processedLabel);

      replyCount++;
      console.log(`自動返信送信: ${thread.getFirstMessageSubject()}`);

      // 送信間隔を空ける
      Utilities.sleep(2000);
    } catch (e) {
      console.error(`自動返信失敗: ${e.message}`);
    }
  });

  console.log(`自動返信完了: ${replyCount}件`);
}

自動返信の注意点

自動返信は強力ですが、事故が起きやすい機能でもあります。以下を必ず守ってください。

返信ループを防ぐ
自動返信に対してさらに自動返信が返ってきて無限ループになるケースがあります。

対策として、処理済みラベルを必ず付けることで同じスレッドに二度返信しないようにしています。
-label:自動返信済みで既に処理済みのスレッドを検索から除外する構造です。

本人確認のない自動返信は信頼を損なう
問い合わせ元が本物の顧客かどうかを判定せずに自動返信するとスパムメールにも返信してしまい、スパム送信者にメールアドレスが生きていることを教える結果になります。

Gmailのフィルタである程度フィルタリングしてからGASに渡すのが安全です。

自動返信であることを明記する
本文に「このメールは自動返信です」と書くのは最低限のマナーです。

送信数制限を意識する
前述の通り1日100受信者(無料アカウント)の制限があります。

大量の問い合わせが来るシステムでは、GASでの自動返信には向きません。


実践③:添付ファイルの自動保存

特定のメールに添付されたファイルをGoogleドライブの指定フォルダに自動保存するパターンです。
請求書・領収書の管理などに便利です。

/**
 * 請求書メールの添付PDFをGoogleドライブに保存する
 */
function saveInvoiceAttachments() {
  // 保存先フォルダのIDをスクリプトプロパティから取得
  const folderId = PropertiesService.getScriptProperties().getProperty('INVOICE_FOLDER_ID');
  const folder = DriveApp.getFolderById(folderId);

  // 処理済みラベル
  const processedLabel = GmailApp.getUserLabelByName('保存済み')
    || GmailApp.createLabel('保存済み');

  // 請求書メールを検索
  const threads = GmailApp.search(
    'subject:(請求書 OR インボイス) has:attachment -label:保存済み newer_than:30d',
    0,
    50
  );

  let savedCount = 0;

  threads.forEach(thread => {
    let hasError = false;
    let savedInThread = false;

    thread.getMessages().forEach(message => {
      const attachments = message.getAttachments();

      attachments.forEach(attachment => {
        // PDFのみを対象にする
        if (attachment.getContentType() !== 'application/pdf') return;

        try {
          // ファイル名に送信日を付けて保存
          const dateStr = Utilities.formatDate(message.getDate(), 'Asia/Tokyo', 'yyyyMMdd');
          const originalName = attachment.getName();
          const newName = `${dateStr}_${originalName}`;

          folder.createFile(attachment.copyBlob().setName(newName));
          savedCount++;
          savedInThread = true;
          console.log(`保存: ${newName}`);
        } catch (e) {
          console.error(`保存失敗 [${attachment.getName()}]: ${e.message}`);
          hasError = true;
        }
      });
    });

    // PDFを1件以上保存し、かつ失敗がなかったスレッドのみ処理済みにする
    if (savedInThread && !hasError) {
      thread.addLabel(processedLabel);
    } else if (!savedInThread) {
      console.log(`対象PDFなし、スキップ: ${thread.getFirstMessageSubject()}`);
    } else {
      console.warn(`保存失敗があったためラベルを付けません: ${thread.getFirstMessageSubject()}`);
    }
  });

  console.log(`${savedCount}件のファイルを保存しました`);
}

設計のポイント

フォルダIDはスクリプトプロパティで管理
Googleドライブのフォルダを開いたときのURL末尾がフォルダIDです(https://drive.google.com/drive/folders/XXXXXXXX のXXXXXXXX部分)。

コードにベタ書きせずスクリプトプロパティに保存しておくのが安全です。

ContentTypeでフィルタする
attachment.getContentType()でMIMEタイプがわかります。

PDFだけ欲しい場合はapplication/pdf、画像ならimage/jpegやimage/pngで絞れます。
これがないと署名画像など不要なファイルまで保存されます。

ファイル名に日付プレフィックス
受信日を先頭に付けておくとドライブでソートしたときに時系列で並びます。

yyyyMMdd形式にしておくと文字列ソートで正しい順序になります。


実践④:毎朝のメールサマリー配信

昨日届いた重要メールを1通にまとめて毎朝自分に送る仕組みです。
情報の見落とし防止に効果的です。

/**
 * 昨日のメールサマリーを自分宛に送信する
 */
function sendDailySummary() {
  // 昨日0:00〜今日0:00の範囲
  const now = new Date();
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);

  const dateStr = Utilities.formatDate(yesterday, 'Asia/Tokyo', 'yyyy/MM/dd');
  const afterStr = Utilities.formatDate(yesterday, 'Asia/Tokyo', 'yyyy/MM/dd');
  const beforeStr = Utilities.formatDate(today, 'Asia/Tokyo', 'yyyy/MM/dd');

  // 重要メール(スター付き or 特定ラベル)を検索
  const query = `(is:starred OR label:重要) after:${afterStr} before:${beforeStr}`;
  const threads = GmailApp.search(query, 0, 50);

  if (threads.length === 0) {
    console.log('昨日の重要メールはありませんでした');
    return;
  }

  // メール一覧をHTML形式で組み立てる
  let htmlBody = `<h2>${dateStr} のメールサマリー</h2>`;
  htmlBody += `<p>昨日届いた重要メール: <strong>${threads.length}件</strong></p>`;
  htmlBody += '<hr>';

  threads.forEach(thread => {
    const messages = thread.getMessages();
    // 昨日の日付範囲内で最も新しいメッセージを使う(その後の返信で表示がずれるのを防ぐ)
    const targetMessage = messages
      .filter(m => m.getDate() >= yesterday && m.getDate() < today)
      .pop() || messages[messages.length - 1];
    const from = escapeHtml(targetMessage.getFrom());
    const subject = escapeHtml(thread.getFirstMessageSubject());
    const snippet = escapeHtml(targetMessage.getPlainBody().substring(0, 150).replace(/\n+/g, ' '));
    const permalink = thread.getPermalink();

    htmlBody += `
      <div style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #4285f4;">
        <div style="color: #5f6368; font-size: 12px;">${from}</div>
        <div style="font-weight: bold; margin: 5px 0;">
          <a href="${permalink}" style="color: #1a73e8; text-decoration: none;">${subject}</a>
        </div>
        <div style="color: #3c4043; font-size: 13px;">${snippet}...</div>
      </div>
    `;
  });

  // 自分宛に送信(時間主導トリガーではgetEffectiveUser()を使う)
  const myEmail = Session.getEffectiveUser().getEmail();
  GmailApp.sendEmail(
    myEmail,
    `【メールサマリー】${dateStr}(${threads.length}件)`,
    'このメールはHTML形式で表示してください',
    { htmlBody: htmlBody }
  );

  console.log(`サマリーを送信しました: ${threads.length}件`);
}

/**
 * HTMLエスケープ
 */
function escapeHtml(str) {
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

設計のポイント

スター付き or 重要ラベルで絞る
Gmailは大量のメールを受信するので全件サマリーすると逆にノイズになります。

あらかじめGmailの機能で「重要」を定義しておき(スター付ける、重要ラベル貼る等)、それだけをサマリー対象にするのが実用的です。

日付範囲内のメッセージを使う
GmailApp.search()はスレッドを返すため検索後に返信が届いていると最新メッセージが昨日とは別の日のものになります。

取得したメッセージを昨日の日付でフィルタしその中で最も新しいものを表示に使うことでサマリーの内容が意図通りになります。

getEffectiveUser()で自分のメールアドレスを取得する
時間主導トリガーで実行されるスクリプトでは、Session.getActiveUser().getEmail()がセキュリティポリシー次第で空文字を返す場合があります。

トリガー実行時も安定してスクリプトオーナーのアドレスを返す Session.getEffectiveUser().getEmail()を使うのが適切です。

getPermalink()でメールへの直接リンクを生成
HTML本文に仕込めばサマリーから元のメールにワンクリックで飛べます。

HTMLエスケープを必ず入れる
メールの件名や本文に<や&が含まれているとHTMLが壊れます。

escapeHtml関数で防御するのは必須です。

トリガー設定

このサマリーは毎朝送ってこそ意味があるのでトリガーで自動化します。

function setupDailySummaryTrigger() {
  // 既存トリガーを削除
  ScriptApp.getProjectTriggers().forEach(trigger => {
    if (trigger.getHandlerFunction() === 'sendDailySummary') {
      ScriptApp.deleteTrigger(trigger);
    }
  });

  // 毎日午前7時に実行
  ScriptApp.newTrigger('sendDailySummary')
    .timeBased()
    .everyDays(1)
    .atHour(7)
    .create();

  console.log('毎朝7時のサマリートリガーを設定しました');
}

GASでGmailを扱うときの注意点

承認スコープが強い

GmailAppを使うスクリプトはGmailの全メールを読み書きする権限を要求します。
初回実行時にGoogleの承認画面が出て、「このアプリは検証されていません」という警告が出ることもあります。

自分で書いたスクリプトなら問題ありませんが他人のスクリプトをコピーして動かすときは中身を必ず確認してください。

実行時間制限は変わらず

GmailAppも他のGASと同じく1回の実行が6分制限です。
大量のメールを処理する場合は処理対象の検索クエリを狭めるか、以前の記事で紹介したバッチ処理パターン(処理位置を保存して分割実行)を使ってください。

検索結果の件数に注意

GmailApp.search()の第3引数では取得件数を指定できますが大量件数の一括取得は公式でもページ分割での取得が推奨されています。
大量のメールを扱う場合は、検索条件を時期で区切って複数回実行するのが安全です。


まとめ

この記事で紹介した内容を整理します。

  • GmailApp.search()とGmail検索演算子を組み合わせて、対象メールを柔軟に絞り込める

  • スプレッドシート転記はメッセージIDで重複を防ぐのが鉄則

  • 自動返信は処理済みラベルで返信ループを防ぐのが必須

  • 添付ファイルの自動保存はMIMEタイプでフィルタし、ファイル名に日付を付けると管理しやすい

  • メールサマリーはHTMLエスケープgetPermalinkでの直接リンクで実用度が上がる

  • 1日100受信者(無料アカウント)の送信制限を常に意識する

Gmailの自動化は慣れればスプレッドシート以上に「日常の手間を減らす」効果が大きい領域です。
フィルタ機能と組み合わせることで、専用ツールを入れなくても業務自動化の多くが実現できます。

外部API連携の実践としてeBay Browse APIとGASを組み合わせたリサーチ自動化ツールの作り方も公開しています。OAuth認証やレート制限対策など、本格的なAPI連携のハマりどころが気になる方はこちらもどうぞ。


このnoteでは、エンジニアの副業・自動化の実践過程を発信しています。
「GAS自動化レシピ」マガジンに他の記事もまとめていますので、よろしければどうぞ。


いいなと思ったら応援しよう!