（市民吹奏楽団向け）freemlの代替手段をGASとLineNotifyで構築した話

## はじめに
市民吹奏楽団に所属するみなさんにとって、2019年にひとつの大事件が起きたと思います。
**freemlのサービス終了**
大手メーリングリストサービスの終了です。

その代替として、以下を検討した団体も多いかと思います。

- Slack
- Googleグループ

Slackは技術者ではない人には馴染みが薄く、
Googleグループは管理が面倒くさい上にメールが届かない人もいる…
（freemlもそうでしたが）

私の所属する団体でも同様の問題に直面しており、
メールの届かない人にはLINE等で個別に連絡する事態になっていました。
そこで、通知をメールに頼らない、**Gmailに送ったメールをLINEに通知する**という仕組みを構築しました。

なお、本記事は市民吹奏楽団向けと記載していますが、
要件さえ合えばサークルや部活にも使えるのではないでしょうか。

## 要件
本仕組みは、以下を必須の要件として考えています。
### Gmailの宛先を解析してLINEグループに通知する
連絡用メールアドレスを作成し、そこに送られた内容をLINEグループに通知します。
通知は個人宛てではなく、LINEグループという制限があります。

### 複数の宛先に対応する
市民吹奏楽団には「係」が存在しているかと思います。
お金を管理する「会計係」や演奏会の演出を考える「企画係」など。
それぞれの係ごとに対応できるようにしています。
LINEグループさえあれば、パートでの対応も可能です。
つまり、

- 団全体向けの通知
- 会計係向けの通知
- 企画係向けの通知
- トランペットパート向けの通知

等々と振り分けを行います。
また、通知の宛先毎にLINEグループが必要です。

### LINEグループに入っていない人からもメールが送れる
全員に連絡するだけであれば、LINEのトークで十分です。
**係に所属していない人でも、その係に連絡が取れる**
ことを目的としています。
例えば、「この楽器を買いたいんだけど、承認お願いします」と会計係に連絡するような場合です。
ここが他の代替サービスでなかなか実現できなかったことです。

### 定期的な自動実行
1分周期でGmailを確認し、メールがあればLINEグループに通知するようにします。
メールがなければなにもせずに終了します。

