📧

Cognito + SES + Lambdaで任意のタイミングでメール送信

2023/12/16に公開

はじめに

この記事はJij Inc. Advent Calendar 2023の16日目の記事です。
はじめまして、株式会社Jij のtaqroです。

概要

Cognito User Poolに登録されているEmailを取得し、SESを利用してメール送信を行うLambda関数を作成しました。これにより、簡単に顧客にメール送信を送ることができるようになりました。本記事ではその説明をします。

背景

Jijでは2週間のスパンのsprintで開発をしています。sprintが完了後、そこでのプロダクトのアップデート内容をプロダクトを利用している顧客に向けてメール送信を行っています。しかし、そこでのメール送信はCEOの山城さんが手動で行っていました。Cognito User Poolに登録されたアドレスを取得しメールを書いていたので、手間と安全性に課題がありました。そこでより簡単かつ安全にメール送信を行える方法が必要となりました。

技術説明

具体的には、Cognito User Poolからemailを取得し、SESでその宛先に(bcc)でメール送信処理を行うLambda関数を作成し、それを実行することで一連の処理を行うようにしました。

準備

Cognito User Pool

Cognito User Poolに顧客情報のemailが登録されていること。

SES

検証済み IDでドメインが登録されていること。その際に、以下のようにDKIMが有効化されていること。
これが有効化されていないとメールを送った際に迷惑メールに振り分けられてしまいます。

https://docs.aws.amazon.com/ja_jp/ses/latest/dg/send-email-authentication-dkim.html

Parameter Store

メールの文面用で利用するために以下のパラメータを設定します。これはメールの文面を変える際にコードを変える必要をなくすためと、Lambda関数が2つありどちらも同じものを参照するためです。つまり、新たにメールを送信する際に編集するのはこの部分のみになります。

email_subject:"メールの件名"
email_body_text:"メールの文面(テキスト形式)"
email_body_html:"メールの文面(HTML形式)"

IAM

準備したAWSリソースにアクセスするための権限をLambda用に作成します。
それぞれ以下のポリシーをLambda用のロールにアタッチします。

  • Cognito
{
    "Version": "2012-10-17",
    "Statement": [
        {
	    "Sid": "VisualEditor0",
            "Action": [
                "cognito-idp:DescribeUserPool",
                "cognito-idp:ListUsers"
            ],
            "Effect": "Allow",
            "Resource": "arn:aws:cognito-idp:{region}:{Account_ID}:userpool/{ユーザープール ID}"
        }
    ]
}
  • SES
{
    "Version": "2012-10-17",
    "Statement": [
        {
	    "Sid": "VisualEditor0",
            "Action": "ses:SendEmail",
            "Effect": "Allow",
            "Resource": "arn:aws:ses:{region}:{Account_ID}:identity/{domain}"
        }
    ]
}
  • parameter store
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "ssm:GetParameter",
                "ssm:GetParameters"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:ssm:{region}:{Account_ID}:parameter/email_subject",
                "arn:aws:ssm:{region}:{Account_ID}:parameter/email_body_text",
                "arn:aws:ssm:{region}:{Account_ID}:parameter/email_body_html"
            ]
        }
    ]
}

Lambda関数

以下のLambda関数を作成しました。

  • email_function_confirm
    社内に実際にメールを送ってみて文面がきちんと表示されるかの確認を行う関数です。送り先アドレスは環境変数から取得しています。
  • email_function_production
    プロダクトのcognito user poolから顧客のアドレスを取得し、一斉送信を行う関数です。

コードは以下になります。

email_function_confirm.py
import boto3
import os
import json
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

# 環境変数から情報を取得
SES_SENDER_EMAIL = os.environ['SES_SENDER_EMAIL']
SES_EMAIL_RECIPIENTS = os.environ.get('SES_EMAIL_RECIPIENTS', '')


# AWS Systems Managerクライアントを初期化
ssm = boto3.client('ssm')

# Parameter Storeからデータを取得する関数
def get_parameter(name):
    response = ssm.get_parameter(Name=name, WithDecryption=True)
    return response['Parameter']['Value']

# メールの内容
EMAIL_SUBJECT = get_parameter('email_subject')
EMAIL_BODY_TEXT = get_parameter('email_body_text')
EMAIL_BODY_HTML = get_parameter('email_body_html')

# SESのクライアントを初期化
ses_client = boto3.client('ses')

def lambda_handler(event, context):
    # 環境変数からメールアドレスを取得し、リストに変換
    email_addresses = [email.strip() for email in SES_EMAIL_RECIPIENTS.split(',')]

    try:
        # SESを使用してメールを送信
        if email_addresses:
            response = ses_client.send_email(
                Source=SES_SENDER_EMAIL,
                Destination={'BccAddresses': email_addresses},
                Message={
                    'Subject': {'Data': EMAIL_SUBJECT},
                    'Body': {
                        'Text': {'Data': EMAIL_BODY_TEXT},
                        'Html': {'Data': EMAIL_BODY_HTML}
                    }
                }
            )

            # メール送信の詳細をログに記録
            print("Email sent successfully.")
            print(f"Sender: {SES_SENDER_EMAIL}")
            print(f"Recipients: {email_addresses}")
            print(f"Subject: {EMAIL_SUBJECT}")
            print(f"Email Body (Text): {EMAIL_BODY_TEXT}")
            print(f"Email Body (HTML): {EMAIL_BODY_HTML}")
            print(f"Response: {json.dumps(response)}")

    except Exception as e:
        print(f"Error: {str(e)}")
        return {"statusCode": 500, "body": "Error: " + str(e)}

    return {"statusCode": 200, "body": "Emails sent successfully"}

