vital-smtpを作った

Vim Advent Calendar 2014の7日目の記事です。 昨日は@katono123さんのVim script + Lua で rogue.vim を作った話でした。

以前unite-notmuchというメールを閲覧するプラグインを作ったのですが、 メールを送信する機能がないため、これと組み合わせて使うメール送信プラグインが欲しいと思っていました。

探してみると

などが見つかりましたが、これらは単体で使用するためのプラグインなので他のプラグインと組み合わせて使うには向いていないようです。
やはりVimでメールを送りたいという人がいないからなのか、これといったものが見つからなかったので自作してみることにしました。

他のプラグインと組み合わせて使うというと真っ先に思いつくのがvital.vimです。
少し前から気にはなっていたのですが(vital-overなど)、vital.vimは独自に外部モジュール作ることができるようなので今回はそれを試してみます。

1. vital外部モジュールの作成

作成するのはSMTPでメールを送信するvitalモジュールなのでプラグイン名は vital-smtp とします。
まずはプラグイン用のディレクトリ、 vital-smtp を作成します。
中身は本家vital.vimと同じでautoload配下に vital/__latest__ を配置すれば良いようです。
プラグイン本体はその配下に配置することになるので SMTP.vim を作成します。

vital-smtp
└── autoload
     └── vital
         └── __latest__
             └── SMTP.vim

完成したプラグインこちらです。
pythonのsmtplibを利用してメールを送信する s:sendmail という関数があるだけの簡単なプラグインになりました。

function! s:sendmail(addr, user, password, from_addr, to_addrs, msg)
    " 省略
endfunction

2. vital外部モジュールの使い方

vitalの外部モジュールを作成したら次は実際にプラグインに組み込んでみることにします。
とりあえず確認用なので適当なディレクトリにサンプルプラグインを作ります。

$ mkdir vim-sendmail-example
$ cd vim-sendmail-example/
$ mkdir autoload plugin
$ vim plugin/sendmail_example.vim

組み込み方はvital-smtpVimのruntimepathに通っていれば通常のvitalモジュールと同じようにVitalizeコマンドを実行するだけです。

:Vitalize --name=sendmail_example . SMTP

プラグイン内での呼び出し方も同様で、 import するだけで先ほど作成した関数を使うことができるようになります。

let s:SMTP = vital#of('sendmail_example').import('SMTP')
" call s:SMTP.sendmail()が使えるようになる

完成したサンプルプラグインこちらです。
:Sendmail コマンドを実行するとプロンプトに入力した内容でメールを送信します。

3. メール送信の確認

手っ取り早く使えそうなSMTPサーバが思いつかなかったのでローカルのメールサーバでサンプルプラグインの確認をします。

# pythonに付属しているデバッグ用サーバを起動する
$ sudo python -m smtpd -c DebuggingServer localhost:10025

サーバが起動したらVimを立ち上げてサンプルプラグインの:Sendmailコマンドを実行します。

:Sendmail
host: localhost<Enter>
port: 10025<Enter>
user: <Enter><Enter>
password: <Enter>
from: test@example.com<Enter>
to: test@example.com<Enter>
msg: test<Enter>

msgにtestと入力してEnterを押すとメールサーバのコンソールにVimから送ったメールが表示されました。

---------- MESSAGE FOLLOWS ----------
From: test@example.com
To: test@example.com
X-Peer: 127.0.0.1

test
------------ END MESSAGE ------------

4. テストの追加

順番は置いておいて、せっかくプラグインを作ったのでthincaさんのvim-themisを使ったテストを追加しておきます。

vital-smtp
├── autoload
│   └── vital
│       └── __latest__
│           └── SMTP.vim
└── test
    └── SMTP.vim

テストはthemisコマンドに --runtimepath でvital-smtpに必要なプラグインを指定して実行します。

$ themis --runtimepath /tmp/vital.vim
1..9
ok 1 - SMTP test_import
ok 2 - SMTP test_sendmail_success
ok 3 - SMTP test_sendmail_success_without_starttls
ok 4 - SMTP test_sendmail_success_without_login
ok 5 - SMTP test_sendmail_failed_to_connect
ok 6 - SMTP test_sendmail_failed_to_starttls
ok 7 - SMTP test_sendmail_failed_to_login
ok 8 - SMTP test_sendmail_failed_to_send
ok 9 - SMTP test_sendmail_failed_to_quit

# tests 9
# passes 9

5. ハマったところ

if_pyの例外

pythonで例外が発生するとコマンドラインにはpythonのTraceが表示されますが、 v:exception が常に 'Vim(python):Traceback (most recent call last):' になってしまうため、 Vim script内ではcatchできなくて困りました。
これが仕様なのかはよくわかりませんが、今回はif_py側で例外を捕捉したら文字列にしてVim script側に返す作りにしました。

if_pyを使った関数のテスト

pythonのsmtplib周りのテストにはメールサーバが必要なのですが、 例外を発生させるためのメールサーバを用意するのが面倒だったので mockを使うことにしました。
ただテストが

  1. Vim script: テスト関数が呼ばれるとmockを使うためにpythonを呼び出す(テストスクリプト)
  2. python: mockでsmtplibの挙動を制御して外部モジュールを呼び出す(テストスクリプト)
  3. Vim script: 引数を受け取ってpythonに渡す(外部モジュール)
  4. python: smtplibを使ってメールを送信し、例外が発生したらVim scriptに返す(外部モジュール)
  5. Vim script: pythonで例外が発生していたらthrowする(外部モジュール)
  6. python: 外部モジュールで例外が発生していたら文字列にして返す(テストスクリプト)
  7. Vim script: 戻り値が期待通りか判定する(テストスクリプト)

と、Vim scriptとpythonを行ったり来たりでとても気持ち悪いものになってしまいました。


さて、目的のメール送信プラグインは作れたのでそのうちunite-notmuchにvital-smtpを組み込むなどしてメールを送信できるようにしたいと思います。
が、MIME関連の機能が足りないのでその前にvital-mimeが必要になるのかも...!?

それからテストをTravis CIで動かしたくてTravis CIにアカウントを作ってみたのですがローカルで全部通ったテストが大コケしたまま、 この記事を書いていたTokyo Vim#23タイムアウト(スマブラタイム)してしまったので時間があるときに調べてみることにします。

 
明日は去年に引き続き@Linda_ppさんです。  
 
 
スマブラでボコボコにされている様子。