Amazon SES を使ってメールを送ると、普通は送信エラーをその場で検出できません。その場でっていうのは、非同期ではなくって意味です。ここでは SES に Amazon SNS と Amazon SQS を組み合わせて、なるべく送信エラーをその場で検出するコードを紹介します。エラーチェックのためにちょっと時間かかりますが、我慢してください。
処理の概要としてはこんな感じです。
俺コード → 下にある SQS のキューをチェック
↓ メール送信
Amazon SES
↓ 送信エラー等のフィードバック
Amazon SNS
↓ フィードバックのキューイング
Amazon SQS
なんだか分かりにくいですね。エラーがあるときと無さそうなときに分けるとこうなります。
SES でメール送信
↓
(AWS 内で SES から SNS へ送信エラーがフィードバックされる)
↓
(AWS 内で SNS から SQS へフィードバックがキューイングされる)
↓
SQS のキューをチェック
↓
送信エラーが発覚!
SES でメール送信
↓
SQS のキューをチェック...
↓
SQS のキューをチェック...
↓
SQS のキューをチェック...
↓
フィードバックが無いからきっと成功したんだ
(もしかしたらまだエラーになっていないだけかもしれない)
SES, SNS, SQS のつなぎ込みの詳細については公式ドキュメントに譲ります(SES → SNS, SNS → SQS)。それぞれのサービスに対する設定はざっくりこんな感じです。途中で色々な ARN を参照する必要が出てくるので、サービスごとにタブを開いておくと捗ります。
- SES で送信元アドレスを作って、フィードバック設定に SNS のトピックを設定する
- SNS でトピックを作り(名前は例えば ses-feedback)、サブスクリプションにキューを設定する
- SQS でキューを作り、SNS のトピックに対して SendMessage パーミッションを設定する
この設定ができたら、送信エラー等のフィードバックが SQS に入ってくるか確認しましょう。コンソールで SES を開くなどして bounce@simulator.amazonses.com
にメールしてください。このアドレスは SES の sandbox に書いてあるサンドボックス用アドレスです。サンドボックス外の人は zan-nen@sayaka.chan
とかに送ってみても OK です。
メールを送信したら SQS のコンソールを開いてください。念のため右上の Refresh を押して Messages Available が 1 になってたらめでたく成功です。中身はたぶんこんな感じです。
{
"Type" : "Notification",
"MessageId" : "212f5821-b209-47bc-84c8-8cbed9de7c69",
"TopicArn" : "arn:aws:sns:us-east-1:略:ses-feedback",
"Message" : "JSONをエスケープしたもの",
"Timestamp" : "2012-10-24T12:18:31.531Z",
"SignatureVersion" : "1",
"Signature" : "略",
"SigningCertURL" : "略",
"UnsubscribeURL" : "略"
}
ふぅ……ここまでで環境設定は終わり。ここからはコーディングです。言語は Python、ライブラリは boto を使っています。Java でも aws-sdk 使えば同じ処理を書くことができます。
# -*- coding: utf8 -*-
import boto
from boto.sqs.message import RawMessage
import json
import time
AWS_KEY = "YOUR_AWS_KEY"
AWS_SECRET_KEY = "YOUR_AWS_SECRET_KEY"
def send_email(source, subject, body, to_addresses, timeout=5):
# SES、SQS へのコネクションを作成する。
ses = boto.connect_ses(AWS_KEY, AWS_SECRET_KEY)
sqs = boto.connect_sqs(AWS_KEY, AWS_SECRET_KEY)
# キューの取得。RawMessage を指定しなかったらえらいことになった
queue = sqs.get_queue("ses-feedback")
queue.set_message_class(RawMessage)
# ここまで下準備。ほんとはこの関数の外でやったほうがいい。
# メールを送信する
ses_resp = ses.send_email(source, subject, body, to_addresses)
# メール送信結果の messageId を記録しておく。
# キューの中を検索するときのキーとして使える唯一無二の文字列。
ses_message_id = ses_resp["SendEmailResponse"]["SendEmailResult"]["MessageId"]
print "Sent mail %s" % ses_message_id
# タイムアウトするまでキューを調べる
start_time = time.time()
while time.time() < start_time + timeout:
time.sleep(0.5)
# キューに入っているメッセージを最大10個取得する。
# キューから取ったメッセージはなるべく早く手放したいので visibility timeout は1秒。
# 注: SQS の特性上、メッセージが10個入っていても一度に10個取れない場合がある
sqs_messages = queue.get_messages(10, visibility_timeout=1)
for sqs_message in sqs_messages:
# メッセージの中からフィードバックそのものを取り出す
ses_feedback = json.loads(json.loads(sqs_message.get_body())["Message"])
# フィードバックがあったメール送信結果の messageId を出力
print "Found feedback %s" % ses_feedback["mail"]["messageId"]
# さっき送ったメール送信結果の messageId と一致すればキューから削除しておしまい。
# 見つからなければタイムアウトまでキューをチェックし続ける。
if ses_message_id == ses_feedback["mail"]["messageId"]:
queue.delete_message(sqs_message)
return False
return True
if __name__ == "__main__":
source = "sender として verify したアドレス"
# 実行例
print "should be true..."
print send_email(source, "subject", "body", ["success@simulator.amazonses.com"])
print "should be false..."
print send_email(source, "subject", "body", ["bounce@simulator.amazonses.com"])
処理の説明は、コメントに書いたとおりです。正常にメールを送ることができた場合は、タイムアウトするまで待つことになります。タイムアウトの数字は各自のさじ加減でお願いします。個人的には長くて5秒、なるべく3秒かなぁと思ってます。実際にコードを走らせてみて、送信エラーの検出率を見ながら調整するのがベストですね。
サンプルコードの注意点とか:
- JSON の要素チェックとかザルです。突然の死、あります。
- SES からのフィードバックである bounce(バウンスメール)や complaint(苦情メール)以外がキューに入るケースを考慮していません。SQS への SendMessage を許可してないので入らないと思いますけど。
- お金はほとんどかからないと思います。
- 2012/10 現在、SES のフィードバックは US-East-1 の SNS にしか送れないようなので、日本リージョンへのこだわりがある方は残念でした。
参考にしたサイト: