GmailをLINEに即時転送したい

子供の「習い事終わったよ」連絡がGmailに通知されるんですが、
頻繁に利用しているLINEで通知してほしい。
そんなわがままをかなえるための自作GASを作ったので、紹介します。

# 前置き：仕組みの模索
## GASの周期トリガーで頑張る案
「Gmail LINE 転送」でググるとまず最初に登場するのは、GASの周期トリガーでLINE Notifyをキックする方法。
例：[GmailをLINEに転送！3ステップで解説｜メールの確認が多い経営者におすすめ！](https://note.com/pasokonta/n/n763e0bce3167)

ただこの仕組みにはやや難点がありました。
それは、転送のトリガーが、最短1分周期でしか設定できないこと。

うちの子供はまだ小さいので、「習い事終わったよ」連絡が来たら、なるはやで迎えに行く必要があります。
最大1分待った場合、子供は教室から外に飛び出す可能性があるからです。
だから即時通知が必要で、この案は不採用にしました。

また、試しに1分周期で動かしてみたところ、頻度高すぎ！と、エラーになったので、
やはりこの案は、私の利用用途には合わないようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922520/ad89b068-6360-6826-8e04-f1eb22ce4734.png)

## Gmail転送とMailgunで即時動作させる案
というわけで、周期トリガーではなく、PUSHで動く仕組みを探しました。
[GmailからLINEにメッセージを「すぐに」届けるための技術](https://tomorrowkey.hatenablog.jp/entry/2023/12/12/Gmail%E3%81%8B%E3%82%89LINE%E3%81%AB%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%82%92%E3%80%8C%E3%81%99%E3%81%90%E3%81%AB%E3%80%8D%E5%B1%8A%E3%81%91%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E6%8A%80)

基本的にはこの仕組みでうまくいくんですが、
システムの作成途中で不穏なメッセージが表示されていることに気が付きました。
[LINE Notify提供終了のお知らせ](https://notify-bot.line.me/closing-announce)

作成してすぐにシステムが機能しなくなっては困るので、
LINEが後継としておすすめしているMessaging APIを使った構成に変更しました。

# 作成するシステムの全体像
構成としてはシンプル。
![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922520/08135fcd-a09a-7fe4-2d3e-3fd36e08709a.png)

ここからは細かな設定や実装内容を紹介します。

依存関係の都合で、呼び出し関係の後ろの方から順に作成します。
全体の流れはこちら。
1. Messsaging APIの設定
1. GASのスクリプトを作成
1. Mailgunの設定
1. Gmailで転送を設定

# 作成手順
## Messsaging APIの設定
### LINE公式チャネルを作成する
Messaging APIはLINE公式チャネルでメッセージを送信するためのAPI。
そのため、まずは公式チャネルを作成する必要があります。

LINEの[Messaging APIサイト](https://developers.line.biz/ja/services/messaging-api/)の「今すぐはじめよう」を開くと、
LINEビジネスIDでログインするよう求められます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922520/fb6a0d96-8a34-faa1-5dc3-a80460594fec.png)

LINEビジネスIDがなくても、「アカウントを作成」に進めば、
普段利用しているLINEアカウントを使って、LINEビジネスIDなるものが作成できます。

アカウントを作成し、ログイン出来たら、右上にあるメニューからコンソールを開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922520/f6ae3fb3-2bbb-a635-9ae8-cf2026abcd7f.png)

次に、プロバイダーを作成します。
プロバイダー名は、システム利用上のどこにも表示されない様子なので、好きな名前を付ければよさそう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922520/2a20c8b8-2af5-ef50-ff25-cccb0d51acc4.png)

プロバイダーが作成できたら、次は公式チャネルを作成します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922520/55e27b88-83a1-887c-4ca5-261ed872b1be.png)

作成するチャネルの種類は、Messaging APIとします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922520/291a96b6-79e1-9513-edae-f02b64decfbb.png)

