読者です 読者をやめる 読者になる 読者になる

/var/lib/azumakuniyuki

Sisimaiとか技術的なことはこっちに書いてみようかという試み

Sisimai 4.19で実装したコールバック機能

sisimai

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に繋いで取ってきた何かを入れてもよいです。

得られた解析結果

出力されるJSONPerl版でも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:シシマイ: バウンスメールを解析して構造化するライブラリです