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 エラーになります。