PythonでAmazon SESからメールを送ってみた

#きっかけ
定期的に社内の複数宛先にメールを一斉送信する必要があり、今まではスプレッドシート+GASでGmailを平和に送っていた。
が！社内のメール送信システムに仕様変更が発生！（誤送信防止システム）
結果、スプレッドシート+GASのGmailが時々うまく動かなくなる :sob: うわぁぁぁぁん
Gmailはダメだ、何か別のメールを・・・・よし、[Amazon SES](https://aws.amazon.com/jp/ses/) にしよう！

#PythonでAmazon SESからのメール送信実験
まずは [Boto3の公式ドキュメント](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) の [SES](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ses.html) をチェック。
ふむふむ、`send_email(**kwargs)` か `send_raw_email(**kwargs)` を使ったら良さそう。
違いは  `send_raw_email(**kwargs)` だとヘッダー指定やファイル添付が出来る様子。
逆に `send_email(**kwargs)` はその辺に対応していない代わりにシンプルと。
ファイル添付はしたいけれど今回はさくっと実装してメールを送れる状態にさっさとしたいから `send_email(**kwargs)` でいこう。

まずBoto3でsesを読み込んで

```python
import boto3

client = boto3.client('ses', 
        aws_access_key_id = accesskey, 
        aws_secret_access_key = secretkey, 
        region_name = region
    )
```

`send_email(**kwargs)` で送信

```python
client.send_email(
        Source = 'sender@example.com',
        Destination = {
            'ToAddresses': [
                'recipient@example.com',
            ]
        },
        Message = {
            'Subject': {
                'Data': 'テストメール',
                'Charset': 'UTF-8'
            },
            'Body': {
                'Text': {
                    'Data': 'テスト本文',
                    'Charset': 'UTF-8'
                }
            }
        }
    )
```

よし、基本はOK。次は実際に使えるものに書き換えていきます。
注意：新規で作ったAmazon SESは機能が制限されており、使用するメールアドレスを事前に登録しておく必要があります。
詳しくは公式の[Amazon SES に関してよくある質問](https://aws.amazon.com/jp/ses/faqs/)の `Q: Amazon SES サンドボックスとは何ですか?` を確認してみてください。

#本番実装
`send_email(**kwargs)` でうまく送信出来ることが確認できたので本番で使えるように実装していきます。
まず最初にAWSの認証情報をコード内に埋め込むのは嫌なのでCSVファイル（IAMからダウンロードするファイル）から読み取るようにします。

```python
import csv

def get_credentials():
    with open('認証情報のCSVファイル') as f:
        credentials_file = csv.reader(f)
        # AWSの認証情報のCSVファイルの1行目が項目名なので2行目を読み出します
        next(credentials_file)
        for credentials_array in credentials_file:
            return credentials_array
```

次にメールの内容は更新がしやすい方がいいと思うので外部テキストファイルから読み取るようにします。
メールタイトルとメール本文を分けたいのでテキストファイル内に `-----ここからメール本文-----` と適当な目標をつけることにします。
メール内容ファイルのイメージ

```tex
テストメール件名
-----ここからメール本文-----
テストメール本文。
```

```python
def get_mail_content():
    with open('メール内容のテキストファイル') as f:
        mail_content_all = f.read()
    mail_content_splitted = mail_content_all.split('\n-----ここからメール本文-----\n')
    return mail_content_splitted
```

宛先のメールアドレスも更新がしやすいように外部CSVファイルから読み取るようにします。

```python
def get_addresses():
    with open('メールアドレスのCSVファイル') as f:
        addresses_file = csv.reader(f)
        for addresses_array in addresses_file:
            return addresses_array
```

これでメールに必要な情報が揃ったので、作った関数から必要な情報を集めて `send_email(**kwargs)` で送信。

```python
# AWSの認証情報取得
access_key = get_credentials()[0]
secret_key = get_credentials()[1]

client = boto3.client('ses',
    aws_access_key_id = access_key,
    aws_secret_access_key = secret_key,
    region_name = 'Amazon SESで利用しているリージョン （まだ日本はない）'
)

# メール内容の取得
mail_title = get_mail_content()[0]
mail_body = get_mail_content()[1]

# メール宛先の取得
to_addresses = get_addresses()
# メールアドレスは配列で渡す必要があるので [ ] で囲む
cc_addresses = ['CCの宛先メールアドレス']

# メール送信
client.send_email(
    Source = '送信元メールアドレス',
    Destination = {
        'ToAddresses': to_addresses,
        'CcAddresses': cc_addresses
    },
    Message = {
        'Subject': {
            'Data': mail_title,
            'Charset': 'UTF-8'
        },
        'Body': {
            'Text': {
                'Data': mail_body,
                'Charset': 'UTF-8'
            }
        }
    }
)
```


これでOK！・・・と思いましたが、公式の [Amazon SES に関してよくある質問](https://aws.amazon.com/jp/ses/faqs/) にこんな説明が
>Q: 1 つの E メールメッセージで指定できる受信者の数には制限がありますか?
>
>Amazon SES を使用して送信するそれぞれのメッセージに対して、最大 50 人の受信者を指定できます。「To:」、「CC:」、「BCC:」フィールドのすべてのアドレスがこの制限に含まれます。50 人以上の受信者に E メールメッセージを送信する場合は、受信者リストを 50 人以下のグループに分割し、各グループに分けてメッセージを送信する必要があります。

ちなみに50人を超えた場合は以下のエラーメッセージが返されるようです。

```
botocore.exceptions.ClientError: An error occurred (InvalidParameterValue) when calling the SendEmail operation: Recipient count exceeds 50.
```

# 50人送信対応
なるほど。
一度に送る宛先を50人以内にする処理が必要ですね。
まずメール送信部分も関数にします。

```python
def send_email(client, to_addresses, cc_addresses, mail_title, mail_body):
    # メール送信処理
    client.send_email(
        Source = '送信元メールアドレス',
        Destination = {
            'ToAddresses': to_addresses,
            'CcAddresses': cc_addresses
        },
        Message = {
            'Subject': {
                'Data': mail_title,
                'Charset': 'UTF-8'
            },
            'Body': {
                'Text': {
                    'Data': mail_body,
                    'Charset': 'UTF-8'
                }
            }
        }
    )
```

このsend_emailを宛先50人ごとに呼び出せば

```python
cc_addresses = ['CCの宛先メールアドレス']
to_addresses = get_addresses()
to_addresses_splitted = []
# 50毎のセット数をloop_countに代入
loop_count = len(to_addresses) // 50 + 1
for i in range(loop_count):
    # セット内の50件（CCメールアドレス含む）を一つずつ取得
    for j in range(50 - len(cc_addresses)):
        # send_emailに渡すメールアドレス一覧のto_addresses_splittedに順番にメールアドレスを入れていく
        try:
            to_addresses_splitted.append(to_addresses[i * 50 + j])
        # 存在しないインデックスを指定した時の例外処理
        except:
            pass
    send_email(client, to_addresses_splitted, cc_addresses, mail_title, mail_body)
    # セット毎にクリアする
    to_addresses_splitted = []
```
おっと、このままだと分割したメール全てがCCメールアドレスに送られてしまいますね。
メール送信としては1回でCCメールアドレスにも1通しか送りたくないので、分割送信の1回目だけCCメールアドレスに送るようにします。

```python
def send_email(client, to_addresses, cc_addresses, mail_title, mail_body, i):
    # 分割1回目だけCCアドレスに送る
    if 0 == i:
        destination_value = {
            'ToAddresses': to_addresses,
            'CcAddresses': cc_addresses
        }
    else:
        destination_value = {
            'ToAddresses': to_addresses
        }

    # メール送信処理
    client.send_email(
        Source = '送信元メールアドレス',
        Destination = destination_value,
        Message = {
            'Subject': {
                'Data': mail_title,
                'Charset': 'UTF-8'
            },
            'Body': {
                'Text': {
                    'Data': mail_body,
                    'Charset': 'UTF-8'
                }
            }
        }
    )
```

# まとめ

```python
import boto3
import csv

def main():
    # AWS情報
    access_key = get_credentials()[0]
    secret_key = get_credentials()[1]

    client = boto3.client('ses',
        aws_access_key_id = access_key,
        aws_secret_access_key = secret_key,
        region_name = 'us-east-1'
    )

    # メール内容の取得
    mail_title = get_mail_content()[0]
    mail_body = get_mail_content()[1]

    # メール宛先の取得
    cc_addresses = ['CCの宛先メールアドレス']
    to_addresses = get_addresses()
    to_addresses_splitted = []
    # 50毎のセット数をloop_countに代入
    loop_count = len(to_addresses) // 50 + 1
    for i in range(loop_count):
        # セット内の50件（CCメールアドレス含む）を一つずつ取得
        for j in range(50 - len(cc_addresses)):
            # send_emailに渡すメールアドレス一覧のto_addresses_splittedに順番にメールアドレスを入れていく
            try:
                to_addresses_splitted.append(to_addresses[i * 50 + j])
            # 存在しないインデックスを指定した時の例外処理
            except:
                pass
        send_email(client, to_addresses_splitted, cc_addresses, mail_title, mail_body, i)
        # セット毎にクリアする
        to_addresses_splitted = []

def get_credentials():
    with open('credentials.csv') as f:
        credentials_file = csv.reader(f)
        # AWSの認証情報のCSVファイルの1行目が項目名なので2行目を読み出します
        next(credentials_file)
        for credentials_array in credentials_file:
            return credentials_array

def get_addresses():
    with open('addresses.csv') as f:
        addresses_file = csv.reader(f)
        for addresses_array in addresses_file:
            return addresses_array

def get_mail_content():
    with open('mail_content.txt') as f:
        mail_content_all = f.read()
    mail_content_splitted = mail_content_all.split('\n---------------------ここからメール本文---------------------\n')
    return mail_content_splitted

def send_email(client, to_addresses, cc_addresses, mail_title, mail_body, i):
    # 分割1回目だけCCアドレスに送る
    if 0 == i:
        destination_value = {
            'ToAddresses': to_addresses,
            'CcAddresses': cc_addresses
        }
    else:
        destination_value = {
            'ToAddresses': to_addresses
        }

    # メール送信処理
    client.send_email(
        Source = '送信元メールアドレス',
        Destination = destination_value,
        Message = {
            'Subject': {
                'Data': mail_title,
                'Charset': 'UTF-8'
            },
            'Body': {
                'Text': {
                    'Data': mail_body,
                    'Charset': 'UTF-8'
                }
            }
        }
    )

if __name__ == "__main__":
    main()
```

これでローカルから心配なくメール送信が出来るようになりました。
ただローカルからだと他の人が送れない（＝メール送信担当になってしまう）のでそのうちAWS Lambda化かなー。

#追記（2020/07/27）
しばらくAmazon SESを使いましたが、やはり他の人が送れないのがネックなのと最大50人の受信者制限がめんどくさいので、Amazon Pinpointを使うことにしました。
Amazon PinpointだとCC指定が出来ないようなので、CC送信は諦めました。
送信元のサンドボックスはSESとPinpointで共通のようなので、SESで設定が出来ていればPinpointでの設定は特に必要ないようです。

#We're hiring!
AIチャットボットを開発しています。
ご興味ある方は [Wantedlyページ](https://www.wantedly.com/projects/266837) からお気軽にご連絡ください！

#参考記事
[Boto3の公式ドキュメント](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html)
[Amazon SES に関してよくある質問](https://aws.amazon.com/jp/ses/faqs/)