スプレッドシートから Gmail で通知メールを送るスクリプト

## この記事の目的
小学校で開催するイベントを Google フォームで受け付けたあとで、申込者にまとめてメールを送りたいことがあります。スプレッドシートにあるメールアドレスや名前をなどの情報を使って、個別の参加者にメールを送る方法として、 (1) Google Apps Script (GAS) で `GmailApps` クラスを使ってメール送信する方法と。 (2) JavaScript で `nodemailer` を使ってメール送信する方法、の2つを解説します。

## 2つの方法の違い
### (1) GAS で `GmailApps` クラスを使ってメール送信する方法
GAS は、Gmail アカウントがあれば利用でき、Google スプレッドシートと関連付けて実行することができます。すべてブラウザの中で使うことができるので、自分の PC に開発環境を整える必要はなく、ログインさえできればどこの PC で同じように使用できます。PC に開発環境をもたないので「何もしないのに動かなくなった！」などという問題も起こりにくいかもしれません。
GAS では、Google の提供するクラウドサービスを操作するためのクラスが用意されており、[`SpreadsheetApp` クラス](https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet-app?hl=ja) でスプレッドシートの操作ができ、[`GmailApps` クラス](https://developers.google.com/apps-script/reference/gmail/gmail-app) でメールの送信ができます。
ただし無償で利用できる一般ユーザーでは `GmailApps` クラスから送信できるメールの宛先（メール送信者）は一日当たり100件に制限されています。この制限は [Google サービスの割り当て](https://developers.google.com/apps-script/guides/services/quotas)に記載されています。
GASではなく、Gmail 画面を操作してメール送信するのであれば、[メールの送受信数に関する制限](https://support.google.com/mail/answer/22839) によると500件までの宛先にメールが送れるようです。

###  (2) JavaScript で nodemailer を使ってメール送信する方法
スクリプトの実行環境を作り、スプレッドシートを CSV として読み込んでメールを送るスクリプトを実装することもできます。これによって、先に説明した一般ユーザーとして `GmailApp` を使うことでのメールの宛先が一日当たり100件までとなる制限から解放されます。Google サービスを使う縛りもなくなるので、Gmail 以外のメールサービスを使うことで一日500件の制限をなくすこともできます。
その一方で開発環境を自分の PC に用意したり、CSV やメールサーバーとの接続で発生するいくつか少しばかり面倒な設定、例えば文字コードであったりサーバー接続の認証といったことを自分で解決する必要があり、少々難易度が上がります。
この記事では、Node.js をインストールして JavaScript を nodemailer を使ってメール送信する方法を解説します。

## 使用するデータ

ここでは以下のような列データを持つスプレッドシートを使うことにします。商品の注文申込データをもとに確認メールを送るという想定です。

| 列名 | 説明 |
|:--- |:--- |
| 処理対象 | メール送信の対象となる行にあらかじめ `y` と入力しておく |
| 処理済 | スクリプトでメール送信処理をしたら、スクリプトが `y` と入力する |
| 氏名 | 注文者の氏名 |
| メール | 注文者のメールアドレス |
| 注文商品 | 注文した商品名 |
| 注文数 | 注文した商品の数 |

![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2530785/0d6b67df-8836-4aed-9a66-1354e855333f.png)

スクリプトでは `処理対象` が `y` で `処理済` が設定されていない注文者にメールを送り、処理が行われたら `処理済` に `y` と設定します。

 
## (1) GAS で `GmailApps` クラスを使ってメール送信する方法

1. Google スプレッドシートから **拡張機能** → **Apps Script** を選択して GAS プロジェクトを開きます。

    ![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2530785/f9f7ddc6-3018-e3a0-0a63-40334e4e9025.png)

`myFunction()` という関数の定義だけが自動で生成されるので、関数内に以下のような処理を記載します。コードの説明については、コメントを参照してください。

```JavaScript
function myFunction() {
  // 確認のために自分自身に CC するようオプションを設定
  const myEmail = Session.getActiveUser().getEmail();
  const options = { cc: myEmail };

  // 関連付けられたスプレッドシートを開く
  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet()

  // 「注文リスト」シートを取得
  const orderSheet = spreadsheet.getSheetByName('注文リスト')

  // データの入っている範囲を取得
  const orderRange = orderSheet.getDataRange();
  const orderValues = orderRange.getValues();

  // 最初の行をスキップして各行を処理
  for (var i = 1; i < orderValues.length; i++) {
    const row = orderValues[i];
    // 「処理対象」と「処理済」のデータを Boolean で取得
    const isTarget = null == row[0] ? false : "y" === row[0].toString().toLowerCase();
    const isProessed = null == row[1] ? false : "y" === row[1].toString().toLowerCase();
    if (isTarget && !isProessed) { // 処理対象が y で処理対象が y でないときを処理
      const name = row[2];
      const email = row[3];
      const product = row[4];
      const count = row[5];

      // メールの件名と本文を作成
      const title = `【${name} 様】ご注文の確認`;
      const body =`${name} 様,

このたびはご注文をありがとうございました。
以下の内容で発送処理を開始しました。

商品：${product}
数量：${count}

今後ともどうぞよろしくお願いします。
`;

      // メール送信
      Logger.log(`${name} 様 (${email}) にメールを送信します。`);
      GmailApp.sendEmail(email, title, body, options);

      // この行の「処理済」列を取得して "y" を設定
      const isProcessedCell = orderRange.getCell(i+1, 2);
      isProcessedCell.setValue("y");
    } else {
      Logger.log(`${row[2]} 様は処理されませんでした。`);
    }
  }
}
```
編集画面のアクションから **実行** をクリックして、スクリプトを実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2530785/3aed962e-90ff-08df-2a2c-fe3baacac7f6.png)

実行にあたり「アカウントを選択してください」ダイアログが出たときは、使用している Gmail アカウントを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2530785/8cc8d9fd-6a07-f34d-747d-f37eb9ba7326.png)

「Google はこのアプリを検証していません」と出たら「高度（Advanced）」リンクをクリックしてから「無題のプロジェクトへ（危険）」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2530785/3c83ef51-18fa-d784-6628-78004c65ae48.png)

