2013年1月9日水曜日

SESってなんじゃ?(突然postfixでSMTPリレー)

先日、突然EC2からのメール送信が必要ということが発覚したときの顛末を書いてみます。

公開日当日に、突然以下のような内容のメールが届きました。

Dear EC2 Customer,
You recently reached a limit on the volume of email you were able to send out of SMTP port 25 on your instance:xxxxxxx

EC2内のMTAからメールを送信するプログラムでは、そのままだと、上記のようなメールが届き、メールの送信制限に引っかかります。

そのため、メールの上限解除申請フォームから解除申請とメールドメインの逆引き申請を行う必要がありました。

この時点で例えば以下の内容(メールアドレスやサーバー名などはサンプルです)で、メールを送ってみると、
# php -a
> mail("memorycraft@gmail.com", "Subject1", "Body", "From: server@memorycraft.jp");

この時点ではメールヘッダの内容は、例として以下のように、なっていました。
Return-Path: <root@ip-10-132-10-146.localdomain>
Received: from ip-10-132-10-146.localdomain (ec2-54-248-82-123.ap-northeast-1.compute.amazonaws.com. [54.248.82.123])
        by mx.google.com with ESMTP id qc4si62972240pbb.326.2013.01.08.08.20.17;
        Tue, 08 Jan 2013 08:20:17 -0800 (PST)
Received-SPF: neutral (google.com: 54.248.82.123 is neither permitted nor denied by best guess record for domain of root@ip-10-132-10-146.localdomain) client-ip=54.248.82.123;
Authentication-Results: mx.google.com; spf=neutral (google.com: 54.248.82.123 is neither permitted nor denied by best guess record for domain of root@ip-10-132-10-146.localdomain) smtp.mail=root@ip-10-132-10-146.localdomain
Received: by ip-10-132-10-146.localdomain (Postfix, from userid 0)
 id E5D5C1E1D; Wed,  9 Jan 2013 01:20:16 +0900 (JST)

Received-SPFとAuthentication-Resultsがneutralなのがまずそうです。
これらがpassになっていないと、メーラーによっては迷惑メールとして扱われてしまうことがあるようです。


一応申請したものの、すぐに解除されるわけではないので、次善策としてSESを利用するのはどうかという意見がありました。
そこで、まずプロダクション使用申請フォームからSESのプロダクション申請を行いました。
ただし、これもすぐには通らないので、既にプロダクションモードになっている別の社内アカウントのSESを一時的に使うことにしました。

まず、SESで送信元のメールアドレスを認証させるために、メールアドレスを登録します。



確認メールが送られてくるので、リンクをクリックして認証します。すると以下のような画面が現れます。



また、SESのAPIを使うようなプログラムの変更はなるべく避けたいのでメールを送信しているEC2内のpostfixからリレーしないといけません。
ここで、suz-labブログの記事「PostfixからSESにリレー(stunnel使わない編)」が助けてくれました。

それにしたがって設定していきます。
/etc/ssl/certs/ca-bundle.crtがあることを確認して、/etc/postfix/main.cfの末尾に以下を追記します。
relayhost = email-smtp.us-east-1.amazonaws.com:25
smtp_sasl_auth_enable = yes
smtp_sasl_security_options = noanonymous
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_use_tls = yes
smtp_tls_security_level = encrypt
smtp_tls_note_starttls_offer = yes
smtp_tls_CAfile = /etc/ssl/certs/ca-bundle.crt


次に、sasl_passwordファイルを作ります。
SMTP用のIAMを作るためにAWS ConsoleのSESのSMTP Settingsから「Create My SMTP Credentials」ボタンでアクセスキーとシークレットキーを取得します。
muramasaさんによると、ここではIAMで普通に作成してもダメで、SESのSMTP Settingsから作成しないと行けないそうです。



そして、取得したアクセスキー、シークレットキーを元に、sasl_passwordファイルを作成します。
# echo "email-smtp.us-east-1.amazonaws.com:25 xxxxxxxxxxxxxxxxxxx:yyyyyyyyyyyyyyyyyyy" > /etc/postfix/sasl_password


