Amazon SESで受信したメールをトリガーに、AWS Lambdaで本文の電話番号へAmazon Connectから自動発信してみた

Amazon SESで受信したメールをトリガーに、AWS Lambdaで本文の電話番号へAmazon Connectから自動発信してみた

Amazon SESで受信したメールをトリガーに、本文から電話番号を抽出してAmazon Connectで自動発信する構成を実装しました。SES、S3、SQS、Lambdaを組み合わせることで、メール送信から約10秒で電話がかかる仕組みの構築方法を紹介します。
2026.08.05

はじめに

以前、Amazon SQS と AWS Lambda を使って、Amazon Connect の StartOutboundVoiceContact API 呼び出しペースを抑える構成を試しました。

https://dev.classmethod.jp/articles/sqs-lambda-amazon-connect-rate-limit/

今回はその構成を拡張し、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

cm-hirai-screenshot 2026-05-26 17.32.54

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 アクションイベントについては、以下のドキュメントが参考になります。

https://docs.aws.amazon.com/ses/latest/dg/receiving-email-action-lambda-event.html

また、Amazon Connect の StartOutboundVoiceContact API には呼び出しレートのクォータがあります。今回の構成では、前回の記事と同様に SQS と Lambda のイベントソースマッピングを使って、API 呼び出しペースを抑えています。

StartOutboundVoiceContact API のクォータは AWS General Reference で確認できます。

https://docs.aws.amazon.com/general/latest/gr/connect_region.html

作成したリソース

今回作成、または利用した主なリソースは以下です。

リソース 名前
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 とし、その他はデフォルト設定で作成しました。

cm-hirai-screenshot 2026-05-26 11.43.30
S3 バケット my-ses-inbound-mail-bucket を作成している画面

S3 バケットポリシーを設定する

SES が S3 バケットに raw email を保存できるように、S3 バケットポリシーを設定します。

SES が S3 バケットへメールを保存するための権限については、以下の AWS 公式ドキュメントが参考になります。

https://docs.aws.amazon.com/ses/latest/dg/receiving-email-permissions.html

今回の例では、以下のポリシーを設定しました。アカウント 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-bucketincoming/ 配下への 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 に送信するための権限設定については、以下のドキュメントも参考になります。

https://docs.aws.amazon.com/AmazonS3/latest/userguide/grant-destinations-permissions-to-s3.html

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 にしています。詳細は以下の記事を参照してください。

https://dev.classmethod.jp/articles/sqs-lambda-amazon-connect-rate-limit/

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 の作り方

ClientTokenStartOutboundVoiceContact 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 のドキュメントも参考になります。

https://docs.aws.amazon.com/ja_jp/connect/latest/APIReference/API_StartOutboundVoiceContact.html#connect-StartOutboundVoiceContact-request-ClientToken

Connect SDK のリトライを抑える

今回のコードでは、Amazon Connect クライアントを作成する際に、botocore のリトライ設定を指定しています。

connect = boto3.client(
    "connect",
    config=Config(
        retries={
            "total_max_attempts": 1,
            "mode": "standard"
        }
    )
)

total_max_attempts1 にすることで、SDK 内部での追加リトライを抑えています。

今回の構成では、SQS イベントソースマッピングの最大同時実行数と Lambda 内の sleep によって、StartOutboundVoiceContact API の呼び出しペースを抑えています。
SDK 内部で自動リトライが行われると、Lambda コード上は 1 回だけ API を呼んだつもりでも、実際の API リクエスト数が増える可能性があります。

そのため、Connect API 呼び出しのリトライは SDK 内部ではなく、SQS の再処理に寄せる方針にしました。
StartOutboundVoiceContact API の呼び出しで例外が発生した場合、Lambda は例外を握りつぶさず失敗させます。これにより、SQS メッセージは削除されず、可視性タイムアウト後に再処理されます。

同じ S3 object に対する再処理では ClientToken が同じ値になるため、再処理時も同じ発信リクエストとして扱えます。

botocore の Config については、以下のドキュメントも参考になります。

https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html

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 にしました。

cm-hirai-screenshot 2026-05-26 15.30.48
SES の受信ルール save-mail-to-s3 を作成している画面

受信者の条件には、検証用の受信用メールアドレスを指定しました。

cm-hirai-screenshot 2026-05-26 15.50.00
受信者の条件を指定している画面

アクションには、S3 バケットへの配信を指定します。

設定項目
アクション S3 バケットに配信
S3 バケット my-ses-inbound-mail-bucket
オブジェクトキープレフィックス incoming/

cm-hirai-screenshot 2026-05-26 15.31.43
S3 バケット my-ses-inbound-mail-bucket に raw email を保存する設定

SES の S3 アクションでは、受信メールを raw MIME 形式で S3 に保存できます。

https://docs.aws.amazon.com/ses/latest/dg/receiving-email-action-s3.html

作成した受信ルールを使用するには、受信ルールセットを有効化する必要があります。

cm-hirai-screenshot 2026-05-26 17.44.05
受信ルールセットを有効化している画面

本番利用時の注意

今回の構成では、受信用メールアドレスにメールを送信できる第三者が、本文に E.164 形式の電話番号を記載すると、意図しない電話発信を実行できる可能性があります。

今回は動作検証を目的とした構成ですが、本番環境で利用する場合は、例えば以下のような対策を検討してください。

  • 許可した送信元からのメールだけを処理する
  • SPF、DKIM、DMARC などのメール認証結果を確認する
  • 発信可能な国番号や電話番号を許可リストで制限する
  • 一定時間あたりの発信回数に上限を設定する
  • CloudWatch Logs やメトリクスを監視し、異常な発信を検知する
  • 最初は DRY_RUN=true で動作を確認する

メールの From ヘッダーは詐称される可能性があるため、送信元メールアドレスだけで許可判定を行うのではなく、メール認証結果などと組み合わせて判定する必要があります。

動作確認

メール本文に電話番号を書いて、検証用の受信用メールアドレスにメールを送信しました。

メール本文は以下です。

Please call +819012345678

cm-hirai-screenshot 2026-05-26 17.32.54

最初は 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 で読み取る構成にすることで、メール本文を解析して後続処理につなげられました。

この記事をシェアする

関連記事