Ubuntu 22.04 でメールサーバーを作ったのでメモ

令和にもなって自分でメールサーバーを作ってみたのでメモ。

OS は Ubuntu 22.04。

パッケージ更新後に自動的に再起動

メールとは関係ないけど apt で再起動が必要な更新があった場合は自動的に再起動するようにした。

/etc/apt/apt.conf.d/50unattended-upgrades:

Unattended-Upgrade::Automatic-Reboot "true";

Lets Encrypt

TLS 証明書を作るために certbot をインストール。自分はさくらのクラウドのDNSを使ってるのでそれ用のモジュールも追加。

# apt install certbot python3-certbot-dns-sakuracloud

https://certbot-dns-sakuracloud.readthedocs.io/en/stable/ に従って /root/.secrets/certbot/sakuracloud.ini を作っておく:

dns_sakuracloud_api_token = XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
dns_sakuracloud_api_secret = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

証明書作成:

# certbot certonly --dns-sakuracloud --dns-sakuracloud-credentials ~/.secrets/certbot/sakuracloud.ini -m hoge@example.com -d \*.example.com -d \*.example.net

証明書の期限が近づいたら自動的に更新するように cron に設定:

/etc/cron.weekly/letsencrypt:

#!/bin/bash
certbot renew

[追記] ↑ 自動的に /etc/cron.d/certbot が作られるからこれは不要だった。

Postfix

SMTP サーバーとして Postfix をインストール。

# apt install postfix postfix-policyd-spf-python

root 宛のメールは自分に転送するように設定。

/etc/aliases:

root: tommy
# newaliases

master.cf の submission(587ポート) と smtps(465ポート)のコメントを外して有効にする。

/etc/postfix/master.cf:

submission inet n       -       y       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_tls_auth_only=yes
  -o smtpd_reject_unlisted_recipient=no
  -o smtpd_client_restrictions=$mua_client_restrictions
  -o smtpd_helo_restrictions=$mua_helo_restrictions
  -o smtpd_sender_restrictions=$mua_sender_restrictions
  -o smtpd_recipient_restrictions=
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING
smtps     inet  n       -       y       -       -       smtpd
  -o syslog_name=postfix/smtps
  -o smtpd_tls_wrappermode=yes
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_reject_unlisted_recipient=no
  -o smtpd_client_restrictions=$mua_client_restrictions
  -o smtpd_helo_restrictions=$mua_helo_restrictions
  -o smtpd_sender_restrictions=$mua_sender_restrictions
  -o smtpd_recipient_restrictions=
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING

受信メールを SPF チェックする。

/etc/postfix/master.cf:

spfcheck  unix  -       n       n       -       0       spawn
  user=policyd-spf argv=/usr/bin/policyd-spf /etc/postfix-policyd-spf-python/policyd-spf.conf

main.cf はこんな感じで。そんなに特殊なことはしてないはず。

/etc/postfix/main.cf:

compatibility_level = 3.6
alias_maps = hash:/etc/aliases
myhostname = host.example.com
mydestination = $myhostname, $mydomain
home_mailbox = Maildir/
smtpd_relay_restrictions =
 permit_mynetworks
 permit_sasl_authenticated
 reject_unauth_destination
 check_policy_service unix:private/spfcheck
smtpd_helo_required = yes
mailbox_size_limit = 0
message_size_limit = 102400000
recipient_delimiter = +-
strict_rfc821_envelopes = yes
smtpd_use_tls = yes
smtpd_tls_cert_file = /etc/letsencrypt/live/example.com/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/example.com/privkey.pem
smtpd_tls_loglevel = 1
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
mua_client_restrictions =
mua_helo_restrictions =
mua_sender_restrictions =

IMAP は Dovecot を使うので SASL として Dovecot を使用するのと、SPF チェック用の設定 check_policy_service をしているくらい。

mua_*_restrictions は master.cf の submission と smtps に書かれてるので、設定しておかないと warning が出るので空で設定しておく。

Dovecot

IMAP サーバーとして Dovecot をインストール。

# apt install dovecot-imapd

143 ポートは使わないので無効化。

/etc/dovecot/conf.d/10-master.conf:

