mailman3 で発信用MLサーバを docker compose で構築

# 目的
ML管理したいが、そのためにメールサーバを設定するのはセキュリティとか色々と考慮する必要があり大変。
リレー先メールサーバさえ転送を受け付けてくれれば、発信専門のMLを構築できそう。

ということで、コンテナでサクッと発信専門のMLサーバ構築したので以下にその手順を示す。

# 注意事項
リレー先メールサーバが転送を受け付けてくれる前提であり、メールサーバ側の設定については関与しない。
発信専門であるため、発信元メアドに送信しても no such address となる。

# docker compose 設定

[docker-mailman/docker-compose.yaml](https://github.com/maxking/docker-mailman/blob/main/docker-compose.yaml) を参考に設定。

変更点は以下:
* mailman設定関連をローカルフォルダに置くよう /opt/mailman → . に変更
* mailman-web アクセス用の webサーバとして nginx を設定
* nginx 以外は expose しないよう設定
* MTA用の postfix を設定
* postfix の転送信頼アドレスとして mynetwork に各サービスが属するよう IPアドレス空間(172.18.0.0/24)を設定
* SMTP_HOST に postfix サービスを指定

## docker-compose.yaml
```yaml
services:
  mailman-core:
    image: maxking/mailman-core:0.4 # Use a specific version tag (tag latest is not published)
    container_name: mailman-core
    hostname: mailman-core
    restart: unless-stopped
    volumes:
    - ./core:/opt/mailman/
    # - /opt/mailman/core:/opt/mailman/
    stop_grace_period: 30s
    links:
    - database:database
    depends_on:
      database:
        condition: service_healthy
    environment:
    - DATABASE_URL=${DATABASE_URL:-postgresql://mailman:mailmanpass@database/mailmandb}
    - DATABASE_TYPE=${DATABASE_TYPE:-postgres}
    - DATABASE_CLASS=${DATABASE_CLASS:-mailman.database.postgresql.PostgreSQLDatabase}
    - HYPERKITTY_API_KEY=${HYPERKITTY_API_KEY:-someapikey}
    - MTA=${MTA:-postfix}
    #- MM_HOSTNAME=${MM_HOSTNAME:-}
    - SMTP_HOST=${SMTP_HOST:-postfix}
    - SMTP_PORT=${SMTP_PORT:-25}
    - SMTP_HOST_USER=${SMTP_HOST_USER:-}
    - SMTP_HOST_PASSWORD=${SMTP_HOST_PASSWORD:-}
    - SMTP_SECURE_MODE=${SMTP_SECURE_MODE:-smtp}
    - SMTP_VERIFY_HOSTNAME=${SMTP_VERIFY_HOSTNAME:-true}
    - SMTP_VERIFY_CERT=${SMTP_VERIFY_CERT:-true}
    # ports:
    # - "127.0.0.1:8001:8001" # API
    # - "127.0.0.1:8024:8024" # LMTP - incoming emails
    networks:
     - net_mailman

  mailman-web:
    image: maxking/mailman-web:0.4 # Use a specific version tag (tag latest is not published)
    container_name: mailman-web
    hostname: mailman-web
    restart: unless-stopped
    depends_on:
      database:
        condition: service_healthy
    links:
    - mailman-core:mailman-core
    - database:database
    volumes:
    - ./web:/opt/mailman-web-data
    # - /opt/mailman/web:/opt/mailman-web-data
    environment:
    - DATABASE_TYPE=${DATABASE_TYPE:-postgres}
    - DATABASE_URL=${DATABASE_URL:-postgresql://mailman:mailmanpass@database/mailmandb}
    - HYPERKITTY_API_KEY=${HYPERKITTY_API_KEY:-someapikey}
    - SECRET_KEY=${SECRET_KEY:-}
    - SERVE_FROM_DOMAIN=${SERVE_FROM_DOMAIN:-example.com}
    - MAILMAN_ADMIN_USER=${MAILMAN_ADMIN_USER:-admin}
    - MAILMAN_ADMIN_EMAIL=${MAILMAN_ADMIN_EMAIL:-admin@example.com}
    - SMTP_HOST=${SMTP_HOST:-postfix}
    # ports:
    # - "127.0.0.1:8000:8000" # HTTP
    # - "127.0.0.1:8080:8080" # uwsgi
    networks:
     - net_mailman

  database:
    environment:
    - POSTGRES_DB=mailmandb
    - POSTGRES_USER=mailman
    - POSTGRES_PASSWORD=mailmanpass
    image: postgres:12-alpine
    volumes:
    - ./database:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready --dbname mailmandb --username mailman"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
     - net_mailman

  postfix:
    build: ./postfix
    hostname: postfix
    container_name: postfix
    environment:
    - MAIL_DOMAIN=${MAIL_DOMAIN:-example.com}
    volumes:
    - ./postfix/main.cf:/etc/postfix/main.cf
    - ./core/var/data/:/opt/mailman/core/var/data/
    networks:
     - net_mailman

  nginx:
    image: nginx:latest
    ports:
    - ${SERVICE_PORT:-80}:80
    volumes:
    - ./nginx/conf.d:/etc/nginx/conf.d
    - ./web:/opt/mailman-web-data
    depends_on:
    - mailman-web
    networks:
     - net_mailman

networks:
  net_mailman:
    driver: bridge
    ipam:
      driver: default
      config:
      - subnet: 172.18.0.0/24
```

## .env 設定例
```conf
SECRET_KEY=適当に設定

SERVE_FROM_DOMAIN=適当なドメイン名

## SMTPホスト検証をしたくないとき
SMTP_VERIFY_HOSTNAME=false
SMTP_VERIFY_CERT=false

## 管理者アカウント設定
MAILMAN_ADMIN_USER=管理者アカウント
MAILMAN_ADMIN_EMAIL=管理者メアド
```

# mailman-web設定

## CSRF検証を無効にするとき

管理者メアドの検証の際に django で mailman-web 稼働サーバのCSRF検証を行おうとするため、これを無効化する。
具体的には、 settings_local.py にて CSRF モジュールを外すよう settings.py 設定を上書きする。
デフォルトの MIDDLEWARE 設定は https://github.com/maxking/docker-mailman/blob/main/web/mailman-web/settings.py を見ればわかる。

### ./web/settings_local.py:

```py
MIDDLEWARE = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    # 'django.middleware.csrf.CsrfViewMiddleware',
    'django.middleware.locale.LocaleMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'allauth.account.middleware.AccountMiddleware',
    'django_mailman3.middleware.TimezoneMiddleware',
    'postorius.middleware.PostoriusMiddleware',
)
```

## ALLOWED_HOSTS 設定

[Mailman-webのREADME](https://github.com/maxking/docker-mailman/blob/main/README.md#mailman-web-1) には ALLOWED_HOSTS の設定のためにドメイン名(example.com)を SERVE_FROM_DOMAIN に設定するとあるが、実際はそのまま[環境変数を設定](https://github.com/maxking/docker-mailman/blob/main/web/mailman-web/settings.py)しているため効かない。

正しくは ".ドメイン名" とすることでワイルドカードが効くようになる。
> refs: [DjangoのALLOWED_HOSTSの仕様](https://docs.djangoproject.com/en/3.1/ref/settings/#allowed-hosts)

設定は上記と同じく settings_local.py に追加設定。

### web/settings_local.py:
```py
ALLOWED_HOSTS = [
    '.localhost', '127.0.0.1',
    'mailman-web',
    '.適当なドメイン',
]
```

# MTA(postfix) の設定

## コンテナ作成

作り方は [Dockerコンテナ内で、Postfixを使ってメール送受信を行う](https://qiita.com/hirobex/items/099c87cef29d4a0be445) を参考。

### postfix/Dockerfile:
```Dockerfile
FROM ubuntu:22.04

RUN apt-get update && apt-get install -y \
    postfix \
    # iputils-ping iproute2 net-tools dnsutils \
    && rm -rf /var/lib/apt/lists/*

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

CMD ["/entrypoint.sh"]
```

### postfix/entrypoint.sh:
```sh
#!/bin/sh

## postfix の DNS 参照は resolv.conf でなく postfix 用の resolv.conf を参照するため、サービス実行前にコピー
## refs: https://zenn.dev/flyingbarbarian/articles/5bb1d38b1ada40
cp /etc/resolv.conf /var/spool/postfix/etc/resolv.conf

#postfix start-fg
postfix start

### ログ監視をフォアグラウンド実行してサービス実行を継続
tail -F /var/log/maillog
```

### postfix/main.cf:
postfixコンテナを一旦起動し、デフォルト設定をコピーして編集するとよい。

```sh
$ docker build . -t postfix
$ docker run -d --rm --name postfix postfix
$ docker cp postfix:/etc/postfix/main.cf .
```

デフォルト設定から以下を編集。
docker-compose で指定したネットワークアドレスを mynetworks に指定する。
これを設定しないと mailman-web や mailman-core からのメール転送を受け付けてくれない。

```conf
myhostname = postfix
mydomain = 適当なドメイン
mydestination = $myhostname, localhost.$mydomain, localhost

## メールのリレー先
relayhost = [転送先メールサーバ]

## docker composeサービス内のメール転送を受け付けるよう IP範囲を設定
mynetworks = 127.0.0.0/8 172.18.0.0/24

## ログをファイル出力
maillog_file = /var/log/maillog

## for mailman
# Support the default VERP delimiter.
recipient_delimiter = +
unknown_local_recipient_reject_code = 550
owner_request_special = no

transport_maps =
    regexp:/opt/mailman/core/var/data/postfix_lmtp
local_recipient_maps =
    regexp:/opt/mailman/core/var/data/postfix_lmtp
relay_domains =
    regexp:/opt/mailman/core/var/data/postfix_domains
```

この状態で docker compose サービス内からメール送信できるようになる。

例えば mailman-web サービスから以下のように送信。

```sh
mailman-web# python
>>> from smtplib import SMTP
>>> smtp = SMTP('postfix')
>>> smtp.sendmail('from_addr@mailman-web', '適当なメアド', 'test message')
```

# mailman-core 設定

postfix に設定するファイルを仮出力しておく。

```sh
$ mkdir -p core/var/data
$ touch ./core/var/data/postfix_lmtp
$ touch ./core/var/data/postfix_domains
```

# nginx設定

## nginx/conf.d/default.conf

proxy_pass で設定するか、 uwsgi_pass で設定するかはどちらでもよい。

```conf
server {
    listen  [::]:80;
    server_name  ML管理用webサーバ;

    location /static {
        alias /opt/mailman-web-data/static;
        autoindex off;
    }

    location / {
		  proxy_pass http://mailman-web:8000 ;
          # uwsgi_pass mailman-web:8080;
		  include uwsgi_params;
		  uwsgi_read_timeout 300;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}
```

## mailman-web の転送先アドレス設定

上記設定までで各サービスは動くが、パスワードリセット時のアドレスが
https://mailman-web:8000/accounts/password/reset/key/xxxxxxxxxx/
のようになり、
https://ML管理用サーバ/accounts/password/reset/key/xxxxxxxxxx/
に書き換えないとパスワードリセットができない。

これでは不便のため、nginx設定を以下のように書き換える。

### nginx/conf.d/default.conf
```conf
server {
  location / {
    proxy_set_header Host $server_name:$server_port;
  }
```

# サービス起動

docker compose up するだけ。

```sh
$ docker compose up -d
```

これでサーバアドレスにアクセスして以下のような mailman3 画面が表示されればOK。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/579756/59efc642-b5b7-4a94-bff9-f28770bce098.png)