Amazon Simple Email ServiceをSMTPプロトコルで利用する

SESをSMTPで利用した時のメモです。Bounce Mailの管理も設定します。

送信先として利用するテスト用Emailアドレスの検証

初回利用開始時、SESはsandbox状態となっています。sandbox状態では、送信先となるEmailアドレスに対して制限をかけられており、事前に対象アドレスを検証する必要があります。その他sandbox状態での影響は、以下を参照。

Moving out of the Amazon SES sandbox

以下、検証作業を実施します。公式ドキュメント。

Verifying an email address

送信先としたいEmailアドレスを指定して実行します。

$ aws ses verify-email-identity --email-address xxx@gmail.com --region=ap-northeast-1 --profile xxx

aws ses verify-email-identity

該当Emailアドレスに、Verify処理用のメールが届くため、メール本文内のVerify処理をクリック。有効時間は24時間です。

The link in the verification message expires 24 hours after the message was sent. If 24 hours have passed since you received the verification email, repeat steps 1–5 to receive a verification email with a valid link.

検証されたことの確認。

$ aws ses list-identities --identity-type EmailAddress --region ap-northeast-1 --profile xxx
{
  "Identities": [
    "xxx@gmail.com"
  ]
}

aws ses list-identities

送信元として利用するドメインの検証

メール送信元とするドメイン情報を検証します。本処理は、非sandbox状態であっても必要となります。以下、公式ドキュメント。

Verifying a domain with Amazon SES

ドメイン検証のコマンドを実行。

$ aws ses verify-domain-identity --domain goodbyegangster.com --profile xxx
{
  "VerificationToken": "RkfXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}

aws ses verify-domain-identity

登録されたドメイン(今回の場合は goodbyegangster.com )のVerification Statusは、 pending vertification ステータスとして作成されます。

検証処理を実施するため、登録ドメインを管理するDNSサーバーに、以下のTXTレコードを登録します。

Name Type Value
_amazonses.goodbyegangster.com TXT RkfXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  • Name文字列
    • _amzonses.sample.com より、 sample.com の部分を利用したいドメイン名に変更した値
  • Value文字列
    • ドメイン登録時に取得したVerificationTokenの値

Amazon SES domain verification TXT records

DNSレコードによる検証処理が完了すると、Verification Statusが verified に変更されます。

SMTP利用の初期設定

SESでSMTPプロトコルを利用するための設定を実施します。

Obtaining your Amazon SES SMTP credentials

SESコンソールページの SMTP Settings より、 Create My STMP Credentials をクリックします。

f:id:goodbyegangster:20210401105634p:plain

SMTP通信用資格情報を作成するための、IAMユーザーを作成します。適当なIAM User名を入力して、 Create をクリックします。

f:id:goodbyegangster:20210401105652p:plain

SMTP通信用の資格情報が作成されるため、この情報を保存しておきます。

f:id:goodbyegangster:20210401105711p:plain

コマンドラインから送信テスト

メール送信できるようなっている筈のため、送信テストをしてみます。コマンドラインからSMTP送信テストするための方法を、公式ドキュメントに記載してくれています。

Test your connection to the Amazon SES SMTP interface using the command line

上で取得したSMTP通信用資格情報の SMTP UsernameSMTP Password の、Base64エンコードした値を取得します。

$ echo -n "AKIXXXXXXXXXXXXXXXXX" | openssl enc -base64
$ echo -n "BINXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" | openssl enc -base64

以下の通り、SMTP通話用ファイルを用意します。

input.txt

EHLO mail.goodbyegangster.com
AUTH LOGIN
QUtXXXXXXXXXXXXXXXXX                           <-Base64EncodedSMTPUserName
QklXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX   <-Base64EncodedSMTPPassword
MAIL FROM: sample@mail.goodbyegangster.com
RCPT TO: xxx@gmail.com
DATA
From: sample@mail.goodbyegangster.com
To: xxx@gmail.com
Subject: Amazon SES SMTP Test

This message was sent using the Amazon SES SMTP interface.
.
QUIT
文字列 説明
mail.goodbyegangster.com 送信元ドメイン
Base64EncodedSMTPUserName BASE64エンコードしたSMTP Username
Base64EncodedSMTPPassword BASE64エンコードしたSMTP Password
sample@mail.goodbyegangster.com 送信元Mailアドレス
xxx@gmail.comf 送信先Mailアドレス

以下を実行します。

$ openssl s_client -crlf -quiet -starttls smtp -connect email-smtp.ap-northeast-1.amazonaws.com:587 < input.txt
depth=2 C = US, O = Amazon, CN = Amazon Root CA 1
verify return:1
depth=1 C = US, O = Amazon, OU = Server CA 1B, CN = Amazon
verify return:1
depth=0 CN = email-smtp.ap-northeast-1.amazonaws.com
verify return:1
250 Ok
250-email-smtp.amazonaws.com
250-8BITMIME
250-STARTTLS
250-AUTH PLAIN LOGIN
250 Ok
334 XXXXXXXXXXXX
334 XXXXXXXXXXXX
235 Authentication successful.
250 Ok
250 Ok
354 End data with <CR><LF>.<CR><LF>
250 Ok 0106017815d23993-ee75503d-90e2-43a7-8d72-dc80ffc25347-000000
451 4.4.2 Timeout waiting for data from client.

送信先Mailアドレスにメールが届いています。以下Gmailの画面より。

f:id:goodbyegangster:20210401105743p:plain

送信元(ENVELOPE FROM)が ap-northeast-1.amazonses.com 、署名元が amazonses.com となっていることに注目。

DMARCの設定

DMARC(Domain-based Message Authetication, Reporting, and Conformance)とは、

DMARC (Domain-based Message Authentication, Reporting, and Conformance)とは、 電子メールにおける送信ドメイン認証技術の一つであり、 RFC 7489*1で標準化されています。

送信ドメイン認証で用いられる技術には、 SPF (Sender Policy Framework)2やDKIM (DomainKeys Identified Mail)3があります。 前者のSPFは、送信元メールサーバのIPアドレス等が正当なものかどうかを判別する手段です。 そして後者のDKIMは、メールに電子署名を付加することで、 メールの送信者および内容が改ざんされていないかどうかを検証できるようにするものです。 DMARCは、両者を利用したメールのドメイン認証を補強する技術です。

JPNIC DMARCとは

SPFDKIMの設定をすれば良いみたいです。

カスタムMAIL FROMドメインの設定(SPFレコードの設定)

SPFの設定をする前にカスタムMAIL FROMドメインの設定を実施します。MAIL FROMドメインとは、ENVELOPE FROMのことです。カスタムMAIL FROMドメインを設定する理由を、公式マニュアルに記載してくれています。

Using a custom MAIL FROM domain

ざっくりまとめると。

By default, messages that you send through Amazon SES use a subdomain of amazonses.com as the MAIL FROM domain. Sender Policy Framework (SPF) authentication successfully validates these messages because the default MAIL FROM domain matches the application that sent the email— in this case, Amazon SES.

デフォルトでは、MAIL FROMドメイン(ENVELOPE FROMドメイン)はamazonses.comのサブドメインを利用し、AWS側にてAmazon SESドメイン用のSPF設定を実施してくれているため、SPFの検証は成功します。

There are two ways to achieve DMARC validation: using Sender Policy Framework (SPF), and using DomainKeys Identified Mail (DKIM). The only way to comply with DMARC through SPF is to use a custom MAIL FROM domain, because SPF validation requires the domain in the From address to match the MAIL FROM domain. By using your own MAIL FROM domain, you have the flexibility to use SPF, DKIM, or both to achieve DMARC validation.

DMARC認証でのSPF検証を成功させるには、MAIL FROMドメイン(ENVELOPE FROMドメイン)とFROMアドレス(ヘッダーFROMドメイン)が一致している必要がある、とのこと。このために、カスタムMAIL FROMドメインで、MAIL FROMドメイン(ENVELOPE FROMドメイン)を変更してあげる必要あるのですね。

カスタムMAIL FROMドメイン(ENVELOPE FROMドメイン)にて、利用できるドメインの条件です。

・The MAIL FROM domain has to be a subdomain of the verified identity (email address or domain) that you send email from. For example, mail.example.com is a valid MAIL FROM domain for the domain example.com.

・The MAIL FROM domain shouldn't be a domain that you send email from. If you have to use the MAIL FROM domain in a From address, either disable email feedback forwarding and receive your bounces through Amazon SNS notifications, or ensure that your MAIL FROM domain is not the destination for feedback forwarding. To determine the destination of email forwarding feedback, see Email feedback forwarding destination.

・The MAIL FROM domain shouldn't be a domain that you use to receive email.

設定します。

$ aws ses set-identity-mail-from-domain --identity goodbyegangster.com \
--mail-from-domain mail.goodbyegangster.com \
--behavior-on-mx-failure RejectMessage \
--profile xxx

aws ses set-identity-mail-from-domain

DNSサーバーに、以下のレコードを登録します。

Name Type Value
subdomain.domain.com MX 10 feedback-smtp.region.amazonses.com
subdomain.domain.com TXT "v=spf1 include:amazonses.com ~all"

subdomain.domain.comは利用するカスタムMAIL FROMドメインを、regionにはSESを利用しているリージョンを指定します。

以下はSESコンソール画面での、Verify Status確認画面です。

f:id:goodbyegangster:20210401105814p:plain

上にて、カスタムMAIL FROMドメインSPFレコードの設定を実施している訳なので、カスタムMAIL FROMドメインでのSPF検証も成功するようになっています。

DKIMの設定

続いてDKIMの設定を実施します。公式マニュアルはこちら。

Easy DKIM in Amazon SES

ドメインに対して設定すれば、そのサブドメインに対してもDKIMが有効となるそうです。

設定します。

$ aws ses set-identity-dkim-enabled --identity goodbyegangster.com --dkim-enabled --profile xxx

aws ses set-identity-dkim-enabled

SESコンソール画面の対象ドメインページのDKIM設定画面を確認すると、DKIMのステータスがverification attempts failedとなっており、VerifyするためのCNAMEレコードが生成されています。このCNAMEレコードの先に、DKIMで利用する公開鍵があるだろうと予想しています。このCNAMEレコードをDNSサーバーに登録します。

f:id:goodbyegangster:20210401105844p:plain

DNSレコードを登録すると、ステータスがenabledとなります。

送信テスト

改めて送信テストを実施してみます。

f:id:goodbyegangster:20210401105902p:plain

送信元(ENVELOPE FROM)が mail.goodbyegangster.com 、署名元が goodbyegangster.com となっています。

Gmailのメール・ソース画面では、SPFDKIMの検証をPASSしているかどうか確認できます。

f:id:goodbyegangster:20210401105928p:plain

検索してみると、上記画面にてDMARCの検証をPASSしているかどうかも確認できるぽいのですが、僕のGmailアカウントでは表示されていませんでした(もしくはDMARC検証をしていない?)。フリーのGmailではなく、Google WorkspaceのGmailであれば、表示されるのかもしれません。ちなみに、outlook.comメールの方ではDMARC検証をしているようで、こっちに送信したメールでソースを確認してみたところ、ちゃんとDMARC検証をPASSしていました。

Bounceメール管理用のAWSリソース作成

SESでは、BouceやComplaint発生時に、SNSと連携する機能を提供してくれています。その機能を利用して、Bouce情報を管理するための仕組みを用意しておきます。以下みたいな、AWSピタゴラスイッチの仕組みとなります。

f:id:goodbyegangster:20210401105948p:plain

必要となるAWSリソース作成用のCloudFormationテンプレートを置いておきます。

https://github.com/goodbyegangster/cloudformation/tree/master/sns-and-kinesis-firehose

SESのNotification設定

goodbyegangster.com ドメインに対して、Bounce発生時にSNSトピックに連携してくれる設定をしておきます。

$ aws ses set-identity-notification-topic \
--identity goodbyegangster.com \
--notification-type Bounce \
--sns-topic arn:aws:sns:ap-northeast-1:XXXXXXXXXXXX:ses_notifications_bounce \
--profile xxx

aws ses set-identity-notification-topic

正しく設定できている場合、S3上に AmazonSnsSubscriptionSucceeded というメッセージが出力されています。

s3://bucket-name/bounce/YYYY/MM/DD/HH/ファイル

{
  "notificationType":"AmazonSnsSubscriptionSucceeded",
  "message":"You have successfully subscribed your Amazon SNS topic 'arn:aws:sns:ap-northeast-1:XXXXXXXXXXXX:ses_notifications_bounce' to receive 'Bounce' notifications from Amazon SES for identity 'goodbyegangster.com'."
}

テスト用Bounceの発生

意図的にBouceを発生することできるメールアドレス bounce@simulator.amazonses.com を用意してくれており、これを利用してテストを行います。

Using the Mailbox Simulator

SMTP通信用ファイルの送信先アドレスに、Bounce用メールアドレスを設定します。

input_bounce.txt

EHLO mail.goodbyegangster.com
AUTH LOGIN
QUtXXXXXXXXXXXXXXXXX
QklXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
MAIL FROM: sample@mail.goodbyegangster.com
RCPT TO: bounce@simulator.amazonses.com
DATA
From: sample@mail.goodbyegangster.com
To: bounce@simulator.amazonses.com
Subject: Amazon SES SMTP Test

This message was sent using the Amazon SES SMTP interface.
.
QUIT

メール送信します。

$ openssl s_client -crlf -quiet -starttls smtp -connect email-smtp.ap-northeast-1.amazonaws.com:587 < input_bounce.txt
depth=2 C = US, O = Amazon, CN = Amazon Root CA 1
verify return:1
depth=1 C = US, O = Amazon, OU = Server CA 1B, CN = Amazon
verify return:1
depth=0 CN = email-smtp.ap-northeast-1.amazonaws.com
verify return:1
250 Ok
250-email-smtp.amazonaws.com
250-8BITMIME
250-STARTTLS
250-AUTH PLAIN LOGIN
250 Ok
334 XXXXXXXXXXXX
334 XXXXXXXXXXXX
235 Authentication successful.
250 Ok
250 Ok
354 End data with <CR><LF>.<CR><LF>
250 Ok 01060178163781da-45359d35-aa36-43b3-8879-7a202363eccc-000000
451 4.4.2 Timeout waiting for data from client.

以下のようなBouce情報を記載したファイルが、S3上に作成されます。

s3://bucket-name/bounce/YYYY/MM/DD/HH/ファイル

{
  "notificationType":"Bounce",
  "bounce":{
    "feedbackId":"0106017819e950c6-52016b3a-e1c4-49e0-bb48-2b394b3e4ed7-000000",
    "bounceType":"Permanent",
    "bounceSubType":"General",
    "bouncedRecipients":[{
      "emailAddress":"bounce@simulator.amazonses.com",
      "action":"failed",
      "status":"5.1.1",
      "diagnosticCode":"smtp; 550 5.1.1 user unknown"
    }],
    "timestamp":"2021-03-10T02:13:44.000Z",
    "remoteMtaIp":"xxx.xxx.xxx.xxx",
    "reportingMTA":"dsn; e234-3.smtp-out.ap-northeast-1.amazonses.com"
  },
  "mail":{
    "timestamp":"2021-03-10T02:13:43.258Z",
    "source":"sample@mail.goodbyegangster.com",
    "sourceArn":"arn:aws:ses:ap-northeast-1:XXXXXXXXXXXX:identity/goodbyegangster.com",
    "sourceIp":"xxx.xxx.xxx.xxx",
    "sendingAccountId":"XXXXXXXXXXXX",
    "messageId":"0106017819e94cda-496105c8-061f-4869-82ca-d661289d15aa-000000",
    "destination":["bounce@simulator.amazonses.com"]
  }
}

Athenaで参照

S3上にあるBounce情報を、Athenaで参照できるように設定しておきます。

Bounce情報用のテーブル定義を作成します。

CREATE EXTERNAL TABLE bounce (
  notificationType string,
  bounce struct<
    feedbackId:string,
    bounceType:string,
    bounceSubType:string,
    bouncedRecipients:array<struct<
      emailAddress:string,
      action:string,
      status:string,
      diagnosticCode:string
    >>,
    `timestamp`:string,
    remoteMtaIp:string,
    reportingMTA:string
  >,
  mail struct<
    `timestamp`:string,
    source:string,
    sourceArn:string,
    sourceIp:string,
    sendingAccountId:string,
    messageId:string,
    destination:array<string>
  >
)
PARTITIONED BY
(
  datehour STRING
)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
WITH SERDEPROPERTIES ('ignore.malformed.json' = 'true')
LOCATION 's3://bucket-name/bounce/'
TBLPROPERTIES
(
  "projection.enabled" = "true",
  "projection.datehour.type" = "date",
  "projection.datehour.range" = "2018/01/01/00,NOW",
  "projection.datehour.format" = "yyyy/MM/dd/HH",
  "projection.datehour.interval" = "1",
  "projection.datehour.interval.unit" = "HOURS",
  "storage.location.template" = "s3://bucket-name/bounce/${datehour}"
)

ネストされたJSONをパースするためのCREATE TABLE文の作り方は、以下の公式ブログの情報を参考にしています。

JSONSerDe によるマッピングを使って、入れ子の JSON から Amazon Athena のテーブルを作成する

org.openx.data.jsonserde.JsonSerDeというHive用に用意されたSerde(formatみたいなものなのか?)を利用してパース文を書くこととなり、以下のGitHubのReadmeにも参考を記載してくれていますが、まあ、分かりにくいですね。try&errorを繰り返しているうちに、パースできるCREATE TABLE文が見つかる感じです。

JsonSerde - a read/write SerDe for JSON Data

Kinesis Firehoseにて、日時単位で階層化してファイルを送信しているので、それに応じてパーティション化もしてあげています。随時、動的にパーティション化してくれます。

Partition Projection with Amazon Athena

こんな感じで検索できます。

SELECT notificationtype,
         mail.source,
         mail.timestamp,
         mail.destination
FROM default.bounce
WHERE notificationtype = 'Bounce'
        AND datehour
    BETWEEN '2021/03/31/00'
        AND '2021/04/01/00'

結果。

f:id:goodbyegangster:20210401110106p:plain

今回設定したものはBounceだけですが、ComplaintやDeliveryの情報も、同じように設定できます。