GAS(GoogleAppScript)でGmailで受信したデータ(画像などの添付ファイル&文字)を自動的にGoogleDriveに保存する方法

#概要
GoogleAppScriptを利用して、Gmailに届いたメールの内容(抽出したいデータ)を自動的にGoogleDriveに保存する方法を備忘録として記録する。主に企業や個人サイトなどの「お問い合わせフォーム」経由で送られてくるメールの内容（文字データや画像などのファイルデータ）の抽出を対象としている。

文字データのみ、もしくは画像などのファイルデータのみの抽出についての記事は多く見受けられたが異なる2つの要素を同時に抽出するプログラムについての記事が無かったので、試行錯誤してなんとか自作に成功した。
#必要な準備
+ SpreadSheetとGoogleAppScriptの作成
+ GoogleDriveの空ファイル(画像などのファイルデータ抽出が必要な場合）
+ お問い合わせフォームから送られてくるメールの形式設定(XML形式)

##SpreadSheetとGoogeAppScriptの作成
まずは自身のGoogleアカウントもしくはGoogleDriveから新規SpreadSheetを作成する。
![スクリーンショット 2020-04-02 2.02.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/503486/40c15c03-faf0-10b0-0e0b-22b1872694d9.png)
そして上記の様にツールバーの「ツール」>>「<>スクリプトエディタ」を選択する。
すると下の様な画面が出てくるので、ここにコードを記述していく。
（※SpreadSheetの名前及びシート名は自由に設定してOK。）
![スクリーンショット 2020-04-02 2.12.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/503486/24535748-397c-d327-6ea8-cd264ff69c2e.png)

##GoogleDriveの空ファイル作成
Gmailから自動的に抽出した画像などのファイルデータがこのファイルに保存される。
これは特に画像などのファイルデータをメールから抽出する必要がない場合は特に準備は不要となる。

##お問い合わせフォームから送られてくるメールの形式設定(XML形式)
XML形式についての詳しい説明はここでは省くが、簡単に説明すると文書の論理構造をタグを利用して分かりやすく形式化したものである。

メールの形式設定についてだが、主にPHPなどで記述されたお問い合わせフォームの自動返信メールを想定しているので、形式変更はできるものとして話を進める。

例えば、必要な情報が「お問い合わせ日時」「名前」「メールアドレス」「年齢」「性別」である場合はメール本文に以下の様な形式で内容が届く様に設定する。この際、本文にはこれ以外の余計な文章や空白などはデータ読み取りに障害になる場合がある為できるだけ避けたい。

```text:
From: info@sample.com
Subject: お問い合わせがありました
----------------本文------------------
<text>
<date>2020-04-01 18:00</date>
<name>田中太郎</name>
<email>taro.tanaka@gmail.com</email>
<age>30</age>
<sex>男性</sex>
</text>
```
以上で下準備は完了なので、これ以降は実際にGoogleAppScriptにコードを記述していく。

#GoogleAppScriptの作成
まずはコードを記述していくに当たって、今回の設定を確認する。
抽出したい情報は、

1. お問い合わせ日時
2. お名前
3. メールアドレス
4. 年齢
5. 性別
6. 添付ファイル（画像など）

以上、5つの文字データと1つの添付ファイルデータ（画像など）と仮定する。
以下のコードには大きく分けて2つのプログラム(function)が記述されており、それぞれ独立しているがまとめて1つのスクリプトに記述している。

##プログラムのフロー
メールの受信
↓
未読の該当メールの探索（function spreadSheet)
↓
文字データの抽出
↓
当該メールへのスター付与&既読
↓
スター付きの該当メール探索(function googleDrive)
↓
画像などのファイルデータの抽出
↓
スター解除

##GoogleAppScript プログラム

