GoogleForm と GASで年賀状をGmailから自動送信してみた

# 背景
我が家では、数年前から年賀状にGoogleFormのQRコードを貼って、年賀状をメールで受け取りたい方にはメールアドレスを登録して頂き、1月1日に手動でメールを送信しています。

手動でメールを送信するのが面倒くさくなってきたので、これを自動化したいと思います。

# 目標


- 1月1日にスプレッドシートに登録されているメールアドレスに 年賀状画像付きのHTMLメールを送信する

- 送信が完了したアドレスを別のシートにまとめる

- 12月30日時点でGoogleDriveに今年の年賀状がアップロードされていない時、Slackにメッセージを投げる




# 詳細

- 年賀状画像はGoogleDriveの特定フォルダに ``YYYY.png`` の形式で保存しておく

- ファイルが見つからない時はSlackにメッセージを送信する

- 年賀状はGmailから送信する



# Apps Script

スプレッドシートの拡張機能から``Apps Script``を選んで適当にコードを書きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635641/59c80d91-b3ab-6370-ba6e-12d62fa395a7.png)




# スプレッドシートのログから最後に送信した年を取得する

```javascript
// ログから最後に送信したのが何年か取得する
const getLastYearFromLogSheet = () => {
  const spreadSheets = SpreadsheetApp.getActiveSpreadsheet();
  const sheets = spreadSheets.getSheets()

  // 年のリストをログシートの名前から取得
  const logYearList = 
    sheets.map((v) => {
      const name = v.getName();
      const split = name.split("-");
      
      if (split[0] === "log") return split[1];

      return "";
    }).filter((v) => {
      if (v === "") return false;
      return true;
    });


  // 配列を年で並び替える
  const sortLogYearList = logYearList.sort((a,b) => {
    a = Number(a);
    b = Number(b);

    if (!a || !b) {
      sendLogToSlack("getLastYearFromLogSheet.gsでエラーが発生しました。シート名 log-YYYY を期待しましたが、 Number(YYYY)でエラーが発生しています。" );
    }

    return b - a;

  })

  
  console.log(`ログに保存されている最後の年 = ${sortLogYearList[0]}`);
  return Number(sortLogYearList[0])
}

```

# Google Drive に年賀状の画像データが保存されているか確認する

## Drive API を追加する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635641/32d7d14f-b7a2-ca71-a13f-3508705c30c3.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635641/729c8152-2fe7-70a7-d0a2-d973291140bb.png)

## コード

年賀状の画像ファイルは特定の``Google Drive``フォルダに``YYYY.png``という名前で保存されるので来年の画像ファイルがあるか確認します。

```javascript
function checkGoogleDriveData() {

  //画像ディレクトリのファイルをすべて取得
  const folder = DriveApp.getFolderById(env.googleDriveFolderId)

  const files = folder.getFiles();

  const imageDirFiles = [];

  while(files.hasNext()) {
    imageDirFiles.push(files.next());
  }
  
  //　来年
  const nextYear = getLastYearFromLogSheet() + 1;

  // 次に送信する年賀状の画像ファイル
  const nextYearImage = imageDirFiles.find((v) => {
    if (v.getName() === `${nextYear}.png`) return true;
    return false;
  })

  if (!nextYearImage) return false; // 来年送信予定の画像ファイルが見つからない

  console.log(`送信予定のファイル : ${nextYearImage.getName()}`);

  return {// 来年送信予定の画像ファイルが見つかった
    year : nextYear,
    imageBlob : nextYearImage.getBlob()
  }; 
}
```

# 送信対象者リストを配列で取得する

``Google Form`` からのデータは
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635641/2863f767-4304-95de-ffe9-65032eefb9a2.png)

この形式なので、このデータから

```
[
 {
  name : string,
  email : string
 },
 {
  name : string,
  email : string
 }
]
```
このような配列を生成します。