あとは指示に従って必要事項を入力しつつ、チャネルを作成すればOK。

### LINE Messaging APIを有効にする
[LINE Developersコンソール](https://developers.line.biz/console/)を開き、
これまでに作成したLINE ビジネスIDでログインします。

作成した公式チャネルの[Messaging API設定]タブを開くと、
チャネル参加用のQRコードが表示されるので、友だち追加します。
※友だち追加しておかないと、公式チャネルからのメッセージが受信できません
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922520/03c47906-4f19-76b6-89af-f4770637b5b7.png)

さらに、[Messaging API設定]タブの下の方にある、
チャネルアクセストークンを発行します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922520/bd11e69f-38ee-a173-714a-956b2053eea1.png)

LINE公式チャネルに関する設定は以上となります。

しかし、GASからLINE公式チャネルをつかってLINEグループにメッセージを送信する場合、
さらにLINEグループのグループIDが必要になります。
[【GAS】LINEのgroupIDを返してくれるbot](https://qiita.com/enbanbunbun123/items/2504687e4b6c13a289db)などを参考に、
次のGAS実装より前に、グループIDを取得しておいてください。

## GASのスクリプトを作成
### GASの転送スクリプトを実装する
単にメールを転送するだけなら以下プログラムをGASに実装してください。
GASのファイル名は任意です。わかりやすい名前を付けてあげてください。

```js
// 本体処理
function doPost(event) {
  Logger.log(event); // 受信したデータをログに出力

  // デバッグ実行の場合はeventがnullになるため、その時点で終了する
  if (!event) {
    return;
  }

  const groupId = 'LINE_GROUP_ID'; // LINEのグループID
  const token = 'LINE_CHANNEL_ACCESS_TOKEKN'; // LINEのチャンネルアクセストークン

  const mailBody = event.parameters['body-plain'] ? event.parameters['body-plain'][0].trim() : 'No email body received';

  // LINEメッセージ送信処理
  sendLineMessage(groupId, token, mailBody);
  }
}

// LINEメッセージ送信処理
function sendLineMessage(groupId, token, messageText) {
  const url = 'https://api.line.me/v2/bot/message/push';
  const options = {
    method: 'post',
    contentType: 'application/json',
    headers: {
      Authorization: `Bearer ${token}`
    },
    payload: JSON.stringify({
      to: groupId,
      messages: [{ type: 'text', text: messageText }]
    })
  };

  try {
    const response = UrlFetchApp.fetch(url, options);
    Logger.log(response.getContentText());
  } catch (error) {
    Logger.log('Error: ' + error.message);
  }
}
```

我が家の場合は、習い事+αのメールごとに、必要最低限の情報のみに絞ってLINEに通知したかったので、
送信元のメールアドレスを取得し、かつ、それごとにメッセージ文を加工する処理を入れました。
ついでにメールの送信元がわかるような接頭辞も追加して送信するようにしました。
興味がない人は、ここはスルーしてください。

```js
// 事前の設定
const emailSettings = {
  'aaa@mail.jp': {
    handler: handleAAAMail,  // handleAAAMailはメッセージを加工する関数
    prefix: '[aaa]',
  },
  'bbb@mail.jp': {
    handler: handleBBBMail,
    prefix: '[bbb]',
  },
};
// デフォルトの設定
const defaultSettings = {
  handler: (body) => body,  // そのまま返す
  prefix: '[General]',
};

// 本体処理
function doPost(event) {
  //
  // はじめは同じなので省略
  //
  
  const mailBody = event.parameters['body-plain'] ? event.parameters['body-plain'][0].trim() : 'No email body received';
  const fromField = event.parameters['from'] ? event.parameters['from'][0] : 'Unknown sender';
  
  // メールアドレスを抽出するための正規表現
  const emailMatch = fromField.match(/<([^>]+)>/);
  const fromEmail = emailMatch ? emailMatch[1] : fromField.replace(/.*<|>.*/g, '').trim(); // メールアドレス部分を取得

  // 当該メールの設定を取得
  const settings = emailSettings[fromEmail] || defaultSettings;

  // メールごとに設定された関数を使って、LINEに送信するメッセージ文を作成
  let messageText = settings.handler(mailBody);
  messageText = `${settings.prefix}\n${messageText}`;
  messageText = messageText.replace(/\n+$/, '');  // 末尾の空行を削除

  // LINEメッセージ送信処理
  sendLineMessage(groupId, token, messageText);
  }
}

function handleAAAMail(body) {
  // 入退室時刻以外の情報は不要なので、削除する
  return body.replace(/○○教室　○○先生$/, '').trim();
}

function handleBBBMail(body) {
  // そのまま転送すればよいため、特に加工しない
  return body;
}
```

### 実装したスクリプトをデプロイする
実際に動作させるためには、デプロイする必要があります。
右上の「デプロイ」ボタンから、新しいデプロイから、デプロイの設定をします。

この時、MailgunからHTTPリクエストを受け付ける必要があるので、
デプロイタイプ(種類の選択)では、「ウェブアプリ」を指定してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922520/30bbff5f-c3c7-7d7f-f072-ff2138ceeb02.png)
また、Mailgunは認証なしで動くので、アクセスできるユーザーは「全員」にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922520/5aa9d6db-ef77-5875-1c39-f3788a5b927a.png)
設定が完了したらデプロイします。

