【GAS活用】Gmailのメールを自動で期間ごとに集計！スプレッドシートに記録する

## 【GAS活用】Gmailのメールを自動で期間ごとに集計！スプレッドシートに記録する完全ガイド

日々大量に届くGmail。特定の条件に合うメールだけを抜き出して、後で確認したり集計したりしたいと思ったことはありませんか？手作業でやると時間がかかり、ミスも起こりがちです。

この記事では、**Google Apps Script (GAS)** を使って、指定した条件のGmailメールを検索し、自動でスプレッドシートに記録していく方法を、コードの解説から使い方まで詳しくご紹介します。

**このスクリプトの主な特徴:**

*   **自動期間更新:** 古い日付から指定した月数ごとに期間を区切り、自動で次の期間の処理に進みます。
*   **中断＆自動再開:** GASの実行時間制限（通常6分）に達しても、自動的に続きから処理を再開します。大量のメールも確実に処理できます。
*   **処理済みメールのスキップ:** 一度処理したメールは記録済みIDとして保存し、次回以降はスキップするため、重複記録を防ぎます。
*   **定期実行対応:** トリガーを設定すれば、毎日や毎時など、定期的に新しいメールをチェックして追記できます。
*   **柔軟なカスタマイズ:** 検索条件、記録するスプレッドシート名、処理期間、出力項目などを簡単に変更できます。

**こんな方におすすめ:**

*   特定の差出人や件名のメールを一覧化したい方
*   プロジェクト関連のメールを時系列でまとめたい方
*   問い合わせメールなどをスプレッドシートで管理したい方
*   GASを使ってGmail操作を自動化したい方

### 完成したスクリプトの全体像

まずは、今回作成するスクリプトの全コードです。

```javascript
/**
 * Gmailのメールを検索し、スプレッドシートに記録するスクリプト
 * (自動期間更新機能付き: 古い期間から新しい期間へ順に処理)
 *
 * @description 指定した条件のGmailメールを指定した期間ごとに検索し、
 *              結果を指定したスプレッドシートに追記します。
 *              期間は自動的に更新され、古い期間から順に処理を進めます。
 *              中断された場合も、次回実行時に続きから再開する機能や、
 *              定期実行、手動での継続実行機能も備えています。
 *              現在日付より先には進まないよう制限されています。
 */

// ====================================
// グローバル設定 (★利用前にここを必ず確認・調整してください★)
// ====================================

/** @const {string} スクリプトプロパティに次回開始日を保存するキー名 */
var SCRIPT_PROPERTY_KEY_NEXT_START_DATE = "NEXT_PROCESS_START_DATE";

/** @const {string} 最初に処理を開始する日付 (YYYY/MM/DD形式)。これより古いメールは対象外。 */
var VERY_OLDEST_MAIL_DATE_STR = "2023/01/01"; // ★例: 2023年1月1日から開始する場合

/** @const {number} 一度に処理する期間の月数 (1 = 1ヶ月ごと) */
var PROCESS_INTERVAL_MONTHS = 1;

/** @const {string} 処理済みメールIDを保存するスクリプトプロパティのキー名 */
var PROCESSED_MESSAGE_IDS_KEY = "PROCESSED_MESSAGE_IDS";

/** @const {number} 処理済みとして記録しておくメールIDの最大件数 (古いものから削除される) */
var MAX_IDS_TO_STORE = 5000; // スクリプトプロパティのサイズ上限(500KB)に注意

// ====================================
// メイン処理関数
// ====================================

/**
 * 指定された期間のメールを検索し、シートに記録。期間完了後に次の期間の開始日を記録する。
 * @return {object} 処理結果 {status, newEmailsCount, period_start, period_end}
 */
function updateEmailsToSheet() {

  // --- 設定 ---
  /** @type {string} 基本となるGmail検索クエリ (期間指定は後で追加される) */
  var baseSearchQuery = "from:your-address@example.com OR to:your-address@example.com OR cc:your-address@example.com OR bcc:your-address@example.com"; // ★例: 特定のアドレスに関するメール
  /** @type {string} 結果を記録するスプレッドシートのファイル名 */
  var fileName = "メールログ"; // ★例: スプレッドシート名を指定

  // --- 期間決定 ---
  var scriptProperties = PropertiesService.getScriptProperties();
  var nextStartDateStr = scriptProperties.getProperty(SCRIPT_PROPERTY_KEY_NEXT_START_DATE);
  var targetStartDate, targetEndDate;
  if (nextStartDateStr) {
    targetStartDate = new Date(nextStartDateStr);
    Logger.log("前回の記録に基づき、" + nextStartDateStr + " から処理を再開します。");
  } else {
    targetStartDate = new Date(VERY_OLDEST_MAIL_DATE_STR);
    Logger.log("初回実行または記録がないため、" + VERY_OLDEST_MAIL_DATE_STR + " から処理を開始します。");
  }
  
  // 現在の日付を取得して、処理上限を設定
  var today = new Date();
  today.setHours(0, 0, 0, 0); // 今日の0時0分0秒に設定
  
  // 開始日が今日以降の場合は処理しない
  if (targetStartDate >= today) {
    Logger.log("開始日 " + Utilities.formatDate(targetStartDate, Session.getScriptTimeZone(), "yyyy/MM/dd") + " が本日以降のため処理を行いません。");
    return { status: "future_date", newEmailsCount: 0, period_start: Utilities.formatDate(targetStartDate, Session.getScriptTimeZone(), "yyyy/MM/dd"), period_end: null };
  }
  
  targetEndDate = new Date(targetStartDate);
  targetEndDate.setMonth(targetEndDate.getMonth() + PROCESS_INTERVAL_MONTHS);
  
  // 終了日が今日を超える場合は、今日までに制限
  if (targetEndDate > today) {
    targetEndDate = new Date(today);
    Logger.log("終了日が本日を超えるため、本日までに制限します。");
  }
  
  var timeZone = Session.getScriptTimeZone();
  var startDateStr = Utilities.formatDate(targetStartDate, timeZone, "yyyy/MM/dd");
  var endDateStr = Utilities.formatDate(targetEndDate, timeZone, "yyyy/MM/dd");
  var searchQuery = baseSearchQuery + " after:" + startDateStr + " before:" + endDateStr;
  Logger.log("★★★ 今回の検索期間: " + startDateStr + " から " + endDateStr + " まで ★★★");

  // --- リミット対策設定 ---
  var processInBatches = true; var batchSize = 20; var maxNewEmailsPerRun = 100;
  var sleepInterval = 10; var sleepDuration = 1000; var maxExecutionTime = 5 * 60 * 1000; // 5分

  // --- 初期化 ---
  var startTime = new Date().getTime();
  var processedIdSet = getProcessedIds();
  var ss;
  try {
    ss = getOrCreateSpreadsheet(fileName);
  } catch (e) {
     Logger.log("スプレッドシートの取得/作成失敗。処理中断。Error: " + e);
     return { status: "error_spreadsheet", newEmailsCount: 0, period_start: startDateStr, period_end: endDateStr };
  }
  var sheet = ss.getSheets()[0];
  setUpSheetHeaders(sheet);

  // --- メール検索 ---
  Logger.log("メール検索開始。クエリ: \"" + searchQuery + "\"");
  var threads = []; var searchError = null;
  try {
    threads = GmailApp.search(searchQuery); Logger.log("取得スレッド総数: " + threads.length);
    if (threads.length > 0) { try { /* Debug Log: First/Last mail date */ } catch(e){} }
    else { Logger.log("対象期間のスレッドは見つかりませんでした。"); }
  } catch (e) { Logger.log("Gmail検索エラー: " + e); searchError = e; }

  // --- メール処理ループ ---
  var processedInThisRun = 0; var newEmailsAdded = 0;
  var exceedTimeLimit = false; var exceedNewEmailLimit = false; var processingCompleted = false;

  if (!searchError && threads.length > 0) {
    Logger.log("取得スレッド対象に未処理メール探索 (新規上限: " + maxNewEmailsPerRun + "件)");
    threadLoop:
    for (var i = 0; i < threads.length; i++) {
      var thread = threads[i]; var messages;
      try { messages = thread.getMessages(); } catch (e) { Logger.log("スレッド " + thread.getId() + " メッセージ取得エラー: " + e); continue; }
      for (var j = 0; j < messages.length; j++) {
        var message = messages[j]; var msgId = message.getId();
        // 時間制限チェック
        if ((new Date().getTime()) - startTime > maxExecutionTime) { Logger.log("実行時間制限(" + (maxExecutionTime / 60000) + "分)で中断"); exceedTimeLimit = true; break threadLoop; }
        // 処理済みチェック
        if (processedIdSet.has(msgId)) { processedInThisRun++; continue; }
        // スリープ処理
        if (processedInThisRun > 0 && sleepInterval > 0 && processedInThisRun % sleepInterval === 0) { Logger.log(sleepInterval + "件毎に " + (sleepDuration / 1000) + " 秒 スリープ (総:" + processedInThisRun + ")"); Utilities.sleep(sleepDuration); }
        // メール処理実行
        try {
          processEmail(message, sheet, processedIdSet); newEmailsAdded++; processedInThisRun++;
          // 件数上限チェック
          if (newEmailsAdded >= maxNewEmailsPerRun) { Logger.log("新規メール追加上限(" + maxNewEmailsPerRun + "件)で中断"); exceedNewEmailLimit = true; break threadLoop; }
        } catch (e) { Logger.log("メール処理エラー (ID: " + msgId + "): " + e); /* エラーでもループ継続 */ }
        // バッチ保存
        if (processInBatches && (newEmailsAdded > 0 && newEmailsAdded % batchSize === 0)) { saveProcessedIds(processedIdSet); Logger.log(newEmailsAdded + "件追加時点で中間保存実行"); }
      } // End Message Loop
    } // End Thread Loop
    // 完了判定
    if (!exceedTimeLimit && !exceedNewEmailLimit) { processingCompleted = true; Logger.log("ループ正常完走"); }
  } else if (searchError) { Logger.log("検索エラーのため処理スキップ"); }
  else { Logger.log("対象スレッドなし。処理スキップ"); processingCompleted = true; Logger.log("対象期間メール無いため完了扱い"); }

  // --- 終了処理 & 期間更新 ---
  saveProcessedIds(processedIdSet); Logger.log("最終処理済ID保存 (" + processedIdSet.size + "件)");
  var finalStatus = "incomplete";
  if (searchError) { finalStatus = "error_search"; } else if (processingCompleted) { finalStatus = "complete"; }
  else if (exceedTimeLimit) { finalStatus = "incomplete_time"; } else if (exceedNewEmailLimit) { finalStatus = "incomplete_count"; }
  var statusMessage = "処理結果: " + finalStatus + "。新規追加: " + newEmailsAdded + "件。対象期間: " + startDateStr + " - " + endDateStr; Logger.log(statusMessage);
  // 期間更新
  if (finalStatus === "complete") {
    Logger.log("期間 (" + startDateStr + " - " + endDateStr + ") 処理完了");
    try { 
      // 次の期間の開始日が本日以降になる場合は、次回開始日を更新しない
      if (targetEndDate >= today) {
        Logger.log("次の期間の開始日が本日以降になるため、次回開始日を更新しません。現在の日付まで処理完了。");
      } else {
        scriptProperties.setProperty(SCRIPT_PROPERTY_KEY_NEXT_START_DATE, endDateStr); 
        Logger.log("次回開始日を " + endDateStr + " に更新");
      }
    }
    catch (e) { Logger.log("★★★ エラー: 次回開始日記録更新失敗: " + e); }
  } else { Logger.log("期間 (" + startDateStr + " - " + endDateStr + ") 処理中断。次回も " + startDateStr + " から再開"); }

  return { status: finalStatus, newEmailsCount: newEmailsAdded, period_start: startDateStr, period_end: endDateStr };
} // updateEmailsToSheet 関数終了


// ====================================
// ヘルパー関数群
// ====================================

/** スプレッドシート取得or作成 */
function getOrCreateSpreadsheet(fileName){
  var files = DriveApp.getFilesByName(fileName);
  if (files.hasNext()) {
    var file = files.next(); Logger.log("既存スプレッドシート使用: " + fileName + " (ID: " + file.getId() + ")");
    try { return SpreadsheetApp.open(file); } catch (e) { Logger.log("スプレッドシートオープン失敗: " + e); throw new Error("スプレッドシートが開けません。権限確認要。"); }
  } else {
    Logger.log("スプレッドシート新規作成: " + fileName); var ss = SpreadsheetApp.create(fileName); var sheet = ss.getSheets()[0];
    setUpSheetHeaders(sheet, true); return ss;
  }
}

/** ヘッダー行設定 */
function setUpSheetHeaders(sheet, isFirstCreation){
  isFirstCreation = isFirstCreation || false; var header = ["日時", "From", "To", "件名", "メール内容"]; // ★ヘッダー項目
  try {
    if (sheet.getMaxRows() < 1) { sheet.insertRowBefore(1); Logger.log("空シートのため1行目挿入"); isFirstCreation = true; }
    var firstRow; try { firstRow = sheet.getRange(1, 1, 1, header.length); } catch (e) { Logger.log("ヘッダー範囲取得失敗(列不足?):" + e); isFirstCreation = true; }
    var currentHeader = ""; if (!isFirstCreation && firstRow) { try { currentHeader = firstRow.getValues()[0].join(""); } catch (e) { Logger.log("既存ヘッダー読取失敗:" + e); isFirstCreation = true; } }
    if (isFirstCreation || !firstRow || currentHeader !== header.join("")) {
      Logger.log("ヘッダー行 設定/再設定実行"); if (sheet.getMaxColumns() < header.length) { var colsToAdd = header.length - sheet.getMaxColumns(); sheet.insertColumns(sheet.getMaxColumns() + 1, colsToAdd); Logger.log(colsToAdd + "列追加"); }
      firstRow = sheet.getRange(1, 1, 1, header.length); firstRow.setValues([header]); Logger.log("ヘッダー設定完了: " + header.join(", "));
      sheet.setFrozenRows(1); sheet.setColumnWidth(1, 150); sheet.setColumnWidth(2, 200); sheet.setColumnWidth(3, 200); sheet.setColumnWidth(4, 300); sheet.setColumnWidth(5, 500); // ★列幅調整
      firstRow.setBackground('#f3f3f3').setFontWeight('bold').setHorizontalAlignment('center'); Logger.log("ヘッダー書式設定完了");
    }
  } catch (e) { Logger.log("ヘッダー設定中エラー: " + e + "\n" + e.stack); }
}

/** 処理済みID読み込み */
function getProcessedIds(){
  var scriptProperties = PropertiesService.getScriptProperties(); var processedIdsJson = scriptProperties.getProperty(PROCESSED_MESSAGE_IDS_KEY);
  var processedIdList = []; if (processedIdsJson) { try { processedIdList = JSON.parse(processedIdsJson); if (!Array.isArray(processedIdList)) { Logger.log("処理済ID形式不正(非配列),リセット"); processedIdList = []; } } catch (e) { Logger.log("処理済IDパース失敗,リセット. Error:" + e); processedIdList = []; scriptProperties.deleteProperty(PROCESSED_MESSAGE_IDS_KEY); } }
  return new Set(processedIdList);
}

/** 処理済みID保存 */
function saveProcessedIds(processedIdSet){
  var scriptProperties = PropertiesService.getScriptProperties(); var idArray = Array.from(processedIdSet);
  if (idArray.length > MAX_IDS_TO_STORE) { Logger.log("保存ID数上限(" + MAX_IDS_TO_STORE + ")超過(" + idArray.length + "件)。古いID削除"); idArray = idArray.slice(idArray.length - MAX_IDS_TO_STORE); Logger.log("削除後、最新 " + idArray.length + "件保持"); }
  try { scriptProperties.setProperty(PROCESSED_MESSAGE_IDS_KEY, JSON.stringify(idArray)); } catch (e) { Logger.log("処理済ID保存中エラー: " + e); if (e.message && e.message.includes("Argument too large: value")) { Logger.log("★★★警告:保存データ過大。上限数(MAX_IDS_TO_STORE)減らすかcleanup検討要"); } }
}

/** 個別メール処理 */
function processEmail(message, sheet, processedIdSet){
  var msgId = message.getId(); var date = message.getDate(); var from = message.getFrom(); var to = message.getTo(); var cc = message.getCc() || ""; var subject = message.getSubject(); var body = cleanEmailBody(message.getPlainBody());
  var insertRowPosition = 2; // ヘッダーの次
  try {
    sheet.insertRowBefore(insertRowPosition);
    var range = sheet.getRange(insertRowPosition, 1, 1, 5); // 5列分 (日時,From,To,件名,内容)
    range.setValues([[
      date,
      extractEmailAddress(from),
      extractEmailAddress(to) + (cc ? " (Cc:" + extractEmailAddress(cc) + ")" : ""), // ToとCc
      subject,
      body
    ]]);
    processedIdSet.add(msgId); // 成功したらID追加
   } catch (e) { Logger.log("行挿入/書込失敗(pos=" + insertRowPosition + "):" + e); throw new Error("行挿入/書込失敗。処理中断"); }
}

/** メール本文整形 */
function cleanEmailBody(body){
  // 引用や署名を除去し、長文を切り詰める (実装は上記参照、必要に応じて調整)
  if (!body) return ""; var lines = body.split('\n'); var resultLines = []; var signatureFound = false;
  var signaturePatterns = [/^\s*--\s*$/,/^\s*-{3,}\s*$/,/^\s*_{3,}\s*$/,/^\s*Sent from my /i,/^\s*メールフッター/i,/^\s*----------/i,/^\s*From:/i,/^\s*送信者:/i,/^\s*Sent:/i,/^\s*To:/i,/^\s*Cc:/i,/^\s*Subject:/i,/^\s*連絡先:/i,/^\s*電話番号:/i,/^\s*株式会社/];
  var quotePatterns = [/^\s*>/,/^\s*On .+ wrote:/i,/^\s*\d{4}\/\d{1,2}\/\d{1,2}.+:/,/^\s*From:\s*.+<.+@.+>/i,/^\s*Sent:\s*.+$/,/^\s*To:\s*.+<.+@.+>/i,/^\s*Cc:\s*.+<.+@.+>/i];
  for (var i = 0; i < lines.length; i++) { var line = lines[i]; var trimmedLine = line.trim(); if (!signatureFound) { for (var k = 0; k < signaturePatterns.length; k++) { if (signaturePatterns[k].test(trimmedLine)) { signatureFound = true; break; } } } if (signatureFound) { continue; } var isQuote = false; for (var k = 0; k < quotePatterns.length; k++) { if (quotePatterns[k].test(line)) { isQuote = true; break; } } if (isQuote) { continue; } resultLines.push(line); }
  var cleanedBody = resultLines.join('\n').replace(/\n{3,}/g, '\n\n').trim(); var maxLength = 49000; if (cleanedBody.length > maxLength) { cleanedBody = cleanedBody.substring(0, maxLength) + "... [長文のため省略]"; Logger.log("メール本文長いため切詰"); } return cleanedBody;
}

/** メールアドレス抽出 */
function extractEmailAddress(addressString){
  // "名前 <email>" から email 部分を抽出 (実装は上記参照)
  if (!addressString) return ""; var addresses = addressString.split(','); var extractedAddresses = []; for (var i = 0; i < addresses.length; i++) { var part = addresses[i].trim(); if (!part) continue; var match = part.match(/<([^>]+)>/); if (match && match[1]) { extractedAddresses.push(match[1].trim()); } else { if (part.includes('@')) { extractedAddresses.push(part); } } } return extractedAddresses.join(', ');
}

// ====================================
// トリガー設定・管理関数群
// ====================================

/** 定期実行トリガー設定 (例: 1時間毎) */
function setHourlyTrigger() {
  deleteTriggers_('updateEmailsToSheet'); // 既存削除
  ScriptApp.newTrigger('updateEmailsToSheet').timeBased().everyHours(1).create();
  Logger.log('updateEmailsToSheet 1時間毎実行トリガー設定');
}

/** 特定関数名のトリガー削除 */
function deleteTriggers_(functionName) {
  var triggers = ScriptApp.getProjectTriggers(); var deletedCount = 0; for (var i = 0; i < triggers.length; i++) { if (triggers[i].getHandlerFunction() === functionName) { ScriptApp.deleteTrigger(triggers[i]); deletedCount++; } } if (deletedCount > 0) { Logger.log(functionName + ' 既存トリガー ' + deletedCount + ' 個削除'); }
}

/** 手動実行 & 継続トリガー設定 */
function startInitialDataProcessing() {
  Logger.log("データ処理 開始/再開..."); var result = updateEmailsToSheet();
  if (result && result.status && result.status.startsWith("incomplete")) {
    Logger.log("処理未完了(" + result.status + ")。5分後に続き実行。対象期間: " + result.period_start + " - " + result.period_end);
    deleteTriggers_('continueInitialDataProcessing'); // 既存継続トリガー削除
    ScriptApp.newTrigger('continueInitialDataProcessing').timeBased().after(5 * 60 * 1000).create();
    Logger.log("5分後に continueInitialDataProcessing 実行トリガー設定");
  } else if (result && result.status === "complete") {
    Logger.log("期間 (" + result.period_start + " - " + result.period_end + ") 処理完了。次回は次の期間処理。");
    deleteTriggers_('continueInitialDataProcessing');
  } else if (result && result.status === "future_date") {
    Logger.log("処理開始日が未来日のため終了。今日までの期間全て処理完了。");
    deleteTriggers_('continueInitialDataProcessing');
  } else {
    Logger.log("処理結果予期せぬ状態/検索エラー。継続トリガー設定せず。 Result: " + JSON.stringify(result));
    deleteTriggers_('continueInitialDataProcessing');
  }
}

/** 継続処理用関数 */
function continueInitialDataProcessing() {
  Logger.log("中断処理再開...");
  startInitialDataProcessing(); // 再度メイン処理呼び出し
}

// ====================================
// 手動実行用ユーティリティ関数
// ====================================

/** 手動用: 処理済みIDクリーンアップ */
function cleanupProcessedIds() {
  Logger.log("処理済IDクリーンアップ開始..."); var scriptProperties = PropertiesService.getScriptProperties(); var processedIdsJson = scriptProperties.getProperty(PROCESSED_MESSAGE_IDS_KEY); var processedIdList = []; if (processedIdsJson) { try { processedIdList = JSON.parse(processedIdsJson); if (!Array.isArray(processedIdList)) { processedIdList = []; Logger.log("処理済IDデータ不正(非配列),リセット扱"); } } catch (e) { processedIdList = []; Logger.log("処理済IDデータパース失敗,リセット扱. Error:" + e); scriptProperties.deleteProperty(PROCESSED_MESSAGE_IDS_KEY); } }
  if (processedIdList.length > MAX_IDS_TO_STORE) { Logger.log("処理済ID数(" + processedIdList.length + ")上限(" + MAX_IDS_TO_STORE + ")超過。古いID削除"); var originalLength = processedIdList.length; processedIdList = processedIdList.slice(originalLength - MAX_IDS_TO_STORE); try { scriptProperties.setProperty(PROCESSED_MESSAGE_IDS_KEY, JSON.stringify(processedIdList)); Logger.log("クリーンアップ完了。最新 " + processedIdList.length + " 件保存"); } catch (e) { Logger.log("クリーンアップ後ID保存中エラー: " + e); if (e.message && e.message.includes("Argument too large: value")) { Logger.log("★★★警告: クリーンアップ後もデータ過大。上限数(MAX_IDS_TO_STORE)減らす検討要"); } }
  } else { Logger.log("処理済ID数は上限以下(" + processedIdList.length + "/" + MAX_IDS_TO_STORE + ")。クリーンアップ不要"); }
}

/** 手動用: 次回処理開始日リセット */
function resetNextProcessStartDateProperty() {
  var scriptProperties = PropertiesService.getScriptProperties();
  scriptProperties.deleteProperty(SCRIPT_PROPERTY_KEY_NEXT_START_DATE);
  Logger.log("スクリプトプロパティ \"" + SCRIPT_PROPERTY_KEY_NEXT_START_DATE + "\" を削除しました。次回実行時は最も古い指定日 (\"" + VERY_OLDEST_MAIL_DATE_STR + "\") から開始されます。");
}

// ====================================
// === スクリプト終端 ===
// ====================================
```

### スクリプトの使い方

#### 1. 準備

1.  **Google Apps Script エディタを開く:** Googleドライブを開き、「新規」 > 「その他」 > 「Google Apps Script」を選択します。または、既存のスプレッドシートから「ツール」 > 「スクリプトエディタ」でも開けます。
2.  **コードを貼り付け:** エディタが開いたら、デフォルトで表示されているコード (`function myFunction() { ... }`) をすべて削除し、上記の完成コードを貼り付けます。
3.  **プロジェクト名を付ける:** 左上の「無題のプロジェクト」をクリックし、分かりやすい名前（例: Gmailメールログ収集）を付けて「名前を変更」をクリックします。
4.  **保存:** フロッピーディスクのアイコン💾をクリックして、スクリプトを保存します。

#### 2. 初期設定 (★重要★)

コードを貼り付けたら、利用する前に **「グローバル設定」** セクションと **`updateEmailsToSheet`関数内の設定** を自分の環境に合わせて調整する必要があります。

*   `VERY_OLDEST_MAIL_DATE_STR`: 最初にメール検索を開始したい日付を `"YYYY/MM/DD"` 形式で指定します。これより古いメールは検索対象外になります。
    ```javascript
    var VERY_OLDEST_MAIL_DATE_STR = "2023/01/01"; // 例: 2023年1月1日から処理したい場合
    ```
*   `PROCESS_INTERVAL_MONTHS`: 一度に処理する期間を月単位で指定します。`1` なら1ヶ月ごと、`3` なら3ヶ月ごとに処理が進みます。
    ```javascript
    var PROCESS_INTERVAL_MONTHS = 1; // 例: 1ヶ月ごとに区切る場合
    ```
*   `MAX_IDS_TO_STORE`: 処理済みとして記録しておくメールIDの最大件数です。GASのスクリプトプロパティには保存容量の上限（約500KB）があるため、あまりに古いIDを保持し続けると上限に達する可能性があります。通常はデフォルトの`5000`程度で問題ありませんが、大量のメールを長期間処理する場合は調整が必要になるかもしれません。
*   `baseSearchQuery` (`updateEmailsToSheet`関数内): **最も重要な設定項目です。** ここに、収集したいメールの条件をGmailの検索演算子を使って指定します。期間 (`after:`, `before:`) はスクリプトが自動で付加するので、それ以外の条件を指定します。
    ```javascript
    // 例1: 特定の差出人からのメール
    var baseSearchQuery = "from:info@example.com";

    // 例2: 特定のラベルが付いたメール
    var baseSearchQuery = "label:重要プロジェクト";

    // 例3: 特定のキーワードを含むメール
    var baseSearchQuery = "subject:(見積依頼 OR 請求書)";

    // 例4: 複数の条件を組み合わせる (ORやANDも利用可能)
    var baseSearchQuery = "from:partner@example.com label:対応待ち";
    ```
    **注意:** `your-address@example.com` の部分は必ず実際のメールアドレスや検索条件に書き換えてください。Gmailの検索ボックスで試して、意図したメールが検索できるか確認することをおすすめします。
