Ruby, MySQL のうるう秒の扱い

2015/7/1 にうるう秒が挿入されるということで、うるう秒の話題が盛り上がってるようなので自分も書いてみます。

Linux 上のプログラムが時刻で60秒を刻むには、うるう秒対応のタイムゾーンを使う必要があります。

通常はうるう秒を考慮していないタイムゾーンが使用されているので、60秒を含む時刻になることはありません。 60秒を含む時刻を扱うには、right/Japan のように right/ を前につけたタイムゾーンを指定します。

前回のうるう秒は 2012/7/1 08:59:60 (JST) だったので、これで試してみます。

% TZ=Japan date --date='2012-07-01 08:59:60'
date: `2012-07-01 08:59:60' は無効な日付です
% TZ=right/Japan date --date='2012-07-01 08:59:60'
2012年  7月  1日 日曜日 08:59:60 JST

Ruby

うるう秒なしのタイムゾーン:

% TZ=Japan ruby -rtime -e 'p Time.at(p Time.parse("2012-07-01 08:59:59").to_i)'
1341100799
2012-07-01 08:59:59 +0900
% TZ=Japan ruby -rtime -e 'p Time.at(p Time.parse("2012-07-01 08:59:60").to_i)'
1341100800
2012-07-01 09:00:00 +0900
% TZ=Japan ruby -rtime -e 'p Time.at(p Time.parse("2012-07-01 09:00:00").to_i)'
1341100800
2012-07-01 09:00:00 +0900

08:59:60 を指定しても 09:00:00 として扱われています。

うるう秒ありのタイムゾーン:

% TZ=right/Japan ruby -rtime -e 'p Time.at(p Time.parse("2012-07-01 08:59:59").to_i)'
1341100823
2012-07-01 08:59:59 +0900
% TZ=right/Japan ruby -rtime -e 'p Time.at(p Time.parse("2012-07-01 08:59:60").to_i)'
1341100824
2012-07-01 08:59:60 +0900
% TZ=right/Japan ruby -rtime -e 'p Time.at(p Time.parse("2012-07-01 09:00:00").to_i)'
1341100825
2012-07-01 09:00:00 +0900

ちゃんと 08:59:60 を扱うことができています。

うるう秒を扱えるかどうかで時刻の内部表現(1970-01-01 00:00:00 UTC からの経過秒数)が25秒ずれていますが、これは今までに25回うるう秒が挿入されたためです。

MySQL

うるう秒なしのタイムゾーン(TZ=Japan で mysqld を起動):

mysql> select from_unixtime(1341100799), unix_timestamp('2012-07-01 08:59:59');
+---------------------------+---------------------------------------+
| from_unixtime(1341100799) | unix_timestamp('2012-07-01 08:59:59') |
+---------------------------+---------------------------------------+
| 2012-07-01 08:59:59       |                            1341100799 |
+---------------------------+---------------------------------------+
mysql> select from_unixtime(1341100800), unix_timestamp('2012-07-01 08:59:60');
+---------------------------+---------------------------------------+
| from_unixtime(1341100800) | unix_timestamp('2012-07-01 08:59:60') |
+---------------------------+---------------------------------------+
| 2012-07-01 09:00:00       |                                     0 |
+---------------------------+---------------------------------------+
mysql> select from_unixtime(1341100800), unix_timestamp('2012-07-01 09:00:00');
+---------------------------+---------------------------------------+
| from_unixtime(1341100800) | unix_timestamp('2012-07-01 09:00:00') |
+---------------------------+---------------------------------------+
| 2012-07-01 09:00:00       |                            1341100800 |
+---------------------------+---------------------------------------+
1 row in set (0.00 sec)

08:59:60 はパースできずに 0 を返しています。

うるう秒ありのタイムゾーン(TZ=right/Japan で mysqld を起動):

mysql> select from_unixtime(1341100823), unix_timestamp('2012-07-01 08:59:59');
+---------------------------+---------------------------------------+
| from_unixtime(1341100823) | unix_timestamp('2012-07-01 08:59:59') |
+---------------------------+---------------------------------------+
| 2012-07-01 08:59:59       |                            1341100823 |
+---------------------------+---------------------------------------+
mysql> select from_unixtime(1341100824), unix_timestamp('2012-07-01 08:59:60');
+---------------------------+---------------------------------------+
| from_unixtime(1341100824) | unix_timestamp('2012-07-01 08:59:60') |
+---------------------------+---------------------------------------+
| 2012-07-01 08:59:59       |                                     0 |
+---------------------------+---------------------------------------+
mysql> select from_unixtime(1341100825), unix_timestamp('2012-07-01 09:00:00');
+---------------------------+---------------------------------------+
| from_unixtime(1341100825) | unix_timestamp('2012-07-01 09:00:00') |
+---------------------------+---------------------------------------+
| 2012-07-01 09:00:00       |                            1341100825 |
+---------------------------+---------------------------------------+

08:59:60 は 08:59:59 として扱われます。やはりパースはできません。

まとめ

  • Linux の場合は特別な設定をしない限り、通常は 60秒を含む時刻を返すことはありません。
  • 60秒を含む時刻をどのように扱うかはプログラム次第です。
  • Ruby は 60秒を含む時刻文字列をパースできますが、MySQL はできません。
  • うるう秒を扱えるタイムゾーンの場合、Ruby は 60秒を含む時刻を返すことがありますが、MySQL は 60秒になることはありません。

2014年の振り返り

あけましておめでとうございます。

2014年も無事終了したので一年を振り返ってみます。

ブログ

1年間で27件の記事を書きました。2013年が24件だったので少し増えました。

はてなブックマークで2桁以上ブクマされたものを並べてるとこんな感じです。

Twitter

1年間で4474件ツイートしました(公式RT含む)。平均すると一日 12.3件です。

月ごとの件数はこんな感じです。

329 2014-01
366 2014-02
354 2014-03
313 2014-04
391 2014-05
266 2014-06
346 2014-07
502 2014-08
502 2014-09
302 2014-10
340 2014-11
463 2014-12

リツイートが多かったツイート上位10件:

イベント

NSEG

毎月長野でやってる NSEG 勉強会にできるだけ参加してます。2014年は、第47回, 第49回, 第50回, 第51回, 第52回, 第53回, 第54回, 第55回 の8回参加しました。出席率は 8/12 = 66.7% でした。

自分が発表したのは、

第49回 MySQLの始め方

第50回 Dockerさわってみた

第54回 Dockerイメージを作る

でした。

その他

2015年は

ブログ

  • もうちょっと頻繁にブログを書こうと思ってます(去年も同じこと言ってた)。

技術文書

  • MySQLプロトコルについてまとめたものを電子書籍にできたらいいなぁ…とか。
  • 「Postfix辞典」を Postfix の最新版に対応したものとかも…。
  • (去年も同じこと言ってた)

イベント

  • NSEG にはできるだけ参加したいです。
  • 今年も RubyWorld Conference には行きたいです。
  • しばらく行ってなかった RubyKaigi にまた行ってみようかと思ってます。
  • YAPC::Asia Tokyo は行ったことなかったんですけど、楽しそうなので行ってみたいです。
  • 予定が合えば OSC にも行きたいです。

ということで 2015年もよろしくおねがいします。

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