はじめに
Google Apps Script (GAS) を使って、特定の件名のメールをスプレッドシートに自動転記するスクリプトを作成していた際のこと。「返信 (Re:)」や「転送 (Fwd:)」は除外したいけれど、肝心の 「元の依頼メール」まで拾えていないという現象に遭遇しました。
原因はGmail特有の 「スレッド機能」 と、GASでの取得ロジックの組み合わせにありました。同じミスを防ぐために、事象と解決策をまとめておきます。
1. 起きていた問題
「見積もり依頼」というキーワードを含むメールを抽出したい。ただし、「Re:」や「Fwd:」がついている返信・転送メールはノイズになるので除外したい。
期待する動作
- 【対象】【社内】見積もり依頼(A社) → 取得したい
- 【除外】Re: 【社内】見積もり依頼(A社) → 除外したい
実際の動作
- 【除外】Re: 【社内】見積もり依頼(A社) → OK(除外された)
- 【不明】【社内】見積もり依頼(A社) → なぜか一緒に除外されてしまう!
2. 原因:スレッドの「最新の1通」しか見ていなかった
Gmailの仕様として、件名が同じやり取りは1つの「スレッド」にまとめられます。
当初のコードでは、以下のように記述していました。
// 【NGコード】スレッドごとのループ
for (const thread of threads) {
// スレッド内の「最新の1通」だけを取得
const message = thread.getMessages().pop();
// そのメッセージが「Re:」ならスキップ
if (message.getSubject().match(/^Re:/)) {
continue; // ← ここでスレッドごと処理終了してしまう!
}
// 書き込み処理...
}
何が起きていたか?
- あるスレッドに「元の依頼メール」と、それに対する「返信メール (Re:)」が含まれている。
- GASの検索(
GmailApp.search)はスレッド単位でヒットする。 - コードは
thread.getMessages().pop()で、一番最後のメール(つまりRe:がついたメール)だけを取り出す。 - 「あ、これはRe:がついているから除外対象だ」と判断し、
continueする。 - 結果、そのスレッドに含まれていた「Re:がついていない元のメール」を見る前に、次のスレッドへ行ってしまう!
3. 解決策:スレッド内の全メッセージをループする
スレッドを取得した後、その中のメッセージを1つずつ回して判定する必要がありました。
// 【OKコード】スレッド → メッセージ の2重ループ
for (const thread of threads) {
const messages = thread.getMessages(); // スレッド内の全メールを取得
// メッセージを1つずつチェック
for (const message of messages) {
const subject = message.getSubject();
// 「このメッセージ」がRe:ならスキップ(スレッド全体はスキップしない)
if (REPLY_FORWARD_REGEX.test(subject)) {
continue;
}
// 書き込み処理...
}
}
これで、「Re:メール」は除外しつつ、同じスレッド内にある「元メール」は正しく拾えるようになりました。
4. 今後のためのチェックリスト(教訓)
Gmail関連のGASを書く際は、以下の点に注意が必要です。
✅ GmailApp.search は「スレッド」を返す
- 検索条件にヒットしたメールが1通でもあれば、スレッドごと取得されます。
✅ 除外判定は「メッセージ単位」で行う
- スレッド単位で
continueすると、巻き添えで必要なメールも捨ててしまうリスクがあります。
✅ getMessages() は配列を返す
-
pop()(最新) や[0](最古) だけで処理を済ませようとせず、必要に応じてforループで全件チェックしましょう。
まとめ
「最新の状態だけ知りたい」なら pop() で良いですが、「ログとして過去のやり取りも抽出したい」 場合は、必ずメッセージ単位でのループ処理が必要です。
Gmailのスレッド機能は便利ですが、GASで扱う際には「スレッド ≠ メール」という点を意識しないと、思わぬ落とし穴にはまります。
同じような問題で悩んでいる方の参考になれば幸いです!
タグ: #GAS #GoogleAppsScript #Gmail #スプレッドシート #自動化 #備忘録