*   `fileName` (`updateEmailsToSheet`関数内): 収集結果を記録するスプレッドシートのファイル名を指定します。この名前のファイルがマイドライブのルートに見つからない場合は、自動的に新規作成されます。
    ```javascript
    var fileName = "プロジェクトAメールログ"; // 例: ファイル名を指定
    ```

設定を変更したら、必ず再度保存💾してください。

#### 3. 初回実行と権限承認

過去のメールを一括で処理する場合や、初めてスクリプトを実行する場合は、手動で実行を開始します。

1.  GASエディタの上部にある関数選択ドロップダウンリストで `startInitialDataProcessing` を選択します。
2.  「実行」ボタン（再生▶アイコン）をクリックします。
3.  初回実行時には「承認が必要です」というダイアログが表示されます。
    *   「権限を確認」をクリックします。
    *   Googleアカウントを選択する画面が表示されるので、スクリプトを実行したいアカウントを選びます。
    *   「このアプリはGoogleで確認されていません」という警告が表示される場合がありますが、「詳細」をクリックし、「（プロジェクト名）（安全でないページ）に移動」をクリックします。（これは自作のスクリプトでは一般的な表示です）
    *   スクリプトがリクエストしている権限（Gmailの読み取り、スプレッドシートの編集、外部サービスへの接続など）の一覧が表示されるので、内容を確認し、「許可」をクリックします。