service imap-login {
  inet_listener imap {
    port = 0
  }
  inet_listener imaps {
    port = 993
    ssl = yes
  }

Postfix 認証用の Socket ファイルを指定。

/etc/dovecot/conf.d/10-master.conf:

service auth {
〜〜
  unix_listener /var/spool/postfix/private/auth {
    mode = 0666
    user = postfix
    group = postfix
  }
〜〜
}

メールボックスは各ユーザーのホーム直下の Maildir

/etc/dovecot/conf.d/10-mail.conf:

mail_location = maildir:~/Maildir

TLS 証明書は Lets Encrypt のやつ。

/etc/dovecot/conf.d/10-ssl.conf:

ssl_cert = </etc/letsencrypt/live/example.com/fullchain.pem
ssl_key = </etc/letsencrypt/live/example.com/privkey.pem

パスワードはデフォルトで OS のユーザーパスワードと同じものが使われるけど、外から叩かれるものなので OS のパスワードとは変えておきたい。

/etc/dovecot/conf.d/10-auth.confauth-system.conf.ext をコメントアウトして1行追加:

#!include auth-system.conf.ext
!include auth-hoge.conf.ext

専用の認証設定ファイルを作成。

/etc/dovecot/conf.d/auth-hoge.conf.ext:

passdb {
  driver = passwd-file
  args = scheme=SHA512-CRYPT username_format=%u /etc/dovecot/users
}

userdb {
  # <doc/wiki/AuthDatabase.Passwd.txt>
  driver = passwd
  # [blocking=no]
  #args = 

  # Override fields from passwd
  #override_fields = home=/home/virtual/%u
}

passdbauth-passwdfile.conf.ext から、userdbauth-system.conf.ext からコピー。

パスワードファイルの /etc/dovecot/users はこんな形式:

ユーザー名:{SHA512-CRYPT}$6$Gm.4X5ktmas.00pC$z9zEBRJvyyAtuczb81eyr26K/sdjkt9uZ.9mgQT1RR6s6JijHxdnyhQtDGnu70DV9v9Ijkn0bvWYFfkOFWZij0

ハッシュ文字列はこんな風にして作成できる:

# doveadm pw -s SHA512-CRYPT -p hogehoge
{SHA512-CRYPT}$6$Gm.4X5ktmas.00pC$z9zEBRJvyyAtuczb81eyr26K/sdjkt9uZ.9mgQT1RR6s6JijHxdnyhQtDGnu70DV9v9Ijkn0bvWYFfkOFWZij0

fail2ban

Postfix や Dovecot の認証が同じIPアドレスから複数回失敗したときにそのIPアドレスからの接続をブロックするために fail2ban をインストール。デフォルトで SSH 認証にも効くようになってる。

# apt install fail2ban

/etc/fail2ban/jail.d/postfix.conf:

[postfix-sasl]
enabled = true

/etc/fail2ban/jail.d/dovecot.conf:

[dovecot]
enabled = true

デフォルトでは10分以内に5回認証に失敗したら10分間ブロックする。

証明書更新時にサービスをリロードする

はてぶで、証明書更新時にサービスをリロードしないといけないのでは…という指摘があったので追記。

たしかにその通りでした 🙏

/etc/letsencrypt/renewal-hooks/post 配下に実行ファイルを置いておくと証明書が更新されたときに実行してくれるので、次のようなファイルを実行権つきで置いておく。

/etc/letsencrypt/renewal-hooks/post/postfix

#!/bin/bash
/usr/sbin/postfix reload

/etc/letsencrypt/renewal-hooks/post/dovecot

#!/bin/bash
/usr/sbin/dovecot reload

Postfix は証明書ファイルは smtpd プロセスが読み込んでて、smtpd プロセスはある程度動作したら自動的に再起動するので、この処理は実はやらなくてもいい。けどちゃんとやっといた方が安心感はある。

Dovecot は imap-login プロセスが TLS の処理をしてて、これは接続毎に起動されるんだけど、証明書ファイルの読み込みは config という別のプロセスがやっててこれは明に指示しないと再読込してくれないのだった。

DNS の設定

ちゃんと書いておいた方がいいかもしれないと思って追記。

上にも書いたけど DNS はさくらのクラウドのサービスを使ってる。1ゾーンあたり44円/月。安い。

メールサービスなので、MX と A と SPF(TXT) と PTR は必須。 こんな感じで設定:

@     MX   10 mx.example.com.
@     TXT  v=spf1 mx -all
mx    A    192.0.2.1
host  A    192.0.2.1

最後の host(host.example.com)は、Postfix の myhostname に書いたホスト名。これを PTR に設定しておく。 MTA によってはクライアントのIPアドレスを逆引きして正引きした結果がそのIPアドレスを含まないと接続を拒否されることがあるので。 PTR はサーバーのIPアドレスを管理してる業者じゃないと設定できない。業者によってやり方は異なる。 Oracle Cloud は無料で使えるんでそこにメールサーバー立てようかと思ってたんだけど、無料だと PTR を設定できないので断念した。

しかし大昔は自力で DNS サーバーも立ててたんだけど面倒くさくなってしまったな…。

Postfixでオレオレ認証を作る

Postfix で SMTP 認証するには、Cyrus SASL と Dovecot SASL のどちらかを使用できる。 Cyrus SASL は、Postfix のビルド時に SASL ライブラリを組み込む必要があるが、Dovecot SASL は特殊なライブラリは必要ない。 Dovecot SASL はネットワーク通信で、通信相手として Dovecot サーバーが必要なのだが、シンプルなテキストベースのプロトコルなので、簡単に独自の認証プログラムを作ることもできる。

Dovecot 認証プロトコルについての詳細は https://doc.dovecot.org/developer_manual/design/auth_protocol/ に説明があるが、Postfix に対応するだけであれば、完全な実装は必要ない。

以下、Postfix がしゃべる Dovecot 認証プロトコルの説明。

プロトコル

接続

以下、クライアント(C)は Postfix(smtpd)、サーバー(S)は認証プログラム。

smtpd 起動時:

C: VERSION<tab>1<tab>0
C: CPID<tab>$pid
  • $pid : Postfix のプロセス番号。

応答

S: VERSION<tab>1<tab>$minor
S: MECH<tab>$name<tab>$params
S: SPID<tab>$pid
S: DONE
  • $minor
    • プロトコルのマイナーバージョン。無視されるので数値であればなんでもいい。なおメジャーバージョンは 1 でないと Postfix がエラーになる。
  • $name
    • SASL mechanism 名。PLAINLOGINCRAM-MD5 等。
  • $params
    • SASL mechanism の属性。有効な値は anonymous, plaintext, dictionary, active, forward-secrecy, mutual-authprivate は無視される。タブ区切りで複数指定可。 これは Postfix の smtpd_sasl_tls_security_options の値と関連する。たとえば、smtpd_sasl_tls_security_options=noplaintext の場合、plaintext 属性の mechanism は Postfix は使用しない(EHLO の応答にもでてこない)。
  • $pid
    • サーバープロセスのプロセス番号。無視される。

MECH は複数行可。MECH で返した名前が EHLO の応答の AUTH に現れる。

SPID は Postfix には無視されるだけなのでなくてもよい。けど、MECH よりも前に返すとエラーになる(これはプロトコルに合ってないような気がする…)。

認証

SMTP の AUTH 命令時:

C: AUTH<tab>$id<tab>$mech<tab>service=$service<tab>$params
  • $id : 接続識別子。AUTH 命令の度にインクリメントされる。応答の $id と一致しなければエラー。
  • $mech : SASL mechanism 名。
  • $service : Postfix の smtpd_sasl_service の値。

Postfix から渡される $params は次の通り:

  • nologin
    • なにこれ?謎…。
  • lip=$localip
    • SMTP 接続のサーバー側IPアドレス。
  • rip=$remoteip
    • SMTP 接続のクライアント側IPアドレス。
  • secured
    • SMTP 接続が TLS の場合に付加。
  • resp=$resp
    • SMTP AUTH 命令に引数があれば付加。たとえば AUTH PLAIN xxxx の場合は xxxx の部分。

応答

認証成功時:

S: OK<tab>$id<tab>user=$userid
  • $userid : ユーザー名

認証失敗時:

S: FAIL<tab>$id<tab>reason=$reason
  • $reason : 認証失敗の理由

認証のために続きのデータが必要な場合:

S: CONT<tab>$id<tab>$data
  • $data SMTP クライアントに 334 で送られるデータ。LOGIN 認証時のプロンプト文字列(Username:)等。

認証の続き

C: CONT<tab>$id<tab>$data
  • $id : AUTH 時の $id と同じもの。
  • $data : 認証に必要なデータ。

切断

特に命令はない。クライアント(Postfix)からネットワークを切断するだけ。

オレオレ認証

Ruby でオレオレ認証を作ってみる。

ネットワーク通信するデーモンプログラムを作って自前で起動する仕組みを作るのは面倒なので、Postfix に管理をまかせる。 spawn を使うと標準入出力を使って簡単に書ける(inetd みたいな感じ)。

etc/postfix/main.cf

smtpd_sasl_path = private/saslauth

/etc/postfix/master.cf

saslauth unix - n n - - spawn user=daemon argv=/usr/local/bin/saslauth.rb

/usr/local/bin/saslauth.rb

#!/usr/bin/ruby

def main
  gets  # VERSION
  gets  # CPID
  puts "VERSION\t1\t0"
  puts "MECH\tOREORE\tplaintext"
  puts "DONE"

  while line = gets
    cmd, id, mech, *args = line.chomp.split(/\t/)  # AUTH
    params = args.map{|arg| (arg.split(/=/, 2)+[nil])[0, 2]}.to_h
    case mech
    when 'OREORE'
      oreore(id, params)
    else
      puts "FAIL\t#{id}\treason=unknown mechanism"
    end
  end
end

# オレオレ認証
# ユーザー名だけで信じちゃう
def oreore(id, params)
  unless params['resp']
    puts "FAIL\t#{id}\treason=parameter required"
    return
  end
  user = params['resp'].unpack1('m')
  puts "OK\t#{id}\tuser=#{user}"
end

$stdout.sync = true
main

テスト

% nc localhost 25
220 servername.localdomain ESMTP Postfix
EHLO client
250-servername.localdomain
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-AUTH OREORE
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250 CHUNKING
AUTH OREORE aG9nZWhvZ2U=       ← "hogehoge" をBase64化したもの
235 2.7.0 Authentication successful
QUIT
221 2.0.0 Bye

うまくいった。

Postfix 3.6

4/29 に Postfix 3.6 がリリースされたので、その変更点などを。

3.5 と 3.6 のパラメータの差分はこちら

デュアルライセンス

今まで IBM Public License 1.0 というライセンスだったんだけど、それに加えて Eclipse Public License 2.0 も追加された。 (実は 3.3 からだった)

whitelist / blacklist が allowlist / denylist に変更

最近流行りの言葉狩り的なアレ。

postscreen のパラメータ名とログメッセージが変更された。

パラメータ名の変更:

  • postscreen_whitelist_interfacespostscreen_allowlist_interfaces
  • postscreen_blacklist_actionpostscreen_denylist_action
  • postscreen_dnsbl_whitelist_thresholdpostscreen_dnsbl_allowlist_threshold

互換のため、新しいパラメータのデフォルト値は古いパラメータの値を見るようになってる。 こんな感じ:

postscreen_allowlist_interfaces=${postscreen_whitelist_interfaces?{$postscreen_whitelist_interfaces}:{static:all}}

ログの方は、respectful_logging=no を設定するとログには今まで通り WHITELIST / BLACKLIST で出力される。

内部プロトコル変更

内部プロトコルが変更になったらしい。なのでバージョンアップ前にちゃんと postfix stop しておく必要があるとのこと。

あ、postfix_daemon が動かなくなるな…。まあいいや、そのうち直そう。

TLS まわり

OpenSSL の最低バージョンが 1.1.1 になった。 OpenSSL 1.1.1 は 2023-09-11 に EOL だけど、Postfix 3.6 は 2025 までサポートする予定なので、必要になったらアップデートするらしい。

lmtp_tls_fingerprint_digest, smtp_tls_fingerprint_digest, smtpd_tls_fingerprint_digest のデフォルト値が md5 から sha256 に変更になった。

tlsproxy_tls_dh512_param_file は無効になった。指定しても無視される。

tlstype.pl スクリプト追加。collate.pl スクリプトの出力を入力することでメッセージごとの TLS 情報を出力してくれるらしい。

compatibility_level が Postfix のバージョンになった

今まで compatibility_level0, 1, 2 だったんだけど、Postfix バージョン(3.6 とか 3.6.0 とか)になった。 と言ってもデフォルト値は 0 のままなので、主に compatibility_level を評価する側の表記の問題。

今までは main.cf 中で ${{$compatibility_level} < {1} ? {yes} : {no}} みたいに書いてたんだけど、<<= は単純な文字列比較らしく 3.9 よりも 3.10 が小さく評価されてしまうとのことで、<level, <=level という記述が導入された。 ${{$compatibility_level} <level {3.6} ? {yes} : {no}} みたいに書く。

known_tcp_ports パラメータ

TCP ポートの指定に数字ではなく smtpsmtps みたいにサービス名で記述すると /etc/services ファイルを見てたんだけど、新設された Postfix パラメータの known_tcp_ports を参照するようになった。参照コストが高かったということなのかな…。 パラメータのデフォルト値は lmtp=24, smtp=25, smtps=submissions=465, submission=587。 このパラメータに無いサービス名は今までどおり /etc/services ファイルを見る。

local_login_sender_maps パラメータ

sendmail コマンドや postdrop コマンドでメールを送る際、今まではどのユーザーでも任意のエンベロープ送信者アドレスを指定できたけんだけど、それを制限できるようになった。

このマップは UNIX ログイン名をキーとして、返される値が許されるパターン。

  • * は何でもOK
  • <>(empty_address_local_login_sender_maps_lookup_key の値) は空アドレス
  • @ドメイン名 はそのドメインであればOK
  • それ以外はメールアドレス

たとえば、

main.cf:
local_login_sender_maps = hash:login_senders
login_senders:
hoge  hoge@example.com @example.net
fuga  *
piyo  <>

みたいにすると、hoge ユーザーは hoge@example.com または examlpe.net ドメインのアドレス、fuga ユーザーは何でもOK、piyo ユーザーは空アドレスのみOK…という感じ。

デフォルトは static:* で、従来どおり誰でも何でも指定可能。

smtpd_relay_restrictionssmtpd_recipient_restrictions の評価順

3.5 までは smtpd_recipient_restrictionssmtpd_relay_restrictions の順に評価されていたのが、3.6 からは smtpd_relay_restrictionssmtpd_recipient_restrictions の順になった。

smtpd_relay_before_recipient_restrictions=no に設定すると 3.5 までと同じ順になる。

バウンスメールを元メールへの返信にできる

enable_threaded_bounces=yes を設定すると、バウンスメールのヘッダに ReferencesIn-Reply-To がついて、元メールの返信になるようにできる。デフォルトは no なのでヘッダはつかない。

smtpd_sasl_mechanism_filter パラメータ

リリースノートには smtpd_sasl_mechanism_list と書かれてるけど間違い。

SASLライブラリが返す mechanism の中で採用するもののフィルタ。マップはキーだけが有効で値は意味がない。

デフォルト値は !external, static:restexternal を除外してそれ以外を使用する。 マップの値は使われないので static:restrest は無意味。

配送エージェントのログ追加

local の mailbox_transport や smtp の best_mx_transport のように他の配送エージェントに転送する場合に次のようなログを吐くようになった。

postfix/smtp[pid]: queueid: passing <recipient> to transport=local

errno が 0 のときのログメッセージ

エラー時に errno が 0 の場合に Unknown error: 0 と出てたのを Application error と出るようになった。

DNS API 関数変更

DNS API として res_XXXX() 関数の代わりに res_nXXXX() 関数を使用するようになった。

以前と同じようにビルドするには CCARGS=-DNO_RES_NCALLS を指定する。

Ubuntu で Postfix の SMTP AUTH を設定して fail2ban で認証に失敗したIPアドレスをブロックする

Ubuntu 18.04 の Postfix で次のようにして SMTP AUTH を有効にしました。

/etc/postfix/master.cf

submission inet n       -       n       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_recipient_restrictions=
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject

/etc/postfix/main.cf

smtpd_sasl_local_domain = $myhostname
broken_sasl_auth_clients = yes
smtpd_sasl_security_options = noanonymous

/etc/postfix/sasl/smtpd.conf

pwcheck_method: saslauthd
mech_list: PLAIN LOGIN

/etc/default/saslauthd (変更部分のみ)

START=yes

設定反映

# systemctl reload postfix

で、 fail2ban というパッケージをインストールすると、SMTP AUTH に何回か失敗するとそのクライアントの IPアドレスがブロックされるようになります。10分経つとブロックが解除されます。

/etc/fail2ban/jail.d/postfix.conf (作成)

[postfix-sasl]
enabled = true

設定反映

# systemctl reload fail2ban

回数やブロック解除までの時間等は /etc/fail2ban/jail.conf で指定できます。

# "bantime" is the number of seconds that a host is banned.
bantime  = 10m

# A host is banned if it has generated "maxretry" during the last "findtime"
# seconds.
findtime  = 10m

# "maxretry" is the number of failures before a host get banned.
maxretry = 5

ログは /var/log/fail2ban.log に出力されます。こんな感じ。

2019-01-14 23:29:00,987 fail2ban.filter         [1566]: INFO    [postfix-sasl] Found x.x.x.x - 2019-01-14 23:29:00
2019-01-14 23:29:14,921 fail2ban.filter         [1566]: INFO    [postfix-sasl] Found x.x.x.x - 2019-01-14 23:29:14
2019-01-14 23:29:29,230 fail2ban.filter         [1566]: INFO    [postfix-sasl] Found x.x.x.x - 2019-01-14 23:29:29
2019-01-14 23:29:39,035 fail2ban.filter         [1566]: INFO    [postfix-sasl] Found x.x.x.x - 2019-01-14 23:29:39
2019-01-14 23:29:39,189 fail2ban.actions        [1566]: NOTICE  [postfix-sasl] Ban x.x.x.x
2019-01-14 23:39:40,070 fail2ban.actions        [1566]: NOTICE  [postfix-sasl] Unban x.x.x.x

なお fail2ban をインストールするだけで sshd についても有効になります。

/etc/fail2ban/jail.d/defaults-debian.conf

[sshd]
enabled = true

これを false にするかコメントアウトすれば sshd について無効にできます。

PostfixDaemon - Ruby で Postfix のデーモンを書くライブラリ

この前、まつもとりーさんに自分でも忘れていたpostfix-mrubyというのを発掘されて、

これによって、前に作ろうと思ってそのまま忘れてたものを思い出したので、作ってみました。

github.com

こんなプログラムを /usr/lib/postfix/sbin に置いておいて、

require 'postfix_daemon'

PostfixDaemon.start do |socket, addr|
  while s = socket.gets
    socket.puts s.update
  end
end

Postfix の master.cf にテキトーに書くと、ポートにアクセスがあると Postfix の master(8) に起動してもらえます。

まあ master を inetd のように使ってる感じです。標準入出力ではないですけど。

シングルスレッドなので一つの接続を処理中はそのプロセスは新しい接続には応えることができませんが、その場合は Postfix の master が新たなプロセスを生成してくれます。これも inetd と同じです。

メールの配送に関係するデーモンを作った時に独自に起動管理する必要がないというメリットもあります。

Postfix の master を inetd のように使うというと、spawn(8) が用意されています。これは標準入出力でクライアントと通信できるので、本当に inetd ぽいです。普通は spawn を使うのが簡単でいいと思います。

ただし spawn は接続があった時にプロセスを生成し、切断された場合にプロセスが終了するので、Ruby のようにプロセス起動が重めなプログラムの場合は、繰り返し使われるとそれなりに負荷が掛かってしまいます。

Postfix のデーモンとすることで、一度起動されたら接続が切れてもしばらくは、死なずに次の接続を待つことが出来ます。アクセス頻度が高い場合でも spawn に比べて負荷があがりにくいんじゃないかと思ってます(測ってない)。

(これ書いてて思ったけど PostfixService という名前の方が良かったかも知れない…)

Postfixパラメータ比較

MySQL Parameters が我ながら便利だったので、Postfix版を作ってみました。

Postfix Parameters

MySQLと違ってソースからパラメータを拾ってるんで、ビルド環境によって異なるパラメータは複数個の値が表示されてます。

UI的にはかなり使いにくいんで、そのうち直すかもしれません。

Vue.js は変数に値を入れるだけでHTML要素が自動で変更されて面白いですね。 もうちょっと勉強しよう。

Postfix 3.1 の新機能 / JSON形式キュー表示と配送流量制御

Postfix 3.1 がリリースされました

個人的に気になった新機能は

  • JSON-format Postfix queue listing.
  • Destination-independent delivery rate delay

の2つです。

JSON形式キュー表示

今までは mailq や postqueue -p コマンドで次のような表示がされていました。

~% postqueue -p
-Queue ID-  --Size-- ----Arrival Time---- -Sender/Recipient-------
8A9AE6EF        275 Sun Mar  6 23:47:06  sender@example.com
                                                          (deferred transport)
                                         rcpt1@example.net
                                         rcpt2@example.net

94AC010A!       298 Sun Mar  6 23:46:33  sender@example.com
                                                          (deferred transport)
                                         rcpt@example.net

-- 0 Kbytes in 2 Requests.

人が読むには良いのですが、プログラムで処理するにはちょっと難しい形式でした。

これが JSON 形式で出力できるようになりました。postqueue -j コマンドを使用します。

~% postqueue -j
{"queue_name": "deferred", "queue_id": "8A9AE6EF", "arrival_time": 1457275626, "message_size": 275, "sender": "sender@example.com", "recipients": [{"address": "rcpt1@example.net", "delay_reason": "deferred transport"}, {"address": "rcpt2@example.net", "delay_reason": "deferred transport"}]}
{"queue_name": "hold", "queue_id": "94AC010A", "arrival_time": 1457275593, "message_size": 298, "sender": "sender@example.com", "recipients": [{"address": "rcpt@example.net", "delay_reason": "deferred transport"}]}

キュー毎に JSON 形式でリストされます。時刻はUNIX時刻(1970-01-01 00:00:00 UTC からの経過秒数)で表され、受信者は配列形式で表されます。プログラムで処理するのも簡単そうです。

配送流量制御

今まで Postfix は配送時の流量を制御することはできませんでした。相手が受け付けるだけメールを送信していました。

3.1 では明にメッセージ配送を遅延させることができるようになりました。

たとえば default_transport_rate_delay = 3s を設定すれば、キューに入ってから配送を開始するまで3秒間待つようになります。

smtp_transport_rate_delay = 3s とすれば smtp 配送だけに効果があります。

transport_maps と master.cf の設定をうまく使えば特定のドメイン宛だけ遅延させることもできると思います。

次は default_transport_rate_delay = 3s とした状態で、メールをほぼ同時に5通送った時の配送ログです。 約3秒間隔で配送されていることがわかります。

Mar  7 00:31:03 x220 postfix/smtp[29564]: 10D0610A: to=<tmtms@example.com>, relay=mx.example.com[49.212.128.207]:25, delay=3.8, delays=0.08/3.4/0.14/0.17, dsn=2.0.0, status=sent (250 2.0.0 Ok: queued as AA13B9FCBF)
Mar  7 00:31:03 x220 postfix/qmgr[29553]: 10D0610A: removed
Mar  7 00:31:07 x220 postfix/smtp[29564]: 6AD316EF: to=<tmtms@example.com>, relay=mx.example.com[49.212.128.207]:25, delay=6.7, delays=0.09/6.3/0.14/0.19, dsn=2.0.0, status=sent (250 2.0.0 Ok: queued as 046759FCBF)
Mar  7 00:31:07 x220 postfix/qmgr[29553]: 6AD316EF: removed
Mar  7 00:31:10 x220 postfix/smtp[29598]: BD2BC1A0E: to=<tmtms@example.com>, relay=mx.example.com[49.212.128.207]:25, delay=9.7, delays=0.12/9.3/0.14/0.17, dsn=2.0.0, status=sent (250 2.0.0 Ok: queued as 522C19FCBF)
Mar  7 00:31:10 x220 postfix/qmgr[29553]: BD2BC1A0E: removed
Mar  7 00:31:13 x220 postfix/smtp[29564]: 164231AA5: to=<tmtms@example.com>, relay=mx.example.com[49.212.128.207]:25, delay=13, delays=0.12/12/0.14/0.17, dsn=2.0.0, status=sent (250 2.0.0 Ok: queued as 9E3459FCBF)
Mar  7 00:31:13 x220 postfix/qmgr[29553]: 164231AA5: removed
Mar  7 00:31:17 x220 postfix/smtp[29598]: 642CC1CE2: to=<tmtms@example.com>, relay=mx.example.com[49.212.128.207]:25, delay=16, delays=0.12/15/0.14/0.18, dsn=2.0.0, status=sent (250 2.0.0 Ok: queued as EA5DD9FCBF)
Mar  7 00:31:17 x220 postfix/qmgr[29553]: 642CC1CE2: removed

Postfix mruby plugin

Postfix の mruby plugin を作ってみました。

誰得かわからないんですけど、Postfix のルックアップテーブルで mruby スクリプトで結果を返すことができます。

たとえば、次のようなスクリプトを作れば、

class Hoge
  def lookup(key)
    key.reverse
  end
end
Hoge.new

与えられたキーを反転して返すことができます。

% postmap -q hoge mruby:/path/to/hoge.rb
egoh

Postfix で独自のルックアップテーブルを作る

Postfix 3.0 から導入された動的データベースプラグイン機構を使用して、独自のルックアップテーブルを作ってみます。

Postfix を次のようにしてインストールしてあります。

重要なのは dynamicmaps=yes なので、これさえあれば他のパラメータ指定はなんでもいいです。

% make makefiles dynamicmaps=yes\
 command_directory=/usr/local/postfix/sbin\
 config_directory=/usr/local/postfix/etc\
 default_database_type=hash\
 daemon_directory=/usr/local/postfix/libexec\
 data_directory=/var/local/postfix/lib\
 html_directory=no\
 mail_spool_directory=/var/mail\
 mailq_path=/usr/local/postfix/bin/mailq\
 manpage_directory=/usr/local/man\
 meta_directory=/usr/local/postfix/etc\
 newaliases_path=/usr/local/postfix/bin/newaliases\
 queue_directory=/var/local/postfix/spool\
 readme_directory=no\
 sendmail_path=/usr/local/postfix/sbin/sendmail\
 shlib_directory=/usr/local/postfix/lib
% make
% sudo make install
% sudo cp -r include /usr/local/postfix/

ルックアップテーブルは与えられた文字列に対して一つの値を返します。

今回は与えられた文字列中の小文字を大文字に変換して返すようなルックアップテーブルのプラグインを作成してみます。

#include "sys_defs.h"
#include "dict.h"

/* name に対応する値を返す */
static const char *dict_upcase_lookup(DICT *dict, const char *name)
{
  static char buf[256];
  int i = 0;

  /* 256バイト以上の文字列に対する値は見つからなかったことにする */
  if (strlen(name) > 255) {
    return NULL;
  }
  while (*name) {
    buf[i++] = toupper(*name++);
  }
  buf[i] = '\0';
  return buf;
}

/* 終了 */
static void dict_upcase_close(DICT *dict)
{
  dict_free(dict);
}

/* 初期化 */
DICT *dict_upcase_open(const char *name, int open_flags, int dict_flags)
{
  DICT *dict;

  dict = dict_alloc("upcase", name, sizeof(DICT));
  dict->lookup = dict_upcase_lookup;
  dict->close = dict_upcase_close;
  dict->flags = dict_flags;
  return dict;
}

作成

% gcc -I /usr/local/postfix/include -fPIC -g -O -DLINUX3 -c dict_upcase.c
% gcc -shared -o postfix-upcase.so dict_upcase.o
# sudo cp postfix-upcase.so /usr/local/postfix/lib

設定 [/usr/local/postfix/etc/dynamicmaps.cf]

upcase  postfix-upcase.so       dict_upcase_open

作成したライブラリのファイル名と初期化関数を dynamicmaps.cf に登録します。 shlib_directory 以外の場所に置くこともできます。その場合はフルパスで記述します。

postmap コマンドで試してみます。

% /usr/local/postfix/sbin/postmap -q hoge upcase:dummy
HOGE

ちゃんと動いてるようです。

ルックアップテーブルは hash:/etc/aliases のように type:name の形式で指定します。 今回は name 部分は特に使用していませんが、初期化関数の第一引数として渡されるので、必要な場合はそれを使えばいいです。

Postfix 3.0 の主な変更

Postfix 3.0.0 がリリースされたのでアナウンス文を勝手に翻訳してみました。

原文: http://www.postfix.org/announcements/postfix-3.0.0.html


Postfix stable release 3.0.0 が利用可能になりました。このリリースにより Postfix 2.8 のサポートは終了します。

主な変更(順不同):

  • RFC 6530 と関連ドキュメントで定義された国際化ドメイン名とアドレスのローカルパートのための SMTPUTF8 サポート。 この実装は Arnt Gulbrandsen が寄稿したコードをベースとし、CNNIC によってスポンサーされました。 SMTPUTF8 サポートはまだ途中です; Postfix 3.1 開発サイクル中で完了する予定です。 要約と制限については SMTPUTF8_README を参照してください。

  • Postfix 動的リンクライブラリとデータベースプラグインのサポート。 この実装は LaMont Jones による Debian Linux のためのコードをベースとしています。 有効なオプションの詳細な説明は INSTALL を参照してください。

  • 新しい Postfix デフォルト設定の選択的な採用のための OPT-IN 安全策。 何もしなければ、基本的には古い Postfix デフォルト設定は残ったままにすべきです (そうでなければ、あなたの下流のメンテナに文句を言ってください)。 Postfix ログファイルメッセージの詳細な説明は COMPATIBILITY_README を参照してください。

  • 複数のルックアップテーブル操作のサポート。 pipemap:{map1,map2...} データベースタイプはルックアップテーブルのパイプラインを実行します。 これは、一つのルックアップテーブルからの結果が次のテーブルのクエリになります; unionmap:{map1,map2,...} データベースタイプは、同じクエリを複数のルックアップテーブルに送り、その結果を結合します。

  • 単純なものを簡単に行う仮想テーブルのサポート。 inline:{key1=value1,key2=value2,...} テーブルは、少ないアイテムのためだけに外部のファイルを生成する必要を回避します; randmap{value1,value2,...} テーブルは、指定された値からのランダムな選択を実行します。

  • DNS ルックアップ結果と、配送エージェントステータスコードとメッセージのテーブル駆動変換。 通常は、問題のある DNS 応答を修正したり、配送エラーのハンドリングを修正するために、PCRE テーブルを使用します。 smtp_dns_reply_filter, smtp_delivery_status_filter と、他の Postfix デーモン用の類似の名前のパラメータを参照してください。

  • 設定ファイルの文法の改善。 ${name?{iftrue}:{iffalse}} のような三項演算子のサポート、${{expr1}==${expr2}?{iftrue}:{iffalse}} のような比較演算子、Milter毎とポリシーサーバー毎のタイムアウトと他の設定、空白を含む master.cf パラメータ、空白を含む import/export_environment 設定、空白を含む "static" テーブルルックアップ結果。 access(5) と header/body_checks(5) テーブルの複数のルックアップ結果のサポートは Postfix 3.1 開発サイクルで完了する予定です。

  • セッション毎のコマンドプロファイル。各インバウンド SMTP セッションの最後に記録されます。 たとえば、パスワード推測ボットは、"disconnect from name[addr] ehlo=1 auth=0/1 commands=1/2" と記録され、 これは、クライアントが、成功したひとつの EHLO コマンドと失敗したひとつの AUTH コマンドを送信し、QUIT コマンドを送信せずにハングアップしたことを意味しています。 この情報は常に記録され、冗長なロギングやネットワークの監視なしにパズルを解くことを助けます。

Postfix のソースコードは http://www.postfix.org/ にリストされたミラーにあります。

Postfix Advent Calendar 2014

これはPostfix Advent Calendar 2014の26日目の記事…じゃなくてまとめです。

11月下旬、今年もどっかのアドベントカレンダーに記事を書こうかとQiita の Advent Calendar 2014を眺めてたんですが、Postfix が無いことに気がつきました。

無いなら自分で作ろうかと思ったんですけど、Postfixで25日分もネタが集まるとは思えなかったし、自分で全部埋められるとも思わなかったので、どうしようかなーと考えてたら、上島竜兵風に「押すなよ!絶対押すなよ!」と言ってる御仁を見つけたので、

押してみました。

はじめはどうなることかと思ってたのですが、終わってみるとちゃんと全枠埋まって良かったです。

まだ全部はちゃんと読めてないのですが、自分の知らなかったことが書かれた記事がいくつもありました。

ご参加頂いた皆様ありがとうございました。

  1. Postfix の DB ファイルを一括生成・更新
  2. Postfix の main.cf のフォーマットについて
  3. Postfix で IPv6 無効化
  4. Postfix の拡張メールアドレス
  5. Postfix 2.10 の smtpd_relay_restrictions 新設の背景
  6. 7bit と 8bit の狭間で
  7. Postfix の詳細ログを採取する
  8. postfix + milter + PostfixAdmin + Roundcube で作るメールサーバ
  9. parent_domain_matches_subdomains
  10. milterプロトコル
  11. Postfixでのサブミッションスパムの簡易対策方法
  12. Postfixの証明書認証アレコレ
  13. sendmailコマンドによるメール発信
  14. Amazon LinuxでPostfix/PostgreSQLを用いたPostfixAdmin構築
  15. Postfix から MySQL を使う
  16. Postfix 2.12 の compatibility_level
  17. 中継メールサーバーの高可用性・負荷分散に対応する
  18. postscreenってどんなもの?
  19. UNIXユーザーネームサービス障害時の宛先存在確認問題と対策
  20. セカンダリメールサーバの設定(認証サーバが落ちても動くようにプレインテキストにユーザ一覧を記入する構成)
  21. 1通のメールに対して procmail と Sieve の処理を両立させる
  22. Postfix aliasesとS3とSWFでメール受信時にイベントドリブン的に堅く処理する
  23. 危ないPostfix+MySQL構成
  24. CentOS7.0/64bitでメールサーバ構築
  25. VERP でメール不達エラーの宛先アドレスを識別する

Postfix から MySQL を使う

これは Postfix Advent Calendar 2014 の15日めの記事です。

ルックアップテーブル

Postfix のルックアップテーブルは Linux だと通常は hash 形式のファイルですが、ファイルの代わりに MySQL, PostgreSQL, LDAP 等を参照することができます。

どの形式が使えるかは postconf -m コマンドで使用できる形式の一覧を見ることができます。Ubuntu だと次のようになってます。

% postconf -m
btree
cidr
environ
fail
hash
internal
memcache
nis
proxy
regexp
sdbm
socketmap
sqlite
static
tcp
texthash
unix

Ubuntu では deb で対応形式を追加できるようになっています。

% sudo apt-get install postfix-cdb postfix-ldap postfix-mysql postfix-pcre postfix-pgsql
...
% postconf -m
btree
cdb
cidr
environ
fail
hash
internal
ldap
memcache
mysql
nis
pcre
pgsql
proxy
regexp
sdbm
socketmap
sqlite
static
tcp
texthash
unix

あとから追加できるのは Ubuntu(Debian?)の独自拡張で、通常はコンパイル時に指定したものしか使えません。 CentOS の場合は mysql はありますが、pgsql(PostgreSQL) はありません。Postfix をコンパイルし直さない限り、あとで追加することもできません。

% postconf -m
btree
cidr
environ
fail
hash
internal
ldap
memcache
mysql
nis
pcre
proxy
regexp
socketmap
static
tcp
texthash
unix

MySQL

ルックアップテーブルは、ある値(キー)を与えるとそれに対応する値を返すテーブルで、*_maps という名前のパラメータや、smtpd_*_restrictions に指定するアクセス制御で使用されます。

たとえば alias_maps パラメータで MySQL を参照したい場合は次のように指定します。

[main.cf]

alias_maps = mysql:/etc/postfix/alias.mysql

/etc/postfix/alias.mysql ファイルには MySQL 接続用の情報とクエリを記述します。

[alias.mysql]

hosts = mysql_server
user = mysql_user
password = user_password
dbname = some_db
query = select result from alias_table where name='%s'

この例では、検索キーが alias_table の name カラムに一致したら result カラムの値を返します。

%s 内の '" はちゃんとエスケープされるので、SQL インジェクションについては気にしなくても大丈夫です。

クエリが複数カラムや複数レコードを返す場合は、, で結合されて返ります。

my.cnf

Postfix 2.11 から my.cnf や他のファイルを読むことができるようになりました。

option_group = group
option_file = /etc/postfix/mysql.cnf

option_file は my.cnf ファイルの代わりに読み込むファイル名を指定します。 option_group は cnf ファイル中のグループを指定します。

option_fileoption_group も指定されない場合は、my.cnf も読み込まれません。

これを使用することで、default-character-setinit-command 等の MySQL の色んなオプションが指定できるようになります。

charset

Postfix から MySQL への接続の charset は Ubuntu ではデフォルトは latin1 になっています。

試してみます。

[/tmp/hoge.mysql]

hosts = localhost
user = test
password = abcdefg
dbname = test
query = show variables like 'character_set_connection'
% postmap -q hoge mysql:/tmp/hoge.mysql
character_set_connection,latin1

上述したように Postfix 2.11 では my.cnf を読ませることができるので、my.cnf で default-character-set を設定すればそれに従います。

[/tmp/hoge.mysql]

hosts = localhost
user = test
password = abcdefg
dbname = test
query = show variables like 'character_set_connection'
option_group = hoge

[/etc/mysql/my.cnf]

[hoge]
default-character-set = utf8mb4
% postmap -q hoge mysql:/tmp/hoge.mysql
character_set_connection,utf8mb4

Postfix 2.11 未満では my.cnf を読むことができないので、charset を指定することができません。

接続用の charset がカラムの charset と異なる場合、クエリがエラーになることがあります。

たとえば、ascii のカラムにメールアドレスが格納されている場合、接続が latin1 だと 8bit 文字をクエリに渡すとエラーになります。

[/tmp/virtual_alias.mysql] *1

hosts = 127.0.0.1
user = test
password = abcdefg
dbname = test
query = select address from alias where alias='%s'

[main.cf]

virtual_alias_maps = mysql:/tmp/virtual_alias.mysql

[SMTP]

MAIL FROM:<hoge>
250 2.1.0 Ok
RCPT TO:<ほげ>
rcpt to:<ほげ@example.com>
451 4.3.0 <      @example.com>: Temporary lookup failure

[mail.log]

Dec 15 02:51:38 x220 postfix/smtpd[9860]: connect from localhost[127.0.0.1]
Dec 15 02:51:48 x220 postfix/smtpd[9860]: warning: mysql query failed: Illegal mix of collations (ascii_general_ci,IMPLICIT) and (latin1_swedish_ci,COERCIBLE) for operation '='
Dec 15 02:51:48 x220 postfix/smtpd[9860]: warning: mysql:/tmp/virtual_alias.mysql lookup error for "??????@example.com"
Dec 15 02:51:48 x220 postfix/smtpd[9860]: NOQUEUE: reject: RCPT from localhost[127.0.0.1]: 451 4.3.0 <      @example.com>: Temporary lookup failure; from=<hoge> to=<??????@example.com> proto=SMTP
Dec 15 02:51:52 x220 postfix/smtpd[9860]: disconnect from localhost[127.0.0.1]

Postfix はメールアドレスのローカルパートに 8bit 文字があってもエラーにはせず、そのまま処理を行おうとします*2。そのため ascii カラムと latin1 リテラルを比較しようとして MySQL エラーになってしまいます。

クエリを次のように変更すると、リテラルを強制的に ascii とみなすようになるため、MySQL エラーにはなりません。

[/tmp/virtual_alias.mysql]

query = select address from alias where alias=_ascii'%s'

[SMTP]

RCPT TO:<ほげ@example.com>
550 5.1.1 <      @example.com>: Recipient address rejected: example.com

[mail.log]

Dec 15 02:53:13 x220 postfix/smtpd[10080]: connect from localhost[127.0.0.1]
Dec 15 02:53:21 x220 postfix/smtpd[10080]: NOQUEUE: reject: RCPT from localhost[127.0.0.1]: 550 5.1.1 <      @example.com>: Recipient address rejected: example.com; from=<hoge> to=<??????@example.com> proto=SMTP
Dec 15 02:53:23 x220 postfix/smtpd[10080]: disconnect from localhost[127.0.0.1]

一旦 Temporary lookup failure のエラーになると、しばらくエラーの状態が記憶されてしまうため、その後のクエリもエラーになってしまいます。

MySQL を使用する場合には注意しましょう。

*1:Ubuntu では Postfix のデーモンは chroot 下で動作するため、MySQL の socket ファイルが見れないので、localhost ではなく 127.0.0.1 を指定して TCP 接続を使用するようにしています。

*2:ドメインパートに 8bit 文字があると SMTP エラーになります。

7bit と 8bit の狭間で

これは Postfix Advent Calendar 2014 の6日目の記事です。

その昔、電子メールは 7bit データでした。

日本語は ASCII の範囲におさまらないのですが、ISO-2022-JP*1にエンコードすることで 7bit になるので、日本語でメールする人たちはそのようにしてました。今でも日本語を扱うメールアプリのデフォルトのエンコーディングは ISO-2022-JP になってることが多いと思います。

ただしヘッダの From や To フィールドには規格上 ISO-2022-JP は書けないので、メールアドレスの表示名には日本語は使えませんでした。

余談ですが、メール本文の冒頭で自分の名前を名乗る日本の風習は From に日本語で名前が書けなかったためじゃないかと、個人的に妄想してます。

バイナリデータは uuencode 等でテキストに変換して、メール本文に貼り付けて送ったものでした。

その後 MIME という規格ができて、メールヘッダにも ASCII 以外の文字列を記述できたり、メールにファイルを添付することができるようになり、便利に使えるようになりました。*2

MIME では本文中の 8bit 文字を ISO-2022-JP のような 7bit に変換しなくてもそのまま記述できます。ただし、ヘッダに Content-Transfer-Encoding: 8bit の記述が必要です。*3

UTF-8 のテキストをそのまま埋め込んだメールデータは次のようになります。あいかわらずヘッダ中では 8bit 文字は書けないので、Subject はエンコーディングしています(この例では「テスト」)。

Subject: =?utf-8?b?44OG44K544OI?=
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit

本日は晴天なり

8bit メールは新しい規格なので(と言っても20年くらい前ですが)、相手がちゃんと処理できるかどうかわかりません。

SMTP の EHLO 命令に対する応答に 8BITMIME が含まれてれば対応しています。Postfix は対応しています。

EHLO hoge
250-x220
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-STARTTLS
250-ENHANCEDSTATUSCODES
250-8BITMIME              ★
250 DSN

Postfix が 8bit メールを配送する時に相手が対応していない場合はどのようになるのか試してみます。

master.cf を次のようにします。

smtp      inet  n       -       -       -       -       smtpd
 -o content_filter=smtp:localhost:10025
10025     inet  n       -       -       -       -       smtpd
 -o smtpd_discard_ehlo_keywords=8BITMIME

25番ポートでうけつけたメールを 10025番ポートに中継しますが、10025番ポートの Postfix は EHLO に 8BITMIME を返しません。

この状態で 25番ポートに先ほどのメールを送信すると、最終的に届いた内容は次のようになりました。

Subject: =?utf-8?b?44OG44K544OI?=
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable                         ★

=E6=9C=AC=E6=97=A5=E3=81=AF=E6=99=B4=E5=A4=A9=E3=81=AA=E3=82=8A     ★

10025番ポートで動いている MTA が 8BITMIME を返さなかったので、25番ポートの Postfix が本文部のエンコーディングを 8bit から quoted-printable に変換して送信しました。

テキストを添付した次のメールを送ると、

Subject: txt attached
Content-Type: multipart/mixed; boundary=123456789

--123456789
Content-Type: text/plain
Content-Transfer-Encoding: 7bit

ASCII BODY
--123456789
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit

本日は晴天なり
--123456789--

該当パートだけ quoted-printable になります。

Subject: txt attached
Content-Type: multipart/mixed; boundary=123456789

--123456789
Content-Type: text/plain
Content-Transfer-Encoding: 7bit

ASCII BODY
--123456789
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable                         ★

=E6=9C=AC=E6=97=A5=E3=81=AF=E6=99=B4=E5=A4=A9=E3=81=AA=E3=82=8A     ★
--123456789--

メールを添付した次のメールを送ると、

Subject: eml attached
Content-Type: multipart/mixed; boundary=123456789

--123456789
Content-Type: text/plain
Content-Transfer-Encoding: 7bit

ASCII BODY
--123456789
Content-Type: message/rfc822
Content-Transfer-Encoding: 8bit

Subject: =?utf-8?b?44OG44K544OI?=
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit

本日は晴天なり
--123456789--

添付パートの Content-Transport-Type と、添付されたメールの本文が変換されます。

Subject: eml attached
Content-Type: multipart/mixed; boundary=123456789

--123456789
Content-Type: text/plain
Content-Transfer-Encoding: 7bit

ASCII BODY
--123456789
Content-Type: message/rfc822
Content-Transfer-Encoding: 7bit                                       ★

Subject: =?utf-8?b?44OG44K544OI?=
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable                           ★

=E6=9C=AC=E6=97=A5=E3=81=AF=E6=99=B4=E5=A4=A9=E3=81=AA=E3=82=8A       ★
--123456789--

添付されたメールを丸ごと quoted-printable にしないのは、Content-Type: message/rfc822 に指定できる Content-Transfer-Encoding は 7bit, 8bit,binary のいずれかだと決められているためです。

このように Postfix は配送先が 8bit メールを受け取れるかどうかによってメールの中身を変換することがありますが、main.cf に disable_mime_output_conversion = yes を設定するとこの 8bit → quoted-printable 変換を行いません。 相手が EHLO に 8BITMIME を返さなくても無理やり 8bit のまま送りつけます。

また、メールを受け取るときに Content-Transfer-Encoding: 8bit がないのに 8bit 文字が入っていた時にエラーにすることもできます。mian.cf に strict_8bitmime_body = yes を設定します。

この場合、DATA 命令の応答として次のようなエラーを返します。

550 5.6.0 improper use of 8-bit data in message body

この時ログには次のように記録されます。

postfix/cleanup[5085]: 7F3813A6: reject: mime-error improper use of 8-bit data in message body: ????????????????????? from localhost[127.0.0.1]; from=<sender@example.com> to=<rcpt@example.com>

*1:昔は「いわゆるJISコード」とか言われてました。

*2:便利になった分複雑になり、SMTP(Simple Mail Transfer Protocol)のどこがシンプルやねん!と言いたくなることもしばしばですが…。

*3:8bit をそのまま書けると言ってもテキストである必要があります。SMTP は行指向なので、行の区切りがないバイナリデータはやはりそのまま書くことはできません。

Postfix の拡張メールアドレス

これは Postfix Advent Calendar 2014 の4日目の記事です。

Postfix では拡張メールアドレスを使うことができます。

tmtms@example.com というメールアドレスがあった場合、tmtms+ext@example.com というメールアドレスも自動的に有効になります。ext 部分はなんでも構いません。

サービス毎に異なるメールアドレスを登録したい場合にいちいち aliases 等でメールアドレスを新たに作成する必要はありません。

Gmail でも使えるようですが自分は Gmail 使ってないので詳しくは知りません。元々の発祥は qmail だと思います。

[追記] どうやら qmail 以前に Sendmail でも使えたようです。

拡張メールアドレスを使うには、recipient_delimiter パラメータに区切り文字を設定します。 上のように tmtms+ext@example.com としたい場合は、recipient_delimiter = + とします。

この前 Postfix の最近のパラメータについて調べたときに気がついたのですが、Postfix 2.11 から recipient_delimiter に複数文字を登録できるようになってました。

recipient_delimiter = +- と設定すると、tmtms+ext@example.comtmtms-ext@example.com の両方が拡張アドレスとして使えるようになります。

ときどきメールアドレスに + 文字が使えないダメなサイトがあるので、その時に代わりに - を使ったりすることができて便利ですね。

RFC5322 的にローカルパートで普通に使える文字は次のとおりです。これらの文字以外は recipient_delimiter には指定しない方がいいと思います。

ASCII英数字と ! # $ % & ' * + - / = ? ^ _ ` { | } ~

特定の拡張メールアドレス宛にきたメールだけ特別な処理をしたい場合は、$HOME/.forward+ext として forward ファイルを書いておくとそれが使用されます。

たとえば、あるサービスに tmtms+hoge@example.com として登録した後、そのメールアドレスに迷惑メールが送られてきてしまうという場合は、$HOME/.forward+hoge ファイルに /dev/null と書いておくとメールを見なくてすみます。

Postfix の main.cf のフォーマットについて

これは Postfix Advent Calendar 2014 の2日目の記事です。

Postfix が登場する以前、MTA と言えば Sendmail でした。Sendmail の設定ファイル sendmail.cf は人間が読むことも書くことも難しくて、設定ファイルを簡単に書くためのツールがいくつかあるくらいでした。それに比べたら Postfix の設定ファイルはかなり簡単です。

Postfix の重要な設定ファイルは主に2つあります。master.cf と main.cf です。

今回は main.cf のフォーマットについて詳しく書いてみます。

基本形式

基本は次の形式です。簡単です。

パラメータ名 = 値

「=」の前後の空白はあってもなくても構いません。また行末の空白文字は無視されます。

コメント

#」で始まる行はコメントです。

# コメント

#」は必ず行頭になければなりません。次のように書いてもパラメータの値の一部として扱われます。

パラメータ名 = 値 # コメント

空白

空白は書いたとおりにパラメータの値となります。ただし、先頭と末尾の空白は除去されます。

値に連続した空白を含む場合、postconf コマンドの出力では一つの空白として見えますが、実際には複数の空白がそのまま維持されています。

# grep smtpd_banner /etc/postfix/main.cf
smtpd_banner = a  b   c                    ← 連続した空白を含む値を指定
# postconf smtpd_banner
smtpd_banner = a b c                       ← postconf の出力では1個になっているが
# telnet localhost 25
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
220 a  b   c                               ← 設定した通りの値が使用される

複数の値を取るパラメータの場合は空白はカンマ(,)と共に区切り記号として扱われます。 値の間の空白はいくつ連続していてもひとつの区切りとして扱われます。

継続行

空白で始まる行は直前の行からの継続行として扱われます。

パラメータ名 =
 値1
  値2
   値3

改行は無視されますが、空白はそのまま残ります。ですので上の例だとパラメータ値は「値1 値2 値3」となります。

また、継続行の途中にコメント行を入れることもできます。

パラメータ名 =
  値1
# 値2
  値3

これは、「値1 値3」として扱われます。

パラメータ値の展開

パラメータの値の中で他のパラメータの値を展開することができます。

param1 = value
param2 = $param1

main.cf 中の記述順には影響しません。次のように書いても同じ結果になります。

param2 = $param1
param1 = value

パラメータ値を展開するための記述方法は「$param」、「${param}」、「$(param)」です。直後に別の文字が続くような場合は括弧つきの表記を使うのがよいでしょう。

$」をそのまま「$」として扱いたい場合は「$$」と記述します。

${param?value}」は param の値が空でない場合に value の値になります。param が空の場合は空のままです。

${param:value}」は param の値が空の場合に value の値になります。param が空でない場合は空になります。

括弧は {} でも () でも構いません。

value 部分でさらに他のパラメータを展開することができます。

param = ${param1?${param2:hoge}}

これは param1 が空でなく param2 が空の場合に param の値が「hoge」になり、それ以外の場合は空の値になります。

param に値がある時と空の時でそれぞれ異なる値を設定したい場合は次のようにするのがよいでしょう。

パラメータ名 = ${param?abc}${param:xyz}

ユーザー定義パラメータ

パラメータは Postfix であらかじめ定義されたもの以外に、自分で定義することもできます。

当然ですが直接 Postfix の動きに作用することはできません。できるのは、他のパラメータの値の中に展開することくらいです。

パラメータ名は ASCII 英数字と「_」です。数字だけのパラメータ名でも特に問題ないようです。

ユーザー定義パラメータを main.cf に記述して、そのパラメータがどこからも参照されない場合は、warning が出力されます。

# postconf -e 'hoge = abc'
# postfix reload
/usr/sbin/postconf: warning: /etc/postfix/main.cf: unused parameter: hoge=abc
postfix/postfix-script: refreshing the Postfix mail system

Postfix の設定ファイルの記述は簡単なのですけど、詳しく見てみたら何か新しい発見があるかと思って調べてみました。自分にとっては連続した空白の扱いが新たな発見でした。みなさんも何か新しい発見があったでしょうか。