Chapter 09無料公開

Exim4の構築と特定ドメイン転送

ArkBig
ArkBig
2022.05.14に更新

社内サービスを作るときメールの誤送信で情報漏洩してしまうのは嫌だなと思い、特定のドメインのみ正規のSMTPへリレーし、通常はMailHogへリレーする機能が欲しくてExim4を使いました。

http://exim.org

設定が多くすべてを把握するのは難しく、今回使用する部分のみの理解です。(これでもSendmailよりシンプルらしい?)

本章はarkbig/devbaseリポジトリのexim4フォルダの説明になります。

https://github.com/arkbig/devbase

本章で説明するファイル
📂 devbase/
├── 📄.env(必要なら作る)
├── 📄compose.override.yaml(必要なら作る)
├── 📄compose.yaml
└── 📂exim4/
    ├── 📄Dockerfile              # exim4を実行するコンテナ作成指示書
    ├── 📄entrypoint.sh           # コンテナ起動時に実行するヘルパースクリプト
    ├── 📄exim4.conf.localmacros  # exim4.conf生成用のdefine定義
    ├── 📄exim4.conf.template     # exim4.conf生成用のテンプレート
    └── 📄update-exim4.conf.conf  # exim4.conf生成用の変数定義

設定ファイルの関係

exim4.confが最終的な設定ファイルですが、通常はupdate-exim4.confコマンドで自動生成するようです。
その際exim4.conf.templateが元になり、exim4.conf.localmacrosの定義で挙動を変えられます。
またupdate-exim4.conf.confでコマンドに変数を渡すことができるみたいです。
きっとね。

exim4.conf.localmacros

exim4.conf.templateの展開時に使用されるdefineを定義しています。

exim4/exim4.conf.localmacros
IGNORE_SMTP_LINE_LENGTH_LIMIT = 1

なんとなく1行の長さ制限を解除しています。

exim4.conf.template

exim4.confを作るひな型です。長くなるので、デフォルトとの差分だけ示します。

exim4/exim4.conf.template
  @@ -322,6 +325,8 @@
  smtputf8_advertise_hosts = MAIN_SMTPUTF8_ADVERTISE_HOSTS
  .endif

+ ## NOTE: Customized by arkbig
+ # for performance
+ split_spool_directory = true
+ # no limit
+ smtp_accept_queue_per_connection = 0
+ # Listen
+ daemon_smtp_ports = 587
+
  #####################################################
  ### end main/02_exim4-config_options
  #####################################################

  @@ -1000,10 +1007,10 @@
  ######################################################################
  #     THE ORDER IN WHICH THE ROUTERS ARE DEFINED IS IMPORTANT!       #
  # An address is passed to each router in turn until it is accepted.  #
  ######################################################################

  begin routers

+ ## NOTE: Customized by arkbig
+ relay_routes:
+   debug_print = "R: relay_routes for $local_part@$domain"
+   driver = manualroute
+   domains = ! +local_domains
+   transport = remote_smtp
+   host_find_failed = ignore
+   same_domain_copy_routing = yes
+   route_data = ${lookup{$domain}partial-lsearch{/etc/exim4/relay_routes}}
+
  #####################################################
  ### end router/00_exim4-config_header
  #####################################################

1つ目の追加箇所で変数を設定しています。(もしかしたら、update-exim4.conf.confに書くべきかも?)
同時に何通も送ったら問題が起きたのでその対策です。

2つ目の追加箇所で特定ドメインなら正規のSMTPサーバーへリレーする設定を書いています。
/etc/exim4/relay_routesファイルからドメインと宛先を読み込みます。

update-exim4.conf.conf

update-exim4.confコマンドで使う変数を定義しています。

exim4/update-exim4.conf.conf
dc_eximconfig_configtype='satellite'
dc_other_hostnames=''
dc_local_interfaces='0.0.0.0 ; ::0'
dc_readhost=''
dc_relay_domains=''
dc_minimaldns='false'
dc_relay_nets='0.0.0.0/0'
dc_smarthost='' #これは $EXIM4_SMARTHOST で置き換えられる
CFILEMODE='644'
dc_use_split_config='false'
dc_hide_mailname=''
dc_mailname_in_oh='true'
dc_localdelivery='mail_spool'

