package Sisimai; use feature ':5.10'; use strict; use warnings; use version; our $VERSION = version->declare('v4.24.1'); our $PATCHLV = 0; sub version { return $VERSION.($PATCHLV > 0 ? 'p'.$PATCHLV : '') } sub sysname { 'bouncehammer' } sub libname { 'Sisimai' } sub make { # Wrapper method for parsing mailbox or Maildir/ # @param [String] argv0 Path to mbox or Maildir/ # @param [Hash] argv0 or Hash (decoded JSON) # @param [Handle] argv0 or STDIN # @param [Hash] argv1 Parser options # @options argv1 [Integer] delivered 1 = Including "delivered" reason # @options argv1 [Code] hook Code reference to a callback method # @options argv1 [String] input Input data format: 'email', 'json' # @options argv1 [Array] field Email header names to be captured # @return [Array] Parsed objects # @return [Undef] Undef if the argument was wrong or an empty array my $class = shift; my $argv0 = shift // return undef; die ' ***error: wrong number of arguments' if scalar @_ % 2; my $argv1 = { @_ }; my $input = $argv1->{'input'} || undef; my $field = $argv1->{'field'} || []; die ' ***error: "field" accepts an array reference only' if ref $field ne 'ARRAY'; unless( $input ) { # "input" did not specified, try to detect automatically. my $rtype = ref $argv0; if( ! $rtype || $rtype eq 'SCALAR' ) { # The argument may be a path to email OR a scalar reference to an # email text $input = 'email'; } elsif( $rtype eq 'ARRAY' || $rtype eq 'HASH' ) { # The argument may be a decoded JSON object $input = 'json'; } } my $methodargv = {}; my $delivered1 = { 'delivered' => $argv1->{'delivered'} // 0 }; my $hookmethod = $argv1->{'hook'} || undef; my $bouncedata = []; require Sisimai::Data; require Sisimai::Message; if( $input eq 'email' ) { # Path to mailbox or Maildir/, or STDIN: 'input' => 'email' require Sisimai::Mail; my $mail = Sisimai::Mail->new($argv0); return undef unless $mail; while( my $r = $mail->read ) { # Read and parse each mail file $methodargv = { 'data' => $r, 'hook' => $hookmethod, 'input' => 'email', 'field' => $field, }; my $mesg = Sisimai::Message->new(%$methodargv); next unless defined $mesg; my $data = Sisimai::Data->make('data' => $mesg, %$delivered1); push @$bouncedata, @$data if scalar @$data; } } elsif( $input eq 'json' ) { # Decoded JSON object: 'input' => 'json' my $type = ref $argv0; my $list = []; if( $type eq 'ARRAY' ) { # [ {...}, {...}, ... ] for my $e ( @$argv0 ) { next unless ref $e eq 'HASH'; push @$list, $e; } } else { push @$list, $argv0; } while( my $e = shift @$list ) { $methodargv = { 'data' => $e, 'hook' => $hookmethod, 'input' => 'json' }; my $mesg = Sisimai::Message->new(%$methodargv); next unless defined $mesg; my $data = Sisimai::Data->make('data' => $mesg, %$delivered1); push @$bouncedata, @$data if scalar @$data; } } else { # The value of "input" neither "email" nor "json" die ' ***error: invalid value of "input"'; } return undef unless scalar @$bouncedata; return $bouncedata; } sub dump { # Wrapper method to parse mailbox/Maildir and dump as JSON # @param [String] argv0 Path to mbox or Maildir/ # @param [Hash] argv0 or Hash (decoded JSON) # @param [Handle] argv0 or STDIN # @param [Hash] argv1 Parser options # @options argv1 [Integer] delivered 1 = Including "delivered" reason # @options argv1 [Code] hook Code reference to a callback method # @options argv1 [String] input Input data format: 'email', 'json' # @return [String] Parsed data as JSON text my $class = shift; my $argv0 = shift // return undef; die ' ***error: wrong number of arguments' if scalar @_ % 2; my $argv1 = { @_ }; my $nyaan = __PACKAGE__->make($argv0, %$argv1) // []; # Dump as JSON require Module::Load; Module::Load::load('JSON', '-convert_blessed_universally'); my $jsonparser = JSON->new->allow_blessed->convert_blessed; my $jsonstring = $jsonparser->encode($nyaan); utf8::encode $jsonstring if utf8::is_utf8 $jsonstring; return $jsonstring; } sub engine { # Parser engine list (MTA modules) # @return [Hash] Parser engine table my $class = shift; my $names = [qw|Bite::Email Bite::JSON ARF RFC3464 RFC3834|]; my $table = {}; my $loads = ''; while( my $e = shift @$names ) { my $r = 'Sisimai::'.$e; ($loads = $r) =~ s|::|/|g; require $loads.'.pm'; if( $e eq 'Bite::Email' || $e eq 'Bite::JSON' ) { # Sisimai::Bite::Email or Sisimai::Bite::JSON for my $ee ( @{ $r->index } ) { # Load and get the value of "description" from each module my $rr = 'Sisimai::'.$e.'::'.$ee; ($loads = $rr) =~ s|::|/|g; require $loads.'.pm'; $table->{ $rr } = $rr->description; } } else { # Sisimai::ARF, Sisimai::RFC3464, and Sisimai::RFC3834 $table->{ $r } = $r->description; } } return $table; } sub reason { # Reason list Sisimai can detect # @return [Hash] Reason list table my $class = shift; my $table = {}; require Sisimai::Reason; my $names = Sisimai::Reason->index; my $loads = ''; # These reasons are not included in the results of Sisimai::Reason->index push @$names, (qw|Delivered Feedback Undefined Vacation|); while( my $e = shift @$names ) { # Call ->description() method of Sisimai::Reason::* my $r = 'Sisimai::Reason::'.$e; ($loads = $r) =~ s|::|/|g; require $loads.'.pm'; $table->{ $e } = $r->description; } return $table; } sub match { # Try to match with message patterns # @param [String] Error message text # @return [String] Reason text my $class = shift; my $argvs = shift || return undef; require Sisimai::Reason; return Sisimai::Reason->match(lc $argvs); } 1; __END__ =encoding utf-8 =head1 NAME Sisimai - Mail Analyzing Interface for bounce mails. =head1 SYNOPSIS use Sisimai; =head1 DESCRIPTION C is a Mail Analyzing Interface for email bounce, is a Perl module to parse RFC5322 bounce mails and generating structured data as JSON from parsed results. C is a coined word: Sisi (the number 4 is pronounced "Si" in Japanese) and MAI (acronym of "Mail Analyzing Interface"). =head1 BASIC USAGE =head2 C)>> C method provides feature for getting parsed data from bounced email messages like following. use Sisimai; my $v = Sisimai->make('/path/to/mbox'); # or Path to Maildir/ # $v = Sisimai->make(\'From Mailer-Daemon ...'); if( defined $v ) { for my $e ( @$v ) { print ref $e; # Sisimai::Data print ref $e->recipient; # Sisimai::Address print ref $e->timestamp; # Sisimai::Time print $e->addresser->address; # shironeko@example.org # From print $e->recipient->address; # kijitora@example.jp # To print $e->recipient->host; # example.jp print $e->deliverystatus; # 5.1.1 print $e->replycode; # 550 print $e->reason; # userunknown my $h = $e->damn; # Convert to HASH reference my $j = $e->dump('json'); # Convert to JSON string my $y = $e->dump('yaml'); # Convert to YAML string } # Dump entire list as a JSON use JSON '-convert_blessed_universally'; my $json = JSON->new->allow_blessed->convert_blessed; printf "%s\n", $json->encode($v); } If you want to get bounce records which reason is "delivered", set "delivered" option to make() method like the following: my $v = Sisimai->make('/path/to/mbox', 'delivered' => 1); =head2 C)>> C method provides feature to get parsed data from bounced email as JSON. use Sisimai; my $v = Sisimai->dump('/path/to/mbox'); # or Path to Maildir print $v; # JSON string =head1 OTHER WAYS TO PARSE =head2 Read email data from STDIN If you want to pass email data from STDIN, specify B at the first argument of dump() and make() method like following command: % cat ./path/to/bounce.eml | perl -MSisimai -lE 'print Sisimai->dump(STDIN)' =head2 Callback Feature Beginning from v4.19.0, `hook` argument is available to callback user defined method like the following codes: my $cmethod = sub { my $argv = shift; my $data = { 'queue-id' => '', 'x-mailer' => '', 'precedence' => '', }; # Header part of the bounced mail for my $e ( 'x-mailer', 'precedence' ) { next unless exists $argv->{'headers'}->{ $e }; $data->{ $e } = $argv->{'headers'}->{ $e }; } # Message body of the bounced email if( $argv->{'message'} =~ /^X-Postfix-Queue-ID:\s*(.+)$/m ) { $data->{'queue-id'} = $1; } return $data; }; my $message = Sisimai::Message->new( 'data' => $mailtxt, 'hook' => $cmethod, 'field' => ['X-Mailer', 'Precedence'] ); print $message->catch->{'x-mailer'}; # Apple Mail (2.1283) print $message->catch->{'queue-id'}; # 2DAEB222022E print $message->catch->{'precedence'}; # bulk =head1 OTHER METHODS =head2 C> C method provides table including parser engine list and it's description. use Sisimai; my $v = Sisimai->engine(); for my $e ( keys %$v ) { print $e; # Sisimai::MTA::Sendmail print $v->{ $e }; # V8Sendmail: /usr/sbin/sendmail } =head2 C> C method provides table including all the reasons Sisimai can detect use Sisimai; my $v = Sisimai->reason(); for my $e ( keys %$v ) { print $e; # Blocked print $v->{ $e }; # 'Email rejected due to client IP address or a hostname' } =head1 SEE ALSO =over =item L - Mailbox or Maildir object =item L - Parsed data object =item L - Sisimai — A successor to bounceHammer, Library to parse error mails =item L - RFC3463: Enhanced Mail System Status Codes =item L - RFC3464: An Extensible Message Format for Delivery Status Notifications =item L - RFC5321: Simple Mail Transfer Protocol =item L - RFC5322: Internet Message Format =back =head1 REPOSITORY L - Sisimai on GitHub =head1 WEB SITE L - A successor to bounceHammer, Library to parse error mails. L - Ruby version of Sisimai =head1 AUTHOR azumakuniyuki =head1 COPYRIGHT Copyright (C) 2014-2018 azumakuniyuki, All rights reserved. =head1 LICENSE This software is distributed under The BSD 2-Clause License. =cut