DockerコンテナからPHP、Postfix、AWS SESでメール送信

# おおまかな構成

PHPのmb_send_mail関数でメールを送るため、PHPと同じコンテナ内にPostfixを構築し、AWS SESを使ってメールを送信します。

# 構築手順

## SESの設定

「[Amazon SES の ID の検証](https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/verify-addresses-and-domains.html)」にしたがって、送信元のメールアドレスを検証しておきます。

任意の宛先にメールを送るためには、「[Amazon SES サンドボックス外への移動](https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/request-production-access.html)」も必要です。

## Dockerfileの記述

```dockerfile:Dockerfile
FROM php:5.6.37-apache

ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update -y \
    && apt-get install -y postfix libsasl2-modules syslog-ng

COPY entrypoint.sh /usr/local/bin/

COPY html/ /var/www/html/

ENTRYPOINT ["entrypoint.sh"]
```

インストールしているのは以下の3つです。

- postfix
- libsasl2-modules
    - SESのドキュメント「[Amazon SES と Postfix の統合](https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/postfix.html#send-email-postfix-prereqs)」にしたがってインストールしています。
    - PostfixがSMTPの認証に使うようです。
- syslog-ng
    - syslog-ngを起動しないと、Postfixのログが/var/log/mail.logに出力されません。[^1]

[^1]: https://stackoverflow.com/questions/46998673/docker-postfix-missing-logs

ENV DEBIAN_FRONTEND=noninteractive は、Postfixのインストール時にインタラクティブな操作が求められるのを避けるための環境変数です。

SMTPの接続情報はコンテナ起動時に環境変数として指定したいので、Postfixの設定はentrypoint.sh内で行います。

## entrypoint.shの記述

```bash:entrypoint.sh
#!/bin/bash -eu

# Postfix setting
sed -i '/relayhost/d' /etc/postfix/main.cf
cat << EOT >> /etc/postfix/main.cf
relayhost = [$SMTP_ENDPOINT]:$SMTP_PORT
smtp_sasl_auth_enable = yes
smtp_sasl_security_options = noanonymous
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_use_tls = yes
smtp_tls_security_level = encrypt
smtp_tls_note_starttls_offer = yes
EOT

echo "[$SMTP_ENDPOINT]:$SMTP_PORT $SMTP_USER_NAME:$SMTP_PASSWORD" > /etc/postfix/sasl_passwd
postmap hash:/etc/postfix/sasl_passwd
chmod 600 /etc/postfix/sasl_passwd /etc/postfix/sasl_passwd.db
postconf -e 'smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt'
cp /etc/resolv.conf /var/spool/postfix/etc/resolv.conf

# PHP setting
cat << EOT > `php -i | grep php.ini | awk '{print $NF}'`/php.ini
[Date]
date.timezone = "Asia/Tokyo"
[mail function]
sendmail_path = /usr/sbin/sendmail -t -i
EOT

service syslog-ng start
service postfix start
exec docker-php-entrypoint apache2-foreground
```

Postfixの設定は、SESのドキュメント「[Amazon SES と Postfix の統合](https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/postfix.html#send-email-postfix-prereqs)」にしたがって実施していますが、2点だけドキュメントと違います。

1. master.cf内の設定変更は実施せず

    master.cfに `-o smtp_fallback_relay=` という行がなかったため、コメントアウトする処理は行いませんでした。

2. resolv.confのコピーを実施

    /var/spool/postfix/etc/resolv.confがないと、email-smtp.us-east-1.amazonaws.com を名前解決できないというエラーが発生しました。

## SMTP認証情報の取得

SESのドキュメント「[Amazon SES SMTP 認証情報の取得](https://docs.aws.amazon.com/ja_jp/ses/latest/DeveloperGuide/smtp-credentials.html)」にしたがって、SMTP接続情報を取得してください。

SMTPの認証情報は、IAMユーザのアクセスキーとは違います。ご注意ください。

## .envを記載

環境変数をDockerfileやdocker-compose.ymlから切り離し、.gitignoreに記載することでコミットを防ぎます。

```bash:.env
SMTP_ENDPOINT=email-smtp.us-east-1.amazonaws.com
SMTP_PORT=587
SMTP_USER_NAME=xxx
SMTP_PASSWORD=xxx
```

## docker-compose.ymlの記述

docker buildやdocker runをシェルスクリプト化する代わりに、docker-compose.ymlを記述します。

```yaml:docker-compose.yml
version: '3'
services:

  php-postfix-ses:
    build: .
    image: php-postfix-ses
    env_file:
      - .env
    ports:
      - "80:80"
```

# 起動

以下のコマンドでコンテナが起動します。

```bash
$ docker-compose up -d
```

イメージをビルドしていない場合、ビルドが走ります。

ビルドだけ実行したい場合は、以下のコマンドです。

```bash
$ docker-compose build
```

# 動作確認

## sendmailでコンテナ内からメール送信

```bash
cat << EOT | sendmail -f from@example.com to@example.com
From: from@example.com
To: to@example.com
Subject: Hello World!
Hello World!
.
EOT
```

SESでは検証済みのメールアドレスからの送信のみを許可しているため、sendmailの-fオプションでenvelop-fromを設定することが必須です。

## PHPでコンテナ内からメール送信

mb_send_mailでメールを送るコードを記述します。

```php5:mail_sample.php
<?php
    mb_language("Japanese"); 
    mb_internal_encoding("UTF-8");

    $to = 'to@example.com';
    $from = 'from@example.com';
    $subject = 'Hello World!';
    $body = 'こんにちは！';
    $header = 'From: ' . $from;

    mb_send_mail($to, $subject, $body, $header, '-f ' . $from);
?>
```

sendmailコマンドの場合と同様に、mb_send_mailの第5引数の-fでenvelope-fromを設定することが必須です。

docker-compose execでコンテナに接続し、phpコマンドでプログラムを実行します。

```
$ docker-compose exec php-postfix-ses bash
# php mail_sample.php
```

日本語でも問題なく送れます。

## ログ確認

PHPのログ確認は以下のコマンドです。

```bash
$ docker-compose logs -f
```

Postfixのログ確認は以下のコマンドです。

```
$ docker-compose exec php-postfix-ses bash
# tail -f /var/log/mail.log
```

# こうすればよかった...

以下の2つの理由から、PHPとPostfixのコンテナは分けるべきでした。

- この構成ではPostfixのログが/var/log/mail.logに出力されるが、本来なら標準出力に出したい。しかし、標準出力にPostfixのログを出すと、PHPのログと混ざる
- Postfixだけのコンテナなら、サンプルが大量に見つかる

なぜ 1 コンテナ 1 プロセスにすべきと言われるか、身をもって学びました。。。