## 使用するサービス
本仕組みでは、以下のサービスを組み合わせています。
### LINE Notify
LINEが提供している、外部サービスとの連携機能です。
提供されているAPIを使用して後述のGASからLINEに通知します。
[LINE Notify](https://notify-bot.line.me/ja/)

### Gmail
ユーザ名の後ろに「+」と任意の文字を付与することで複数アドレスとして扱える仕様を活用します。
例えば以下のような（自動リンク回避しています。アドレスは架空です。）。
sample@ gmail.com（元々のアドレス）
sample+all@ gmail.com
sample+kaikei@ gmail.com
これらは全て「sample@ gmail.com」に届きますが、「宛先」に設定されたアドレスが異なります。
この「+」以降に文字を付与することで複数の宛先を実現しています。
詳しくは以下の記事がわかりやすいです。
https://www.howtonote.jp/gmail/sent/index18.html

### GoogleAppsScript（GAS）
Googleのサービスとの連携に特化しているスクリプトです。
Web上で作成するため、開発環境を整える必要がないという特徴があります。
今回はその中から、Gmailに関する機能を活用していきます。
詳しくは以下の記事がわかりやすいです。
[超初心者へGoogleAppsScriptを始めるメリットをこれでもかと説明します](https://tonari-it.com/google-apps-script-start/)

これらのサービスを組み合わせ、
**Gmailに送られたメールをGASで宛先の解析を行い、LINE Notifyを使用して対応するLINEグループに通知する**
仕組みを作成していきます。

![スライド1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569369/30745dc8-807a-5b7e-2c83-02bf9c34b2cb.png)

## 準備
### LINE Notify
まずはLINEを通知するグループを作りましょう。すでに存在しているグループでも問題ないです。
この仕組を作成する人が一旦グループに入る必要がありますが、あとで抜けても大丈夫です。

そしてLINEグループに対応するLINE Notifyの登録をしましょう。
登録方法は以下のページを参照ください。非常にわかりやすいです。
[[超簡単]LINE notify を使ってみる](https://qiita.com/iitenkida7/items/576a8226ba6584864d95)

**グループLINEに「LINE Notify」を招待する**ところまでできればOKです
作成したトークンは忘れないようにメモしておきましょう。

### Gmail
専用のGoogleアカウントを用意しましょう。
また、複数アカウントにログインした状態だとGASが操作できない場合がありますので、
一旦他のアカウントからはログアウトしてください。
本仕組みが出来上がった後は個人アカウントにのみログインした状態で問題ないです。

###GAS
上記で作成したGoogleアカウントにログインし、Googleドライブから任意の場所でスクリプトを作成していきます。
「新規」ボタンから「その他」→「Google Apps Script」を選択します。
![20200202_134448.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569369/b0e065c9-da0f-8931-b174-7aa7ce354847.jpeg)

以下のような画面が表示されたかと思います。
![20200202_134701.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569369/be6214ad-55f5-1eba-7979-9e86ebee98f7.jpeg)

名前もそれらしいものに変更しましょう。
「Rename」から可能です。今回は「sendLine」として作成しています。
![20200202_134837.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569369/828845c5-3bce-f88a-0cb0-b6366d6e304b.jpeg)

ではソースコードを書いていきましょう。

## ソースコード
それではスクリプトのソースコードになります。
まずは全体を。

```javascript:sendLine.gs
function splitAddress(address_all){
  var address = address_all.split(",");
  for(var i = 0; i < address.length; i++){
    address[i] = getAddress(address[i]);
  }

  return address;
}

function getAddress(address){
  var match_address = address.match(/[\w\-\.\+\_]+@[\w\-\.]+\.[a-zA-Z]+/);
  /* メールアドレスではない文字の場合は空白にする（後続処理にて送信しない） */
  if(null === match_address){
    match_address = " ";
  }
  var trim_address = match_address[0].trim().toLowerCase();

  return trim_address;
}

function getToken(address){
  var token;
  var pr_adr_all = PropertiesService.getScriptProperties().getProperty('ADR_ALL');
  var pr_adr_kaikei = PropertiesService.getScriptProperties().getProperty('ADR_KAIKEI');
  var pr_adr_kikaku = PropertiesService.getScriptProperties().getProperty('ADR_KIKAKU');
  
  switch (address){
    case pr_adr_all:
      token = PropertiesService.getScriptProperties().getProperty('TOKEN_ALL');
      break;
    case pr_adr_kaikei:
      token = PropertiesService.getScriptProperties().getProperty('TOKEN_KAIKEI');
      break;
    case pr_adr_kikaku:
      token = PropertiesService.getScriptProperties().getProperty('TOKEN_KIKAKU');
      break;
    default:
      token = "none";
      break;
  }

  return token;
}

function sendLine(address,message){
  for(var i = 0; i<address.length; i++){
    var token = getToken(address[i]);
    /* 対象外のアドレスの場合はスキップ */
    if("none" == token){
      continue;
    }
    
    var subject = message[0].getSubject();
    var from = message[0].getFrom();
    from = getAddress(from);
    var body = message[0].getPlainBody();

    /* 通知本文作成 */
    var content = "";
    content += "件名："+ subject + "\n";
    content += "From：" + from + "\n";
    content += "--------\n";
    content += body;
        
    /* 送信処理 */
    var options = 
        {
          "method" : "post",
          "payload" : "message=" + content,
          "headers" : {"Authorization" : "Bearer "+ token}
    };
    var ret = UrlFetchApp.fetch("https://notify-api.line.me/api/notify",options);
  }
}

function main() {
  var threads = GmailApp.search("is:unread label:inbox");
  var messages = GmailApp.getMessagesForThreads(threads);

  for(var i = messages.length - 1; 0 <= i; i--){
    /* Toの処理 */
    var to_all = messages[i][0].getTo();
    if(0 != to_all.length){
      var to_array = splitAddress(to_all);
      sendLine(to_array,messages[i]);
    }
    
    /* Ccの処理 */
    var cc_all = messages[i][0].getCc();
    if(0 != cc_all.length){
      var cc_array = splitAddress(cc_all);
      sendLine(cc_array,messages[i]);
    }

    /* Bccの処理 */
    var bcc_all = messages[i][0].getBcc();
    if(0 != bcc_all.length){
      var bcc_array = splitAddress(bcc_all);
      sendLine(bcc_array,messages[i]);
    }
  }
  
  /* 既読にする */
  for(var i = 0; i < threads.length; i++) {
    threads[i].markRead();
  }
  return;
}
```

関数ごとに個別に解説していきます。

### メイン関数(main)
フローは以下の通りな単純なものです。
![スライド2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569369/d777d616-eb83-1f7c-7154-a4dc41020c4e.png)

<details><summary>コード（再掲）</summary><div>

```javascript

function main() {
  var threads = GmailApp.search("is:unread label:inbox");
  var messages = GmailApp.getMessagesForThreads(threads);

  for(var i = messages.length - 1; 0 <= i; i--){
    /* Toの処理 */
    var to_all = messages[i][0].getTo();
    if(0 != to_all.length){
      var to_array = splitAddress(to_all);
      sendLine(to_array,messages[i]);
    }
    
    /* Ccの処理 */
    var cc_all = messages[i][0].getCc();
    if(0 != cc_all.length){
      var cc_array = splitAddress(cc_all);
      sendLine(cc_array,messages[i]);
    }

    /* Bccの処理 */
    var bcc_all = messages[i][0].getBcc();
    if(0 != bcc_all.length){
      var bcc_array = splitAddress(bcc_all);
      sendLine(bcc_array,messages[i]);
    }
  }
  
  /* 既読にする */
  for(var i = 0; i < threads.length; i++) {
    threads[i].markRead();
  }
  return;
}
```
</div></details>

```javascript
var threads = GmailApp.search("is:unread label:inbox");
var messages = GmailApp.getMessagesForThreads(threads);
```


Gmailから、未読のスレッドを取得します。
そして、取得したスレッドのかたまりからメールを取得します。
取得したメールは二次元配列に格納されるため、以降の処理ではスレッド先頭のメールを以下の記述で取得しています。
```messages[i][0]```
（取得したスレッドi番目のうち、先頭のメール）

```javascript
var to_all = messages[i][0].getTo();
if(0 != to_all.length){
  var to_array = splitAddress(to_all);
  sendLine(to_array,messages[i]);
}
```

メールからToに設定されているアドレスを取得します。
複数設定されていた場合は、カンマ区切りで取得されますので、
後述の処理で配列に格納し直します。

同様の処理をCcとBccに対しても実施します。

```javascript
  /* 既読にする */
  for(var i = 0; i < threads.length; i++) {
    threads[i].markRead();
  }
```

メールの処理が終わったら、次回実行時に再送信しないように既読にしてしまいます。

### 宛先メールアドレスの分割関数(splitAddress、getAddress)
<details><summary>コード（再掲）</summary><div>

```javascript
function splitAddress(address_all){
  var address = address_all.split(",");
  for(var i = 0; i < address.length; i++){
    address[i] = getAddress(address[i]);
  }

  return address;
}

function getAddress(address){
  var match_address = address.match(/[\w\-\.\+\_]+@[\w\-\.]+\.[a-zA-Z]+/);
  /* メールアドレスではない文字の場合は空白にする（後続処理にて送信しない） */
  if(null === match_address){
    match_address = " ";
  }
  var trim_address = match_address[0].trim().toLowerCase();

  return trim_address;
}
```
</div></details>

`split(",")`でメールアドレスを配列に格納しなおします。
ただし、`getTo`（`getCc`、`getBcc`）は、送信者のアドレス帳での登録名を含んでいます。
例：`"sample+kaikei@gmail.com" <会計用アドレス>`
そのため、`match`を使用してメールアドレスを抜き出します。
また、後続処理でアドレスの解析をしやすくするために`toLowerCase`で大文字小文字を統一します。

### LINEへの通知関数（sendLine）

<details><summary>コード（再掲）</summary><div>

```javascript
function sendLine(address,message){
  for(var i = 0; i<address.length; i++){
    var token = getToken(address[i]);
    /* 対象外のアドレスの場合はスキップ */
    if("none" == token){
      continue;
    }

    var subject = message[0].getSubject();
    var from = message[0].getFrom();
    from = getAddress(from);
    var body = message[0].getPlainBody();

    /* 通知本文作成 */
    var content = "";
    content += "件名："+ subject + "\n";
    content += "From：" + from + "\n";
    content += "--------\n";
    content += body;

    /* 送信処理 */
    var options = 
        {
          "method" : "post",
          "payload" : "message=" + content,
          "headers" : {"Authorization" : "Bearer "+ token}
    };
    var ret = UrlFetchApp.fetch("https://notify-api.line.me/api/notify",options);
  }
}
```
</div></details>

LINE NotifyのAPIを使用する際には宛先に対応するトークンが必要なので、
宛先に応じたトークンを取得します。
メールの宛先に無関係のアドレスが含まれる場合もあるので、その場合は`continue`で飛ばします。

その後は送信する内容を作成していくだけです。
`getSubject` 件名を取得。
`getFrom` 送信元を取得。宛先と同様にアドレスのみを切り出します。
`getPlainBody` 本文を取得。LINEでは装飾ができないため、HTMLタグを取り除いた本文を取得します。

その他に取得できる情報はリファレンスを参照ください。
[リファレンス](https://developers.google.com/apps-script/reference/gmail/gmail-message)

```javascript
    var options = 
        {
          "method" : "post",
          "payload" : "message=" + content,
          "headers" : {"Authorization" : "Bearer "+ token}
    };
    var ret = UrlFetchApp.fetch("https://notify-api.line.me/api/notify",options);
```

作成した内容や取得したトークンをセットしてAPIを実行します。

###トークン取得処理（getToken）

<details><summary>コード（再掲）</summary><div>

```javascript
function getToken(address){
  var token;
  var pr_adr_all = PropertiesService.getScriptProperties().getProperty('ADR_ALL');
  var pr_adr_kaikei = PropertiesService.getScriptProperties().getProperty('ADR_KAIKEI');
  var pr_adr_kikaku = PropertiesService.getScriptProperties().getProperty('ADR_KIKAKU');
  
  switch (address){
    case pr_adr_all:
      token = PropertiesService.getScriptProperties().getProperty('TOKEN_ALL');
      break;
    case pr_adr_kaikei:
      token = PropertiesService.getScriptProperties().getProperty('TOKEN_KAIKEI');
      break;
    case pr_adr_kikaku:
      token = PropertiesService.getScriptProperties().getProperty('TOKEN_KIKAKU');
      break;
    default:
      token = "none";
      break;
  }

  return token;
}
```
</div></details>

メールアドレス（sample+xxxxx@ gmail.com）に対応したLine Notifyのトークンを返します。
メールアドレスやトークンはソースコードにベタ書きしたくないですよね。
その場合は**スクリプトプロパティ**に登録しましょう。

「ファイル」から「プロジェクトのプロパティ」を選択します。
![20200202_144845.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569369/f59d9cad-dea8-0041-4848-19837a04c9e8.jpeg)
「スクリプトのプロパティ」タブで登録していきます。
「プロパティ」がソースコードで使用する名前、
「値」が実際の値です。
今回はメールアドレスやLINE Notifyで作成したトークンを登録していきましょう。
![20200202_145343.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569369/da55ae89-1489-0f89-2a5e-e47db370209e.jpeg)

登録したプロパティはソースコード上で
```PropertiesService.getScriptProperties().getProperty('プロパティ名');```
で使用することができます。

## 実行
正しく動作するか実行してみましょう。
GASを手動で実行する場合は、
実行する関数を選択して実行ボタンを押します。
![20200202_145624.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569369/843c1248-527b-67f8-332c-65c6c7f8ebec.jpeg)
![20200202_145639.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569369/8b89f92e-e88d-3cb3-5ff4-4e7585619731.jpeg)



初回の実行時、以下のダイアログが出ると思います。
Googleから許可を求められているものですので、許可しましょう。
![20200202_145707.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569369/ab733476-595b-98bf-eb52-8dcbe802def2.jpeg)
![20200202_145839.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569369/cd58761a-9475-06c9-47cd-bdcc703823b4.jpeg)
![20200202_145856.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569369/716d57b0-dd52-1868-df2a-26ccc0f99123.jpeg)
![20200202_145932.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569369/2f28b275-c0b4-6220-28b4-d2fa7121fe9c.jpeg)

## 定期的な自動実行
ここまでで、Gmailを解析してLINEに送信することが**手動で**できました。
あとは**自動化**です。
「編集」→「現在のプロジェクトのトリガー」を選択します。
![20200202_150047.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569369/02d3a07a-bc39-989a-debf-021e8092734f.jpeg)

「自分のプロジェクト」から作成したスクリプトの「トリガー」を選択します。
![20200202_150652.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569369/420a1aab-05a0-821f-f3bc-7a70abeb5cc5.jpeg)

「トリガーを追加」を選択します。
トリガーを設定します。本仕組みでは以下を設定しています。

|実行する関数を選択|実行するデプロイを選択|イベントのソースを選択|時間ベースのトリガーのタイプを選択|時間の間隔を選択（分）|
|:-:|:-:|:-:|:-:|:-:|
|main|Head|時間主導型|分ベースのタイマー|1分おき|

これで、1分おきにスクリプトを自動で実行できます。

以上で「Gmailに送ったメールをLINEに通知する」仕組みの完成です。
本記事では汎用的な処理しかしていませんが、

- 内容をCSVに出力する
- 送信時間制限をつける

など、団体に応じた処理を追加してみてくださいね。