GCP のサーバで Node.js の nodemailer と Mailgun を使ってメール送信機能を実装する（TypeScript版）

## 概要

Node.js のメール送信パッケージ「[Nodemailer](https://nodemailer.com/)」を実装するときの備忘録。

GPC の GCE で立てたサーバは、セキュリティの観点から、単純にサーバから 25 ポートやその他のポートでメール送信することがデフォルト設定ではブロックされているため使えない。
そこで、GCP で推奨されているメール転送サービスの１つ「Mailgun」を使って、GCE の Web アプリからメール送信する機能の実装を実現する。

JavaScript の記事はいっぱいあるが、今回は TypeScript 版で記載する。

### 前提条件

- GPC のユーザアカウントをもっていること
- Node.js のアプリ環境が整っていること
- 独自ドメインの取得が完了し、使用できる有効なものであること。（独自ドメインを利用しない場合は不要）

## 手順（メールサービス側）

今回は Mailgun を利用するので、その手順について記載。

### Mailgun に無料登録

https://app.mailgun.com/google から登録すると、無料プランで利用できる。
（無料になるのは、10,000通/月まで）

### 本人認証を完了させる

この手順を行わないと、アプリ側でメール送信実行時に以下のエラーが発生する

```bash:アカウント未認証エラー
Please activate your Mailgun account. Check your inbox or log in to your control panel to resend the activation email.
```

### 認証用の受信メールを設定

この手順を行わないと、アプリ側でメール送信実行時に以下のエラーが発生する

```bash:独自ドメインまたはメール受信者の未設定エラー
Sandbox subdomains are for test purposes only. Please add your own domain or add the address to authorized recipients in Account Settings.
```

厳密には、「独自ドメイン」または「認証用メールアドレス」のいずれかを追加設定しなければならないという旨のエラー。

Mailgun 側では登録すると自動発行されるサブドメインを利用する方法を推奨しているが、それだとMailgun 側に登録された個別の認証済みメールアドレスにしか飛ばせない（メールアドレスを自由に指定して送信できない）ので、今回は独自ドメインの追加を採用する。

#### ドメイン追加

[Domains](https://app.mailgun.com/app/domains/) の「Add New Domain」ボタンを押下し、独自で取得したドメインを入力する。

#### ドメイン詳細設定

各自が独自ドメインを取得したドメインレジストラに移動し、Mailgun のドメイン詳細画面に記載の DNS リソースレコード（「TXT」「MX」「CNAME」タイプの３種類のリソース情報）をレジストラ側にすべて追加する。

#### ドメインが認証されるまで待つ

登録時のメールアドレスに認証完了の通知が行くまでは、追加したドメイン情報ではメール送信ができない。

「Domain Information」 で認証が「Verified」と表示されたら Mailgun 側の設定は完了。

ちなみに、実際に未認証状態でメール送信をアプリ側で実行すると、以下のようなエラーになる。

```bash:Mailgunドメイン未認証エラー
The domain is unverified and requires DNS configuration. Log in to your control panel to view required DNS records.
```

### おまけ：認証用の受信メールを設定（省略可）

独自ドメインが未設定の状態で、この手順を行わないと、アプリ側でメール送信実行時に以下のエラーが発生する

```bash:独自ドメインまたはメール受信者の未設定エラー
Sandbox subdomains are for test purposes only. Please add your own domain or add the address to authorized recipients in Account Settings.
```

##### 認証したい受信者メールを追加

[Authorized Recipients](https://app.mailgun.com/app/account/authorized) で「Invite New Recipient」ボタンを押下し、受信者に設定するメールアドレスと入力。

設定したメール側でも認証を許可すると、以下のように「Verified」となる。

<img width="1440" alt="スクリーンショット 2019-01-10 14.13.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/199602/7155702a-4aec-08cd-567b-db915981bf7a.png">

これで、完了。

## 手順（アプリ側）

### まずインストール

```bash
# パッケージインストール
npm i nodemailer

# 型定義ファイルインストール
npm i -D @types/nodemailer
```

### バックエンドで実装

TypeScript でバックエンドを以下のように作成
（本来はコントローラにロジックは書きたくないところですが、あくまで参考ソースなので、各自で読み替えてください）

```ts
import { createTransport, SendMailOptions } from 'nodemailer';

/** メール コントローラ */
export class MailContoroller {

  public async send(request: any): Promise<any> {
    const response = {
      statusCode: 200,
      body: { result: 'send mail succeed.' }
    };

    // SMTPサーバの設定
    const smtp = createTransport({
      host: 'smtp.mailgun.org',
      port: 2525 // Mailgun で使用される SMTP ポート
    });

    // メール情報の作成
    const message: SendMailOptions = {
      from: 'hoge@xxxxx.co.jp', // 表示名つきにする場合は '表示名<Fromアドレス>' とする
      to: 'fuga@xxxxx.co.jp', // Toアドレス
      subject: '[テスト] nodemailer test mail',
      text: 'nodemailer から送信したテストメールです。'
    };

    // メール送信
    try {
      const mailRes = await smtp.sendMail(message);
      console.log('[送信成功]: ', mailRes);
    } catch (error) {
      console.log('[送信エラー]: ', error.message);
      response.statusCode = 500;
      response.body = { result: 'send mail failed.' };
    }

    return response;
  }

}
```

### 試しにメール送ってみたら、エラー発生で送信できない

```bash:メール送信実行時のコンソール出力
［送信エラー］: all recipients were rejected: 550 5.7.1 Relaying denied
```

どうやら、「SMTP Authentication（SMTP認証）」の設定がされていない場合、セキュリティ制限がかかり、このエラーが表示されるようだ。
SMTP認証すると、セキュリティ制限を回避し、メールを送信することができる。

Auth 情報をオプションにセットすればいいけど、ちょっと面倒くさい。。。
と、思っていたら、下記の対応策を発見。

### SMTP ではなく Mailgun の API を使用して電子メールを送信するパッケージを導入

以下のパッケージをインストールする

```bash
# パッケージ インストール
npm i nodemailer-mailgun-transport

# 型定義ファイルインストール
npm i -D @types/nodemailer-mailgun-transport
```

### バックエンドの実装を修正

```ts
import { Logger } from '../logger';
import { createTransport, SendMailOptions } from 'nodemailer';
import * as mailgunTransport from 'nodemailer-mailgun-transport'; // 追加

/** メール コントローラ */
export class MailContoroller {

  public async send(request: any): Promise<any> {
    const response = {
      statusCode: 200,
      body: { result: 'send mail succeed.' }
    };

    // Mailgun API キーの設定
    const opts: mailgunTransport.Options = {
      // https://app.mailgun.com/app/domains/ を参照して下記の値をセット
      auth: {
        api_key: '1234xxxxxxxxxxxxxxxxxxxxx-01234-abcde', // API Key
        domain: 'sandbox1234xxxxxxxxxx.mailgun.org' // Domain
      }
    };

    // SMTPサーバの設定 (mailgunTransport 利用版)
    const smtp = createTransport(mailgunTransport(opts));

    // SMTPサーバの設定 (nodemailer 利用版)
    // const smtp = createTransport({
    //   host: 'smtp.mailgun.org',
    //   port: 2525
    // });

    // メール情報の作成
    const message: SendMailOptions = {
      from: 'hoge@xxxxx.co.jp', // 表示名つきにする場合は '表示名<Fromアドレス>' とする
      to: 'fuga@xxxxx.co.jp', // Toアドレス
      subject: '[テスト] nodemailer test mail',
      text: 'nodemailer から送信したテストメールです。'
    };

    // メール送信
    try {
      const mailRes = await smtp.sendMail(message);
      console.log('[送信成功]: ', mailRes);
    } catch (error) {
      console.log('[送信エラー]: ', error.message);
      response.statusCode = 500;
      response.body = { result: 'send mail failed.' };
    }

    return response;
  }

}
```

これで送信を実行する。

### 実行確認

以下のような結果となり、送信に成功した。
実際にメールアドレスにも飛んできた。

```bash:メール送信結果_コンソール表示
...
 ［送信成功］:  {
  id: '<20190110055320.1.123XXXXX@sandbox1234xxxxxxxxxx.mailgun.org>',
  message: 'Queued. Thank you.',
  messageId: '<20190110055320.1.123XXXXX@sandbox1234xxxxxxxxxx.mailgun.org>'
}
```

以上で完了。