sasl_passwordファイルを検索テーブルに設定します。
# postmap hash:/etc/postfix/sasl_passwd

再起動します。
# /etc/init.d/postfix restart

これでメールを送ってみます。
# php -a
> mail("memorycraft@gmail.com", "Subject2", "Body", "From: server@memorycraft.jp");


メールが届きません。
/var/log/mailogを見てみると、、
Jan  9 03:06:09 ip-10-132-134-107 postfix/smtp[11252]: warning: SASL authentication failure: No worthy mechs found
Jan  9 03:06:09 ip-10-132-134-107 postfix/smtp[11252]: 9CA2413E5: SASL authentication failed; cannot authenticate to server email-smtp.us-east-1.amazonaws.com[54.243.73.188]: no mechanism available

となっており、SASL認証に必要なライブラリが無いようです。
以下のように、ライブラリをインストールします。
# yum -y install cyrus-sasl-plain cyrus-sasl-md5

もう一度送信してみますが、メールは届きません。。

maillogには以下のようになっていました。
Jan  9 04:04:51 ip-10-132-10-146 postfix/qmgr[31664]: CE9C11E25: from=<root@ip-10-132-10-146.localdomain>, size=362, nrcpt=1 (queue active)
Jan  9 04:04:54 ip-10-132-10-146 postfix/smtp[31726]: CE9C11E25: to=<memorycraft@gmail.com>, relay=email-smtp.us-east-1.amazonaws.com[23.21.84.203]:25, delay=2.5, delays=0.03/0/1.6/0.87, dsn=5.0.0, status=bounced (host email-smtp.us-east-1.amazonaws.com[23.21.84.203] said: 554 Message rejected: Email address is not verified. (in reply to end of DATA command))

Email address is not verified. となっており、メールアドレスが認証されていないようです。 
これは、Fromヘッダでしか送信元を設定していないためで、Return-Path(エンベロープFrom)に送信元メールアドレスをセットしてあげる必要があるようです。


 この場合、送信コマンド側でMTAにReturnPathを渡します。 
PHPのmailコマンドの場合は以下のように第4引数で-fオプションを渡します。
# php -a
> mail("memorycraft@gmail.com", "Subject30", "Body", "From: server@memorycraft.jp", "-fserver@memorycraft.jp");

すると、正常に送信され、届いたメールのヘッダも以下のようにpassになりました。
Received-SPF: pass (google.com: domain of 0000013c1b944998-1b5393d9-728a-496a-8c9d-2bbf57427150-000000@amazonses.com designates 199.255.194.183 as permitted sender) client-ip=199.255.194.183;
Authentication-Results: mx.google.com; spf=pass (google.com: domain of 0000013c1b944998-1b5393d9-728a-496a-8c9d-2bbf57427150-000000@amazonses.com designates 199.255.194.183 as permitted sender) smtp.mail=0000013c1b944998-1b5393d9-728a-496a-8c9d-2bbf57427150-000000@amazonses.com
Return-Path: 0000013c1b944998-1b5393d9-728a-496a-8c9d-2bbf57427150-000000@amazonses.com


ちなみに、

cakePHPの場合
$config['additionalParameters'] = '-f '.$from_address;
$email = new CakeEmail();
$email->transportClass()->config($config);
$email->from(array($from_address=>'Information'))
->to($target)
->subject($mail_title)
->send($mail_message);


FuelPHPの場合
$email = Email::forge();
$email->from($from_address);
$email->to($target);
$email->subject($mail_title);
$email->return_path($from_address);
$email->body($mail_message);
$mail->send();

のようにするようです。
ひとまず解決し、ほっとしました。

意外と見落としがちなメール送信ですが、いざというとき、どの方法をとっても新規からではなんらかの申請が必要になり、即時反映は難しそうでした。
こういった場合のために、常に一時的なリレー用のSESを確保しておくのは有りかも知れません。

以上です。