email_function_production.py
import boto3
import os

# 環境変数から情報を取得
COGNITO_USER_POOL_ID = os.environ['COGNITO_USER_POOL_ID']
SES_SENDER_EMAIL = os.environ['SES_SENDER_EMAIL']

# AWS Systems Managerクライアントを初期化
ssm = boto3.client('ssm')

# Parameter Storeからデータを取得する関数
def get_parameter(name):
    response = ssm.get_parameter(Name=name, WithDecryption=True)
    return response['Parameter']['Value']

# メールの内容
EMAIL_SUBJECT = get_parameter('email_subject')
EMAIL_BODY_TEXT = get_parameter('email_body_text')
EMAIL_BODY_HTML = get_parameter('email_body_html')

# Cognitoのクライアントを初期化
cognito_client = boto3.client('cognito-idp')
# SESのクライアントを初期化
ses_client = boto3.client('ses')

def lambda_handler(event, context):
    email_addresses = []  # メールアドレスを格納するリスト
    pagination_token = None

    try:
        # Cognitoユーザープールからユーザーのメールアドレスを繰り返し取得(デフォルトでは60件までのため)
        while True:
            if pagination_token:
                response = cognito_client.list_users(
                    UserPoolId=COGNITO_USER_POOL_ID, 
                    PaginationToken=pagination_token
                )
            else:
                response = cognito_client.list_users(UserPoolId=COGNITO_USER_POOL_ID)

            for user in response['Users']:
                for attr in user['Attributes']:
                    if attr['Name'] == 'email':
                        email_addresses.append(attr['Value'])

            pagination_token = response.get('PaginationToken')
            if not pagination_token:
                break

        # メール送信の詳細をログに記録
        print("Email would be sent with the following details:")
        print(f"Sender: {SES_SENDER_EMAIL}")
        print(f"Recipients: {email_addresses}")
        print(f"Subject: {EMAIL_SUBJECT}")
        print(f"Email Body (Text): {EMAIL_BODY_TEXT}")
        print(f"Email Body (HTML): {EMAIL_BODY_HTML}")

        # メール送信
        if email_addresses:
            ses_client.send_email(
                Source=SES_SENDER_EMAIL,
                Destination={'BccAddresses': email_addresses},
                Message={
                    'Subject': {'Data': EMAIL_SUBJECT},
                    'Body': {
                        'Text': {'Data': EMAIL_BODY_TEXT},
                        'Html': {'Data': EMAIL_BODY_HTML}
                    }
                }
            )
            print(f"Email sent to {len(email_addresses)} recipients via BCC.")

    except Exception as e:
        print(f"Error: {str(e)}")
        return {"statusCode": 500, "body": "Error: " + str(e)}

    return {"statusCode": 200, "body": "Emails sent successfully"}

ここでcognitoのlist_usersではデフォルトでは60件までしか取得できませんので工夫が必要でした。解決法としては、pagination_tokenを利用することでUser Poolに登録されているすべてのユーザー情報を取得することができます。

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cognito-idp/client/list_users.html#list-users

https://qiita.com/oiz-y/items/c7313fb273c415976e40

https://zenn.dev/not75743/articles/7a7d3a2fc7e788

メール送信手順

おおまかな流れとしては文章を作成し、それをParamter Storeに反映し、Lambda関数を実行するのみです。

文章作成

プロダクトのアップデート内容の文面、件名を作成します。

Parameter Storeの値に反映

作成された内容を既存の変数に反映させます。

レビュー(きちんと反映されているかどうか)

念のためにparameter storeに正しく反映されているかをダブルチェックを行います。

確認用のLambda関数実行

確認用のemail_function_confirmを実行し、社内に向けてメールを送信します。

メール確認

先ほど送信したメールで想定した表示がされているかを確認します。

本番用のLambda関数実行

確認後、email_function_productionを実行し、顧客に向けてメールを送信します。

まとめ

本記事ではCognito + SES + Lambdaを利用してメール送信を簡単に行う方法を紹介しました。これにより簡単かつ安全に顧客にメール送信ができるようになりました。今後の展望としては、現状直接Parameter StoreやLambdaを触って実行しているので、GUIの作成やSlackと連携をしてより手軽に文面の修正をするなどが考えらます。

おわりに

\Rustエンジニア・数理最適化エンジニア募集中!/
株式会社Jijでは、数学や物理学のバックグラウンドを活かし、量子計算と数理最適化のフロンティアで活躍するRustエンジニア、数理最適化エンジニアを募集しています!
詳細は下記のリンクからご覧ください。皆さんのご応募をお待ちしております!
Rustエンジニア: https://open.talentio.com/r/1/c/j-ij.com/pages/51062
数理最適化エンジニア: https://open.talentio.com/r/1/c/j-ij.com/pages/75132

Discussion