GASでGmailの領収書メールをGoogle Driveに自動保存する方法

スタバでモバイルオーダーを使うと、購入のたびにメールが届きます。
今回は、Gmailに届いた領収書メールをGoogle Driveに自動保存する方法を書きます。
まずはスタバのモバイルオーダーのメールのように、件名で見つけやすいものから始めるのがおすすめです。

目次

領収書メールの保存を手動でやると地味に面倒

税理士事務所に経理代行を頼んでいても、領収書は自分で収集し送らなければなりません。
今はクラウドストレージで共有できるので、以前よりかなりラクになりました。

紙のレシートであれば袋にまとめて入れればいいだけですが、
メールで届く領収書は、あとでまとめて保存しようと思って、そのままになりがちです。

今回のGASを使えば、領収書メールをGoogle Driveに自動で保存できます。
その保存先フォルダを税理士事務所と共有しておけば、毎回送る手間も減ります。
経理を外注していても、この部分は効率化しやすいです。

なお、私はレシートを1枚ずつ入力する形の記帳代行はやっていません。
ただ、メール本文から日付や金額を抽出してスプレッドシートにまとめる形であれば対応しています。

GASで領収書メールを自動保存するやり方

GASとは

GAS(Google Apps Script)は、Googleが無料で提供しているプログラム実行環境です。

GmailやDriveをプログラムで操作できます。
しかもGoogleのサーバー上で動くので、パソコンを閉じていても自動で実行されます。
Googleアカウントがあれば試せます。

コードのポイント

コードの中で編集が必要な部分は TARGETS だけです。

const TARGETS = [
  {
    name: 'starbucks',
    query: 'subject:"Mobile Order & Pay"',
    folder: '領収書/starbucks',
  },
];
  • name:ファイル名の先頭につく名前
  • query:Gmailの検索条件です。件名で絞ります。今回は「 Mobile Order & Pay 」
  • folder:Driveの保存先フォルダです。なければ自動で作られます。

他のサービスを追加したいときは、この TARGETS に1つ追加するだけです。

初回実行と通常運用の違い

関数が2つあります。用途が違います。

初回だけ実行する

saveAllReceipts()

過去のメールをすべて取得してDriveに保存します。
最初の1回だけ手動で実行します。

週1で自動実行する

saveRecentReceipts()

直近14日分だけ確認します。
週1のトリガーに設定しておけば、その後は自動で動きます。
重複チェックはファイル名でしているので、同じメールが何度も保存されることはありません。

設定手順

  1. Googleドライブでスプレッドシートを1つ作る
  2. 「拡張機能」→「Apps Script」を開く

3.後述のコードを貼り付けて保存する(ctrl+v → ctrl+s)
4.saveAllReceipts を選んで実行する(ctrl+r)
5.初回はGmailとDriveへのアクセス許可を求められるので許可する
6.「トリガー」から saveRecentReceipts を週1で実行するよう設定する
トリガーの設定は、Apps Scriptの画面左側にある時計アイコンから操作できます。

コード全文

// ============================================================
// ここだけ編集すれば他のサービスにも使えます
// ============================================================
const TARGETS = [
  {
    name: 'starbucks',
    query: 'subject:"Mobile Order & Pay"',
    folder: '領収書/starbucks',
  },
];

function saveAllReceipts() {
  saveReceipts(target => getAllThreads(target.query));
}

function saveRecentReceipts() {
  saveReceipts(target => getRecentThreads(target.query, 14));
}

function saveReceipts(getThreadsFn) {
  TARGETS.forEach(target => {
    const folder = getOrCreateFolder(target.folder);
    const threads = getThreadsFn(target);

    threads.forEach(thread => {
      thread.getMessages().forEach(message => {
        try {
          const dateForName = Utilities.formatDate(
            message.getDate(),
            Session.getScriptTimeZone(),
            'yyyyMMdd_HHmmss'
          );
          const shortId = message.getId().slice(-6);
          const fileName = `${target.name}_${dateForName}_${shortId}.pdf`;

          if (fileExists(folder, fileName)) return;

          const dateStr = Utilities.formatDate(
            message.getDate(),
            Session.getScriptTimeZone(),
            'yyyy/MM/dd HH:mm:ss'
          );

          const html = `
            <html>
              <head>
                <meta charset="UTF-8">
                <style>
                  body { font-family: sans-serif; padding: 24px; font-size: 14px; line-height: 1.6; }
                  h1 { font-size: 16px; margin: 0 0 8px; }
                  .meta { color: #666; font-size: 12px; margin-bottom: 16px; }
                  hr { margin: 16px 0; }
                </style>
              </head>
              <body>
                <h1>${escapeHtml(message.getSubject())}</h1>
                <div class="meta">受信日時: ${dateStr}</div>
                <hr>
                ${message.getBody()}
              </body>
            </html>
          `;

          const blob = Utilities.newBlob(html, 'text/html')
            .getAs('application/pdf')
            .setName(fileName);

          folder.createFile(blob);

        } catch (e) {
          Logger.log(`保存失敗: ${target.name} / ${message.getId()} / ${e}`);
        }
      });
    });
  });
}

function getAllThreads(query) {
  const allThreads = [];
  let start = 0;
  const batchSize = 100;
  while (true) {
    const threads = GmailApp.search(query, start, batchSize);
    if (threads.length === 0) break;
    allThreads.push(...threads);
    start += batchSize;
  }
  return allThreads;
}

function getRecentThreads(query, days) {
  return getAllThreads(`${query} newer_than:${days}d`);
}

function getOrCreateFolder(path) {
  const parts = path.split('/');
  let current = DriveApp.getRootFolder();
  parts.forEach(part => {
    const found = current.getFoldersByName(part);
    current = found.hasNext() ? found.next() : current.createFolder(part);
  });
  return current;
}

function fileExists(folder, fileName) {
  return folder.getFilesByName(fileName).hasNext();
}

function escapeHtml(str) {
  if (!str) return '';
  return str
    .replace(/&/g, '&')
    .replace(/</g, '<')
    .replace(/>/g, '>')
    .replace(/"/g, '"')
    .replace(/'/g, ''');
}

実際に使って感じたこと

設定は最初の1回だけです。
その後は何もしなくても、Driveに領収書メールがたまっていきます。

スタバ以外にも広げやすいです。
TARGETS にAmazonなどを追加すれば、同じやり方を使えます。
件名で絞りやすいサービスなら応用しやすいでしょう。

今回は、Driveへの保存までです。
保存の自動化としてはこれで十分役立ちます。

経理入力まで速くしたいなら、PDF保存より、メール本文から日付や金額を抽出してスプレッドシートにまとめる方法のほうが早いです。
スタバの二重計上の記事でGASのコードを載せています。↓
スタバカードや交通系ICカードで二重計上していませんか。正しい処理と自動化の話

【余白ログ】
昨日は、税理士業とブログを書いてから、ジム。

目次