```js:コード.gs
///////文字データをspreadSheetに自動書き込み///////
function spreadSheet(){
  // 特定のスプレッドシートを指定してそこに書き込む場合
  // スプレッドシートのIDは、https://docs.google.com/spreadsheets/d/ここがスプレッドシートのID/edit#gid=0
  var fileid = "xxxxxxxxxxxxx_yyyyyy_zzz"
  var sheetfile = SpreadsheetApp.openById(fileid)
  var sheet    = sheetfile.getSheetByName('シート名');
  
  // スプレッドシートに挿入するデータ列数
  // ここではお問い合わせ日時、お名前、メールアドレス、年齢、性別の5つ
  var insertCol = 5;

  // 最新10件
  var thds = GmailApp.getInboxThreads(0,10);
  // 条件に合ったメッセージ群を抽出
  msgss = selectMsgs_Sp(thds);
  // データ保存先となるシートの最終行。すなわち挿入開始位置
  var row = sheet.getLastRow() + 1;
  // 取得したメール内容を格納する用の配列
  var resultArr = new Array();
  // スレッド内のメールを解析して resultArr　に格納
  returnData(msgss, resultArr);
  Utilities.sleep(1000);
  if( msgss.length != 0 ){
     sheet.getRange(row, 1, msgss.length, insertCol).setValues( resultArr ); // データ保存
   }
}

function selectMsgs_Sp(thds) {
  msgss = [];
  for (var i=0; i<thds.length; i++) {
    // 特定の条件に合うメッセージのみ抽出
    // falseが返ってきたら次のメッセージを処理、配列が返ってきたら結合
    sp_msgss = selectSpecificMsgs_Sp(thds[i].getMessages());
    if (!sp_msgss) { continue; }
    Array.prototype.push.apply(msgss,sp_msgss);
  }
  return msgss;
}

function selectSpecificMsgs_Sp(msgss) {
  var sp_msgss = [];
  var count = 0;
  // ここから条件にあったものだけを追加するような感じ
  for (var j=0; j<msgss.length; j++) {
    // 未読メールの探索
    if (!msgss[j].isUnread()) { continue; }
    subject = msgss[j].getSubject();
    // メールの件名で抽出したいメールを選択
    if (!subject.match(/お問い合わせを受け付けました/)) { continue; }
    sp_msgss.push(msgss[j]);
    // 処理したものはスターを付与
    msgss[j].star();
    // 処理したものは既読にする
    msgss[j].markRead();
    // 配列に追加したらcountする
    count++;
  }
  // 一度もcountされていない=条件に合わなかった、のでfalseを返す
  if (count == 0) { return false; };
  return sp_msgss;
}

function returnData(msgss, resultArr) {

for(var m=0; m<msgss.length; m++){
  try{

    var tempArray = new Array();
    var msg = msgss[m];

    // メール本文をPlainBodyで取得
    // getPlainBody以外については、https://developers.google.com/apps-script/reference/gmail/gmail-message
    var body = msg.getPlainBody();

    // bodyをXMLパーサーで解析
    var xml = XmlService.parse(body);

    // XML解析結果のルート要素を取得
    var root = xml.getRootElement();

    // XML内の各子要素を指定してその値を取得。下記のgetChildの中身は自分で設定したXMLのマークアップに合わせてください。
    var date = root.getChild("date").getText();
    var name = root.getChild("name").getText();
    var email = root.getChild("email").getText();
    var age = root.getChild("age").getText();
    var sex = root.getChild("sex").getText();

    // 各値を配列に格納
    tempArray[0] = date;
    tempArray[1] = name;
    tempArray[2] = email;
    tempArray[3] = age;
    tempArray[4] = sex;

    // 配列をpushで末尾に追加
    resultArr.push(tempArray);

   }catch(e){
     Logger.log("Error: " + e);
   }
  }
}

///////画像などのファイルデータをgoogleDriveに自動保存////////
function googleDrive() {
  // 最新10件
  var thds = GmailApp.getInboxThreads(0,10);
  // 条件に合ったメッセージ群を抽出
  msgss = selectMsgs(thds);
  // 各メッセージについて処理
  for (var i=0; i<msgss.length; i++) {
    // 添付ファイルを取り出す
    var files = msgss[i].getAttachments();
    // Google Driveに保存
    saveFiles(files);
  }
}

function selectMsgs(thds) {
  msgss = [];
  for (var i=0; i<thds.length; i++) {
    // 特定の条件に合うメッセージのみ抽出
    // falseが返ってきたら次のメッセージを処理、配列が返ってきたら結合
    sp_msgss = selectSpecificMsgs(thds[i].getMessages());
    if (!sp_msgss) { continue; }
    Array.prototype.push.apply(msgss,sp_msgss);
  }
  return msgss;
}

function selectSpecificMsgs(msgss) {
  var sp_msgss = [];
  var count = 0;
  // ここから条件にあったものだけを追加するような感じ
  for (var j=0; j<msgss.length; j++) {
    if (!msgss[j].isStarred()) { continue; }
    subject = msgss[j].getSubject();
    // メールの件名で抽出したいメールを選択
    if (!subject.match(/お問い合わせを受け付けました/)) { continue; }
    sp_msgss.push(msgss[j]);
    // 処理したものはスター解除する
    msgss[j].unstar();
    // 配列に追加したらcountする
    count++;
  }
  // 一度もcountされていない=条件に合わなかった、のでfalseを返す
  if (count == 0) { return false; };
  return sp_msgss;
}

function saveFiles(files) {
  // IDはブラウザで当該ディレクトリにアクセスしたときのURLの末尾
  var folder = DriveApp.getFolderById('xxxxxxxxxxxxxxxxxxxxxxxxxxx');
  for (var i=0; i < files.length; i++) {
    Logger.log(files[i].getName());
    folder.createFile(files[i]);
  }
}
```
##各functionのトリガー設定
![gas_qiita.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/503486/3c77b4d8-0ff0-3764-2a72-ee791c26fcc2.png)
コードを保存してプログラム編集画面の左上の青いアイコンをクリックすると上のページに飛ぶ。そこで、先程作成したプログラムの上にカーソルをホバーさせると右側にメニューが出てくるのでそこの「トリガー」をクリックする。

