Amazon SESで受信したメールをトリガーに、AWS Lambdaで本文の電話番号へAmazon Connectから自動発信してみた
はじめに
以前、Amazon SQS と AWS Lambda を使って、Amazon Connect の StartOutboundVoiceContact API 呼び出しペースを抑える構成を試しました。
今回はその構成を拡張し、Amazon SES で受信したメールをトリガーに、Lambda でメール本文から電話番号を抽出し、Amazon Connect の StartOutboundVoiceContact API で自動発信する構成を試しました。
やりたいことは以下です。
外部メール
↓
Amazon SES Eメール受信
↓
Amazon S3 に raw email を保存
↓
S3 イベント通知
↓
Amazon SQS
↓
AWS Lambda
↓
Amazon Connect StartOutboundVoiceContact
↓
メール本文の電話番号宛に電話を発信
今回の検証では、メール本文に E.164 形式の電話番号を書いて送信しました。
Please call +819012345678

Lambda で raw email を解析し、本文から電話番号を抽出して StartOutboundVoiceContact API に渡します。
実際に試したところ、メール送信から約 10 秒で電話がかかってくることを確認できました。
前提
今回は以下を前提とします。
- Amazon Connect インスタンスは作成済み
- 発信用の問い合わせフローは作成済み
- Amazon Connect で発信元電話番号を利用できる状態
- Amazon SES の受信用ドメイン ID は作成済み
- 受信用ドメインの MX レコードは Amazon SES Eメール受信のエンドポイントを向いている
- SQS キュー
connect-rate-limited-call-queueは前回の記事の設定で作成済み - 検証リージョンは
ap-northeast-1
SQS キューは前回の記事で作成した connect-rate-limited-call-queue を利用するため、本記事では作成手順を省略します。
ドメイン名やメールアドレスは環境に依存するため、記事中では必要な箇所以外は具体値を省略しています。実際に利用する場合は、自身のドメインとメールアドレスに置き換えてください。
構成と処理の流れ
Amazon SES Eメール受信
↓
受信ルール
↓
S3 に raw email 保存
↓
S3 イベント通知
↓
SQS 標準キュー
↓
Lambda
↓
Amazon Connect StartOutboundVoiceContact
↓
メール本文の電話番号宛に電話を発信
SES の受信ルールでは Lambda アクションを指定することもできますが、Lambda アクションのイベントにはメール本文が含まれません。
そのため、今回は SES で受信したメールを S3 に raw email として保存し、S3 イベント通知を起点に Lambda で S3 からメール本文を読み取る構成にしました。
SES の Lambda アクションイベントについては、以下のドキュメントが参考になります。
また、Amazon Connect の StartOutboundVoiceContact API には呼び出しレートのクォータがあります。今回の構成では、前回の記事と同様に SQS と Lambda のイベントソースマッピングを使って、API 呼び出しペースを抑えています。
StartOutboundVoiceContact API のクォータは AWS General Reference で確認できます。
作成したリソース
今回作成、または利用した主なリソースは以下です。
| リソース | 名前 |
|---|---|
| S3 バケット | my-ses-inbound-mail-bucket |
| SQS キュー | connect-rate-limited-call-queue |
| Lambda 関数 | ses-mail-to-connect-call |
| SES 受信ルール | save-mail-to-s3 |
S3 バケットを作成する
SES で受信した raw email を保存する S3 バケットを作成します。
今回は、バケット名を my-ses-inbound-mail-bucket とし、その他はデフォルト設定で作成しました。

S3 バケット my-ses-inbound-mail-bucket を作成している画面
S3 バケットポリシーを設定する
SES が S3 バケットに raw email を保存できるように、S3 バケットポリシーを設定します。
SES が S3 バケットへメールを保存するための権限については、以下の AWS 公式ドキュメントが参考になります。
今回の例では、以下のポリシーを設定しました。アカウント ID は例示として 111111111111 にしています。実際に利用する場合は、自身の AWS アカウント ID に置き換えてください。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSESPutObject",
"Effect": "Allow",
"Principal": {
"Service": "ses.amazonaws.com"
},
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-ses-inbound-mail-bucket/incoming/*",
"Condition": {
"StringEquals": {
"AWS:SourceAccount": "111111111111"
}
}
}
]
}
このポリシーでは、SES サービスプリンシパルに対して、my-ses-inbound-mail-bucket の incoming/ 配下への s3:PutObject を許可しています。
SQS キューのポリシーを設定する
S3 イベント通知から SQS にメッセージを送信するため、SQS キューのアクセスポリシーに s3.amazonaws.com からの SQS:SendMessage を許可するステートメントを追加します。
今回の例では、SQS キュー connect-rate-limited-call-queue に以下を追加しました。
アカウント ID は例示として 111111111111 にしています。
{
"Sid": "AllowS3SendMessage",
"Effect": "Allow",
"Principal": {
"Service": "s3.amazonaws.com"
},
"Action": "SQS:SendMessage",
"Resource": "arn:aws:sqs:ap-northeast-1:111111111111:connect-rate-limited-call-queue",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "111111111111"
},
"ArnLike": {
"aws:SourceArn": "arn:aws:s3:::my-ses-inbound-mail-bucket"
}
}
}
ここで指定する aws:SourceArn は S3 バケット ARN です。
この指定を誤ると、S3 イベント通知の保存時に以下のエラーが発生しました。
Unable to validate the following destination configurations
S3 イベント通知から SQS に送信するための権限設定については、以下のドキュメントも参考になります。
Lambda 関数を作成する
Lambda 関数は以下の内容で作成しました。
- 関数名:
ses-mail-to-connect-call - ランタイム:Python 3.14
- タイムアウト:30 秒
Lambda の実行ロールには、SQS からメッセージを受け取るために AWSLambdaSQSQueueExecutionRole を付与しました。
追加で、S3 に保存された raw email の読み取りと、Amazon Connect の StartOutboundVoiceContact API 呼び出しのために、以下のインラインポリシーを付与しました。
アカウント ID は例示として 111111111111 にしています。<connect-instance-id> は自身の Amazon Connect インスタンス ID に置き換えてください。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadInboundMailFromS3",
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::my-ses-inbound-mail-bucket/incoming/*"
},
{
"Sid": "StartOutboundVoiceContact",
"Effect": "Allow",
"Action": [
"connect:StartOutboundVoiceContact"
],
"Resource": "arn:aws:connect:ap-northeast-1:111111111111:instance/<connect-instance-id>/contact/*"
}
]
}
Resource は、対象の Amazon Connect インスタンス配下の contact に絞っています。
Lambda には、SQS キュー connect-rate-limited-call-queue をトリガーとして設定しました。
SQS トリガーの設定は前回の記事と同様に、バッチサイズを 1、最大同時実行数を 2 にしています。詳細は以下の記事を参照してください。
Lambda コード
今回の Lambda コードは以下です。
このコードでは、SQS メッセージ本文に含まれる S3 イベント通知から bucket と object key を取り出し、S3 から raw email を読み込みます。
その後、メール本文から E.164 形式の電話番号を正規表現で抽出し、Amazon Connect の StartOutboundVoiceContact API を呼び出します。
import json
import os
import re
import time
import hashlib
import urllib.parse
from email import policy
from email.parser import BytesParser
import boto3
from botocore.config import Config
s3 = boto3.client("s3")
connect = boto3.client(
"connect",
config=Config(
retries={
"total_max_attempts": 1,
"mode": "standard"
}
)
)
CONNECT_INSTANCE_ID = os.environ["CONNECT_INSTANCE_ID"]
CONTACT_FLOW_ID = os.environ["CONTACT_FLOW_ID"]
SOURCE_PHONE_NUMBER = os.environ["SOURCE_PHONE_NUMBER"]
DRY_RUN = os.environ.get("DRY_RUN", "true").lower() == "true"
PHONE_RE = re.compile(r"\+[1-9]\d{1,14}\b")
def lambda_handler(event, context):
print("event:", json.dumps(event, ensure_ascii=False))
for sqs_record in event.get("Records", []):
body = json.loads(sqs_record["body"])
if body.get("Event") == "s3:TestEvent":
print("Received s3:TestEvent. Ignore.")
continue
for s3_record in body.get("Records", []):
bucket = s3_record["s3"]["bucket"]["name"]
key = urllib.parse.unquote_plus(s3_record["s3"]["object"]["key"])
print(f"Processing s3://{bucket}/{key}")
raw_email = get_raw_email(bucket, key)
subject, text = parse_email(raw_email)
destination_phone_number = extract_phone_number(subject + "\n" + text)
if not destination_phone_number:
print(f"No E.164 phone number found in s3://{bucket}/{key}. Skip.")
continue
client_token = make_client_token(bucket, key)
print({
"bucket": bucket,
"key": key,
"subject": subject,
"destinationPhoneNumber": destination_phone_number,
"clientToken": client_token,
"dryRun": DRY_RUN
})
if DRY_RUN:
print("DRY_RUN=true, skip StartOutboundVoiceContact.")
else:
start_outbound_voice_contact(
destination_phone_number=destination_phone_number,
client_token=client_token,
bucket=bucket,
key=key,
subject=subject
)
time.sleep(1)
return {
"statusCode": 200
}
def get_raw_email(bucket, key):
obj = s3.get_object(Bucket=bucket, Key=key)
return obj["Body"].read()
def parse_email(raw_email_bytes):
msg = BytesParser(policy=policy.default).parsebytes(raw_email_bytes)
subject = msg.get("subject", "")
texts = []
if msg.is_multipart():
for part in msg.walk():
content_type = part.get_content_type()
content_disposition = part.get_content_disposition()
if content_disposition == "attachment":
continue
if content_type == "text/plain":
try:
texts.append(part.get_content())
except Exception as e:
print(f"Failed to read text/plain part: {e}")
else:
try:
if msg.get_content_type() == "text/plain":
texts.append(msg.get_content())
else:
texts.append(str(msg.get_content()))
except Exception as e:
print(f"Failed to read message content: {e}")
return subject, "\n".join(texts)
def extract_phone_number(text):
match = PHONE_RE.search(text)
if not match:
return None
return match.group(0)
def make_client_token(bucket, key):
raw = f"{bucket}/{key}"
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
def start_outbound_voice_contact(destination_phone_number, client_token, bucket, key, subject):
params = {
"InstanceId": CONNECT_INSTANCE_ID,
"ContactFlowId": CONTACT_FLOW_ID,
"DestinationPhoneNumber": destination_phone_number,
"SourcePhoneNumber": SOURCE_PHONE_NUMBER,
"ClientToken": client_token,
"Attributes": {
"ClientToken": client_token,
"S3Bucket": bucket,
"S3Key": key[:1024],
"MailSubject": subject[:1024]
}
}
response = connect.start_outbound_voice_contact(**params)
print("StartOutboundVoiceContact response:", response)
return response
環境変数は以下を設定しました。
| 環境変数 | 用途 |
|---|---|
CONNECT_INSTANCE_ID |
Amazon Connect インスタンス ID |
CONTACT_FLOW_ID |
発信用の問い合わせフロー ID |
SOURCE_PHONE_NUMBER |
発信元電話番号。本実装では QueueId を指定していないため必須 |
DRY_RUN |
true の場合は StartOutboundVoiceContact API を呼び出さずログ出力のみ行う |
最初は DRY_RUN=true にして、SES 受信、S3 保存、SQS 通知、Lambda 実行、メール解析までを確認しました。
問題ないことを確認した後、DRY_RUN=false に変更して実際に発信しました。
電話番号の抽出方法
今回のコードでは、以下の正規表現で E.164 形式の電話番号を抽出しています。
PHONE_RE = re.compile(r"\+[1-9]\d{1,14}\b")
そのため、以下のような形式をメール本文に含める必要があります。
+819012345678
一方で、以下のような日本国内表記は今回のコードでは対象外です。
090-1234-5678
09012345678
+81 90 1234 5678
必要に応じて、日本国内表記を E.164 形式に正規化する処理を追加してください。
メール本文の解析対象
今回のコードでは、multipart メールの場合、添付ファイルではない text/plain パートのみを解析対象としています。
そのため、text/html パートしか含まれていないメールでは、本文から電話番号を抽出できません。必要に応じて、HTML からテキストを抽出する処理を追加してください。
また、件名と本文を結合した文字列から、最初に見つかった E.164 形式の電話番号を発信先として使用します。そのため、署名や引用された本文に別の電話番号が含まれている場合は、意図しない電話番号が選択される可能性があります。
ClientToken の作り方
ClientToken は StartOutboundVoiceContact API の冪等性制御に使う値です。
同じ発信リクエストに対して同じ ClientToken を指定することで、同一メールの再処理による二重発信を抑えるために利用します。
今回のコードでは、同じ S3 object に対する再処理では同じ値になるように、以下を SHA-256 でハッシュ化しています。
bucket + "/" + objectKey
def make_client_token(bucket, key):
raw = f"{bucket}/{key}"
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
S3 イベント通知や SQS、Lambda は重複処理が起こり得るため、最終的な発信 API に同じ ClientToken を渡すことで、同じメールに対する二重発信の副作用を抑える狙いです。
ClientToken については、StartOutboundVoiceContact API のドキュメントも参考になります。
Connect SDK のリトライを抑える
今回のコードでは、Amazon Connect クライアントを作成する際に、botocore のリトライ設定を指定しています。
connect = boto3.client(
"connect",
config=Config(
retries={
"total_max_attempts": 1,
"mode": "standard"
}
)
)
total_max_attempts を 1 にすることで、SDK 内部での追加リトライを抑えています。
今回の構成では、SQS イベントソースマッピングの最大同時実行数と Lambda 内の sleep によって、StartOutboundVoiceContact API の呼び出しペースを抑えています。
SDK 内部で自動リトライが行われると、Lambda コード上は 1 回だけ API を呼んだつもりでも、実際の API リクエスト数が増える可能性があります。
そのため、Connect API 呼び出しのリトライは SDK 内部ではなく、SQS の再処理に寄せる方針にしました。
StartOutboundVoiceContact API の呼び出しで例外が発生した場合、Lambda は例外を握りつぶさず失敗させます。これにより、SQS メッセージは削除されず、可視性タイムアウト後に再処理されます。
同じ S3 object に対する再処理では ClientToken が同じ値になるため、再処理時も同じ発信リクエストとして扱えます。
botocore の Config については、以下のドキュメントも参考になります。
S3 イベント通知を設定する
S3 バケット my-ses-inbound-mail-bucket に S3 イベント通知を設定します。
設定内容は以下です。
| 設定項目 | 値 |
|---|---|
| イベント名 | notify-incoming-mail-to-sqs |
| プレフィックス | incoming/ |
| イベントタイプ | すべてのオブジェクト作成イベント |
| 送信先 | SQS キュー |
| SQS キュー | connect-rate-limited-call-queue |
これにより、SES が incoming/ 配下に raw email を保存すると、S3 イベント通知によって SQS にメッセージが送信されます。
SES 受信ルールを作成する
Amazon SES の Eメール受信で受信ルールを作成します。
今回は、受信ルール名を save-mail-to-s3 にしました。

SES の受信ルール save-mail-to-s3 を作成している画面
受信者の条件には、検証用の受信用メールアドレスを指定しました。

受信者の条件を指定している画面
アクションには、S3 バケットへの配信を指定します。
| 設定項目 | 値 |
|---|---|
| アクション | S3 バケットに配信 |
| S3 バケット | my-ses-inbound-mail-bucket |
| オブジェクトキープレフィックス | incoming/ |

S3 バケット my-ses-inbound-mail-bucket に raw email を保存する設定
SES の S3 アクションでは、受信メールを raw MIME 形式で S3 に保存できます。
作成した受信ルールを使用するには、受信ルールセットを有効化する必要があります。

受信ルールセットを有効化している画面
本番利用時の注意
今回の構成では、受信用メールアドレスにメールを送信できる第三者が、本文に E.164 形式の電話番号を記載すると、意図しない電話発信を実行できる可能性があります。
今回は動作検証を目的とした構成ですが、本番環境で利用する場合は、例えば以下のような対策を検討してください。
- 許可した送信元からのメールだけを処理する
- SPF、DKIM、DMARC などのメール認証結果を確認する
- 発信可能な国番号や電話番号を許可リストで制限する
- 一定時間あたりの発信回数に上限を設定する
- CloudWatch Logs やメトリクスを監視し、異常な発信を検知する
- 最初は
DRY_RUN=trueで動作を確認する
メールの From ヘッダーは詐称される可能性があるため、送信元メールアドレスだけで許可判定を行うのではなく、メール認証結果などと組み合わせて判定する必要があります。
動作確認
メール本文に電話番号を書いて、検証用の受信用メールアドレスにメールを送信しました。
メール本文は以下です。
Please call +819012345678

最初は Lambda の環境変数 DRY_RUN=true の状態で動作確認しました。
CloudWatch Logs には以下のようなログが出力されました。
Processing s3://my-ses-inbound-mail-bucket/incoming/4cklatksvuq0qc90g1k73lufosj88q5vueag8eg1
{
'bucket': 'my-ses-inbound-mail-bucket',
'key': 'incoming/4cklatksvuq0qc90g1k73lufosj88q5vueag8eg1',
'subject': 'テストです。',
'destinationPhoneNumber': '+819012345678',
'clientToken': '9ec31a9179cce2d6ddfafc1b228ff4721a1cbe41f6f6f050d4a7a18ec4f6abc8',
'dryRun': True
}
DRY_RUN=true, skip StartOutboundVoiceContact.
このログから、以下を確認できました。
- SQS 経由で Lambda が起動した
- S3 イベント通知から S3 bucket と object key を取得できた
- S3 に保存された raw email を読み取れた
- メール件名を取得できた
- メール本文から
+819012345678を抽出できた ClientTokenを生成できたDRY_RUN=trueのため、実際の発信はスキップされた
その後、Lambda の環境変数を以下のように変更しました。
- DRY_RUN=true
+ DRY_RUN=false
再度メールを送信したところ、メール送信から約 10 秒で電話がかかってくることを確認できました。
今回の検証では、以下の一連の流れが動作しました。
メール送信
↓
SES で受信
↓
S3 に raw email 保存
↓
S3 イベント通知で SQS に通知
↓
Lambda 起動
↓
メール本文から電話番号を抽出
↓
StartOutboundVoiceContact API 呼び出し
↓
電話発信
まとめ
Amazon SES で受信したメールを S3 に保存し、S3 イベント通知、SQS、Lambda を経由して Amazon Connect の StartOutboundVoiceContact API を呼び出す構成を試しました。
今回の検証では、メール本文に書かれた E.164 形式の電話番号を Lambda で抽出し、メール送信から約 10 秒で電話がかかってくることを確認できました。
SES の Lambda アクションではメール本文を直接扱わず、S3 に保存した raw email を Lambda で読み取る構成にすることで、メール本文を解析して後続処理につなげられました。