## コード
```javascript
// ファイルの中から送信が必要なユーザーを配列で取得し返す

/*
- return のフォーマット

[
  {
    name : string,
    email : string
  },
  {
    name : string,
    email : string
  }
]
 */
const getSendUserList = () => {
    const spreadSheets = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = spreadSheets.getSheets()[0];
    let values = sheet.getDataRange().getValues();

    // メールを選択していない列を削除
    values = values.filter ((v,i) => {
      if (i === 0) return false; // 1列目は無視する
      if (!(v[2] === "メールによる受け取り")) return false; // メールによる受け取りを選択していない場合無視する
      if (!(v[1])) return false; // 名前が未設定の場合無視
      if (!(v[3])) return false; // メールアドレスが未設定の場合無視

      return true;
    });

    // オブジェクトに変換
    values = values.map((v) => {
      return ({
        name : v[1],
        email : v[3]
      })
    });

    return (values);
}

```

# メールの送信

送信すべき人リストが取得できるようになったので、次にメールを送信する処理を実装します。

## Gmail API を追加する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635641/b97abfbb-824d-2bf6-cc0a-c971e0fa99c8.png)


## コード

```javascript
const sendEmail = (name, email, imageBlob, year) => {

  sendLogToSlack(`${name}さんの ${email} に年賀状メールを送信しています...`);


  try {
    GmailApp.sendEmail(email, `${env.myName}からの年賀状`, "この端末でHTMLメールは表示できません。HTMLメールの設定を確認するかデバイスを変更してください。", {
      name : env.myName,
      inlineImages : {
        mainImage : imageBlob
      },
      htmlBody : `
        <img src="cid:mainImage" style="position:absolute; width:90%; left:5%">
      `,
    })
    saveLog(name, email, year, "成功");
  } catch (e){
    sendLogToSlack(`${name} さんの ${email} にメールを送信できませんでした。` + e);
    saveLog(name, email, year, "失敗");
  }
}
```


# メールを送信した事を別のシートに記録する

```javascript
const saveLog = (name = "test name", email = "test@test.com", year = 400, status="不明")  => {
    const spreadSheets = SpreadsheetApp.getActiveSpreadsheet();
    const sheets = spreadSheets.getSheets();

    // 今年のシートを探す
    let thisYearLogSheet = sheets.find((v) => {
      if (v.getName() === `log-${year}`) return true;
      return false;
    });

    // 今年のシートが無い場合作成
    if (!thisYearLogSheet) {
      thisYearLogSheet = spreadSheets.insertSheet();
      thisYearLogSheet.setName(`log-${year}`);
      console.log("新しいlogシートを作成しました。");
      thisYearLogSheet.appendRow(["名前", "メールアドレス", "ステータス"])

    };

    // 行を追加
    thisYearLogSheet.appendRow([name, email, status]);

}
```

# 定期的に処理を実行する
トリガーが未設定なので
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635641/849e3e95-c261-9577-6c67-6725a72db286.png)
トリガーを設定して
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635641/322fbd05-9d3d-1406-ea3a-62f58d478947.png)
完成です。

# その他

## すべてのファイル

<details><summary>appsscript.json</summary><div>

```json
{
  "timeZone": "Asia/Tokyo",
  "dependencies": {
    "enabledAdvancedServices": [
      {
        "userSymbol": "Drive",
        "version": "v2",
        "serviceId": "drive"
      },
      {
        "userSymbol": "Gmail",
        "version": "v1",
        "serviceId": "gmail"
      }
    ]
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}
```
</div></details>


<details><summary>env.gs</summary><div>

```javascript
/**
 * - 一番最初にあるシートをGoogleフォームと繋がっているシートとして処理します。
 */

const env = {
  myName : "〇〇家", // 送信元の名前
  slackWebSocketUrl : "https://hooks.slack.com/services/**********",// 家族SlackのWebSocketUrl
  googleDriveFolderId : "**********",//年賀状の画像データが保存されているGoogleDriveのフォルダID
  checkSendDay : { // 年賀状を送信する日
    month : 1,
    day : 1
  },
  checkGoogleDriveDataDay: { // 年賀状のデータがGoogleDriveに存在するか確認する日
    month : 12,
    day : 30
  }
}
```
</div></details>


