メールをLINEグループに転送するGoogle Apps Script（改良版）

## 背景

以前に[メーリングリストの投稿をLINEグループに転送するGoogle Apps Script(GAS)](https://qiita.com/hann-solo/items/5aae93dd9352c21e5a3f)について書いた。

特定のメールをLINEグループに転送できると（相変わらず）便利だし、知人からの問い合わせからもいただいている。ところが、スクリプトにバグがあって、期待通りに動いていなかったので修正した。

「GASで特定のメールを抽出して処理する」ことができれば、LINEグループへの転送だけじゃなくて、いろいろ応用範囲が広いとおもわれる。（他のメッセージアプリへの転送、スプレッドシートへの転記など）


## 環境

ここで書いていることは、下記のバージョンで実施しました。

- Gmail（フリー版）
- Safari バージョン14.1.2 (16611.3.10.1.3)
- macOS BigSur 11.5.1

## この記事の記述範囲

大まかな手順は下記の通り。

- LINE Notifyのトークンを取得する
- Google Apps Script(GAS)を調製する　★
- Google Apps Script(GAS)のトリガーを設定する
- LINEグループにLINE Notyfyを招待する

今回は★部分についての記事。その他の箇所に関しては[下記リンク](#過去記事へのリンク)ご参照。


## Google Apps Script（改良版）

```javascript

var lineToken = "xxxxxxxx";  // LINE Notifyのトークン
var intervalMinute = 5; //●分前～現在の新着メールを取得 #--トリガーの時間間隔をこれに合わせる

/**
 * 取得したメッセージをLINEに転送します
 */
function main() {
 
  // 転送するメッセージを取得
  var newMessages = fetchMail();
  
  // 取得したメッセージをLINEに転送
  newMessages.forEach( msg => {
    console.log("msg: " + msg);
    forwardLine(msg);
  })

}

/**
 * 転送するメッセージを取得します
 */
function fetchMail() {

  // 取得の基準時刻をセット
  var nowTime       = Math.floor( new Date().getTime() / 1000 );  // 現在時刻
  var referenceTime = nowTime - ( ( intervalMinute * 60 ) + 3 );  // 秒に変換し、さらに3秒前に

  // 検索条件を指定
  // NOTE: 「転送用」ラベルをGmailで指定しておくこと
  var strSearch = '(label:11_xxxx転送用 after:' + referenceTime + ')';
  
  // スレッドを検索して取得
  // NOTE: 念の為10スレッドに制限
  var arrThreads = GmailApp.search(strSearch, 0, 10);

  // スレッドからメッセージを取得
  // NOTE: 二次元配列で、[[スレッドのインデックス][メッセージのインデックス]]で参照できる
  var arrMsgs = GmailApp.getMessagesForThreads(arrThreads);

  // ①基準時刻より後で、②エラーメッセージではないもの（正常メール）を抽出し配列に格納
  var newMsgs = [];

  arrMsgs.forEach( thread => {
    thread.forEach( msg => {

      // ①秒換算し、基準時刻より後かを判定（boolean）
      var isAfter = ( (msg.getDate() / 1000 - referenceTime ) > 0 );

      // ②正常かを判定（boolean）
      var isFine  = msg.getSubject().startsWith('[');

      // log for debug //
      // console.log('isAfter: ' + isAfter);
      // console.log('isFine:  ' + isFine);
      // console.log('msg.getDate(): ' + msg.getDate());
      // console.log('msg.getSubject(): ' + msg.getSubject());

      if (isAfter && isFine) {
        // メッセージ文字列を生成
        var strMsg = " "
          + Utilities.formatDate(msg.getDate() , 'Asia/Tokyo', 'MM/dd HH:mm')
          + "\n\n[送信者]" + msg.getFrom()
          + "\n\n[件名]"   + msg.getSubject()
          + "\n\n[本文]\n" + msg.getPlainBody();
        // 配列に追加
        newMsgs.push(strMsg);
      }

    })
  })

 return newMsgs;
}


/**
 * LINEにメッセージを転送します
 * @param {String} Me メッセージの内容
 */
function forwardLine(Me) {

  var payload = {'message' : Me};
  var options = {
    "method"  : "post",
    "payload" : payload,
    "headers" : {"Authorization" : "Bearer "+ lineToken}  
  };

  UrlFetchApp.fetch("https://notify-api.line.me/api/notify", options);

}


```

ほとんどコメントに書いてあるが、若干補足。

### 基準時刻のセット

```javascript
// 取得の基準時刻をセット
var nowTime       = Math.floor( new Date().getTime() / 1000 );  // 現在時刻
var referenceTime = nowTime - ( ( intervalMinute * 60 ) + 3 );  // 秒に変換し、さらに3秒前に
```

[Google Apps ScriptでGmailの情報を取得する際に、受信日時を細かく指定する方法 - Qiita
](https://qiita.com/3mc/items/39b2c8241c6b52811ad2)にあるとおり、

- 日時でなく、秒（UNIX時間）で検索条件を設定できる
- `getTime()`はミリ秒を返すので、検索のために秒に変換（÷1000）する

ただし、UNIX時間をつかっても、メールの検索はできない（スレッドの検索のみ可能）ので、スレッドに含まれる過去メールを引っ張ってきてしまう。

また、`+3秒`というのは適当で、GASの実行時間を考慮し、メールの取りこぼしを防ぐため。

```javascript
// ①秒換算し、基準時刻より後かを判定（boolean）
var isAfter = ( (msg.getDate() / 1000 - referenceTime ) > 0 );
```

ここも考えかたはおなじで、個々のメッセージの日時がミリ秒で取れるので、秒に変換したのち、基準時刻と比較。

### スレッドの検索

```javascript
// スレッドを検索して取得
// NOTE: 念の為10スレッドに制限
var arrThreads = GmailApp.search(strSearch, 0, 10);
```

- `GmailApp.search(query, start, max)`は
    - スレッドを対象に検索（メールを対象に検索はできない）
    - startは開始番号
    - maxは取得スレッドの最大数
    - 返り値は`GmailThread[]`（スレッドの1次元配列）

取得スレッドが大きくなると、実行時間制限にひっかかる。いまの使い方で取得スレッドが多くなることは有り得ないが、万が一の場合のリソース消費を抑えるために最大数を記述しておいた。

## 改良点

[過去記事](#過去記事へのリンク)のスクリプトとの違いは下記。

- 転送するメールの件数
  - Before: Gmailのスレッドごとに、いちばん新しいメール１通を転送
  - After: Gmailのスレッドに含まれるメールのうち、指定時刻より後のメールをすべて転送
- 転送するメールの条件
    - Before: エラーメールであっても転送する
    - After: エラーメールは転送しない
- リファクタリング
    - 変数名の明瞭化、forループを`forEach`に変えて見通しを良くした

### （参考）Gmailのしくみの前提

Gmailの構成は、`GmailApp > スレッド > メッセージ（メール）`になっている。スレッドに紐付かないメッセージは存在しない。

- 検索は、スレッドに対して行われる（メッセージに対する検索はGASにない）
- ラベルは、スレッドに対してつけられる（メッセージに対してではない）
- 設定でスレッド表示のON/OFFができるが、表示を変えられるだけで、内部機能としてはスレッドが生きている

この前提を理解するのにいちばん時間がかかった。

### （参考）過去記事のGASのバグの現象と原因

現象

- メーリングリストでエラーメッセージが返ってくると、LINEグループにはエラーメッセージしか転送されない

原因

- エラーメッセージは数秒で返ってくる
- GASは5分おきにcronしている
- スクリプトが「5分以内に届いたスレッドのうち、最新のメッセージ１通を転送せよ」という記述になっていた

よって、エラーメッセージが返ってこない場合は、期待通りにメッセージがLINEに転送されるが、エラーメッセージが返ってくると、エラーメッセージだけLINEに転送されていた。

また、続けてメーリングリストへの投稿があると（めったにないが）、最新の１通のみ転送されていた。

## リンク

### 過去記事へのリンク

- [メールをラインに転送する方法! iPhone/AndroidのGmailを自動でLINEに通知しよう - あっとはっく](https://sp7pc.com/gadget/line/23926)
- [やってみた｜メーリングリストに届いた投稿をLINEに転送する - Qiita](https://qiita.com/hann-solo/items/5aae93dd9352c21e5a3f)

### その他リンク

- 公式リファレンス　⇒　[Gmail Service - Google Apps Script](https://developers.google.com/apps-script/reference/gmail)
- [Google Apps ScriptでGmailの情報を取得する際に、受信日時を細かく指定する方法 - Qiita](https://qiita.com/3mc/items/39b2c8241c6b52811ad2)
- [Gmail非公開テクニック：時間帯を指定してメール検索 - ライフハッカー](https://www.lifehacker.jp/2016/12/161221_gmail_timesearch.html)