dc_eximconfig_configtype='satellite'としてdc_smarthostへ転送することを表しています。
dc_smarthostはentrypoint.shで動的に設定します。

Dockerfile

次はコンテナのためのイメージを作るDockerfileです。

exim4/Dockerfile ※Shift+マウスホイールで横スクロール
FROM ubuntu

#---------------(1)--------------#
RUN set -eux \
 && apt-get update -y \
 && apt-get install -y --no-install-recommends \
        exim4 \
 && apt-get -y clean \
 && rm -rf /var/lib/apt/lists/*

#---------------(2)--------------#
# Debian-eximユーザー&グループが作られて、デーモン設定で起動するとそれが使われる
# ENV CONTAINER_UID=${CONTAINER_UID:-1000}
# ENV CONTAINER_GID=${CONTAINER_GID:-1000}
# RUN groupadd -g ${CONTAINER_GID} -o smtpstaff \
#  && useradd -g smtpstaff -m -o -u ${CONTAINER_UID} smtpstaff
# WORKDIR /home/smtpstaff/

#---------------(3)--------------#
# exim4.conf.templateは初期設定をコピーしてきて一部追記したものです。
# 追記箇所には"## NOTE: Custimezed by arkbig"の目印を書いてます。
COPY exim4.conf.localmacros exim4.conf.template update-exim4.conf.conf /etc/exim4/
COPY entrypoint.sh ./
RUN chmod +x ./entrypoint.sh

# 通常のメール転送先
ENV EXIM4_SMARTHOST=mailhog::1025
# 送信先が$EXIM4_RELAY_DOMAINなら$EXIM4_RELAY_ADDRへ転送する
# EXIM4_RELAY_{DOMAIN,ADDR}_1〜EXIM4_RELAY_{DOMAIN,ADDR}_Nまで連番で対応(抜け番があれば停止だが、"-"はスキップ)
ENV EXIM4_RELAY_DOMAIN=''
ENV EXIM4_RELAY_ADDR=''

EXPOSE 587

ENTRYPOINT ["./entrypoint.sh"]
# -bd   This option runs Exim as a daemon, awaiting incoming SMTP connections.
# -q<qflags><time>   When a time value is present, the -q option causes Exim to run as a daemon,
#                    starting a queue runner process at intervals specified by the given time value.
# -v   This option causes Exim to write information to the standard error stream, describing what it is doing.
CMD ["exim", "-bd", "-q10m", "-v"]

大きく3つのパートに分かれています。

  1. OSのパッケージマネージャーを使って必要なソフトexim4をインストール
  2. コマンド実行用のアカウント作成(コメントアウト)
  3. 実行するコマンドをコンテナへコピー&設定

2番目のアカウント作成はExim4ではコメントアウトしています。aptでインストールするとDebian-eximユーザー&グループが作られて、デーモン設定で起動するとそちらが使われます。

3番目のコピー&設定でイメージ作成時に設定ファイルをコピーしています。一応compose.yamlでvolumesに指定することで上書き可能です。ただし最終的な設定ファイルexim4.confはentrypoint.shでupdate-exim4.confコマンドを走らせることで生成されます。

entrypoint.sh

次にコンテナ実行のヘルパースクリプト(ENTRYPOINT)です。

exim4/entrypoint.sh ※Shift+マウスホイールで横スクロール
#!/usr/bin/env sh
#====================================================================
# begin of 定型文
# このスクリプトを厳格に実行
set -eu
# set -eux
# 環境に影響を受けないようにしておく
umask 0022
# PATH='/usr/bin:/bin'
IFS=$(printf ' \t\n_')
IFS=${IFS%_}
export IFS LC_ALL=C LANG=C PATH
# end of 定型文
#--------------------------------------------------------------------

#---------------(1)--------------#
# 専用のDebian-eximが使われる
# UID,GIDを合わせる
# uid=$(stat -c "%u" .)
# gid=$(stat -c "%g" .)
# ug_name=smtpstaff
# if [ "${CONTAINER_GID}" != "${gid}" ]; then
#     groupmod -g "${CONTAINER_GID}" -o "${ug_name}"
#     chgrp -R "${CONTAINER_GID}" .
# fi
# if [ "${CONTAINER_UID}" != "${uid}" ]; then
#     usermod -g "${ug_name}" -o -u "${CONTAINER_UID}" "${ug_name}"
# fi

#---------------(2)--------------#
# eximコマンドなら設定ファイルを編集する
if [ "${1}" = "exim" ]; then
    command_is_exim=true
elif [ "${1}" = "${1#-}" ]; then
    command_is_exim=false
else
    set -- exim "$@"
    command_is_exim=true
fi
if ${command_is_exim}; then
    # リレールートファイル作成
    echo '' >/etc/exim4/relay_routes
    skip_relay=false
    if [ -z "${EXIM4_RELAY_DOMAIN}" ]; then
        echo "EXIM4_RELAY_DOMAIN environment variable is empty so relay not added."
        skip_relay=true
    fi
    if [ -z "${EXIM4_RELAY_ADDR}" ]; then
        echo "EXIM4_RELAY_ADDR environment variable is empty so relay not added."
        skip_relay=true
    fi
    if ! ${skip_relay}; then
        echo "${EXIM4_RELAY_DOMAIN}: ${EXIM4_RELAY_ADDR}" >/etc/exim4/relay_routes
        sequence_number=1
        while true; do
            sequence_domain=$(eval "printf $(printf "%s" "\${EXIM4_RELAY_DOMAIN_${sequence_number}-\"\"}")")
            sequence_addr=$(eval "printf $(printf "%s" "\${EXIM4_RELAY_ADDR_${sequence_number}-\"\"}")")
            if [ -z "${sequence_domain}" ] || [ -z "${sequence_addr}" ]; then
                # 未定義なので終了
                break
            fi
            if [ "${sequence_domain}" = "-" ] || [ "${sequence_addr}" = "-" ]; then
                echo "skip sequence number ${sequence_number} because EXIM4_RELAY_DOMAIN_${sequence_number} or EXIM4_RELAY_ADDR_${sequence_number} is '-'."
            else
                echo "${sequence_domain}: ${sequence_addr}" >>/etc/exim4/relay_routes
            fi
            sequence_number=$((sequence_number + 1))
        done
    fi
    echo /etc/exim4/relay_routes
    cat /etc/exim4/relay_routes

    # 設定ファイル更新
    sed -i s/dc_smarthost=.*/dc_smarthost="${EXIM4_SMARTHOST}"/ /etc/exim4/update-exim4.conf.conf
    update-exim4.conf -v