<details><summary>index.gs</summary><div>

```javascript
const myFunction = () => {

  console.log("処理が開始されました。");

  if (checkSendDay()) {
    // 年賀状の送信処理を開始
    sendLogToSlack("年賀状の送信処理を開始します。");

    const sendUserList = getSendUserList();

    sendLogToSlack(`${sendUserList.length}人に対して年賀状メールの送信を実行します。`);

    const imageData = checkGoogleDriveData();

    if (imageData === false) {

      sendLogToSlack("メールの送信を開始しようとしましたが、GoogleDriveにファイルが見つからなかった為キャンセルしました。");

      return;
    }

    // 全員に送信
    sendUserList.forEach((v,i) => {
      sendEmail(v.name, v.email, imageData.imageBlob, imageData.year);
    })

  };


  if (checkGoogleDriveDataDay()) {
    // GoogleDriveのデータを確認開始
    sendLogToSlack("Google Drive に年賀状データが保存されているか確認します。 確認結果をお待ち下さい...");

    if (checkGoogleDriveData()) {
      sendLogToSlack("Google Drive に年賀状データが保存されています。");

    } else {
      sendLogToSlack("Google Drive に年賀状データが存在しません。年賀状の送信日までに年賀状のデータを Google Drive にアップロードしてください。 ※pngファイル");

    }
  }
}
```
</div></details>

<details><summary>checkDay.gs</summary><div>

```javascript


// 使いやすい形式の月と日を返す
const getDate = () => {
  return (
    {
      month : new Date().getMonth() + 1,
      day : new Date().getDate()
    }
  )
}

// 年賀状を送信する日の場合Trueを返します。
const checkSendDay = () => {
  const date = getDate();

  if (
      env.checkSendDay.month === date.month
      &&
      env.checkSendDay.day === date.day
    ) {
      // envの日付と一致した時
      return true;
    } else {
      // envの日付と一致しなかった時
      return false;
    }

}

// GoogleDriveのデータをチェックする日の場合Trueを返します。
const checkGoogleDriveDataDay = () => {
  const date = getDate();

  if (
      env.checkGoogleDriveDataDay.month === date.month
      &&
      env.checkGoogleDriveDataDay.day === date.day
    ) {
      // envの日付と一致した時
      return true;
    } else {
      // envの日付と一致しなかった時
      return false;
    }
}



```
</div></details>


<details><summary>getSendUserList.gs</summary><div>

```javascript
// ファイルの中から送信が必要なユーザーを配列で取得し返す

/*
- return のフォーマット

[
  {
    name : string,
    email : string
  },
  {
    name : string,
    email : string
  }
]
 */
const getSendUserList = () => {
    const spreadSheets = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = spreadSheets.getSheets()[0];
    let values = sheet.getDataRange().getValues();

    // メールを選択していない列を削除
    values = values.filter ((v,i) => {
      if (i === 0) return false; // 1列目は無視する
      if (!(v[2] === "メールによる受け取り")) return false; // メールによる受け取りを選択していない場合無視する
      if (!(v[1])) return false; // 名前が未設定の場合無視
      if (!(v[3])) return false; // メールアドレスが未設定の場合無視

      return true;
    });

    // オブジェクトに変換
    values = values.map((v) => {
      return ({
        name : v[1],
        email : v[3]
      })
    });

    return (values);
}

```
</div></details>


<details><summary>checkGoogleDriveData.gs</summary><div>

```javascript
function checkGoogleDriveData() {

  //画像ディレクトリのファイルをすべて取得
  const folder = DriveApp.getFolderById(env.googleDriveFolderId)

  const files = folder.getFiles();

  const imageDirFiles = [];

  while(files.hasNext()) {
    imageDirFiles.push(files.next());
  }
  
  //　来年
  const nextYear = getLastYearFromLogSheet() + 1;

  // 次に送信する年賀状の画像ファイル
  const nextYearImage = imageDirFiles.find((v) => {
    if (v.getName() === `${nextYear}.png`) return true;
    return false;
  })

  if (!nextYearImage) return false; // 来年送信予定の画像ファイルが見つからない

  console.log(`送信予定のファイル : ${nextYearImage.getName()}`);

  return {// 来年送信予定の画像ファイルが見つかった
    year : nextYear,
    imageBlob : nextYearImage.getBlob()
  }; 
}
```
</div></details>

