メールの解析、構造化されたデータやJSON文字列の取得など、 シシマイが使える環境にバウンスメールを用意すれば直ぐに解析結果を得られる 最も簡単で基本的な使い方を紹介します。
標準入力(STDIN)からバウンスメールのデータを読み込み、 シシマイで解析する方法の記述例です。 PerlやRubyのワンライナーでパイプラインを経由して 読み込む場合に便利な入力方法です。
変数からバウンスメールのデータを読み込み シシマイで解析する方法の記述例です。バウンスメール をデータベースやジョブキューシステムから読み出して 解析する場合に便利な方法です。
Sisimaiは既定の動作で配信できなかったメールだけを解析結果として 出力しますが、配信が成功したメール(配信通知)も解析結果に含める Sisimaiクラスのメソッド使用方法を紹介します。
Sisimaiは既定の動作で配信できなかったメールだけを解析結果として 出力しますが、不在応答(Vacation)で返ってきたメールも解析結果に含める Sisimaiクラスのメソッド使用方法を紹介します。
コールバック機能を使うとシシマイが解析する前のデータ に対して、特定ヘッダの取得や部分的な書き換えなど、 独自の処理を行い、その結果を解析データに含めることができます。
Sisimaiクラスのdump()と
rise()メソッドは、第一引数にファイルハンドルSTDINを指定すると、
標準入力からバウンスメールのデータを読み込み、解析結果を返します。
% cat ./path/to/email | perl -MSisimai -lE 'print Sisimai->dump(STDIN)' | jq [ { "listid": "", "smtpagent": "Sendmail", ...
% cat ./path/to/email | perl -MSisimai -lE 'print $_->recipient->address for @{ Sisimai->rise(STDIN) }' kijitora@example.jp sironeko@example.org
Ruby版Sisimaiも同様に、Sisimaiクラスの
dump()とrise()メソッドの
第一引数にSTDINオブジェクトを指定すると、
標準入力からバウンスメールのデータを読み込み、解析結果を返します。
% cat ./path/to/email | ruby -Ilib -rsisimai -e 'puts Sisimai.dump(STDIN)' | jq [ { "catch": "", "token": "d295b517a3ad6f8748031e1068e07ebd089c0277", ...
% cat ./path/to/email | ruby -rsisimai -e 'p Sisimai.rise(STDIN)' [#<Sisimai::Data:0x007fa1b0993050 @addresser=#<Sisimai::Address:0x007fa1b0990e40 @user="kijitora", ...
Go版Sisimaiも同様に、libsisimai.org/sisimaiパッケージの
Rise()とDump()関数の
第一引数に文字列"STDIN"を指定すると、
標準入力からバウンスメールのデータを読み込み、解析結果を返します。
package main
import "fmt"
import "libsisimai.org/sisimai"
func main() {
args := sisimai.Args()
json, _ := sisimai.Dump("STDIN", args)
if json != nil && *json != "" { fmt.Printf("%s\n", *json) }
}
% cat ./path/to/email | go run ./sisid.go | jq [ { "catch": "", "token": "d295b517a3ad6f8748031e1068e07ebd089c0277", ...
Sisimaiクラスの make()メソッドはSisimai 5.0.0で廃止になりました。 新たに実装されたrise()メソッドをかわりに使ってください。
Sisimaiクラスのrise()と dump()の第一引数に変数 (Perlではスカラーリファレンス・RubyではStringオブジェクト・Goでは文字列) を指定することによって、ファイルとしてのメールや標準入力以外からバウンスメールのデータを読み込むことができます。
バウンスメールをデータベースやジョブキューシステムから読み出す場合に有用なこの機能 (Sisimai::Mail::Memory)はSisimai 4.23.0で実装されました。
rise()とdump()の第一引数に
スカラーリファレンスを指定すると、
Sisimai内部ではSisimai::Mail::Memoryが呼び出され、
メールのファイルや標準入力から読み込んだのと同様、解析結果が得られます。
#! /usr/bin/env perl
use Sisimai;
my $f = 'From MAILER-DAEMON Thu Dec 22 17:54:04 2015...'; # バウンスメール全体
my $v = Sisimai->rise(\$f);
for my $e ( @$v ) {
print $e->action; # "delayed"
print $e->deliverystatus; # "4.7.0"
print $e->reason; # "expired"
print $e->diagnosticcode; # "Envelope expired"
print $e->hardbounce; # 0
}
Ruby版Sisimaiではrise()と
dump()の第一引数にStringオブジェクト
を指定すると、Sisimai内部でSisimai::Mail::Memory
が呼び出され、メールのファイルや標準入力から読み込んだのと同様、解析結果が得られます。
#! /usr/bin/env ruby
require 'sisimai'
f = 'From MAILER-DAEMON Thu Dec 22 17:54:04 2015...' # バウンスメール全体
v = Sisimai.rise(f)
v.each do |e|
puts e.action # "delayed"
puts e.deliverystatus # "4.7.0"
puts e.reason # "expired"
puts e.diagnosticcode # "Envelope expired"
puts e.hardbounce # false
end
Rise()関数とDump()関数の第一引数に
バウンスメール全体を含んだ文字列を指定すると、Sisimai内部では
libisismai.org/sisimai/mail/memoryが呼び出され、
メールのファイルや標準入力から読み込んだのと同様に、解析結果が得られます。
package main
import "fmt"
import "libsisimai.org/sisimai"
func main() {
mail := 'From MAILER-DAEMON Fri Feb 2 18:30:22 2018...' // バウンスメール全体
args := sisimai.Args()
data, _ := sisimai.Rise(mail, args)
if len(*data) > 0 { fmt.Printf("%s\n", data[0].Recipient.Address) }
}
Sisimaiクラスの make()メソッドはSisimai 5.0.0で廃止になりました。 新たに実装されたrise()メソッドをかわりに使ってください。
Sisimaiクラスのrise()と dump()は第二引数にdelivered を指定することによって、配信が成功した事を通知するメールも解析結果に含めることができます。
配送が成功したメールは
github.com/sisimai/set-of-emails
リポジトリの
rfc3464-28.eml
がサンプルとして用意してあります。
ここでは、このメールを解析するサンプルコードを紹介します。
rise()とdump()の第二引数に
delivered => 1を指定すると、
配信に成功したメール(配信通知)も解析結果に含まれるようになります。
deliveredに偽となる値(0,undef,'')
を指定した場合は、解析結果は生成されません。
#! /usr/bin/env perl
use Sisimai;
my $f = './set-of-emails/maildir/bsd/rfc3464-28.eml';
my $v = Sisimai->rise($f, 'delivered' => 1);
for my $e ( @$v ) {
print $e->action; # "delivered"
print $e->deliverystatus; # "2.1.5"
print $e->reason; # "delivered"
print $e->diagnosticcode; # "250 2.1.5 Ok"
print $e->hardbounce; # 0
}
Perl版と同様に、Ruby版Sisimaiでもrise()と
dump()の第二引数にdelivered: trueを指定すると、
配信に成功したメール(配信通知)も解析結果に含まれるようになります。
deliveredに偽となる値(nil,false)を指定した場合は、
解析結果は生成されません。
#! /usr/bin/env ruby
require 'sisimai'
f = './set-of-emails/maildir/bsd/rfc3464-28.eml'
v = Sisimai.rise(f, delivered: true)
v.each do |e|
puts e.action # "delivered"
puts e.deliverystatus # "2.1.5"
puts e.reason # "delivered"
puts e.diagnosticcode # "250 2.1.5 Ok"
puts e.hardbounce # false
end
Perl版やRuby版と同様にGo版SisimaiでもRise()と
Dump()の第二引数に
指定する
sis.DecodingArgs
構造体のDelivered
にtrueを指定すると、配信に成功したメール(配信通知)も
解析結果に含まれるようになります。Deliveredの初期値は
falseです。
package main
import "fmt"
import "libsisimai.org/sisimai"
func main() {
path := "./set-of-emails/maildir/bsd/rfc3464-28.eml"
args := sisimai.Args()
args.Delivered = true
sisi, _ := sisimai.Rise(path, args)
if len(*sisi) > 0 { fmt.Printf("Recipient = %s\n", *sisi[0].Recipient.Address }
}
Sisimaiクラスの make()メソッドはSisimai 5.0.0で廃止になりました。 新たに実装されたrise()メソッドをかわりに使ってください。
Sisimai 5.0.0でバウンス理由が vacationとなる バウンスメールはデフォルトで解析されなくなりました。必要な場合は vacationパラメーターをrise() メソッドに指定してください。
Sisimaiクラスのrise()と dump()は第二引数にvacation を指定することによって、不在応答(Vacation)のメールも解析結果に含めることができます。
不在応答のメールは
github.com/sisimai/set-of-emails
リポジトリの
rfc3834-01.eml
がサンプルとして用意してあります。
ここでは、このメールを解析するサンプルコードを紹介します。
rise()とdump()の第二引数に
vacation=> 1を指定すると、
不在応答のメールも解析結果に含まれるようになります。vacation
に偽となる値(0,undef,'')を指定した場合は、解析結果は生成されません。
#! /usr/bin/env perl
use Sisimai;
my $f = './set-of-emails/maildir/bsd/rfc3834-01.eml';
my $v = Sisimai->rise($f, 'vacation' => 1);
for my $e ( @$v ) {
print $e->action; # ""
print $e->deliverystatus; # ""
print $e->reason; # "vacation"
print $e->diagnosticcode; # "I am currently away returning to the office on May 5th."
print $e->hardbounce; # 0
}
Perl版と同様に、Ruby版Sisimaiでもrise()と
dump()の第二引数にvacation: trueを指定すると、
不在通知のメールも解析結果に含まれるようになります。vacation
に偽となる値(nil,false)を指定した場合は、解析結果は生成されません。
#! /usr/bin/env ruby
require 'sisimai'
f = './set-of-emails/maildir/bsd/rfc3834-01.eml'
v = Sisimai.rise(f, vacation: true)
v.each do |e|
puts e.action # ""
puts e.deliverystatus # ""
puts e.reason # "vacation"
puts e.diagnosticcode # "I am currently away returning to the office on May 5th."
puts e.hardbounce # false
end
Perl版やRuby版と同様にGo版SisimaiでもRise()関数と
Dump()関数の第二引数に指定する構造体
sis.DecodingArgs
の
Vacationにtrueを指定すると、
不在応答のメールも解析結果に含まれるようになります。
Vacationの初期値はfalseです。
package main
import "fmt"
import "libsisimai.org/sisimai"
func main() {
path := "./set-of-emails/maildir/bsd/rfc3834-01.eml"
args := sisimai.Args()
args.Vacation = true
sisi, _ := sisimai.Rise(path, args)
if len(*sisi) > 0 { fmt.Printf("Recipient = %s\n", *sisi[0].Recipient.Address }
}
Sisimaiクラスの make()メソッドはSisimai 5.0.0で廃止になりました。 新たに実装されたrise()メソッドをかわりに使ってください。
Sisimai 5.0.0でコールバック機能の仕様(引数の数・型)が変りました。 非互換の変更につき、Version 4でのコールバック機能は4-stable ブランチの GitHub/Callback Feature を確認してください。
Sisimai 4.19.0から実装されたコールバック機能を使用すると、 Sisimai::FactまたはSisimai::Message のcatch()メソッドを通して、バウンスメールを独自に処理した結果を取得することができます。 この機能はSisimaiが解析結果に含めない元メッセージ全体や 元メッセージの特定のヘッダの値(例えば配信IDなど)を取り出したりするのに便利です。
Sisimaiクラスのrise()メソッドにおける
c___ (cと_が三つ、
釣り針に見える)に渡せる引数は、Perlのフックメソッド(サブルーチン)には配列リファレンス1つ、
RubyのフックメソッドにはArrayオブジェクト1つ
です。引数の構造は以下のようになっています。
c___に渡す配列のインデックス | 処理対象 |
---|---|
[0] (下記コード例にある$code0) | メールのヘッダーheadersおよび本文 message |
[1] (下記コード例にある$code1) | 元メールのファイルに対して |
my $code0 = sub { my $args = shift; my $head = $args->{'headers'}; # ヘッダー my $body = $args->{'message'}; # 本文 }; my $code1 = sub { my $args = shift; my $kind = $args->{'kind'}; # (String) Sisimai::Mail->kind my $mail = $args->{'mail'}; # (*String) Entire email message my $path = $args->{'path'}; # (String) Sisimai::Mail->path my $fact = $args->{'fact'}; # (*Array) List of Sisimai::Fact }; my $list = Sisimai->rise('/path/to/mbox', 'c___' => [$code0, $code1]);
messageで得られる文字列はバウンスメールの本文です。
バウンスメールそのもののヘッダーは含まれていませんので、ハッシュ化された
headersを参照してください。
キー名 | データ型 | 説明 |
---|---|---|
headers | Hash | バウンスメールのヘッダ部分(ハッシュ化済み) |
message | String | ヘッダーは含まないバウンスメールのメール本文 |
mailで得られる文字列は、バウンスメールそのもの
のヘッダーも含むバウンスメール全体です。
キー名 | データ型 | 説明 |
---|---|---|
kind | String | 元メールファイルの種類 |
*String | ヘッダーも含むバウンスメール全体 | |
path | String | 元メールファイルへのPATH |
fact | Array | 解析結果であるSisimai::Factのリスト |
libsisimai.org/sisimaiパッケージのRise()
関数における
sis.DecodingArgs
構造体のCallback0と
Callback1の引数と戻り値はそれぞれ以下のような構造になっています。
sis.DecodingArgs | 引数 | 戻り値 |
---|---|---|
Callback0 |
*sis.CallbackArg0
|
|
Callback1 |
*sis.CallbackArg1
|
|
args := sisimai.Args() args.Callback0 = func(arg *sisimai.CallbackArg0) (map[string]interface{}, error) { fmt.Printf("Headers = %v\n", (*arg).Headers) // ヘッダー全て fmt.Printf("- From: = %s\n", (*arg).Headers["from"][0]) fmt.Printf("- Subject: = %s\n", (*arg).Headers["subject"][0]) fmt.Printf("Body = %s\n", (*arg).Payload) // 本文 data := make(map[string]interface{}) data["from"] = (*arg).Headers["from"][0] return data, nil } args.Callback1 = func(arg *sisimai.CallbackArg1) (bool, error) { fmt.Printf("- Path = %s\n", arg.Path) // 元メールファイルのPATH fmt.Printf("- Kind = %s\n", arg.Kind) // 元メールファイルの種類 fmt.Printf("Body = %s\n", (*arg.Mail)) // 本文 fmt.Printf("Fact = %s\n", (*arg.Fact)) // 解析結果 return true, nil } sisi, nyaan := sisimai.Rise(path, args)
rhost-google-03.eml にある以下の内容をコールバックで実行するコードによって取得し、元メールへの操作例を示します。
Return-Path: <> Received: from mail4.example.com (mx4.example.com [192.0.2.24]) by secure.example.jp (Postfix) with ESMTPS id 4SnNbh0r3mz24sXs for; Thu, 9 Apr 2022 23:34:45 +0900 (JST) Received: by mail4.example.com (Postfix) id t8WLKlmm99zqL71C; Thu, 9 Apr 2022 23:34:45 +0900 (JST) Authentication-Results: secure.example.jp; spf=fail smtp.helo=mail4.example.com Date: Thu, 9 Apr 2022 23:34:45 +0900 (JST) From: MAILER-DAEMON@mail4.example.com (Mail Delivery System) Subject: Undelivered Mail Returned to Sender To: nginx@mail4.example.com ... (expanded from ): host gmail-smtp-in.l.google.com[64.233.188.26] said: 550-5.7.26 The MAIL FROM domain [mail4.example.com] has an SPF record with a 550-5.7.26 hard fail policy (-all) but it fails to pass SPF checks with the ip: 550-5.7.26 [192.0.2.24]. To best protect our users from spam and phishing, 550-5.7.26 the message has been blocked. For instructions on setting up 550-5.7.26 authentication, go to 550 5.7.26 https://support.google.com/mail/answer/81126#authentication 2-222222630d46222222b002b2d2neko2cat2222222cat.22 - gsmtp (in reply to end of DATA command) --Lms5yfCw0Vz5CRp3.1647426971/mail4.example.com Content-Description: Delivery report Content-Type: message/delivery-status Reporting-MTA: dns; mail4.example.com X-Postfix-Queue-ID: Lms5yfCw0Vz5CRp3 X-Postfix-Sender: rfc822; nginx@mail4.example.com Arrival-Date: Thu, 9 Apr 2022 23:34:45 +0900 (JST) Final-Recipient: rfc822; kijitora@google.example.com Original-Recipient: rfc822;neko@example.co.jp Action: failed Status: 5.7.26 Remote-MTA: dns; gmail-smtp-in.l.google.com Diagnostic-Code: smtp; 550-5.7.26 The MAIL FROM domain [mail4.example.com] has ...
Sisimaiクラスのrise()とdump()
メソッドに、実行したい処理内容を書いたサブルーチンのコードリファレンス2個を第二引数
c___として渡すことができます。
% cat set-of-emails/maildir/bsd/rhost-google-03.eml | perl ./sisid.pl
#! /usr/bin/env perl
# sisid.pl
use Sisimai;
my $c0 = sub {
my $args = shift;
my $data = { 'queue-id' => '', 'from' => '', 'return-path' => '' };
$data->{'return-path'} = $args->{'headers'}->{'return-path'} || '';
$data->{'from'} = $args->{'headers'}->{'from'} || '';
if( $args->{'message'} =~ m/^X-Postfix-Queue-ID:\s*(.+)$/m ) {
$data->{'queue-id'} = $1;
}
return $data;
};
my $c1 = sub {
my $args = shift;
my $kind = $args->{'kind'}; # (String) Sisimai::Mail->kind
my $mail = $args->{'mail'}; # (*String) Entire email message
my $path = $args->{'path'}; # (String) Sisimai::Mail->path
if $kind eq 'stdin' {
my $f = IO::File->new("/tmp/sisimai-decoded-1.eml", 'w');
print $f $$mail; $f->close;
}
};
my $rv = Sisimai->rise(STDIN, 'c___' => [$c0, $c1]);
printf("From: %s\n", $rv->[0]->{'catch'}->{'from'});
printf("Queue-ID: %s\n", $rv->[0]->{'catch'}->{'queue-id'});
printf("Return-Path: %s\n", $rv->[0]->{'catch'}->{'return-path'});
Sisimaiクラスのrise()と
dump()メソッドに、実行したい処理内容を書いた
Procオブジェクトをc___:
として渡すことができます。
% cat set-of-emails/maildir/bsd/rhost-google-03.eml | ruby ./sisid.rb
#! /usr/bin/env ruby
# sisid.rb
require 'sisimai'
c0 = lambda do |args|
data = { 'queue-id' => '', 'from' => '', 'return-path' => '' }
data['return-path'] = args['headers']['return-path'] || ''
data['from'] = args['headers']['from'] || ''
if cv = args['message'].match(/^X-Postfix-Queue-ID:\s*(.+)$/)
data['queue-id'] = cv[1]
end
return data
end
c1 = lambda do |args|
kind = args['kind'] # (String) Sisimai::Mail.kind
mail = args['mail'] # (*String) Entire email message
path = args['path'] # (String) Sisimai::Mail.path
if kind == 'stdin'
File.open("/tmp/sisimai-decoded-1.eml", "w") do |f|
f.puts(mail)
end
end
end
rv = Sisimai.rise(fn, c___: [c0, c1])
printf("From: %s\n", rv[0].catch['from'])
printf("X-Postfix-Queue-ID: %s\n", rv[0].catch['queue-id'])
printf("Return-Path: %s\n", rv[0].catch['return-path'])
libsisimai.org/sisimaiパッケージのRise()関数と
Dump()関数に渡す
sis.DecodingArgs
構造体の
Callback0とCallback1に
実行したい処理内容を書いた関数を代入して渡すことができます。
% cat set-of-emails/maildir/bsd/rhost-google-03.eml | go run ./sisid.go
package main
// sisid.go
import "fmt"
import "strings"
import "libsisimai.org/sisimai"
func main() {
args := sisimai.Args() // sis.DecodingArgs{}
list := []string{"Return-Path", "From"}
args.Callback0 = func(arg *sisimai.CallbackArg0) (map[string]interface{}, error) {
data := make(map[string]interface{})
data["queue-id"] = ""
for _, e := range list {
ff := strings.ToLower(e)
if len((*arg).Headers[ff]) == 0 || (*arg).Headers[ff][0] == "" { continue }
data[ff] = (*arg).Headers[ff][0]
}
if arg.Payload != nil && len(*arg.Payload) > 0 {
name := "X-Postfix-Queue-ID"
mesg := *arg.Payload
p0 := strings.Index(mesg, "\n" + name + ": "); if p0 < 0 { return data, nil }
p1 := p0 + len(name) + 3
p2 := strings.Index(mesg[p1:], "\n"); if p2 < 0 { return data, nil }
data["queue-id"] = mesg[p1:p1 + p2]
}
return data, nil
}
args.Callback1 = func(arg *sisimai.CallbackArg1) (bool, error) {
if nyaan := ioutil.WriteFile("/tmp/sisimai-decoded-1.eml", []byte(*arg.Mail), 0600); nyaan != nil {
return false, nyaan
}
return true, nil
}
// sisi is a pointer to []sis.Facti
sisi, nyaan := sisimai.Rise("STDIN", args)
if len(*sisi) > 0 {
list = append(list, "Queue-ID")
for _, e := range *sisi {
// e is a sis.Fact struct
fmt.Printf("Recipient = %s\n", e.Recipient.Address)
re, as := e.Catch.(map[string]interface{})
if as == false { continue }
for _, f := range list {
ff := strings.ToLower(f)
if ca, ok := re[ff].(string); ok {
fmt.Printf("- Catch[%s] = %s\n", f, ca)
}
}
}
}
// nyaan is a pointer to []sis.NotDecoded
if len(*nyaan) > 0 { fmt.Fprintf(os.Stderr, "%v\n", *nyaan) }
}
Sisimaiクラスのdump() メソッドにフックメソッドを渡して得たJSON文字列は次のような内容になります。
[ { "addresser": "kijitora@google.example.com", "messageid": "vDcsF1hr4zzywp69@mail4.example.com", "recipient": "kijitora@google.example.com", "diagnosticcode": "host gmail-smtp-in.l.google.com[64.233.188.26] said: The MAIL FROM domain [mail4.example.com] has an SPF record with a hard fail policy (-all) but it fails to pass SPF checks with the ip: [192.0.2.24]. To best protect our users from spam and phishing, the message has been blocked. For instructions on setting up authentication, go to https://support.google.com/mail/answer/81126#authentication 2-222222630d46222222b002b2d2neko2cat2222222cat.22 - gsmtp (in reply to end of DATA command)", "smtpagent": "Postfix", "destination": "google.example.com", "alias": "neko@example.co.jp", "reason": "authfailure", "lhost": "gmail-smtp-in.l.google.com", "senderdomain": "google.example.com", "token": "fe75b94dd0489b106fa041c460064dd6afa8196b", "subject": "Nyaan", "deliverystatus": "5.7.26", "timezoneoffset": "+0900", "listid": "", "origin": "set-of-emails/maildir/bsd/rhost-google-03.eml", "action": "failed", "timestamp": 1649514885, "hardbounce": 0, "rhost": "gmail-smtp-in.l.google.com", "catch": { "size": 4436, "kind": "Mailbox", "from": "MAILER-DAEMON@mail4.example.com (Mail Delivery System)", "return-path": "<>", "queue-id": "Lms5yfCw0Vz5CRp3", }, "diagnostictype": "SMTP", "replycode": 550, "feedbacktype": "", "smtpcommand": "DATA" } ]