メールの解析、構造化されたデータやJSON文字列の取得など、 シシマイが使える環境にバウンスメールを用意すれば直ぐに解析結果を得られる 最も簡単で基本的な使い方を紹介します。
標準入力(STDIN)からバウンスメールのデータを読み込み、 シシマイで解析する方法の記述例です。 PerlやRubyのワンライナーでパイプラインを経由して 読み込む場合に便利な入力方法です。
変数からバウンスメールのデータを読み込み シシマイで解析する方法の記述例です。バウンスメール をデータベースやジョブキューシステムから読み出して 解析する場合に便利な方法です。
Sisimaiは既定の動作で配信できなかったメールだけを解析結果として 出力しますが、配信が成功したメール(配信通知)も解析結果に含める Sisimaiクラスのメソッド使用方法を紹介します。
Sisimaiは既定の動作で配信できなかったメールだけを解析結果として 出力しますが、不在応答(Vacation)で返ってきたメールも解析結果に含める Sisimaiクラスのメソッド使用方法を紹介します。
コールバック機能を使うとシシマイが解析する前のデータ に対して、特定ヘッダの取得や部分的な書き換えなど、 独自の処理を行い、その結果を解析データに含めることができます。
Sisimai 5.8.0より標準入力から読み込むデータの最大値は2GB に制限されるようになりました。また、読み込んだデータのうちメール一通あたりは 64MBまでに制限されます。
Sisimaiクラスの make()メソッドはSisimai 5.0.0で廃止になりました。 新たに実装されたrise()メソッドをかわりに使ってください。
Sisimai(シシマイ)のrise()やdump()メソッドは、 引数にファイル名を指定する以外に、STDINを指定することによって、 標準入力(STDIN)からバウンスメールのデータを読み込むことができます。 この機能は初期のSisimaiから実装されていましたが、バグがあったため、 読み込みができない場合がありました。このバグはSisimai 4.18.1 (Perl版) (Ruby版) にて修正済みです。
Sisimaiクラスのdump()と
rise()メソッドは、第一引数にファイルハンドルSTDINを指定すると、
標準入力からバウンスメールのデータを読み込み、解析結果を返します。
% cat ./path/to/email | perl -MSisimai -lE 'print Sisimai->dump(STDIN)' | jq
[
{
"listid": "",
"decodedby": "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/v5"
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 5.8.0より変数から読み込むデータの最大値は2GB に制限されるようになりました。また、読み込んだデータのうちメール一通あたりは 64MBまでに制限されます。
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が呼び出され、
メールのファイルや標準入力から読み込んだのと同様に、解析結果が得られます。
Sisimai 5.4.0からRise()関数はポインターではなく スライスを返すようになりました。
package main
import "fmt"
import "libsisimai.org/sisimai/v5"
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()の第二引数に
指定する
siba.DecodingArgs
構造体のDelivered
にtrueを指定すると、配信に成功したメール(配信通知)も
解析結果に含まれるようになります。Deliveredの初期値は
falseです。
Sisimai 5.4.0からRise()関数はポインターではなく スライスを返すようになりました。
package main
import "fmt"
import "libsisimai.org/sisimai/v5"
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()関数の第二引数に指定する構造体
siba.DecodingArgs
の
Vacationにtrueを指定すると、
不在応答のメールも解析結果に含まれるようになります。
Vacationの初期値はfalseです。
Sisimai 5.4.0からRise()関数はポインターではなく スライスを返すようになりました。
package main
import "fmt"
import "libsisimai.org/sisimai/v5"
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()
関数における
siba.DecodingArgs
構造体のCallback0と
Callback1の引数と戻り値はそれぞれ以下のような構造になっています。
| siba.DecodingArgs | 引数 | 戻り値 |
|---|---|---|
| Callback0 |
*siba.CallbackArg0
|
|
| Callback1 |
*siba.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()関数に渡す
siba.DecodingArgs
構造体の
Callback0とCallback1に
実行したい処理内容を書いた関数を代入して渡すことができます。
Sisimai 5.4.0からRise()関数はポインターではなく スライスを返すようになりました。
% 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/v5"
func main() {
args := sisimai.Args() // siba.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, nyaan := sisimai.Rise("STDIN", args)
if len(sisi) > 0 {
list = append(list, "Queue-ID")
for _, e := range sisi {
// e is a siba.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)
}
}
}
}
if len(nyaan) > 0 { fmt.Fprintf(os.Stderr, "%v\n", nyaan) }
}
Sisimaiクラスのdump() メソッドにフックメソッドを渡して得たJSON文字列は次のような内容になります。
{
"addresser": "michitsuna@example.jp",
"recipient": "kijitora@google.example.com",
"timestamp": 1650119685,
"action": "failed",
"alias": "nekochan@example.co.jp",
"catch": {
"nekochan": [
"Lui",
"Michitsuna"
],
"return-path": "",
"x-mailer": ""
},
"command": "DATA",
"decodedby": "Postfix",
"deliverystatus": "5.7.26",
"destination": "google.example.com",
"diagnosticcode": "host gmail-smtp-in.l.google.com[64.233.187.27] said: This mail has been blocked because the sender is unauthenticated. Gmail requires all senders to authenticate with either SPF or DKIM. Authentication results: DKIM = did not pass SPF [relay3.example.com] with ip: [192.0.2.22] = did not pass For instructions on setting up authentication, go to https://support.google.com/mail/answer/81126#authentication c2-202200202020202020222222cat.127 - gsmtp (in reply to end of DATA command)",
"diagnostictype": "SMTP",
"feedbackid": "",
"feedbacktype": "",
"hardbounce": false,
"lhost": "relay3.example.com",
"listid": "",
"messageid": "hwk7pzjzjtz0rf9y@relay3.example.com",
"origin": "./gmail-5.7.26.eml",
"reason": "authfailure",
"rhost": "gmail-smtp-in.l.google.com",
"replycode": "550",
"senderdomain": "example.jp",
"subject": "nyaan",
"timezoneoffset": "+0900",
"token": "84656774898baa90660be3e12fe0526e108d4473",
"toxic": false
}