「許可する」をクリックしてこのプロジェクトのへのアクセスを許可します。なお、ここで表示される許可の内容は、実際のスクリプトの処理内容で変わることがあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2530785/12226a4a-f3a2-6b7e-9fd8-6f13b956352a.png)

確認のために自分自身にメールを CC しています。スクリプトの実行をし相手先と自分自身にメール送信されたことを確認します。

##  (2) JavaScript で nodemailer を使ってメール送信する方法

### Google アプリパスワードの取得
スクリプトから Gmail にアクセスする際にアプリパスワードを使用します。アプリパスワードについては [「アプリ パスワードでログインする」](https://support.google.com/accounts/answer/185833) で説明されています。
まず、Google アカウントのセキュリティ設定からアプリパスワードを生成します。
1. https://myaccount.google.com/security を開きます。アプリパスワードを設定するには「2段階認証プロセス」を有効にしておく必要があります。
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2530785/ca8eb0f9-4b59-5ae2-ca9c-e4debb51556e.png)

1. 「2段階認証プロセス」ページを開き、ページの下の方に行くと「アプリパスワード」の設定があります。
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2530785/3a06bf12-b4a7-2109-cef2-74a170ac66d3.png)

1. 「アプリパスワード」のページでアプリパスワードを生成するアプリとデバイスを適当に選びます。ここでは「メール」と「Windows パソコン」を選びました。
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2530785/a9658ffb-1128-80f3-38c0-66520edff0db.png)

1. 「生成」ボタンをクリックするとアプリパスワードが生成されるので、生成された文字列を記録しておきます。
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2530785/ff1e591b-00aa-d1f3-163b-b99277a4fb0b.png)



### JavaScript の開発と実行環境の構築

