AWS SESをちゃんと使うためにやるべきこと


# AWS SESをちゃんと使う

AWS SESは安価で簡単にメールの送信／受信トリガーが実現できるサービスです。AWSを使っているサービスの場合、けっこう使うことになると思います。が、ハマりどころや注意すべき所があるため、サービスの特性を理解して使ったほうが良いと思い、まとめてみました。

## やること

- SESの制限解除申請をする
- DKIMに対応する／逆にSPFは対応しなくてOK
- バウンスと苦情の対応をSNS/SQSなどを使い自動化する
- 25番ポートは使用せず、465/587を使用する
- 配信メトリクスを監視する

### SESの制限解除申請

- 新規AWSアカウントの場合、SESはサンドボックスモードになる
	- サンドボックスモードだと、検証済みEmail/ドメイン宛にしか送信できない
	- 24時間あたり200通までの送信制限 / 1秒あたり1メッセージの受信制限
- サンドボックスモードを解除するには[申請](http://aws.amazon.com/ses/extendedaccessrequest/)する
	- 制限はリージョン毎に行われる
	- 詳しくは公式のドキュメントを参照してください [Amazon SES サンドボックスの外への移動](https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/request-production-access.html)

### DKIMに対応する

メールの正当性を証明するためにSPFとDKIMという認証があります。AWS SESでは、SPFの設定をする必要はありません。代わりにDKIMは必ず設定するようにしましょう。

設定方法は簡単で、DKIMを使用するにように設定し、DNSに発行されたCNAMEレコードを3つ登録します。その後、DKIMを有効にして数分待つと認証されます。

![ses_dkim.png](https://qiita-image-store.s3.amazonaws.com/0/2152/0e2556a8-1583-2334-1e19-d052e8df2519.png "ses_dkim.png")

#### DKIMの確認

DKIMがちゃんと反映されているか確認するには、SES経由でメールを送信し、受信したメールのヘッダに`DKIM-Signature`ヘッダがあるかどうかを見ます。2つの`DKIM-Signature`ヘッダがあるのが確認できると思います。

```
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple;
	s=zxgy5rdfasdosaxjw5dfdaadxxf7x6o; d=example.com; t=1455598717;
	h=Date:From:To:Message-ID:Subject:Mime-Version:Content-Type:Content-Transfer-Encoding;
	bh=hEIqR+uEInm6TP28390btfxKLhp/20N76HMKtw1xflQ=;
	b=YQpD7sSHpsgnpvmYq2of169DHgsWmPGfRBSICX+8LD/TdJfjyiJF0fqLnoPfrKW9
	DOey/69nh8t7tLN2gGttHTMUuEQ98ZDTiQ2whotVrixskadvymzhS5JUv7mKPDd8J4A
	CcyqWrxxY7wf7GU/UoCKEln2lc8JXZeBBygFlG8E=
DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple;
	s=7v7vs6w4dfaadk5mmtsdfadgaai6n; d=amazonses.com; t=1455598717;
	h=Date:From:To:Message-ID:Subject:Mime-Version:Content-Type:Content-Transfer-Encoding:Feedback-ID;
	bh=hEIqR+uEInm6TP28390btfxKLhp/20N76HMKtw1xflQ=;
	b=bYi11ZlmdjM8COFF956FHp54ElT/6uwam1O+ghVfY7gKOPyBXrwdMMCtJdAvD+kN
	FnosjWwH9E4UCqzwETPjWb/qB8drE4PhH8JeT5JzaWgUxc9JlyRNyCWWBF2WLP5T/iI
	U+A0eoDkxxSm4J3WRfKvqJWccTV1ePy68YxowPXI=
```

### バウンスと苦情の対応

AWS SESに限った話じゃないですが、バウンスメールや苦情の対応はSESでは特に気にしてあげる必要があります（バウンスメールとは何らかの理由でメールが正常に届かなかった旨を知らせるメールです）。対応方法は、一言で言えば配信リストをクリーンに保ち続ける、です。

どれくらいの率を保てばいいかは、AWSの[SESベストプラクティスホワイトペーパー](http://d36cz9buwru1tt.cloudfront.net/jp/wp/AWS_Amazon_SES_Best_Practices.pdf)に記載されています。

>バウンス率は 5% 以下に維持
>苦情率は 0.1% 未満に維持

#### バウンス/苦情を通知する

SESには、3種類の通知機能があります。ここからSNSのトピックを関連付けることができます。SNSは、メールやHTTP／SQS／Lambdaなどにデータを投げられます。

- Bounce Notifications（バウンス）
- Complaint Notifications（苦情）
- Delivery Notifications（配信）

そこで上2つの通知機能を使って、配信リストに反映させるようにします。イメージにするとこんな感じです。キューイングじゃなくてSNSから直接HTTPのWebAPIにアクセスしても良いと思います。

![ses-notification.png](https://qiita-image-store.s3.amazonaws.com/0/2152/749eaedc-e4f4-ced5-fc5f-a506a43a713c.png "ses-notification.png")

#### バウンスをシミュレートする

実際にバウンスのシミュレートするには、AWSが用意してくれているメールアドレスを使用します。自前でダミーのメールアドレスを用意してバウンスさせまくっていると、バウンス率が上がって死ぬので注意です。

- [Amazon SES E メール送信のテスト](https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/mailbox-simulator.html)
	- バウンス : bounce@simulator.amazonses.com
	- 苦情 : complaint@simulator.amazonses.com

#### SNS to SQS

SNSからSQSにキューを投げられるように、SQSのパーミッション設定を行います。

- プリンシパル：全員
- アクション：SendMessage
- 条件：
	- 条件：ArnEquals
	- キー：aws:SourceArn
	- 値：SNSのARN

![SQSパーミッション.png](https://qiita-image-store.s3.amazonaws.com/0/2152/5e8b02f5-a250-9f81-6582-2dc46374b0ee.png "SQSパーミッション.png")

あとは、SNSのトピックにSQSのSubscriptionを追加すれば連携は完了です。

#### SQS to Worker

これでSQSに格納されているSESのバウンスメールの結果がJSON形式で取得できます。実用的なコードではないですが…。

```sqs_worker.rb
require 'aws-sdk'
require 'json'

sqs = Aws::SQS::Client.new(region: 'ap-northeast-1', profile: 'プロファイル名')

msg = sqs.receive_message({queue_url: 'https://sqs.us-west-2.amazonaws.com/SQSのキューURL'})

msg_body = JSON.parse(msg.messages[0].body)
JSON.parse(msg_body["Message"])
```

以下の様なJSONが得られます。`bouncedRecipients`を参照して配信リストに反映させるようにすればOKです。

```
{
  "notificationType": "Bounce",
  "bounce":
  {
    "bounceSubType": "General",
    "bounceType": "Permanent",
    "reportingMTA": "dsn; a27-11.smtp-out.us-west-2.amazonses.com",
    "bouncedRecipients": [
      {
        "action": "failed",
        "emailAddress": "bounce@simulator.amazonses.com", 
        "status": "5.1.1", 
        "diagnosticCode": "smtp; 550 5.1.1 user unknown"
      }
    ],
    "timestamp": "2016-02-17T08:56:03.795Z",
    "feedbackId": "00000152ee718740-4f23798c-ae91-4be4-ad8c-b963675cc7df-000000"
  },
  "mail": 
  {
    "timestamp": "2016-02-17T08:56:02.000Z",
    "source": "hoge@example.com",
    "sourceArn": "arn:aws:ses:us-west-2:000000000000:identity/hoge@example.com",
    "messageId": "00000152ee71825f-3fb1d588-2263-40ed-84ae-aa6128218ea7-000000",
    "destination": ["bounce@simulator.amazonses.com"],
    "sendingAccountId": "1234567890"
  }
}
```

## 送信制限を超えた場合

SESには2つの送信制限があります。

- 送信クォータ：24時間当たりに送信できるメールの最大数
- 最大送信レート：SESが1秒あたりにアカウントから受け付けるメールの最大数

どちらも設定値に関しては申請することで引き上げることができますし、正常に配信し続けていると勝手に引き上げてくれます。ただ、突如想定以上のメール送信がされて、制限を超えてしまった場合を考えてエラーハンドリングをしましょう。

SMTPで送信している場合は、下記のようなエラーが返されるのでエラー処理をします。

- 454 Throttling failure: Maximum sending rate exceeded
- 454 Throttling failure: Daily message quota exceeded

Rubyの簡単な例です。どちらも`Net::SMTPServerBusy`な例外を投げるので、再送処理ができるようにキューに投げたり、エラー通知をしたりすると良いと思います。

```mail.rb
require 'mail'

begin
  Mail.deliver do
    from    'hoge@example.com'
    to      'piyo@example.com'
    subject 'test'
    body    'test'
  end
rescue Net::SMTPServerBusy => e
  p e.class
  #=> Net::SMTPServerBusy
  
  p e.message
  # 下記どちらかのエラー
  #=> "454 Throttling failure: Maximum sending rate exceeded.\n"
  #=> "454 Throttling failure: Daily message quota exceeded.\n"
  
  # ここで再送処理ができるようにキューに投げたりする
end
```

## 配信メトリクスを監視する

バウンス率や迷惑メール率に変動がないかを監視します。なぜかSESメトリクスはCloudWatchに対応していない？ため、自前でSDKを使って監視する必要があります。

```status.rb
require 'aws-sdk'

ses = Aws::SES::Client.new(region: 'us-west-2', profile: 'hoge')

statuses = []
ses.get_send_statistics.send_data_points.each do |s|
  statuses << s
end

# なぜか時系列順になってないのでソートする
statuses.sort!{|a,b|a[:timestamp] <=> b[:timestamp]}

# 最新の情報を表示
statuses.last
#=> #<struct Aws::SES::Types::SendDataPoint
#     timestamp=2016-02-25 01:46:00 UTC,
#     delivery_attempts=351,
#     bounces=0,
#     complaints=0,
#     rejects=0>
```

これでバウンス率や迷惑メール率などが取得できるようになりました。なお、この情報は15分間隔で更新されるようです。

### 現在の制限値を取得する

`get_send_quota`で現在の制限値を取得することもできます。

```
ses.get_send_quota
#=> #<struct Aws::SES::Types::GetSendQuotaResponse
#     max_24_hour_send=200.0,
#     max_send_rate=1.0,
#     sent_last_24_hours=431.0>
```

## SESで微妙なところ？

- 日本国内モバイルキャリアとは相性が良くないらしい
	- 実務上では迷惑メールフィルタでバウンスしてブラックリスト入りしたケース以外では体感できなかった
- ブラックリスト入りしたアドレスの解除申請がコンパネ経由じゃないといけない
	- ブラックリストはアカウントをまたいで共通なのでつらい