fi

#---------------(3)--------------#
# 専用のDebian-eximが使われる
# if [ "$(id -u)" = "${CONTAINER_UID}" ]; then
#     exec "$@"
# else
#     # ユーザー変更してコマンド実行
#     exec /usr/sbin/gosu "${ug_name}" "$@"
# fi
exec "$@"

大きく3つのパートに分かれています。

  1. UID,GIDをホストに合わせる(コメントアウト)
  2. exim4コマンドの設定ファイル更新
  3. コマンド実行

1番目のUID,GID設定ですが、不要なのでコメントアウトしています。

2番目のexim4コマンドの引数設定がここでのメイン処理です。
実行するコマンドがeximの場合、正規SMTPサーバーへリレーするドメイン設定ファイルを動的に生成しています。
EXIM4_RELAY_{DOMAIN,ADDR}_1_2と連番で複数指定できるようにしています。
この複数指定がなければシンプルにecho "${EXIM4_RELAY_DOMAIN}: ${EXIM4_RELAY_ADDR}" > /etc/exim4/relay_routesだけです。

またEXIM4_SMARTHOST環境変数で/etc/exim4/update-exim4.conf.confの値を書き換えています。

最後にupdate-exim4.confコマンドを実行して/etc/exim4/exim4.confファイルを更新しています。

