Google Apps Script で Gmail を自動監視し、LINE と Discord に転送する方法

## 全体の紹介

本記事では、Google Apps Script (GAS) を使って、Gmail の特定のスレッドを自動で監視し、本文や件名をクリーニングして Discord に転送する方法を紹介します。
LINE Notify でも同様の処理が可能ですが、**LINE Notify は今年度末（2023年度末）でサービス終了が予定されているため**、本記事では主に **Discord の Webhook** を使った通知手法を中心に解説します。

## なんで作ったか

筆者（私）は学生の頃、メールチェックが習慣化できておらず、つい見落としてしまうことが多々ありました。しかし、就職活動が始まると、選考や面接の詳細連絡など、重要なやり取りがメールで届くようになります。そこで「メールを見逃さないようにしたい」と思い、いつも使っている Discord へ自動転送する仕組みを作りました。

## Line Notify が今年度終わること

2025年3月31日をもってLINE Notifyはサービス終了になるため、今後は使えなくなります。もしどうしてもLINE Notifyで通知を受け取りたい場合は、ご自身で API キーの取得方法をお調べください。

## 動作方法

### Discord Webhook URL の取り方

1. Discord の任意のサーバーで、**管理権限またはチャンネル管理権限**を持っているテキストチャンネルを用意します。
2. テキストチャンネルの設定を開き、**「連携のサービス」 → 「ウェブフック」** から**新しいウェブフック**をクリックします。
3. 作成した Webhook の **「Webhook URL」** をコピーしておきます。このURLが今回のスクリプトで使う `DISCORD_WEBHOOK_URL` となります。

### 検索条件について

スクリプト内では `SEARCH_QUERY` を使用して検索条件を指定しています。
Gmail の検索演算子や記号（`from:` や `-label:` など）は非常に柔軟に使えるので、以下の Google ヘルプを参考にカスタマイズしてみてください。