まず自分の PC に JavaScript を実行する環境を作ります。そのために JavaScript の実行環境となる Node.js と関連するライブラリのパッケージ管理をする npm をインストールします。OS ごとの Node.js と npm のインストール方法は [「WindowsにNode.jsをインストールする方法」](https://kinsta.com/jp/blog/how-to-install-node-js/#nodejs) に書かれています。
JavaScript はもともとブラウザので表示された Web ページ内で動作するスクリプト言語です。その JavaScript をブラウザを使わずに PC 上で独立して使うための実行環境が Node.js です。しかし JavaScript 単体でできることは限られており、CSV ファイルを扱ったりメールを送信したりするには外部のパッケージと呼ばれるライブラリが必要になります。そのようなパッケージを管理するのが npm (Node Package Manager) です。npm を使って JavaScript の実行環境を構築すると `package.json` というファイルが作られます。使用している JavaScript の実行で必要としているパッケージはこの `package.json` で管理されており、すでに構成済みの JavaScript の実行環境を別の場所に再構築するときには、JavaScript ファイル (`.js` ファイル) と `package.json` をコピーすれば簡単に環境の再構築ができます。

### npm の初期化とパッケージのインストール

npm の使用方法についての詳細な説明はネット上にたくさんの記事がありますので、ここでは割愛します。今回やろうとしている JavaScript で CSV を読み込んでメール送信するスクリプトの実行のための手順だけを簡単に紹介します。

#### 1. npm の初期化

以下のコマンドで npm の初期化をします。
ここでは `-y` で細かい設定を省略して自動設定させています。
  
```shell
> npm init -y
```

このコマンドで `package.json` が生成されます。

#### 2. パッケージのインストール

今回は以下のパッケージをインストールして使います。これらのパッケージの詳細については、関連する記事を参照してください。

- [csv-parse](https://www.npmjs.com/package/csv-parse) と [csv-stringify](https://www.npmjs.com/package/csv-stringify)
    [csv-parse](https://www.npmjs.com/package/csv-parse) はCSV 形式を JSON 配列に変換するパッケージで、[csv-stringify](https://www.npmjs.com/package/csv-stringify) は配列を CSV 形式に変換するパッケージ
    - 使い方: [Node.jsでcsvファイルを読み書きする](https://qiita.com/b-coffin/items/33948e1d1316215cc8d1)
    - [`ERR_PACKAGE_PATH_NOT_EXPORTED` エラーが出たときの対応](https://teratail.com/questions/4rwqp3rtv6ev0b)
    - [出力された CSV ファイルを Excel で開くと文字化けするときの対応](https://symfoware.blog.fc2.com/blog-entry-2531.html)
- [nodemailer](https://www.npmjs.com/package/nodemailer)
    メール送信をするパッケージ
    - 使い方: [JavaScriptを使ってGmailを自動送信しよう](https://news.mynavi.jp/techplus/article/zerojavascript-20/)


以下のコマンドでパッケージをインストールします。

```shell
> npm install csv-parse
> npm install csv-stringify
> npm install nodemailer
```

インストールされたパッケージは `node_modules` フォルダの下に置かれるとともに、以下のように `package.json` に記載されます。

```JSON
{
  "name": "sendgmailviajavascript",
  "version": "1.0.0",
  "description": "",
  "main": "myFirstSendGmail.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "csv-parse": "^5.4.0",
    "csv-stringify": "^6.4.0",
    "nodemailer": "^6.9.3"
  }
}
```

この JavaScript 環境を他に移植するときには、この `package.json` とこの後で紹介する JavaScript ファイルをコピーし `npm install` コマンドを実行することで、個々のパッケージが自動的にインストールされます。

### 入力 CSV ファイル

スクリプトの実行で使用する入力ファイルを以下に置きます。これは `注文リスト.csv` というファイル名で、先ほど npm 初期化を行ったフォルダに UTF-8 形式で保存してください。

```CSV
処理対象,処理済,氏名,メール,注文商品,注文数
,,鶴見つばさ,user@company.com,りんご,3
Y,,寺尾ぴよ,user@gmail.com,みかん,2
y,,馬場うまのすけ,name@oraganization.org,バナナ,1
y,y,生麦なまたまご,nama@company.co.jp,いちご,6
```

### スクリプトの作成と実行

`sendGmailFromCSV.js` というファイルを、先ほど npm 初期化を行ったフォルダに置き、以下のスクリプトを記載してください。このスクリプトの処理の詳細はコメントを参照してください。

```javascript
const fs = require('fs');
const { parse }  = require('csv-parse/sync');
const { stringify } = require('csv-stringify/sync');
const nodemailer = require('nodemailer')

// メール送信先を指定した入力ファイル
const INPUT_CSV = "./注文リスト.csv";
// 結果の出力ファイル
const OUTPUT_CSV = "./処理結果.csv"

// メール送信をする Gmail アカウントとアプリパスワード
const SENDER_GMAIL = "******@gmail.com";
const APP_PASSWORD = "n*******s";

// Nodemailer transport オブジェクトを生成
const porter = nodemailer.createTransport({
  service: 'gmail',
  port: 465,
  secure: true,
  auth: {
    user: SENDER_GMAIL,
    pass: APP_PASSWORD
  }
});

/**
 * メール送信をする関数
 * mailTo 送信先メールアドレス
 * title メールの件名
 * body メールの本文
 */
sendMail = async (mailTo, title, body) => {
  return new Promise((resolve, reject) => {
    porter.sendMail({
      from: SENDER_GMAIL,
      to: mailTo,
      subject: title,
      text: body
    }, function (err, info) {
      if (err) {
        reject(err);
      }
      resolve(info);
    });
  });
}

/**
 * メイン関数
 */
main = async () => {
  // CSV ファイルを読み込んで JSON Array に変換
  const inputCsv = fs.readFileSync(INPUT_CSV, { encoding : "utf8" });
  const orderValues = parse(inputCsv, { columns : true });

  // 最初の行をスキップして各行を処理
  for (let i = 0; i < orderValues.length; i++) {
    const row = orderValues[i];
    // 「処理対象」と「処理済」のデータを Boolean で取得
    const isTarget = "y" === row["処理対象"].toString().toLowerCase();
    const isProessed = "y" === row["処理済"].toString().toLowerCase();
    if (isTarget && !isProessed) { // 処理対象が y で処理対象が y でないときを処理
      const name = row["氏名"];
      const email = row["メール"];
      const product = row["注文商品"];
      const count = row["注文数"];

      // メールの件名と本文を作成
      const title = `【${name} 様】ご注文の確認`;
      const body =`${name} 様,

このたびはご注文をありがとうございました。
以下の内容で発送処理を開始しました。

商品：${product}
数量：${count}

今後ともどうぞよろしくお願いします。
`;

      try {
        // メール送信
        console.log(`${name} 様 (${email}) にメールを送信します。`);
        const res = await sendMail(email, title, body);
        
        // 「処理済」列に "y" を設定
        row["処理済"] = "y"
      } catch (err) {
        console.log("Error: " + JSON.stringify(err, null,2));
      }
    } else {
      console.log(`${row["氏名"]} 様は処理されませんでした。`);
    }
  }

  // UTF-8 を示す BOM を付けて出力ファイルへ書き込み
  const outputCsv = "\ufeff" + stringify(orderValues, { header : true });
  fs.writeFileSync(OUTPUT_CSV, outputCsv, { encoding: "utf8" });
};

// メイン関数の呼び出し
main();
```

このスクリプト内で `sendMail` 関数を `async` で宣言して、`sendMail` の呼び出しで `await` を付けています。これは非同期処理されている関数の結果をコールバックではなく、その処理を待ってシーケンシャルにスクリプトを動作させるテクニックです。`nodemailer` で提供しているtransport オブジェクトの `sendMail()` 関数が非同期処理になっているのですが、この非同期処理の終了を待っています。
JavaScript での非同期処理を `asnyc`/`await` で実装するやり方については [「callbackの関数をawait化する」](https://zenn.dev/bluepost/articles/6fc8d4760a40b1) が参考になります。


JavaScript ファイルを保存して、以下のコマンドで実行します。

```shell
> node sendGmailFromCSV.js
```