するとトリガーの編集画面に飛ぶので、そこで右下の「+トリガーを追加」をクリックして編集する。

| 属性 | 設定 |
|:-:|:-:|
| 実行する関数  | spreadSheet  |
| デプロイ時に実行  | Head  |
| イベントのソース  | 時間主導型  |
| 時間ベースのトリガーのタイプ  | 時間ベースのタイマー  |
| 時間の間隔  | 2時間おき  |

| 属性 | 設定 |
|:-:|:-:|
| 実行する関数  | googleDrive  |
| デプロイ時に実行  | Head  |
| イベントのソース  | 時間主導型  |
| 時間ベースのトリガーのタイプ  | 時間ベースのタイマー  |
| 時間の間隔  | 2時間おき  |

この際、文字データと画像などのファイルデータ両方抽出するのでトリガーを2つ設置する必要がある。
※トリガーの時間間隔設定は自由に行っても問題ない。

編集を保存すると、以下の様な承認要求メッセージが表示される（場合がある)。

```text
このアプリは確認されていません
このアプリは、Google による確認が済んでいません。よく知っている信頼できるデベロッパーの場合に限り続行してください。

デベロッパーの場合は、この画面が表示されないようにするには確認リクエストを送信してください。 ヘルプ

詳細
```
その際は、焦らずに下の「詳細」をクリックして、指示通りに承認作業を進れば無事にトリガーを追加できる。以上でトリガーの設定は終了。

#まとめ
今回は画像などのファイルデータと文書データ両方を自動的にドライブに保存する方法を記述した。
特に拘った点は実行フロー部分であり、実行プログラムが2つ存在する場合はどの記事にも特に触れられていなかった。単に未読メールを処理後に既読にした場合には片方のプログラムがどうしても探索できない状況にあったのでスターを噛ませることで解決に導いた。
もし何か不具合や疑問点があればお気軽にコメント頂けると嬉しい。