<details><summary>sendLogToSlack.gs</summary><div>

```javascript
// Slackにログを送信する
const sendLogToSlack = (message) => {


  const content = JSON.stringify({
     "text" : message,
  });

  const sendData = {
    "method" : "post",
    "contentType" : "application/json",
    "payload" : content
  };

  UrlFetchApp.fetch(env.slackWebSocketUrl, sendData);
  console.log(`${message} : Slack に送信済み`);

}
```
</div></details>

<details><summary>sendEmail.gs</summary><div>

```javascript
const sendEmail = (name, email, imageBlob, year) => {

  sendLogToSlack(`${name}さんの ${email} に年賀状メールを送信しています...`);


  try {
    GmailApp.sendEmail(email, `${env.myName}からの年賀状`, "この端末でHTMLメールは表示できません。HTMLメールの設定を確認するかデバイスを変更してください。", {
      name : env.myName,
      inlineImages : {
        mainImage : imageBlob
      },
      htmlBody : `
        <img src="cid:mainImage" style="position:absolute; width:90%; left:5%">
      `,
    })
    saveLog(name, email, year, "成功");
  } catch (e){
    sendLogToSlack(`${name} さんの ${email} にメールを送信できませんでした。` + e);
    saveLog(name, email, year, "失敗");
  }
}
```
</div></details>

<details><summary>getLastYearFromLogSheet.gs</summary><div>

```javascript
// ログから最後に送信したのが何年か取得する
const getLastYearFromLogSheet = () => {
  const spreadSheets = SpreadsheetApp.getActiveSpreadsheet();
  const sheets = spreadSheets.getSheets()

  // 年のリストをログシートの名前から取得
  const logYearList = 
    sheets.map((v) => {
      const name = v.getName();
      const split = name.split("-");
      
      if (split[0] === "log") return split[1];

      return "";
    }).filter((v) => {
      if (v === "") return false;
      return true;
    });


  // 配列を年で並び替える
  const sortLogYearList = logYearList.sort((a,b) => {
    a = Number(a);
    b = Number(b);

    if (!a || !b) {
      sendLogToSlack("getLastYearFromLogSheet.gsでエラーが発生しました。シート名 log-YYYY を期待しましたが、 Number(YYYY)でエラーが発生しています。" );
    }

    return b - a;

  })

  
  console.log(`ログに保存されている最後の年 = ${sortLogYearList[0]}`);
  return Number(sortLogYearList[0])
}

```
</div></details>

<details><summary>saveLog.gs</summary><div>

```javascript
const saveLog = (name = "test name", email = "test@test.com", year = 400, status="不明")  => {
    const spreadSheets = SpreadsheetApp.getActiveSpreadsheet();
    const sheets = spreadSheets.getSheets();

    // 今年のシートを探す
    let thisYearLogSheet = sheets.find((v) => {
      if (v.getName() === `log-${year}`) return true;
      return false;
    });

    // 今年のシートが無い場合作成
    if (!thisYearLogSheet) {
      thisYearLogSheet = spreadSheets.insertSheet();
      thisYearLogSheet.setName(`log-${year}`);
      console.log("新しいlogシートを作成しました。");
      thisYearLogSheet.appendRow(["名前", "メールアドレス", "ステータス"])

    };

    // 行を追加
    thisYearLogSheet.appendRow([name, email, status]);

}

```
</div></details>

# 感想
色々考えて実装したつもりだったけど、後で冷静に考えると``年``の取得方法は普通に``new Date()``から取れば良かったかも。でも結構短時間で実装できたので良しとする。
何より冬場のキーボードは辛い。