- [Gmail で検索を絞り込む - パソコン - Gmail ヘルプ](https://support.google.com/mail/answer/7190)

```javascript
const SEARCH_QUERY = [
  "{面接 選考 試験 東京}", // 検索に含ませたい文言を大括弧内に空白をあけて書く (OR検索)
  "-{楽天ポイントカード 楽天toto}", // 検索に含ませたくない文言を大括弧内に空白をあけて書く
  "-label:BotRead", // この行は変更しない
].join(" ");
```

#### 使い方のポイント

1. **`"{面接 選考 試験 東京}"`**
   - `{}` 内に空白区切りでキーワードを並べると、いずれかのキーワードを含む（OR 検索）スレッドを探せます。
   - 例：`面接 OR 選考 OR 試験 OR 東京` という意味になります。

2. **`"-{楽天ポイントカード 楽天toto}"`**
   - 先頭に `-`（マイナス）を付けると、キーワードを含むメールを検索結果から除外します。
   - ここでは「楽天ポイントカード」または「楽天toto」のどちらかを含むスレッドは除外します。

### GAS にコードを貼り付け

GAS のエディタ（[script.google.com/home](https://script.google.com/home)）を開き、新規プロジェクトを作成して以下のサンプルコードを貼り付けます。

:::note warn
忘れずに【あなたの Discord Webhook URL】部分を取得したURLで置き換えてください。
すみつき括弧【】も必要ありません
:::

<details open>
<summary>サンプルコード</summary>

```javascript
/***************************************
 * 定数宣言
 ***************************************/
// すでに BotRead がついているスレッドは除外し、
// キーワードに該当するスレッドを検索する
const SEARCH_QUERY = [
  "{面接 選考 試験 東京}", // 検索に含ませたい文言を大括弧内に空白をあけて書く (OR検索)
  "-{楽天ポイントカード 楽天toto}", // 検索に含ませたくない文言を大括弧内に空白をあけて書く
  "-label:BotRead", // この行は変更しない
].join(" ");

// スレッド処理後につけるラベル名
const BOT_READ_LABEL_NAME = "BotRead";  // この行は変更しない

// 送信先
const DISCORD_WEBHOOK_URL = "【あなたの Discord Webhook URL】";
// const LINE_TOKEN = "【Line Notify API Key】";  // Line Notify を使用する場合

// Discord 制限用
const DISCORD_MAX_LENGTH = 2000;

/***************************************
 * メイン処理
 ***************************************/
function mailBotMain() {
  // BotRead が付いていないスレッドを検索
  const threads = GmailApp.search(SEARCH_QUERY);

  threads.forEach(thread => {
    // スレッド内のメール一覧を取得
    const messages = thread.getMessages();

    messages.forEach(message => {
      // すでに既読のメールはスキップしたい場合は以下を有効化
      // if (!message.isUnread()) return;

      // 件名・本文の取得とクリーニング
      const subject = message.getSubject();
      let plainBody = message.getPlainBody();
      plainBody = clean(plainBody);

      // 件名＋本文をまとめて送信
      const text = "# " + subject + "\n\n" + plainBody;
      sendMessage(text);
    });

    // スレッドに BotRead ラベルを付与
    let label = GmailApp.getUserLabelByName(BOT_READ_LABEL_NAME);
    if (!label) {
      // ラベルが存在しない場合は新規作成
      label = GmailApp.createLabel(BOT_READ_LABEL_NAME);
    }
    thread.addLabel(label);
  });
}

/***************************************
 * テキスト整形関連
 ***************************************/
/**
 * メール本文から不要な空白や改行を整理する
 */
function clean(text) {
  // 改行コード統一
  text = text.replace(/\r\n/g, '\n');
  // 3つ以上連続する改行は2つにする
  text = text.replace(/\n{3,}/g, '\n\n');
  // 各行末尾の空白を除去
  text = text.replace(/[ \t]+$/gm, '');
  return text;
}

/***************************************
 * メッセージ送信関連
 ***************************************/
function sendMessage(text) {
  // sendLine(text); // LINE Notifyを使う場合
  sendDiscord(text);
}

/**
 * Discord Webhook にメッセージを送信
 * content フィールドには 2000 文字制限があるため、長文は分割。
 */
function sendDiscord(text) {
  // Discord は 2000文字制限があるので、必要なら分割して送る
  const chunks = splitByLengthWithNewline(text, DISCORD_MAX_LENGTH);

  chunks.forEach(chunk => {
    const payload = {
      content: chunk,
      username: "mail GAS Bot"  // 任意
    };

    const options = {
      method: "post",
      contentType: "application/json",
      payload: JSON.stringify(payload)
    };

    try {
      UrlFetchApp.fetch(DISCORD_WEBHOOK_URL, options);
      Logger.log("メッセージが送信されました。");
    } catch (error) {
      Logger.log("エラーが発生しました: " + error);
      // 例：400 (2000文字以上), 429 (Rate Limit)など
    }
  });
}

/**
 * 指定した文字数以内の改行でテキストを分割するためのユーティリティ
 * 改行が見つからない場合は maxLength で強制的に分割します
 */
function splitByLengthWithNewline(text, maxLength) {
  const result = [];
  let startIndex = 0;

  while (startIndex < text.length) {
    let endIndex = startIndex + maxLength;
    if (endIndex > text.length) {
      endIndex = text.length;
    }

    // 指定範囲内で最後の改行位置を探す
    const lastNewlineIndex = text.lastIndexOf('\n', endIndex);

    if (lastNewlineIndex > startIndex) {
      // 改行が見つかった場合、その位置で分割
      result.push(text.substring(startIndex, lastNewlineIndex));
      startIndex = lastNewlineIndex + 1; // 改行文字の次から開始
    } else {
      // 改行が見つからない場合、maxLengthで分割
      result.push(text.substring(startIndex, endIndex));
      startIndex = endIndex;
    }
  }

  return result;
}

/**
 * LINE Notify にメッセージを送信
 */
function sendLine(text) {
  const options = {
    method: "post",
    headers: {
      Authorization: "Bearer " + LINE_TOKEN
    },
    payload: {
      message: text
    }
  };
  UrlFetchApp.fetch("https://notify-api.line.me/api/notify", options);
}

```

</details>

### GAS 定期実行のさせ方

1. 「時計アイコン（トリガー）」を選択します。
2. 「トリガーを追加」ボタンから以下の設定で保存します。
    - 実行する関数を選択
        - **mailBotMain**
    - 実行するデプロイを選択
        - **Head**
    - イベントのソースを選択
        - **時間主導型**
    - 時間ベースのトリガーのタイプを選択
        - **分ベースのタイマー**
    - 時間の間隔を選択（分）
        - **1分おき**


このようにすれば、定期的に Gmail をチェックし、新着メールを検出して Discord に送信する自動化が実現できます。

:::note info

以上で動作させられます

:::

:::note warn

### 初回動作時の注意

GAS には **1回あたり6分** という実行制限があります。
初回に大量の未処理メールを取得すると、6分以内に完了せずエラーになる場合があります。その際は、手動で古いメールを読んだ扱いにしたり、最初だけ検索キーワードを絞るなど調整しましょう。
:::

## 主な流れ

1. **検索クエリで Gメール スレッドを取得**
   - `SEARCH_QUERY` に指定したキーワード／ラベル等にマッチするスレッドのみを取得します。
2. **スレッド内のメールを順次処理**
   - 件名・本文を取得。
   - `clean()` 関数で余分な改行や空白を削除。
   - `sendMessage()` 関数に渡し、Discord へ投稿。
3. **ラベル付与**
   - 処理済みのスレッドに `BotRead` ラベルを付け、次回以降重複して送信しないようにする。

## コード解説

- **定数宣言**
  - `SEARCH_QUERY` … Gmail の検索クエリ文字列。
  - `DISCORD_WEBHOOK_URL` … Discord の Webhook URL。
  - `DISCORD_MAX_LENGTH` … Discord メッセージの最大文字数 (2000)。

- **`mailBotMain()`**
  - メイン関数。`GmailApp.search()` で検索したスレッドごとにメールを読み出し、件名と本文を取得 → 整形 → Discordへ送信 → ラベル付与、の流れを処理します。

- **`clean(text)`**
  - メール本文の不要な改行や空白を整形する。
  - `\r\n` を `\n` に統一し、行末の空白を削除。連続する改行を1行に減らすなどして見やすくします。

- **`sendMessage(text)`**
  - 将来的に複数送信先を管理したい場合の拡張ポイント。
  - ここでは `sendDiscord(text)` を呼び出しています。
  - LINE Notify を使いたい場合はここに `sendLine(text)` も追加します。

- **`sendDiscord(text)`**
  - Discord の Webhook へメッセージを送信する。
  - 1回の送信文字数制限が 2000 文字のため、`splitByLengthWithNewline()` でテキストを分割してから複数回送信します。

- **`splitByLengthWithNewline(text, maxLength)`**
  - Discord の文字数制限に対応するため、指定した長さを超えないように分割する。
  - できる限り改行で区切って読みやすさを保つ仕組みです。

## まとめ

- Google Apps Script を使うことで、Gmail の任意のメールを自動収集し、Discord (または LINE Notify) へ転送できます。
- **LINE Notify は終了予定**のため、今から導入するなら Discord Webhook がおすすめです。
- 大量のメールがある場合、初回実行は 6 分制限に注意が必要です。
- 定期トリガーを設定することで、常に新着メールを確認し、見逃しのない運用を実現します。

ぜひ活用して、メールを見逃さない環境を整えてみてください。