なお、初回デプロイ時には、「承認が必要」といった画面が表示されます。
今回のように、外部サービスを利用する場合には表示されるメッセージなようです。
焦らず「権限を確認」しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922520/e64fb003-3b44-df4b-c54f-e4dc463edab9.png)
やや物騒なメッセージが表示されますが、
「詳細」を開いて許可ボタンを押してあげてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922520/1ed25d28-23ed-7851-8374-1f478e3fedb0.png)

GASのデプロイが完了すると、ウェブアプリのURLが発行されるので、これを取得しておきます。
GASの実装は以上となります。

## Mailgunの設定
### Mailgunのアカウントを作成し、ログインする
詳しい説明は省略しますが、
[Mailgun](https://www.mailgun.com/)からアカウントを作成し、ログインします。

### メール受信時にHTTPリクエストする設定をする
メニューからReceivingを開き、「Create route」を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922520/6608c19b-c0ac-bc6b-0eae-b8af9683e8e7.png)

Expression typeを「Catch All」に、
Forwardを有効にして、GASのウェブアプリURLを指定します。
設定が完了したら「Create route」します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922520/114a66e5-bffa-af8e-e3e2-0d134f8e34a0.png)

次に、Gmailのメール転送に使うメールアドレスを取得します。
[Seiding] - [Domain settings] - [Add new SMTP user]を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922520/78d0848f-d228-989b-4afb-bcd1f9702672.png)
メールアドレスに使う文字列を指定し、Createします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922520/ce151fa4-4919-9971-8169-ffe453e2118e.png)

作成したメールアドレスは、次のGmailの転送設定に使うので、取得しておきます。

## Gmailで転送を設定

ようやくGmailまでたどり着きましたね。
Gmailを開いたら、右上の設定ボタンから「すべての設定を表示」を開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922520/e15dd496-af61-274d-1ace-877ecbf1843c.png)

[メール転送とPOP/IMAP] - [転送先アドレスを追加]から、
先ほど取得したMailgunのメールアドレスを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922520/bad67ddd-32ec-6a72-9cc8-b6f7e4cbf287.png)

すべてのメールを転送する場合は、ここで転送するのラジオボタンを選択します。
特定のアドレスから送られてきたメールのみ転送する場合は、[フィルタとブロック中のアドレス]から転送する送信元を指定し、転送の設定をします。

以上ですべての設定が完了です。
正しく動作するか、デバッグしてくださいね。


# 追加で発生した課題：LINEメッセージ上限に引っかかる
冒頭に記述した習い事+αを通知するよう実装しましたが、
我が家では、子供が2人いて、子供が1人のご家庭と比べ、単純計算で2倍のメッセージが通知されます。