権限が承認されると、スクリプトの実行が開始されます。

#### 4. 処理の仕組み（初回実行と継続処理）

*   `startInitialDataProcessing` を実行すると、まず `updateEmailsToSheet` 関数が呼び出され、設定された最も古い期間からメールの検索と記録が始まります。
*   GASには通常6分間の実行時間制限があります。もし処理対象のメールが多く、5分以内（コード内の `maxExecutionTime` で設定）にその期間の処理が終わらなかった場合、スクリプトは自動的に中断します。
*   中断する際、スクリプトは「5分後に `continueInitialDataProcessing` 関数を実行する」という**時間ベースのトリガー**を自動で設定します。
*   5分後、トリガーによって `continueInitialDataProcessing` が実行されると、この関数は再度 `startInitialDataProcessing` を呼び出します。
*   `startInitialDataProcessing` は、中断した箇所（保存されている次回開始日）から処理を再開します。
*   このように、「実行 → 中断 → 5分後にトリガーで再開」を繰り返しながら、指定した最も古い日付から現在までのメールを少しずつ処理していきます。
*   ある期間の処理が5分以内に完了した場合（`status` が `complete` になった場合）、次の期間の開始日が記録され、継続トリガーは設定されずに終了します。`startInitialDataProcessing` が再度手動または定期トリガーで実行されると、次の期間から処理が始まります。