3番目のコマンド実行ですが、exim4側の機能に任せてrootのまま実行しています。

compose.yaml(抜粋)

そして、dockerで使用するcomposeファイルです。

compose.yaml ※Shift+マウスホイールで横スクロール
services:
  exim4:
    image: arkbig/exim4
    build:
      context: ./exim4
      args:
        no_proxy: ${no_proxy-}
        http_proxy: ${http_proxy-}
        https_proxy: ${https_proxy-}
    init: true
    restart: unless-stopped
    ports:
      - 127.0.0.1::587
    labels:
      - traefik.enable=true
      # Traefikにホスト名みてもらうためにSMTP over TLSにしたかったけど、exim4の設定がわからなかった
      # 複数SMTPサーバーが欲しくなったらstunnelというソフトを試すかな
      - traefik.tcp.routers.exim4-${COMPOSE_PROJECT_NAME:-devbase}.entrypoints=smtps
      - traefik.tcp.routers.exim4-${COMPOSE_PROJECT_NAME:-devbase}.rule=HostSNI(`*`)
      # - traefik.tcp.routers.exim4-${COMPOSE_PROJECT_NAME:-devbase}.rule=HostSNI(`exim4-${COMPOSE_PROJECT_NAME:-devbase}${DOMAIN:-.dev.test}`)
      # - traefik.tcp.routers.exim4-${COMPOSE_PROJECT_NAME:-devbase}.tls.passthrough=true

init: trueを指定しコンテナ内でinitプロセス(PID 1)を実行しています。これによりサービス起動しているexim4がシグナルを正しく受け取れるようになりコンテナの停止が正しくできます。
もし指定しなかった場合、compose downで終了するとき10秒のタイムアウト待ち後に強制終了されます。

ports:はexim4/exim4.conf.templateで設定したポート番号になります。

labels:でTraefikの設定をしています。ホスト名でサービスを切り替えたかったのですが、exim4の設定が分かりませんでした。
最近はStartTLSという方式で、途中からTLSに切り替えるのが流行っている?ようです。ただTraefik目的だと最初からTLSになっている必要があります。

Exim4は両方対応しているようですが、よく分かりませんでした。
MetabaseからTraefik経由でExim4へアクセスする環境でテストしましたが、自前サービスで直接SMTP通信を書いたときにまた調べてみたいところです。

https://www.exim.org/exim-html-current/doc/html/spec_html/ch-encrypted_smtp_connections_using_tlsssl.html

また、必要に応じてenvironment:で動作を変更できます。個人環境の設定ですので、Git管理外のcompose.override.yamlで指定します。
下記がデフォルト値での設定例です。

compose.override.yaml
services:
  exim4:
    environment:
      # 通常のメール転送先(ポート番号指定する場合"::"コロンが2つなので注意)
      EXIM4_SMARTHOST: mailhog::1025
      # 宛先ドメインが指定したものだったら、専用の転送先に送る
      EXIM4_RELAY_DOMAIN:
      # 専用の転送先(これもポート番号指定する場合"::"コロンが2つなので注意)
      EXIM4_RELAY_ADDR:
      # 追加の変更ドメイン
      EXIM4_RELAY_DOMAIN_1:
      # 追加の変更IPアドレス
      EXIM4_RELAY_ADDR_1:
      # 以降も連番で指定可能
      # 空文字れつもしくは未定義に遭遇するとそこで終了
      # "-"ハイフンだけならその番号はスキップして、次の番号を処理

Exim4を起動する

これまで説明したファイルたちを利用してExim4を起動するには次のコマンドを実行します。

docker compose up -d exim4

TraefikとDnsmasqが起動していれば、「exim4-devbase.dev.test:587」を指定すればExim4にメールできます。(実際は*としているので、ホスト名は届けばなんでもいい)

docker compose logs exim4でログを確認できます。
メールを受け取ればその内容も表示されます。メール内容は秘密にしたい場合はcompose.override.yamlでcommand: -bd -q10mなどで-vオプションがない引数で実行します。

✔️本章の作業チェックリスト