社内サービスを作るときメールの誤送信で情報漏洩してしまうのは嫌だなと思い、特定のドメインのみ正規のSMTPへリレーし、通常はMailHogへリレーする機能が欲しくてExim4を使いました。
設定が多くすべてを把握するのは難しく、今回使用する部分のみの理解です。(これでもSendmailよりシンプルらしい?)
本章はarkbig/devbaseリポジトリのexim4フォルダの説明になります。
📂 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を定義しています。
IGNORE_SMTP_LINE_LENGTH_LIMIT = 1
なんとなく1行の長さ制限を解除しています。
exim4.conf.template
exim4.confを作るひな型です。長くなるので、デフォルトとの差分だけ示します。
@@ -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コマンドで使う変数を定義しています。
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です。
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つのパートに分かれています。
- OSのパッケージマネージャーを使って必要なソフト
exim4
をインストール - コマンド実行用のアカウント作成(コメントアウト)
- 実行するコマンドをコンテナへコピー&設定
2番目のアカウント作成はExim4ではコメントアウトしています。apt
でインストールするとDebian-eximユーザー&グループが作られて、デーモン設定で起動するとそちらが使われます。
3番目のコピー&設定でイメージ作成時に設定ファイルをコピーしています。一応compose.yamlでvolumesに指定することで上書き可能です。ただし最終的な設定ファイルexim4.confはentrypoint.shでupdate-exim4.conf
コマンドを走らせることで生成されます。
entrypoint.sh
次にコンテナ実行のヘルパースクリプト(ENTRYPOINT)です。
#!/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つのパートに分かれています。
- UID,GIDをホストに合わせる(コメントアウト)
- exim4コマンドの設定ファイル更新
- コマンド実行
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ファイルです。
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通信を書いたときにまた調べてみたいところです。
また、必要に応じてenvironment:
で動作を変更できます。個人環境の設定ですので、Git管理外の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
オプションがない引数で実行します。