過去分を一括処理したい場合は、全ての期間が完了するまで `startInitialDataProcessing` が（トリガー経由で）繰り返し実行されるのを待つか、手動で複数回実行する必要があります。

#### 5. 定期実行の設定（オプション）

過去分の処理が終わり、今後は新しいメールを定期的にチェックして追記したい場合は、定期実行トリガーを設定します。

1.  GASエディタの関数選択ドロップダウンリストで `setHourlyTrigger` を選択します。
2.  「実行」ボタン▶をクリックします。（権限承認が再度求められる場合があります）
3.  ログに「updateEmailsToSheet 1時間毎実行トリガー設定」のように表示されれば成功です。これで1時間ごとに `updateEmailsToSheet` 関数が自動実行され、新しいメールが追記されるようになります。

    *   実行間隔を変更したい場合は、`setHourlyTrigger` 関数内の `.everyHours(1)` を `.everyMinutes(30)`（30分毎）や `.everyDays(1)`（毎日）などに変更して再度 `setHourlyTrigger` を実行してください。
    *   トリガーはGASエディタの左メニューにある時計アイコン（トリガー）からも確認・編集・削除できます。

#### 6. ログの確認

スクリプトがどのように動作しているか、エラーが発生していないかなどを確認するには、ログを見るのが便利です。

1.  GASエディタの左メニューにある「実行数」をクリックします。
2.  過去の実行履歴が一覧表示されるので、確認したい実行の行をクリックすると、その実行時のログ（`Logger.log(...)` で出力された内容）が表示されます。
    *   処理の開始・終了、検索期間、エラーメッセージなどが確認できます。

### コードの解説

ここでは、スクリプト内の主要な関数とその役割を簡単に解説します。