LINE公式チャネルの無料で送信できるメッセージ数は200件/月で、ぎりぎりアウト。
なんらかメッセージ数を削減する措置が必要となりました。
もともと子供2人はほぼ一緒に行動していて、
メッセージもほぼ同時に2件ずつ送られてきている状況だったので、1件に統合します。

ざっくりいうと、排他制御と永続化のサービスを利用して、
指定時間内に来たメールを1通にまとめて送信するように変更します。

### 変更内容：GAS

メールの送信元によって、すぐに通知してほしい場合と、そうでない場合があったので、
メールごとの設定に、待機時間の設定を追加しました。

また、排他制御や永続化のサービスはいくつかありますが、
今回はそれぞれ、スクリプトロック、PropertiesServiceを使いました。
PropertiesServiceを触る処理には、LockServiceを使って排他制御します。

そして、メッセージをPropertiesServiceに入れてから取り出すまでの間に、sleepを入れることで、
この間に受信したメールは1件にまとめられるという、仕組みです。

```js
// 事前の設定
const emailSettings = {
  'aaa@mail.jp': {
    waitTime: 5000, // 待機時間はミリ秒指定
    handler: handleAAAMail,  // handleAAAMailはメッセージを加工する関数
    prefix: '[aaa]',
  },
  'bbb@mail.jp': {
  　waitTime: 10000,
    handler: handleBBBMail,
    prefix: '[bbb]',
  },
};
// デフォルトの設定
const defaultSettings = {
  waitTime: 10000,
  handler: (body) => body,  // そのまま返す
  prefix: '[General]',
};

// メール文の一時保管領域
const messageStorage = PropertiesService.getScriptProperties();

function doPost(event) {
  // 略

  // メールごとに設定された関数を使って、LINEに送信するメッセージ文を作成
  let messageText = settings.handler(mailBody);
  messageText = `${settings.prefix}\n${messageText}`;
  messageText = messageText.replace(/\n+$/, '');  // 末尾の空行を削除

  // 最大20秒待機する
  const lock = LockService.getScriptLock();
  const lockSuccess = lock.tryLock(20000);
  if (!lockSuccess) {
    Logger.log(`排他エラー:lockSuccess`);
    return;    
  } else {
    const existingMessage = messageStorage.getProperty(fromEmail) || '';
    const updatedMessage = existingMessage ? `${existingMessage}\n\n${messageText}` : messageText;
    messageStorage.setProperty(fromEmail, updatedMessage.trim());
    lock.releaseLock();
  }

  Utilities.sleep(settings.waitTime); // 指定した送信間隔が空くまで待機する

  const relock = LockService.getScriptLock();  
  const relockSuccess = relock.tryLock(20000);
  if (!relockSuccess) {
    Logger.log(`排他エラー:relockSuccess`);
  } else {
    const finalMessage = messageStorage.getProperty(fromEmail);
    if (finalMessage) {
      // LINEメッセージ送信処理
      sendLineMessage(groupId, token, finalMessage);

      messageStorage.deleteProperty(fromEmail); // 送信後はメール文の一時保管領域をクリアする
    }
    relock.releaseLock();
  }
}
```

# まとめ

本記事では、Gmailに送信されたメールをLINEグループに転送するシステムを構築しました。
また、通知量が多いと、LINE公式チャネルの無料で送信できるメッセージ数の上限に引っかかってしまうため、メッセージ数を削減するための仕組みも入れました。

普段はGASや排他制御とは縁遠い世界の住人なので、なかなかて個づりました。
それに、もっと美しい実装にできる気がしますが、
狙った通りの動作を実装できたので、個人的には満足です。

# おわりに

最後まで読んでいただき、ありがとうございます。

文末で恐縮ですが、
本文中に引用している参考資料は大変参考になりました。
作成してくださった方々には、感謝申し上げます。