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/ にリストされたミラーにあります。

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