上記のエントリで言及していたメールの受信サーバdovecotをmrubyで制御するpluginが概ね完成しましたので紹介します。というのも、一月前ぐらいにはできていたのですがバタバタしておりブログにできていませんでした。
dovecot-mruby-pluginはメール受信のIMAPサーバとして動くdovecotをmrubyで色々制御することができます。今日はその制御の例を幾つか紹介します。
mrubyでIMAPコマンドを作る
dovecot-mruby-pluginでは、dovecotのIMAPプロトコルで使うコマンドをmrubyで作ることができます。例えば以下のようにRubyのコードを書き、dovecotに読み込ませます。
# Register new commands %w( matsumotory test ).each do |cmd| Dovecot::IMAP.command_register(cmd) do |args| Dovecot::IMAP.send_line "Hi, #{Dovecot::IMAP.username}" if cmd == Dovecot::IMAP.username Dovecot::IMAP.send_line "You are me." else Dovecot::IMAP.send_line "I am #{cmd}. Not you with #{args}" end end end
その上で、以下のようにdovecot.confにdovecot上で有効な環境変数を設定します。
import_environment = DOVECOT_MRUBY_INIT_PATH
そして、dovecotに上記のRubyコードを環境変数経由で渡して起動させます。
DOVECOT_MRUBY_INIT_PATH=/path/to/command_register.rb ./dovecot/target/sbin/dovecot \ -c ./dovecot/configuration/dovecot.conf
そして実際にtelnetでIMAPサーバにアクセスしてみましょう。
$ telnet 127.0.0.1 6070 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. * OK [CAPABILITY ...] Dovecot ready. 1 login test testPassword 1 OK [CAPABILITY ...] Logged in 1 matsumotory hoge 1 * matsumotory Hi, test * matsumotory I am matsumotory. Not you with ["hoge", "1"] 1 OK matsumotory completed (0.001 + 0.000 secs). 1 test * test Hi, test * test You are me. 1 OK test completed (0.001 + 0.000 secs). 1 matsumotory * matsumotory Hi, test * matsumotory I am matsumotory. Not you with [] 1 OK matsumotory completed (0.001 + 0.000 secs).
なんということでしょう。testコマンドやmatsumotoryコマンドが有効となり、その引数をRubyコード上で受けて何かしら処理するコマンドが動いていることがわかります。これでオレオレコマンドやコマンドの拡張なども、dovecot側のコードを弄らずとも簡単に実装できるようになりますね。なんということでしょう!
既存のIMAPコマンドのaliasコマンドを作る
dovecot-mruby-pluginは既存のIMAPコマンドのaliasを作ることもできます。例えば以下のようにコードを書きます。
# Register CAP command which run CAPABILITY command Dovecot::IMAP.alias_command_register("CAP") do Dovecot::IMAP.send_line "execute CAPABILITY commands" Dovecot::IMAP.capability end Dovecot::IMAP.alias_command_register("LIST_ALIAS") do Dovecot::IMAP.send_line "alias LIST commands" Dovecot::IMAP.list end
そして、上述した読み込み方でdovecotにRubyコードを読み込ませます。その後、telnetでアクセスしてみましょう。
1 cap * cap execute CAPABILITY commands * CAPABILITY IMAP4rev1 LITERAL+ SASL-IR ... 1 OK Capability completed (0.001 + 0.000 secs). 1 list "" * * LIST (\HasNoChildren) "/" INBOX 1 OK List completed (0.001 + 0.000 secs). 1 list_alias "" * * list_alias alias LIST commands * LIST (\HasNoChildren) "/" INBOX 1 OK List completed (0.001 + 0.000 secs).
おお、なんということでしょう!capability
コマンドのaliasであるcap
コマンドがちゃんと動いており、list
コマンドであるaliasのlist_alias
が有効になっているではありませんか。なんということでしょう。
IMAPコマンドの前後でRubyコードをフックする
さらに、各IMAPコマンドの前後にRubyコードをフックすることもできます。
例えば、list
コマンドが非常にCPU負荷を書けてしまう場合に、その処理を拒否したり中断したりするのではなく、CPU使用率の割り当てを30%に抑えたい、ということを簡単に実現することができます。
以下のように、コマンドの前に処理をするpre_list.rb
と、コマンド実行後に処理をするpost_list.rb
を用意します。
pre_list.rb
rate = Cgroup::CPU.new "test" # limit cpu 30% usage rate.cfs_quota_us = 30000 rate.create if Dovecot::IMAP.username == "test" && Dovecot::IMAP.command_name == "LIST" rate.attach end
post_list.rb
rate = Cgroup::CPU.new "test" if Dovecot::IMAP.username == "test" && Dovecot::IMAP.command_name == "LIST" rate.detach end
そして、仮想的にlist
コマンドがCPUを使い切る状況を再現するために、上記で解説したように、新たにRubyでlist
コマンドを再定義し、ループをする処理にlist
コマンドを上書きします。
Dovecot::IMAP.command_register("LIST") do |args| 100000000.times {} Dovecot::IMAP.send_line "done!" end
そして、以下のようにdovecot.confにpre_list.rb
とpost_list.rb
を登録しておきます。
- 95-mruby.conf
protocol imap { mail_plugins = $mail_plugins imap_mruby } plugin { mruby_pre_list_path = /path/to/pre_list.rb mruby_post_list_path = /path/to/post_list.rb }
そして、実際にtelnetでアクセスし、list
コマンドを実行してみましょう。すると、
29906 ubuntu 20 0 87640 9508 7532 R 29.9 0.1 0:04.46 imap
なんということでしょう!
まとめ
以上のように、dovecot-mruby-pluginを使えば、dovecotのIMAPサーバを非常に容易に拡張することができます。また、dovecotは1セッションに対して1プロセスを生成破棄するCGI的プロセスモデルを採用しているため、mrubyのインタプリタもプロセス単位で読み込んでおり、各セッション間でmrubyの情報を共有することがなく安全です。
これまでのミドルウェア実装の経験上、セッション単位でプロセスを生成破棄するモデルは、セッション毎にmrubyインタプリタを作ったとしても、性能的にはプロセスの生成破棄がオーバーヘッドになることがわかっている(プロセス生成破棄が1秒間に500セッション処理できるマシンで、mrubyのインタプリタは1秒間に2000回インタプリタを作れる程度のコスト)ため、通常、性能面でも組み込みアーキテクチャが問題にはなることないでしょう。そういうこともあり、実装については非常に簡単でした。
とはいえ、dovecotのコードを読んでdovecotのアーキテクチャや実装を理解した上でpluginの仕様に基いて実装をするということについては、例のごとくドキュメントもなくC言語で書くしか無いので、それなりに敷居が高く手間もかかると思われますので、それをmrubyでwrapしてRubyで比較的誰でも拡張できるような状況になると良いなと思い作りました。いつものモチベーションですね。
ということで、是非まずは色々遊んで頂いて、その後各種用途にご活用頂けると幸いです。また、昨日については最低限ですので、追加機能やメソッドについてはissueで連絡頂けると積極的に実装したいと考えておりますのでお気軽にお問い合わせください。