*   **`updateEmailsToSheet()`:** スクリプトの中核となるメイン関数。期間の決定、メール検索、ループ処理、結果の記録、期間更新などを行います。
*   **`getOrCreateSpreadsheet(fileName)`:** 指定されたファイル名のスプレッドシートを探し、存在すればそれを開き、なければ新規作成します。
*   **`setUpSheetHeaders(sheet, isFirstCreation)`:** スプレッドシートの1行目にヘッダー行を設定し、列幅や書式を設定します。初回作成時やヘッダーが異なる場合に実行されます。
*   **`getProcessedIds()`:** スクリプトプロパティに保存されている処理済みメールIDのリスト（JSON形式）を読み込み、重複チェックが高速な`Set`オブジェクトとして返します。
*   **`saveProcessedIds(processedIdSet)`:** 処理済みのメールIDが含まれる`Set`オブジェクトをJSON形式の文字列に変換し、スクリプトプロパティに保存します。保存件数上限を超えた場合は古いIDを削除します。
*   **`processEmail(message, sheet, processedIdSet)`:** 個々のメールオブジェクトを受け取り、必要な情報（日時、From/To/Cc、件名、本文）を抽出し、整形してスプレッドシートの新しい行に書き込みます。成功したら処理済みIDセットにそのメールのIDを追加します。
*   **`cleanEmailBody(body)`:** メールの本文（プレーンテキスト）を受け取り、引用部分（`>`で始まる行など）や署名（`--`など）を正規表現で判定して除去し、整形した本文を返します。また、長すぎる本文を指定文字数で切り詰めます。**※ここの正規表現はメールの形式によって調整が必要な場合があります。**
*   **`extractEmailAddress(addressString)`:** `"名前 <email@example.com>"` のような文字列からメールアドレス部分 (`email@example.com`) を抽出します。カンマ区切りの複数アドレスにも対応します。
*   **`setHourlyTrigger()`:** `updateEmailsToSheet` 関数を1時間ごとに実行する時間ベーストリガーを設定します。既存の同名トリガーは削除してから設定します。
*   **`deleteTriggers_(functionName)`:** 指定された関数名を持つトリガーをすべて削除するヘルパー関数です。
*   **`startInitialDataProcessing()`:** 手動実行用の関数。`updateEmailsToSheet` を呼び出し、結果が「未完了」であれば、5分後に継続処理 (`continueInitialDataProcessing`) を行うトリガーを設定します。
*   **`continueInitialDataProcessing()`:** 時間ベーストリガーから呼び出され、中断された処理を再開するために `startInitialDataProcessing` を再度呼び出します。
*   **`cleanupProcessedIds()`:** 手動実行用のユーティリティ関数。処理済みIDが保存上限数 (`MAX_IDS_TO_STORE`) を超えている場合に、古いIDを削除して整理します。
*   **`resetNextProcessStartDateProperty()`:** 手動実行用のユーティリティ関数。次回処理開始日の記録を削除します。これを実行すると、次に `updateEmailsToSheet` や `startInitialDataProcessing` が実行された際に、`VERY_OLDEST_MAIL_DATE_STR` で指定した最も古い日付から処理が再開されます。

### カスタマイズのヒント

このスクリプトは、いくつかの箇所を修正することでもっと便利にカスタマイズできます。

*   **検索条件の変更:** `updateEmailsToSheet` 関数内の `baseSearchQuery` を変更することで、収集するメールを自由に変えられます。Gmailの検索演算子を活用しましょう。
*   **出力項目の変更:**
    *   スプレッドシートのヘッダーを変更したい場合は、`setUpSheetHeaders` 関数内の `header` 配列を修正します。
    *   書き込むデータを変更したい場合は、`processEmail` 関数内の `range.setValues([...])` の部分を修正します。例えば、メールのラベル情報を取得して追加したり、本文を記録しないようにしたりできます。（※列数を変更した場合は `getRange` の最後の引数も合わせる必要があります）
*   **本文整形の調整:** `cleanEmailBody` 関数内の `signaturePatterns` や `quotePatterns` の正規表現を、実際のメールに合わせて調整することで、より精度高く本文のみを抽出できます。
*   **エラー通知:** `try...catch` ブロック内で特定のエラーが発生した場合に、`MailApp.sendEmail()` を使って自分にエラー通知メールを送るような処理を追加することも可能です。

### 注意点

*   **GASの実行時間制限:** 通常、1回の実行時間は6分までです。このスクリプトでは5分で中断するようにしていますが、非常に複雑な処理を追加すると制限にかかる可能性があります。
*   **Gmail APIの利用上限:** 短時間に大量のメールを処理しようとすると、Gmail APIの利用上限に達して一時的にエラーになることがあります。スクリプト内には `Utilities.sleep()` による待機処理が入っていますが、それでもエラーが頻発する場合は、`sleepInterval` や `sleepDuration` の値を調整してみてください。
*   **スクリプトプロパティの上限:** 処理済みIDを保存するスクリプトプロパティには約500KBのサイズ上限があります。非常に長期間（数年以上）かつ大量のメールを処理する場合、IDリストが上限に達する可能性があります。`MAX_IDS_TO_STORE` の値を調整したり、定期的に `cleanupProcessedIds` を実行したりすることを検討してください。
*   **権限承認:** スクリプトはGmailやスプレッドシートにアクセスするための権限を必要とします。承認する際は内容を確認してください。
*   **自己責任での利用:** このスクリプトは提供された情報に基づいて作成されていますが、利用は自己責任でお願いします。重要なデータに影響を与える可能性があるため、事前にテスト用のメールやスプレッドシートで十分に動作確認を行ってください。

### まとめ

今回は、Google Apps Scriptを使ってGmailのメールを検索し、スプレッドシートに自動で記録していく方法をご紹介しました。期間の自動更新や中断・再開機能により、手間なく効率的にメールログを蓄積できます。

ぜひこのスクリプトをベースに、ご自身の用途に合わせてカスタマイズして、日々のメール管理や情報収集に役立ててみてください！