Sisimai 4.19で実装したコールバック機能
2016年の10月18日にリリースしたSisimai 4.19.0*1 で、コールバック機能を実装しました。バウンスメールの解析を実行するmake()
メソッドと解析結果をJSONで返すdump()
メソッドに、任意のコードを引数として渡せるようにしました。
この機能の目的は、バウンスメール本文(BODY)から任意の値を取り出して、それを解析結果に含めることです。たとえば、元メッセージにある配信IDの値のような固有値や、Sisimaiが標準で取得しない値を解析結果に入れるのに便利です。
使い方の例
バウンスメールの内容
Microsoft Exchange Server 2007が返してきたバウンスメールから、X-Display-Name: Neko
の値(Neko)を取り出して、解析結果に入れるコードを書くことにします。下記は一部を抜粋したもので、全文はリポジトリにexchange2007-01.emlという名前であります。
Return-Path: <> X-Original-To: kijitora@example.jp Delivered-To: kijitora@example.jp Received: from localhost (localhost [127.0.0.1]) by example.jp.localdomain (Postfix) with ESMTP id FFFF0000FFFF for <kijitora@example.jp>; Thu, 22 Feb 2011 23:34:45 +0900 (JST) (長いので省略) Message-ID: <000000000000000000000000000000000000000000000000@example.com> Subject: Undeliverable: Nyaan --0000ffff-0000-0000-0000-0000 Content-Type: multipart/alternative; differences=Content-Type; boundary="eeee0000-0022-2200-2220" --eeee0000-0022-2200-2220 Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: quoted-printable Delivery has failed to these recipients or distribution lists: Neko<mailto:mikeneko@example.co.jp> The recipient's e-mail address was not found in the recipient's e-mail syst= em. Microsoft Exchange will not try to redeliver this message for you. Plea= se check the e-mail address and try resending this message, or provide the = following diagnostic text to your system administrator. ________________________________ Sent by Microsoft Exchange Server 2007 Diagnostic information for administrators: Generating server: mta4.example.org mikeneko@example.co.jp #550 5.1.1 RESOLVER.ADR.RecipNotFound; not found ## Original message headers: Received: from mx9.example.net (172.21.25.141) by SMTP2.TCAABUDHABI.AE (172.21.24.13) with Microsoft SMTP Server id 8.2.234.1; Thu, 22 Feb 2011 23:34:45 +0900 Date: Thu, 22 Feb 2011 23:34:45 +0900 Subject: Nyaan From: Kijitora <kijitora@example.jp> To: Neko <mikeneko@example.co.jp> MIME-Version: 1.0 Content-Type: text/plain; --eeee0000-0022-2200-2220 Content-Type: text/html; charset="us-ascii" Content-Transfer-Encoding: quoted-printable (長いのでHTMLパートも省略) --eeee0000-0022-2200-2220-- --0000ffff-0000-0000-0000-0000 Content-Type: message/delivery-status Reporting-MTA: dns;mx4.example.org Received-From-MTA: dns;mx9.example.net Arrival-Date: Thu, 22 Feb 2011 23:34:45 +0900 Final-Recipient: rfc822;mikeneko@example.co.jp Action: failed Status: 5.1.1 Diagnostic-Code: smtp;550 5.1.1 RESOLVER.ADR.RecipNotFound; not found X-Display-Name: Neko ←この値を取り出す --0000ffff-0000-0000-0000-0000 Content-Type: message/rfc822 Received: from mx9.example.net (192.0.2.229) by mx8.example.net (192.0.2.228) with Microsoft SMTP Server id 8.2.222.2; Thu, 22 Feb 2011 23:34:45 +0900 Date: Thu, 22 Feb 2011 23:34:45 +0900 Subject: Nyaan From: Kijitora <kijitora@example.jp> To: Neko <mikeneko@example.co.jp> MIME-Version: 1.0 Content-Type: multipart/alternative; boundary="_=neko00022222002202020=_" (長いので元メッセージのBODYも省略)
呼び出しコード例(Perl)
Perl版Sisimaiでは、make()
とdump()
メソッドの引数hook
にコードリファレンスを渡します。
#!/usr/bin/env perl use strict; use warnings; use Sisimai; my $file = './exchange2007-01.eml'; my $call = sub { my $argv = shift; my $data = { 'name' => '' }; if( $argv->{'message'} =~ m/^X-Display-Name:\s*(.+)$/m ) { # 正規表現で目的の値を捉える $data->{'name'} = $1; } return $data; }; my $data = Sisimai->make($file, 'hook' => $call); my $json = Sisimai->dump($file, 'hook' => $call); print $data->[0]->catch->{'name'}; # catch()メソッドで呼び出す print $json;
コードリファレンスの先にあるサブルーチンがreturn
する値が、そのまま解析結果のcatch()
メソッドで呼出せます。
呼び出しコード例(Ruby)
Ruby版SisimaiでもPerl版と同様です。make()
やdump()
メソッドのhook
に、Procオブジェクトを指定します。
#!/usr/bin/env ruby require 'sisimai' file = './exchange2007-01.eml'; call = lambda { |argv| data = { 'name' => '' } if cv = argv['message'].match(/^X-Display-Name:\s*(.+)$/) # 正規表現で欲しい値を捕まえる data['name'] = cv[1] end return data } data = Sisimai.make(file, hook: call) json = Sisimai.dump(file, hook: call) puts data[0].catch['name'] # catch()メソッドで呼び出す puts json
Ruby版でもPerl版と同じ要領です。例として書いたコードではX-Display-Name
の値だけを取り出していますが、引数として渡すコードリファレンス・Procオブジェクトが返す値はなんでも入りますし、どんな巨大なデータでも入ります。
また、バウンスメールの中身と関係のない値、例えば解析を実行した時刻やホスト名を入れても良いですし、プロセスIDでも良いですし、会員DBに繋いで取ってきた何かを入れてもよいです。
得られた解析結果
出力されるJSONはPerl版でもRuby版でも同じです。コールバック機能を使ったコードリファレンスやProcオブジェクトを指定してなんらかの値が帰ってきた場合は、catch
の中にJSON化された値が入ります。
[ { "catch": { "name": "Neko" }, "token": "e7bc284c04f9c8481d4fad50828be411c864833a", "lhost": "", "rhost": "", "alias": "", "listid": "", "reason": "userunknown", "action": "failed", "subject": "Nyaan", "messageid": "", "replycode": "550", "smtpagent": "Exchange2007", "softbounce": 0, "smtpcommand": "", "destination": "example.co.jp", "senderdomain": "example.jp", "feedbacktype": "", "diagnosticcode": "#550 5.1.1 RESOLVER.ADR.RecipNotFound; not found ##", "diagnostictype": "", "deliverystatus": "5.1.1", "timezoneoffset": "+0900", "addresser": "kijitora@example.jp", "recipient": "mikeneko@example.co.jp", "timestamp": 1298385285 } ]
入ってくるデータ構造
コードリファレンスの先にある無名サブルーチンやProcオブジェクトに入ってくるデータ構造は決まっています。解析対象のバウンスメールのヘッダ部分がHash化されたものと、BODY部分が改行を含む文字列になったもの、の二つがまとまって入ってきます。
キー名 | データ型 | 説明 |
---|---|---|
headers | Hash | バウンスメールのヘッダ部分(ハッシュ化済み) |
message | String | バウンスメールのメール本文(改行・元メッセージを含む) |
前述のexchange2007-01.eml
ファイルは、次のようなデータ構造で無名サブルーチン・Procオブジェクトの第1引数として渡されます。
$VAR1 = { 'headers' => { 'reply-to' => undef, 'content-language' => 'en-US', 'from' => 'mailer-daemon@example.com', 'message-id' => '<000000000000000000000000000000000000000000000000@example.com>', 'x-mailer' => undef, 'to' => 'kijitora@example.jp', 'subject' => 'Undeliverable: Nyaan', 'content-transfer-encoding' => undef, 'content-type' => 'multipart/report; report-type=delivery-status; boundary="0000ffff-0000-0000-0000-0000"', 'return-path' => '<>', 'received' => [ 'from localhost (localhost [127.0.0.1]) by example.jp.localdomain (Postfix) with ESMTP id FFFF0000FFFF for <kijitora@example.jp>; Thu, 22 Feb 2011 23:34:45 +0900 (JST)', 'from example.jp.localdomain ([127.0.0.1]) by localhost (example.jp [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id ffff00ee for <kijitora@example.jp>; Thu, 22 Feb 2011 23:34:45 +0900 (JST)' ], 'date' => 'Thu, 22 Feb 2011 23:34:45 +0900' }, 'message' => '--0000ffff-0000-0000-0000-0000 Content-Type: multipart/alternative; differences=Content-Type; boundary="eeee0000-0022-2200-2220" ...(長いので省略) --0000ffff-0000-0000-0000-0000-- ' };
変なコードを渡すと...
渡したコードリファレンスはeval {}
で、Procオブジェクトはbegin ... rescue ... end
で包んでいるので、実行時にエラーが発生すると、次のような警告が出るだけで、解析処理は続行されます。
Perl
#!/usr/bin/env perl use strict; use warnings; use Sisimai; my $file = './exchange2007-01.eml'; my $call = sub { 4 / 0; }; # 0除算する print Sisimai->dump($file, 'hook' => $call);
わざとエラーを発生させるために、0除算するコードを入れたコードリファレンスを渡すと、警告として標準エラーに何かが出ますが、処理は続行されるので解析結果は得られます。
***warning: Something is wrong in hook method:Illegal division by zero at ./cb.pl line 15. [{"replycode":550,"diagnosticcode":"#550 5.1.1 RESOLVER.ADR.RecipNotFound; not found ##","addresser":"kijitora@example.jp","recipient":"mikeneko@example.co.jp","senderdomain":"example.jp","token":"e7bc284c04f9c8481d4fad50828be411c864833a","action":"failed","rhost":"","listid":"","catch":null,"messageid":"","alias":"","diagnostictype":"SMTP","softbounce":0,"feedbacktype":"","deliverystatus":"5.1.1","reason":"userunknown","smtpcommand":"","smtpagent":"Exchange2007","subject":"Nyaan","lhost":"","timestamp":1298385285,"timezoneoffset":"+0900","destination":"example.co.jp"}]
Ruby
#!/usr/bin/env ruby require 'sisimai' file = './exchange2007-01.eml'; call = lambda { |argv| 4 / 0 } puts Sisimai.dump(file, hook: call)
Ruby版でも同じく、0除算のエラーが警告として標準エラーに表示されますが、処理は続行されて解析結果を得ることができます。
***warning: Something is wrong in hook method :divided by 0 [{"catch":"","token":"e7bc284c04f9c8481d4fad50828be411c864833a","lhost":"","rhost":"","alias":"","listid":"","reason":"userunknown","action":"failed","subject":"Nyaan","messageid":"","replycode":"550","smtpagent":"Exchange2007","softbounce":0,"smtpcommand":"","destination":"example.co.jp","senderdomain":"example.jp","feedbacktype":"","diagnosticcode":"#550 5.1.1 RESOLVER.ADR.RecipNotFound; not found ##","diagnostictype":"","deliverystatus":"5.1.1","timezoneoffset":"+0900","addresser":"kijitora@example.jp","recipient":"mikeneko@example.co.jp","timestamp":1298385285}]
実装した経緯
自分が案件で必要になったから、です。僕以外でも、配信するメールに追跡用のヘッダや配信IDのような固有値を入れている場合は、役に立つ機能かもしれないです、たぶん。
*1:シシマイ: バウンスメールを解析して構造化するライブラリです