diff --git a/lib/Sisimai/ARF.pm b/lib/Sisimai/ARF.pm index 1c4ae6f7e..61a357919 100644 --- a/lib/Sisimai/ARF.pm +++ b/lib/Sisimai/ARF.pm @@ -13,26 +13,30 @@ sub is_arf { # 0: is not Feedback loop my $class = shift; my $heads = shift || return 0; - my $match = 0; + my $abuse = ['staff@hotmail.com', 'complaints@email-abuse.amazonses.com']; + my $ctype = $heads->{"content-type"}; # Content-Type: multipart/report; report-type=feedback-report; ... - return 1 if Sisimai::String->aligned(\$heads->{'content-type'}, ['report-type=', 'feedback-report']); + return 1 if Sisimai::String->aligned(\$ctype, ["report-type=", "feedback-report"]); - if( index($heads->{'content-type'}, 'multipart/mixed') > -1 ) { + if( index($ctype, "multipart/mixed") > -1 ) { # Microsoft (Hotmail, MSN, Live, Outlook) uses its own report format. # Amazon SES Complaints bounces - if( index($heads->{'subject'}, 'complaint about message from ') > -1 ) { + if( index($heads->{"subject"}, "complaint about message from ") > -1 ) { # From: staff@hotmail.com # From: complaints@email-abuse.amazonses.com # Subject: complaint about message from 192.0.2.1 - my $rf = ['staff@hotmail.com', 'complaints@email-abuse.amazonses.com']; - my $cv = Sisimai::Address->s3s4($heads->{'from'}); - $match = 1 if grep { index($cv, $_) > -1 } @$rf; + return 1 if grep { index($heads->{"from"}, $_) > -1 } @$abuse; } } - $match = 1 if ($heads->{'x-apple-unsubscribe'} // '') eq 'true'; # X-Apple-Unsubscribe: true - return $match; + APPLE: while(1) { + # X-Apple-Unsubscribe: true + last unless exists $heads->{"x-apple-unsubscribe"}; + return 1 if $heads->{"x-apple-unsubscribe"} eq "true"; + last APPLE; + } + return 0; } sub inquire { @@ -53,40 +57,32 @@ sub inquire { # Netease DMARC uses: This is a spf/dkim authentication-failure report for an email message received from IP # OpenDMARC 1.3.0 uses: This is an authentication failure report for an email message received from IP # Abusix ARF uses: this is an autogenerated email abuse complaint regarding your network. - state $startingof = { - 'rfc822' => ['Content-Type: message/rfc822', 'Content-Type: text/rfc822-headers'], - 'report' => ['Content-Type: message/feedback-report'], - 'message' => [ - ['this is a', 'abuse report'], - ['this is a', 'authentication', 'failure report'], - ['this is a', ' report for'], - ['this is an authentication', 'failure report'], - ['this is an autogenerated email abuse complaint'], - ['this is an email abuse report'], - ], - }; state $indicators = Sisimai::Lhost->INDICATORS; - state $longfields = Sisimai::RFC5322->LONGFIELDS; + state $reportfrom = "Content-Type: message/feedback-report"; + state $boundaries = [ + "Content-Type: message/rfc822", + "Content-Type: text/rfc822-headers", + "Content-Type: text/rfc822-header", # ?? + ]; + state $arfpreface = [ + ["this is a", "abuse report"], + ["this is a", "authentication", "failure report"], + ["this is a", " report for"], + ["this is an authentication", "failure report"], + ["this is an autogenerated email abuse complaint"], + ["this is an email abuse report"], + ]; my $dscontents = [Sisimai::Lhost->DELIVERYSTATUS]; - my $rfc822part = ''; # (String) message/rfc822-headers part - my $previousfn = ''; # (String) Previous field name - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $rcptintext = ''; # (String) Recipient address in the message body - my $commondata = { - 'diagnosis' => '', # Error message - 'from' => '', # Original-Mail-From: - 'rhost' => '', # Reporting-MTA: - }; - my $arfheaders = { - 'feedbacktype' => '', # Feedback-Type: - 'rhost' => '', # Source-IP: - 'agent' => '', # User-Agent: - 'date' => '', # Arrival-Date: - 'authres' => '', # Authentication-Results: - }; - my $v = undef; + my $reportpart = 0; + my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); + my $readcursor = 0; # Points the current cursor position + my $recipients = 0; # The number of "Final-Recipient" header + my $timestamp0 = ""; # The value of "Arrival-Date" or "Received-Date" + my $remotehost = ""; # The value of "Source-IP" field + my $reportedby = ""; # The value of "Reporting-MTA" field + my $anotherone = ""; # Other fields(append to Diagnosis) + my $v = $dscontents->[-1]; # 3.1. Required Fields # @@ -108,74 +104,30 @@ sub inquire { # generator is using to generate the report. The version number in # this specification is set to "1". # - for my $e ( split("\n", $$mbody) ) { - # Read each line between the start of the message and the start of rfc822 part. - - # This is an email abuse report for an email message with the - # message-id of 0000-000000000000000000000000000000000@mx - # received from IP address 192.0.2.1 on - # Thu, 29 Apr 2010 00:00:00 +0900 (JST) - my $p = lc $e; - $commondata->{'diagnosis'} ||= $e if grep { Sisimai::String->aligned(\$p, $_) } $startingof->{'message'}->@*; - + for my $e ( split("\n", $emailparts->[0]) ) { + # Read error messages and delivery status lines from the head of the email to the + # previous line of the beginning of the original message. unless( $readcursor ) { # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'report'}->[0]) == 0; - } - - unless( $readcursor & $indicators->{'message-rfc822'} ) { - # Beginning of the original message part - if( index($e, $startingof->{'rfc822'}->[0]) == 0 || - index($e, $startingof->{'rfc822'}->[1]) == 0 ) { - $readcursor |= $indicators->{'message-rfc822'}; - next; + my $r = lc $e; + for my $f ( @$arfpreface ) { + # Hello, + # this is an autogenerated email abuse complaint regarding your network. + next unless Sisimai::String->aligned(\$r, $f); + $readcursor |= $indicators->{'deliverystatus'}; + $v->{"diagnosis"} .= " ".$e; + last; } + next; } + next unless $readcursor & $indicators->{'deliverystatus'}; + next unless length $e; + if( $e eq $reportfrom ) { $reportpart = 1; next } - if( $readcursor & $indicators->{'message-rfc822'} ) { - # message/rfc822 OR text/rfc822-headers part - if( index($e, 'X-HmXmrOriginalRecipient:') == 0 ) { - # Microsoft ARF: original recipient. - $dscontents->[-1]->{'recipient'} = Sisimai::Address->s3s4(substr($e, index($e, ':') + 1,)); - $recipients++; - - # The "X-HmXmrOriginalRecipient" header appears only once so we take this opportunity - # to hard-code ARF headers missing in Microsoft's implementation. - $arfheaders->{'feedbacktype'} = 'abuse'; - $arfheaders->{'agent'} = 'Microsoft Junk Mail Reporting Program'; - - } elsif( index($e, 'From: ') == 0 ) { - # Microsoft ARF: original sender. - $commondata->{'from'} ||= Sisimai::Address->s3s4(substr($e, 6,)); - $previousfn = 'from'; - - } elsif( index($e, ' ') == 0 ) { - # Continued line from the previous line - if( $previousfn eq 'from' ) { - # Multiple lines at From: field - $commondata->{'from'} .= $e; - next; - - } else { - $rfc822part .= $e."\n" if exists $longfields->{ $previousfn }; - next if length $e; - } - $rcptintext .= $e if $previousfn eq 'to'; - - } else { - # Get required headers only - my($lhs, $rhs) = split(/:[ ]/, $e, 2); - next unless $lhs = lc($lhs || ''); - - $previousfn = $lhs; - $rfc822part .= $e."\n"; - $rcptintext = $rhs if $lhs eq 'to'; - } - } else { - # message/feedback-report part - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - + if( $reportpart ) { + # Content-Type: message/feedback-report + # MIME-Version: 1.0 + # # Feedback-Type: abuse # User-Agent: SomeGenerator/1.0 # Version: 0.1 @@ -183,154 +135,113 @@ sub inquire { # Original-Rcpt-To: # Received-Date: Thu, 29 Apr 2009 00:00:00 JST # Source-IP: 192.0.2.1 - $v = $dscontents->[-1]; - - if( index($e, 'Original-Rcpt-To: ') == 0 || index($e, 'Redacted-Address: ') == 0 ) { - # Original-Rcpt-To header field is optional and may appear any - # number of times as appropriate: - # Original-Rcpt-To: - # Redacted-Address: localpart@ - if( $v->{'recipient'} ) { + if( index($e, "Original-Rcpt-To: ") == 0 || index($e, "Removal-Recipient: ") == 0 ) { + # Original-Rcpt-To header field is optional and may appear any number of times as appropriate: + # Original-Rcpt-To: + # Removal-Recipient: user@example.com + my $cv = Sisimai::Address->s3s4(substr($e, index($e, " ") + 1,)); next unless Sisimai::Address->is_emailaddress($cv); + my $cw = scalar @$dscontents; next if $cw > 0 && $cv eq $dscontents->[$cw - 1]->{"recipient"}; + + if( $v->{"recipient"} ) { # There are multiple recipient addresses in the message body. push @$dscontents, Sisimai::Lhost->DELIVERYSTATUS; $v = $dscontents->[-1]; } - $v->{'recipient'} = Sisimai::Address->s3s4(substr($e, index($e, ' ') + 1,)); + $v->{"recipient"} = Sisimai::Address->s3s4(substr($e, index($e, " ") +1,)); $recipients++; - } elsif( index($e, 'Feedback-Type: ') == 0 ) { + } elsif( index($e, "Feedback-Type: ") == 0 ) { # The header field MUST appear exactly once. # Feedback-Type: abuse - $arfheaders->{'feedbacktype'} = substr($e, index($e, ' ') + 1,); + $v->{"feedbacktype"} = substr($e, index($e, " ") + 1,); - } elsif( index($e, 'Authentication-Results: ') == 0 ) { + } elsif( index($e, "Authentication-Results: ") == 0 ) { # "Authentication-Results" indicates the result of one or more authentication checks # run by the report generator. # # Authentication-Results: mail.example.com; # spf=fail smtp.mail=somespammer@example.com - $arfheaders->{'authres'} = substr($e, index($e, ' ') + 1,); + $anotherone .= $e.", "; - } elsif( index($e, 'User-Agent: ') == 0 ) { + } elsif( index($e, "User-Agent: ") == 0 ) { # The header field MUST appear exactly once. # User-Agent: SomeGenerator/1.0 - $arfheaders->{'agent'} = substr($e, index($e, ' ') + 1,); + $anotherone .= $e.", "; - } elsif( index($e, 'Received-Date: ') == 0 || index($e, 'Arrival-Date: ') == 0 ) { + } elsif( index($e, "Received-Date: ") == 0 || index($e, "Arrival-Date: ") == 0 ) { # Arrival-Date header is optional and MUST NOT appear more than once. # Received-Date: Thu, 29 Apr 2010 00:00:00 JST # Arrival-Date: Thu, 29 Apr 2010 00:00:00 +0000 - $arfheaders->{'date'} = substr($e, index($e, ' ') + 1,); + $timestamp0 = substr($e, index($e, " ") + 1,); - } elsif( index($e, 'Reporting-MTA: dns; ') == 0 ) { + } elsif( index($e, "Reporting-MTA: ") == 0 ) { # The header is optional and MUST NOT appear more than once. # Reporting-MTA: dns; mx.example.jp - $commondata->{'rhost'} = substr($e, index($e, ';') + 2,); + my $cv = Sisimai::RFC1894->field($e); next if scalar(@$cv) == 0; + $reportedby = $cv->[2]; - } elsif( index($e, 'Source-IP: ') == 0 ) { + } elsif( index($e, "Source-IP: ") == 0 ) { # The header is optional and MUST NOT appear more than once. # Source-IP: 192.0.2.45 - $arfheaders->{'rhost'} = substr($e, index($e, ' ') + 1,); + $remotehost = substr($e, index($e, ' ') + 1,); - } elsif( index($e, 'Original-Mail-From: ') == 0 ) { + } elsif( index($e, "Original-Mail-From: ") == 0 ) { # the header is optional and MUST NOT appear more than once. # Original-Mail-From: - $commondata->{'from'} ||= Sisimai::Address->s3s4(substr($e, index($e, ' ') + 1,)); + $anotherone .= $e.", "; } - } # End of if: rfc822 - } - - if( ($arfheaders->{'feedbacktype'} eq 'auth-failure' ) && $arfheaders->{'authres'} ) { - # Append the value of Authentication-Results header - $commondata->{'diagnosis'} .= ' '.$arfheaders->{'authres'} - } - - unless( $recipients ) { - # The original recipient address was not found - if( Sisimai::String->aligned(\$rfc822part, ["\nTo: ", '@']) ) { - # pick the address from To: header in message/rfc822 part. - my $p1 = index($rfc822part, "\nTo: ") + 5; - my $p2 = index($rfc822part, "\n", $p1 + 1); - my $cm = $p2 > 0 ? $p2 - $p1 : 255; - $dscontents->[-1]->{'recipient'} = Sisimai::Address->s3s4(substr($rfc822part, $p1, $cm)); - $recipients = 1; - } - - while(1) { - # Insert pseudo recipient address when there is no valid recipient address in the message - # for example, - # Date: Thu, 29 Apr 2015 23:34:45 +0000 - # To: "undisclosed" - # Subject: Nyaan - # Message-ID: - last if index($dscontents->[-1]->{'recipient'}, '@') > 0; - $dscontents->[-1]->{'recipient'} = Sisimai::Address->undisclosed(1); - $recipients = 1; - last; + } else { + # Messages before "Content-Type: message/feedback-report" part + $v->{"diagnosis"} .= " ".$e; } } - unless( Sisimai::String->aligned(\$rfc822part, ['From: ', '@']) ) { - # There is no "From:" header in the original message Append the value of "Original-Mail-From" - # value as a sender address. - $rfc822part .= 'From: '.$commondata->{'from'}."\n" if $commondata->{'from'}; - } - - if( index($mhead->{'subject'}, 'complaint about message from ') > -1 ) { - # Microsoft ARF: remote host address. - $arfheaders->{'rhost'} = substr($mhead->{'subject'}, rindex($mhead->{'subject'}, ' ') + 1,); - $commondata->{'diagnosis'} = sprintf( - "This is a Microsoft email abuse report for an email message received from IP %s on %s", - $arfheaders->{'rhost'}, $mhead->{'date'}); - - } elsif( index($mhead->{'subject'}, 'unsubscribe') > -1 ) { - # Apple Mail sent this email to unsubscribe from the message - while(1) { - # Subject: unsubscribe - # Content-Type: text/plain; charset=UTF-8 - # Auto-Submitted: auto-replied + while( $recipients == 0 ) { + # There is no recipient address in the message + if( exists $mhead->{"x-apple-unsubscribe"} ) { # X-Apple-Unsubscribe: true - # - # Apple Mail sent this email to unsubscribe from the message - last unless $mhead->{'x-apple-unsubscribe'}; - last unless $mhead->{'x-apple-unsubscribe'} eq 'true'; - last unless index($$mbody, 'Apple Mail sent this email to unsubscribe from the message') > -1; - - $dscontents->[-1]->{'recipient'} = Sisimai::Address->s3s4($mhead->{'from'}); - $dscontents->[-1]->{'feedbacktype'} = 'opt-out'; - last; - } - } + last unless $mhead->{"x-apple-unsubscribe"} eq "true"; + last unless index($mhead->{"from"}, "@") > 1; + $dscontents->[0]->{"recipient"} = $mhead->{"from"}; + $dscontents->[0]->{"diagnosis"} = Sisimai::String->sweep($emailparts->[0]); + $dscontents->[0]->{"feedbacktype"} = "opt-out"; - for my $e ( @$dscontents ) { - # AOL = http://forums.cpanel.net/f43/aol-brutal-work-71473.html - $e->{'recipient'} = Sisimai::Address->s3s4($rcptintext) if substr($e->{'recipient'}, -1, 1) eq '@'; - $e->{ $_ } ||= $arfheaders->{ $_ } for keys %$arfheaders; - delete $e->{'authres'}; - - $e->{'diagnosis'} ||= $commondata->{'diagnosis'}; - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); - $e->{'date'} ||= $mhead->{'date'}; - $e->{'reason'} = 'feedback'; - $e->{'command'} = ''; - $e->{'action'} = ''; - $e->{'agent'} = 'Feedback-Loop'; - - # Get the remote IP address from the message body - next if $e->{'rhost'}; - if( $commondata->{'rhost'} ) { - # The value of "Reporting-MTA" header - $e->{'rhost'} = $commondata->{'rhost'}; + # Addpend To: field as a pseudo header + $emailparts->[1] = sprintf("To: <%s>\n", $mhead->{"from"}) if $emailparts->[1] eq ""; } else { - # Try to get an IP address from the error message - # This is an email abuse report for an email message received from IP address 24.64.1.1 - # on Thu, 29 Apr 2010 00:00:00 +0000 - my $ip = Sisimai::String->ipv4($e->{'diagnosis'}) || []; - $e->{'rhost'} = $ip->[0] if scalar @$ip; + # Pick it from the original message part + my $p1 = index($emailparts->[1], "\nTo:"); last if $p1 < 0; + my $p2 = index($emailparts->[1], "\n", $p1 + 4); last if $p2 < 0; + my $cv = Sisimai::Address->s3s4(substr($emailparts->[1], $p1 + 4, $p2 - $p1)); + + # There is no valid email address in the To: header of the original message such as + # To: + $cv = Sisimai::Address->undisclosed("r") unless Sisimai::Address->is_emailaddress($cv); + $dscontents->[0]->{"recipient"} = $cv; } + $recipients++; + } + return undef if $recipients == 0; + + $anotherone = ": ".Sisimai::String->sweep($anotherone) if $anotherone ne ""; + substr($anotherone, -1, 1, "") if substr($anotherone, -1, 1) eq ","; + + my $j = -1; for my $e ( @$dscontents ) { + # Tidy up the error message in e.Diagnosis, Try to detect the bounce reason. + $j++; + $e->{"diagnosis"} = Sisimai::String->sweep($e->{"diagnosis"}.$anotherone); + $e->{"reason"} = "feedback"; + $e->{"rhost"} = $remotehost; + $e->{"lhost"} = $reportedby; + $e->{"date"} = $timestamp0; + + # Copy some values from the previous element when the report have 2 or more email address + next if $j == 0 || scalar(@$dscontents) == 1; + $e->{"diagnosis"} = $dscontents->[$j - 1]->{"diagnosis"}; + $e->{"feedbacktype"} = $dscontents->[$j - 1]->{"feedbacktype"}; } - return { 'ds' => $dscontents, 'rfc822' => $rfc822part }; + return { "ds" => $dscontents, "rfc822" => $emailparts->[1] }; } 1; diff --git a/lib/Sisimai/Fact.pm b/lib/Sisimai/Fact.pm index 0b0dd3e5c..0f943e589 100644 --- a/lib/Sisimai/Fact.pm +++ b/lib/Sisimai/Fact.pm @@ -14,6 +14,7 @@ use Sisimai::SMTP::Command; use Sisimai::SMTP::Failure; use Sisimai::String; use Sisimai::Rhost; +use Sisimai::LDA; use Class::Accessor::Lite ('new' => 0, 'rw' => [ 'action', # [String] The value of Action: header 'addresser', # [Sisimai::Address] From address @@ -156,11 +157,19 @@ sub rise { my $recv = $mesg1->{'header'}->{'received'} || []; unless( $piece->{'rhost'} ) { # Try to pick a remote hostname from Received: headers of the bounce message - for my $re ( reverse @$recv ) { - # Check the Received: headers backwards and get a remote hostname - my $cv = Sisimai::RFC5322->received($re)->[0]; - next unless Sisimai::RFC1123->is_validhostname($cv); - $piece->{'rhost'} = $cv; last; + my $ir = Sisimai::RFC1123->find($e->{'diagnosis'}); + $piece->{'rhost'} = $ir if Sisimai::RFC1123->is_internethost($ir); + + unless( $piece->{'rhost'} ) { + # The remote hostname in the error message did not exist or is not a valid + # internet hostname + for my $re ( reverse @$recv ) { + # Check the Received: headers backwards and get a remote hostname + last if $piece->{'rhost'}; + my $cv = Sisimai::RFC5322->received($re)->[0]; + next unless Sisimai::RFC1123->is_internethost($cv); + $piece->{'rhost'} = $cv; + } } } $piece->{'lhost'} = '' if $piece->{'lhost'} eq $piece->{'rhost'}; @@ -170,7 +179,7 @@ sub rise { for my $le ( @$recv ) { # Check the Received: headers forwards and get a local hostname my $cv = Sisimai::RFC5322->received($le)->[0]; - next unless Sisimai::RFC1123->is_validhostname($cv); + next unless Sisimai::RFC1123->is_internethost($cv); $piece->{'lhost'} = $cv; last; } } @@ -308,7 +317,7 @@ sub rise { my $ar = Sisimai::Address->new({'address' => $piece->{'recipient'}}) || next RISEOF; my @ea = (qw| action deliverystatus diagnosticcode diagnostictype feedbacktype lhost listid - messageid origin reason replycode rhost smtpagent smtpcommand subject + messageid origin reason replycode rhost smtpagent smtpcommand subject |); $thing = { @@ -356,7 +365,11 @@ sub rise { if( $thing->{'reason'} eq '' || exists $retryindex->{ $thing->{'reason'} } ) { # The value of "reason" is empty or is needed to check with other values again my $re = $thing->{'reason'} || 'undefined'; - $thing->{'reason'} = Sisimai::Rhost->find($thing) || Sisimai::Reason->find($thing) || $re; + my $cr = "Sisimai::Reason"; + my $or = Sisimai::LDA->find($thing); if( $cr->is_explicit($or) ){ $thing->{'reason'} = $or; last } + $or = Sisimai::Rhost->find($thing); if( $cr->is_explicit($or) ){ $thing->{'reason'} = $or; last } + $or = Sisimai::Reason->find($thing); if( $cr->is_explicit($or) ){ $thing->{'reason'} = $or; last } + $thing->{'reason'} = $thing->{'diagnosticcode'} ? "onhold" : $re; } } @@ -406,8 +419,10 @@ sub rise { $thing->{'action'} = $ox->[2]; } } - $thing->{'action'} = 'delayed' if $thing->{'reason'} eq 'expired'; - $thing->{'action'} ||= 'failed' if $cx->[0] eq '4' || $cx->[0] eq '5'; + $thing->{'action'} = 'delivered' if $thing->{'reason'} eq 'delivered'; + $thing->{'action'} ||= 'delayed' if $thing->{'reason'} eq 'expired'; + $thing->{'action'} ||= 'failed' if $cx->[0] eq '4' || $cx->[0] eq '5'; + $thing->{'action'} ||= ""; } push @$listoffact, bless($thing, __PACKAGE__); diff --git a/lib/Sisimai/LDA.pm b/lib/Sisimai/LDA.pm new file mode 100644 index 000000000..5e0328e5a --- /dev/null +++ b/lib/Sisimai/LDA.pm @@ -0,0 +1,135 @@ +package Sisimai::LDA; +use v5.26; +use strict; +use warnings; + +state $LocalAgent = { + # Each error message should be a lower-cased string + # dovecot/src/deliver/deliver.c + # 11: #define DEFAULT_MAIL_REJECTION_HUMAN_REASON \ + # 12: "Your message to <%t> was automatically rejected:%n%r" + "dovecot" => ["Your message to ", " was automatically rejected:"], + "mail.local" => ["mail.local: "], + "procmail" => ["procmail: ", "/procmail "], + "maildrop" => ["maildrop: "], + "vpopmail" => ["vdelivermail: "], + "vmailmgr" => ["vdeliver: "], +}; + +state $MessagesOf = { + # Each error message should be a lower-cased string + "dovecot" => { + # dovecot/src/deliver/mail-send.c:94 + "mailboxfull" => [ + "not enough disk space", + "quota exceeded", # Dovecot 1.2 dovecot/src/plugins/quota/quota.c + "quota exceeded (mailbox for user is full)", # dovecot/src/plugins/quota/quota.c + ], + "userunknown" => ["mailbox doesn't exist: "], + }, + "mail.local" => { + "mailboxfull" => ["disc quota exceeded", "mailbox full or quota exceeded"], + "systemerror" => ["temporary file write error"], + "userunknown" => [ + ": invalid mailbox path", + ": unknown user:", + ": user missing home directory", + ": user unknown", + ], + }, + "procmail" => { + "mailboxfull" => ["quota exceeded while writing", "user over quota"], + "systemerror" => ["service unavailable"], + "systemfull" => ["no space left to finish writing"], + }, + "maildrop" => { + "mailboxfull" => ["maildir over quota."], + "userunknown" => ["invalid user specified.", "cannot find system user"], + }, + "vpopmail" => { + "filtered" => ["user does not exist, but will deliver to "], + "mailboxfull" => ["domain is over quota", "user is over quota"], + "suspend" => ["account is locked email bounced"], + "userunknown" => ["sorry, no mailbox here by that name."], + }, + "vmailmgr" => { + "mailboxfull" => ["delivery failed due to system quota violation"], + "userunknown" => [ + "invalid or unknown base user or domain", + "invalid or unknown virtual user", + "user name does not refer to a virtual user" + ], + }, +}; + +sub find { + # Decode the message body and return a bounce reason detected by the error message of LDA + # @param [Sisimai::Fact] argvs Decoded email object + # @return [String] The value of bounce reason + my $class = shift; + my $argvs = shift // return undef; + + return "" unless length $argvs->{"diagnosticcode"}; + return "" unless $argvs->{"smtpcommand"} eq "" || $argvs->{"smtpcommand"} eq "DATA"; + + my $deliversby = ""; # [String] Local Delivery Agent name + my $reasontext = ""; # [String] Detected bounce reason + my $issuedcode = lc $argvs->{"diagnosticcode"}; + + for my $e ( keys %$LocalAgent ) { + # Find a local delivery agent name from the lower-cased error message + next unless grep { index($issuedcode, $_) > -1 } $LocalAgent->{ $e }->@*; + $deliversby = $e; last; + } + return "" unless $deliversby; + + for my $e ( keys $MessagesOf->{ $deliversby }->%* ) { + # The key nane is a bounce reason name + next unless grep { index($issuedcode, $_) > -1 } $MessagesOf->{ $deliversby }->{ $e }->@*; + $reasontext = $e; last; + } + + $reasontext ||= "mailererror"; # procmail: Couldn't create "/var/mail/tmp.nekochan.22" + return $reasontext; +} + +1; +__END__ + +=encoding utf-8 + +=head1 NAME + +Sisimai::LDA - Error message decoder for LDA; Local Delivery Agent + +=head1 SYNOPSIS + + use Sisimai::LDA; + my $fact = Sisimai::Fact->rise($v); + my $ldav = Sisimai::LDA->find($fact->[0]); # Returns the bounce reason LDA generated + +=head1 DESCRIPTION + +C decodes bounced email which created by some LDA, such as Dovecot, C, +C, and so on. This class is called from C only. + +=head1 CLASS METHODS + +=head2 C)>> + +C method detects the bounce reason using the error message generated by LDA + +=head1 AUTHOR + +azumakuniyuki + +=head1 COPYRIGHT + +Copyright (C) 2014-2016,2018-2024 azumakuniyuki, All rights reserved. + +=head1 LICENSE + +This software is distributed under The BSD 2-Clause License. + +=cut + diff --git a/lib/Sisimai/Lhost.pm b/lib/Sisimai/Lhost.pm index c3e0c4f29..2872361f2 100644 --- a/lib/Sisimai/Lhost.pm +++ b/lib/Sisimai/Lhost.pm @@ -22,7 +22,6 @@ sub DELIVERYSTATUS { 'replycode', => '', # SMTP Reply Code 'diagnosis' => '', # The value of Diagnostic-Code header 'recipient' => '', # The value of Final-Recipient header - 'hardbounce' => '', # Hard bounce or not 'feedbacktype' => '', # Feedback Type }; } @@ -41,11 +40,10 @@ sub index { # Alphabetical sorted MTA module list # @return [Array] MTA list with order return [qw| - Activehunter Amavis AmazonSES AmazonWorkMail Aol ApacheJames Barracuda Bigfoot Biglobe Courier - Domino DragonFly EZweb EinsUndEins Exchange2003 Exchange2007 Exim FML Facebook GMX GSuite GoogleGroups - Gmail IMailServer InterScanMSS KDDI MXLogic MailFoundry MailMarshalSMTP MailRu McAfee MessageLabs - MessagingServer Notes Office365 OpenSMTPD Outlook Postfix PowerMTA ReceivingSES SendGrid Sendmail - SurfControl V5sendmail Verizon X1 X2 X3 X4 X5 X6 Yahoo Yandex Zoho mFILTER qmail + Activehunter AmazonSES ApacheJames Biglobe Courier Domino DragonFly EZweb + EinsUndEins Exchange2003 Exchange2007 Exim FML GMX GoogleGroups GoogleWorkspace Gmail + IMailServer InterScanMSS KDDI MailFoundry MailMarshalSMTP MessagingServer Notes + OpenSMTPD Postfix Sendmail V5sendmail Verizon X1 X2 X3 X6 Zoho mFILTER qmail |]; } diff --git a/lib/Sisimai/Lhost/Amavis.pm b/lib/Sisimai/Lhost/Amavis.pm deleted file mode 100644 index 9fd7ca584..000000000 --- a/lib/Sisimai/Lhost/Amavis.pm +++ /dev/null @@ -1,207 +0,0 @@ -package Sisimai::Lhost::Amavis; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -# https://www.amavis.org -sub description { 'amavisd-new: https://www.amavis.org/' } -sub inquire { - # Detect an error from amavisd-new - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decode or the arguments are missing - # @since v4.25.0 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - - # From: "Content-filter at neko1.example.jp" - # Subject: Undeliverable mail, MTA-BLOCKED - return undef unless index($mhead->{'from'}, '"Content-filter at ') == 0; - - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['Content-Type: text/rfc822-headers']; - state $startingof = { 'message' => ['The message '] }; - state $messagesof = { - # amavisd-new-2.11.1/amavisd:1840|%smtp_reason_by_ccat = ( - # amavisd-new-2.11.1/amavisd:1840| # currently only used for blocked messages only, status 5xx - # amavisd-new-2.11.1/amavisd:1840| # a multiline message will produce a valid multiline SMTP response - # amavisd-new-2.11.1/amavisd:1840| CC_VIRUS, 'id=%n - INFECTED: %V', - # amavisd-new-2.11.1/amavisd:1840| CC_BANNED, 'id=%n - BANNED: %F', - # amavisd-new-2.11.1/amavisd:1840| CC_UNCHECKED.',1', 'id=%n - UNCHECKED: encrypted', - # amavisd-new-2.11.1/amavisd:1840| CC_UNCHECKED.',2', 'id=%n - UNCHECKED: over limits', - # amavisd-new-2.11.1/amavisd:1840| CC_UNCHECKED, 'id=%n - UNCHECKED', - # amavisd-new-2.11.1/amavisd:1840| CC_SPAM, 'id=%n - spam', - # amavisd-new-2.11.1/amavisd:1840| CC_SPAMMY.',1', 'id=%n - spammy (tag3)', - # amavisd-new-2.11.1/amavisd:1840| CC_SPAMMY, 'id=%n - spammy', - # amavisd-new-2.11.1/amavisd:1840| CC_BADH.',1', 'id=%n - BAD HEADER: MIME error', - # amavisd-new-2.11.1/amavisd:1840| CC_BADH.',2', 'id=%n - BAD HEADER: nonencoded 8-bit character', - # amavisd-new-2.11.1/amavisd:1840| CC_BADH.',3', 'id=%n - BAD HEADER: contains invalid control character', - # amavisd-new-2.11.1/amavisd:1840| CC_BADH.',4', 'id=%n - BAD HEADER: line made up entirely of whitespace', - # amavisd-new-2.11.1/amavisd:1840| CC_BADH.',5', 'id=%n - BAD HEADER: line longer than RFC 5322 limit', - # amavisd-new-2.11.1/amavisd:1840| CC_BADH.',6', 'id=%n - BAD HEADER: syntax error', - # amavisd-new-2.11.1/amavisd:1840| CC_BADH.',7', 'id=%n - BAD HEADER: missing required header field', - # amavisd-new-2.11.1/amavisd:1840| CC_BADH.',8', 'id=%n - BAD HEADER: duplicate header field', - # amavisd-new-2.11.1/amavisd:1840| CC_BADH, 'id=%n - BAD HEADER', - # amavisd-new-2.11.1/amavisd:1840| CC_OVERSIZED, 'id=%n - Message size exceeds recipient\'s size limit', - # amavisd-new-2.11.1/amavisd:1840| CC_MTA.',1', 'id=%n - Temporary MTA failure on relaying', - # amavisd-new-2.11.1/amavisd:1840| CC_MTA.',2', 'id=%n - Rejected by next-hop MTA on relaying', - # amavisd-new-2.11.1/amavisd:1840| CC_MTA, 'id=%n - Unable to relay message back to MTA', - # amavisd-new-2.11.1/amavisd:1840| CC_CLEAN, 'id=%n - CLEAN', - # amavisd-new-2.11.1/amavisd:1840| CC_CATCHALL, 'id=%n - OTHER', # should not happen - # ... - # amavisd-new-2.11.1/amavisd:15289|my $status = setting_by_given_contents_category( - # amavisd-new-2.11.1/amavisd:15289| $blocking_ccat, - # amavisd-new-2.11.1/amavisd:15289| { CC_VIRUS, "554 5.7.0", - # amavisd-new-2.11.1/amavisd:15289| CC_BANNED, "554 5.7.0", - # amavisd-new-2.11.1/amavisd:15289| CC_UNCHECKED, "554 5.7.0", - # amavisd-new-2.11.1/amavisd:15289| CC_SPAM, "554 5.7.0", - # amavisd-new-2.11.1/amavisd:15289| CC_SPAMMY, "554 5.7.0", - # amavisd-new-2.11.1/amavisd:15289| CC_BADH.",2", "554 5.6.3", # nonencoded 8-bit character - # amavisd-new-2.11.1/amavisd:15289| CC_BADH, "554 5.6.0", - # amavisd-new-2.11.1/amavisd:15289| CC_OVERSIZED, "552 5.3.4", - # amavisd-new-2.11.1/amavisd:15289| CC_MTA, "550 5.3.5", - # amavisd-new-2.11.1/amavisd:15289| CC_CATCHALL, "554 5.7.0", - # amavisd-new-2.11.1/amavisd:15289| }); - # ... - # amavisd-new-2.11.1/amavisd:15332|my $response = sprintf("%s %s%s%s", $status, - # amavisd-new-2.11.1/amavisd:15333| ($final_destiny == D_PASS ? "Ok" : - # amavisd-new-2.11.1/amavisd:15334| $final_destiny == D_DISCARD ? "Ok, discarded" : - # amavisd-new-2.11.1/amavisd:15335| $final_destiny == D_REJECT ? "Reject" : - # amavisd-new-2.11.1/amavisd:15336| $final_destiny == D_BOUNCE ? "Bounce" : - # amavisd-new-2.11.1/amavisd:15337| $final_destiny == D_TEMPFAIL ? "Temporary failure" : - # amavisd-new-2.11.1/amavisd:15338| "Not ok ($final_destiny)" ), - 'spamdetected' => [' - spam'], - 'virusdetected' => [' - infected'], - 'contenterror' => [' - bad header:'], - 'exceedlimit' => [' - message size exceeds recipient'], - 'systemerror' => [ - ' - temporary mta failure on relaying', - ' - rejected by next-hop mta on relaying', - ' - unable to relay message back to mta', - ], - }; - - my $fieldtable = Sisimai::RFC1894->FIELDTABLE; - my $permessage = {}; # (Hash) Store values of each Per-Message field - - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $v = undef; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0; - next; - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - next unless my $f = Sisimai::RFC1894->match($e); - - # $e matched with any field defined in RFC3464 - next unless my $o = Sisimai::RFC1894->field($e); - $v = $dscontents->[-1]; - - if( $o->[-1] eq 'addr' ) { - # Final-Recipient: rfc822; kijitora@example.jp - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - if( $o->[0] eq 'final-recipient' ) { - # Final-Recipient: rfc822; kijitora@example.jp - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = $o->[2]; - $recipients++; - } else { - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - $v->{'alias'} = $o->[2]; - } - } elsif( $o->[-1] eq 'code' ) { - # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown - $v->{'spec'} = $o->[1]; - $v->{'spec'} = 'SMTP' if uc $v->{'spec'} eq 'X-POSTFIX'; - $v->{'diagnosis'} = $o->[2]; - - } else { - # Other DSN fields defined in RFC3464 - next unless exists $fieldtable->{ $o->[0] }; - $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - - next unless $f == 1; - $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2]; - } - } - return undef unless $recipients; - - for my $e ( @$dscontents ) { - # Set default values if each value is empty. - $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage; - $e->{'diagnosis'} ||= Sisimai::String->sweep($e->{'diagnosis'}); - my $q = lc $e->{'diagnosis'}; - DETECT_REASON: for my $p ( keys %$messagesof ) { - # Try to detect an error reason - for my $r ( $messagesof->{ $p }->@* ) { - # Try to find an error message including lower-cased string defined in $messagesof - next unless index($q, $r) > -1; - $e->{'reason'} = $p; - last(DETECT_REASON) - } - } - } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::Amavis - bounce mail decoder class for C. - -=head1 SYNOPSIS - - use Sisimai::Lhost::Amavis; - -=head1 DESCRIPTION - -C decodes a bounce email which created by C. Methods in the -module are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::Amavis->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2019-2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/AmazonSES.pm b/lib/Sisimai/Lhost/AmazonSES.pm index 7f0257a88..4b54a73c1 100644 --- a/lib/Sisimai/Lhost/AmazonSES.pm +++ b/lib/Sisimai/Lhost/AmazonSES.pm @@ -4,315 +4,213 @@ use v5.26; use strict; use warnings; +# --------------------------------------------------------------------------------------------- +# "notificationType": "Bounce" +# https://docs.aws.amazon.com/ses/latest/dg/notification-contents.html#bounce-object +# +# Bounce types +# The bounce object contains a bounce type of Undetermined, Permanent, or Transient. The +# Permanent and Transient bounce types can also contain one of several bounce subtypes. +# +# When you receive a bounce notification with a bounce type of Transient, you might be +# able to send email to that recipient in the future if the issue that caused the message +# to bounce is resolved. +# +# When you receive a bounce notification with a bounce type of Permanent, it's unlikely +# that you'll be able to send email to that recipient in the future. For this reason, you +# should immediately remove the recipient whose address produced the bounce from your +# mailing lists. +# +# "bounceType"/"bounceSubType" "Desription" +# Undetermined/Undetermined -- The bounce message didn't contain enough information for +# Amazon SES to determine the reason for the bounce. +# +# Permanent/General ---------- When you receive this type of bounce notification, you should +# immediately remove the recipient's email address from your +# mailing list. +# Permanent/NoEmail ---------- It was not possible to retrieve the recipient email address +# from the bounce message. +# Permanent/Suppressed ------- The recipient's email address is on the Amazon SES suppression +# list because it has a recent history of producing hard bounces. +# Permanent/OnAccountSuppressionList +# Amazon SES has suppressed sending to this address because it +# is on the account-level suppression list. +# +# Transient/General ---------- You might be able to send a message to the same recipient +# in the future if the issue that caused the message to bounce +# is resolved. +# Transient/MailboxFull ------ the recipient's inbox was full. +# Transient/MessageTooLarge -- message you sent was too large +# Transient/ContentRejected -- message you sent contains content that the provider doesn't allow +# Transient/AttachmentRejected the message contained an unacceptable attachment +state $ReasonPair = { + "Supressed" => "suppressed", + "OnAccountSuppressionList" => "suppressed", + "General" => "onhold", + "MailboxFull" => "mailboxfull", + "MessageTooLarge" => "mesgtoobig", + "ContentRejected" => "contenterror", + "AttachmentRejected" => "securityerror", +}; + # https://aws.amazon.com/ses/ sub description { 'Amazon SES(Sending): https://aws.amazon.com/ses/' }; sub inquire { # Detect an error from Amazon SES - # @param [Hash] mhead Message headers of a bounce email + # @param [Hash] mhead Message headers of a bounce email (JSON) # @param [String] mbody Message body of a bounce email # @return [Hash] Bounce data list and message/rfc822 part # @return [undef] failed to decode or the arguments are missing + # @see https://docs.aws.amazon.com/ses/latest/dg/notification-contents.html # @since v4.0.2 my $class = shift; my $mhead = shift // return undef; my $mbody = shift // return undef; - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['Content-Type: message/rfc822']; - state $startingof = { 'message' => ['The following message to <', 'An error occurred while trying to deliver the mail '] }; - state $messagesof = { 'expired' => ['Delivery expired'] }; - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - - if( index($$mbody, '{') == 0 ) { - # The message body is JSON string - return undef unless exists $mhead->{'x-amz-sns-message-id'}; - return undef unless $mhead->{'x-amz-sns-message-id'}; - - # https://docs.aws.amazon.com/en_us/ses/latest/DeveloperGuide/notification-contents.html - my $bouncetype = { - 'Permanent' => { 'General' => '', 'NoEmail' => '', 'Suppressed' => '' }, - 'Transient' => { - 'General' => '', - 'MailboxFull' => 'mailboxfull', - 'MessageTooLarge' => 'mesgtoobig', - 'ContentRejected' => '', - 'AttachmentRejected' => '', - }, - }; - my $jsonstring = ''; - my $foldedline = 0; - my $sespayload = undef; - - for my $e ( split(/\n/, $$mbody) ) { - # Find JSON string from the message body - next unless length $e; - last if $e eq '--'; - - substr($e, 0, 1, '') if $foldedline; # The line starts with " ", continued from !\n. - $foldedline = 0; - - if( substr($e, -1, 1) eq '!' ) { - # ... long long line ...![\n] - substr($e, -1, 1, ''); - $foldedline = 1; - } - $jsonstring .= $e; - } - - require JSON; - eval { - my $jsonparser = JSON->new; - my $jsonobject = $jsonparser->decode($jsonstring); - - if( exists $jsonobject->{'Message'} ) { - # 'Message' => '{"notificationType":"Bounce",... - $sespayload = $jsonparser->decode($jsonobject->{'Message'}); - - } else { - # 'mail' => { 'sourceArn' => '...',... }, 'bounce' => {...}, - $sespayload = $jsonobject; - } - }; - if( $@ ) { - # Something wrong in decoding JSON - warn sprintf(" ***warning: Failed to decode JSON: %s", $@); - return undef; + return undef unless index($$mbody, "{") > -1; + return undef unless exists $mhead->{'x-amz-sns-message-id'}; + return undef unless $mhead->{'x-amz-sns-message-id'}; + + my $proceedsto = 0; + my $sespayload = $$mbody; + while(1) { + # Remote the following string begins with "--" + # -- + # If you wish to stop receiving notifications from this topic, please click or visit the link below to unsubscribe: + # https://sns.us-west-2.amazonaws.com/unsubscribe.html?SubscriptionArn=arn:aws:sns:us-west-2:1... + my $p1 = index($$mbody, "\n\n--\n"); + $sespayload = substr($$mbody, 0, $p1) if $p1 > 0; + $sespayload =~ s/!\n //g; + my $p2 = index($sespayload, '"Message"'); + + if( $p2 > 0 ) { + # The JSON included in the email is a format like the following: + # { + # "Type" : "Notification", + # "MessageId" : "02f86d9b-eecf-573d-b47d-3d1850750c30", + # "TopicArn" : "arn:aws:sns:us-west-2:123456789012:SES-EJ-B", + # "Message" : "{\"notificationType\"... + $sespayload =~ s/\\//g; + my $p3 = index($sespayload, "{", $p2 + 9); + my $p4 = index($sespayload, "\n", $p2 + 9); + $sespayload = substr($sespayload, $p3, $p4 - $p3); + $sespayload =~ s/,$//g; + $sespayload =~ s/"$//g; } - return undef unless exists $sespayload->{'notificationType'}; - - my $rfc822head = {}; # (Hash) Check flags for headers in RFC822 part - my $labeltable = { - 'Bounce' => 'bouncedRecipients', - 'Complaint' => 'complainedRecipients', - }; - my $p = $sespayload; - my $v = undef; - - if( $p->{'notificationType'} eq 'Bounce' || $p->{'notificationType'} eq 'Complaint' ) { - # { "notificationType":"Bounce", "bounce": { "bounceType":"Permanent",... - my $o = $p->{ lc $p->{'notificationType'} }; - my $r = $o->{ $labeltable->{ $p->{'notificationType'} } } || []; - - for my $e ( @$r ) { - # 'bouncedRecipients' => [ { 'emailAddress' => 'bounce@si...' }, ... ] - # 'complainedRecipients' => [ { 'emailAddress' => 'complaint@si...' }, ... ] - next unless Sisimai::Address->is_emailaddress($e->{'emailAddress'}); + last unless index($sespayload, "notificationType") > -1; + last unless index($sespayload, "{") == 0; + last unless substr($sespayload, -1, 1) eq "}"; + $proceedsto = 1; last; + } + return undef unless $proceedsto; + + # Load as JSON string and decode + require JSON; + my $jsonobject = undef; + eval { $jsonobject = JSON->new->decode($sespayload) }; + if( $@ ) { + # Something wrong in decoding JSON + warn sprintf(" ***warning: Failed to decode JSON: %s", $@); + return undef; + } + return undef unless exists $jsonobject->{'notificationType'}; + + require Sisimai::String; + require Sisimai::RFC1123; + require Sisimai::SMTP::Reply; + require Sisimai::SMTP::Status; + require Sisimai::SMTP::Command; + + my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; + my $recipients = 0; + my $whatnotify = substr($jsonobject->{"notificationType"}, 0, 1) || ""; + my $v = $dscontents->[-1]; + + if( $whatnotify eq "B" ) { + # "notificationType":"Bounce" + my $p = $jsonobject->{"bounce"}; + my $r = $p->{"bounceType"} eq "Permanent" ? "5" : "4"; + + for my $e ( $p->{"bouncedRecipients"}->@* ) { + # {"emailAddress":"neko@example.jp", "action":"failed", "status":"5.1.1", "diagnosticCode": "..."} + if( $v->{"recipient"} ) { + # There are multiple recipient addresses in the message body. + push @$dscontents, __PACKAGE__->DELIVERYSTATUS; $v = $dscontents->[-1]; - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $recipients++; - $v->{'recipient'} = $e->{'emailAddress'}; - - if( $p->{'notificationType'} eq 'Bounce' ) { - # 'bouncedRecipients => [ { - # 'emailAddress' => 'bounce@simulator.amazonses.com', - # 'action' => 'failed', - # 'status' => '5.1.1', - # 'diagnosticCode' => 'smtp; 550 5.1.1 user unknown' - # }, ... ] - $v->{'action'} = $e->{'action'}; - $v->{'status'} = $e->{'status'}; - - my $p0 = index($e->{'diagnosticCode'}, '; '); - if( $p0 > 3 ) { - # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown - $v->{'spec'} = uc substr($e->{'diagnosticCode'}, 0, $p0); - $v->{'diagnosis'} = substr($e->{'diagnosticCode'}, $p0 + 2,); - - } else { - $v->{'diagnosis'} = $e->{'diagnosticCode'}; - } - - # 'reportingMTA' => 'dsn; a27-23.smtp-out.us-west-2.amazonses.com', - $p0 = index($o->{'reportingMTA'}, 'dsn; '); - $v->{'lhost'} = Sisimai::String->sweep(substr($o->{'reportingMTA'}, $p0 + 5,)) if $p0 == 0; - - if( exists $bouncetype->{ $o->{'bounceType'} } && - exists $bouncetype->{ $o->{'bounceType'} }->{ $o->{'bounceSubType'} } ) { - # 'bounce' => { - # 'bounceType' => 'Permanent', - # 'bounceSubType' => 'General' - # }, - $v->{'reason'} = $bouncetype->{ $o->{'bounceType'} }->{ $o->{'bounceSubType'} }; - } - } else { - # 'complainedRecipients' => [ { - # 'emailAddress' => 'complaint@simulator.amazonses.com' }, ... ], - $v->{'reason'} = 'feedback'; - $v->{'feedbacktype'} = $o->{'complaintFeedbackType'} || ''; - } - ($v->{'date'} = $o->{'timestamp'} || $p->{'mail'}->{'timestamp'}) =~ s/[.]\d+Z\z//; } - } elsif( $p->{'notificationType'} eq 'Delivery' ) { - # { "notificationType":"Delivery", "delivery": { ... - my $o = $p->{'delivery'}; - my $r = $o->{'recipients'} || []; - - for my $e ( @$r ) { - # 'delivery' => { - # 'timestamp' => '2016-11-23T12:01:03.512Z', - # 'processingTimeMillis' => 3982, - # 'reportingMTA' => 'a27-29.smtp-out.us-west-2.amazonses.com', - # 'recipients' => [ - # 'success@simulator.amazonses.com' - # ], - # 'smtpResponse' => '250 2.6.0 Message received' - # }, - next unless Sisimai::Address->is_emailaddress($e); - - $v = $dscontents->[-1]; - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $recipients++; - $v->{'recipient'} = $e; - $v->{'lhost'} = $o->{'reportingMTA'} || ''; - $v->{'diagnosis'} = $o->{'smtpResponse'} || ''; - $v->{'status'} = Sisimai::SMTP::Status->find($v->{'diagnosis'}) || ''; - $v->{'reason'} = 'delivered'; - $v->{'action'} = 'delivered'; - ($v->{'date'} = $o->{'timestamp'} || $p->{'mail'}->{'timestamp'}) =~ s/[.]\d+Z\z//; + $v->{"recipient"} = $e->{"emailAddress"}; + $v->{"diagnosis"} = Sisimai::String->sweep($e->{"diagnosticCode"}); + $v->{"command"} = Sisimai::SMTP::Command->find($v->{"diagnosis"}); + $v->{"action"} = $e->{"action"}; + $v->{"status"} = Sisimai::SMTP::Status->find($v->{"diagnosis"}, $r); + $v->{"replycode"} = Sisimai::SMTP::Reply->find($v->{"diagnosis"}, $v->{"status"}); + $v->{"date"} = $p->{"timestamp"}; + $v->{"lhost"} = Sisimai::RFC1123->find($p->{"reportingMTA"}); + $recipients++; + + for my $f ( keys %$ReasonPair ) { + # Try to find the bounce reason by "bounceSubType" + next unless $ReasonPair->{ $f } eq $p->{"bounceSubType"}; + $v->{"reason"} = $f; last; } - } else { - # The value of "notificationType" is not any of "Bounce", "Complaint", or "Delivery". - return undef; } - return undef unless $recipients; - - if( exists $p->{'mail'}->{'headers'} ) { - # "headersTruncated":false, - # "headers":[ { ... - for my $e ( $p->{'mail'}->{'headers'}->@* ) { - # 'headers' => [ { 'name' => 'From', 'value' => 'neko@nyaan.jp' }, ... ], - next unless grep { $e->{'name'} eq $_ } ('From', 'To', 'Subject', 'Message-ID', 'Date'); - $rfc822head->{ lc $e->{'name'} } = $e->{'value'}; + } elsif( $whatnotify eq "C" ) { + # "notificationType":"Complaint" + my $p = $jsonobject->{"complaint"}; + for my $e ( $p->{"complainedRecipients"}->@* ) { + # {"emailAddress":"neko@example.jp"} + if( $v->{"recipient"} ) { + # There are multiple recipient addresses in the message body. + push @$dscontents, __PACKAGE__->DELIVERYSTATUS; + $v = $dscontents->[-1]; } + $v->{"recipient"} = $e->{"emailAddress"}; + $v->{"reason"} = "feedback"; + $v->{"feedbacktype"} = $p->{"complaintFeedbackType"}; + $v->{"date"} = $p->{"timestamp"}; + $v->{"diagnosis"} = sprintf(qq|{"feedbackid":"%s", "useragent":"%s"}|, $p->{"feedbackId"}, $p->{"userAgent"}); + $recipients++; } - - unless( $rfc822head->{'message-id'} ) { - # Try to get the value of "Message-Id". - # 'messageId' => '01010157e48f9b9b-891e9a0e-9c9d-4773-9bfe-608f2ef4756d-000000' - $rfc822head->{'message-id'} = $p->{'mail'}->{'messageId'} if $p->{'mail'}->{'messageId'}; + } elsif( $whatnotify eq "D" ) { + # "notificationType":"Delivery" + my $p = $jsonobject->{"delivery"}; + for my $e ( $p->{"recipients"}->@* ) { + # {"recipients":["neko@example.jp"]} + if( $v->{"recipient"} ) { + # There are multiple recipient addresses in the message body. + push @$dscontents, __PACKAGE__->DELIVERYSTATUS; + $v = $dscontents->[-1]; + } + $v->{"recipient"} = $e; + $v->{"reason"} = "delivered"; + $v->{"action"} = "delivered"; + $v->{"date"} = $p->{"timestamp"}; + $v->{"lhost"} = $p->{"reportingMTA"}; + $v->{"diagnosis"} = $p->{"smtpResponse"}; + $v->{"status"} = Sisimai::SMTP::Status->find($v->{"diagnosis"}, "2"); + $v->{"replycode"} = Sisimai::SMTP::Reply->find($v->{"diagnosis"}, "2"); + $recipients++; } - return { 'ds' => $dscontents, 'rfc822' => $rfc822head }; - } else { - # The message body is an email - # 'from' => qr/\AMAILER-DAEMON[@]email[-]bounces[.]amazonses[.]com\z/, - # 'subject' => qr/\ADelivery Status Notification [(]Failure[)]\z/, - my $xmail = $mhead->{'x-mailer'} || ''; - return undef if index($xmail, 'Amazon WorkMail') > -1; - - # X-SenderID: Sendmail Sender-ID Filter v1.0.0 nijo.example.jp p7V3i843003008 - # X-Original-To: 000001321defbd2a-788e31c8-2be1-422f-a8d4-cf7765cc9ed7-000000@email-bounces.amazonses.com - # X-AWS-Outgoing: 199.255.192.156 - # X-SES-Outgoing: 2016.10.12-54.240.27.6 - my $match = 0; - $match ||= 1 if $mhead->{'x-aws-outgoing'}; - $match ||= 1 if $mhead->{'x-ses-outgoing'}; - return undef unless $match; - - my $fieldtable = Sisimai::RFC1894->FIELDTABLE; - my $permessage = {}; # (Hash) Store values of each Per-Message field - my $readcursor = 0; # (Integer) Points the current cursor position - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $v = undef; - my $p = ''; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read each line between the start of the message and the start of rfc822 part. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - if( index($e, $startingof->{'message'}->[0]) == 0 || - index($e, $startingof->{'message'}->[1]) == 0 ) { - $readcursor |= $indicators->{'deliverystatus'}; - next; - } - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; + # Unknown "notificationType" value + warn sprintf(" ***warning: There is no notificationType field or unknown type of notificationType field"); + return undef; + } + return undef unless $recipients; - if( my $f = Sisimai::RFC1894->match($e) ) { - # $e matched with any field defined in RFC3464 - next unless my $o = Sisimai::RFC1894->field($e); - $v = $dscontents->[-1]; + # Time::Piece->strptime() cannot parse "2016-11-25T01:49:01.000Z" format + for my $e ( @$dscontents ) { s/T/ /, s/[.]\d{3}Z$// for $e->{'date'} } - if( $o->[-1] eq 'addr' ) { - # Final-Recipient: rfc822; kijitora@example.jp - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - if( $o->[0] eq 'final-recipient' ) { - # Final-Recipient: rfc822; kijitora@example.jp - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = $o->[2]; - $recipients++; - - } else { - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - $v->{'alias'} = $o->[2]; - } - } elsif( $o->[-1] eq 'code' ) { - # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown - $v->{'spec'} = $o->[1]; - $v->{'diagnosis'} = $o->[2]; - - } else { - # Other DSN fields defined in RFC3464 - next unless exists $fieldtable->{ $o->[0] }; - $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - - next unless $f == 1; - $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2]; - } - } else { - # Continued line of the value of Diagnostic-Code field - next unless index($p, 'Diagnostic-Code:') == 0; - next unless index($e, ' ') == 0; - $v->{'diagnosis'} .= ' '.$e; - } - } continue { - # Save the current line for the next loop - $p = $e; - } - return undef unless $recipients; - - for my $e ( @$dscontents ) { - # Set default values if each value is empty. - $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage; - - $e->{'diagnosis'} =~ y/\n/ /; - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); - - if( index($e->{'status'}, '.0.0') > 0 || index($e->{'status'}, '.1.0') > 0 ) { - # Get other D.S.N. value from the error message - # 5.1.0 - Unknown address error 550-'5.7.1 ... - my $errormessage = $e->{'diagnosis'}; - my $p1 = index($e->{'diagnosis'}, "-'"); $p1 = index($e->{'diagnosis'}, '-"') if $p1 < 0; - my $p2 = rindex($e->{'diagnosis'}, "' "); $p2 = rindex($e->{'diagnosis'}, '" ') if $p2 < 0; - $errormessage = substr($e->{'diagnosis'}, $p1 + 2, $p2 - $p1 - 2) if $p1 > -1 && $p2 > -1; - $e->{'status'} = Sisimai::SMTP::Status->find($errormessage) || $e->{'status'}; - } - $e->{'replycode'} ||= Sisimai::SMTP::Reply->find($e->{'diagnosis'}, $e->{'status'}); + # Generate pseudo email headers as the original message + my $cv = ""; + my $ch = ["date", "subject"]; + my $or = $jsonobject->{'mail'}; - SESSION: for my $r ( keys %$messagesof ) { - # Verify each regular expression of session errors - next unless grep { index($e->{'diagnosis'}, $_) > -1 } $messagesof->{ $r }->@*; - $e->{'reason'} = $r; - last; - } - } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; - } + map { $cv .= sprintf("%s: %s\n", $_->{"name"}, $_->{"value"}) } $or->{"headers"}->@*; + map { $cv .= sprintf("%s: %s\n", ucfirst($_), $or->{"commonHeaders"}->{ $_ }) if exists $or->{"commonHeaders"}->{ $_ } } @$ch; + + return { 'ds' => $dscontents, 'rfc822' => $cv }; } 1; @@ -330,9 +228,8 @@ Sisimai::Lhost::AmazonSES - bounce mail decoder class for Amazon SES L decodes a bounce email or a JSON string which created by Amazon Simple -Email Service L. -Methods in the module are called from only C. +C decodes a JSON string which created by Amazon Simple Email Service +L. Methods in the module are called from only C. =head1 CLASS METHODS @@ -347,11 +244,6 @@ C returns description string of this module. C method decodes a bounced email and return results as a array reference. See C for more details. -=head2 C)>> - -C method adapts Amazon SES bounce object (JSON) for Perl hash object used at -C class. - =head1 AUTHOR azumakuniyuki diff --git a/lib/Sisimai/Lhost/AmazonWorkMail.pm b/lib/Sisimai/Lhost/AmazonWorkMail.pm deleted file mode 100644 index edc6dd608..000000000 --- a/lib/Sisimai/Lhost/AmazonWorkMail.pm +++ /dev/null @@ -1,164 +0,0 @@ -package Sisimai::Lhost::AmazonWorkMail; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -# https://aws.amazon.com/workmail/ -sub description { 'Amazon WorkMail: https://aws.amazon.com/workmail/' } -sub inquire { - # Detect an error from Amazon WorkMail - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decode or the arguments are missing - # @since v4.1.29 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - my $match = 0; - my $xmail = $mhead->{'x-original-mailer'} || $mhead->{'x-mailer'} || ''; - - # X-Mailer: Amazon WorkMail - # X-Original-Mailer: Amazon WorkMail - # X-Ses-Outgoing: 2016.01.14-54.240.27.159 - $match++ if $mhead->{'x-ses-outgoing'}; - if( $xmail ) { - # X-Mailer: Amazon WorkMail - # X-Original-Mailer: Amazon WorkMail - $match++ if $xmail eq 'Amazon WorkMail'; - } - return undef if $match < 2; - - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['Content-Type: message/rfc822']; - state $startingof = { 'message' => ['Technical report:'] }; - - my $fieldtable = Sisimai::RFC1894->FIELDTABLE; - my $permessage = {}; # (Hash) Store values of each Per-Message field - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $v = undef; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0; - next; - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - - if( my $f = Sisimai::RFC1894->match($e) ) { - # $e matched with any field defined in RFC3464 - next unless my $o = Sisimai::RFC1894->field($e); - $v = $dscontents->[-1]; - - if( $o->[-1] eq 'addr' ) { - # Final-Recipient: rfc822; kijitora@example.jp - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - if( $o->[0] eq 'final-recipient' ) { - # Final-Recipient: rfc822; kijitora@example.jp - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = $o->[2]; - $recipients++; - - } else { - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - $v->{'alias'} = $o->[2]; - } - } elsif( $o->[-1] eq 'code' ) { - # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown - $v->{'spec'} = $o->[1]; - $v->{'diagnosis'} = $o->[2]; - - } else { - # Other DSN fields defined in RFC3464 - next unless exists $fieldtable->{ $o->[0] }; - $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - - next unless $f == 1; - $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2]; - } - } - - # - # - # - # - last if index($e, '') == 0; - } - return undef unless $recipients; - - for my $e ( @$dscontents ) { - # Set default values if each value is empty. - $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage; - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); - - if( index($e->{'status'}, '.0.0') > 0 || index($e->{'status'}, '.1.0') > 0 ) { - # Get other D.S.N. value from the error message - # 5.1.0 - Unknown address error 550-'5.7.1 ... - $e->{'status'} = Sisimai::SMTP::Status->find($e->{'diagnosis'}) || $e->{'status'}; - } - - # 554 4.4.7 Message expired: unable to deliver in 840 minutes. - # <421 4.4.2 Connection timed out> - $e->{'replycode'} = Sisimai::SMTP::Reply->find($e->{'diagnosis'}) || ''; - $e->{'reason'} ||= Sisimai::SMTP::Status->name($e->{'status'}) || ''; - } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::AmazonWorkMail - bounce mail decoder class for Amazon WorkMail L. - -=head1 SYNOPSIS - - use Sisimai::Lhost::AmazonWorkMail; - -=head1 DESCRIPTION - -C decodes a bounce email which created by Amazon WorkMail L. -Methods in the module are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::AmazonWorkMail->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2016-2021,2023,2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/Aol.pm b/lib/Sisimai/Lhost/Aol.pm deleted file mode 100644 index f261388ff..000000000 --- a/lib/Sisimai/Lhost/Aol.pm +++ /dev/null @@ -1,167 +0,0 @@ -package Sisimai::Lhost::Aol; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -sub description { 'Aol Mail: https://www.aol.com' } -sub inquire { - # Detect an error from Aol Mail - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decoded or the arguments are missing - # @since v4.1.3 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - - # X-AOL-IP: 192.0.2.135 - # X-AOL-VSS-INFO: 5600.1067/98281 - # X-AOL-VSS-CODE: clean - # x-aol-sid: 3039ac1afc14546fb98a0945 - # X-AOL-SCOLL-EIL: 1 - # x-aol-global-disposition: G - # x-aol-sid: 3039ac1afd4d546fb97d75c6 - # X-BounceIO-Id: 9D38DE46-21BC-4309-83E1-5F0D788EFF1F.1_0 - # X-Outbound-Mail-Relay-Queue-ID: 07391702BF4DC - # X-Outbound-Mail-Relay-Sender: rfc822; shironeko@aol.example.jp - return undef unless $mhead->{'x-aol-ip'}; - - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['Content-Type: message/rfc822']; - state $startingof = { 'message' => ['Content-Type: message/delivery-status'] }; - state $messagesof = { - 'hostunknown' => ['Host or domain name not found'], - 'notaccept' => ['type=MX: Malformed or unexpected name server reply'], - }; - - my $fieldtable = Sisimai::RFC1894->FIELDTABLE; - my $permessage = {}; # (Hash) Store values of each Per-Message field - - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $v = undef; - my $p = ''; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0; - next; - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - - if( my $f = Sisimai::RFC1894->match($e) ) { - # $e matched with any field defined in RFC3464 - next unless my $o = Sisimai::RFC1894->field($e); - $v = $dscontents->[-1]; - - if( $o->[-1] eq 'addr' ) { - # Final-Recipient: rfc822; kijitora@example.jp - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - if( $o->[0] eq 'final-recipient' ) { - # Final-Recipient: rfc822; kijitora@example.jp - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = $o->[2]; - $recipients++; - - } else { - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - $v->{'alias'} = $o->[2]; - } - } elsif( $o->[-1] eq 'code' ) { - # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown - $v->{'spec'} = $o->[1]; - $v->{'diagnosis'} = $o->[2]; - - } else { - # Other DSN fields defined in RFC3464 - next unless exists $fieldtable->{ $o->[0] }; - $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - - next unless $f == 1; - $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2]; - } - } else { - # Continued line of the value of Diagnostic-Code field - next unless index($p, 'Diagnostic-Code:') == 0; - next unless index($e, ' ') == 0; - $v->{'diagnosis'} .= ' '.Sisimai::String->sweep($e); - } - } continue { - # Save the current line for the next loop - $p = $e; - } - return undef unless $recipients; - - for my $e ( @$dscontents ) { - # Set default values if each value is empty. - $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage; - - $e->{'diagnosis'} =~ y/\n/ /; - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); - SESSION: for my $r ( keys %$messagesof ) { - # Verify each regular expression of session errors - next unless grep { index($e->{'diagnosis'}, $_) > -1 } $messagesof->{ $r }->@*; - $e->{'reason'} = $r; - last; - } - } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::Aol - bounce mail decoder class for Aol Mail L. - -=head1 SYNOPSIS - - use Sisimai::Lhost::Aol; - -=head1 DESCRIPTION - -C decodes a bounce email which created by Aol Mail L. -Methods in the module are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::Aol->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2014-2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/ApacheJames.pm b/lib/Sisimai/Lhost/ApacheJames.pm index 4d4ebc900..28c96635a 100644 --- a/lib/Sisimai/Lhost/ApacheJames.pm +++ b/lib/Sisimai/Lhost/ApacheJames.pm @@ -17,42 +17,41 @@ sub inquire { my $mbody = shift // return undef; my $match = 0; - # 'subject' => qr/\A\[BOUNCE\]\z/, - # 'received' => qr/JAMES SMTP Server/, - # 'message-id' => qr/\d+[.]JavaMail[.].+[@]/, $match ||= 1 if $mhead->{'subject'} eq '[BOUNCE]'; $match ||= 1 if defined $mhead->{'message-id'} && rindex($mhead->{'message-id'}, '.JavaMail.') > -1; $match ||= 1 if grep { rindex($_, 'JAMES SMTP Server') > -1 } $mhead->{'received'}->@*; return undef unless $match; state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['Content-Type: message/rfc822']; + state $boundaries = ["Content-Type: message/rfc822"]; state $startingof = { # apache-james-2.3.2/src/java/org/apache/james/transport/mailets/ # AbstractNotify.java|124: out.println("Error message below:"); # AbstractNotify.java|128: out.println("Message details:"); - 'message' => [''], - 'error' => ['Error message below:'], + "message" => ["Message details:"], }; my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $issuedcode = ''; # (String) Alternative diagnostic message - my $subjecttxt = undef; # (String) Alternative Subject text - my $gotmessage = 0; # (Integer) Flag for error message - my $v = undef; + my $readcursor = 0; # Points the current cursor position + my $recipients = 0; # The number of 'Final-Recipient' header + my $alternates = ["", "", "", ""]; # [Envelope-From, Header-From, Date, Subject] + my $v = $dscontents->[-1]; for my $e ( split("\n", $emailparts->[0]) ) { # Read error messages and delivery status lines from the head of the email to the previous # line of the beginning of the original message. unless( $readcursor ) { # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0; + if( index($e, $startingof->{"message"}->[0]) == 0 ) { + # Message details: + # Subject: Nyaaan + $readcursor |= $indicators->{"deliverystatus"}; next; + } + $v->{"diagnosis"} .= $e." " if $e ne ""; next; } - next unless $readcursor & $indicators->{'deliverystatus'}; + next unless $readcursor & $indicators->{"deliverystatus"}; next unless length $e; # Message details: @@ -64,57 +63,50 @@ sub inquire { # To: kijitora@example.org # Size (in bytes): 1024 # Number of lines: 64 - $v = $dscontents->[-1]; - - if( index($e, ' RCPT TO: ') == 0 ) { + if( index($e, " RCPT TO: ") == 0 ) { # RCPT TO: kijitora@example.org - if( $v->{'recipient'} ) { + if( $v->{"recipient"} ) { # There are multiple recipient addresses in the message body. push @$dscontents, __PACKAGE__->DELIVERYSTATUS; $v = $dscontents->[-1]; } - $v->{'recipient'} = substr($e, 12,); + $v->{"recipient"} = substr($e, 12,); $recipients++; - } elsif( index($e, ' Sent date: ') == 0 ) { + } elsif( index($e, " Sent date: ") == 0 ) { # Sent date: Thu Apr 29 01:20:50 JST 2015 - $v->{'date'} = substr($e, 13,); + $v->{"date"} = substr($e, 13,); + $alternates->[2] = $v->{"date"}; - } elsif( index($e, ' Subject: ') == 0 ) { + } elsif( index($e, " Subject: ") == 0 ) { # Subject: Nyaaan - $subjecttxt = substr($e, 11,) - - } else { - next if $gotmessage == 1; - - if( $v->{'diagnosis'} ) { - # Get an error message text - if( $e eq 'Message details:' ) { - # Message details: - # Subject: nyaan - # ... - $gotmessage = 1; - - } else { - # Append error message text like the followng: - # Error message below: - # 550 - Requested action not taken: no such user here - $v->{'diagnosis'} .= ' '.$e; - } - } else { - # Error message below: - # 550 - Requested action not taken: no such user here - $v->{'diagnosis'} = $e if $e eq $startingof->{'error'}->[0]; - $v->{'diagnosis'} .= ' '.$e unless $gotmessage; - } + $alternates->[3] = substr($e, 11,); + + } elsif( index($e, " MAIL FROM: ") == 0 ) { + # MAIL FROM: shironeko@example.jp + $alternates->[0] = substr($e, 13,); + + } elsif( index($e, " From: ") == 0 ) { + # From: Neko + $alternates->[1] = substr($e, 8,); } } return undef unless $recipients; - # Set the value of $subjecttxt as a Subject if there is no original message - # in the bounce mail. - $emailparts->[1] .= sprintf("Subject: %s\n", $subjecttxt) if index($emailparts->[1], "\nSubject:") < 0; - $_->{'diagnosis'} = Sisimai::String->sweep($_->{'diagnosis'} || $issuedcode) for @$dscontents; + if( $emailparts->[1] eq "" ) { + # The original message is empty + $emailparts->[1] .= sprintf("From: %s\n", $alternates->[1]) if $alternates->[1] ne ""; + $emailparts->[1] .= sprintf("Date: %s\n", $alternates->[2]) if $alternates->[2] ne ""; + } + if( index($emailparts->[1], "Return-Path: ") < 0 ) { + # Set the envelope from address as a Return-Path: header + $emailparts->[1] .= sprintf("Return-Path: <%s>\n", $alternates->[0]) if $alternates->[0] ne ""; + } + if( index($emailparts->[1], "\nSubject: ") < 0 ) { + # Set the envelope from address as a Return-Path: header + $emailparts->[1] .= sprintf("Subject: %s\n", $alternates->[3]) if $alternates->[3] ne ""; + } + $_->{"diagnosis"} = Sisimai::String->sweep($_->{"diagnosis"}) for @$dscontents; return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; } diff --git a/lib/Sisimai/Lhost/Barracuda.pm b/lib/Sisimai/Lhost/Barracuda.pm deleted file mode 100644 index a051752c8..000000000 --- a/lib/Sisimai/Lhost/Barracuda.pm +++ /dev/null @@ -1,136 +0,0 @@ -package Sisimai::Lhost::Barracuda; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -sub description { 'Barracuda: https://www.barracuda.com' } -sub inquire { - # Detect an error from Barracuda - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decode or the arguments are missing - # @since v4.25.6 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - - # Subject: **Message you sent blocked by our bulk email filter** - return undef unless index($mhead->{'subject'}, 'our bulk email filter') > 0; - - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['Content-Type: text/rfc822-headers']; - state $startingof = { 'message' => ['Your message to:'] }; - - my $fieldtable = Sisimai::RFC1894->FIELDTABLE; - my $permessage = {}; # (Hash) Store values of each Per-Message field - - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $v = undef; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0; - next; - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - - if( my $f = Sisimai::RFC1894->match($e) ) { - # $e matched with any field defined in RFC3464 - next unless my $o = Sisimai::RFC1894->field($e); - $v = $dscontents->[-1]; - - if( $o->[-1] eq 'addr' ) { - # Final-Recipient: rfc822; kijitora@example.jp - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - if( $o->[0] eq 'final-recipient' ) { - # Final-Recipient: rfc822; kijitora@example.jp - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = $o->[2]; - $recipients++; - } else { - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - $v->{'alias'} = $o->[2]; - } - } elsif( $o->[-1] eq 'code' ) { - # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown - $v->{'spec'} = $o->[1]; - $v->{'diagnosis'} = $o->[2]; - - } else { - # Other DSN fields defined in RFC3464 - next unless exists $fieldtable->{ $o->[0] }; - $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - - next unless $f == 1; - $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2]; - } - } - } - return undef unless $recipients; - - for my $e ( @$dscontents ) { - # Set default values if each value is empty. - $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage; - $e->{'diagnosis'} ||= Sisimai::String->sweep($e->{'diagnosis'}); - } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::Barracuda - bounce mail decoder class for Barracuda L. - -=head1 SYNOPSIS - - use Sisimai::Lhost::Barracuda; - -=head1 DESCRIPTION - -C decodes a bounce email which created by Barracuda L. -Methods in the module are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::Barracuda->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2020,2021,2023,2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/Bigfoot.pm b/lib/Sisimai/Lhost/Bigfoot.pm deleted file mode 100644 index 32f15e345..000000000 --- a/lib/Sisimai/Lhost/Bigfoot.pm +++ /dev/null @@ -1,169 +0,0 @@ -package Sisimai::Lhost::Bigfoot; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -sub description { 'Bigfoot: http://www.bigfoot.com' } -sub inquire { - # Detect an error from Bigfoot - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decode or the arguments are missing - # @since v4.1.10 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - my $match = 0; - - # 'subject' => qr/\AReturned mail: /, - $match ||= 1 if rindex($mhead->{'from'}, '@bigfoot.com>') > -1; - $match ||= 1 if grep { rindex($_, '.bigfoot.com ') > -1 } $mhead->{'received'}->@*; - return undef unless $match; - - require Sisimai::SMTP::Command; - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['Content-Type: message/partial']; - state $markingsof = { 'message' => ' ----- Transcript of session follows -----' }; - - my $fieldtable = Sisimai::RFC1894->FIELDTABLE; - my $permessage = {}; # (Hash) Store values of each Per-Message field - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $thecommand = ''; # (String) SMTP Command name begin with the string '>>>' - my $esmtpreply = ''; # (String) Reply from remote server on SMTP session - my $v = undef; - my $p = ''; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if index($e, $markingsof->{'message'}) == 0; - next; - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - - if( my $f = Sisimai::RFC1894->match($e) ) { - # $e matched with any field defined in RFC3464 - next unless my $o = Sisimai::RFC1894->field($e); - $v = $dscontents->[-1]; - - if( $o->[-1] eq 'addr' ) { - # Final-Recipient: rfc822; kijitora@example.jp - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - if( $o->[0] eq 'final-recipient' ) { - # Final-Recipient: rfc822; kijitora@example.jp - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = $o->[2]; - $recipients++; - - } else { - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - $v->{'alias'} = $o->[2]; - } - } elsif( $o->[-1] eq 'code' ) { - # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown - $v->{'spec'} = $o->[1]; - $v->{'diagnosis'} = $o->[2]; - - } else { - # Other DSN fields defined in RFC3464 - next unless exists $fieldtable->{ $o->[0] }; - $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - - next unless $f == 1; - $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2]; - } - } else { - # The line does not begin with a DSN field defined in RFC3464 - if( substr($e, 0, 1) ne ' ' ) { - # ----- Transcript of session follows ----- - # >>> RCPT TO: - # <<< 553 Invalid recipient destinaion@example.net (Mode: normal) - if( index($e, '>>> ') == 0 ) { - # >>> DATA - $thecommand = Sisimai::SMTP::Command->find($e); - - } elsif( index($e, '<<< ') == 0 ) { - # <<< Response - $esmtpreply = substr($e, 4,); - } - } else { - # Continued line of the value of Diagnostic-Code field - next unless index($p, 'Diagnostic-Code:') == 0; - next unless index($e, ' ') == 0; - $v->{'diagnosis'} .= Sisimai::String->sweep(substr($e, 1,)); - } - } - } continue { - # Save the current line for the next loop - $p = $e; - } - return undef unless $recipients; - - for my $e ( @$dscontents ) { - # Set default values if each value is empty. - $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage; - - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); - $e->{'command'} ||= $thecommand || ''; - $e->{'command'} ||= 'EHLO' if $esmtpreply; - } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::Bigfoot - bounce mail decoder class for Bigfoot L. - -=head1 SYNOPSIS - - use Sisimai::Lhost::Bigfoot; - -=head1 DESCRIPTION - -C decodes a bounce email which created by Bigfoot L. -Methods in the module are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::Bigfoot->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2014-2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/Courier.pm b/lib/Sisimai/Lhost/Courier.pm index b7e0be59d..549df85b2 100644 --- a/lib/Sisimai/Lhost/Courier.pm +++ b/lib/Sisimai/Lhost/Courier.pm @@ -73,7 +73,7 @@ sub inquire { next unless my $o = Sisimai::RFC1894->field($e); $v = $dscontents->[-1]; - if( $o->[-1] eq 'addr' ) { + if( $o->[3] eq 'addr' ) { # Final-Recipient: rfc822; kijitora@example.jp # X-Actual-Recipient: rfc822; kijitora@example.co.jp if( $o->[0] eq 'final-recipient' ) { @@ -90,7 +90,7 @@ sub inquire { # X-Actual-Recipient: rfc822; kijitora@example.co.jp $v->{'alias'} = $o->[2]; } - } elsif( $o->[-1] eq 'code' ) { + } elsif( $o->[3] eq 'code' ) { # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown $v->{'spec'} = $o->[1]; $v->{'diagnosis'} = $o->[2]; diff --git a/lib/Sisimai/Lhost/Domino.pm b/lib/Sisimai/Lhost/Domino.pm index 5f5e69355..ad64c4fcb 100644 --- a/lib/Sisimai/Lhost/Domino.pm +++ b/lib/Sisimai/Lhost/Domino.pm @@ -31,6 +31,8 @@ sub inquire { state $boundaries = ['Content-Type: message/rfc822']; state $startingof = { 'message' => ['Your message'] }; state $messagesof = { + 'filtered' => ['Cannot route mail to user'], + 'systemerror' => ['Several matches found in Domino Directory'], 'userunknown' => [ 'not listed in Domino Directory', 'not listed in public Name & Address Book', @@ -38,8 +40,6 @@ sub inquire { "non répertorié dans l'annuaire Domino", 'Domino ディレクトリには見つかりません', ], - 'filtered' => ['Cannot route mail to user'], - 'systemerror' => ['Several matches found in Domino Directory'], }; my $fieldtable = Sisimai::RFC1894->FIELDTABLE; @@ -78,6 +78,7 @@ sub inquire { $v = $dscontents->[-1]; if( $e eq 'was not delivered to:' ) { # was not delivered to: + # kijitora@example.net if( $v->{'recipient'} ) { # There are multiple recipient addresses in the message body. push @$dscontents, __PACKAGE__->DELIVERYSTATUS; @@ -93,6 +94,7 @@ sub inquire { } elsif( $e eq 'because:' ) { # because: + # User some.name (kijitora@example.net) not listed in Domino Directory $v->{'diagnosis'} = $e; } else { @@ -104,12 +106,13 @@ sub inquire { # Subject: Nyaa $subjecttxt = substr($e, 11,); - } elsif( my $f = Sisimai::RFC1894->match($e) ) { + } else { # There are some fields defined in RFC3464, try to match - next unless my $o = Sisimai::RFC1894->field($e); - next if $o->[-1] eq 'addr'; + my $f = Sisimai::RFC1894->match($e); next if $f < 1; + my $o = Sisimai::RFC1894->field($e); next unless $o; + next if $o->[3] eq 'addr'; - if( $o->[-1] eq 'code' ) { + if( $o->[3] eq 'code' ) { # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown $v->{'spec'} ||= $o->[1]; $v->{'diagnosis'} ||= $o->[2]; diff --git a/lib/Sisimai/Lhost/EZweb.pm b/lib/Sisimai/Lhost/EZweb.pm index 3cfe4389f..6c423e03c 100644 --- a/lib/Sisimai/Lhost/EZweb.pm +++ b/lib/Sisimai/Lhost/EZweb.pm @@ -35,39 +35,40 @@ sub inquire { require Sisimai::SMTP::Command; state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['--------------------------------------------------', 'Content-Type: message/rfc822']; - state $markingsof = { 'message' => ['The user(s) ', 'Your message ', 'Each of the following', '<'] }; - state $refailures = { + state $boundaries = ["--------------------------------------------------", "Content-Type: message/rfc822"]; + state $startingof = { "message" => ['The user(s) ', 'Your message ', 'Each of the following', '<'] }; + state $messagesof = { #'notaccept' => ['The following recipients did not receive this message:'], + 'expired' => [ + # Your message was not delivered within 0 days and 1 hours. + # Remote host is not responding. + 'Your message was not delivered within ', + ], 'mailboxfull' => ['The user(s) account is temporarily over quota'], - 'suspend' => [ + 'onhold' => ['Each of the following recipients was rejected by a remote mail server'], + 'suspend' => [ # http://www.naruhodo-au.kddi.com/qa3429203.html # The recipient may be unpaid user...? 'The user(s) account is disabled.', 'The user(s) account is temporarily limited.', ], - 'expired' => [ - # Your message was not delivered within 0 days and 1 hours. - # Remote host is not responding. - 'Your message was not delivered within ', - ], - 'onhold' => ['Each of the following recipients was rejected by a remote mail server'], }; my $fieldtable = Sisimai::RFC1894->FIELDTABLE; my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my @rxmessages; push @rxmessages, $refailures->{ $_ }->@* for keys %$refailures; + my $readcursor = 0; # Points the current cursor position + my $recipients = 0; # The number of 'Final-Recipient' header + my $substrings = []; # All the values of "messagesof" my $v = undef; + map { push @$substrings, $messagesof->{ $_ }->@* } keys %$messagesof; for my $e ( split("\n", $emailparts->[0]) ) { # Read error messages and delivery status lines from the head of the email to the previous # line of the beginning of the original message. unless( $readcursor ) { # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if grep { index($e, $_) > -1 } $markingsof->{'message'}->@*; + $readcursor |= $indicators->{'deliverystatus'} if grep { index($e, $_) > -1 } $startingof->{'message'}->@*; } next unless $readcursor & $indicators->{'deliverystatus'}; next unless length $e; @@ -95,7 +96,8 @@ sub inquire { push @$dscontents, __PACKAGE__->DELIVERYSTATUS; $v = $dscontents->[-1]; } - $v->{'recipient'} = Sisimai::Address->s3s4(substr($e, $p1, $p2 - $p1)); + $v->{'recipient'} = Sisimai::Address->s3s4(substr($e, $p1, $p2 - $p1)); + $v->{"diagnosis"} .= " ".$e; $recipients++; } elsif( my $f = Sisimai::RFC1894->match($e) ) { @@ -107,35 +109,33 @@ sub inquire { } else { # The line does not begin with a DSN field defined in RFC3464 next if Sisimai::String->is_8bit(\$e); - if( index($e, ' >>> ') > -1 ) { + if( index($e, " >>> ") > -1 ) { # >>> RCPT TO:<******@ezweb.ne.jp> - $v->{'command'} = Sisimai::SMTP::Command->find($e) || ''; + $v->{"command"} = Sisimai::SMTP::Command->find($e); + $v->{"diagnosis"} .= " ".$e; + + } elsif( index($e, " <<< ") > -1 ) { + # <<< 550 ... + $v->{"diagnosis"} .= " ".$e; } else { # Check error message - if( grep { index($e, $_) > -1 } @rxmessages ) { - # Check with regular expressions of each error - $v->{'diagnosis'} .= ' '.$e; - } else { - # >>> 550 - $v->{'alterrors'} .= ' '.$e; + my $isincluded = 0; + if( grep { index($e, $_) > -1 } @$substrings ) { + # Try to find that the line contains any error message text + $v->{"diagnosis"} .= ' '.$e; + $isincluded = 1; } + $v->{"diagnosis"} .= " ".$e if $isincluded == 0; } } # End of error message part } return undef unless $recipients; for my $e ( @$dscontents ) { - if( exists $e->{'alterrors'} && $e->{'alterrors'} ) { - # Copy alternative error message - $e->{'diagnosis'} ||= $e->{'alterrors'}; - if( index($e->{'diagnosis'}, '-') == 0 || substr($e->{'diagnosis'}, -2, 2) eq '__' ) { - # Override the value of diagnostic code message - $e->{'diagnosis'} = $e->{'alterrors'} if $e->{'alterrors'}; - } - delete $e->{'alterrors'}; - } + # Check each value of DeliveryMatter{}, try to detect the bounce reason. $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); + $e->{"command"} ||= Sisimai::SMTP::Command->find($e->{"diagnosis"}) || ""; if( defined $mhead->{'x-spasign'} && $mhead->{'x-spasign'} eq 'NG' ) { # Content-Type: text/plain; ..., X-SPASIGN: NG (spamghetti, au by EZweb) @@ -143,26 +143,20 @@ sub inquire { $e->{'reason'} = 'filtered'; } else { - if( $e->{'command'} eq 'RCPT' ) { - # set "userunknown" when the remote server rejected after RCPT command. - $e->{'reason'} = 'userunknown'; - - } else { - # SMTP command is not RCPT - SESSION: for my $r ( keys %$refailures ) { - # Try to match with each session error message - PATTERN: for my $rr ( $refailures->{ $r }->@* ) { - # Check each error message pattern - next(PATTERN) unless index($e->{'diagnosis'}, $rr) > -1; - $e->{'reason'} = $r; - last(SESSION); - } + # There is no X-SPASIGN header or the value of the header is not "NG" + FINDREASON: for my $r ( keys %$messagesof ) { + # Try to match with each session error message + for my $f ( $messagesof->{ $r }->@* ) { + # Check each error message pattern + next unless index($e->{'diagnosis'}, $f) > -1; + $e->{'reason'} = $r; + last FINDREASON; } } } next if $e->{'reason'}; next if index($e->{'recipient'}, '@ezweb.ne.jp') > 1 || index($e->{'recipient'}, '@au.com') > 1; - $e->{'reason'} = 'userunknown'; + $e->{"reason"} = "userunknown" if index($e->{"diagnosis"}, "<") == 0; } return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; } diff --git a/lib/Sisimai/Lhost/Exchange2007.pm b/lib/Sisimai/Lhost/Exchange2007.pm index 32da3e867..0d16bdd86 100644 --- a/lib/Sisimai/Lhost/Exchange2007.pm +++ b/lib/Sisimai/Lhost/Exchange2007.pm @@ -15,65 +15,69 @@ sub inquire { my $class = shift; my $mhead = shift // return undef; my $mbody = shift // return undef; - my $match = 0; - # Content-Language: en-US, fr-FR - $match ||= 1 if index($mhead->{'subject'}, 'Undeliverable') == 0; - $match ||= 1 if index($mhead->{'subject'}, 'Non_remis_') == 0; - $match ||= 1 if index($mhead->{'subject'}, 'Non recapitabile') == 0; - return undef unless $match > 0; - - return undef unless defined $mhead->{'content-language'}; - $match += 1 if length $mhead->{'content-language'} == 2; # JP - $match += 1 if length $mhead->{'content-language'} == 5; # ja-JP - return undef unless $match > 1; + my $proceedsto = 0; + my $emailtitle = [ + # "Subject:" "Content-Language:" + "Undeliverable", # en-US + "Non_remis_", # fr-FR + "Non remis ", # fr-FR + "Non recapitabile", # it-CH + "Olevererbart", # sv-SE + ]; + my $mailsender = ['postmaster@outlook.com', ".onmicrosoft.com"]; - # These headers exist only a bounce mail from Office365 - return undef if $mhead->{'x-ms-exchange-crosstenant-originalarrivaltime'}; - return undef if $mhead->{'x-ms-exchange-crosstenant-fromentityheader'}; + $proceedsto++ if grep { index($mhead->{"subject"}, $_) > -1 } @$emailtitle; + $proceedsto++ if grep { index($mhead->{"from"}, $_) > 1 } @$mailsender; + $proceedsto++ if defined $mhead->{"content-language"}; + return undef if $proceedsto < 2; + require Sisimai::RFC1123; state $indicators = __PACKAGE__->INDICATORS; state $boundaries = [ - 'Original message headers:', # en-US + "Original Message Headers", + "Original message headers:", # en-US "tes de message d'origine :", # fr-FR/En-têtes de message d'origine - 'Intestazioni originali del messaggio:', # it-CH + "Intestazioni originali del messaggio:", # it-CH + "Ursprungshuvuden:", # sv-SE ]; - state $markingsof = { - 'message' => [ - 'Diagnostic information for administrators:', # en-US - 'Informations de diagnostic pour les administrateurs', # fr-FR - 'Informazioni di diagnostica per gli amministratori', # it-CH + state $startingof = { + "error" => [" RESOLVER.", " QUEUE."], + "message" => [ + "Error Details", + "Diagnostic information for administrators:", # en-US + "Informations de diagnostic pour les administrateurs", # fr-FR + "Informazioni di diagnostica per gli amministratori", # it-CH + "Diagnostisk information f", # sv-SE ], - 'error' => [' RESOLVER.', ' QUEUE.'], - 'rhost' => [ - 'Generating server', # en-US - 'Serveur de g', # fr-FR/Serveur de g辿n辿ration - 'Server di generazione', # it-CH + "rhost" => [ + "DSN generated by:", + "Generating server", # en-US + "Serveur de g", # fr-FR/Serveur de g辿n辿ration + "Server di generazione", # it-CH + "Genererande server", # sv-SE ], }; state $ndrsubject = { - 'SMTPSEND.DNS.NonExistentDomain'=> 'hostunknown', # 554 5.4.4 SMTPSEND.DNS.NonExistentDomain - 'SMTPSEND.DNS.MxLoopback' => 'networkerror', # 554 5.4.4 SMTPSEND.DNS.MxLoopback - 'RESOLVER.ADR.BadPrimary' => 'systemerror', # 550 5.2.0 RESOLVER.ADR.BadPrimary - 'RESOLVER.ADR.RecipNotFound' => 'userunknown', # 550 5.1.1 RESOLVER.ADR.RecipNotFound - 'RESOLVER.ADR.ExRecipNotFound' => 'userunknown', # 550 5.1.1 RESOLVER.ADR.ExRecipNotFound - 'RESOLVER.ADR.RecipLimit' => 'toomanyconn', # 550 5.5.3 RESOLVER.ADR.RecipLimit - 'RESOLVER.ADR.InvalidInSmtp' => 'systemerror', # 550 5.1.0 RESOLVER.ADR.InvalidInSmtp - 'RESOLVER.ADR.Ambiguous' => 'systemerror', # 550 5.1.4 RESOLVER.ADR.Ambiguous, 420 4.2.0 RESOLVER.ADR.Ambiguous - 'RESOLVER.RST.AuthRequired' => 'securityerror', # 550 5.7.1 RESOLVER.RST.AuthRequired - 'RESOLVER.RST.NotAuthorized' => 'rejected', # 550 5.7.1 RESOLVER.RST.NotAuthorized - 'RESOLVER.RST.RecipSizeLimit' => 'mesgtoobig', # 550 5.2.3 RESOLVER.RST.RecipSizeLimit - 'QUEUE.Expired' => 'expired', # 550 4.4.7 QUEUE.Expired + "SMTPSEND.DNS.NonExistentDomain" => "hostunknown", # 554 5.4.4 SMTPSEND.DNS.NonExistentDomain + "SMTPSEND.DNS.MxLoopback" => "networkerror", # 554 5.4.4 SMTPSEND.DNS.MxLoopback + "RESOLVER.ADR.BadPrimary" => "systemerror", # 550 5.2.0 RESOLVER.ADR.BadPrimary + "RESOLVER.ADR.RecipNotFound" => "userunknown", # 550 5.1.1 RESOLVER.ADR.RecipNotFound + "RESOLVER.ADR.RecipientNotFound" => "userunknown", # 550 5.1.1 RESOLVER.ADR.RecipientNotFound + "RESOLVER.ADR.ExRecipNotFound" => "userunknown", # 550 5.1.1 RESOLVER.ADR.ExRecipNotFound + "RESOLVER.ADR.RecipLimit" => "toomanyconn", # 550 5.5.3 RESOLVER.ADR.RecipLimit + "RESOLVER.ADR.InvalidInSmtp" => "systemerror", # 550 5.1.0 RESOLVER.ADR.InvalidInSmtp + "RESOLVER.ADR.Ambiguous" => "systemerror", # 550 5.1.4 RESOLVER.ADR.Ambiguous, 420 4.2.0 RESOLVER.ADR.Ambiguous + "RESOLVER.RST.AuthRequired" => "securityerror", # 550 5.7.1 RESOLVER.RST.AuthRequired + "RESOLVER.RST.NotAuthorized" => "rejected", # 550 5.7.1 RESOLVER.RST.NotAuthorized + "RESOLVER.RST.RecipSizeLimit" => "exceedlimit", # 550 5.2.3 RESOLVER.RST.RecipSizeLimit + "QUEUE.Expired" => "expired", # 550 4.4.7 QUEUE.Expired }; my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); my $readcursor = 0; # (Integer) Points the current cursor position my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $connvalues = 0; # (Integer) Flag, 1 if all the value of $connheader have been set - my $connheader = { - 'rhost' => '', # The value of Reporting-MTA header or "Generating Server:" - }; my $v = undef; for my $e ( split("\n", $emailparts->[0]) ) { @@ -81,80 +85,91 @@ sub inquire { # line of the beginning of the original message. unless( $readcursor ) { # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if grep { index($e, $_) == 0 } $markingsof->{'message'}->@*; + $readcursor |= $indicators->{"deliverystatus"} if grep { index($e, $_) == 0 } $startingof->{"message"}->@*; next; } - next unless $readcursor & $indicators->{'deliverystatus'}; + next unless $readcursor & $indicators->{"deliverystatus"}; + next unless length $e; + + # Diagnostic information for administrators: + # + # Generating server: mta2.neko.example.jp + # + # kijitora@example.jp + # #550 5.1.1 RESOLVER.ADR.RecipNotFound; not found ## + # + # Original message headers: + $v = $dscontents->[-1]; - if( $connvalues == scalar(keys %$connheader) ) { - # Diagnostic information for administrators: - # - # Generating server: mta2.neko.example.jp - # + if( index($e, " ") < 0 && index($e, '@') > 1 ) { # kijitora@example.jp - # #550 5.1.1 RESOLVER.ADR.RecipNotFound; not found ## - # - # Original message headers: - $v = $dscontents->[-1]; - - if( index($e, ' ') < 0 && index($e, '@') > 1 ) { - # kijitora@example.jp - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = Sisimai::Address->s3s4($e); - $recipients++; + if( $v->{"recipient"} ) { + # There are multiple recipient addresses in the message body. + push @$dscontents, __PACKAGE__->DELIVERYSTATUS; + $v = $dscontents->[-1]; + } + $v->{"recipient"} = Sisimai::Address->s3s4($e); + $recipients++; + + } else { + # Try to pick the remote hostname and status code, reply code from the error message + if( grep { index($e, $_) == 0 } $startingof->{"rhost"}->@* ) { + # Generating server: SG2APC01HT234.mail.protection.outlook.com + # DSN generated by: NEKONYAAN0022.apcprd01.prod.exchangelabs.com + my $cv = Sisimai::RFC1123->find($e); + $v->{"rhost"} = $cv if Sisimai::RFC1123->is_internethost($cv); } else { - my $cr = Sisimai::SMTP::Reply->find($e) || ''; - my $cs = Sisimai::SMTP::Status->find($e) || ''; - if( $cr || $cs ) { - # #550 5.1.1 RESOLVER.ADR.RecipNotFound; not found ## - # #550 5.2.3 RESOLVER.RST.RecipSizeLimit; message too large for this recipient ## + # #550 5.1.1 RESOLVER.ADR.RecipNotFound; not found ## + # #550 5.2.3 RESOLVER.RST.RecipSizeLimit; message too large for this recipient ## + my $cr = Sisimai::SMTP::Reply->find($e) || ""; + my $cs = Sisimai::SMTP::Status->find($e) || ""; + if( $cr ne "" || $cs ne "" || index($e, "Remote Server ") > -1 ) { # Remote Server returned '550 5.1.1 RESOLVER.ADR.RecipNotFound; not found' # 3/09/2016 8:05:56 PM - Remote Server at mydomain.com (10.1.1.3) returned '550 4.4.7 QUEUE.Expired; message expired' - $v->{'replycode'} = $cr; - $v->{'status'} = $cs; - $v->{'diagnosis'} = $e; - - } else { - # Continued line of error messages - next unless $v->{'diagnosis'}; - next unless substr($v->{'diagnosis'}, -1, 1) eq '='; - substr($v->{'diagnosis'}, -1, 1, $e); + $v->{"replycode"} = $cr; + $v->{"status"} = $cs; + $v->{"diagnosis"} .= $e." "; } } - } else { - # Diagnostic information for administrators: - # - # Generating server: mta22.neko.example.org - next unless grep { index($e, $_) == 0 } $markingsof->{'rhost'}->@*; - next if $connheader->{'rhost'}; - $connheader->{'rhost'} = substr($e, index($e, ':') + 1,); - $connvalues++; } } + + while( $recipients == 0 ) { + # Try to pick the recipient address from the following formatted bounce message: + # + # Original Message Details + # Created Date: 4/29/2017 11:23:34 PM + # Sender Address: neko@example.com + # Recipient Address: kijitora-nyaan@neko.kyoto.example.jp + # Subject: Nyaan? + my $p1 = index($emailparts->[0], "Original Message Details"); last if $p1 < 0; + my $p2 = index($emailparts->[0], "\nRecipient Address: "); last if $p2 < 0; + my $p3 = index($emailparts->[0], "\n", $p2 + 20); last if $p3 < 0; + my $cv = Sisimai::Address->s3s4(substr($emailparts->[0], $p2 + 20, $p3 - $p2 - 20)); + + last unless Sisimai::Address->is_emailaddress($cv); + $dscontents->[0]->{"recipient"} = $cv; + $recipients++; + } return undef unless $recipients; for my $e ( @$dscontents ) { - my $p = -1; + # Tidy up the error message in $e->{'diagnosis'}, Try to detect the bounce reason. + $e->{"diagnosis"} = Sisimai::String->sweep($e->{"diagnosis"}); - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); - for my $q ( $markingsof->{'error'}->@* ) { - # Find an error message, get an error code - $p = index($e->{'diagnosis'}, $q); - last if $p > -1; + my $p0 = -1; for my $r ( $startingof->{"error"}->@* ) { + # Try to find the NDR subject string such as "RESOLVER.ADR.RecipientNotFound" from the + # error message + $p0 = index($e->{"diagnosis"}, $r); last if $p0 > -1; } - next unless $p > 0; + next if $p0 < 0; - # #550 5.1.1 RESOLVER.ADR.RecipNotFound; not found ## - my $f = substr($e->{'diagnosis'}, $p + 1, index($e->{'diagnosis'}, ';') - $p - 1); + my $cv = substr($e->{"diagnosis"}, $p0 + 1, index($e->{"diagnosis"}, ";") - $p0 - 1); for my $r ( keys %$ndrsubject ) { - # Try to match with error subject strings - next unless $f eq $r; - $e->{'reason'} = $ndrsubject->{ $r }; + # Try to match with error subject strings such as "RESOLVER.ADR.RecipNotFound" + next unless $cv eq $r; + $e->{"reason"} = $ndrsubject->{ $r }; last; } } diff --git a/lib/Sisimai/Lhost/Exim.pm b/lib/Sisimai/Lhost/Exim.pm index ee4afdb01..7e7a28bc0 100644 --- a/lib/Sisimai/Lhost/Exim.pm +++ b/lib/Sisimai/Lhost/Exim.pm @@ -15,22 +15,50 @@ sub inquire { my $class = shift; my $mhead = shift // return undef; my $mbody = shift // return undef; - return undef if index($mhead->{'from'}, '.mail.ru') > 0; # Message-Id: # X-Failed-Recipients: kijitora@example.ed.jp - my $match = 0; - my $msgid = $mhead->{'message-id'} || ''; - $match++ if index($mhead->{'from'}, 'Mail Delivery System') == 0; - $match++ if index($msgid, '<') == 0 && index($msgid, '-') == 8 && index($msgid, '@') == 18; - $match++ if grep { index($mhead->{'subject'}, $_) > -1 } ( 'Delivery Status Notification', - 'Mail delivery failed', - 'Mail failure', - 'Message frozen', - 'Warning: message ', - 'error(s) in forwarding or filtering'); - return undef if $match < 2; + my $thirdparty = 0; + my $proceedsto = 0; + my $messageidv = $mhead->{"message-id"} || ""; + my $emailtitle = [ + "Delivery Status Notification", + "Mail delivery failed", + "Mail failure", + "Message frozen", + "Warning: message ", + "error(s) in forwarding or filtering", + ]; + $proceedsto++ if index($mhead->{"from"}, "Mail Delivery System") > -1; + + while( $messageidv ne "" ) { + # Message-Id: + last if index($messageidv, '<') != 0; + last if index($messageidv, '-') != 8; + last if index($messageidv, '@') != 18; + $proceedsto++; last; + } + for my $e ( @$emailtitle ) { + # Subject: Mail delivery failed: returning message to sender + # Subject: Mail delivery failed + # Subject: Message frozen + next if index($mhead->{"subject"}, $e) < 0; + $proceedsto++; last; + } + + while(1) { + # Exim clones of the third Parties + # 1. McAfee Saas (Formerly MXLogic) + if( exists $mhead->{"x-mx-bounce"} ) { $thirdparty = 1; last; } + if( exists $mhead->{"x-mxl-hash"} ) { $thirdparty = 1; last; } + if( exists $mhead->{"x-mxl-notehash"} ) { $thirdparty = 1; last; } + if( index($messageidv, " -1 ) { $thirdparty = 1; last; } + last; + } + return undef if $proceedsto < 2 && $thirdparty == 0; + require Sisimai::Address; + require Sisimai::SMTP::Command; state $indicators = __PACKAGE__->INDICATORS; state $boundaries = [ # deliver.c:6423| if (bounce_return_body) fprintf(f, @@ -39,6 +67,7 @@ sub inquire { # deliver.c:6426|"------ This is a copy of the message's headers. ------\n"); '------ This is a copy of the message, including all the headers. ------', 'Content-Type: message/rfc822', + "Included is a copy of the message header:\n-----------------------------------------", # MXLogic ]; state $startingof = { # Error text strings which defined in exim/src/deliver.c @@ -58,73 +87,66 @@ sub inquire { # deliver.c:6304|"could not be delivered to one or more of its recipients. The following\n" # deliver.c:6305|"address(es) failed:\n", sender_address); # deliver.c:6306| } - 'deliverystatus' => ['Content-Type: message/delivery-status'], - 'frozen' => [' has been frozen', ' was frozen on arrival'], + "alias" => [" an undisclosed address"], + "command" => ["SMTP error from remote ", "LMTP error after "], + 'deliverystatus' => ["Content-Type: message/delivery-status"], + 'frozen' => [" has been frozen", " was frozen on arrival"], 'message' => [ - 'This message was created automatically by mail delivery software.', - 'A message that you sent was rejected by the local scannning code', - 'A message that you sent contained one or more recipient addresses ', - 'A message that you sent could not be delivered to all of its recipients', - ' has been frozen', - ' was frozen on arrival', - ' router encountered the following error(s):', + "This message was created automatically by mail delivery software.", + "A message that you sent was rejected by the local scannning code", + "A message that you sent contained one or more recipient addresses ", + "A message that you sent could not be delivered to all of its recipients", + " has been frozen", + " was frozen on arrival", + " router encountered the following error(s):", ], }; - state $markingsof = { 'alias' => ' an undisclosed address' }; - state $recommands = [ - # transports/smtp.c:564| *message = US string_sprintf("SMTP error from remote mail server after %s%s: " - # transports/smtp.c:837| string_sprintf("SMTP error from remote mail server after RCPT TO:<%s>: " - qr/SMTP error from remote (?:mail server|mailer) after ([A-Za-z]{4})/, - qr/SMTP error from remote (?:mail server|mailer) after end of ([A-Za-z]{4})/, - qr/LMTP error after ([A-Za-z]{4})/, - qr/LMTP error after end of ([A-Za-z]{4})/, - ]; state $messagesof = { # find exim/ -type f -exec grep 'message = US' {} /dev/null \; # route.c:1158| DEBUG(D_uid) debug_printf("getpwnam() returned NULL (user not found)\n"); - 'userunknown' => ['user not found'], + "userunknown" => ["user not found"], # transports/smtp.c:3524| addr->message = US"all host address lookups failed permanently"; # routers/dnslookup.c:331| addr->message = US"all relevant MX records point to non-existent hosts"; # route.c:1826| uschar *message = US"Unrouteable address"; - 'hostunknown' => [ - 'all host address lookups failed permanently', - 'all relevant MX records point to non-existent hosts', - 'Unrouteable address', + "hostunknown" => [ + "all host address lookups failed permanently", + "all relevant MX records point to non-existent hosts", + "Unrouteable address", ], # transports/appendfile.c:2567| addr->user_message = US"mailbox is full"; # transports/appendfile.c:3049| addr->message = string_sprintf("mailbox is full " # transports/appendfile.c:3050| "(quota exceeded while writing to file %s)", filename); - 'mailboxfull' => [ - 'mailbox is full', - 'error: quota exceed', + "mailboxfull" => [ + "mailbox is full", + "error: quota exceed", ], # routers/dnslookup.c:328| addr->message = US"an MX or SRV record indicated no SMTP service"; # transports/smtp.c:3502| addr->message = US"no host found for existing SMTP connection"; - 'notaccept' => [ - 'an MX or SRV record indicated no SMTP service', - 'no host found for existing SMTP connection', + "notaccept" => [ + "an MX or SRV record indicated no SMTP service", + "no host found for existing SMTP connection", ], # parser.c:666| *errorptr = string_sprintf("%s (expected word or \"<\")", *errorptr); # parser.c:701| if(bracket_count++ > 5) FAILED(US"angle-brackets nested too deep"); # parser.c:738| FAILED(US"domain missing in source-routed address"); # parser.c:747| : string_sprintf("malformed address: %.32s may not follow %.*s", - 'syntaxerror' => [ - 'angle-brackets nested too deep', + "syntaxerror" => [ + "angle-brackets nested too deep", 'expected word or "<"', - 'domain missing in source-routed address', - 'malformed address:', + "domain missing in source-routed address", + "malformed address:", ], # deliver.c:5614| addr->message = US"delivery to file forbidden"; # deliver.c:5624| addr->message = US"delivery to pipe forbidden"; # transports/pipe.c:1156| addr->user_message = US"local delivery failed"; - 'systemerror' => [ - 'delivery to file forbidden', - 'delivery to pipe forbidden', - 'local delivery failed', - 'LMTP error after ', + "systemerror" => [ + "delivery to file forbidden", + "delivery to pipe forbidden", + "local delivery failed", + "LMTP error after ", ], # deliver.c:5425| new->message = US"Too many \"Received\" headers - suspected mail loop"; - 'contenterror' => ['Too many "Received" headers'], + "contenterror" => ['Too many "Received" headers'], }; state $delayedfor = [ # retry.c:902| addr->message = (addr->message == NULL)? US"retry timeout exceeded" : @@ -136,22 +158,30 @@ sub inquire { # deliver.c:7586| "Message %s has been frozen%s.\nThe sender is <%s>.\n", message_id, # receive.c:4021| moan_tell_someone(freeze_tell, NULL, US"Message frozen on arrival", # receive.c:4022| "Message %s was frozen on arrival by %s.\nThe sender is <%s>.\n", - 'retry timeout exceeded', - 'No action is required on your part', - 'retry time not reached for any host after a long failure period', - 'all hosts have been failing for a long time and were last tried', - 'Delay reason: ', - 'has been frozen', - 'was frozen on arrival by ', + "retry timeout exceeded", + "No action is required on your part", + "retry time not reached for any host after a long failure period", + "all hosts have been failing for a long time and were last tried", + "Delay reason: ", + "has been frozen", + "was frozen on arrival by ", ]; + if( index($$mbody, "\n----- This is a copy ") > -1 ) { + # There are extremely rare cases where there are only five hyphens. + # https://github.com/sisimai/set-of-emails/blob/master/maildir/bsd/lhost-exim-05.eml + # ----- This is a copy of the message, including all the headers. ------ + my $p0 = index($$mbody, "\n----- This is a copy "); + substr($$mbody, $p0 + 1, 1, "--"); + } + my $fieldtable = Sisimai::RFC1894->FIELDTABLE; my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position + my $readcursor = 0; # Points the current cursor position my $nextcursor = 0; - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $boundary00 = ''; # (String) Boundary string + my $recipients = 0; # The number of 'Final-Recipient' header + my $boundary00 = ''; # Boundary string my $v = undef; if( $mhead->{'content-type'} ) { @@ -166,6 +196,7 @@ sub inquire { unless( $readcursor ) { # Beginning of the bounce message or message/delivery-status part if( grep { index($e, $_) > -1 } $startingof->{'message'}->@* ) { + # Check the message defined in $startingof->{"message"}, {"frozen"} $readcursor |= $indicators->{'deliverystatus'}; next unless grep { index($e, $_) > -1 } $startingof->{'frozen'}->@*; } @@ -183,30 +214,31 @@ sub inquire { # host neko.example.jp [192.0.2.222]: 550 5.1.1 ... User Unknown $v = $dscontents->[-1]; - my $cv = ''; + my $cv = ""; my $ce = 0; while(1) { # Check if the line matche the following patterns: - last unless index($e, ' ') == 0; # The line should start with " " (2 spaces) - last unless index($e, '@' ) > 1; # "@" should be included (email) - last unless index($e, '.' ) > 1; # "." should be included (domain part) - last unless index($e, 'pipe to |') == -1; # Exclude "pipe to /path/to/prog" line + last if index($e, ' ') != 0; # The line should start with " " (2 spaces) + last if index($e, '@' ) < 2; # "@" should be included (email) + last if index($e, '.' ) < 2; # "." should be included (domain part) + last if index($e, 'pipe to |') > -1; # Exclude "pipe to /path/to/prog" line my $cx = substr($e, 2, 1); - last unless $cx ne ' '; - last unless $cx ne '<'; + last if $cx eq " "; # The 3rd character is " " + last if $thirdparty == 0 && $cx eq "<"; # MXLogic returns " :..." $ce = 1; last; } - if( $ce == 1 || index($e, $markingsof->{'alias'}) > 0 ) { + if( $ce == 1 || grep { index($e, $_) > 0 } $startingof->{"alias"}->@* ) { # The line is including an email address if( $v->{'recipient'} ) { + # There are multiple recipient addresses in the message body. push @$dscontents, __PACKAGE__->DELIVERYSTATUS; $v = $dscontents->[-1]; } - if( index($e, $markingsof->{'alias'}) > 0 ) { + if( grep { index($e, $_) > 0 } $startingof->{"alias"}->@* ) { # The line does not include an email address # deliver.c:4549| printed = US"an undisclosed address"; # an undisclosed address @@ -217,8 +249,8 @@ sub inquire { # kijitora@example.jp # sabineko@example.jp: forced freeze # mikeneko@example.jp : ... - $p1 = index($e, ' <'); - $p2 = index($e, '>:'); + $p1 = index($e, "<"); + $p2 = index($e, ">:"); if( $p1 > 1 && $p2 > 1 ) { # There are an email address and an error message in the line @@ -230,7 +262,7 @@ sub inquire { # parser.c:748| s-1, (int)(s - US mailbox - 1), mailbox); # parser.c:749| goto PARSE_FAILED; # parser.c:750| } - $cv = Sisimai::Address->s3s4(substr($e, $p1 + 1, $p2 - $p1 - 1)); + $cv = Sisimai::Address->s3s4(substr($e, $p1, $p2 - $p1 - 1)); $v->{'diagnosis'} = Sisimai::String->sweep(substr($e, $p2 + 1,)); } else { @@ -238,19 +270,21 @@ sub inquire { # kijitora@example.jp $cv = Sisimai::Address->s3s4(substr($e, 2,)); } + next unless Sisimai::Address->is_emailaddress($cv); } $v->{'recipient'} = $cv; $recipients++; - } elsif( index($e, ' (generated from ') > 0 || index($e, ' generated by ') > 0 ) { + } elsif( index($e, " (generated from ") > 0 || index($e, " generated by ") > 0 ) { # (generated from kijitora@example.jp) # pipe to |/bin/echo "Some pipe output" # generated by userx@myhost.test.ex - $v->{'alias'} = Sisimai::Address->s3s4(substr($e, rindex($e, ' ') + 1,)); - + for my $f ( split(" ", $e) ) { + # Find the alias address + next if index($f, '@') < 0; + $v->{'alias'} = Sisimai::Address->s3s4($f); + } } else { - next unless length $e; - if( grep { index($e, $_) > -1 } $startingof->{'frozen'}->@* ) { # Message *** has been frozen by the system filter. # Message *** was frozen on arrival by ACL. @@ -264,12 +298,12 @@ sub inquire { # $e matched with any field defined in RFC3464 next unless my $o = Sisimai::RFC1894->field($e); - if( $o->[-1] eq 'addr' ) { + if( $o->[3] eq 'addr' ) { # Final-Recipient: rfc822;|/bin/echo "Some pipe output" next unless $o->[0] eq 'final-recipient'; $v->{'spec'} ||= rindex($o->[2], '@') > -1 ? 'SMTP' : 'X-UNIX'; - } elsif( $o->[-1] eq 'code' ) { + } elsif( $o->[3] eq 'code' ) { # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown $v->{'spec'} = uc $o->[1]; $v->{'diagnosis'} = $o->[2]; @@ -282,6 +316,7 @@ sub inquire { } else { # Error message ? next if $nextcursor; + # Content-type: message/delivery-status $nextcursor = 1 if index($e, $startingof->{'deliverystatus'}->[0]) == 0; $v->{'alterrors'} .= $e.' ' if index($e, ' ') == 0; @@ -290,20 +325,14 @@ sub inquire { # There is no boundary string in $boundary00 if( scalar @$dscontents == $recipients ) { # Error message - next unless length $e; $v->{'diagnosis'} .= $e.' '; } else { # Error message when email address above does not include '@' and domain part. - if( index($e, ' pipe to |/') > -1 ) { - # pipe to |/path/to/prog ... - # generated by kijitora@example.com - $v->{'diagnosis'} = $e; - - } else { - next unless index($e, ' ') == 0; - $v->{'alterrors'} .= $e.' '; - } + # pipe to |/path/to/prog ... + # generated by kijitora@example.com + next unless index($e, " ") == 0; + $v->{"diagnosis"} .= $e." "; } } } @@ -344,6 +373,7 @@ sub inquire { for my $e ( @$dscontents ) { # Check the error message, the rhost, the lhost, and the smtp command. + $e->{"alterrors"} ||= ""; if( ! $e->{'diagnosis'} && length($boundary00) > 0 ) { # Empty Diagnostic-Code: or error message # --NNNNNNNNNN-eximdsn-MMMMMMMMMM @@ -360,14 +390,10 @@ sub inquire { # Status: 5.0.0 $e->{'diagnosis'} = $dscontents->[0]->{'diagnosis'} || ''; $e->{'spec'} ||= $dscontents->[0]->{'spec'}; - - if( $dscontents->[0]->{'alterrors'} ) { - # The value of "alterrors" is also copied - $e->{'alterrors'} = $dscontents->[0]->{'alterrors'}; - } + $e->{'alterrors'} = $dscontents->[0]->{'alterrors'} if $dscontents->[0]->{'alterrors'}; } - if( exists $e->{'alterrors'} && $e->{'alterrors'} ) { + if( $e->{'alterrors'} ) { # Copy alternative error message $e->{'diagnosis'} ||= $e->{'alterrors'}; @@ -384,7 +410,7 @@ sub inquire { delete $e->{'alterrors'}; } $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); $p1 = index($e->{'diagnosis'}, '__'); - $e->{'diagnosis'} = substr($e->{'diagnosis'}, 0, $p1) if $p1 > 1; + $e->{'diagnosis'} = substr($e->{'diagnosis'}, 0, $p1) if $p1 > 1; unless( $e->{'rhost'} ) { # Get the remote host name @@ -398,10 +424,10 @@ sub inquire { unless( $e->{'command'} ) { # Get the SMTP command name for the session - SMTP: for my $r ( @$recommands ) { + SMTP: for my $r ( $startingof->{"command"}->@* ) { # Verify each regular expression of SMTP commands - next unless $e->{'diagnosis'} =~ $r; - $e->{'command'} = uc $1; + next if index($e->{'diagnosis'}, $r) < 0; + $e->{'command'} = Sisimai::SMTP::Command->find($e->{'diagnosis'}) || next; last; } @@ -415,9 +441,9 @@ sub inquire { $e->{'reason'} = 'onhold'; } else { - # Verify each regular expression of session errors + # Try to match the error message with each message pattern SESSION: for my $r ( keys %$messagesof ) { - # Check each regular expression + # Check each message pattern next unless grep { index($e->{'diagnosis'}, $_) > -1 } $messagesof->{ $r }->@*; $e->{'reason'} = $r; last; diff --git a/lib/Sisimai/Lhost/Facebook.pm b/lib/Sisimai/Lhost/Facebook.pm deleted file mode 100644 index dadb1765a..000000000 --- a/lib/Sisimai/Lhost/Facebook.pm +++ /dev/null @@ -1,235 +0,0 @@ -package Sisimai::Lhost::Facebook; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -sub description { 'Facebook: https://www.facebook.com' } -sub inquire { - # Detect an error from Facebook - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decodes or the arguments are missing - # @since v4.0.0 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - - return undef unless $mhead->{'from'} eq 'Facebook '; - return undef unless $mhead->{'subject'} eq 'Sorry, your message could not be delivered'; - - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['Content-Disposition: inline']; - state $startingof = { 'message' => ['This message was created automatically by Facebook.'] }; - state $errorcodes = { - # http://postmaster.facebook.com/response_codes - # NOT TESTD EXCEPT RCP-P2 - 'userunknown' => [ - 'RCP-P1', # The attempted recipient address does not exist. - 'INT-P1', # The attempted recipient address does not exist. - 'INT-P3', # The attempted recpient group address does not exist. - 'INT-P4', # The attempted recipient address does not exist. - ], - 'filtered' => [ - 'RCP-P2', # The attempted recipient's preferences prevent messages from being delivered. - 'RCP-P3', # The attempted recipient's privacy settings blocked the delivery. - ], - 'blocked' => [ - 'POL-P1', # Your mail server's IP Address is listed on the Spamhaus PBL. - 'POL-P2', # Facebook will no longer accept mail from your mail server's IP Address. - ], - 'mesgtoobig' => [ - 'MSG-P1', # The message exceeds Facebook's maximum allowed size. - 'INT-P2', # The message exceeds Facebook's maximum allowed size. - ], - 'contenterror' => [ - 'MSG-P2', # The message contains an attachment type that Facebook does not accept. - 'MSG-P3', # The message contains multiple instances of a header field that can only be present once. - 'POL-P6', # The message contains a url that has been blocked by Facebook. - 'POL-P7', # The message does not comply with Facebook's abuse policies and will not be accepted. - ], - 'securityerror' => [ - 'POL-P7', # The message does not comply with Facebook's Domain Authentication requirements. - ], - 'notaccept' => [ - 'POL-P3', # Facebook is not accepting messages from your mail server. This will persist for 4 to 8 hours. - 'POL-P4', # Facebook is not accepting messages from your mail server. This will persist for 24 to 48 hours. - 'POL-T1', # Facebook is not accepting messages from your mail server, but they may be retried later. This will persist for 1 to 2 hours. - 'POL-T2', # Facebook is not accepting messages from your mail server, but they may be retried later. This will persist for 4 to 8 hours. - 'POL-T3', # Facebook is not accepting messages from your mail server, but they may be retried later. This will persist for 24 to 48 hours. - ], - 'rejected' => [ - 'DNS-P1', # Your SMTP MAIL FROM domain does not exist. - 'DNS-P2', # Your SMTP MAIL FROM domain does not have an MX record. - 'DNS-T1', # Your SMTP MAIL FROM domain exists but does not currently resolve. - 'DNS-P3', # Your mail server does not have a reverse DNS record. - 'DNS-T2', # You mail server's reverse DNS record does not currently resolve. - ], - 'systemerror' => [ - 'CON-T1', # Facebook's mail server currently has too many connections open to allow another one. - 'RCP-T1', # The attempted recipient address is not currently available due to an internal system issue. This is a temporary condition. - ], - 'toomanyconn' => [ - 'CON-T2', # Your mail server currently has too many connections open to Facebook's mail servers. - 'CON-T3', # Your mail server has opened too many new connections to Facebook's mail servers in a short period of time. - ], - 'virusdetected' => [ - 'POL-P5', # The message contains a virus. - ], - 'suspend' => [ - 'RCP-T4', # The attempted recipient address is currently deactivated. The user may or may not reactivate it. - ], - 'undefined' => [ - 'MSG-T1', # The number of recipients on the message exceeds Facebook's allowed maximum. - 'CON-T4', # Your mail server has exceeded the maximum number of recipients for its current connection. - ], - }; - - my $fieldtable = Sisimai::RFC1894->FIELDTABLE; - my $permessage = {}; # (Hash) Store values of each Per-Message field - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $fbresponse = ''; # (String) Response code from Facebook - my $v = undef; - my $p = ''; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0; - next; - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - - if( my $f = Sisimai::RFC1894->match($e) ) { - # $e matched with any field defined in RFC3464 - next unless my $o = Sisimai::RFC1894->field($e); - $v = $dscontents->[-1]; - - if( $o->[-1] eq 'addr' ) { - # Final-Recipient: rfc822; kijitora@example.jp - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - if( $o->[0] eq 'final-recipient' ) { - # Final-Recipient: rfc822; kijitora@example.jp - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = $o->[2]; - $recipients++; - - } else { - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - $v->{'alias'} = $o->[2]; - } - } elsif( $o->[-1] eq 'code' ) { - # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown - $v->{'spec'} = $o->[1]; - $v->{'diagnosis'} = $o->[2]; - - } else { - # Other DSN fields defined in RFC3464 - next unless exists $fieldtable->{ $o->[0] }; - $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - - next unless $f == 1; - $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2]; - } - } else { - # Continued line of the value of Diagnostic-Code field - next unless index($p, 'Diagnostic-Code:') == 0; - next unless index($e, ' ') == 0; - $v->{'diagnosis'} .= ' '.substr($e, rindex($e, ' ') + 1,); - } - } continue { - # Save the current line for the next loop - $p = $e; - } - return undef unless $recipients; - - for my $e ( @$dscontents ) { - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); - - my $p0 = index($e->{'diagnosis'}, '-'); - $fbresponse = substr($e->{'diagnosis'}, $p0 - 3, 6) if $p0 > 0; - - SESSION: for my $r ( keys %$errorcodes ) { - # Verify each regular expression of session errors - PATTERN: for my $rr ( $errorcodes->{ $r }->@* ) { - # Check each regular expression - next(PATTERN) unless $fbresponse eq $rr; - $e->{'reason'} = $r; - last(SESSION); - } - } - next if $e->{'reason'}; - - # http://postmaster.facebook.com/response_codes - # Facebook System Resource Issues - # These codes indicate a temporary issue internal to Facebook's - # system. Administrators observing these issues are not required to - # take any action to correct them. - # - # * INT-Tx - # - # https://groups.google.com/forum/#!topic/cdmix/eXfi4ddgYLQ - # This block has not been tested because we have no email sample - # including "INT-T?" error code. - next unless index($fbresponse, 'INT-T') == 0; - $e->{'reason'} = 'systemerror'; - } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::Facebook - bounce mail decoder class for Facebook L. - -=head1 SYNOPSIS - - use Sisimai::Lhost::Facebook; - -=head1 DESCRIPTION - -C decodes a bounce email which created by Facebook L. -Methods in the module are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::Facebook->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2014-2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/GSuite.pm b/lib/Sisimai/Lhost/GSuite.pm deleted file mode 100644 index aa4b1c528..000000000 --- a/lib/Sisimai/Lhost/GSuite.pm +++ /dev/null @@ -1,237 +0,0 @@ -package Sisimai::Lhost::GSuite; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -sub description { 'Google Workspace: https://workspace.google.com/' } -sub inquire { - # Detect an error from Google Workspace (Transfer from the Google Workspace to the destination host) - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decode or the arguments are missing - # @since v4.21.0 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - - return undef unless rindex($mhead->{'from'}, '') > -1; - return undef unless index($mhead->{'subject'}, 'Delivery Status Notification') > -1; - return undef unless $mhead->{'x-gm-message-state'}; - - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['Content-Type: message/rfc822', 'Content-Type: text/rfc822-headers']; - state $markingsof = { - 'message' => ['** '], - 'error' => ['The response was:', 'The response from the remote server was:'], - }; - state $messagesof = { - 'userunknown' => ["because the address couldn't be found. Check for typos or unnecessary spaces and try again."], - 'notaccept' => ['Null MX'], - 'networkerror' => [' had no relevant answers.', ' responded with code NXDOMAIN'], - }; - - my $fieldtable = Sisimai::RFC1894->FIELDTABLE; - my $permessage = {}; # (Hash) Store values of each Per-Message field - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $endoferror = 0; # (Integer) Flag for a blank line after error messages - my $emptylines = 0; # (Integer) The number of empty lines - my $anotherset = { # (Hash) Another error information - 'diagnosis' => '', - }; - my $v = undef; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if grep { index($e, $_) == 0 } $markingsof->{'message'}->@*; - } - next unless $readcursor & $indicators->{'deliverystatus'}; - - if( my $f = Sisimai::RFC1894->match($e) ) { - # $e matched with any field defined in RFC3464 - next unless my $o = Sisimai::RFC1894->field($e); - $v = $dscontents->[-1]; - - if( $o->[-1] eq 'addr' ) { - # Final-Recipient: rfc822; kijitora@example.jp - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - if( $o->[0] eq 'final-recipient' ) { - # Final-Recipient: rfc822; kijitora@example.jp - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = $o->[2]; - $recipients++; - - } else { - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - $v->{'alias'} = $o->[2]; - } - } elsif( $o->[-1] eq 'code' ) { - # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown - $v->{'spec'} = $o->[1]; - $v->{'diagnosis'} = $o->[2]; - - } else { - # Other DSN fields defined in RFC3464 - next unless exists $fieldtable->{ $o->[0] }; - $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - - if( $fieldtable->{ $o->[0] } eq 'lhost' ) { - # Do not set an email address as a hostname in "lhost" value - $v->{'lhost'} = '' if index($v->{'lhost'}, '@'); - } - - next unless $f == 1; - $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2]; - } - } else { - # The line does not begin with a DSN field defined in RFC3464 - if( ! $endoferror && $v->{'diagnosis'} ) { - # Append error messages continued from the previous line - $endoferror ||= 1 if $e eq ''; - next if $endoferror; - $v->{'diagnosis'} .= $e; - - } elsif( grep { index($e, $_) == 0 } $markingsof->{'error'}->@* ) { - # The response from the remote server was: - $anotherset->{'diagnosis'} .= ' '.$e; - - } else { - # ** Address not found ** - # - # Your message wasn't delivered to * because the address couldn't be found. - # Check for typos or unnecessary spaces and try again. - # - # The response from the remote server was: - # 550 #5.1.0 Address rejected. - next if index($e, 'Content-Type:') == 0; - if( $anotherset->{'diagnosis'} ) { - # Continued error messages from the previous line like "550 #5.1.0 Address rejected." - next if $emptylines > 5; - unless( length $e ) { - # Count and next() - $emptylines += 1; - next; - } - $anotherset->{'diagnosis'} .= ' '.$e - - } else { - # ** Address not found ** - # - # Your message wasn't delivered to * because the address couldn't be found. - # Check for typos or unnecessary spaces and try again. - next unless grep { index($e, $_) == 0 } $markingsof->{'message'}->@*; - $anotherset->{'diagnosis'} = $e; - } - } - } # End of message/delivery-status - } - return undef unless $recipients; - - for my $e ( @$dscontents ) { - # Set default values if each value is empty. - $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage; - - if( exists $anotherset->{'diagnosis'} && $anotherset->{'diagnosis'} ) { - # Copy alternative error message - $e->{'diagnosis'} ||= $anotherset->{'diagnosis'}; - if( index($e->{'diagnosis'}, ' ') < 0 && int($e->{'diagnosis'}) > 0 ) { - # Override the value of diagnostic code message - $e->{'diagnosis'} = $anotherset->{'diagnosis'}; - - } else { - # More detailed error message is in "$anotherset" - my $as = undef; # status - my $ar = undef; # replycode - - if( $e->{'status'} eq '' || $e->{'status'} eq '5.0.0' || $e->{'status'} eq '4.0.0' ) { - # Check the value of D.S.N. in $anotherset - $as = Sisimai::SMTP::Status->find($anotherset->{'diagnosis'}) || ''; - if( length($as) > 0 && substr($as, -4, 4) ne '.0.0' ) { - # The D.S.N. is neither an empty nor *.0.0 - $e->{'status'} = $as; - } - } - - if( $e->{'replycode'} eq '' || $e->{'replycode'} eq '500' || $e->{'replycode'} eq '400' ) { - # Check the value of SMTP reply code in $anotherset - $ar = Sisimai::SMTP::Reply->find($anotherset->{'diagnosis'}) || ''; - if( length($ar) > 0 && substr($ar, -2, 2) ne '00' ) { - # The SMTP reply code is neither an empty nor *00 - $e->{'replycode'} = $ar; - } - } - - if( $as || $ar && ( length($anotherset->{'diagnosis'}) > length($e->{'diagnosis'}) ) ) { - # Update the error message in $e->{'diagnosis'} - $e->{'diagnosis'} = $anotherset->{'diagnosis'}; - } - } - } - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); - - for my $q ( keys %$messagesof ) { - # Guess an reason of the bounce - next unless grep { index($e->{'diagnosis'}, $_) > -1 } $messagesof->{ $q }->@*; - $e->{'reason'} = $q; - last; - } - } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::GSuite - bounce mail decoder class for Google Workspace L - -=head1 SYNOPSIS - - use Sisimai::Lhost::GSuite; - -=head1 DESCRIPTION - -C decodes a bounce email which created by Google Workspace L. -Methods in the module are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::GSuite->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2017-2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/Gmail.pm b/lib/Sisimai/Lhost/Gmail.pm index 6751aa618..3a408b53e 100644 --- a/lib/Sisimai/Lhost/Gmail.pm +++ b/lib/Sisimai/Lhost/Gmail.pm @@ -223,7 +223,7 @@ sub inquire { my $hostname = substr($e->{'diagnosis'}, $p1 + 4, $p2 - $p1 - 4); my $ipv4addr = substr($e->{'diagnosis'}, $p2 + 3, rindex($e->{'diagnosis'}, ']. ') - $p2 - 3); - $e->{'rhost'} = $hostname if Sisimai::RFC1123->is_validhostname($hostname); + $e->{'rhost'} = $hostname if Sisimai::RFC1123->is_internethost($hostname); $e->{'rhost'} ||= $ipv4addr; } diff --git a/lib/Sisimai/Lhost/GoogleGroups.pm b/lib/Sisimai/Lhost/GoogleGroups.pm index 0f15a92f0..008aade54 100644 --- a/lib/Sisimai/Lhost/GoogleGroups.pm +++ b/lib/Sisimai/Lhost/GoogleGroups.pm @@ -16,6 +16,7 @@ sub inquire { my $mhead = shift // return undef; my $mbody = shift // return undef; + return undef unless index($$mbody, "Google Groups") > -1; return undef unless rindex($mhead->{'from'}, '') > -1; return undef unless index($mhead->{'subject'}, 'Delivery Status Notification') > -1; return undef unless exists $mhead->{'x-failed-recipients'}; diff --git a/lib/Sisimai/Lhost/GoogleWorkspace.pm b/lib/Sisimai/Lhost/GoogleWorkspace.pm new file mode 100644 index 000000000..d9531f199 --- /dev/null +++ b/lib/Sisimai/Lhost/GoogleWorkspace.pm @@ -0,0 +1,135 @@ +package Sisimai::Lhost::GoogleWorkspace; +use parent 'Sisimai::Lhost'; +use v5.26; +use strict; +use warnings; + +sub description { "Google Workspace: https://workspace.google.com/" } +sub inquire { + # Detect an error from Google Workspace (Transfer from the Google Workspace to the destination host) + # @param [Hash] mhead Message headers of a bounce email + # @param [String] mbody Message body of a bounce email + # @return [Hash] Bounce data list and message/rfc822 part + # @return [undef] failed to decode or the arguments are missing + # @since v4.21.0 + my $class = shift; + my $mhead = shift // return undef; + my $mbody = shift // return undef; + + return undef if index($$mbody, "\nDiagnostic-Code:") > -1; + return undef if index($$mbody, "\nFinal-Recipient:") > -1; + return undef unless rindex($mhead->{'from'}, '') > -1; + return undef unless index($mhead->{'subject'}, "Delivery Status Notification") > -1; + + state $indicators = __PACKAGE__->INDICATORS; + state $boundaries = ["Content-Type: message/rfc822", "Content-Type: text/rfc822-headers"]; + state $startingof = { + 'message' => ["** "], + 'error' => ["The response was:", "The response from the remote server was:"], + }; + state $messagesof = { + "userunknown" => ["because the address couldn't be found. Check for typos or unnecessary spaces and try again."], + "notaccept" => ["Null MX"], + "networkerror" => [" had no relevant answers.", " responded with code NXDOMAIN"], + }; + + my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; + my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); + my $entiremesg = ""; + my $readcursor = 0; # (Integer) Points the current cursor position + my $recipients = 0; + + for my $e ( split("\n", $emailparts->[0]) ) { + # Read error messages and delivery status lines from the head of the email to the previous + # line of the beginning of the original message. + unless( $readcursor ) { + # Beginning of the bounce message or message/delivery-status part + if( index($e, $startingof->{'message'}->[0]) == 0 ) { + # ** Message not delivered ** + $readcursor |= $indicators->{'deliverystatus'}; + $entiremesg .= $e." "; + } + } + next unless $readcursor & $indicators->{'deliverystatus'}; + next unless $e; + + # ** Message not delivered ** + # You're sending this from a different address or alias using the 'Send mail as' feature. + # The settings for your 'Send mail as' account are misconfigured or out of date. Check those settings and try resending. + # Learn more here: https://support.google.com/mail/?p=CustomFromDenied + # The response was: + # Unspecified Error (SENT_SECOND_EHLO): Smtp server does not advertise AUTH capability + next if index($e, "Content-Type: ") == 0; + $entiremesg .= $e." "; + } + + while( $recipients == 0 ) { + # Pick the recipient address from the value of To: header of the original message after + # Content-Type: message/rfc822 field + my $p0 = index($emailparts->[1], "\nTo:"); last if $p0 < 0; + my $p1 = index($emailparts->[1], "\n", $p0 + 2); + my $cv = Sisimai::Address->s3s4(substr($emailparts->[1], $p0 + 4, $p1 - $p0)); + $dscontents->[0]->{'recipient'} = $cv; + $recipients++; + } + return undef unless $recipients; + + $dscontents->[0]->{'diagnosis'} = $entiremesg; + for my $e ( @$dscontents ) { + # Tidy up the error message in e.Diagnosis, Try to detect the bounce reason. + $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); + + for my $r ( keys %$messagesof ) { + # Guess an reason of the bounce + next unless grep { index($e->{'diagnosis'}, $_) > -1 } $messagesof->{ $r }->@*; + $e->{'reason'} = $r; last; + } + } + return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; +} + +1; +__END__ + +=encoding utf-8 + +=head1 NAME + +Sisimai::Lhost::GoogleWorkspace - bounce mail decoder class for Google Workspace L + +=head1 SYNOPSIS + + use Sisimai::Lhost::GoogleWorkspace; + +=head1 DESCRIPTION + +C decodes a bounce email which created by Google Workspace L. +Methods in the module are called from only C. + +=head1 CLASS METHODS + +=head2 C> + +C returns description string of this module. + + print Sisimai::Lhost::GoogleWorkspace->description; + +=head2 C, I)>> + +C method decodes a bounced email and return results as a array reference. +See C for more details. + +=head1 AUTHOR + +azumakuniyuki + +=head1 COPYRIGHT + +Copyright (C) 2017-2024 azumakuniyuki, All rights reserved. + +=head1 LICENSE + +This software is distributed under The BSD 2-Clause License. + +=cut + diff --git a/lib/Sisimai/Lhost/MXLogic.pm b/lib/Sisimai/Lhost/MXLogic.pm deleted file mode 100644 index f147df259..000000000 --- a/lib/Sisimai/Lhost/MXLogic.pm +++ /dev/null @@ -1,232 +0,0 @@ -package Sisimai::Lhost::MXLogic; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -# Based on Sisimai::Lhost::Exim -sub description { 'McAfee SaaS' } -sub inquire { - # Detect an error from McAfee Saas (Formerly MXLogic) - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decode or the arguments are missing - # @since v4.1.1 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - my $match = 0; - - # X-MX-Bounce: mta/src/queue/bounce - # X-MXL-NoteHash: ffffffffffffffff-0000000000000000000000000000000000000000 - # X-MXL-Hash: 4c9d4d411993da17-bbd4212b6c887f6c23bab7db4bd87ef5edc00758 - $match ||= 1 if defined $mhead->{'x-mx-bounce'}; - $match ||= 1 if defined $mhead->{'x-mxl-hash'}; - $match ||= 1 if defined $mhead->{'x-mxl-notehash'}; - $match ||= 1 if index($mhead->{'from'}, 'Mail Delivery System') == 0; - $match ||= 1 if grep { index($mhead->{'subject'}, $_) > -1 } ( 'Delivery Status Notification', - 'Mail delivery failed', - 'Warning: message '); - return undef unless $match; - - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['Included is a copy of the message header:']; - state $startingof = { 'message' => ['This message was created automatically by mail delivery software.'] }; - state $recommands = [ - qr/SMTP error from remote (?:mail server|mailer) after ([A-Za-z]{4})/, - qr/SMTP error from remote (?:mail server|mailer) after end of ([A-Za-z]{4})/, - ]; - state $messagesof = { - 'userunknown' => ['user not found'], - 'hostunknown' => [ - 'all host address lookups failed permanently', - 'all relevant MX records point to non-existent hosts', - 'Unrouteable address', - ], - 'mailboxfull' => [ - 'mailbox is full', - 'error: quota exceed', - ], - 'notaccept' => [ - 'an MX or SRV record indicated no SMTP service', - 'no host found for existing SMTP connection', - ], - 'syntaxerror' => [ - 'angle-brackets nested too deep', - 'expected word or "<"', - 'domain missing in source-routed address', - 'malformed address:', - ], - 'systemerror' => [ - 'delivery to file forbidden', - 'delivery to pipe forbidden', - 'local delivery failed', - 'LMTP error after ', - ], - 'contenterror' => ['Too many "Received" headers'], - }; - state $delayedfor = [ - 'retry timeout exceeded', - 'No action is required on your part', - 'retry time not reached for any host after a long failure period', - 'all hosts have been failing for a long time and were last tried', - 'Delay reason: ', - 'has been frozen', - 'was frozen on arrival by ', - ]; - - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $v = undef; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0; - next; - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - - # This message was created automatically by mail delivery software. - # - # A message that you sent could not be delivered to one or more of its - # recipients. This is a permanent error. The following address(es) failed: - # - # kijitora@example.jp - # SMTP error from remote mail server after RCPT TO:: - # host neko.example.jp [192.0.2.222]: 550 5.1.1 ... User Unknown - $v = $dscontents->[-1]; - - if( index($e, ' <') == 0 && index($e, '@') > 1 && index($e, '>:') > 1 ) { - # A message that you have sent could not be delivered to one or more - # recipients. This is a permanent error. The following address failed: - # - # : 550 5.1.1 ... - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = substr($e, 3, index($e, '>:') - 3); - $v->{'diagnosis'} = substr($e, index($e, '>:') + 3,); - $recipients++; - - } elsif( scalar @$dscontents == $recipients ) { - # Error message - next unless length $e; - $v->{'diagnosis'} .= $e.' '; - } - } - return undef unless $recipients; - - # Get the name of the local MTA - # Received: from marutamachi.example.org (c192128.example.net [192.0.2.128]) - my $receivedby = $mhead->{'received'} || []; - my $recvdtoken = Sisimai::RFC5322->received($receivedby->[-1]); - - for my $e ( @$dscontents ) { - # Check the error message, the rhost, the lhost, and the smtp command. - $e->{'diagnosis'} =~ s/[-]{2}.*\z//g; - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); - - unless( length $e->{'rhost'} ) { - # Get the remote host name - my $p1 = index($e->{'diagnosis'}, 'host '); - my $p2 = index($e->{'diagnosis'}, ' ', $p1 + 5); - - # host neko.example.jp [192.0.2.222]: 550 5.1.1 ... User Unknown - # Get the remote host name from the error message or the Received header. - $e->{'rhost'} = substr($e->{'diagnosis'}, $p1 + 5, $p2 - $p1 - 5) if $p1 > -1; - $e->{'rhost'} ||= $recvdtoken->[1]; - } - $e->{'lhost'} ||= $recvdtoken->[0]; - - unless( $e->{'command'} ) { - # Get the SMTP command name for the session - SMTP: for my $r ( @$recommands ) { - # Verify each regular expression of SMTP commands - next unless $e->{'diagnosis'} =~ $r; - $e->{'command'} = uc $1; - last; - } - - # Detect the reason of bounce - if( $e->{'command'} eq 'MAIL' ) { - # MAIL | Connected to 192.0.2.135 but sender was rejected. - $e->{'reason'} = 'rejected'; - - } elsif( $e->{'command'} eq 'HELO' || $e->{'command'} eq 'EHLO' ) { - # HELO | Connected to 192.0.2.135 but my name was rejected. - $e->{'reason'} = 'blocked'; - - } else { - # Verify each regular expression of session errors - SESSION: for my $r ( keys %$messagesof ) { - # Check each regular expression - next unless grep { index($e->{'diagnosis'}, $_) > -1 } $messagesof->{ $r }->@*; - $e->{'reason'} = $r; - last; - } - - unless( $e->{'reason'} ) { - # The reason "expired" - $e->{'reason'} = 'expired' if grep { index($e->{'diagnosis'}, $_) > -1 } @$delayedfor; - } - } - } - $e->{'command'} ||= ''; - } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::MXLogic - bounce mail decoder class for McAfee SAAS (formerly MX Logic). - -=head1 SYNOPSIS - - use Sisimai::Lhost::MXLogic; - -=head1 DESCRIPTION - -C decodes a bounce email which created by McAfee SaaS (formerly MX Logic). -Methods in the module are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::MXLogic->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2014-2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/MailRu.pm b/lib/Sisimai/Lhost/MailRu.pm deleted file mode 100644 index 2a9864f40..000000000 --- a/lib/Sisimai/Lhost/MailRu.pm +++ /dev/null @@ -1,254 +0,0 @@ -package Sisimai::Lhost::MailRu; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -# Based on Sisimai::Lhost::Exim -sub description { '@mail.ru: https://mail.ru' } -sub inquire { - # Detect an error from @mail.ru - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decode or the arguments are missing - # @since v4.1.4 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - my $msgid = $mhead->{'message-id'} || return undef; - my $mfrom = lc $mhead->{'from'}; - my $match = 0; - - # Message-Id: - $match++ if index($mfrom, 'mailer-daemon@') > -1 && index($mfrom, 'mail.ru') > -1; - $match++ if index($msgid, '.mail.ru>') > 0 || index($msgid, 'smailru.net>') > 0; - $match++ if grep { index($mhead->{'subject'}, $_) > -1 } ( 'Delivery Status Notification', - 'Mail delivery failed', - 'Mail failure', - 'Message frozen', - 'Warning: message ', - 'error(s) in forwarding or filtering'); - return undef unless $match > 2; - - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['------ This is a copy of the message, including all the headers. ------']; - state $startingof = { 'message' => ['This message was created automatically by mail delivery software.'] }; - state $recommands = [ - qr/SMTP error from remote (?:mail server|mailer) after ([A-Za-z]{4})/, - qr/SMTP error from remote (?:mail server|mailer) after end of ([A-Za-z]{4})/, - ]; - state $messagesof = { - 'expired' => [ - 'retry timeout exceeded', - 'No action is required on your part', - ], - 'userunknown' => ['user not found'], - 'hostunknown' => [ - 'all host address lookups failed permanently', - 'all relevant MX records point to non-existent hosts', - 'Unrouteable address', - ], - 'mailboxfull' => [ - 'mailbox is full', - 'error: quota exceed', - ], - 'notaccept' => [ - 'an MX or SRV record indicated no SMTP service', - 'no host found for existing SMTP connection', - ], - 'systemerror' => [ - 'delivery to file forbidden', - 'delivery to pipe forbidden', - 'local delivery failed', - ], - 'contenterror'=> ['Too many "Received" headers '], - }; - - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $v = undef; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0; - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - - # Это письмо создано автоматически - # сервером Mail.Ru, # отвечать на него не - # нужно. - # - # К сожалению, Ваше письмо не может - # быть# доставлено одному или нескольким - # получателям: - # - # ********************** - # - # This message was created automatically by mail delivery software. - # - # A message that you sent could not be delivered to one or more of its - # recipients. This is a permanent error. The following address(es) failed: - # - # kijitora@example.jp - # SMTP error from remote mail server after RCPT TO:: - # host neko.example.jp [192.0.2.222]: 550 5.1.1 ... User Unknown - $v = $dscontents->[-1]; - - if( index($e, ' ') == 0 && index($e, ' ') < 0 && index($e, '@') > 1 ) { - # kijitora@example.jp - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = substr($e, 2,); - $recipients++; - - } elsif( scalar @$dscontents == $recipients ) { - # Error message - next unless length $e; - $v->{'diagnosis'} .= $e.' '; - - } else { - # Error message when email address above does not include '@' and domain part. - next unless index($e, ' ') == 0; - $v->{'alterrors'} .= $e.' '; - } - } - - unless( $recipients ) { - # Fallback for getting recipient addresses - if( defined $mhead->{'x-failed-recipients'} ) { - # X-Failed-Recipients: kijitora@example.jp - my @rcptinhead = split(',', $mhead->{'x-failed-recipients'}); - $_ =~ y/ //d for @rcptinhead; - $recipients = scalar @rcptinhead; - - for my $e ( @rcptinhead ) { - # Insert each recipient address into @$dscontents - $dscontents->[-1]->{'recipient'} = $e; - next if scalar @$dscontents == $recipients; - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - } - } - } - return undef unless $recipients; - - # Get the name of the local MTA - # Received: from marutamachi.example.org (c192128.example.net [192.0.2.128]) - my $receivedby = $mhead->{'received'} || []; - my $recvdtoken = Sisimai::RFC5322->received($receivedby->[-1]); - my $p1 = -1; my $p2 = -1; - - for my $e ( @$dscontents ) { - # Check the error message, the rhost, the lhost, and the smtp command. - if( exists $e->{'alterrors'} && $e->{'alterrors'} ) { - # Copy alternative error message - $e->{'diagnosis'} ||= $e->{'alterrors'}; - if( index($e->{'diagnosis'}, '-') == 0 || substr($e->{'diagnosis'}, -2, 2) eq '__' ) { - # Override the value of diagnostic code message - $e->{'diagnosis'} = $e->{'alterrors'} if $e->{'alterrors'}; - } - delete $e->{'alterrors'}; - } - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); - $p1 = rindex($e->{'diagnosis'}, '__'); - $e->{'diagnosis'} = substr($e->{'diagnosis'}, 0, $p1 - 1) if $p1 > 2; - - unless( $e->{'rhost'} ) { - # Get the remote host name - # host neko.example.jp [192.0.2.222]: 550 5.1.1 ... User Unknown - $p1 = index($e->{'diagnosis'}, 'host '); - $p2 = index($e->{'diagnosis'}, ' ', $p1 + 5); - $e->{'rhost'} = substr($e->{'diagnosis'}, $p1 + 5, $p2 - $p1 - 5) if $p1 > -1; - $e->{'rhost'} ||= $recvdtoken->[1]; - } - $e->{'lhost'} ||= $recvdtoken->[0]; - - unless( $e->{'command'} ) { - # Get the SMTP command name for the session - SMTP: for my $r ( @$recommands ) { - # Verify each regular expression of SMTP commands - next unless $e->{'diagnosis'} =~ $r; - $e->{'command'} = uc $1; - last; - } - - REASON: while(1) { - # Detect the reason of bounce - if( $e->{'command'} eq 'MAIL' ) { - # MAIL | Connected to 192.0.2.135 but sender was rejected. - $e->{'reason'} = 'rejected'; - - } elsif( $e->{'command'} eq 'HELO' || $e->{'command'} eq 'EHLO' ) { - # HELO | Connected to 192.0.2.135 but my name was rejected. - $e->{'reason'} = 'blocked'; - - } else { - SESSION: for my $r ( keys %$messagesof ) { - # Verify each regular expression of session errors - next unless grep { index($e->{'diagnosis'}, $_) > -1 } $messagesof->{ $r }->@*; - $e->{'reason'} = $r; - last; - } - } - last; - } - } - $e->{'command'} ||= ''; - } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::MailRu - bounce mail decoder class for @mail.ru L. - -=head1 SYNOPSIS - - use Sisimai::Lhost::MailRu; - -=head1 DESCRIPTION - -C decodes a bounce email which created by @mail.ru L. -Methods in the module are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::MailRu->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2014-2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/McAfee.pm b/lib/Sisimai/Lhost/McAfee.pm deleted file mode 100644 index 39fa48bcd..000000000 --- a/lib/Sisimai/Lhost/McAfee.pm +++ /dev/null @@ -1,154 +0,0 @@ -package Sisimai::Lhost::McAfee; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -sub description { 'McAfee Email Appliance' } -sub inquire { - # Detect an error from McAfee Email Appliance - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decode or the arguments are missing - # @since v4.1.1 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - - # X-NAI-Header: Modified by McAfee Email and Web Security Virtual Appliance - return undef unless defined $mhead->{'x-nai-header'}; - return undef unless index($mhead->{'x-nai-header'}, 'Modified by McAfee') > -1; - return undef unless $mhead->{'subject'} eq 'Delivery Status'; - - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['Content-Type: message/rfc822']; - state $startingof = { 'message' => ['--- The following addresses had delivery problems ---'] }; - state $messagesof = { 'userunknown' => [' User not exist', ' unknown.', '550 Unknown user ', 'No such user'] }; - - my $fieldtable = Sisimai::RFC1894->FIELDTABLE; - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $issuedcode = ''; # (String) Alternative diagnostic message - my $v = undef; - my $p = ''; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) > -1; - next; - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - - # Content-Type: text/plain; name="deliveryproblems.txt" - # - # --- The following addresses had delivery problems --- - # - # (User unknown user@example.com) - # - # --------------Boundary-00=_00000000000000000000 - # Content-Type: message/delivery-status; name="deliverystatus.txt" - # - $v = $dscontents->[-1]; - - if( Sisimai::String->aligned(\$e, ['<', '@', '>', '(', ')']) ) { - # (Unknown user kijitora@example.co.jp) - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = Sisimai::Address->s3s4(substr($e, index($e, '<'), index($e, '>'))); - $issuedcode = substr($e, index($e, '(') + 1,); - $recipients++; - - } elsif( Sisimai::RFC1894->match($e) ) { - # $e matched with any field defined in RFC3464 - my $o = Sisimai::RFC1894->field($e); - unless( $o ) { - # Fallback code for empty value or invalid formatted value - if( index($e, 'Original-Recipient: ') == 0 ) { - # - Original-Recipient: - $v->{'alias'} = Sisimai::Address->s3s4(substr($e, index($e, ':') + 1,)); - } - next; - } - next unless exists $fieldtable->{ $o->[0] }; - $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - - } else { - # Continued line of the value of Diagnostic-Code field - next unless index($p, 'Diagnostic-Code:') == 0; - next unless index($e, ' '); - $v->{'diagnosis'} .= ' '.Sisimai::String->sweep($e); - } - } continue { - # Save the current line for the next loop - $p = $e; - } - return undef unless $recipients; - - for my $e ( @$dscontents ) { - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'} || $issuedcode ); - - SESSION: for my $r ( keys %$messagesof ) { - # Verify each regular expression of session errors - next unless grep { index($e->{'diagnosis'}, $_) > -1 } $messagesof->{ $r }->@*; - $e->{'reason'} = $r; - last; - } - } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::McAfee - bounce mail decode class for McAfee Email Appliance. - -=head1 SYNOPSIS - - use Sisimai::Lhost::McAfee; - -=head1 DESCRIPTION - -C decodes a bounce email which created by McAfee Email Appliance. -Methods in the module are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::McAfee->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2014-2021,2023,2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/MessageLabs.pm b/lib/Sisimai/Lhost/MessageLabs.pm deleted file mode 100644 index 228a0d27f..000000000 --- a/lib/Sisimai/Lhost/MessageLabs.pm +++ /dev/null @@ -1,165 +0,0 @@ -package Sisimai::Lhost::MessageLabs; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -sub description { 'Email Security (formerly Messaging Security): https://www.broadcom.com/products/cybersecurity/email' } -sub inquire { - # Detect an error from Email Security (Formerly MessageLabs.com) - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decode or the arguments are missing - # @since v4.1.10 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - - # X-Msg-Ref: server-11.tower-143.messagelabs.com!1419367175!36473369!1 - # X-Originating-IP: [10.245.230.38] - # X-StarScan-Received: - # X-StarScan-Version: 6.12.5; banners=-,-,- - # X-VirusChecked: Checked - return undef unless defined $mhead->{'x-msg-ref'}; - return undef unless rindex($mhead->{'from'}, 'MAILER-DAEMON@messagelabs.com') > -1; - return undef unless index($mhead->{'subject'}, 'Mail Delivery Failure') == 0; - - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['Content-Type: text/rfc822-headers']; - state $startingof = { 'message' => ['Content-Type: message/delivery-status'] }; - state $messagesof = { - 'userunknown' => ['542 ', ' Rejected', 'No such user'], - 'securityerror' => ['Please turn on SMTP Authentication in your mail client'], - }; - - my $fieldtable = Sisimai::RFC1894->FIELDTABLE; - my $permessage = {}; # (Hash) Store values of each Per-Message field - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $v = undef; - my $p = ''; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0; - next; - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - - if( my $f = Sisimai::RFC1894->match($e) ) { - # $e matched with any field defined in RFC3464 - next unless my $o = Sisimai::RFC1894->field($e); - $v = $dscontents->[-1]; - - if( $o->[-1] eq 'addr' ) { - # Final-Recipient: rfc822; kijitora@example.jp - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - if( $o->[0] eq 'final-recipient' ) { - # Final-Recipient: rfc822; kijitora@example.jp - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = $o->[2]; - $recipients++; - - } else { - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - $v->{'alias'} = $o->[2]; - } - } elsif( $o->[-1] eq 'code' ) { - # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown - $v->{'spec'} = $o->[1]; - $v->{'diagnosis'} = $o->[2]; - - } else { - # Other DSN fields defined in RFC3464 - next unless exists $fieldtable->{ $o->[0] }; - $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - - next unless $f == 1; - $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2]; - } - } else { - # Continued line of the value of Diagnostic-Code field - next unless index($p, 'Diagnostic-Code:') == 0; - next unless index($e, ' '); - $v->{'diagnosis'} .= ' '.Sisimai::String->sweep($e); - } # End of message/delivery-status - } continue { - # Save the current line for the next loop - $p = $e; - } - return undef unless $recipients; - - for my $e ( @$dscontents ) { - # Set default values if each value is empty. - $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage; - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); - - SESSION: for my $r ( keys %$messagesof ) { - # Verify each regular expression of session errors - next unless grep { index($e->{'diagnosis'}, $_) > -1 } $messagesof->{ $r }->@*; - $e->{'reason'} = $r; - last; - } - } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::MessageLabs - bounce mail decode class for Email Security (formerly Messaging Security) -L - -=head1 SYNOPSIS - - use Sisimai::Lhost::MessageLabs; - -=head1 DESCRIPTION - -C decodes a bounce email which created by Email Security (formerly Messaging -Security) L (formerly Symantec.cloud: formerly -known as MessageLabs L. -Methods in the module are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::MessageLabs->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2014-2021,2023,2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/Office365.pm b/lib/Sisimai/Lhost/Office365.pm deleted file mode 100644 index e060de6dc..000000000 --- a/lib/Sisimai/Lhost/Office365.pm +++ /dev/null @@ -1,282 +0,0 @@ -package Sisimai::Lhost::Office365; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -sub description { 'Microsoft 365: https://office.microsoft.com/' } -sub inquire { - # Detect an error from Microsoft 365 (formerly Office 365) - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decode or the arguments are missing - # @since v4.1.3 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - my $match = 0; - my $tryto = ['.outbound.protection.outlook.com', '.prod.outlook.com']; - - # X-MS-Exchange-Message-Is-Ndr: - # X-Microsoft-Antispam-PRVS: <....@...outlook.com> - # X-Exchange-Antispam-Report-Test: UriScan:; - # X-Exchange-Antispam-Report-CFA-Test: - # X-MS-Exchange-CrossTenant-OriginalArrivalTime: 29 Apr 2015 23:34:45.6789 (JST) - # X-MS-Exchange-CrossTenant-FromEntityHeader: Hosted - # X-MS-Exchange-Transport-CrossTenantHeadersStamped: ... - $match++ if index($mhead->{'subject'}, 'Undeliverable:') > -1; - $match++ if index($mhead->{'subject'}, 'Onbestelbaar:') > -1; - $match++ if index($mhead->{'subject'}, 'Não_entregue:') > -1; - $match++ if $mhead->{'x-ms-exchange-message-is-ndr'}; - $match++ if $mhead->{'x-microsoft-antispam-prvs'}; - $match++ if $mhead->{'x-exchange-antispam-report-test'}; - $match++ if $mhead->{'x-exchange-antispam-report-cfa-test'}; - $match++ if $mhead->{'x-ms-exchange-crosstenant-originalarrivaltime'}; - $match++ if $mhead->{'x-ms-exchange-crosstenant-fromentityheader'}; - $match++ if $mhead->{'x-ms-exchange-transport-crosstenantheadersstamped'}; - $match++ if grep { index($_, $tryto->[0]) > 0 || index($_, $tryto->[1]) > 0 } $mhead->{'received'}->@*; - if( defined $mhead->{'message-id'} ) { - # Message-ID: <00000000-0000-0000-0000-000000000000@*.*.prod.outlook.com> - $match++ if grep { index($mhead->{'message-id'}, $_) > 0 } @$tryto; - } - return undef if $match < 2; - - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['Content-Type: message/rfc822', 'Original message headers:']; - state $commandset = { 'RCPT' => ['unknown recipient or mailbox unavailable ->', '@'] }; - state $startingof = { - 'eoe' => [ - 'Original message headers:', 'Original Message Headers:', - 'Message Hops', - 'alhos originais da mensagem:', - 'Oorspronkelijke berichtkoppen:', - ], - 'error' => [ - 'Diagnostic information for administrators:', - 'Diagnostische gegevens voor beheerders:', - 'Error Details', - 'stico para administradores:', - ], - 'lhost' => [ - 'Generating server: ', - 'Bronserver: ', - 'Servidor de origem: ', - ], - 'message' => [ - ' rejected your message to the following e', - 'Delivery has failed to these recipients or groups:', - 'Falha na entrega a estes destinat', - 'Original Message Details', - 'Uw bericht kan niet worden bezorgd bij de volgende geadresseerden of groepen:', - ], - 'rfc3464' => ['Content-Type: message/delivery-status'], - }; - state $statuslist = { - # https://support.office.com/en-us/article/Email-non-delivery-reports-in-Office-365-51daa6b9-2e35-49c4-a0c9-df85bf8533c3 - qr/\A4[.]4[.]7\z/ => 'expired', - qr/\A4[.]4[.]312\z/ => 'networkerror', - qr/\A4[.]4[.]316\z/ => 'expired', - qr/\A4[.]7[.]26\z/ => 'authfailure', - qr/\A4[.]7[.][56]\d\d\z/ => 'blocked', - qr/\A4[.]7[.]8[5-9]\d\z/ => 'blocked', - qr/\A5[.]0[.]350\z/ => 'contenterror', - qr/\A5[.]1[.]10\z/ => 'userunknown', - qr/\A5[.]4[.]1\z/ => 'norelaying', - qr/\A5[.]4[.]6\z/ => 'networkerror', - qr/\A5[.]4[.]312\z/ => 'networkerror', - qr/\A5[.]4[.]316\z/ => 'expired', - qr/\A5[.]6[.]11\z/ => 'contenterror', - qr/\A5[.]7[.]1\z/ => 'rejected', - qr/\A5[.]7[.]1[23]\z/ => 'rejected', - qr/\A5[.]7[.]124\z/ => 'rejected', - qr/\A5[.]7[.]13[3-6]\z/ => 'rejected', - qr/\A5[.]7[.]23\z/ => 'authfailure', - qr/\A5[.]7[.]25\z/ => 'networkerror', - qr/\A5[.]7[.]50[1-3]\z/ => 'spamdetected', - qr/\A5[.]7[.]50[4-5]\z/ => 'filtered', - qr/\A5[.]7[.]50[6-7]\z/ => 'blocked', - qr/\A5[.]7[.]508\z/ => 'toomanyconn', - qr/\A5[.]7[.]509\z/ => 'authfailure', - qr/\A5[.]7[.]510\z/ => 'notaccept', - qr/\A5[.]7[.]511\z/ => 'rejected', - qr/\A5[.]7[.]512\z/ => 'policyviolation', - qr/\A5[.]7[.]57\z/ => 'securityerror', - qr/\A5[.]7[.]60[6-9]\z/ => 'blocked', - qr/\A5[.]7[.]6[1-4]\d\z/ => 'blocked', - qr/\A5[.]7[.]7[0-4]\d\z/ => 'toomanyconn', - }; - - my $fieldtable = Sisimai::RFC1894->FIELDTABLE; - my $permessage = {}; # (Hash) Store values of each Per-Message field - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $endoferror = 0; # (Integer) Flag for the end of error messages - my $v = undef; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if grep { index($e, $_) > -1 } $startingof->{'message'}->@*; - next; - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - - # kijitora@example.com - # The email address wasn't found at the destination domain. It might - # be misspelled or it might not exist any longer. Try retyping the - # address and resending the message. - # - # Original Message Details - # Created Date: 4/29/2017 6:40:30 AM - # Sender Address: neko@example.jp - # Recipient Address: kijitora@example.org - # Subject: Nyaan - $v = $dscontents->[-1]; - - my $p1 = index($e, ' 1 || $p2 == 0 ) { - # kijitora@example.com - # Recipient Address: kijitora-nyaan@neko.kyoto.example.jp - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - - $v->{'recipient'} = Sisimai::Address->s3s4(substr($e, index($e, ':') + 1,)); - $recipients++; - - } elsif( grep { index($e, $_) == 0 } $startingof->{'lhost'}->@* ) { - # Generating server: FFFFFFFFFFFF.e0.prod.outlook.com - $permessage->{'lhost'} = substr($e, index($e, ': ') + 2,); - - } else { - if( $endoferror ) { - # After "Original message headers:" - next unless my $f = Sisimai::RFC1894->match($e); - next unless my $o = Sisimai::RFC1894->field($e); - next unless exists $fieldtable->{ $o->[0] }; - - if( $v->{'diagnosis'} ) { - # Do not capture "Diagnostic-Code:" field because error message have already - # been captured - next if $o->[0] eq 'diagnostic-code' || $o->[0] eq 'final-recipient'; - $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - - next unless $f == 1; - $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2]; - - } else { - # Capture "Diagnostic-Code:" field because no error messages have been captured - $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2]; - } - } else { - if( grep { index($e, $_) > -1 } $startingof->{'error'}->@* ) { - # Diagnostic information for administrators: - $v->{'diagnosis'} = $e; - - } else { - # kijitora@example.com - # Remote Server returned '550 5.1.10 RESOLVER.ADR.RecipientNotFound; Recipien= - # t not found by SMTP address lookup' - if( $v->{'diagnosis'} ) { - # The error message text have already captured - if( grep { index($e, $_) > -1 } $startingof->{'eoe'}->@* ) { - # Original message headers: - $endoferror = 1; - next; - } - $v->{'diagnosis'} .= ' '.$e; - - } else { - # The error message text has not been captured yet - $endoferror = 1 if index($e, $startingof->{'rfc3464'}->[0]) == 0; - } - } - } - } - } - return undef unless $recipients; - - for my $e ( @$dscontents ) { - # Set default values if each value is empty. - $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage; - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); - - if( ! $e->{'status'} || substr($e->{'status'}, -4, 4) eq '.0.0' ) { - # There is no value of Status header or the value is 5.0.0, 4.0.0 - $e->{'status'} = Sisimai::SMTP::Status->find($e->{'diagnosis'}) || $e->{'status'}; - } - - for my $p ( keys %$commandset ) { - # Try to match with regular expressions defined in commandset - next unless Sisimai::String->aligned(\$e->{'diagnosis'}, $commandset->{ $p }); - $e->{'command'} = $p; - last; - } - - # Find the error code from $statuslist - next unless $e->{'status'}; - for my $f ( keys %$statuslist ) { - # Try to match with each key as a regular expression - next unless $e->{'status'} =~ $f; - $e->{'reason'} = $statuslist->{ $f }; - last; - } - } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::Office365 - bounce mail decoder class for Microsoft 365 L. - -=head1 SYNOPSIS - - use Sisimai::Lhost::Office365; - -=head1 DESCRIPTION - -C decodes a bounce email which created by Microsoft 365 L. -Methods in the module are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::Office365->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2016-2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/Outlook.pm b/lib/Sisimai/Lhost/Outlook.pm deleted file mode 100644 index 10d473da4..000000000 --- a/lib/Sisimai/Lhost/Outlook.pm +++ /dev/null @@ -1,175 +0,0 @@ -package Sisimai::Lhost::Outlook; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -sub description { 'Microsoft Outlook.com: https://www.outlook.com/' } -sub inquire { - # Detect an error from Microsoft Outlook.com - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decode or the arguments are missing - # @since v4.1.3 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - my $match = 0; - - # X-Message-Delivery: Vj0xLjE7RD0wO0dEPTA7U0NMPTk7bD0xO3VzPTE= - # X-Message-Info: AuEzbeVr9u5fkDpn2vR5iCu5wb6HBeY4iruBjnutBzpStnUabbM... - $match++ if index($mhead->{'subject'}, 'Delivery Status Notification') > -1; - $match++ if $mhead->{'x-message-delivery'}; - $match++ if $mhead->{'x-message-info'}; - $match++ if grep { rindex($_, '.hotmail.com') > -1 } $mhead->{'received'}->@*; - return undef if $match < 2; - - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['Content-Type: message/rfc822']; - state $startingof = { 'message' => ['This is an automatically generated Delivery Status Notification'] }; - state $messagesof = { - 'hostunknown' => ['The mail could not be delivered to the recipient because the domain is not reachable'], - 'userunknown' => ['Requested action not taken: mailbox unavailable'], - }; - - my $fieldtable = Sisimai::RFC1894->FIELDTABLE; - my $permessage = {}; # (Hash) Store values of each Per-Message field - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $v = undef; - my $p = ''; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0; - next; - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - - if( my $f = Sisimai::RFC1894->match($e) ) { - # $e matched with any field defined in RFC3464 - next unless my $o = Sisimai::RFC1894->field($e); - $v = $dscontents->[-1]; - - if( $o->[-1] eq 'addr' ) { - # Final-Recipient: rfc822; kijitora@example.jp - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - if( $o->[0] eq 'final-recipient' ) { - # Final-Recipient: rfc822; kijitora@example.jp - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = $o->[2]; - $recipients++; - - } else { - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - $v->{'alias'} = $o->[2]; - } - } elsif( $o->[-1] eq 'code' ) { - # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown - $v->{'spec'} = $o->[1]; - $v->{'diagnosis'} = $o->[2]; - - } else { - # Other DSN fields defined in RFC3464 - next unless exists $fieldtable->{ $o->[0] }; - $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - - next unless $f == 1; - $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2]; - } - } else { - # Continued line of the value of Diagnostic-Code field - next unless index($p, 'Diagnostic-Code:') == 0; - next unless index($e, ' ') == 0; - $v->{'diagnosis'} .= ' '.$1; - } - } continue { - # Save the current line for the next loop - $p = $e; - } - return undef unless $recipients; - - for my $e ( @$dscontents ) { - # Set default values if each value is empty. - $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage; - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); - - unless( $e->{'diagnosis'} ) { - # No message in 'diagnosis' - if( $e->{'action'} eq 'delayed' ) { - # Set pseudo diagnostic code message for delaying - $e->{'diagnosis'} = 'Delivery to the following recipients has been delayed.'; - - } else { - # Set pseudo diagnostic code message - $e->{'diagnosis'} = 'Unable to deliver message to the following recipients, '; - $e->{'diagnosis'} .= 'due to being unable to connect successfully to the destination mail server.'; - } - } - - SESSION: for my $r ( keys %$messagesof ) { - # Verify each regular expression of session errors - next unless grep { index($e->{'diagnosis'}, $_) > -1 } $messagesof->{ $r }->@*; - $e->{'reason'} = $r; - last; - } - } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::Outlook - bounce mail decoder class for outlook.com L. - -=head1 SYNOPSIS - - use Sisimai::Lhost::Outlook; - -=head1 DESCRIPTION - -C decodes a bounce email which created by Microsoft outlook.com L. -Methods in the module are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::Outlook->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2014-2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/Postfix.pm b/lib/Sisimai/Lhost/Postfix.pm index ff9dd80e3..d53624e0d 100644 --- a/lib/Sisimai/Lhost/Postfix.pm +++ b/lib/Sisimai/Lhost/Postfix.pm @@ -29,6 +29,7 @@ sub inquire { return undef if $match == 0; return undef if $mhead->{'x-aol-ip'}; + require Sisimai::SMTP::Reply; require Sisimai::SMTP::Command; state $indicators = __PACKAGE__->INDICATORS; state $boundaries = ['Content-Type: message/rfc822', 'Content-Type: text/rfc822-headers']; @@ -112,7 +113,7 @@ sub inquire { next unless my $o = Sisimai::RFC1894->field($e); $v = $dscontents->[-1]; - if( $o->[-1] eq 'addr' ) { + if( $o->[3] eq 'addr' ) { # Final-Recipient: rfc822; kijitora@example.jp # X-Actual-Recipient: rfc822; kijitora@example.co.jp if( $o->[0] eq 'final-recipient' ) { @@ -129,7 +130,7 @@ sub inquire { # X-Actual-Recipient: rfc822; kijitora@example.co.jp $v->{'alias'} = $o->[2]; } - } elsif( $o->[-1] eq 'code' ) { + } elsif( $o->[3] eq 'code' ) { # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown $v->{'spec'} = $o->[1]; $v->{'spec'} = 'SMTP' if uc $v->{'spec'} eq 'X-POSTFIX'; @@ -164,8 +165,7 @@ sub inquire { # Alternative error message and recipient if( index($e, ' (in reply to ') > -1 || index($e, 'command)') > -1 ) { # 5.1.1 ... User Unknown (in reply to RCPT TO - my $q = Sisimai::SMTP::Command->find($e); - push @commandset, $q if $q; + my $cv = Sisimai::SMTP::Command->find($e); push @commandset, $cv if $cv; $anotherset->{'diagnosis'} .= ' '.$e if $anotherset->{'diagnosis'}; } elsif( Sisimai::String->aligned(\$e, ['<', '@', '>', '(expanded from <', '):']) ) { diff --git a/lib/Sisimai/Lhost/PowerMTA.pm b/lib/Sisimai/Lhost/PowerMTA.pm deleted file mode 100644 index d4e531ac1..000000000 --- a/lib/Sisimai/Lhost/PowerMTA.pm +++ /dev/null @@ -1,163 +0,0 @@ -package Sisimai::Lhost::PowerMTA; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -sub description { 'PowerMTA: https://bird.com/email/power-mta' } -sub inquire { - # Detect an error from PowerMTA - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decode or the arguments are missing - # @since v4.25.6 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - return undef unless index($mhead->{'subject'}, 'Delivery report') > -1; - - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['Content-Type: text/rfc822-headers']; - state $startingof = { 'message' => ['Hello, this is the mail server on '] }; - state $categories = { - 'bad-domain' => 'hostunknown', - 'bad-mailbox' => 'userunknown', - 'inactive-mailbox' => 'disabled', - 'message-expired' => 'expired', - 'no-answer-from-host' => 'networkerror', - 'policy-related' => 'policyviolation', - 'quota-issues' => 'mailboxfull', - 'routing-errors' => 'systemerror', - 'spam-related' => 'spamdetected', - }; - - my $fieldtable = Sisimai::RFC1894->FIELDTABLE; - my $permessage = {}; # (Hash) Store values of each Per-Message field - - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $v = undef; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - if( rindex($e, $startingof->{'message'}->[0]) > -1 ) { - $readcursor |= $indicators->{'deliverystatus'}; - next; - } - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - - if( my $f = Sisimai::RFC1894->match($e) ) { - # $e matched with any field defined in RFC3464 - next unless my $o = Sisimai::RFC1894->field($e); - $v = $dscontents->[-1]; - - if( $o->[-1] eq 'addr' ) { - # Final-Recipient: rfc822; kijitora@example.jp - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - if( $o->[0] eq 'final-recipient' ) { - # Final-Recipient: rfc822; kijitora@example.jp - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = $o->[2]; - $recipients++; - - } else { - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - $v->{'alias'} = $o->[2]; - } - } elsif( $o->[-1] eq 'code' ) { - # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown - $v->{'spec'} = $o->[1]; - $v->{'diagnosis'} = $o->[2]; - - } else { - # Other DSN fields defined in RFC3464 - next unless exists $fieldtable->{ $o->[0] }; - $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - - next unless $f == 1; - $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2]; - } - } else { - # Hello, this is the mail server on neko2.example.org. - # - # I am sending you this message to inform you on the delivery status of a - # message you previously sent. Immediately below you will find a list of - # the affected recipients; also attached is a Delivery Status Notification - # (DSN) report in standard format, as well as the headers of the original - # message. - # - # delivery failed; will not continue trying - if( index($e, 'X-PowerMTA-BounceCategory: ') == 0 ) { - # X-PowerMTA-BounceCategory: bad-mailbox - $v->{'category'} = substr($e, index($e, ': ') + 2,); - } - } - } - return undef unless $recipients; - - for my $e ( @$dscontents ) { - # Set default values if each value is empty. - $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage; - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); - $e->{'reason'} = $categories->{ $e->{'category'} } || ''; - } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::PowerMTA - bounce mail decoder class for PowerMTA L. - -=head1 SYNOPSIS - - use Sisimai::Lhost::PowerMTA; - -=head1 DESCRIPTION - -C decodes a bounce email which created by PowerMTA L. -Methods in the module are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::PowerMTA->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2020,2021,2023,2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/ReceivingSES.pm b/lib/Sisimai/Lhost/ReceivingSES.pm deleted file mode 100644 index 0ffd1a6df..000000000 --- a/lib/Sisimai/Lhost/ReceivingSES.pm +++ /dev/null @@ -1,173 +0,0 @@ -package Sisimai::Lhost::ReceivingSES; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -sub description { 'Amazon SES(Receiving): https://aws.amazon.com/ses/' }; -sub inquire { - # Detect an error from Amazon SES/Receiving - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decode or the arguments are missing - # @see https://aws.amazon.com/ses/ - # @since v4.1.29 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - - # X-SES-Outgoing: 2015.10.01-54.240.27.7 - # Feedback-ID: 1.us-west-2.HX6/J9OVlHTadQhEu1+wdF9DBj6n6Pa9sW5Y/0pSOi8=:AmazonSES - return undef unless $mhead->{'x-ses-outgoing'}; - - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['Content-Type: text/rfc822-headers']; - state $startingof = { 'message' => ['This message could not be delivered.'] }; - state $messagesof = { - # The followings are error messages in Rule sets/*/Actions/Template - 'filtered' => ['Mailbox does not exist'], - 'mesgtoobig' => ['Message too large'], - 'mailboxfull' => ['Mailbox full'], - 'contenterror' => ['Message content rejected'], - }; - - my $fieldtable = Sisimai::RFC1894->FIELDTABLE; - my $permessage = {}; # (Hash) Store values of each Per-Message field - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $v = undef; - my $p = ''; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0; - next; - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - - if( my $f = Sisimai::RFC1894->match($e) ) { - # $e matched with any field defined in RFC3464 - next unless my $o = Sisimai::RFC1894->field($e); - $v = $dscontents->[-1]; - - if( $o->[-1] eq 'addr' ) { - # Final-Recipient: rfc822; kijitora@example.jp - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - if( $o->[0] eq 'final-recipient' ) { - # Final-Recipient: rfc822; kijitora@example.jp - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = $o->[2]; - $recipients++; - - } else { - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - $v->{'alias'} = $o->[2]; - } - } elsif( $o->[-1] eq 'code' ) { - # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown - $v->{'spec'} = $o->[1]; - $v->{'diagnosis'} = $o->[2]; - - } else { - # Other DSN fields defined in RFC3464 - next unless exists $fieldtable->{ $o->[0] }; - $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - - next unless $f == 1; - $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2]; - } - } else { - # Continued line of the value of Diagnostic-Code field - next unless index($p, 'Diagnostic-Code:') == 0; - next unless index($e, ' ') == 0; - $v->{'diagnosis'} .= ' '.Sisimai::String->sweep($e); - } - } continue { - # Save the current line for the next loop - $p = $e; - } - return undef unless $recipients; - - for my $e ( @$dscontents ) { - # Set default values if each value is empty. - $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage; - $e->{'diagnosis'} =~ y/\n/ /; - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); - - if( index($e->{'status'}, '.0.0') > 0 || index($e->{'status'}, '.1.0') > 0 ) { - # Get other D.S.N. value from the error message - # 5.1.0 - Unknown address error 550-'5.7.1 ... - my $errormessage = $e->{'diagnosis'}; - my $p1 = index($e->{'diagnosis'}, "-'"); $p1 = index($e->{'diagnosis'}, '-"') if $p1 < 0; - my $p2 = rindex($e->{'diagnosis'}, "' "); $p2 = rindex($e->{'diagnosis'}, '" ') if $p2 < 0; - $errormessage = substr($e->{'diagnosis'}, $p1 + 2, $p2 - $p1 - 2) if $p1 > -1 && $p2 > -1; - $e->{'status'} = Sisimai::SMTP::Status->find($errormessage) || $e->{'status'}; - } - - SESSION: for my $r ( keys %$messagesof ) { - # Verify each regular expression of session errors - next unless grep { index($e->{'diagnosis'}, $_) > -1 } $messagesof->{ $r }->@*; - $e->{'reason'} = $r; - last; - } - $e->{'reason'} ||= Sisimai::SMTP::Status->name($e->{'status'}) || ''; - } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::ReceivingSES - bounce mail decoder class for Amazon SES C. - -=head1 SYNOPSIS - - use Sisimai::Lhost::ReceivingSES; - -=head1 DESCRIPTION - -C decodes a bounce email which created by Amazon Simple Email Service -L. Methods in the module are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::ReceivingSES->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2015-2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/SendGrid.pm b/lib/Sisimai/Lhost/SendGrid.pm deleted file mode 100644 index 864c6b8ac..000000000 --- a/lib/Sisimai/Lhost/SendGrid.pm +++ /dev/null @@ -1,214 +0,0 @@ -package Sisimai::Lhost::SendGrid; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -sub description { 'Twilio SendGrid: https://sendgrid.com/' } -sub inquire { - # Detect an error from Twilio SendGrid - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decode or the arguments are missing - # @since v4.0.2 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - - # Return-Path: - # X-Mailer: MIME-tools 5.502 (Entity 5.502) - return undef unless $mhead->{'return-path'}; - return undef unless $mhead->{'return-path'} eq ''; - return undef unless $mhead->{'subject'} eq 'Undelivered Mail Returned to Sender'; - - require Sisimai::SMTP::Command; - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['Content-Type: message/rfc822']; - state $startingof = { 'message' => ['This is an automatically generated message from SendGrid.'] }; - - my $fieldtable = Sisimai::RFC1894->FIELDTABLE; - my $permessage = {}; # (Hash) Store values of each Per-Message field - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $thecommand = ''; # (String) SMTP Command name begin with the string '>>>' - my $v = undef; - my $p = ''; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0; - next; - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - - if( my $f = Sisimai::RFC1894->match($e) ) { - # $e matched with any field defined in RFC3464 - my $o = Sisimai::RFC1894->field($e); - $v = $dscontents->[-1]; - - unless( $o ) { - # Fallback code for empty value or invalid formatted value - # - Status: (empty) - # - Diagnostic-Code: 550 5.1.1 ... (No "diagnostic-type" sub field) - $v->{'diagnosis'} = substr($e, index($e, ':') + 2,) if index($e, 'Diagnostic-Code: ') == 0; - next; - } - - if( $o->[-1] eq 'addr' ) { - # Final-Recipient: rfc822; kijitora@example.jp - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - if( $o->[0] eq 'final-recipient' ) { - # Final-Recipient: rfc822; kijitora@example.jp - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = $o->[2]; - $recipients++; - - } else { - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - $v->{'alias'} = $o->[2]; - } - } elsif( $o->[-1] eq 'code' ) { - # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown - $v->{'spec'} = $o->[1]; - $v->{'diagnosis'} = $o->[2]; - - } elsif( $o->[-1] eq 'date' ) { - # Arrival-Date: 2012-12-31 23-59-59 - next unless index($e, 'Arrival-Date: ') == 0; - my @cf = split(' ', substr($e, index($e, ': ') + 2,)); next unless scalar @cf == 2; - my @cw = split('-', $cf[0]); next unless scalar @cw == 3; - my @ce = split('-', $cf[1]); next unless scalar @ce == 3; - - $o->[1] .= 'Thu, '.$cw[2].' '; - $o->[1] .= Sisimai::DateTime->monthname(0)->[int($cw[1]) - 1]; - $o->[1] .= ' '.$cw[0].' '.join(':', @ce); - $o->[1] .= ' '.Sisimai::DateTime->abbr2tz('CDT'); - - } else { - # Other DSN fields defined in RFC3464 - next unless exists $fieldtable->{ $o->[0] }; - $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - - next unless $f == 1; - $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2]; - } - } else { - # This is an automatically generated message from SendGrid. - # - # I'm sorry to have to tell you that your message was not able to be - # delivered to one of its intended recipients. - # - # If you require assistance with this, please contact SendGrid support. - # - # shironekochan:000000: : 192.0.2.250 : mx.example.jp:[192.0.2.153] : - # 550 5.1.1 ... User Unknown in RCPT TO - # - # ------------=_1351676802-30315-116783 - # Content-Type: message/delivery-status - # Content-Disposition: inline - # Content-Transfer-Encoding: 7bit - # Content-Description: Delivery Report - # - # X-SendGrid-QueueID: 959479146 - # X-SendGrid-Sender: - if( my $cv = Sisimai::SMTP::Command->find($e) ) { - # in RCPT TO, in MAIL FROM, end of DATA - $thecommand = $cv; - - } elsif( index($e, 'Diagnostic-Code: ') == 0 ) { - # Diagnostic-Code: 550 5.1.1 ... User Unknown - $v->{'diagnosis'} = substr($e, index($e, ':') + 2,); - - } else { - # Continued line of the value of Diagnostic-Code field - next unless index($p, 'Diagnostic-Code:') == 0; - next unless index($e, ' ') == 0; - $v->{'diagnosis'} .= ' '.Sisimai::String->sweep($e); - } - } - } continue { - # Save the current line for the next loop - $p = $e; - } - return undef unless $recipients; - - for my $e ( @$dscontents ) { - # Get the value of SMTP status code as a pseudo D.S.N. - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); - $e->{'replycode'} = Sisimai::SMTP::Reply->find($e->{'diagnosis'}) || ''; - $e->{'status'} = substr($e->{'replycode'}, 0, 1).'.0.0' if length $e->{'replycode'} == 3; - $e->{'command'} ||= $thecommand; - - if( $e->{'status'} eq '5.0.0' || $e->{'status'} eq '4.0.0' ) { - # Get the value of D.S.N. from the error message or the value of Diagnostic-Code header. - $e->{'status'} = Sisimai::SMTP::Status->find($e->{'diagnosis'}) || $e->{'status'}; - } - - if( $e->{'action'} eq 'expired' ) { - # Action: expired - $e->{'reason'} = 'expired'; - if( ! $e->{'status'} || substr($e->{'status'}, -4, 4) eq '.0.0' ) { - # Set pseudo Status code value if the value of Status is not defined or 4.0.0 or 5.0.0. - $e->{'status'} = Sisimai::SMTP::Status->code('expired') || $e->{'status'}; - } - } - } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::SendGrid - bounce mail decoder class for Twilio SendGrid L. - -=head1 SYNOPSIS - - use Sisimai::Lhost::SendGrid; - -=head1 DESCRIPTION - -C decodes a bounce email which created by Twilio SendGrid L. -Methods in the module are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::SendGrid->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2014-2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/Sendmail.pm b/lib/Sisimai/Lhost/Sendmail.pm index 56a979d5e..62372dd3b 100644 --- a/lib/Sisimai/Lhost/Sendmail.pm +++ b/lib/Sisimai/Lhost/Sendmail.pm @@ -69,7 +69,7 @@ sub inquire { next unless my $o = Sisimai::RFC1894->field($e); $v = $dscontents->[-1]; - if( $o->[-1] eq 'addr' ) { + if( $o->[3] eq 'addr' ) { # Final-Recipient: rfc822; kijitora@example.jp # X-Actual-Recipient: rfc822; kijitora@example.co.jp if( $o->[0] eq 'final-recipient' ) { @@ -86,7 +86,7 @@ sub inquire { # X-Actual-Recipient: rfc822; kijitora@example.co.jp $v->{'alias'} = $o->[2]; } - } elsif( $o->[-1] eq 'code' ) { + } elsif( $o->[3] eq 'code' ) { # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown $v->{'spec'} = $o->[1]; $v->{'diagnosis'} = $o->[2]; @@ -111,7 +111,7 @@ sub inquire { # Other error messages if( index($e, '>>> ') == 0 ) { # >>> DATA (Client Command) - $thecommand = Sisimai::SMTP::Command->find($e); + $thecommand ||= Sisimai::SMTP::Command->find($e); } elsif( index($e, '<<< ') == 0 ) { # <<< Response from the SMTP server diff --git a/lib/Sisimai/Lhost/SurfControl.pm b/lib/Sisimai/Lhost/SurfControl.pm deleted file mode 100644 index 6d5eaf15f..000000000 --- a/lib/Sisimai/Lhost/SurfControl.pm +++ /dev/null @@ -1,151 +0,0 @@ -package Sisimai::Lhost::SurfControl; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -sub description { 'WebSense SurfControl' } -sub inquire { - # Detect an error from SurfControl - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decode or the arguments are missing - # @since v4.1.2 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - - # X-SEF-ZeroHour-RefID: fgs=000000000 - # X-SEF-Processed: 0_0_0_000__2010_04_29_23_34_45 - # X-Mailer: SurfControl E-mail Filter - return undef unless $mhead->{'x-sef-processed'}; - return undef unless $mhead->{'x-mailer'}; - return undef unless $mhead->{'x-mailer'} eq 'SurfControl E-mail Filter'; - - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['Content-Type: message/rfc822']; - state $startingof = { 'message' => ['Your message could not be sent.'] }; - - my $fieldtable = Sisimai::RFC1894->FIELDTABLE; - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $v = undef; - my $p = ''; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0; - next; - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - - # Your message could not be sent. - # A transcript of the attempts to send the message follows. - # The number of attempts made: 1 - # Addressed To: kijitora@example.com - # - # Thu 29 Apr 2010 23:34:45 +0900 - # Failed to send to identified host, - # kijitora@example.com: [192.0.2.5], 550 kijitora@example.com... No such user - # --- Message non-deliverable. - $v = $dscontents->[-1]; - - if( index($e, 'Addressed To:') == 0 && index($e, '@') > 1 ) { - # Addressed To: kijitora@example.com - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = Sisimai::Address->s3s4(substr($e, index($e, ':') + 2,)); - $recipients++; - - } elsif( grep { index($e, $_) == 0 } (qw|Sun Mon Tue Wed Thu Fri Sat|) ) { - # Thu 29 Apr 2010 23:34:45 +0900 - $v->{'date'} = $e; - - } elsif( Sisimai::String->aligned(\$e, ['@', ':', ' ', '[', '],', '...']) ) { - # kijitora@example.com: [192.0.2.5], 550 kijitora@example.com... No such user - my $p1 = index($e, '['); - my $p2 = index($e, '],', $p1 + 1); - $v->{'rhost'} = substr($e, $p1 + 1, $p2 - $p1 - 1); - $v->{'diagnosis'} = Sisimai::String->sweep(substr($e, $p2 + 2,)); - - } else { - # Fallback, read RFC3464 headers. - if( my $f = Sisimai::RFC1894->match($e) ) { - # $e matched with any field defined in RFC3464 - next unless my $o = Sisimai::RFC1894->field($e); - next if $o->[0] eq 'final-recipient'; - next unless exists $fieldtable->{ $o->[0] }; - $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - - } else { - # Continued line of the value of Diagnostic-Code field - next unless index($p, 'Diagnostic-Code:') == 0; - next unless index($e, ' ') == 0; - $v->{'diagnosis'} .= ' '.Sisimai::String->sweep($e); - } - } - } continue { - # Save the current line for the next loop - $p = $e; - } - return undef unless $recipients; - - $_->{'diagnosis'} = Sisimai::String->sweep($_->{'diagnosis'}) for @$dscontents; - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::SurfControl - bounce mail decoder class for SurfControl. - -=head1 SYNOPSIS - - use Sisimai::Lhost::SurfControl; - -=head1 DESCRIPTION - -C decodes a bounce email which created by WebSense SurfControl. -Methods in the module are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::SurfControl->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2014-2021,2023,2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/X4.pm b/lib/Sisimai/Lhost/X4.pm deleted file mode 100644 index 304929185..000000000 --- a/lib/Sisimai/Lhost/X4.pm +++ /dev/null @@ -1,317 +0,0 @@ -package Sisimai::Lhost::X4; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -sub description { 'Unknown MTA #4 qmail clones' } -sub inquire { - # Detect an error from Unknown MTA #4, qmail clones - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decode or the arguments are missing - # @since v4.1.23 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - my $match = 0; - my $tryto = [['(qmail ', 'invoked for bounce)'], ['(qmail ', 'invoked from ', 'network)']]; - - # Pre process email headers and the body part of the message which generated - # by qmail, see https://cr.yp.to/qmail.html - # e.g.) Received: (qmail 12345 invoked for bounce); 29 Apr 2009 12:34:56 -0000 - # Subject: failure notice - $match ||= 1 if index($mhead->{'subject'}, 'failure notice') == 0; - $match ||= 1 if index($mhead->{'subject'}, 'Permanent Delivery Failure') == 0; - for my $e ( $mhead->{'received'}->@* ) { - # Received: (qmail 2222 invoked for bounce);29 Apr 2017 23:34:45 +0900 - # Received: (qmail 2202 invoked from network); 29 Apr 2018 00:00:00 +0900 - $match ||= 1 if grep { Sisimai::String->aligned(\$e, $_) } $tryto->@*; - } - return undef unless $match; - - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['--- Below this line is a copy of the message.', 'Original message follows.']; - state $startingof = { - # qmail-remote.c:248| if (code >= 500) { - # qmail-remote.c:249| out("h"); outhost(); out(" does not like recipient.\n"); - # qmail-remote.c:265| if (code >= 500) quit("D"," failed on DATA command"); - # qmail-remote.c:271| if (code >= 500) quit("D"," failed after I sent the message"); - # - # Characters: K,Z,D in qmail-qmqpc.c, qmail-send.c, qmail-rspawn.c - # K = success, Z = temporary error, D = permanent error - 'error' => ['Remote host said:'], - 'message' => [ - 'He/Her is not ', - 'unable to deliver your message to the following addresses', - 'Su mensaje no pudo ser entregado', - 'This is the machine generated message from mail service', - 'This is the mail delivery agent at', - 'Unable to deliver message to the following address', - 'Unfortunately, your mail was not delivered to the following address:', - 'Your mail message to the following address', - 'Your message to the following addresses', - "We're sorry.", - ], - 'rhost' => ['Giving up on ', 'Connected to ', 'remote host '], - }; - state $commandset = { - # Error text regular expressions which defined in qmail-remote.c - # qmail-remote.c:225| if (smtpcode() != 220) quit("ZConnected to "," but greeting failed"); - 'conn' => [' but greeting failed.'], - # qmail-remote.c:231| if (smtpcode() != 250) quit("ZConnected to "," but my name was rejected"); - 'ehlo' => [' but my name was rejected.'], - # qmail-remote.c:238| if (code >= 500) quit("DConnected to "," but sender was rejected"); - # reason = rejected - 'mail' => [' but sender was rejected.'], - # qmail-remote.c:249| out("h"); outhost(); out(" does not like recipient.\n"); - # qmail-remote.c:253| out("s"); outhost(); out(" does not like recipient.\n"); - # reason = userunknown - 'rcpt' => [' does not like recipient.'], - # qmail-remote.c:265| if (code >= 500) quit("D"," failed on DATA command"); - # qmail-remote.c:266| if (code >= 400) quit("Z"," failed on DATA command"); - # qmail-remote.c:271| if (code >= 500) quit("D"," failed after I sent the message"); - # qmail-remote.c:272| if (code >= 400) quit("Z"," failed after I sent the message"); - 'data' => [' failed on DATA command', ' failed after I sent the message'], - }; - state $resmtp = { - # Error text regular expressions which defined in qmail-remote.c - # qmail-remote.c:225| if (smtpcode() != 220) quit("ZConnected to "," but greeting failed"); - 'conn' => qr/(?:Error:)?Connected to [^ ]+ but greeting failed[.]/, - # qmail-remote.c:231| if (smtpcode() != 250) quit("ZConnected to "," but my name was rejected"); - 'ehlo' => qr/(?:Error:)?Connected to [^ ]+ but my name was rejected[.]/, - # qmail-remote.c:238| if (code >= 500) quit("DConnected to "," but sender was rejected"); - # reason = rejected - 'mail' => qr/(?:Error:)?Connected to [^ ]+ but sender was rejected[.]/, - # qmail-remote.c:249| out("h"); outhost(); out(" does not like recipient.\n"); - # qmail-remote.c:253| out("s"); outhost(); out(" does not like recipient.\n"); - # reason = userunknown - 'rcpt' => qr/(?:Error:)?[^ ]+ does not like recipient[.]/, - # qmail-remote.c:265| if (code >= 500) quit("D"," failed on DATA command"); - # qmail-remote.c:266| if (code >= 400) quit("Z"," failed on DATA command"); - # qmail-remote.c:271| if (code >= 500) quit("D"," failed after I sent the message"); - # qmail-remote.c:272| if (code >= 400) quit("Z"," failed after I sent the message"); - 'data' => qr{(?: - (?:Error:)?[^ ]+[ ]failed[ ]on[ ]DATA[ ]command[.] - |(?:Error:)?[^ ]+[ ]failed[ ]after[ ]I[ ]sent[ ]the[ ]message[.] - ) - }x, - }; - - # qmail-send.c:922| ... (&dline[c],"I'm not going to try again; this message has been in the queue too long.\n")) nomem(); - state $hasexpired = 'this message has been in the queue too long.'; - # qmail-remote-fallback.patch - state $onholdpair = [' does not like recipient.', 'this message has been in the queue too long.']; - state $failonldap = { - # qmail-ldap-1.03-20040101.patch:19817 - 19866 - 'suspend' => ['Mailaddress is administrative?le?y disabled'], # 5.2.1 - 'userunknown' => ['Sorry, no mailbox here by that name'], # 5.1.1 - 'exceedlimit' => ['The message exeeded the maximum size the user accepts'], # 5.2.3 - 'systemerror' => [ - 'Automatic homedir creator crashed', # 4.3.0 - 'Illegal value in LDAP attribute', # 5.3.5 - 'LDAP attribute is not given but mandatory', # 5.3.5 - 'Timeout while performing search on LDAP server', # 4.4.3 - 'Too many results returned but needs to be unique', # 5.3.5 - 'Permanent error while executing qmail-forward', # 5.4.4 - 'Temporary error in automatic homedir creation', # 4.3.0 or 5.3.0 - 'Temporary error while executing qmail-forward', # 4.4.4 - 'Temporary failure in LDAP lookup', # 4.4.3 - 'Unable to contact LDAP server', # 4.4.3 - 'Unable to login into LDAP server, bad credentials',# 4.4.3 - ], - }; - state $messagesof = { - # qmail-local.c:589| strerr_die1x(100,"Sorry, no mailbox here by that name. (#5.1.1)"); - # qmail-remote.c:253| out("s"); outhost(); out(" does not like recipient.\n"); - 'userunknown' => [ - 'no mailbox here by that name', - 'does not like recipient.', - ], - # error_str.c:192| X(EDQUOT,"disk quota exceeded") - 'mailboxfull' => ['disk quota exceeded'], - # qmail-qmtpd.c:233| ... result = "Dsorry, that message size exceeds my databytes limit (#5.3.4)"; - # qmail-smtpd.c:391| ... out("552 sorry, that message size exceeds my databytes limit (#5.3.4)\r\n"); return; - 'mesgtoobig' => ['Message size exceeds fixed maximum message size:'], - # qmail-remote.c:68| Sorry, I couldn't find any host by that name. (#4.1.2)\n"); zerodie(); - # qmail-remote.c:78| Sorry, I couldn't find any host named "); - 'hostunknown' => ["Sorry, I couldn't find any host "], - 'systemfull' => ['Requested action not taken: mailbox unavailable (not enough free space)'], - 'systemerror' => [ - 'bad interpreter: No such file or directory', - 'system error', - 'Unable to', - ], - 'networkerror'=> [ - "Sorry, I wasn't able to establish an SMTP connection", - "Sorry, I couldn't find a mail exchanger or IP address", - "Sorry. Although I'm listed as a best-preference MX or A for that host", - ], - }; - - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $v = undef; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if grep { index($e, $_) > -1 } $startingof->{'message'}->@*; - next; - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - - # : - # 192.0.2.153 does not like recipient. - # Remote host said: 550 5.1.1 ... User Unknown - # Giving up on 192.0.2.153. - $v = $dscontents->[-1]; - - if( index($e, '<') == 0 && Sisimai::String->aligned(\$e, ['<', '@', '>', ':']) ) { - # : - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = Sisimai::Address->s3s4(substr($e, index($e, '<'),)); - $recipients++; - - } elsif( scalar @$dscontents == $recipients ) { - # Append error message - next unless length $e; - $v->{'diagnosis'} .= $e.' '; - $v->{'alterrors'} = $e if index($e, $startingof->{'error'}->[0]) == 0; - - next if $v->{'rhost'}; - for my $r ( $startingof->{'rhost'}->@* ) { - # Find a remote host name - my $p1 = index($e, $r); next if $p1 == -1; - my $cm = length $r; - my $p2 = index($e, ' ', $p1 + $cm + 1); $p2 = rindex($e, '.') if $p2 == -1; - - $v->{'rhost'} = Sisimai::String->sweep(substr($e, $p1 + $cm, $p2 - $p1 - $cm)); - last; - } - } - } - return undef unless $recipients; - - for my $e ( @$dscontents ) { - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); - - unless( $e->{'command'} ) { - # Get the SMTP command name for the session - SMTP: for my $r ( keys %$commandset ) { - # Verify each regular expression of SMTP commands - next unless grep { index($e->{'diagnosis'}, $_) > 0 } $commandset->{ $r }->@*; - $e->{'command'} = uc $r; - last; - } - - if( index($e->{'diagnosis'}, 'Sorry, no SMTP connection got far enough; most progress was ') > -1 ) { - # Get the last SMTP command:from the error message - $e->{'command'} ||= Sisimai::SMTP::Command->find($e->{'diagnosis'}); - } - } - - # Detect the reason of bounce - if( $e->{'command'} eq 'MAIL' ) { - # MAIL | Connected to 192.0.2.135 but sender was rejected. - $e->{'reason'} = 'rejected'; - - } elsif( $e->{'command'} eq 'HELO' || $e->{'command'} eq 'EHLO' ) { - # HELO | Connected to 192.0.2.135 but my name was rejected. - $e->{'reason'} = 'blocked'; - - } else { - # Try to match with each error message in the table - if( Sisimai::String->aligned(\$e->{'diagnosis'}, $onholdpair) ) { - # To decide the reason require pattern match with Sisimai::Reason::* modules - $e->{'reason'} = 'onhold'; - - } else { - SESSION: for my $r ( keys %$messagesof ) { - # Verify each regular expression of session errors - if( $e->{'alterrors'} ) { - # Check the value of "alterrors" - next unless grep { index($e->{'alterrors'}, $_) > -1 } $messagesof->{ $r }->@*; - $e->{'reason'} = $r; - } - last if $e->{'reason'}; - - next unless grep { index($e->{'diagnosis'}, $_) > -1 } $messagesof->{ $r }->@*; - $e->{'reason'} = $r; - last; - } - - unless( $e->{'reason'} ) { - LDAP: for my $r ( keys %$failonldap ) { - # Verify each regular expression of LDAP errors - next unless grep { index($e->{'diagnosis'}, $_) > -1 } $failonldap->{ $r }->@*; - $e->{'reason'} = $r; - last; - } - } - - unless( $e->{'reason'} ) { - $e->{'reason'} = 'expired' if index($e->{'diagnosis'}, $hasexpired) > -1; - } - } - } - $e->{'command'} ||= Sisimai::SMTP::Command->find($e->{'diagnosis'}); - } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::X4 - bounce mail decoder class for Unknown MTA which is developed as a qmail clone. - -=head1 SYNOPSIS - - use Sisimai::Lhost::X4; - -=head1 DESCRIPTION - -C decodes a bounce email which created by some qmail clone. Methods in the module -are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::X4->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2015-2023,2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/X5.pm b/lib/Sisimai/Lhost/X5.pm deleted file mode 100644 index 88c7c5dfc..000000000 --- a/lib/Sisimai/Lhost/X5.pm +++ /dev/null @@ -1,160 +0,0 @@ -package Sisimai::Lhost::X5; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -sub description { 'Unknown MTA #5' } -sub inquire { - # Detect an error from Unknown MTA #5 - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decode or the arguments are missing - # @since v4.13.0 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - my $match = 0; - my $plain = ''; - - $match++ if defined $mhead->{'to'} && rindex($mhead->{'to'}, 'NotificationRecipients') > -1; - if( rindex($mhead->{'from'}, 'TWFpbCBEZWxpdmVyeSBTdWJzeXN0ZW0') > -1 ) { - # From: "=?iso-2022-jp?B?TWFpbCBEZWxpdmVyeSBTdWJzeXN0ZW0=?=" <...> - # Mail Delivery Subsystem - for my $f ( split(' ', $mhead->{'from'}) ) { - # Check each element of From: header - next unless Sisimai::RFC2045->is_encoded(\$f); - $match++ if rindex(Sisimai::RFC2045->decodeH([$f]), 'Mail Delivery Subsystem') > -1; - last; - } - } - - if( Sisimai::RFC2045->is_encoded(\$mhead->{'subject'}) ) { - # Subject: =?iso-2022-jp?B?UmV0dXJuZWQgbWFpbDogVXNlciB1bmtub3du?= - $plain = Sisimai::RFC2045->decodeH([$mhead->{'subject'}]); - $match++ if rindex($plain, 'Mail Delivery Subsystem') > -1; - } - return undef if $match < 2; - - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['Content-Type: message/rfc822']; - state $startingof = { 'message' => ['Content-Type: message/delivery-status'] }; - - my $fieldtable = Sisimai::RFC1894->FIELDTABLE; - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $v = undef; - my $p = ''; - - # Pick the second message/rfc822 part because the format of email-x5-*.eml is nested structure - my $cutsbefore = [split($boundaries->[0], $$mbody, 2)]; - $cutsbefore->[1] = substr($cutsbefore->[1], index($cutsbefore->[1], "\n\n") + 2,); - my $emailparts = Sisimai::RFC5322->part(\$cutsbefore->[1], $boundaries); - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0; - next; - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - - $v = $dscontents->[-1]; - if( Sisimai::RFC1894->match($e) ) { - # $e matched with any field defined in RFC3464 - next unless my $o = Sisimai::RFC1894->field($e); - $v = $dscontents->[-1]; - - if( $o->[-1] eq 'addr' ) { - # Final-Recipient: rfc822; kijitora@example.jp - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - if( $o->[0] eq 'final-recipient' ) { - # Final-Recipient: rfc822; kijitora@example.jp - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = $o->[2]; - $recipients++; - - } else { - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - $v->{'alias'} = $o->[2]; - } - } elsif( $o->[-1] eq 'code' ) { - # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown - $v->{'spec'} = $o->[1]; - $v->{'diagnosis'} = $o->[2]; - - } else { - # Other DSN fields defined in RFC3464 - next unless exists $fieldtable->{ $o->[0] }; - $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - } - } else { - # Continued line of the value of Diagnostic-Code field - next unless index($p, 'Diagnostic-Code:') == 0; - next unless index($e, ' ') == 0; - $v->{'diagnosis'} .= ' '.Sisimai::String->sweep($e); - } - } continue { - # Save the current line for the next loop - $p = $e; - } - return undef unless $recipients; - - $_->{'diagnosis'} ||= Sisimai::String->sweep($_->{'diagnosis'}) for @$dscontents; - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::X5 - bounce mail decoder class for unknown MTA #5. - -=head1 SYNOPSIS - - use Sisimai::Lhost::X5; - -=head1 DESCRIPTION - -C decodes a bounce email which created by Unknown MTA #5. Methods in the module -are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::X5->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2015-2021,2023,2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/Yahoo.pm b/lib/Sisimai/Lhost/Yahoo.pm deleted file mode 100644 index 8a9c623ea..000000000 --- a/lib/Sisimai/Lhost/Yahoo.pm +++ /dev/null @@ -1,147 +0,0 @@ -package Sisimai::Lhost::Yahoo; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -sub description { 'Yahoo MAIL: https://mail.yahoo.com/' } -sub inquire { - # Detect an error from Yahoo Mail - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decodes or the arguments are missing - # @since v4.1.3 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - - # X-YMailISG: YtyUVyYWLDsbDh... - # X-YMail-JAS: Pb65aU4VM1mei... - # X-YMail-OSG: bTIbpDEVM1lHz... - # X-Originating-IP: [192.0.2.9] - return undef unless $mhead->{'x-ymailisg'}; - - require Sisimai::SMTP::Command; - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['--- Below this line is a copy of the message.']; - state $startingof = { 'message' => ['Sorry, we were unable to deliver your message'] }; - - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $v = undef; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0; - next; - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - - # Sorry, we were unable to deliver your message to the following address. - # - # : - # Remote host said: 550 5.1.1 ... User Unknown [RCPT_TO] - $v = $dscontents->[-1]; - - if( index($e, '<') == 0 && Sisimai::String->aligned(\$e, ['<', '@', '>:']) ) { - # : - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = Sisimai::Address->s3s4(substr($e, 0, index($e, '>:'))); - $recipients++; - - } else { - if( index($e, 'Remote host said:') == 0 ) { - # Remote host said: 550 5.1.1 ... User Unknown [RCPT_TO] - $v->{'diagnosis'} = $e; - - # Get SMTP command from the value of "Remote host said:" - $v->{'command'} = Sisimai::SMTP::Command->find($e); - } else { - # : - # Remote host said: - # 550 5.2.2 ... Mailbox Full - # [RCPT_TO] - if( $v->{'diagnosis'} eq 'Remote host said:' ) { - # Remote host said: - # 550 5.2.2 ... Mailbox Full - if( my $cv = Sisimai::SMTP::Command->find($e) ) { - # [RCPT_TO] - $v->{'command'} = $cv; - - } else { - # 550 5.2.2 ... Mailbox Full - $v->{'diagnosis'} = $e; - } - } else { - # Error message which does not start with 'Remote host said:' - $v->{'diagnosis'} .= ' '.$e; - } - } - } - } - return undef unless $recipients; - - for my $e ( @$dscontents ) { - $e->{'diagnosis'} =~ y/\n/ /; - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); - $e->{'command'} ||= 'RCPT' if Sisimai::String->aligned(\$e->{'diagnosis'}, ['<', '@', '>']); - } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::Yahoo - bounce mail decoder class for Yahoo Mail L. - -=head1 SYNOPSIS - - use Sisimai::Lhost::Yahoo; - -=head1 DESCRIPTION - -C decodes a bounce email which created by Yahoo Mail L. -Methods in the module are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::Yahoo->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2014-2021,2023,2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/Yandex.pm b/lib/Sisimai/Lhost/Yandex.pm deleted file mode 100644 index f9f89e917..000000000 --- a/lib/Sisimai/Lhost/Yandex.pm +++ /dev/null @@ -1,167 +0,0 @@ -package Sisimai::Lhost::Yandex; -use parent 'Sisimai::Lhost'; -use v5.26; -use strict; -use warnings; - -sub description { 'Yandex Mail: https://360.yandex.com/mail/' } -sub inquire { - # Detect an error from Yandex Mail - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decode or the arguments are missing - # @since v4.1.6 - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - - # X-Yandex-Front: mxback1h.mail.yandex.net - # X-Yandex-TimeMark: 1417885948 - # X-Yandex-Uniq: 92309766-f1c8-4bd4-92bc-657c75766587 - # X-Yandex-Spam: 1 - # X-Yandex-Forward: 10104c00ad0726da5f37374723b1e0c8 - # X-Yandex-Queue-ID: 367D79E130D - # X-Yandex-Sender: rfc822; shironeko@yandex.example.com - return undef unless $mhead->{'x-yandex-uniq'}; - return undef unless $mhead->{'from'} eq 'mailer-daemon@yandex.ru'; - - require Sisimai::SMTP::Command; - state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['Content-Type: message/rfc822']; - state $startingof = { 'message' => ['This is the mail system at host yandex.ru.'] }; - - my $fieldtable = Sisimai::RFC1894->FIELDTABLE; - my $permessage = {}; # (Hash) Store values of each Per-Message field - my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; - my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); - my $readcursor = 0; # (Integer) Points the current cursor position - my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my @commandset; # (Array) ``in reply to * command'' list - my $v = undef; - my $p = ''; - - for my $e ( split("\n", $emailparts->[0]) ) { - # Read error messages and delivery status lines from the head of the email to the previous - # line of the beginning of the original message. - unless( $readcursor ) { - # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0; - next; - } - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; - - if( my $f = Sisimai::RFC1894->match($e) ) { - # $e matched with any field defined in RFC3464 - next unless my $o = Sisimai::RFC1894->field($e); - $v = $dscontents->[-1]; - - if( $o->[-1] eq 'addr' ) { - # Final-Recipient: rfc822; kijitora@example.jp - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - if( $o->[0] eq 'final-recipient' ) { - # Final-Recipient: rfc822; kijitora@example.jp - if( $v->{'recipient'} ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, __PACKAGE__->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = $o->[2]; - $recipients++; - - } else { - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - $v->{'alias'} = $o->[2]; - } - } elsif( $o->[-1] eq 'code' ) { - # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown - $v->{'spec'} = $o->[1]; - $v->{'diagnosis'} = $o->[2]; - - } else { - # Other DSN fields defined in RFC3464 - next unless exists $fieldtable->{ $o->[0] }; - $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - - next unless $f == 1; - $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2]; - } - } else { - # The line does not begin with a DSN field defined in RFC3464 - # : host mx.example.jp[192.0.2.153] said: 550 - # 5.1.1 ... User Unknown (in reply to RCPT TO - # command) - if( index($e, ' (in reply to ') > -1 || index($e, 'command)') > -1 ) { - # 5.1.1 ... User Unknown (in reply to RCPT TO - my $cv = Sisimai::SMTP::Command->find($e); - push @commandset, $cv if $cv; - - } else { - # Continued line of the value of Diagnostic-Code field - next unless index($p, 'Diagnostic-Code:') == 0; - next unless index($e, ' ') == 0; - $v->{'diagnosis'} .= ' '.Sisimai::String->sweep($e); - } - } - } continue { - # Save the current line for the next loop - $p = $e; - } - return undef unless $recipients; - - for my $e ( @$dscontents ) { - # Set default values if each value is empty. - $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage; - $e->{'diagnosis'} =~ y/\n/ /; - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); - $e->{'command'} ||= shift @commandset || Sisimai::SMTP::Command->find($e->{'diagnosis'}) || ''; - } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::Lhost::Yandex - bounce mail decoder class for Yandex Mail L. - -=head1 SYNOPSIS - - use Sisimai::Lhost::Yandex; - -=head1 DESCRIPTION - -C decodes a bounce email which created by Yandex Mail L. -Methods in the module are called from only C. - -=head1 CLASS METHODS - -=head2 C> - -C returns description string of this module. - - print Sisimai::Lhost::Yandex->description; - -=head2 C, I)>> - -C method decodes a bounced email and return results as a array reference. -See C for more details. - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2014-2021,2023,2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Lhost/qmail.pm b/lib/Sisimai/Lhost/qmail.pm index 9a9621bd5..9af98d52f 100644 --- a/lib/Sisimai/Lhost/qmail.pm +++ b/lib/Sisimai/Lhost/qmail.pm @@ -15,24 +15,37 @@ sub inquire { my $class = shift; my $mhead = shift // return undef; my $mbody = shift // return undef; - my $match = 0; - my $tryto = [['(qmail ', 'invoked for bounce)'], ['(qmail ', 'invoked from ', 'network)']]; # Pre process email headers and the body part of the message which generated by qmail. # see https://cr.yp.to/qmail.html # e.g.) Received: (qmail 12345 invoked for bounce); 29 Apr 2009 12:34:56 -0000 # Subject: failure notice - $match ||= 1 if $mhead->{'subject'} eq 'failure notice'; - for my $e ( $mhead->{'received'}->@* ) { + my $proceedsto = 0; + my $relayedvia = [["(qmail ", "invoked for bounce)"], ["(qmail ", "invoked from ", "network)"]]; + my $emailtitle = [ + "failure notice", # qmail-send.c:Subject: failure notice\n\ + "Failure Notice", # Yahoo + ]; + $proceedsto++ if grep { $mhead->{"subject"} eq $_ } @$emailtitle; + + for my $e ( $mhead->{"received"}->@* ) { # Received: (qmail 2222 invoked for bounce);29 Apr 2017 23:34:45 +0900 # Received: (qmail 2202 invoked from network); 29 Apr 2018 00:00:00 +0900 - $match ||= 1 if grep { Sisimai::String->aligned(\$e, $_) } $tryto->@*; + $proceedsto ||= 1 if grep { Sisimai::String->aligned(\$e, $_) } $relayedvia->@*; } - return undef unless $match; + return undef if $proceedsto == 0; require Sisimai::SMTP::Command; state $indicators = __PACKAGE__->INDICATORS; - state $boundaries = ['--- Below this line is a copy of the message.']; + state $boundaries = [ + # qmail-send.c:qmail_puts(&qqt,*sender.s ? "--- Below this line is a copy of the message.\n\n" :... + "--- Below this line is a copy of the message.", # qmail-1.03 + "--- Below this line is a copy of the mail header.", + "--- Below the next line is a copy of the message.", # The followings are the qmail clone + "--- Mensaje original adjunto.", + "Content-Type: message/rfc822", + "Original message follows.", + ]; state $startingof = { # qmail-remote.c:248| if (code >= 500) { # qmail-remote.c:249| out("h"); outhost(); out(" does not like recipient.\n"); @@ -41,84 +54,98 @@ sub inquire { # # Characters: K,Z,D in qmail-qmqpc.c, qmail-send.c, qmail-rspawn.c # K = success, Z = temporary error, D = permanent error - 'error' => ['Remote host said:'], - 'message' => ['Hi. This is the qmail'], - 'rhost' => ['Giving up on ', 'Connected to ', 'remote host '], + "error" => ["Remote host said:"], + "message" => [ + "Hi. This is the qmail", # qmail-send.c:Hi. This is the qmail-send program at "); + "He/Her is not ", # The followings are the qmail clone + "unable to deliver your message to the following addresses", + "Su mensaje no pudo ser entregado", + "Sorry, we were unable to deliver your message to the following address", + "This is the machine generated message from mail service", + "This is the mail delivery agent at", + "Unable to deliver message to the following address", + "unable to deliver your message to the following addresses", + "Unfortunately, your mail was not delivered to the following address:", + "Your mail message to the following address", + "Your message to the following addresses", + "We're sorry.", + ], + "rhost" => ['Giving up on ', 'Connected to ', 'remote host '], }; state $commandset = { # Error text regular expressions which defined in qmail-remote.c # qmail-remote.c:225| if (smtpcode() != 220) quit("ZConnected to "," but greeting failed"); - 'CONN' => [' but greeting failed.'], + "CONN" => [" but greeting failed."], # qmail-remote.c:231| if (smtpcode() != 250) quit("ZConnected to "," but my name was rejected"); - 'EHLO' => [' but my name was rejected.'], + "EHLO" => [" but my name was rejected."], # qmail-remote.c:238| if (code >= 500) quit("DConnected to "," but sender was rejected"); # reason = rejected - 'MAIL' => [' but sender was rejected.'], + "MAIL" => [" but sender was rejected."], # qmail-remote.c:249| out("h"); outhost(); out(" does not like recipient.\n"); # qmail-remote.c:253| out("s"); outhost(); out(" does not like recipient.\n"); # reason = userunknown - 'RCPT' => [' does not like recipient.'], + "RCPT" => [" does not like recipient."], # qmail-remote.c:265| if (code >= 500) quit("D"," failed on DATA command"); # qmail-remote.c:266| if (code >= 400) quit("Z"," failed on DATA command"); # qmail-remote.c:271| if (code >= 500) quit("D"," failed after I sent the message"); # qmail-remote.c:272| if (code >= 400) quit("Z"," failed after I sent the message"); - 'DATA' => [' failed on DATA command', ' failed after I sent the message'], + "DATA" => [" failed on DATA command", " failed after I sent the message"], }; # qmail-send.c:922| ... (&dline[c],"I'm not going to try again; this message has been in the queue too long.\n")) nomem(); - state $hasexpired = 'this message has been in the queue too long.'; # qmail-remote-fallback.patch - state $onholdpair = [' does not like recipient.', 'this message has been in the queue too long.']; + state $hasexpired = "this message has been in the queue too long."; + state $onholdpair = [" does not like recipient.", "this message has been in the queue too long."]; state $failonldap = { # qmail-ldap-1.03-20040101.patch:19817 - 19866 - 'exceedlimit' => ['The message exeeded the maximum size the user accepts'], # 5.2.3 - 'userunknown' => ['Sorry, no mailbox here by that name'], # 5.1.1 - 'suspend' => [ # 5.2.1 - 'Mailaddress is administrativly disabled', - 'Mailaddress is administrativley disabled', - 'Mailaddress is administratively disabled', - 'Mailaddress is administrativeley disabled', + "exceedlimit" => ["The message exeeded the maximum size the user accepts"], # 5.2.3 + "userunknown" => ["Sorry, no mailbox here by that name"], # 5.1.1 + "suspend" => [ # 5.2.1 + "Mailaddress is administrativly disabled", + "Mailaddress is administrativley disabled", + "Mailaddress is administratively disabled", + "Mailaddress is administrativeley disabled", ], - 'systemerror' => [ - 'Automatic homedir creator crashed', # 4.3.0 - 'Illegal value in LDAP attribute', # 5.3.5 - 'LDAP attribute is not given but mandatory', # 5.3.5 - 'Timeout while performing search on LDAP server', # 4.4.3 - 'Too many results returned but needs to be unique', # 5.3.5 - 'Permanent error while executing qmail-forward', # 5.4.4 - 'Temporary error in automatic homedir creation', # 4.3.0 or 5.3.0 - 'Temporary error while executing qmail-forward', # 4.4.4 - 'Temporary failure in LDAP lookup', # 4.4.3 - 'Unable to contact LDAP server', # 4.4.3 - 'Unable to login into LDAP server, bad credentials',# 4.4.3 + "systemerror" => [ + "Automatic homedir creator crashed", # 4.3.0 + "Illegal value in LDAP attribute", # 5.3.5 + "LDAP attribute is not given but mandatory", # 5.3.5 + "Timeout while performing search on LDAP server", # 4.4.3 + "Too many results returned but needs to be unique", # 5.3.5 + "Permanent error while executing qmail-forward", # 5.4.4 + "Temporary error in automatic homedir creation", # 4.3.0 or 5.3.0 + "Temporary error while executing qmail-forward", # 4.4.4 + "Temporary failure in LDAP lookup", # 4.4.3 + "Unable to contact LDAP server", # 4.4.3 + "Unable to login into LDAP server, bad credentials",# 4.4.3 ], }; state $messagesof = { - # qmail-local.c:589| strerr_die1x(100,"Sorry, no mailbox here by that name. (#5.1.1)"); - # qmail-remote.c:253| out("s"); outhost(); out(" does not like recipient.\n"); - 'userunknown' => ['no mailbox here by that name'], + # qmail-remote.c:68| Sorry, I couldn't find any host by that name. (#4.1.2)\n"); zerodie(); + # qmail-remote.c:78| Sorry, I couldn't find any host named "); + "hostunknown" => ["Sorry, I couldn't find any host "], # error_str.c:192| X(EDQUOT,"disk quota exceeded") - 'mailboxfull' => ['disk quota exceeded'], + "mailboxfull" => ["disk quota exceeded"], # qmail-qmtpd.c:233| ... result = "Dsorry, that message size exceeds my databytes limit (#5.3.4)"; # qmail-smtpd.c:391| ... out("552 sorry, that message size exceeds my databytes limit (#5.3.4)\r\n"); return; - 'mesgtoobig' => ['Message size exceeds fixed maximum message size:'], - # qmail-remote.c:68| Sorry, I couldn't find any host by that name. (#4.1.2)\n"); zerodie(); - # qmail-remote.c:78| Sorry, I couldn't find any host named "); - 'hostunknown' => ["Sorry, I couldn't find any host "], - 'systemfull' => ['Requested action not taken: mailbox unavailable (not enough free space)'], - 'systemerror' => [ - 'bad interpreter: No such file or directory', - 'system error', - 'Unable to', + "mesgtoobig" => ["Message size exceeds fixed maximum message size:"], + "networkerror"=> [ + "Sorry, I wasn't able to establish an SMTP connection", + "Sorry. Although I'm listed as a best-preference MX or A for that host", ], - 'notaccept' => [ + "notaccept" => [ # notqmail 1.08 returns the following error message when the destination MX is NullMX "Sorry, I couldn't find a mail exchanger or IP address", ], - 'networkerror'=> [ - "Sorry, I wasn't able to establish an SMTP connection", - "Sorry. Although I'm listed as a best-preference MX or A for that host", + "systemerror" => [ + "bad interpreter: No such file or directory", + "system error", + "Unable to", ], + "systemfull" => ["Requested action not taken: mailbox unavailable (not enough free space)"], + # qmail-local.c:589| strerr_die1x(100,"Sorry, no mailbox here by that name. (#5.1.1)"); + # qmail-remote.c:253| out("s"); outhost(); out(" does not like recipient.\n"); + "userunknown" => ["no mailbox here by that name"], }; my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; @@ -132,7 +159,7 @@ sub inquire { # line of the beginning of the original message. unless( $readcursor ) { # Beginning of the bounce message or message/delivery-status part - $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0; + $readcursor |= $indicators->{'deliverystatus'} if grep { index($e, $_) > -1 } $startingof->{'message'}->@*; next; } next unless $readcursor & $indicators->{'deliverystatus'}; @@ -174,58 +201,57 @@ sub inquire { return undef unless $recipients; for my $e ( @$dscontents ) { - $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); + # Tidy up the error message in $e->{'diagnosis'}, Try to detect the bounce reason. + $e->{"diagnosis"} = Sisimai::String->sweep($e->{"diagnosis"}); # Get the SMTP command name for the session SMTP: for my $r ( keys %$commandset ) { - # Verify each regular expression of SMTP commands - next unless grep { index($e->{'diagnosis'}, $_) > 0 } $commandset->{ $r }->@*; - $e->{'command'} = $r; + # Get the last SMTP Command + next unless grep { index($e->{"diagnosis"}, $_) > 0 } $commandset->{ $r }->@*; + $e->{"command"} = $r; last; } - - if( index($e->{'diagnosis'}, 'Sorry, no SMTP connection got far enough; most progress was ') > -1 ) { - # Get the last SMTP command:from the error message - $e->{'command'} ||= Sisimai::SMTP::Command->find($e->{'diagnosis'}) || ''; + if( index($e->{"diagnosis"}, "no SMTP connection got far enough") > -1 ) { + # Sorry, no SMTP connection got far enough; most progress was RCPT TO response; ... + $e->{"command"} ||= Sisimai::SMTP::Command->find($e->{"diagnosis"}); } # Detect the reason of bounce - if( $e->{'command'} eq 'HELO' || $e->{'command'} eq 'EHLO' ) { + if( $e->{"command"} eq "HELO" || $e->{"command"} eq "EHLO" ) { # HELO | Connected to 192.0.2.135 but my name was rejected. - $e->{'reason'} = 'blocked'; + $e->{"reason"} = "blocked"; } else { - # Try to match with each error message in the table - if( Sisimai::String->aligned(\$e->{'diagnosis'}, $onholdpair) ) { - # To decide the reason require pattern match with Sisimai::Reason::* modules - $e->{'reason'} = 'onhold'; + # The error message includes any of patterns defined in the variable avobe + if( Sisimai::String->aligned(\$e->{"diagnosis"}, $onholdpair) ) { + # Need to be matched with error message pattens defined in Sisimai/Reason/* + $e->{"reason"} = "onhold"; } else { # Check that the error message includes any of message patterns or not - FINDREASON: for my $f ( $e->{'alterrors'}, $e->{'diagnosis'} ) { + FINDREASON: for my $f ( $e->{"alterrors"}, $e->{"diagnosis"} ) { # Try to detect an error reason - last if $e->{'reason'}; + last if $e->{"reason"}; next unless $f; MESG: for my $r ( keys %$messagesof ) { # The key is a bounce reason name next unless grep { index($f, $_) > -1 } $messagesof->{ $r }->@*; - $e->{'reason'} = $r; + $e->{"reason"} = $r; last FINDREASON; } LDAP: for my $r ( keys %$failonldap ) { # The key is a bounce reason name next unless grep { index($f, $_) > -1 } $failonldap->{ $r }->@*; - $e->{'reason'} = $r; + $e->{"reason"} = $r; last FINDREASON; } - $e->{'reason'} = 'expired' if index($f, $hasexpired) > -1; + $e->{"reason"} = "expired" if index($f, $hasexpired) > -1; } } } - $e->{'command'} ||= Sisimai::SMTP::Command->find($e->{'diagnosis'}); - $e->{'status'} = Sisimai::SMTP::Status->find($e->{'diagnosis'}) || ''; + $e->{"command"} ||= Sisimai::SMTP::Command->find($e->{"diagnosis"}); } - return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; + return { "ds" => $dscontents, "rfc822" => $emailparts->[1] }; } 1; diff --git a/lib/Sisimai/MDA.pm b/lib/Sisimai/MDA.pm deleted file mode 100644 index c7385b4f5..000000000 --- a/lib/Sisimai/MDA.pm +++ /dev/null @@ -1,183 +0,0 @@ -package Sisimai::MDA; -use v5.26; -use strict; -use warnings; - -sub inquire { - # Decode the message body and return the MDA name, the reason and the error message text - # @param [Hash] mhead Message headers of a bounce email - # @param [String] mbody Message body of a bounce email - # @return [Hash] Bounce data list and message/rfc822 part - # @return [undef] failed to decode or the arguments are missing - my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - my $mfrom = lc $mhead->{'from'}; - my $match = 0; - - while(1) { - $match ||= 1 if index($mfrom, 'mail delivery subsystem') == 0; - $match ||= 1 if index($mfrom, 'mailer-daemon') == 0; - $match ||= 1 if index($mfrom, 'postmaster') == 0; - last; - } - return undef unless $match > 0; - - state $agentnames = { - # dovecot/src/deliver/deliver.c - # 11: #define DEFAULT_MAIL_REJECTION_HUMAN_REASON \ - # 12: "Your message to <%t> was automatically rejected:%n%r" - 'dovecot' => ['Your message to ', ' was automatically rejected:'], - 'mail.local' => ['mail.local: '], - 'procmail' => ['procmail: '], - 'maildrop' => ['maildrop: '], - 'vpopmail' => ['vdelivermail: '], - 'vmailmgr' => ['vdeliver: '], - }; - - # dovecot/src/deliver/mail-send.c:94 - state $messagesof = { - 'dovecot' => { - 'userunknown' => ["mailbox doesn't exist: "], - 'mailboxfull' => [ - 'quota exceeded', # Dovecot 1.2 dovecot/src/plugins/quota/quota.c - 'quota exceeded (mailbox for user is full)', # dovecot/src/plugins/quota/quota.c - 'not enough disk space', - ], - }, - 'mail.local' => { - 'userunknown' => [ - ': unknown user:', - ': user unknown', - ': invalid mailbox path', - ': user missing home directory', - ], - 'mailboxfull' => [ - 'disc quota exceeded', - 'mailbox full or quota exceeded', - ], - 'systemerror' => ['temporary file write error'], - }, - 'procmail' => { - 'mailboxfull' => ['quota exceeded while writing'], - 'systemfull' => ['no space left to finish writing'], - }, - 'maildrop' => { - 'userunknown' => [ - 'invalid user specified.', - 'cannot find system user', - ], - 'mailboxfull' => ['maildir over quota.'], - }, - 'vpopmail' => { - 'userunknown' => ['sorry, no mailbox here by that name.'], - 'filtered' => [ - 'account is locked email bounced', - 'user does not exist, but will deliver to ' - ], - 'mailboxfull' => [ - 'domain is over quota', - 'user is over quota', - ], - }, - 'vmailmgr' => { - 'userunknown' => [ - 'invalid or unknown base user or domain', - 'invalid or unknown virtual user', - 'user name does not refer to a virtual user' - ], - 'mailboxfull' => ['delivery failed due to system quota violation'], - }, - }; - - my $deliversby = ''; # [String] Mail Delivery Agent name - my $reasonname = ''; # [String] Error reason - my $bouncemesg = ''; # [String] Error message - my @linebuffer = split(/\n/, $$mbody); - - for my $e ( keys %$agentnames ) { - # Find a mail delivery agent name from the entire message body - my $p = index($$mbody, $agentnames->{ $e }->[0]); next if $p == -1; - - if( scalar $agentnames->{ $e }->@* > 1 ) { - # Try to find the 2nd element - my $q = index($$mbody, $agentnames->{ $e }->[1]); - next if $q == -1; - next if $p > $q; - } - - $deliversby = $e; - last; - } - return undef unless $deliversby; - - for my $e ( keys $messagesof->{ $deliversby }->%* ) { - # Detect an error reason from message patterns of the MDA. - for my $f ( @linebuffer ) { - # Whether the error message include each message defined in $messagesof - next unless grep { index(lc($f), $_) > -1 } $messagesof->{ $deliversby }->{ $e }->@*; - $reasonname = $e; - $bouncemesg = $f; - last; - } - last if $bouncemesg && $reasonname; - } - - return { - 'mda' => $deliversby, - 'reason' => $reasonname // '', - 'message' => $bouncemesg // '', - }; -} - -1; -__END__ - -=encoding utf-8 - -=head1 NAME - -Sisimai::MDA - Error message decoder for MDA - -=head1 SYNOPSIS - - use Sisimai::MDA; - my $header = { 'from' => 'mailer-daemon@example.jp' }; - my $string = 'mail.local: Disc quota exceeded'; - my $return = Sisimai::MDA->inquire($header, \$string); - -=head1 DESCRIPTION - -C decodes bounced email which created by some MDA, such as Dovecot, C, -C, and so on. This class is called from C only. - -=head1 CLASS METHODS - -=head2 C, I)>> - -C is a decoder for detecting an error from the mail delivery agent. - - my $header = { 'from' => 'mailer-daemon@example.jp' }; - my $string = 'mail.local: Disc quota exceeded'; - my $return = Sisimai::MDA->inquire($header, \$string); - warn Dumper $return; - $VAR1 = { - 'mda' => 'mail.local', - 'reason' => 'mailboxfull', - 'message' => 'mail.local: Disc quota exceeded' - } - -=head1 AUTHOR - -azumakuniyuki - -=head1 COPYRIGHT - -Copyright (C) 2014-2016,2018-2024 azumakuniyuki, All rights reserved. - -=head1 LICENSE - -This software is distributed under The BSD 2-Clause License. - -=cut - diff --git a/lib/Sisimai/Message.pm b/lib/Sisimai/Message.pm index 2e8b2734a..d80fd9de9 100644 --- a/lib/Sisimai/Message.pm +++ b/lib/Sisimai/Message.pm @@ -15,8 +15,13 @@ state $Fields1894 = Sisimai::RFC1894->FIELDINDEX; state $Fields5322 = Sisimai::RFC5322->FIELDINDEX; state $Fields5965 = Sisimai::RFC5965->FIELDINDEX; state $FieldTable = { map { lc $_ => $_ } ($Fields1894->@*, $Fields5322->@*, $Fields5965->@*) }; -state $ReplacesAs = { 'Content-Type' => [['message/xdelivery-status', 'message/delivery-status']] }; -state $Boundaries = ['Content-Type: message/rfc822', 'Content-Type: text/rfc822-headers']; +state $Boundaries = ["Content-Type: message/rfc822", "Content-Type: text/rfc822-headers"]; +state $ReplacesAs = { + "Content-Type" => [ + ["message/xdelivery-status", "message/delivery-status"], + ["message/disposition-notification", "message/delivery-status"], + ], +}; my $TryOnFirst = []; @@ -393,6 +398,7 @@ sub sift { # Feedback Loop message require Sisimai::ARF; $havesifted = Sisimai::ARF->inquire($mailheader, $bodystring); + $modulename = "ARF"; last(DECODER) if $havesifted; } diff --git a/lib/Sisimai/Order.pm b/lib/Sisimai/Order.pm index de9246815..aa0e36911 100644 --- a/lib/Sisimai/Order.pm +++ b/lib/Sisimai/Order.pm @@ -24,84 +24,48 @@ sub make { 'complaint-about' => ['Sisimai::ARF'], 'delivery-failure' => ['Sisimai::Lhost::Domino', 'Sisimai::Lhost::X2'], 'delivery-notification' => ['Sisimai::Lhost::MessagingServer'], - 'delivery-report' => ['Sisimai::Lhost::PowerMTA'], 'delivery-status' => [ - 'Sisimai::Lhost::GSuite', - 'Sisimai::Lhost::Outlook', - 'Sisimai::Lhost::GoogleGroups', - 'Sisimai::Lhost::McAfee', 'Sisimai::Lhost::OpenSMTPD', - 'Sisimai::Lhost::AmazonSES', - 'Sisimai::Lhost::AmazonWorkMail', - 'Sisimai::Lhost::ReceivingSES', + 'Sisimai::Lhost::GoogleWorkspace', 'Sisimai::Lhost::Gmail', + 'Sisimai::Lhost::GoogleGroups', + 'Sisimai::Lhost::AmazonSES', 'Sisimai::Lhost::X3', ], 'dmarc-ietf-dmarc' => ['Sisimai::ARF'], 'email-feedback' => ['Sisimai::ARF'], 'failed-delivery' => ['Sisimai::Lhost::X2'], 'failure-delivery' => ['Sisimai::Lhost::X2'], - 'failure-notice' => [ - 'Sisimai::Lhost::Yahoo', - 'Sisimai::Lhost::qmail', - 'Sisimai::Lhost::mFILTER', - 'Sisimai::Lhost::Activehunter', - 'Sisimai::Lhost::X4', - ], - 'loop-alert' => ['Sisimai::Lhost::FML'], - 'mail-could' => ['Sisimai::Lhost::InterScanMSS'], - 'mail-delivery' => [ + 'failure-notice' => ['Sisimai::Lhost::qmail', 'Sisimai::Lhost::mFILTER', 'Sisimai::Lhost::Activehunter'], + 'loop-alert' => ['Sisimai::Lhost::FML'], + 'mail-could' => ['Sisimai::Lhost::InterScanMSS'], + 'mail-delivery' => [ 'Sisimai::Lhost::Exim', 'Sisimai::Lhost::DragonFly', - 'Sisimai::Lhost::MailRu', 'Sisimai::Lhost::GMX', - 'Sisimai::Lhost::EinsUndEins', 'Sisimai::Lhost::Zoho', - 'Sisimai::Lhost::MessageLabs', - 'Sisimai::Lhost::MXLogic', + 'Sisimai::Lhost::EinsUndEins', ], - 'mail-failure' => ['Sisimai::Lhost::Exim'], - 'mail-not' => ['Sisimai::Lhost::X4'], - 'mail-system' => ['Sisimai::Lhost::EZweb'], - 'message-delivery' => ['Sisimai::Lhost::MailFoundry'], - 'message-frozen' => ['Sisimai::Lhost::Exim'], - 'message-you' => ['Sisimai::Lhost::Barracuda'], - 'não-entregue' => ['Sisimai::Lhost::Office365'], - 'non-recapitabile' => ['Sisimai::Lhost::Exchange2007'], - 'non-remis' => ['Sisimai::Lhost::Exchange2007'], - 'notice' => ['Sisimai::Lhost::Courier'], - 'onbestelbaar' => ['Sisimai::Lhost::Office365'], - 'permanent-delivery' => ['Sisimai::Lhost::X4'], - 'postmaster-notify' => ['Sisimai::Lhost::Sendmail'], - 'returned-mail' => [ + 'mail-failure' => ['Sisimai::Lhost::Exim'], + 'mail-system' => ['Sisimai::Lhost::EZweb'], + 'message-delivery' => ['Sisimai::Lhost::MailFoundry'], + 'message-frozen' => ['Sisimai::Lhost::Exim'], + 'non-recapitabile' => ['Sisimai::Lhost::Exchange2007'], + 'non-remis' => ['Sisimai::Lhost::Exchange2007'], + 'notice' => ['Sisimai::Lhost::Courier'], + 'postmaster-notify' => ['Sisimai::Lhost::Sendmail'], + 'returned-mail' => [ 'Sisimai::Lhost::Sendmail', - 'Sisimai::Lhost::Aol', - 'Sisimai::Lhost::V5sendmail', - 'Sisimai::Lhost::Bigfoot', 'Sisimai::Lhost::Biglobe', + 'Sisimai::Lhost::V5sendmail', 'Sisimai::Lhost::X1', ], - 'sorry-your' => ['Sisimai::Lhost::Facebook'], - 'there-was' => ['Sisimai::Lhost::X6'], - 'undeliverable' => [ - 'Sisimai::Lhost::Office365', - 'Sisimai::Lhost::Exchange2007', - 'Sisimai::Lhost::Aol', - 'Sisimai::Lhost::Exchange2003', - ], - 'undeliverable-mail' => [ - 'Sisimai::Lhost::Amavis', - 'Sisimai::Lhost::MailMarshalSMTP', - 'Sisimai::Lhost::IMailServer', - ], + 'there-was' => ['Sisimai::Lhost::X6'], + 'undeliverable' => ['Sisimai::Lhost::Exchange2007', 'Sisimai::Lhost::Exchange2003'], + 'undeliverable-mail' => ['Sisimai::Lhost::MailMarshalSMTP', 'Sisimai::Lhost::IMailServer'], 'undeliverable-message' => ['Sisimai::Lhost::Notes', 'Sisimai::Lhost::Verizon'], - 'undelivered-mail' => [ - 'Sisimai::Lhost::Postfix', - 'Sisimai::Lhost::Aol', - 'Sisimai::Lhost::SendGrid', - 'Sisimai::Lhost::Zoho', - ], - 'warning' => ['Sisimai::Lhost::Sendmail', 'Sisimai::Lhost::Exim'], + 'undelivered-mail' => ['Sisimai::Lhost::Postfix', 'Sisimai::Lhost::Zoho'], + 'warning' => ['Sisimai::Lhost::Sendmail', 'Sisimai::Lhost::Exim'], }; if( rindex($words[0], ':') > 0 ) { @@ -124,67 +88,45 @@ sub another { # There are another patterns in the value of "Subject:" header of a bounce mail generated by the # following MTA modules state $orderE0 = [ - 'Sisimai::Lhost::MailRu', - 'Sisimai::Lhost::Yandex', 'Sisimai::Lhost::Exim', 'Sisimai::Lhost::Sendmail', - 'Sisimai::Lhost::Aol', - 'Sisimai::Lhost::Office365', 'Sisimai::Lhost::Exchange2007', 'Sisimai::Lhost::Exchange2003', - 'Sisimai::Lhost::AmazonWorkMail', 'Sisimai::Lhost::AmazonSES', - 'Sisimai::Lhost::Barracuda', 'Sisimai::Lhost::InterScanMSS', 'Sisimai::Lhost::KDDI', - 'Sisimai::Lhost::SurfControl', 'Sisimai::Lhost::Verizon', 'Sisimai::Lhost::ApacheJames', 'Sisimai::Lhost::X2', - 'Sisimai::Lhost::X5', 'Sisimai::Lhost::FML', ]; # Fallback list: The following MTA/ESP modules is not listed orderE0 state $orderE1 = [ 'Sisimai::Lhost::Postfix', - 'Sisimai::Lhost::GSuite', - 'Sisimai::Lhost::Yahoo', - 'Sisimai::Lhost::Outlook', - 'Sisimai::Lhost::GMX', + 'Sisimai::Lhost::OpenSMTPD', + 'Sisimai::Lhost::Courier', + 'Sisimai::Lhost::qmail', 'Sisimai::Lhost::MessagingServer', - 'Sisimai::Lhost::EinsUndEins', + 'Sisimai::Lhost::MailMarshalSMTP', 'Sisimai::Lhost::Domino', 'Sisimai::Lhost::Notes', - 'Sisimai::Lhost::qmail', - 'Sisimai::Lhost::Courier', - 'Sisimai::Lhost::OpenSMTPD', + 'Sisimai::Lhost::Gmail', 'Sisimai::Lhost::Zoho', - 'Sisimai::Lhost::MessageLabs', - 'Sisimai::Lhost::MXLogic', + 'Sisimai::Lhost::GMX', + 'Sisimai::Lhost::GoogleGroups', 'Sisimai::Lhost::MailFoundry', - 'Sisimai::Lhost::McAfee', 'Sisimai::Lhost::V5sendmail', - 'Sisimai::Lhost::mFILTER', - 'Sisimai::Lhost::SendGrid', - 'Sisimai::Lhost::ReceivingSES', - 'Sisimai::Lhost::Amavis', - 'Sisimai::Lhost::PowerMTA', - 'Sisimai::Lhost::GoogleGroups', - 'Sisimai::Lhost::Gmail', - 'Sisimai::Lhost::EZweb', 'Sisimai::Lhost::IMailServer', - 'Sisimai::Lhost::MailMarshalSMTP', + 'Sisimai::Lhost::mFILTER', 'Sisimai::Lhost::Activehunter', - 'Sisimai::Lhost::Bigfoot', + 'Sisimai::Lhost::EZweb', 'Sisimai::Lhost::Biglobe', - 'Sisimai::Lhost::Facebook', - 'Sisimai::Lhost::X4', + 'Sisimai::Lhost::EinsUndEins', 'Sisimai::Lhost::X1', 'Sisimai::Lhost::X3', 'Sisimai::Lhost::X6', ]; - return [@$orderE0, @$orderE1]; }; diff --git a/lib/Sisimai/RFC1123.pm b/lib/Sisimai/RFC1123.pm index 2701bf618..193392f61 100644 --- a/lib/Sisimai/RFC1123.pm +++ b/lib/Sisimai/RFC1123.pm @@ -2,17 +2,45 @@ package Sisimai::RFC1123; use v5.26; use strict; use warnings; - -sub is_validhostname { - # Check that the argument is a valid hostname or not +use Sisimai::String; + +state $Sandwiched = [ + # (Postfix) postfix/src/smtp/smtp_proto.c: "host %s said: %s (in reply to %s)", + # - : host re2.example.com[198.51.100.2] said: 550 ... + # - : host r2.example.org[198.51.100.18] refused to talk to me: + ["host ", " said: "], + ["host ", " talk to me: "], + ["while talking to ", ":"], # (Sendmail) ... while talking to mx.bouncehammer.jp.: + ["host ", " ["], # (Exim) host mx.example.jp [192.0.2.20]: 550 5.7.0 + [" by ", ". ["], # (Gmail) ...for the recipient domain example.jp by mx.example.jp. [192.0.2.1]. + + # (MailFoundry) + # - Delivery failed for the following reason: Server mx22.example.org[192.0.2.222] failed with: 550... + # - Delivery failed for the following reason: mail.example.org[192.0.2.222] responded with failure: 552.. + ["delivery failed for the following reason: ", " with"], + ["remote system: ", "("], # (MessagingServer) Remote system: dns;mx.example.net (mx. -- + ["smtp server <", ">"], # (X6) SMTP Server rejected recipient ... + ["-mta: ", ">"], # (MailMarshal) Reporting-MTA: + [" : ", "["], # (SendGrid) cat:000000: : 192.0.2.1 : mx.example.jp:[192.0.2.2]... +]; +state $StartAfter = [ + "generating server: ", # (Exchange2007) en-US/Generating server: mta4.example.org + "serveur de g", # (Exchange2007) fr-FR/Serveur de g辿n辿ration + "server di generazione", # (Exchange2007) it-CH + "genererande server", # (Exchange2007) sv-SE +]; +state $ExistUntil = [ + " did not like our ", # (Dragonfly) mail-inbound.libsisimai.net [192.0.2.25] did not like our DATA: ... +]; + +sub is_internethost { + # Check that the argument is a valid Internet hostname or not # @param [String] argv0 String to be checked # @return [Boolean] 0: is not a valid hostname # 1: is a valid hostname # @since v5.2.0 my $class = shift; my $argv0 = shift || return 0; - my $valid = 1; - my $token = [split(/\./, $argv0)] || ['0']; return 0 if length $argv0 > 255; return 0 if length $argv0 < 4; @@ -23,17 +51,105 @@ sub is_validhostname { return 0 if index($argv0, "-") == 0; return 0 if substr($argv0, -1, 1) eq "-"; - for my $e (split("", uc $argv0)) { - # Check each character (upper-cased) + my $hostnameok = 1; + my @characters = split("", uc $argv0); + for my $e ( @characters ) { + # Check each characater is a number or an alphabet + my $f = ord $e; + if( $f < 45 ) { $hostnameok = 0; last } # 45 = '-' + if( $f == 47 ) { $hostnameok = 0; last } # 47 = '/' + if( $f > 57 && $f < 65 ) { $hostnameok = 0; last } # 57 = '9', 65 = 'A' + if( $f > 90 ) { $hostnameok = 0; last } # 90 = 'Z' + } + return 0 if $hostnameok == 0; + + my $p1 = rindex($argv0, "."); + my $cv = substr($argv0, $p1 + 1,); return 0 if length $cv > 63; + for my $e ( split("", $cv) ) { + # The top level domain should not include a number my $f = ord $e; - $valid = 0 if $f < 45; # 45 = '-' - $valid = 0 if $f == 47; # 47 = '/' - $valid = 0 if $f > 57 && $f < 65; # 57 = '9', 65 = 'A' - $valid = 0 if $f > 90 # 90 = 'Z' + if( $f > 47 && $f < 58 ) { $hostnameok = 0; last } + } + return $hostnameok; +} + +sub find { + # find() returns a valid internet hostname found from the argument + # @param string argv1 String including hostnames + # @return string A valid internet hostname found in the argument + # @since v5.2.0 + my $class = shift; + my $argv1 = shift || return ""; + + my $sourcetext = lc $argv1; + my $sourcelist = []; + my $foundtoken = []; + my $thelongest = 0; + my $hostnameis = ""; + + # Replace some string for splitting by " " + # - mx.example.net[192.0.2.1] => mx.example.net [192.0.2.1] + # - mx.example.jp:[192.0.2.1] => mx.example.jp :[192.0.2.1] + s/\[/ [/g, s/\(/ (/g, s/ /g for $sourcetext; # Suffix a space character behind each bracket + s/:/: /g, s/;/; /g for $sourcetext; # Suffix a space character behind : and ; + $sourcetext = Sisimai::String->sweep($sourcetext); + + MAKELIST: while(1) { + for my $e ( @$Sandwiched ) { + # Check a hostname exists between the $e->[0] and $e->[1] at array "Sandwiched" + # Each array in Sandwiched have 2 elements + next unless Sisimai::String->aligned(\$sourcetext, $e); + + my $p1 = index($sourcetext, $e->[0]); + my $p2 = index($sourcetext, $e->[1]); + my $cw = length $e->[0]; + next if $p1 + $cw >= $p2; + + $sourcelist = [split(" ", substr($sourcetext, $p1 + $cw, $p2 - $cw - $p1))]; + last MAKELIST; + } + + # Check other patterns which are not sandwiched + for my $e ( @$StartAfter ) { + # $StartAfter have some strings, not an array. + my $p1 = index($sourcetext, $e); next if $p1 < 0; + my $cw = length $e; + $sourcelist = [split(" ", substr($sourcetext, $p1 + $cw,))]; + last MAKELIST; + } + + for my $e ( @$ExistUntil ) { + # ExistUntil have some strings, not an array. + my $p1 = index($sourcetext, $e); next if $p1 < 0; + $sourcelist = [split(" ", substr($sourcetext, 0, $p1))]; + last MAKELIST; + } + + $sourcelist = [split(" ", $sourcetext)] if scalar @$sourcelist == 0; + last MAKELIST; + } + + for my $e ( @$sourcelist ) { + # Pick some strings which is 4 or more length, is including "." character + substr($e, -1, 1, "") if substr($e, -1, 1) eq "."; # Remove "." at the end of the string + $e =~ y/[]()<>:;//d; # Remove brackets, colon, and semi-colon + + next if length $e < 4; + next if index($e, ".") < 0; + next if __PACKAGE__->is_internethost($e) == 0; + push @$foundtoken, $e; + } + return "" if scalar @$foundtoken == 0; + return $foundtoken->[0] if scalar @$foundtoken == 1; + + for my $e ( @$foundtoken ) { + # Returns the longest hostname + my $cw = length $e; next if $thelongest >= $cw; + $hostnameis = $e; + $thelongest = $cw; } - return 0 if $valid == 0; - return 0 if $token->[-1] =~ /\d/; - return $valid; + return $hostnameis; } 1; @@ -42,14 +158,14 @@ __END__ =head1 NAME -Sisimai::RFC1123 - Hostname related class +Sisimai::RFC1123 - Internet hostname related class =head1 SYNOPSIS use Sisimai::RFC1123; - print Sisimai::RFC1123->is_validhostname("mx2.example.jp"); # 1 - print Sisimai::RFC1123->is_validhostname("localhost"); # 0 + print Sisimai::RFC1123->is_internethost("mx2.example.jp"); # 1 + print Sisimai::RFC1123->is_internethost("localhost"); # 0 =head1 DESCRIPTION @@ -58,12 +174,12 @@ C is a class related to the Internet hosts =head1 CLASS METHODS -=head2 C)>> +=head2 C)>> -C method returns true when the argument is a valid hostname +C method returns true when the argument is a valid hostname - print Sisimai::RFC1123->is_validhostname("mx2.example.jp"); # 1 - print Sisimai::RFC1123->is_validhostname("localhost"); # 0 + print Sisimai::RFC1123->is_internethost("mx2.example.jp"); # 1 + print Sisimai::RFC1123->is_internethost("localhost"); # 0 =head1 AUTHOR diff --git a/lib/Sisimai/RFC1894.pm b/lib/Sisimai/RFC1894.pm index c41932599..6b5549854 100644 --- a/lib/Sisimai/RFC1894.pm +++ b/lib/Sisimai/RFC1894.pm @@ -2,6 +2,7 @@ package Sisimai::RFC1894; use v5.26; use strict; use warnings; +use Sisimai::String; sub FIELDINDEX { return [qw| @@ -34,8 +35,9 @@ sub match { my $class = shift; my $argv0 = shift || return undef; my $label = __PACKAGE__->label($argv0) || return undef; + my $match = 0; - state $fieldnames = { + state $fieldnames = [ # https://tools.ietf.org/html/rfc3464#section-2.2 # Some fields of a DSN apply to all of the delivery attempts described by that DSN. At # most, these fields may appear once in any DSN. These fields are used to correlate the @@ -48,10 +50,12 @@ sub match { # The following fields are not used in Sisimai: # - Original-Envelope-Id # - DSN-Gateway - 'arrival-date' => ':', - 'received-from-mta' => ';', - 'reporting-mta' => ';', - 'x-original-message-id' => '@', + { + 'arrival-date' => ':', + 'received-from-mta' => ';', + 'reporting-mta' => ';', + 'x-original-message-id' => '@', + }, # https://tools.ietf.org/html/rfc3464#section-2.3 # A DSN contains information about attempts to deliver a message to one or more recipi- @@ -65,23 +69,37 @@ sub match { # The following fields are not used in Sisimai: # - Will-Retry-Until # - Final-Log-ID - 'action' => 'e', - 'diagnostic-code' => ';', - 'final-recipient' => ';', - 'last-attempt-date' => ':', - 'original-recipient' => ';', - 'remote-mta' => ';', - 'status' => '.', - 'x-actual-recipient' => ';', - }; + { + 'action' => 'e', + 'diagnostic-code' => ';', + 'final-recipient' => ';', + 'last-attempt-date' => ':', + 'original-recipient' => ';', + 'remote-mta' => ';', + 'status' => '.', + 'x-actual-recipient' => ';', + }, + ]; + + FIELDS0: for my $e ( keys $fieldnames->[0]->%* ) { + # Per-Message fields + next unless $label eq $e; + next unless index($argv0, $fieldnames->[0]->{ $label }) > 1; + $match = 1; last; + } + return $match if $match > 0; - return 0 unless exists $fieldnames->{ $label }; - return 0 unless index($argv0, $fieldnames->{ $label }) > 0; - return 1; + FIELDS1: for my $e ( keys $fieldnames->[1]->%* ) { + # Per-Recipient fields + next unless $label eq $e; + next unless index($argv0, $fieldnames->[1]->{ $label }) > 1; + $match = 2; last; + } + return $match; } sub label { - # Returns a field name as a lqbel from the given string + # Returns a field name as a label from the given string # @param [String] argv0 A line including field and value defined in RFC3464 # @return [String] Field name as a label # @since v4.25.15 @@ -98,9 +116,9 @@ sub field { my $class = shift; my $argv0 = shift || return undef; - state $correction = { - 'action' => { 'deliverable' => 'delivered', 'expired' => 'delayed', 'failure' => 'failed' }, - }; + state $subtypeset = { "addr" => "RFC822", "cdoe" => "SMTP", "host" => "DNS" }; + state $actionlist = ["failed", "delayed", "delivered", "relayed", "expanded"]; + state $correction = { 'deliverable' => 'delivered', 'expired' => 'delayed', 'failure' => 'failed' }; state $fieldgroup = { 'original-recipient' => 'addr', 'final-recipient' => 'addr', @@ -116,54 +134,70 @@ sub field { 'x-original-message-id' => 'text', }; state $captureson = { - 'addr' => qr/\A((?:Original|Final|X-Actual)-Recipient):[ ](.+?);[ ](.+)/, - 'code' => qr/\A(Diagnostic-Code):[ ](.+?);[ ](.*)/, - 'date' => qr/\A((?:Arrival|Last-Attempt)-Date):[ ](.+)/, - 'host' => qr/\A((?:Received-From|Remote|Reporting)-MTA):[ ](.+?);[ ](.+)/, - 'list' => qr/\A(Action):[ ](delayed|deliverable|delivered|expanded|expired|failed|failure|relayed)/i, - 'stat' => qr/\A(Status):[ ]([245][.]\d+[.]\d+)/, - 'text' => qr/\A(X-Original-Message-ID):[ ](.+)/, - #'text' => qr/\A(Final-Log-ID|Original-Envelope-Id):[ ]*(.+)/, + "addr" => ["Final-Recipient", "Original-Recipient", "X-Actual-Recipient"], + "code" => ["Diagnostic-Code"], + "date" => ["Arrival-Date", "Last-Attempt-Date"], + "host" => ["Received-From-MTA", "Remote-MTA", "Reporting-MTA"], + "list" => ["Action"], + "stat" => ["Status"], + #"text" => ["X-Original-Message-ID", "Final-Log-ID", "Original-Envelope-ID"], }; + my $parts = [split(":", $argv0, 2)]; # ["Final-Recipient", " rfc822; "] my $label = __PACKAGE__->label($argv0) || return undef; my $group = $fieldgroup->{ $label } || return undef; return undef unless exists $captureson->{ $group }; - my $table = ['', '', '', '']; - my $match = 0; - while( $argv0 =~ $captureson->{ $group } ) { - # Try to match with each pattern of Per-Message field, Per-Recipient field - # - 0: Field-Name - # - 1: Sub Type: RFC822, DNS, X-Unix, and so on) - # - 2: Value - # - 3: Field Group(addr, code, date, host, stat, text) - $match = 1; - $table->[0] = lc $1; - $table->[3] = $group; - - if( $group eq 'addr' || $group eq 'code' || $group eq 'host' ) { - # - Final-Recipient: RFC822; kijitora@nyaan.jp - # - Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown - # - Remote-MTA: DNS; mx.example.jp - $table->[1] = uc $2; - $table->[2] = $group eq 'host' ? lc $3 : $3; - $table->[2] = '' if $table->[2] =~ /\A\s+\z/; # Remote-MTA: dns; + # Try to match with each pattern of Per-Message field, Per-Recipient field + # - 0: Field-Name + # - 1: Sub Type: RFC822, DNS, X-Unix, and so on) + # - 2: Value + # - 3: Field Group(addr, code, date, host, stat, text) + # - 4: Comment + my $table = [$label, "", "", $group, ""]; $parts->[1] = Sisimai::String->sweep($parts->[1]); + + if( $group eq 'addr' || $group eq 'code' || $group eq 'host' ) { + # - Final-Recipient: RFC822; kijitora@nyaan.jp + # - Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown + # - Remote-MTA: DNS; mx.example.jp + if( index($parts->[1], ";" ) > 0 ) { + # There is a valid sub type (including ";") + my $v = [split(";", $parts->[1], 2)]; + $table->[1] = uc Sisimai::String->sweep($v->[0]) if scalar @$v > 0; + $table->[2] = Sisimai::String->sweep($v->[1]) if scalar @$v > 1; } else { - # - Action: failed - # - Status: 5.2.2 - $table->[2] = $group eq 'date' ? $2 : lc $2; - - # Correct invalid value in Action field: - last unless $group eq 'list'; - last unless exists $correction->{'action'}->{ $table->[2] }; - $table->[2] = $correction->{'action'}->{ $table->[2] }; + # There is no sub type like "Diagnostic-Code: 550 5.1.1 ..." + $table->[2] = Sisimai::String->sweep($parts->[1]); + $table->[1] = $subtypeset->{ $group } || ""; } - last; + $table->[2] = lc $table->[2] if $group eq "host"; + $table->[2] = '' if $table->[2] =~ /\A\s+\z/; + + } elsif( $group eq "list" ) { + # Action: failed + # Check that the value is an available value defined in "actionlist" or not. + # When the value is invalid, convert to an available value defined in "correction" + my $v = lc $parts->[1]; + $table->[2] = $v if grep { $v eq $_ } @$actionlist; + $table->[2] ||= $correction->{ $v }; + + } else { + # Other groups such as Status:, Arrival-Date:, or X-Original-Message-ID:. + # There is no ";" character in the field. + # - Status: 5.2.2 + # - Arrival-Date: Mon, 21 May 2018 16:09:59 +0900 + $table->[2] = $group eq "date" ? $parts->[1] : lc $parts->[1]; } - return undef unless $match; + if( Sisimai::String->aligned(\$table->[2], [" (", ")"]) ) { + # Extract text enclosed in parentheses as comments + # Reporting-MTA: dns; mr21p30im-asmtp004.me.example.com (tcp-daemon) + my $p1 = index($table->[2], " ("); + my $p2 = index($table->[2], ")"); + $table->[4] = substr($table->[2], $p1 + 2, $p2 - $p1 - 2); + $table->[2] = substr($table->[2], 0, $p1); + } return $table; } @@ -185,9 +219,9 @@ Sisimai::RFC1894 - DSN field defined in RFC3464 (obsoletes RFC1894) print Sisimai::RFC1894->match('Final-Recipient: RFC822; cat@nyaan.jp'); # 2 my $v = Sisimai::RFC1894->field('Reporting-MTA: DNS; mx.nyaan.jp'); - my $r = Sisimai::RFC1894->field('Status: 5.1.1'); - print Data::Dumper::Dumper $v; # ['reporting-mta', 'dns', 'mx.nyaan.org', 'host']; - print Data::Dumper::Dumper $r; # ['status', '', '5.1.1', 'stat']; + my $r = Sisimai::RFC1894->field('Status: 5.1.1 (user unknown)'); + print Data::Dumper::Dumper $v; # ['reporting-mta', 'dns', 'mx.nyaan.org', 'host', '']; + print Data::Dumper::Dumper $r; # ['status', '', '5.1.1', 'stat', 'user unknown']; =head1 DESCRIPTION diff --git a/lib/Sisimai/RFC2045.pm b/lib/Sisimai/RFC2045.pm index 29dadc71a..5c608a374 100644 --- a/lib/Sisimai/RFC2045.pm +++ b/lib/Sisimai/RFC2045.pm @@ -282,6 +282,7 @@ sub makeflat { my $iso2022set = qr/charset=["']?(iso-2022-[-a-z0-9]+)['"]?\b/; my $multiparts = __PACKAGE__->levelout($argv0, $argv1); my $flattenout = ''; + my $delimiters = ["/delivery-status", "/rfc822", "/feedback-report", "/partial"]; while( my $e = shift @$multiparts ) { # Pick only the following parts Sisimai::Lhost will use, and decode each part @@ -335,10 +336,8 @@ sub makeflat { # There is no Content-Transfer-Encoding header in the part $bodystring .= $bodyinside; } - - if( index($mediatypev, '/delivery-status') > -1 || - index($mediatypev, '/feedback-report') > -1 || - index($mediatypev, '/rfc822') > -1 ) { + + if( grep { index($mediatypev, $_) > 0 } @$delimiters ) { # Add Content-Type: header of each part (will be used as a delimiter at Sisimai::Lhost) into # the body inside when the value of Content-Type: is message/delivery-status, message/rfc822, # or text/rfc822-headers diff --git a/lib/Sisimai/RFC3464.pm b/lib/Sisimai/RFC3464.pm index a8c66964d..e378fccbd 100644 --- a/lib/Sisimai/RFC3464.pm +++ b/lib/Sisimai/RFC3464.pm @@ -3,410 +3,290 @@ use v5.26; use strict; use warnings; use Sisimai::Lhost; +use Sisimai::RFC3464::ThirdParty; # http://tools.ietf.org/html/rfc3464 -sub description { 'Fallback Module for MTAs' }; +sub description { 'RFC3464' }; sub inquire { - # Detect an error for RFC3464 + # Decode a bounce mail which have fields defined in RFC3464 # @param [Hash] mhead Message headers of a bounce email # @param [String] mbody Message body of a bounce email # @return [Hash] Bounce data list and message/rfc822 part # @return [undef] failed to decode or the arguments are missing my $class = shift; - my $mhead = shift // return undef; - my $mbody = shift // return undef; - my $match = 0; + my $mhead = shift // return undef; return undef unless keys %$mhead; + my $mbody = shift // return undef; return undef unless ref $mbody eq 'SCALAR'; - return undef unless keys %$mhead; - return undef unless ref $mbody eq 'SCALAR'; + require Sisimai::RFC1894; + require Sisimai::RFC2045; + require Sisimai::RFC5322; + require Sisimai::Address; + require Sisimai::String; state $indicators = Sisimai::Lhost->INDICATORS; - state $startingof = { - 'message' => [ - 'content-type: message/delivery-status', - 'content-type: message/disposition-notification', - 'content-type: message/xdelivery-status', - 'content-type: text/plain; charset=', - 'the original message was received at ', - 'this report relates to your message', - 'your message could not be delivered', - 'your message was not delivered to ', - 'your message was not delivered to the following recipients', - ], - 'rfc822' => [ - 'content-type: message/rfc822', - 'content-type: text/rfc822-headers', - 'return-path: <' - ], - }; - - require Sisimai::Address; - require Sisimai::RFC1894; - my $fieldtable = Sisimai::RFC1894->FIELDTABLE; - my $permessage = {}; # (Hash) Store values of each Per-Message field + state $boundaries = [ + # When the new value added, the part of the value should be listed in $delimiters variable + # defined at Sisimai::RFC2045->makeFlat() method + "Content-Type: message/rfc822", + "Content-Type: text/rfc822-headers", + "Content-Type: message/partial", + "Content-Disposition: inline", # See lhost-amavis-*.eml, lhost-facebook-*.eml + ]; + state $startingof = {"message" => ["Content-Type: message/delivery-status"]}; + state $fieldtable = Sisimai::RFC1894->FIELDTABLE; + + unless( grep { index($$mbody, $_) > 0 } @$boundaries ) { + # There is no "Content-Type: message/rfc822" line in the message body + # Insert "Content-Type: message/rfc822" before "Return-Path:" of the original message + my $p0 = index($$mbody, "\n\nReturn-Path:"); + $$mbody = sprintf("%s%s%s", substr($$mbody, 0, $p0), $boundaries->[0], substr($$mbody, $p0 + 1,)) if $p0 > 0; + } + my $permessage = {}; my $dscontents = [Sisimai::Lhost->DELIVERYSTATUS]; - my $rfc822text = ''; # (String) message/rfc822 part text - my $maybealias = ''; # (String) Original-Recipient field - my $lowercased = ''; # (String) Lowercased each line of the loop - my $blanklines = 0; # (Integer) The number of blank lines + my $alternates = Sisimai::Lhost->DELIVERYSTATUS; + my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries); my $readcursor = 0; # (Integer) Points the current cursor position my $recipients = 0; # (Integer) The number of 'Final-Recipient' header - my $itisbounce = 0; # (Integer) Flag for that an email is a bounce - my $connheader = { - 'date' => '', # The value of Arrival-Date header - 'rhost' => '', # The value of Reporting-MTA header - 'lhost' => '', # The value of Received-From-MTA header - }; + my $beforemesg = ""; # (String) String before $startingof->{"message"} + my $goestonext = 0; # (Bool) Flag: do not append the line into $beforemesg + my $isboundary = [Sisimai::RFC2045->boundary($mhead->{"content-type"}, 0)]; $isboundary->[0] ||= ""; my $v = undef; - my $p = ''; + my $p = ""; + + while( index($emailparts->[0], '@') < 0 ) { + # There is no email address in the first element of emailparts + # There is a bounce message inside of message/rfc822 part at lhost-x5-* + my $p0 = -1; # The index of the boundary string found first + my $p1 = 0; # Offset position of the message body after the boundary string + my $ct = ""; # Boundary string found first such as "Content-Type: message/rfc822" + + for my $e ( @$boundaries ) { + # Look for a boundary string from the message body + $p0 = index($$mbody, $e."\n"); next if $p0 < 0; + $p1 = $p0 + length($e) + 2; + $ct = $e; last; + } + last if $p0 < 0; + + my $cx = substr($$mbody, $p1,); + my $p2 = index($cx,, "\n\n"); + my $cv = substr($cx, $p2 + 2,); + $emailparts = Sisimai::RFC5322->part(\$cv, [$ct], 0); + last; + } - for my $e ( split("\n", $$mbody) ) { - # Read each line between the start of the message and the start of rfc822 part. - $lowercased = lc $e; - unless( $readcursor ) { + if( index($emailparts->[0], $startingof->{"message"}->[0]) < 0 ) { + # There is no "Content-Type: message/delivery-status" line in the message body + # Insert "Content-Type: message/delivery-status" before "Reporting-MTA:" field + my $cv = "\n\nReporting-MTA:"; + my $e0 = $emailparts->[0]; + my $p0 = index($e0, $cv); + $emailparts->[0] = sprintf("%s\n\n%s%s", substr($e0, 0, $p0), $startingof->{"message"}->[0], substr($e0, $p0,)) if $p0 > 0; + } + + for my $e ("Final-Recipient", "Original-Recipient") { + # Fix the malformed field "Final-Recipient: " + my $cv = "\n".$e.": "; + my $cx = $cv."<"; + my $p0 = index($emailparts->[0], $cx); next if $p0 < 0; + + substr($emailparts->[0], $p0, length($cv) + 1, $cv."rfc822; "); + my $p1 = index($emailparts->[0], ">\n", $p0 + 2); substr($emailparts->[0], $p1, 1, ""); + } + + for my $e ( split("\n", $emailparts->[0]) ) { + # Read error messages and delivery status lines from the head of the email to the previous + # line of the beginning of the original message. + if( $readcursor == 0 ) { # Beginning of the bounce message or message/delivery-status part - if( grep { index($lowercased, $_) == 0 } $startingof->{'message'}->@* ) { - $readcursor |= $indicators->{'deliverystatus'}; - next; - } - } + $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0; - unless( $readcursor & $indicators->{'message-rfc822'} ) { - # Beginning of the original message part(message/rfc822) - if( grep { $lowercased eq $_ } $startingof->{'rfc822'}->@* ) { - $readcursor |= $indicators->{'message-rfc822'}; - next; - } - } + while(1) { + # Append each string before startingof["message"][0] except the following patterns + # for the later reference + last if $e eq ""; # Blank line + last if $goestonext; # Skip if the part is text/html, image/icon, in multipart/* + + # This line is a boundary kept in "multiparts" as a string, when the end of the boundary + # appeared, the condition above also returns true. + if( grep { index($e, $_) == 0 } @$isboundary ) { $goestonext = 0; last } + if( index($e, "Content-Type:") == 0 ) { + # Content-Type: field in multipart/* + if( index($e, "multipart/") > 0 ) { + # Content-Type: multipart/alternative; boundary=aa00220022222222ffeebb + # Pick the boundary string and store it into "isboucdary" + push @$isboundary, Sisimai::RFC2045->boundary($e, 0); + + } elsif( index($e, "text/plain") ) { + # Content-Type: "text/plain" + $goestonext = 0; + + } else { + # Other types: for example, text/html, image/jpg, and so on + $goestonext = 1; + } + last; + } - if( $readcursor & $indicators->{'message-rfc822'} ) { - # message/rfc822 OR text/rfc822-headers part - unless( length $e ) { - last if ++$blanklines > 1; - next; + last if index($e, "Content-") == 0; # Content-Disposition, ... + last if index($e, "This is a MIME") == 0; # This is a MIME-formatted message. + last if index($e, "This is a multi") == 0; # This is a multipart message in MIME format + last if index($e, "This is an auto") == 0; # This is an automatically generated ... + last if index($e, "This multi-part") == 0; # This multi-part MIME message contains... + last if index($e, "###") == 0; # A frame like ##### + last if index($e, "***") == 0; # A frame like ***** + last if index($e, "--") == 0; # Boundary string + last if index($e, "--- The follow") > -1; # ----- The following addresses had delivery problems ----- + last if index($e, "--- Transcript") > -1; # ----- Transcript of session follows ----- + $beforemesg .= $e." "; last; } - $rfc822text .= sprintf("%s\n", $e); - - } else { - # message/delivery-status part - next unless $readcursor & $indicators->{'deliverystatus'}; - next unless length $e; + next; + } + next unless $readcursor & $indicators->{'deliverystatus'}; + next unless length $e; + if( my $f = Sisimai::RFC1894->match($e) ) { + # $e matched with any field defined in RFC3464 + next unless my $o = Sisimai::RFC1894->field($e); $v = $dscontents->[-1]; - if( my $f = Sisimai::RFC1894->match($e) ) { - # $e matched with any field defined in RFC3464 - next unless my $o = Sisimai::RFC1894->field($e); - if( $o->[-1] eq 'addr' ) { + if( $o->[3] eq "addr" ) { + # Final-Recipient: rfc822; kijitora@example.jp + # X-Actual-Recipient: rfc822; kijitora@example.co.jp + if( $o->[0] eq "final-recipient" ) { # Final-Recipient: rfc822; kijitora@example.jp - # X-Actual-Recipient: rfc822; kijitora@example.co.jp - if( $o->[0] eq 'final-recipient' || $o->[0] eq 'original-recipient' ) { - # Final-Recipient: rfc822; kijitora@example.jp - if( $o->[0] eq 'original-recipient' ) { - # Original-Recipient: ... - $maybealias = $o->[2]; - - } else { - # Final-Recipient: ... - my $x = $v->{'recipient'} || ''; - my $y = Sisimai::Address->s3s4($o->[2]); - $y = $maybealias unless Sisimai::Address->is_emailaddress($y); - - if( $x && $x ne $y ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, Sisimai::Lhost->DELIVERYSTATUS; - $v = $dscontents->[-1]; - } - $v->{'recipient'} = $y; - $recipients++; - $itisbounce ||= 1; - - $v->{'alias'} ||= $maybealias; - $maybealias = ''; - } - } elsif( $o->[0] eq 'x-actual-recipient' ) { - # X-Actual-Recipient: RFC822; |IFS=' ' && exec procmail -f- || exit 75 ... - # X-Actual-Recipient: rfc822; kijitora@neko.example.jp - $v->{'alias'} = $o->[2] unless index($o->[2], ' ') > -1; + # Final-Recipient: x400; /PN=... + my $cv = Sisimai::Address->s3s4($o->[2]); next unless Sisimai::Address->is_emailaddress($cv); + my $cw = scalar @$dscontents; next if $cw > 0 && $cv eq $dscontents->[$cw - 1]->{'recipient'}; + + if( $v->{'recipient'} ) { + # There are multiple recipient addresses in the message body. + push @$dscontents, Sisimai::Lhost->DELIVERYSTATUS; + $v = $dscontents->[-1]; } - } elsif( $o->[-1] eq 'code' ) { - # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown - $v->{'spec'} = $o->[1]; - $v->{'diagnosis'} = $o->[2]; + $v->{'recipient'} = $cv; + $recipients++; } else { - # Other DSN fields defined in RFC3464 - next unless exists $fieldtable->{ $o->[0] }; - $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - - next unless $f == 1; - $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2]; + # X-Actual-Recipient: rfc822; kijitora@example.co.jp + $v->{'alias'} = $o->[2]; } + } elsif( $o->[3] eq "code" ) { + # Diagnostic-Code: SMTP; 550 5.1.1 ... User Unknown + $v->{'spec'} = $o->[1]; + $v->{'diagnosis'} .= $o->[2]." "; + } else { - # The line did not match with any fields defined in RFC3464 - if( index($e, 'Diagnostic-Code: ') == 0 && index($e, ';') < 0 ) { - # There is no value of "diagnostic-type" such as Diagnostic-Code: 554 ... - $v->{'diagnosis'} = substr($e, index($e, ' ') + 1,); + # Other DSN fields defined in RFC3464 + if( $o->[4] ne "" ) { + # There are other error messages as a comment such as the following: + # Status: 5.0.0 (permanent failure) + # Status: 4.0.0 (cat.example.net: host name lookup failure) + $v->{'diagnosis'} .= " ".$o->[4]." "; + } + next unless exists $fieldtable->{ $o->[0] }; + $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; - } elsif( index($e, 'Status: ') == 0 && Sisimai::SMTP::Reply->find(substr($e, 8, 3)) ) { - # Status: 553 Exceeded maximum inbound message size - $v->{'alterrors'} = substr($e, 8,); + next unless $f == 1; + $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2]; + } + } else { + # Check that the line is a continued line of the value of Diagnostic-Code: field or not + if( index($e, "X-") == 0 && index($e, ": ") > 1 ) { + # This line is a MTA-Specific fields begins with "X-" + next unless Sisimai::RFC3464::ThirdParty->is3rdparty($e); - } elsif( index($p, 'Diagnostic-Code:') == 0 && index($e, ' ') == 0 ) { - # Continued line of the value of Diagnostic-Code field - $v->{'diagnosis'} .= $e; - $e = 'Diagnostic-Code: '.$e; + my $cv = Sisimai::RFC3464::ThirdParty->xfield($e); + if( scalar(@$cv) > 0 && not exists $fieldtable->{ lc $cv->[0] } ) { + # Check the first element is a field defined in RFC1894 or not + $v->{'reason'} = substr($cv->[4], index($cv->[4], ":") + 1,) if index($cv->[4], "reason:") == 0; } else { - # Get error messages which is written in the message body directly - next if index($e, ' ') == 0; - next if index($e, ' ') == 0; - next if index($e, 'X') == 0; - - my $cr = Sisimai::SMTP::Reply->find($e); - my $ca = Sisimai::Address->find($e) || []; - my $co = Sisimai::String->aligned(\$e, ['<', '@', '>']); - - $v->{'alterrors'} .= ' '.$e if length $cr || (scalar @$ca && $co); + # Set the value picked from "X-*" field to $dscontents when the current value is empty + my $z = $fieldtable->{ lc $cv->[0] }; next unless $z; + $v->{ $z } ||= $cv->[2]; + } + } else { + # The line may be a continued line of the value of the Diagnostic-Code: field + if( index($p, 'Diagnostic-Code:') < 0 ) { + # In the case of multiple "message/delivery-status" line + next if index($e, "Content-") == 0; # Content-Disposition:, ... + next if index($e, "--") == 0; # Boundary string + $beforemesg .= $e." "; next } + + # Diagnostic-Code: SMTP; 550-5.7.26 The MAIL FROM domain [email.example.jp] + # has an SPF record with a hard fail + next unless index($e, " ") == 0; + $v->{'diagnosis'} .= " ".Sisimai::String->sweep($e); } - } # End of message/delivery-status + } } continue { # Save the current line for the next loop $p = $e; } - # --------------------------------------------------------------------------------------------- - BODY_DECODER_FOR_FALLBACK: { - # Fallback, decode the entire message body - last if $recipients; - - # Failed to get a recipient address at code above - my $returnpath = lc($mhead->{'return-path'} // ''); - my $headerfrom = lc($mhead->{'from'} // ''); - my $errortitle = lc($mhead->{'subject'} // ''); - my $patternsof = { - 'from' => ['postmaster@', 'mailer-daemon@', 'root@'], - 'return-path' => ['<>', 'mailer-daemon'], - 'subject' => ['delivery fail', 'delivery report', 'failure notice', 'mail delivery', - 'mail failed', 'mail error', 'non-delivery', 'returned mail', - 'undeliverable mail', 'warning: '], - }; - - $match ||= 1 if grep { index($headerfrom, $_) > -1 } $patternsof->{'from'}->@*; - $match ||= 1 if grep { index($errortitle, $_) > -1 } $patternsof->{'subject'}->@*; - $match ||= 1 if grep { index($returnpath, $_) > -1 } $patternsof->{'return-path'}->@*; - last unless $match; - - state $readuntil0 = [ - # Stop reading when the following string have appeared at the first of a line - 'a copy of the original message below this line:', - 'content-type: message/delivery-status', - 'for further assistance, please contact ', - 'here is a copy of the first part of the message', - 'received:', - 'received-from-mta:', - 'reporting-mta:', - 'reporting-ua:', - 'return-path:', - 'the non-delivered message is attached to this message', - ]; - state $readuntil1 = [ - # Stop reading when the following string have appeared in a line - 'attachment is a copy of the message', - 'below is a copy of the original message:', - 'below this line is a copy of the message', - 'message contains ', - 'message text follows: ', - 'original message follows', - 'the attachment contains the original mail headers', - 'the first ', - 'unsent message below', - 'your message reads (in part):', - ]; - state $readafter0 = [ - # Do not read before the following strings - ' the postfix ', - 'a summary of the undelivered message you sent follows:', - 'the following is the error message', - 'the message that you sent was undeliverable to the following', - 'your message was not delivered to ', - ]; - state $donotread0 = [' -----', ' -----', '--', '|--', '*']; - state $donotread1 = ['mail from:', 'message-id:', ' from: ']; - state $reademail0 = [' ', '"', '<',]; - state $reademail1 = [ - # There is an email address around the following strings - 'address:', - 'addressed to', - 'could not be delivered to:', - 'delivered to', - 'delivery failed:', - 'did not reach the following recipient:', - 'error-for:', - 'failed recipient:', - 'failed to deliver to', - 'intended recipient:', - 'mailbox is full:', - 'recipient:', - 'rcpt to:', - 'smtp server <', - 'the following recipients returned permanent errors:', - 'the following addresses had permanent errors', - 'the following message to', - 'to: ', - 'unknown user:', - 'unable to deliver mail to the following recipient', - 'undeliverable to', - 'undeliverable address:', - 'you sent mail to', - 'your message has encountered delivery problems to the following recipients:', - 'was automatically rejected', - 'was rejected due to', - ]; - - my $b = $dscontents->[-1]; - my $hasmatched = 0; # There may be an email address around the line - my $readslices = []; # Previous line of this loop - $lowercased = lc $$mbody; - - for my $e ( @$readafter0 ) { - # Cut strings from the begining of $$mbody to the strings defined in $readafter0 - my $i = index($lowercased, $e); next if $i == -1; - $$mbody = substr($$mbody, $i); - } - - for my $e ( split("\n", $$mbody) ) { - # Get the recipient's email address and error messages. - next unless length $e; - - $hasmatched = 0; - $lowercased = lc $e; - push @$readslices, $lowercased; - - last if grep { index($lowercased, $_) == 0 } $startingof->{'rfc822'}->@*; - last if grep { index($lowercased, $_) == 0 } @$readuntil0; - last if grep { index($lowercased, $_) > -1 } @$readuntil1; - next if grep { index($lowercased, $_) == 0 } @$donotread0; - next if grep { index($lowercased, $_) > -1 } @$donotread1; - - while(1) { - # There is an email address with an error message at this line(1) - last unless grep { index($lowercased, $_) == 0 } @$reademail0; - last unless index($lowercased, '@') > 1; - - $hasmatched = 1; - last; - } - - while(2) { - # There is an email address with an error message at this line(2) - last if $hasmatched > 0; - last unless grep { index($lowercased, $_) > -1 } @$reademail1; - last unless index($lowercased, '@') > 1; - - $hasmatched = 2; - last; - } - - while(3) { - # There is an email address without an error message at this line - last if $hasmatched > 0; - last if scalar @$readslices < 2; - last unless grep { index($readslices->[-2], $_) > -1 } @$reademail1; - last unless index($lowercased, '@') > 1; # Must contain "@" - last unless index($lowercased, '.') > 1; # Must contain "." - last unless index($lowercased, '$') == -1; - $hasmatched = 3; - last; - } - - if( $hasmatched > 0 && index($lowercased, '@') > 0 ) { - # May be an email address - my $w = [split(' ', $e)]; - my $x = $b->{'recipient'} || ''; - my $y = ''; - - for my $ee ( @$w ) { - # Find an email address (including "@") - next unless index($ee, '@') > 1; - $y = Sisimai::Address->s3s4($ee); - next unless Sisimai::Address->is_emailaddress($y); - last; - } - - if( $x && $x ne $y ) { - # There are multiple recipient addresses in the message body. - push @$dscontents, Sisimai::Lhost->DELIVERYSTATUS; - $b = $dscontents->[-1]; - } - $b->{'recipient'} = $y; - $recipients++; - $itisbounce ||= 1; - - } elsif( index($e, '(expanded from') > -1 || index($e, '(generated from') > -1 ) { - # (expanded from: neko@example.jp) - $b->{'alias'} = Sisimai::Address->s3s4(substr($e, rindex($e, ' ') + 1,)); - } - $b->{'diagnosis'} .= ' '.$e; - } - } # END OF BODY_DECODER_FOR_FALLBACK - return undef unless $itisbounce; - - my $p1 = index($rfc822text, "\nTo: "); - my $p2 = index($rfc822text, "\n", $p1 + 6); - if( $recipients == 0 && $p1 > 0 ) { - # Try to get a recipient address from "To:" header of the original message - if( my $r = Sisimai::Address->find(substr($rfc822text, $p1 + 5, $p2 - $p1 - 5), 1) ) { - # Found a recipient address - push @$dscontents, Sisimai::Lhost->DELIVERYSTATUS if scalar(@$dscontents) == $recipients; - my $b = $dscontents->[-1]; - $b->{'recipient'} = $r->[0]->{'address'}; - $recipients++; - } + while( $recipients == 0 ) { + # There is no valid recipient address, Try to use the alias addaress as a final recipient + last unless length $dscontents->[0]->{'alias'} > 0; + last unless Sisimai::Address->is_emailaddress($dscontents->[0]->{'alias'}); + $dscontents->[0]->{'recipient'} = $dscontents->[0]->{'alias'}; + $recipients++; } return undef unless $recipients; + require Sisimai::SMTP::Reply; + require Sisimai::SMTP::Status; require Sisimai::SMTP::Command; - require Sisimai::MDA; - my $mdabounced = Sisimai::MDA->inquire($mhead, $mbody); + + if( $beforemesg ne "" ) { + # Pick some values of $dscontents from the string before $startingof->{'message'} + $beforemesg = Sisimai::String->sweep($beforemesg); + $alternates->{'command'} = Sisimai::SMTP::Command->find($beforemesg); + $alternates->{'replycode'} = Sisimai::SMTP::Reply->find($beforemesg, $dscontents->[0]->{'status'}); + $alternates->{'status'} = Sisimai::SMTP::Status->find($beforemesg, $alternates->{'replycode'}); + } + my $issuedcode = lc $beforemesg; + for my $e ( @$dscontents ) { - # Set default values if each value is empty. - $e->{ $_ } ||= $connheader->{ $_ } || '' for keys %$connheader; - - if( exists $e->{'alterrors'} && $e->{'alterrors'} ) { - # Copy alternative error message - $e->{'diagnosis'} ||= $e->{'alterrors'}; - if( index($e->{'diagnosis'}, '-') == 0 || substr($e->{'diagnosis'}, -2, 2) eq '__') { - # Override the value of diagnostic code message - $e->{'diagnosis'} = $e->{'alterrors'} if $e->{'alterrors'}; - } - delete $e->{'alterrors'}; - } + # Set default values stored in "permessage" if each value in "dscontents" is empty. + $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage; $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); + my $lowercased = lc $e->{'diagnosis'}; + + if( $recipients == 1 ) { + # Do not mix the error message of each recipient with "beforemesg" when there is + # multiple recipient addresses in the bounce message + if( index($issuedcode, $lowercased) > -1 ) { + # $beforemesg contains the entire strings of $e->{'diagnosis'} + $e->{'diagnosis'} = $beforemesg; - if( $mdabounced ) { - # Make bounce data by the values returned from Sisimai::MDA->inquire() - $e->{'agent'} = $mdabounced->{'mda'} || 'RFC3464'; - $e->{'reason'} = $mdabounced->{'reason'} || 'undefined'; - $e->{'diagnosis'} = $mdabounced->{'message'} if $mdabounced->{'message'}; - $e->{'command'} = ''; + } else { + # The value of $e->{'diagnosis'} is not contained in $beforemesg + # There may be an important error message in $beforemesg + $e->{'diagnosis'} = Sisimai::String->sweep(sprintf("%s %s", $beforemesg, $e->{'diagnosis'})) + } } - $e->{'date'} ||= $mhead->{'date'}; - $e->{'status'} ||= Sisimai::SMTP::Status->find($e->{'diagnosis'}) || ''; - $e->{'command'} ||= Sisimai::SMTP::Command->find($e->{'diagnosis'}); + $e->{'command'} = Sisimai::SMTP::Command->find($e->{'diagnosis'}) || $alternates->{'command'}; + $e->{'replycode'} = Sisimai::SMTP::Reply->find($e->{'diagnosis'}, $e->{'status'}) || $alternates->{'replycode'}; + $e->{'status'} ||= Sisimai::SMTP::Status->find($e->{'diagnosis'}, $e->{'replycode'}) || $alternates->{'status'}; } - return { 'ds' => $dscontents, 'rfc822' => $rfc822text }; + + # Set the recipient address as To: header in the original message part + $emailparts->[1] = sprintf("To: <%s>\n", $dscontents->[0]->{'recipient'}) unless $emailparts->[1]; + + return { 'ds' => $dscontents, 'rfc822' => $emailparts->[1] }; } 1; + __END__ =encoding utf-8 =head1 NAME -Sisimai::RFC3464 - bounce mail decoder class for Fallback. +Sisimai::RFC3464 - bounce mail decoder class for a bounce mail which have fields defined in RFC3464 =head1 SYNOPSIS diff --git a/lib/Sisimai/RFC3464/ThirdParty.pm b/lib/Sisimai/RFC3464/ThirdParty.pm new file mode 100644 index 000000000..fd15b1f71 --- /dev/null +++ b/lib/Sisimai/RFC3464/ThirdParty.pm @@ -0,0 +1,148 @@ +package Sisimai::RFC3464::ThirdParty; +use v5.26; +use strict; +use warnings; + +state $ThirdParty = { + #"Aol" => ["X-Outbound-Mail-Relay-"], # X-Outbound-Mail-Relay-(Queue-ID|Sender) + "PowerMTA" => ["X-PowerMTA-"], # X-PowerMTA-(VirtualMTA|BounceCategory) + #"Yandex" => ["X-Yandex-"], # X-Yandex-(Queue-ID|Sender) +}; + +sub is3rdparty { + # is3rdparty() returns true if the argument is a line generated by a MTA which have fields defined + # in RFC3464 inside of a bounce mail the MTA returns + # @param string argv1 A line of a bounce mail + # @return bool The line indicates that a bounce mail generated by the 3rd party MTA + my $class = shift; + my $argv1 = shift || return undef; + return __PACKAGE__->returnedby($argv1) ? 1 : 0; +} + +sub returnedby { + # returnedby() returns an MTA name of the 3rd party + # @param string argv1 A line of a bounce mail + # @return string An MTA name of the 3rd party + my $class = shift; + my $argv1 = shift || return undef; return undef unless index($argv1, "X-") == 0; + + for my $e ( keys %$ThirdParty ) { + # Does the argument include the 3rd party specific field? + return $e if index($argv1, $ThirdParty->{ $e }->[0]) == 0; + } + return "" +} + +sub xfield { + # xfield() returns rfc1894.Field() compatible slice for the specific field of the 3rd party MTA + # @param string argv1 A line of the error message + # @return [] RFC1894->field() compatible array + # @see Sisimai::RFC1894 + my $class = shift; + my $argv1 = shift || return []; + my $party = __PACKAGE__->returnedby($argv1); return [] unless $party; + return sprintf("Sisimai::RFC3464::ThirdParty::%s", $party)->xfield($argv1); +} +1; + +# ------------------------------------------------------------------------------------------------- +package Sisimai::RFC3464::ThirdParty::PowerMTA; +use v5.26; +use strict; +use warnings; + +state $FieldGroup = { + "x-powermta-virtualmta" => "host", # X-PowerMTA-VirtualMTA: mx22.neko.example.jp + "x-powermta-bouncecategory" => "text", # X-PowerMTA-BounceCategory: bad-mailbox +}; +state $MessagesOf = { + "bad-domain" => "hostunknown", + "bad-mailbox" => "userunknown", + "inactive-mailbox" => "disabled", + "message-expired" => "expired", + "no-answer-from-host" => "networkerror", + "policy-related" => "policyviolation", + "quota-issues" => "mailboxfull", + "routing-errors" => "systemerror", + "spam-related" => "spamdetected", +}; + +sub xfield { + # Returns an array which is compatible with the value returned from Sisimai::RFC1894->field() + # @param string argv1 A line of the error message + # @return Array ["field-name", "value-type", "value", "field-group", "comment"] + # @see https://bird.com/email/power-mta + my $class = shift; + my $argv1 = shift || return []; + + my $fieldparts = [split(":", $argv1, 2)]; # ["Final-Recipient", " rfc822; "] + my $xfieldname = lc $fieldparts->[0]; # "final-recipient" + my $xgroupname = $FieldGroup->{ $xfieldname }; return [] unless $xgroupname; + my $xfieldlist = ["", "", Sisimai::String->sweep($fieldparts->[1]), $xgroupname, "", "PowerMTA"]; + + # - 0: Field-Name + # - 1: Sub Type: RFC822, DNS, X-Unix, and so on) + # - 2: Value + # - 3: Field Group(addr, code, date, host, stat, text) + # - 4: Comment + # - 5: 3rd Party MTA-Name + if( $xfieldname eq "x-powermta-bouncecategory" ) { + # X-PowerMTA-BounceCategory: bad-mailbox + # Set the bounce reason picked from the value of the field + $xfieldlist->[0] = $xfieldname; + $xfieldlist->[4] = sprintf("reason:%s", $MessagesOf->{ $xfieldlist->[2] } || ""); + + } elsif( $xfieldname eq "x-powermta-virtualmta" ) { + # X-PowerMTA-VirtualMTA: mx22.neko.example.jp + $xfieldlist->[0] = "Reporting-MTA"; + } + + return $xfieldlist; +} + +1; + +__END__ +=encoding utf-8 + +=head1 NAME + +Sisimai::RFC3464::ThirdParty - bounce mail decoder class for a bounce mail which have fields defined +in RFC3464 and some fields begin with "X-" + +=head1 SYNOPSIS + + use Sisimai::RFC3464::ThirdParty; + +=head1 DESCRIPTION + +C is a class which called from called from only C +when the bounce message have a field which begins with "X-" such as "X-PowerMTA-BounceCategory:" + +=head1 CLASS METHODS + +=head2 C> + +C method returns true when the argument is a supported "X-" field + + print Sisimai::RFC3464->is3rdparty(); + +=head2 C, I)>> + +C method method decodes a bounced email and return results as an array reference. +See C for more details. + +=head1 AUTHOR + +azumakuniyuki + +=head1 COPYRIGHT + +Copyright (C) 2024 azumakuniyuki, All rights reserved. + +=head1 LICENSE + +This software is distributed under The BSD 2-Clause License. + +=cut + diff --git a/lib/Sisimai/Reason.pm b/lib/Sisimai/Reason.pm index 39b6b087e..619c3f253 100644 --- a/lib/Sisimai/Reason.pm +++ b/lib/Sisimai/Reason.pm @@ -7,17 +7,17 @@ my $ModulePath = __PACKAGE__->path; my $GetRetried = __PACKAGE__->retry; my $ClassOrder = [ [qw/MailboxFull MesgTooBig ExceedLimit Suspend HasMoved NoRelaying AuthFailure UserUnknown - Filtered RequirePTR NotCompliantRFC BadReputation Rejected HostUnknown SpamDetected Speeding - TooManyConn Blocked/ + Filtered RequirePTR NotCompliantRFC BadReputation ContentError Rejected HostUnknown + SpamDetected Speeding TooManyConn Blocked/ ], [qw/MailboxFull AuthFailure BadReputation Speeding SpamDetected VirusDetected PolicyViolation NoRelaying SystemError NetworkError Suspend ContentError SystemFull NotAccept Expired - SecurityError MailerError/ + SecurityError Suppressed MailerError/ ], [qw/MailboxFull MesgTooBig ExceedLimit Suspend UserUnknown Filtered Rejected HostUnknown SpamDetected Speeding TooManyConn Blocked SpamDetected AuthFailure SecurityError SystemError NetworkError Suspend Expired ContentError HasMoved SystemFull NotAccept MailerError - NoRelaying SyntaxError OnHold/ + NoRelaying Suppressed SyntaxError OnHold/ ], ]; @@ -26,10 +26,21 @@ sub retry { # @return [Hash] Reason list return { 'undefined' => 1, 'onhold' => 1, 'systemerror' => 1, 'securityerror' => 1, 'expired' => 1, - 'suspend' => 1, 'networkerror' => 1, 'hostunknown' => 1, 'userunknown'=> 1 + 'networkerror' => 1, 'hostunknown' => 1, 'userunknown'=> 1 }; } +sub is_explicit { + # is_explicit() returns 0 when the argument is empty or is "undefined" or is "onhold" + # @param string argv1 Reason name + # @return bool false: The reaosn is not explicit + my $class = shift; + my $argv1 = shift || return 0; + + return 0 if $argv1 eq "undefined" || $argv1 eq "onhold" || $argv1 eq ""; + return 1; +} + sub index { # All the error reason list Sisimai support # @return [Array] Reason list @@ -37,7 +48,8 @@ sub index { AuthFailure BadReputation Blocked ContentError ExceedLimit Expired Filtered HasMoved HostUnknown MailboxFull MailerError MesgTooBig NetworkError NotAccept NotCompliantRFC OnHold Rejected NoRelaying SpamDetected VirusDetected PolicyViolation SecurityError - Speeding Suspend RequirePTR SystemError SystemFull TooManyConn UserUnknown SyntaxError/ + Speeding Suspend RequirePTR SystemError SystemFull TooManyConn Suppressed UserUnknown + SyntaxError/ ]; } diff --git a/lib/Sisimai/Reason/Blocked.pm b/lib/Sisimai/Reason/Blocked.pm index 445baa5c7..9b0bec2c3 100644 --- a/lib/Sisimai/Reason/Blocked.pm +++ b/lib/Sisimai/Reason/Blocked.pm @@ -96,8 +96,8 @@ sub match { ['the ip', ' is blacklisted'], ['veuillez essayer plus tard. service refused, please try later. ', '103'], ['veuillez essayer plus tard. service refused, please try later. ', '510'], + ["your access ip", " has been rejected"], ["your sender's ip address is listed at ", '.abuseat.org'], - ]; return 1 if grep { rindex($argv1, $_) > -1 } @$index; return 1 if grep { Sisimai::String->aligned(\$argv1, $_) } @$pairs; diff --git a/lib/Sisimai/Reason/ContentError.pm b/lib/Sisimai/Reason/ContentError.pm index 8a2d16fbd..7e3226c87 100644 --- a/lib/Sisimai/Reason/ContentError.pm +++ b/lib/Sisimai/Reason/ContentError.pm @@ -35,7 +35,14 @@ sub true { # @return [Integer] 1: rejected due to content error # 0: is not content error # @see http://www.ietf.org/rfc/rfc2822.txt - return undef; + my $class = shift; + my $argvs = shift // return undef; + + require Sisimai::Reason::SpamDetected; + return 1 if $argvs->{'reason'} eq 'contenterror'; + return 0 if Sisimai::Reason::SpamDetected->true($argvs); + return 1 if (Sisimai::SMTP::Status->name($argvs->{'deliverystatus'}) || '') eq 'contenterror'; + return __PACKAGE__->match(lc $argvs->{'diagnosticcode'}); } 1; diff --git a/lib/Sisimai/Reason/Filtered.pm b/lib/Sisimai/Reason/Filtered.pm index 11e572caf..2a355deed 100644 --- a/lib/Sisimai/Reason/Filtered.pm +++ b/lib/Sisimai/Reason/Filtered.pm @@ -20,6 +20,7 @@ sub match { 'due to extended inactivity new mail is not currently being accepted for this mailbox', 'has restricted sms e-mail', # AT&T 'is not accepting any mail', + "message filtered", 'message rejected due to user rules', 'not found recipient account', 'refused due to recipient preferences', # Facebook diff --git a/lib/Sisimai/Reason/MailboxFull.pm b/lib/Sisimai/Reason/MailboxFull.pm index dd8b6a3da..899e5923a 100644 --- a/lib/Sisimai/Reason/MailboxFull.pm +++ b/lib/Sisimai/Reason/MailboxFull.pm @@ -44,6 +44,7 @@ sub match { 'maildir delivery failed: domaindisk quota ', 'mailfolder is full', 'no space left on device', + 'not enough disk space', 'not enough storage space in', 'not sufficient disk space', 'over the allowed quota', diff --git a/lib/Sisimai/Reason/MesgTooBig.pm b/lib/Sisimai/Reason/MesgTooBig.pm index cf0722e87..d62c7cc93 100644 --- a/lib/Sisimai/Reason/MesgTooBig.pm +++ b/lib/Sisimai/Reason/MesgTooBig.pm @@ -16,6 +16,7 @@ sub match { state $index = [ 'exceeded maximum inbound message size', + 'exceeded the maximum incoming message size', 'line limit exceeded', 'max message size exceeded', 'message file too big', diff --git a/lib/Sisimai/Reason/SpamDetected.pm b/lib/Sisimai/Reason/SpamDetected.pm index 3a18dd387..ebf855825 100644 --- a/lib/Sisimai/Reason/SpamDetected.pm +++ b/lib/Sisimai/Reason/SpamDetected.pm @@ -107,6 +107,7 @@ sub match { ]; state $pairs = [ ['greylisted', ' please try again in'], + ['mail score (', ' over '], ['mail rejete. mail rejected. ', '506'], ['our filters rate at and above ', ' percent probability of being spam'], ['rejected by ', ' (spam)'], diff --git a/lib/Sisimai/Reason/Suppressed.pm b/lib/Sisimai/Reason/Suppressed.pm new file mode 100644 index 000000000..f5339604f --- /dev/null +++ b/lib/Sisimai/Reason/Suppressed.pm @@ -0,0 +1,85 @@ +package Sisimai::Reason::Suppressed; +use v5.26; +use strict; +use warnings; + +sub text { 'suppressed' } +sub description { "Email was not delivered due to being listed in the suppression list of MTA" } +sub match { + # Try to match that the given text and regular expressions + # @param [String] argv1 String to be matched with regular expressions + # @return [Integer] 0: Did not match + # 1: Matched + # @since v5.2.0 + return 0; +} + +sub true { + # Whether the address is is the suppression list or not + # @param [Sisimai::Fact] argvs Object to be detected the reason + # @return [Integer] 1: The address is in the suppression list + # 0: is not in the suppression list + # @since v4.1.25 + # @see http://www.ietf.org/rfc/rfc2822.txt + my $class = shift; + my $argvs = shift // return undef; + + return 1 if $argvs->{'reason'} eq 'suppressed'; + return __PACKAGE__->match(lc $argvs->{'diagnosticcode'}); +} + +1; +__END__ + +=encoding utf-8 + +=head1 NAME + +Sisimai::Reason::Suppressed - Bounce reason is C or not. + +=head1 SYNOPSIS + + use Sisimai::Reason::Suppressed; + print Sisimai::Reason::Suppressed->match('address neko@example.jp in the suppression list'); # 1 + +=head1 DESCRIPTION + +C checks the bounce reason is C or not. This class is called +only C class. + +This is the error that the recipient adddress is listed in the suppression list of the relay server, +and was not delivered. + +=head1 CLASS METHODS + +=head2 C> + +C method returns the fixed string C. + + print Sisimai::Reason::Suppressed->text; # suppressed + +=head2 C)>> + +C method returns C<1> if the argument matched with patterns defined in this class. + + print Sisimai::Reason::Suppressed->match('address cat@example.jp is in the suppression list'); # 1 + +=head2 C)>> + +C method returns C<1> if the bounce reason is C. The argument must be C +object and this method is called only from C class. + +=head1 AUTHOR + +azumakuniyuki + +=head1 COPYRIGHT + +Copyright (C) 2024 azumakuniyuki, All rights reserved. + +=head1 LICENSE + +This software is distributed under The BSD 2-Clause License. + +=cut + diff --git a/lib/Sisimai/Reason/SystemError.pm b/lib/Sisimai/Reason/SystemError.pm index 3c7e42d59..abfdca9b9 100644 --- a/lib/Sisimai/Reason/SystemError.pm +++ b/lib/Sisimai/Reason/SystemError.pm @@ -2,6 +2,7 @@ package Sisimai::Reason::SystemError; use v5.26; use strict; use warnings; +use Sisimai::String; sub text { 'systemerror' } sub description { 'Email returned due to system error on the remote host' } @@ -38,7 +39,11 @@ sub match { 'timeout waiting for input', 'transaction failed ', ]; + state $pairs = [ + ['unable to connect ', 'daemon'], + ]; return 1 if grep { rindex($argv1, $_) > -1 } @$index; + return 1 if grep { Sisimai::String->aligned(\$argv1, $_) } @$pairs; return 0; } diff --git a/lib/Sisimai/Rhost.pm b/lib/Sisimai/Rhost.pm index 778c33888..494a9606e 100644 --- a/lib/Sisimai/Rhost.pm +++ b/lib/Sisimai/Rhost.pm @@ -4,19 +4,24 @@ use strict; use warnings; state $RhostClass = { - 'Apple' => ['.mail.icloud.com', '.apple.com', '.me.com'], - 'Cox' => ['cox.net'], - 'FrancePTT' => ['.laposte.net', '.orange.fr', '.wanadoo.fr'], - 'GoDaddy' => ['smtp.secureserver.net', 'mailstore1.secureserver.net'], - 'Google' => ['aspmx.l.google.com', 'gmail-smtp-in.l.google.com'], - 'IUA' => ['.email.ua'], - 'KDDI' => ['.ezweb.ne.jp', 'msmx.au.com'], - 'Microsoft' => ['.prod.outlook.com', '.protection.outlook.com'], - 'Mimecast' => ['.mimecast.com'], - 'NTTDOCOMO' => ['mfsmax.docomo.ne.jp'], - 'Spectrum' => ['charter.net'], - 'Tencent' => ['.qq.com'], - 'YahooInc' => ['.yahoodns.net'], + "Aol" => [".mail.aol.com", ".mx.aol.com"], + "Apple" => [".mail.icloud.com", ".apple.com", ".me.com"], + "Cox" => ["cox.net"], + "Facebook" => [".facebook.com"], + "FrancePTT" => [".laposte.net", ".orange.fr", ".wanadoo.fr"], + "GoDaddy" => ["smtp.secureserver.net", "mailstore1.secureserver.net"], + "Google" => ["aspmx.l.google.com", "gmail-smtp-in.l.google.com"], + "GSuite" => ["googlemail.com"], + "IUA" => [".email.ua"], + "KDDI" => [".ezweb.ne.jp", "msmx.au.com"], + "MessageLabs" => [".messagelabs.com"], + "Microsoft" => [".prod.outlook.com", ".protection.outlook.com", ".onmicrosoft.com", ".exchangelabs.com",], + "Mimecast" => [".mimecast.com"], + "NTTDOCOMO" => ["mfsmax.docomo.ne.jp"], + "Outlook" => [".hotmail.com"], + "Spectrum" => ["charter.net"], + "Tencent" => [".qq.com"], + "YahooInc" => [".yahoodns.net"], }; sub find { @@ -27,19 +32,35 @@ sub find { my $argvs = shift || return undef; return undef unless length $argvs->{'diagnosticcode'}; + my $rhostclass = ''; + my $clienthost = lc $argvs->{'lhost'} || ''; my $remotehost = lc $argvs->{'rhost'} || ''; my $domainpart = lc $argvs->{'destination'} || ''; return undef unless length $remotehost.$domainpart; - my $rhostmatch = undef; - my $rhostclass = ''; - for my $e ( keys %$RhostClass ) { - # Try to match with each value of RhostClass - $rhostmatch = 1 if grep { index($remotehost, $_) > -1 } $RhostClass->{ $e }->@*; - $rhostmatch ||= 1 if grep { index($_, $domainpart) > -1 } $RhostClass->{ $e }->@*; - next unless $rhostmatch; - - $rhostclass = __PACKAGE__.'::'.$e; + FINDRHOST: while( $rhostclass eq "" ) { + # Try to match the hostname patterns with the following order: + # 1. destination: The domain part of the recipient address + # 2. rhost: remote hostname + # 3. lhost: local MTA hostname + for my $e ( keys %$RhostClass ) { + # Try to match the domain part with each value of RhostClass + next unless grep { index($_, $domainpart) > -1 } $RhostClass->{ $e }->@*; + $rhostclass = __PACKAGE__.'::'.$e; last FINDRHOST; + } + + for my $e ( keys %$RhostClass ) { + # Try to match the remote host with each value of RhostClass + next unless grep { index($remotehost, $_) > -1 } $RhostClass->{ $e }->@*; + $rhostclass = __PACKAGE__.'::'.$e; last FINDRHOST; + } + + # Neither the remote host nor the destination did not matched with any value of RhostClass + for my $e ( keys %$RhostClass ) { + # Try to match the client host with each value of RhostClass + next unless grep { index($clienthost, $_) > -1 } $RhostClass->{ $e }->@*; + $rhostclass = __PACKAGE__."::".$e; last FINDRHOST; + } last; } return undef unless $rhostclass; diff --git a/lib/Sisimai/Rhost/Aol.pm b/lib/Sisimai/Rhost/Aol.pm new file mode 100644 index 000000000..9c1f40b78 --- /dev/null +++ b/lib/Sisimai/Rhost/Aol.pm @@ -0,0 +1,69 @@ +package Sisimai::Rhost::Aol; +use v5.26; +use strict; +use warnings; + +sub find { + # Detect bounce reason for Aol Mail: https://www.aol.com + # @param [Sisimai::Fact] argvs Decoded email object + # @return [String] Detected bounce reason + # @since v5.2.0 + my $class = shift; + my $argvs = shift // return undef; return "" unless $argvs->{'diagnosticcode'}; + + state $messagesof = { + "hostunknown" => ["Host or domain name not found"], + "notaccept" => ["type=MX: Malformed or unexpected name server reply"], + }; + + my $issuedcode = $argvs->{'diagnosticcode'}; + my $reasontext = ''; + + for my $e ( keys %$messagesof ) { + # Try to find the error message matches with the given error message string + next unless grep { index($issuedcode, $_) > -1 } $messagesof->{ $e }->@*; + $reasontext = $e; + last; + } + return $reasontext; +} + +1; +__END__ + +=encoding utf-8 + +=head1 NAME + +Sisimai::Rhost::Aol - Detect the bounce reason returned from Aol Mail: https://www.aol.com + +=head1 SYNOPSIS + + use Sisimai::Rhost::Aol; + +=head1 DESCRIPTION + +C detects the bounce reason from the content of C object as an +argument of C method when the value of C of the object is C<*.aol.com>. +This class is called only C class. + +=head1 CLASS METHODS + +=head2 C)>> + +C method detects the bounce reason. + +=head1 AUTHOR + +azumakuniyuki + +=head1 COPYRIGHT + +Copyright (C) 2024 azumakuniyuki, All rights reserved. + +=head1 LICENSE + +This software is distributed under The BSD 2-Clause License. + +=cut + diff --git a/lib/Sisimai/Rhost/Cox.pm b/lib/Sisimai/Rhost/Cox.pm index 6ee62a3a3..812b9c1c5 100644 --- a/lib/Sisimai/Rhost/Cox.pm +++ b/lib/Sisimai/Rhost/Cox.pm @@ -10,7 +10,7 @@ sub find { # @see https://www.cox.com/residential/support/email-error-codes.html # @since v4.25.8 my $class = shift; - my $argvs = shift // return undef; + my $argvs = shift // return undef; return "" unless $argvs->{'diagnosticcode'}; state $errorcodes = { # CXBL diff --git a/lib/Sisimai/Rhost/Facebook.pm b/lib/Sisimai/Rhost/Facebook.pm new file mode 100644 index 000000000..79e89e9ac --- /dev/null +++ b/lib/Sisimai/Rhost/Facebook.pm @@ -0,0 +1,132 @@ +package Sisimai::Rhost::Facebook; +use v5.26; +use strict; +use warnings; + +sub find { + # Detect bounce reason for Facebook + # @param [Sisimai::Fact] argvs Decoded email object + # @return [String] Detected bounce reason + # @see https://www.facebook.com/postmaster/response_codes + # @since v5.2.0 + my $class = shift; + my $argvs = shift // return undef; + return "" unless $argvs->{'diagnosticcode'}; + return "" unless index($argvs->{'diagnosticcode'}, '-'); + + state $errorcodes = { + # http://postmaster.facebook.com/response_codes + # NOT TESTD EXCEPT RCP-P2 + "authfailure" => [ + "POL-P7", # The message does not comply with Facebook's Domain Authentication requirements. + ], + "blocked" => [ + "POL-P1", # Your mail server's IP Address is listed on the Spamhaus PBL. + "POL-P2", # Facebook will no longer accept mail from your mail server's IP Address. + "POL-P3", # Facebook is not accepting messages from your mail server. This will persist for 4 to 8 hours. + "POL-P4", # Facebook is not accepting messages from your mail server. This will persist for 24 to 48 hours. + "POL-T1", # Facebook is not accepting messages from your mail server, but they may be retried later. This will persist for 1 to 2 hours. + "POL-T2", # Facebook is not accepting messages from your mail server, but they may be retried later. This will persist for 4 to 8 hours. + "POL-T3", # Facebook is not accepting messages from your mail server, but they may be retried later. This will persist for 24 to 48 hours. + ], + "contenterror" => [ + "MSG-P2", # The message contains an attachment type that Facebook does not accept. + ], + "filtered" => [ + "RCP-P2", # The attempted recipient's preferences prevent messages from being delivered. + "RCP-P3", # The attempted recipient's privacy settings blocked the delivery. + ], + "mesgtoobig" => [ + "MSG-P1", # The message exceeds Facebook's maximum allowed size. + "INT-P2", # The message exceeds Facebook's maximum allowed size. + ], + "notcompliantrfc" => [ + "MSG-P3", # The message contains multiple instances of a header field that can only be present once. + ], + "rejected" => [ + "DNS-P1", # Your SMTP MAIL FROM domain does not exist. + "DNS-P2", # Your SMTP MAIL FROM domain does not have an MX record. + "DNS-T1", # Your SMTP MAIL FROM domain exists but does not currently resolve. + ], + "requireptr" => [ + "DNS-P3", # Your mail server does not have a reverse DNS record. + "DNS-T2", # You mail server's reverse DNS record does not currently resolve. + ], + "spamdetected" => [ + "POL-P6", # The message contains a url that has been blocked by Facebook. + "POL-P7", # The message does not comply with Facebook's abuse policies and will not be accepted. + ], + "suspend" => [ + "RCP-T4", # The attempted recipient address is currently deactivated. The user may or may not reactivate it. + ], + "systemerror" => [ + "RCP-T1", # The attempted recipient address is not currently available due to an internal system issue. This is a temporary condition. + ], + "toomanyconn" => [ + "CON-T1", # Facebook's mail server currently has too many connections open to allow another one. + "CON-T2", # Your mail server currently has too many connections open to Facebook's mail servers. + "CON-T3", # Your mail server has opened too many new connections to Facebook's mail servers in a short period of time. + "CON-T4", # Your mail server has exceeded the maximum number of recipients for its current connection. + "MSG-T1", # The number of recipients on the message exceeds Facebook's allowed maximum. + ], + "userunknown" => [ + "RCP-P1", # The attempted recipient address does not exist. + "INT-P1", # The attempted recipient address does not exist. + "INT-P3", # The attempted recpient group address does not exist. + "INT-P4", # The attempted recipient address does not exist. + ], + "virusdetected" => [ + "POL-P5", # The message contains a virus. + ], + }; + my $errorindex = index($argvs->{'diagnosticcode'}, "-"); + my $errorlabel = substr($argvs->{'diagnosticcode'}, $errorindex - 3, $errorindex + 3); + my $reasontext = ""; + + for my $e ( keys %$errorcodes ) { + # The key is a bounce reason name + next unless grep { $errorlabel eq $_ } $errorcodes->{ $e }->@*; + $reasontext = $e; last + } + return $reasontext; +} + +1; +__END__ + +=encoding utf-8 + +=head1 NAME + +Sisimai::Rhost::Facebook - Detect the bounce reason returned from Facebook + +=head1 SYNOPSIS + + use Sisimai::Rhost::Facebook; + +=head1 DESCRIPTION + +C detects the bounce reason from the content of C object +as an argument of C method when the value of C of the object includes C. +This class is called only C class. + +=head1 CLASS METHODS + +=head2 C)>> + +C method detects the bounce reason. + +=head1 AUTHOR + +azumakuniyuki + +=head1 COPYRIGHT + +Copyright (C) 2024 azumakuniyuki, All rights reserved. + +=head1 LICENSE + +This software is distributed under The BSD 2-Clause License. + +=cut + diff --git a/lib/Sisimai/Rhost/FrancePTT.pm b/lib/Sisimai/Rhost/FrancePTT.pm index bff3819cf..fae3378cf 100644 --- a/lib/Sisimai/Rhost/FrancePTT.pm +++ b/lib/Sisimai/Rhost/FrancePTT.pm @@ -11,7 +11,7 @@ sub find { # https://smtpfieldmanual.com/provider/orange # @since v4.22.3 my $class = shift; - my $argvs = shift // return undef; + my $argvs = shift // return undef; return "" unless $argvs->{'diagnosticcode'}; state $errorcodes = { # - 550 5.7.1 Service unavailable; client [192.0.2.1] blocked using Spamhaus diff --git a/lib/Sisimai/Rhost/GSuite.pm b/lib/Sisimai/Rhost/GSuite.pm new file mode 100644 index 000000000..89f38781d --- /dev/null +++ b/lib/Sisimai/Rhost/GSuite.pm @@ -0,0 +1,73 @@ +package Sisimai::Rhost::GSuite; +use v5.26; +use strict; +use warnings; + +sub find { + # Detect bounce reason from Google Workspace (formerly G Suite) https://workspace.google.com/ + # @param [Sisimai::Fact] argvs Decoded email object + # @return [String] The bounce reason for GSuite + # @since v5.2.0 + my $class = shift; + my $argvs = shift // return undef; return '' unless length $argvs->{'diagnosticcode'}; + + state $messagesof = { + "hostunknown" => [" responded with code NXDOMAIN", "Domain name not found"], + "networkerror" => [" had no relevant answers.", "responded with code NXDOMAIN", "Domain name not found"], + "notaccept" => ["Null MX"], + "userunknown" => ["because the address couldn't be found. Check for typos or unnecessary spaces and try again."], + }; + my $statuscode = ""; $statuscode = substr($argvs->{'deliverystatus'}, 0, 1) if $argvs->{'deliverystatus'}; + my $esmtpreply = ""; $esmtpreply = substr($argvs->{'replycode'}, 0, 1) if $argvs->{'replycode'}; + my $reasontext = ""; + + for my $e ( keys %$messagesof ) { + # The key is a bounce reason name + next unless grep { index($argvs->{'diagnosticcode'}, $_) > -1 } $messagesof->{ $e }->@*; + next if $e eq "networkerror" && ($statuscode eq "5" || $esmtpreply eq "5"); + next if $e eq "hostunknown" && ($statuscode eq "4" || $statuscode eq ""); + next if $e eq "hostunknown" && ($esmtpreply eq "4" || $esmtpreply eq ""); + $reasontext = $e; last; + } + return $reasontext; +} + +1; +__END__ + +=encoding utf-8 + +=head1 NAME + +Sisimai::Rhost::GSuite - Detect the bounce reason returned from Google Workspace (formerly G Suite) + +=head1 SYNOPSIS + + use Sisimai::Rhost::GSuite; + +=head1 DESCRIPTION + +C detects the bounce reason from the content of C object as +an argument of C method when the value of C of the object end with C +This class is called only C class. + +=head1 CLASS METHODS + +=head2 C)>> + +C method detects the bounce reason. + +=head1 AUTHOR + +azumakuniyuki + +=head1 COPYRIGHT + +Copyright (C) 2024 azumakuniyuki, All rights reserved. + +=head1 LICENSE + +This software is distributed under The BSD 2-Clause License. + +=cut + diff --git a/lib/Sisimai/Rhost/GoDaddy.pm b/lib/Sisimai/Rhost/GoDaddy.pm index eac4dc042..e95698091 100644 --- a/lib/Sisimai/Rhost/GoDaddy.pm +++ b/lib/Sisimai/Rhost/GoDaddy.pm @@ -11,7 +11,7 @@ sub find { # @see https://ca.godaddy.com/help/fix-rejected-email-with-a-bounce-error-40685 # @since v4.22.2 my $class = shift; - my $argvs = shift // return undef; + my $argvs = shift // return undef; return "" unless $argvs->{'diagnosticcode'}; state $errorcodes = { # Sender bounces diff --git a/lib/Sisimai/Rhost/Google.pm b/lib/Sisimai/Rhost/Google.pm index 631c4e3f6..7c33b270c 100644 --- a/lib/Sisimai/Rhost/Google.pm +++ b/lib/Sisimai/Rhost/Google.pm @@ -10,7 +10,7 @@ sub find { # @see https://support.google.com/a/answer/3726730?hl=en # @since v4.0.0 my $class = shift; - my $argvs = shift // return undef; + my $argvs = shift // return undef; return "" unless $argvs->{'diagnosticcode'}; return '' unless Sisimai::SMTP::Reply->test($argvs->{'replycode'}); return '' unless Sisimai::SMTP::Status->test($argvs->{'deliverystatus'}); diff --git a/lib/Sisimai/Rhost/IUA.pm b/lib/Sisimai/Rhost/IUA.pm index f36c11e6a..20b5a7120 100644 --- a/lib/Sisimai/Rhost/IUA.pm +++ b/lib/Sisimai/Rhost/IUA.pm @@ -9,7 +9,7 @@ sub find { # @return [String] The bounce reason at https://www.i.ua/ # @since v4.25.0 my $class = shift; - my $argvs = shift // return undef; + my $argvs = shift // return undef; return "" unless $argvs->{'diagnosticcode'}; state $errorcodes = { # https://mail.i.ua/err/$(CODE) diff --git a/lib/Sisimai/Rhost/KDDI.pm b/lib/Sisimai/Rhost/KDDI.pm index 058e3db7c..293720812 100644 --- a/lib/Sisimai/Rhost/KDDI.pm +++ b/lib/Sisimai/Rhost/KDDI.pm @@ -9,7 +9,7 @@ sub find { # @return [String] The bounce reason au.com and ezweb.ne.jp # @since v4.22.6 my $class = shift; - my $argvs = shift // return undef; + my $argvs = shift // return undef; return "" unless $argvs->{'diagnosticcode'}; state $messagesof = { 'filtered' => '550 : user unknown', # The response was: 550 : User unknown diff --git a/lib/Sisimai/Rhost/MessageLabs.pm b/lib/Sisimai/Rhost/MessageLabs.pm new file mode 100644 index 000000000..d87cdae68 --- /dev/null +++ b/lib/Sisimai/Rhost/MessageLabs.pm @@ -0,0 +1,69 @@ +package Sisimai::Rhost::MessageLabs; +use v5.26; +use strict; +use warnings; + +sub find { + # Detect bounce reason from Email Security (formerly MessageLabs.com) + # @param [Sisimai::Fact] argvs Decoded email object + # @return [String] The bounce reason for MessageLabs + # @see https://www.broadcom.com/products/cybersecurity/email + # @since v5.2.0 + my $class = shift; + my $argvs = shift // return undef; return '' unless length $argvs->{'diagnosticcode'}; + + state $messagesof = { + 'securityerror' => ["Please turn on SMTP Authentication in your mail client"], + 'userunknown' => ["542 ", " Rejected", "No such user"], + }; + my $issuedcode = $argvs->{'diagnosticcode'}; + my $reasontext = ''; + + for my $e ( keys %$messagesof ) { + # Try to find the error message matches with the given error message string + next unless grep { index($issuedcode, $_) > -1 } $messagesof->{ $e }->@*; + $reasontext = $e; + last; + } + return $reasontext; +} + +1; +__END__ + +=encoding utf-8 + +=head1 NAME + +Sisimai::Rhost::MessageLabs - Detect the bounce reason returned from MessageLabs + +=head1 SYNOPSIS + + use Sisimai::Rhost::MessageLabs; + +=head1 DESCRIPTION + +C detects the bounce reason from the content of C object as +an argument of C method when the value of C of the object end with C. +This class is called only C class. + +=head1 CLASS METHODS + +=head2 C)>> + +C method detects the bounce reason. + +=head1 AUTHOR + +azumakuniyuki + +=head1 COPYRIGHT + +Copyright (C) 2024 azumakuniyuki, All rights reserved. + +=head1 LICENSE + +This software is distributed under The BSD 2-Clause License. + +=cut + diff --git a/lib/Sisimai/Rhost/Microsoft.pm b/lib/Sisimai/Rhost/Microsoft.pm index d52917f0a..3874495fa 100644 --- a/lib/Sisimai/Rhost/Microsoft.pm +++ b/lib/Sisimai/Rhost/Microsoft.pm @@ -278,6 +278,7 @@ sub find { # dress of the server or service that's generating the error, which you can use to # identify the party responsible for fixing this. ['4.4.316', 0, 0, 'connection refused'], # [Message=Socket error code 10061] + ['5.4.316', 0, 0, 'connection refused'], # [Message=Socket error code 10061] # - A configuration error has caused an email loop. 5.4.6 is generated by on-premises # Exchange server (you'll see this code in hybrid environments). 5.4.14 is generated @@ -293,6 +294,10 @@ sub find { ['5.4.4', 0, 0, 'invalid arguments'], ['5.4.6', 0, 0, 'routing loop detected'], ['5.4.14', 0, 0, 'routing loop detected'], + + # Imported from Sisimai::Lhost::Office365 + ['4.4.312', 0, 0, 'dns query failed'], # [Message=InfoNoRecords] + ['5.4.312', 0, 0, 'dns query failed'], # [Message=InfoNoRecords] ], 'norelaying' => [ # Exchange Server 2019 ---------------------------------------------------------------- @@ -717,6 +722,9 @@ sub find { # Previous versions of Exchange Server ------------------------------------------------ ['5.1.2', 0, 0, 'invalid x.400 address'], + + # Imported from Sisimai::/Lhost::Office365 + ['5.1.351', 0, 0, 'remote server returned unknown recipient or mailbox unavailable'], ], }; diff --git a/lib/Sisimai/Rhost/Mimecast.pm b/lib/Sisimai/Rhost/Mimecast.pm index 669d42743..19989ed2c 100644 --- a/lib/Sisimai/Rhost/Mimecast.pm +++ b/lib/Sisimai/Rhost/Mimecast.pm @@ -11,6 +11,7 @@ sub find { # @since v4.25.15 my $class = shift; my $argvs = shift // return undef; + return '' unless $argvs->{'diagnosticcode'}; return '' unless Sisimai::SMTP::Reply->test($argvs->{'replycode'}); state $messagesof = { diff --git a/lib/Sisimai/Rhost/NTTDOCOMO.pm b/lib/Sisimai/Rhost/NTTDOCOMO.pm index f5f513074..08ed316d3 100644 --- a/lib/Sisimai/Rhost/NTTDOCOMO.pm +++ b/lib/Sisimai/Rhost/NTTDOCOMO.pm @@ -9,7 +9,7 @@ sub find { # @return [String] The bounce reason for docomo.ne.jp # @since v4.25.15 my $class = shift; - my $argvs = shift // return undef; + my $argvs = shift // return undef; return "" unless $argvs->{'diagnosticcode'}; my $messagesof = { 'mailboxfull' => ['552 too much mail data'], diff --git a/lib/Sisimai/Rhost/Outlook.pm b/lib/Sisimai/Rhost/Outlook.pm new file mode 100644 index 000000000..00d3c322a --- /dev/null +++ b/lib/Sisimai/Rhost/Outlook.pm @@ -0,0 +1,68 @@ +package Sisimai::Rhost::Outlook; +use v5.26; +use strict; +use warnings; + +sub find { + # Detect bounce reason from Microsoft Outlook.com: https://www.outlook.com/ + # @param [Sisimai::Fact] argvs Decoded email object + # @return [String] The bounce reason for Outlook + # @since v5.2.0 + my $class = shift; + my $argvs = shift // return undef; return '' unless length $argvs->{'diagnosticcode'}; + + state $messagesof = { + 'hostunknown' => ['The mail could not be delivered to the recipient because the domain is not reachable'], + 'userunknown' => ['Requested action not taken: mailbox unavailable'], + }; + my $issuedcode = $argvs->{'diagnosticcode'}; + my $reasontext = ''; + + for my $e ( keys %$messagesof ) { + # Try to find the error message matches with the given error message string + next unless grep { index($issuedcode, $_) > -1 } $messagesof->{ $e }->@*; + $reasontext = $e; + last; + } + return $reasontext; +} + +1; +__END__ + +=encoding utf-8 + +=head1 NAME + +Sisimai::Rhost::Outlook - Detect the bounce reason returned from Microsoft Outlook.com + +=head1 SYNOPSIS + + use Sisimai::Rhost::Outlook; + +=head1 DESCRIPTION + +C detects the bounce reason from the content of C object as +an argument of C method when the value of C of the object end with C. +This class is called only C class. + +=head1 CLASS METHODS + +=head2 C)>> + +C method detects the bounce reason. + +=head1 AUTHOR + +azumakuniyuki + +=head1 COPYRIGHT + +Copyright (C) 2024 azumakuniyuki, All rights reserved. + +=head1 LICENSE + +This software is distributed under The BSD 2-Clause License. + +=cut + diff --git a/lib/Sisimai/Rhost/Spectrum.pm b/lib/Sisimai/Rhost/Spectrum.pm index f72536045..6d9541799 100644 --- a/lib/Sisimai/Rhost/Spectrum.pm +++ b/lib/Sisimai/Rhost/Spectrum.pm @@ -9,7 +9,7 @@ sub find { # @return [String] The bounce reason at Spectrum # @since v4.25.8 my $class = shift; - my $argvs = shift // return undef; + my $argvs = shift // return undef; return "" unless $argvs->{'diagnosticcode'}; state $errorcodes = [ # https://www.spectrumbusiness.net/support/internet/understanding-email-error-codes diff --git a/lib/Sisimai/Rhost/Tencent.pm b/lib/Sisimai/Rhost/Tencent.pm index 5054327a2..fcc7f341d 100644 --- a/lib/Sisimai/Rhost/Tencent.pm +++ b/lib/Sisimai/Rhost/Tencent.pm @@ -10,7 +10,7 @@ sub find { # @see https://service.mail.qq.com/detail/122 # @since v4.25.0 my $class = shift; - my $argvs = shift // return undef; + my $argvs = shift // return undef; return "" unless $argvs->{'diagnosticcode'}; state $messagesof = { 'authfailure' => [ diff --git a/lib/Sisimai/Rhost/YahooInc.pm b/lib/Sisimai/Rhost/YahooInc.pm index e9717ae91..cead46541 100644 --- a/lib/Sisimai/Rhost/YahooInc.pm +++ b/lib/Sisimai/Rhost/YahooInc.pm @@ -12,7 +12,7 @@ sub find { # https://www.postmastery.com/yahoo-postmaster/ # @since v5.1.0 my $class = shift; - my $argvs = shift // return undef; + my $argvs = shift // return undef; return "" unless $argvs->{'diagnosticcode'}; state $messagesof = { 'authfailure' => [ diff --git a/lib/Sisimai/SMTP/Command.pm b/lib/Sisimai/SMTP/Command.pm index 78dbb285d..3717f7fea 100644 --- a/lib/Sisimai/SMTP/Command.pm +++ b/lib/Sisimai/SMTP/Command.pm @@ -3,6 +3,16 @@ use v5.26; use strict; use warnings; +state $Availables = [ + "HELO", "EHLO", "MAIL", "RCPT", "DATA", "QUIT", "RSET", "NOOP", "VRFY", "ETRN", "EXPN", "HELP", + "AUTH", "STARTTLS", "XFORWARD", + "CONN", # CONN is a pseudo SMTP command used only in Sisimai +]; +state $Detectable = [ + "HELO", "EHLO", "STARTTLS", "AUTH PLAIN", "AUTH LOGIN", "AUTH CRAM-", "AUTH DIGEST-", "MAIL F", + "RCPT", "RCPT T", "DATA", "QUIT", "XFORWARD", +]; + sub test { # Check that an SMTP command in the argument is valid or not # @param [String] argv0 An SMTP command @@ -10,11 +20,9 @@ sub test { # @since v5.0.0 my $class = shift; my $argv0 = shift // return undef; - my $table = [qw|HELO EHLO MAIL RCPT DATA QUIT RSET NOOP VRFY ETRN EXPN HELP AUTH STARTTLS XFORWARD|]; return undef unless length $argv0 > 3; - return 1 if grep { index($argv0, $_) > -1 } @$table; - return 1 if index($argv0, 'CONN') > -1; # CONN is a pseudo SMTP command used only in Sisimai + return 1 if grep { index($argv0, $_) > -1 } @$Availables; return 0; } @@ -22,33 +30,40 @@ sub find { # Pick an SMTP command from the given string # @param [String] argv0 A transcript text MTA returned # @return [String] An SMTP command - # @return [undef] Failed to find an SMTP command or the 1st argument is missing # @since v5.0.0 my $class = shift; my $argv0 = shift // return undef; return undef unless __PACKAGE__->test($argv0); - state $detectable = [ - 'HELO', 'EHLO', 'STARTTLS', 'AUTH PLAIN', 'AUTH LOGIN', 'AUTH CRAM-', 'AUTH DIGEST-', - 'MAIL F', 'RCPT', 'RCPT T', 'DATA', 'QUIT', 'XFORWARD', - ]; - my $stringsize = length $argv0; + my $issuedcode = ' '.lc($argv0).' '; my $commandmap = { 'STAR' => 'STARTTLS', 'XFOR' => 'XFORWARD' }; my $commandset = []; - my $previouspp = 0; - for my $e ( @$detectable ) { + for my $e ( @$Detectable ) { # Find an SMTP command from the given string - my $p0 = index($argv0, $e, $previouspp); - next if $p0 < 0; - last if $p0 + 4 > $stringsize; - $previouspp = $p0; - - my $cv = substr($argv0, $p0, 4); next if grep { $cv eq $_ } @$commandset; + my $p0 = index($argv0, $e); next if $p0 < 0; + if( index($e, " ") < 0 ) { + # For example, "RCPT T" does not appear in an email address or a domain name + my $cx = 1; while(1) { + # Exclude an SMTP command in the part of an email address, a domain name, such as + # DATABASE@EXAMPLE.JP, EMAIL.EXAMPLE.COM, and so on. + my $ca = ord(substr($issuedcode, $p0, 1)); + my $cz = ord(substr($issuedcode, $p0 + length($e) + 1, 1)); + + last if $ca > 47 && $ca < 58 || $cz > 47 && $cz < 58; # 0-9 + last if $ca > 63 && $ca < 91 || $cz > 63 && $cz < 91; # @-Z + last if $ca > 96 && $ca < 123 || $cz > 96 && $cz < 123; # `-z + $cx = 0; last; + } + next if $cx == 1; + } + + # There is the same command in the "commanset" or nor + my $cv = substr($e, 0, 4); next if grep { index($cv, $_) == 0 } @$commandset; $cv = $commandmap->{ $cv } if exists $commandmap->{ $cv }; push @$commandset, $cv; } - return undef unless scalar @$commandset; + return "" unless scalar @$commandset; return pop @$commandset; } diff --git a/lib/Sisimai/SMTP/Reply.pm b/lib/Sisimai/SMTP/Reply.pm index 498d6e134..ffa6d3ef6 100644 --- a/lib/Sisimai/SMTP/Reply.pm +++ b/lib/Sisimai/SMTP/Reply.pm @@ -163,14 +163,22 @@ sub find { for my $e ( @$replycodes ) { # Try to find an SMTP Reply Code from the given string - my $replyindex = index($esmtperror, $e); next if $replyindex == -1; - my $formerchar = ord(substr($esmtperror, $replyindex - 1, 1)) || 0; - my $latterchar = ord(substr($esmtperror, $replyindex + 3, 1)) || 0; - - next if $formerchar > 45 && $formerchar < 58; - next if $latterchar > 45 && $latterchar < 58; - $esmtpreply = $e; - last; + my $appearance = index($esmtperror, $e); next if $appearance == -1; + my $startingat = 1; + my $mesglength = length $esmtperror; + + while( $startingat + 3 < $mesglength ) { + # Find all the reply code in the error message + my $replyindex = index($esmtperror, $e, $startingat); last if $replyindex == -1; + my $formerchar = ord(substr($esmtperror, $replyindex - 1, 1)) || 0; + my $latterchar = ord(substr($esmtperror, $replyindex + 3, 1)) || 0; + + if( $formerchar > 45 && $formerchar < 58 ){ $startingat += $replyindex + 3; next } + if( $latterchar > 45 && $latterchar < 58 ){ $startingat += $replyindex + 3; next } + $esmtpreply = $e; + last; + } + last if $esmtpreply; } return $esmtpreply; } diff --git a/lib/Sisimai/SMTP/Status.pm b/lib/Sisimai/SMTP/Status.pm index dd53576bc..fcbcb90ce 100644 --- a/lib/Sisimai/SMTP/Status.pm +++ b/lib/Sisimai/SMTP/Status.pm @@ -486,7 +486,7 @@ use constant StandardCode => { '4.1.7' => 'rejected', # Bad sender's mailbox address syntax '4.1.8' => 'rejected', # Bad sender's system address '4.1.9' => 'systemerror', # Message relayed to non-compliant mailer - '4.2.1' => 'suspend', # Mailbox disabled, not accepting messages + '4.2.1' => 'blocked', # Mailbox disabled, not accepting messages '4.2.2' => 'mailboxfull', # Mailbox full '4.2.3' => 'exceedlimit', # Message length exceeds administrative limit '4.2.4' => 'filtered', # Mailing list expansion problem @@ -801,9 +801,11 @@ sub find { push @$statuscode, $readbuffer; } push @$statuscode, $anotherone if length $anotherone; - return '' if scalar @$statuscode == 0; - return shift @$statuscode; + + # Select one from picked status codes + my $cv = shift @$statuscode; for my $e ( @$statuscode ) { $cv = __PACKAGE__->prefer($cv, $e, "") } + return $cv; } sub prefer { @@ -851,11 +853,16 @@ sub prefer { return $statuscode if $zeroindex2->{'error'} > 0; # An SMTP status code is "X.0.0" return $codeinmesg if $statuscode eq '4.4.7'; # "4.4.7" is an ambiguous code + return $codeinmesg if $statuscode eq '4.7.0'; # "4.7.0" indicates "too many errors" return $codeinmesg if index($statuscode, '5.3.') == 0; # "5.3.Z" is an error of a system + return $codeinmesg if index($statuscode, '.5.1') > 0; # "X.5.1" indicates an invalid command + return $codeinmesg if index($statuscode, '.5.2') > 0; # "X.5.2" indicates a syntax error + return $codeinmesg if index($statuscode, '.5.4') > 0; # "X.5.4" indicates an invalid command arguments + return $codeinmesg if index($statuscode, '.5.5') > 0; # "X.5.5" indicates a wrong protocol version if( $statuscode eq '5.1.1' ) { # "5.1.1" is a code of "userunknown" - return $statuscode if $zeroindex1->{'error'} > 0; + return $statuscode if index($codeinmesg, '5.5.') == 0 || $zeroindex1->{'error'} > 0; return $codeinmesg; } elsif( $statuscode eq '5.1.3' ) { diff --git a/lib/Sisimai/String.pm b/lib/Sisimai/String.pm index 5494a838f..5e127fb27 100644 --- a/lib/Sisimai/String.pm +++ b/lib/Sisimai/String.pm @@ -89,7 +89,7 @@ sub ipv4 { my $argv0 = shift || return undef; return [] if length $argv0 < 7; my $ipv4a = []; - for my $e ( '(', ')', '[', ']' ) { + for my $e ( '(', ')', '[', ']', ',' ) { # Rewrite: "mx.example.jp[192.0.2.1]" => "mx.example.jp 192.0.2.1" my $p0 = index($argv0, $e); next if $p0 < 0; substr($argv0, $p0, 1, ' '); diff --git a/set-of-emails/maildir/bsd/lhost-googlegroups-15.eml b/set-of-emails/maildir/bsd/lhost-googleworkspace-01.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-googlegroups-15.eml rename to set-of-emails/maildir/bsd/lhost-googleworkspace-01.eml diff --git a/set-of-emails/maildir/bsd/lhost-x4-08.eml b/set-of-emails/maildir/bsd/lhost-x2-06.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-x4-08.eml rename to set-of-emails/maildir/bsd/lhost-x2-06.eml diff --git a/set-of-emails/maildir/bsd/lhost-gsuite-01.eml b/set-of-emails/maildir/bsd/rfc3464-51.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-gsuite-01.eml rename to set-of-emails/maildir/bsd/rfc3464-51.eml diff --git a/set-of-emails/maildir/bsd/lhost-gsuite-03.eml b/set-of-emails/maildir/bsd/rfc3464-52.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-gsuite-03.eml rename to set-of-emails/maildir/bsd/rfc3464-52.eml diff --git a/set-of-emails/maildir/bsd/lhost-gsuite-04.eml b/set-of-emails/maildir/bsd/rfc3464-53.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-gsuite-04.eml rename to set-of-emails/maildir/bsd/rfc3464-53.eml diff --git a/set-of-emails/maildir/bsd/lhost-gsuite-05.eml b/set-of-emails/maildir/bsd/rfc3464-54.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-gsuite-05.eml rename to set-of-emails/maildir/bsd/rfc3464-54.eml diff --git a/set-of-emails/maildir/bsd/lhost-gsuite-06.eml b/set-of-emails/maildir/bsd/rfc3464-55.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-gsuite-06.eml rename to set-of-emails/maildir/bsd/rfc3464-55.eml diff --git a/set-of-emails/maildir/bsd/lhost-gsuite-07.eml b/set-of-emails/maildir/bsd/rfc3464-56.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-gsuite-07.eml rename to set-of-emails/maildir/bsd/rfc3464-56.eml diff --git a/set-of-emails/maildir/bsd/lhost-gsuite-08.eml b/set-of-emails/maildir/bsd/rfc3464-57.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-gsuite-08.eml rename to set-of-emails/maildir/bsd/rfc3464-57.eml diff --git a/set-of-emails/maildir/bsd/lhost-gsuite-09.eml b/set-of-emails/maildir/bsd/rfc3464-58.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-gsuite-09.eml rename to set-of-emails/maildir/bsd/rfc3464-58.eml diff --git a/set-of-emails/maildir/bsd/lhost-gsuite-10.eml b/set-of-emails/maildir/bsd/rfc3464-59.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-gsuite-10.eml rename to set-of-emails/maildir/bsd/rfc3464-59.eml diff --git a/set-of-emails/maildir/bsd/lhost-gsuite-11.eml b/set-of-emails/maildir/bsd/rfc3464-60.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-gsuite-11.eml rename to set-of-emails/maildir/bsd/rfc3464-60.eml diff --git a/set-of-emails/maildir/bsd/lhost-gsuite-12.eml b/set-of-emails/maildir/bsd/rfc3464-61.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-gsuite-12.eml rename to set-of-emails/maildir/bsd/rfc3464-61.eml diff --git a/set-of-emails/maildir/bsd/lhost-gsuite-13.eml b/set-of-emails/maildir/bsd/rfc3464-62.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-gsuite-13.eml rename to set-of-emails/maildir/bsd/rfc3464-62.eml diff --git a/set-of-emails/maildir/bsd/lhost-gsuite-14.eml b/set-of-emails/maildir/bsd/rfc3464-63.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-gsuite-14.eml rename to set-of-emails/maildir/bsd/rfc3464-63.eml diff --git a/set-of-emails/maildir/bsd/lhost-gsuite-15.eml b/set-of-emails/maildir/bsd/rfc3464-64.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-gsuite-15.eml rename to set-of-emails/maildir/bsd/rfc3464-64.eml diff --git a/set-of-emails/maildir/bsd/lhost-gsuite-02.eml b/set-of-emails/maildir/bsd/rfc3464-65.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-gsuite-02.eml rename to set-of-emails/maildir/bsd/rfc3464-65.eml diff --git a/set-of-emails/maildir/bsd/lhost-aol-01.eml b/set-of-emails/maildir/bsd/rhost-aol-01.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-aol-01.eml rename to set-of-emails/maildir/bsd/rhost-aol-01.eml diff --git a/set-of-emails/maildir/bsd/lhost-aol-02.eml b/set-of-emails/maildir/bsd/rhost-aol-02.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-aol-02.eml rename to set-of-emails/maildir/bsd/rhost-aol-02.eml diff --git a/set-of-emails/maildir/bsd/lhost-aol-03.eml b/set-of-emails/maildir/bsd/rhost-aol-03.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-aol-03.eml rename to set-of-emails/maildir/bsd/rhost-aol-03.eml diff --git a/set-of-emails/maildir/bsd/lhost-aol-04.eml b/set-of-emails/maildir/bsd/rhost-aol-04.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-aol-04.eml rename to set-of-emails/maildir/bsd/rhost-aol-04.eml diff --git a/set-of-emails/maildir/bsd/lhost-aol-05.eml b/set-of-emails/maildir/bsd/rhost-aol-05.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-aol-05.eml rename to set-of-emails/maildir/bsd/rhost-aol-05.eml diff --git a/set-of-emails/maildir/bsd/lhost-aol-06.eml b/set-of-emails/maildir/bsd/rhost-aol-06.eml similarity index 59% rename from set-of-emails/maildir/bsd/lhost-aol-06.eml rename to set-of-emails/maildir/bsd/rhost-aol-06.eml index 630b69b1e..c15951689 100644 --- a/set-of-emails/maildir/bsd/lhost-aol-06.eml +++ b/set-of-emails/maildir/bsd/rhost-aol-06.eml @@ -1,105 +1,105 @@ -Return-Path: -Received: from imb-a04e.mx.aol.example.com (imb-a04.mx.aol.example.com [10.72.101.105]) - (using TLSv1 with cipher ADH-AES256-SHA (256/256 bits)) - (No client certificate requested) - by mtaiw-mcb08.mx.aol.example.com (Internet Inbound) with ESMTPS id C062370000082 - for ; Tue, 2 May 2017 01:16:59 -0400 (EDT) -Received: from omr-a015e.mx.aol.example.com (omr-a015.mx.aol.example.com [10.72.105.232]) - (using TLSv1 with cipher ADH-AES256-SHA (256/256 bits)) - (No client certificate requested) - by imb-a04e.mx.aol.example.com (AOL Mail Bouncer) with ESMTPS id 9E9973800243 - for ; Tue, 2 May 2017 01:16:59 -0400 (EDT) -Received: by omr-a015e.mx.aol.example.com (Outbound Mail Relay) - id 94251380008C; Tue, 2 May 2017 01:16:59 -0400 (EDT) -Date: Tue, 2 May 2017 01:16:59 -0400 (EDT) -From: MAILER-DAEMON@AOL.com (Mail Delivery System) -Subject: Undelivered Mail Returned to Sender -To: kijitora@aol.example.com -Auto-Submitted: auto-replied -MIME-Version: 1.0 -Content-Type: multipart/report; report-type=delivery-status; - boundary="AFC863800087.1493702219/omr-a015e.mx.aol.example.com" -Message-Id: <20170502051659.94251380008C@omr-a015e.mx.aol.example.com> -DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=mx.aol.example.com; - s=20150623; t=1493702219; - bh=iPCbHJ2jPQpy7gdsZmBye4j9C8lNj9z8i6fekcaNeSs=; - h=From:To:Subject:Message-Id:Date:MIME-Version:Content-Type; - b=nGg54EFB072k0xjb0IUv+cyynAeeER49V4O2xUQ6Epc4q4/Ijb/SrBNmkQ1BUhP8L - o9XWFk/xjNbZA1AL1ITG0kyn9mxw8b/0xsu9gUBkT4WIw5+skD192HGAdl/N7wGqjG - fQOQNt70ifmU/u+dqjqlvXgnagttNYGuAevD/FM8= -x-aol-global-disposition: G -x-aol-sid: 3039ac1a32a55908164b3e26 -X-AOL-IP: 10.72.101.105 - -This is a MIME-encapsulated message. - ---AFC863800087.1493702219/omr-a015e.mx.aol.example.com -Content-Description: Notification -Content-Type: text/plain; charset=us-ascii - -This is the mail system at host omr-a015e.mx.aol.example.com. - -I'm sorry to have to inform you that your message could not -be delivered to one or more recipients. It's attached below. - -For further assistance, please send mail to postmaster. - -If you do so, please include this problem report. You can -delete your own text from the attached returned message. - - The mail system - -: Name service error for name=libsisimai.org type=MX: - Malformed or unexpected name server reply - ---AFC863800087.1493702219/omr-a015e.mx.aol.example.com -Content-Description: Delivery report -Content-Type: message/delivery-status - -Reporting-MTA: dns; omr-a015e.mx.aol.example.com -X-Outbound-Mail-Relay-Queue-ID: AFC863800087 -X-Outbound-Mail-Relay-Sender: rfc822; kijitora@aol.example.com -Arrival-Date: Tue, 2 May 2017 01:16:58 -0400 (EDT) - -Final-Recipient: rfc822; neko@libsisimai.org -Original-Recipient: rfc822;neko@libsisimai.org -Action: failed -Status: 5.4.4 -Diagnostic-Code: X-Outbound-Mail-Relay; Name service error for - name=libsisimai.org type=MX: Malformed or unexpected name server reply - ---AFC863800087.1493702219/omr-a015e.mx.aol.example.com -Content-Description: Undelivered Message Headers -Content-Type: text/rfc822-headers - -Return-Path: -Received: from mtaomg-mcb01.mx.aol.example.com (mtaomg-mcb01.mx.aol.example.com [172.26.50.175]) - by omr-a015e.mx.aol.example.com (Outbound Mail Relay) with ESMTP id AFC863800087 - for ; Tue, 2 May 2017 01:16:58 -0400 (EDT) -Received: from core-aaa03a.mail.aol.example.com (core-aaa03.mail.aol.example.com [172.26.125.3]) - by mtaomg-mcb01.mx.aol.example.com (OMAG/Core Interface) with ESMTP id 5CB5338000082 - for ; Tue, 2 May 2017 01:16:58 -0400 (EDT) -Received: from 153.156.254.67 by webprd-m12.mail.aol.example.com (10.74.58.176) with HTTP (WebMailUI); Tue, 02 May 2017 01:16:58 -0400 -Date: Tue, 2 May 2017 01:16:58 -0400 -From: kijitora -To: neko@libsisimai.org -Message-Id: <15bc797120b-5852-14fd1@webprd-m12.mail.aol.example.com> -Subject: Nyaan -MIME-Version: 1.0 -Content-Type: text/plain; charset=utf-8 -Content-Transfer-Encoding: 7bit -X-MB-Message-Source: WebUI -X-MB-Message-Type: User -X-Mailer: JAS STD -X-Originating-IP: [153.156.254.67] -x-aol-global-disposition: G -DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=mx.aol.example.com; - s=20150623; t=1493702218; - bh=ZIzoLxkO6DTsSDI/+AFKxpWxCMR2TOMCWjxgPIUzUQY=; - h=From:To:Subject:Message-Id:Date:MIME-Version:Content-Type; - b=aHrZB9S3AdWEXTXDeTptKwUjqWZnzEN6La3CE9itVcOLt5K1Naxas3k5Y4iGtxeeG - KwedPk81ZD42YU3BHg5pkh6tTOd8sQ4ciHN3d2I/OslKr3ML3QwokPBmWhB9205Xj2 - t3qAf/ES13UiIPv7iqLRozzM2jE9XT4iY1bd6Sik= -x-aol-sid: 3039ac1a32af5908164a0262 - ---AFC863800087.1493702219/omr-a015e.mx.aol.example.com-- +Return-Path: +Received: from imb-a04e.mx.aol.com (imb-a04.mx.aol.com [10.72.101.105]) + (using TLSv1 with cipher ADH-AES256-SHA (256/256 bits)) + (No client certificate requested) + by mtaiw-mcb08.mx.aol.com (Internet Inbound) with ESMTPS id C062370000082 + for ; Tue, 2 May 2017 01:16:59 -0400 (EDT) +Received: from omr-a015e.mx.aol.com (omr-a015.mx.aol.com [10.72.105.232]) + (using TLSv1 with cipher ADH-AES256-SHA (256/256 bits)) + (No client certificate requested) + by imb-a04e.mx.aol.com (AOL Mail Bouncer) with ESMTPS id 9E9973800243 + for ; Tue, 2 May 2017 01:16:59 -0400 (EDT) +Received: by omr-a015e.mx.aol.com (Outbound Mail Relay) + id 94251380008C; Tue, 2 May 2017 01:16:59 -0400 (EDT) +Date: Tue, 2 May 2017 01:16:59 -0400 (EDT) +From: MAILER-DAEMON@AOL.com (Mail Delivery System) +Subject: Undelivered Mail Returned to Sender +To: kijitora@aol.com +Auto-Submitted: auto-replied +MIME-Version: 1.0 +Content-Type: multipart/report; report-type=delivery-status; + boundary="AFC863800087.1493702219/omr-a015e.mx.aol.com" +Message-Id: <20170502051659.94251380008C@omr-a015e.mx.aol.com> +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=mx.aol.com; + s=20150623; t=1493702219; + bh=iPCbHJ2jPQpy7gdsZmBye4j9C8lNj9z8i6fekcaNeSs=; + h=From:To:Subject:Message-Id:Date:MIME-Version:Content-Type; + b=nGg54EFB072k0xjb0IUv+cyynAeeER49V4O2xUQ6Epc4q4/Ijb/SrBNmkQ1BUhP8L + o9XWFk/xjNbZA1AL1ITG0kyn9mxw8b/0xsu9gUBkT4WIw5+skD192HGAdl/N7wGqjG + fQOQNt70ifmU/u+dqjqlvXgnagttNYGuAevD/FM8= +x-aol-global-disposition: G +x-aol-sid: 3039ac1a32a55908164b3e26 +X-AOL-IP: 10.72.101.105 + +This is a MIME-encapsulated message. + +--AFC863800087.1493702219/omr-a015e.mx.aol.com +Content-Description: Notification +Content-Type: text/plain; charset=us-ascii + +This is the mail system at host omr-a015e.mx.aol.com. + +I'm sorry to have to inform you that your message could not +be delivered to one or more recipients. It's attached below. + +For further assistance, please send mail to postmaster. + +If you do so, please include this problem report. You can +delete your own text from the attached returned message. + + The mail system + +: Name service error for name=libsisimai.org type=MX: + Malformed or unexpected name server reply + +--AFC863800087.1493702219/omr-a015e.mx.aol.com +Content-Description: Delivery report +Content-Type: message/delivery-status + +Reporting-MTA: dns; omr-a015e.mx.aol.com +X-Outbound-Mail-Relay-Queue-ID: AFC863800087 +X-Outbound-Mail-Relay-Sender: rfc822; kijitora@aol.com +Arrival-Date: Tue, 2 May 2017 01:16:58 -0400 (EDT) + +Final-Recipient: rfc822; neko@libsisimai.org +Original-Recipient: rfc822;neko@libsisimai.org +Action: failed +Status: 5.4.4 +Diagnostic-Code: X-Outbound-Mail-Relay; Name service error for + name=libsisimai.org type=MX: Malformed or unexpected name server reply + +--AFC863800087.1493702219/omr-a015e.mx.aol.com +Content-Description: Undelivered Message Headers +Content-Type: text/rfc822-headers + +Return-Path: +Received: from mtaomg-mcb01.mx.aol.com (mtaomg-mcb01.mx.aol.com [172.26.50.175]) + by omr-a015e.mx.aol.com (Outbound Mail Relay) with ESMTP id AFC863800087 + for ; Tue, 2 May 2017 01:16:58 -0400 (EDT) +Received: from core-aaa03a.mail.aol.com (core-aaa03.mail.aol.com [172.26.125.3]) + by mtaomg-mcb01.mx.aol.com (OMAG/Core Interface) with ESMTP id 5CB5338000082 + for ; Tue, 2 May 2017 01:16:58 -0400 (EDT) +Received: from 153.156.254.67 by webprd-m12.mail.aol.com (10.74.58.176) with HTTP (WebMailUI); Tue, 02 May 2017 01:16:58 -0400 +Date: Tue, 2 May 2017 01:16:58 -0400 +From: kijitora +To: neko@libsisimai.org +Message-Id: <15bc797120b-5852-14fd1@webprd-m12.mail.aol.com> +Subject: Nyaan +MIME-Version: 1.0 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 7bit +X-MB-Message-Source: WebUI +X-MB-Message-Type: User +X-Mailer: JAS STD +X-Originating-IP: [153.156.254.67] +x-aol-global-disposition: G +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=mx.aol.com; + s=20150623; t=1493702218; + bh=ZIzoLxkO6DTsSDI/+AFKxpWxCMR2TOMCWjxgPIUzUQY=; + h=From:To:Subject:Message-Id:Date:MIME-Version:Content-Type; + b=aHrZB9S3AdWEXTXDeTptKwUjqWZnzEN6La3CE9itVcOLt5K1Naxas3k5Y4iGtxeeG + KwedPk81ZD42YU3BHg5pkh6tTOd8sQ4ciHN3d2I/OslKr3ML3QwokPBmWhB9205Xj2 + t3qAf/ES13UiIPv7iqLRozzM2jE9XT4iY1bd6Sik= +x-aol-sid: 3039ac1a32af5908164a0262 + +--AFC863800087.1493702219/omr-a015e.mx.aol.com-- diff --git a/set-of-emails/maildir/bsd/lhost-facebook-03.eml b/set-of-emails/maildir/bsd/rhost-facebook-03.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-facebook-03.eml rename to set-of-emails/maildir/bsd/rhost-facebook-03.eml diff --git a/set-of-emails/maildir/bsd/lhost-facebook-04.eml b/set-of-emails/maildir/bsd/rhost-facebook-04.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-facebook-04.eml rename to set-of-emails/maildir/bsd/rhost-facebook-04.eml diff --git a/set-of-emails/maildir/bsd/lhost-messagelabs-01.eml b/set-of-emails/maildir/bsd/rhost-messagelabs-01.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-messagelabs-01.eml rename to set-of-emails/maildir/bsd/rhost-messagelabs-01.eml diff --git a/set-of-emails/maildir/bsd/lhost-messagelabs-02.eml b/set-of-emails/maildir/bsd/rhost-messagelabs-02.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-messagelabs-02.eml rename to set-of-emails/maildir/bsd/rhost-messagelabs-02.eml diff --git a/set-of-emails/maildir/bsd/lhost-messagelabs-03.eml b/set-of-emails/maildir/bsd/rhost-messagelabs-03.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-messagelabs-03.eml rename to set-of-emails/maildir/bsd/rhost-messagelabs-03.eml diff --git a/set-of-emails/maildir/bsd/lhost-outlook-01.eml b/set-of-emails/maildir/bsd/rhost-outlook-01.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-outlook-01.eml rename to set-of-emails/maildir/bsd/rhost-outlook-01.eml diff --git a/set-of-emails/maildir/bsd/lhost-outlook-02.eml b/set-of-emails/maildir/bsd/rhost-outlook-02.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-outlook-02.eml rename to set-of-emails/maildir/bsd/rhost-outlook-02.eml diff --git a/set-of-emails/maildir/bsd/lhost-outlook-03.eml b/set-of-emails/maildir/bsd/rhost-outlook-03.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-outlook-03.eml rename to set-of-emails/maildir/bsd/rhost-outlook-03.eml diff --git a/set-of-emails/maildir/bsd/lhost-outlook-04.eml b/set-of-emails/maildir/bsd/rhost-outlook-04.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-outlook-04.eml rename to set-of-emails/maildir/bsd/rhost-outlook-04.eml diff --git a/set-of-emails/maildir/bsd/lhost-outlook-06.eml b/set-of-emails/maildir/bsd/rhost-outlook-06.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-outlook-06.eml rename to set-of-emails/maildir/bsd/rhost-outlook-06.eml diff --git a/set-of-emails/maildir/bsd/lhost-outlook-07.eml b/set-of-emails/maildir/bsd/rhost-outlook-07.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-outlook-07.eml rename to set-of-emails/maildir/bsd/rhost-outlook-07.eml diff --git a/set-of-emails/maildir/bsd/lhost-outlook-08.eml b/set-of-emails/maildir/bsd/rhost-outlook-08.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-outlook-08.eml rename to set-of-emails/maildir/bsd/rhost-outlook-08.eml diff --git a/set-of-emails/maildir/bsd/lhost-outlook-09.eml b/set-of-emails/maildir/bsd/rhost-outlook-09.eml similarity index 100% rename from set-of-emails/maildir/bsd/lhost-outlook-09.eml rename to set-of-emails/maildir/bsd/rhost-outlook-09.eml diff --git a/set-of-emails/maildir/bsd/arf-22.eml b/set-of-emails/maildir/tmp/arf-22.eml similarity index 100% rename from set-of-emails/maildir/bsd/arf-22.eml rename to set-of-emails/maildir/tmp/arf-22.eml diff --git a/set-of-emails/maildir/bsd/arf-23.eml b/set-of-emails/maildir/tmp/arf-23.eml similarity index 100% rename from set-of-emails/maildir/bsd/arf-23.eml rename to set-of-emails/maildir/tmp/arf-23.eml diff --git a/set-of-emails/maildir/bsd/arf-24.eml b/set-of-emails/maildir/tmp/arf-24.eml similarity index 100% rename from set-of-emails/maildir/bsd/arf-24.eml rename to set-of-emails/maildir/tmp/arf-24.eml diff --git a/set-of-emails/maildir/bsd/rfc3464-37.eml b/set-of-emails/maildir/tmp/rfc3464-37.eml similarity index 100% rename from set-of-emails/maildir/bsd/rfc3464-37.eml rename to set-of-emails/maildir/tmp/rfc3464-37.eml diff --git a/set-of-emails/maildir/bsd/rfc3464-38.eml b/set-of-emails/maildir/tmp/rfc3464-38.eml similarity index 100% rename from set-of-emails/maildir/bsd/rfc3464-38.eml rename to set-of-emails/maildir/tmp/rfc3464-38.eml diff --git a/set-of-emails/maildir/bsd/rfc3464-39.eml b/set-of-emails/maildir/tmp/rfc3464-39.eml similarity index 100% rename from set-of-emails/maildir/bsd/rfc3464-39.eml rename to set-of-emails/maildir/tmp/rfc3464-39.eml diff --git a/t/022-mail-maildir.t b/t/022-mail-maildir.t index 90d0288e4..aba050350 100644 --- a/t/022-mail-maildir.t +++ b/t/022-mail-maildir.t @@ -8,7 +8,7 @@ my $Methods = { 'class' => ['new'], 'object' => ['path', 'dir', 'file', 'size', 'offset', 'handle', 'read'], }; -my $MaildirSize = 589; +my $MaildirSize = 583; my $SampleEmail = './set-of-emails/maildir/bsd'; my $NewInstance = $Package->new($SampleEmail); diff --git a/t/032-rfc1894.t b/t/032-rfc1894.t index 06eb57ef7..d157de3f0 100644 --- a/t/032-rfc1894.t +++ b/t/032-rfc1894.t @@ -28,6 +28,10 @@ MAKETEST: { 'Last-Attempt-Date: Sat, 9 Jun 2018 03:06:57 +0900 (JST)', 'Diagnostic-Code: SMTP; Unknown user neko@nyaan.jp', ]; + my $RFC1894Field3 = [ + 'Status: 5.1.1 (user unknown)', + 'Reporting-MTA: dns; mr21p30im-asmtp004.me.example.com (tcp-daemon)', + ]; my $IsNotDSNField = [ 'Content-Type: message/delivery-status', 'Subject: Returned mail: see transcript for details', @@ -66,7 +70,7 @@ MAKETEST: { } for my $e ( @$RFC1894Field2 ) { - is $Package->match($e), 1, '->match('.$e.') returns 1'; + is $Package->match($e), 2, '->match('.$e.') returns 1'; $v = $Package->field($e); isa_ok $v, 'ARRAY', '->field('.$e.') returns Array'; @@ -82,6 +86,16 @@ MAKETEST: { is $q, $v->[0], '->label returns '.$q; } + for my $e ( @$RFC1894Field3 ) { + ok $Package->match($e) > 0, '->match('.$e.') returns 1 or 2'; + + $v = $Package->field($e); + isa_ok $v, 'ARRAY', '->field('.$e.') returns Array'; + is scalar(@$v), 5, '->field('.$e.') returns 5 elements'; + ok length $v->[4], 'v[4] = '.$v->[4]; + unlike $v->[4], qr/[()]/, 'v[4] does not include ( and )'; + } + for my $e ( @$IsNotDSNField ) { is $Package->match($e), 0, '->match('.$e.') returns 0'; @@ -95,4 +109,3 @@ MAKETEST: { done_testing; - diff --git a/t/034-rfc1123.t b/t/034-rfc1123.t index ec8173b51..72febb9f3 100644 --- a/t/034-rfc1123.t +++ b/t/034-rfc1123.t @@ -4,7 +4,7 @@ use lib qw(./lib ./blib/lib); use Sisimai::RFC1123; my $Package = 'Sisimai::RFC1123'; -my $Methods = { 'class' => ['is_validhostname'], 'object' => [] }; +my $Methods = { 'class' => ['is_internethost', 'find'], 'object' => [] }; use_ok $Package; can_ok $Package, @{ $Methods->{'class'} }; @@ -28,15 +28,39 @@ MAKETEST: { 'mx1.example.jp.', 'a.jp', ]; + my $serversaid = [ + ': host neko.example.jp[192.0.2.2] said: 550 5.7.1 This message was not accepted due to domain (libsisimai.org) owner DMARC policy', + 'neko.example.jp[192.0.2.232]: server refused to talk to me: 421 Service not available, closing transmission channel', + '... while talking to neko.example.jp.: <<< 554 neko.example.jp ESMTP not accepting connections', + 'host neko.example.jp [192.0.2.222]: 500 Line limit exceeded', + 'Google tried to deliver your message, but it was rejected by the server for the recipient domain nyaan.jp by neko.example.jp. [192.0.2.2].', + 'Delivery failed for the following reason: Server neko.example.jp[192.0.2.222] failed with: 550 No such user here', + 'Remote system: dns;neko.example.jp (TCP|17.111.174.65|48044|192.0.2.225|25) (neko.example.jp ESMTP SENDMAIL-VM)', + 'SMTP Server rejected recipient (Error following RCPT command). It responded as follows: [550 5.1.1 User unknown]', + 'Reporting-MTA: ', + 'cat@example.jp:000000: : 192.0.2.250 : neko.example.jp:[192.0.2.153] : 550 5.1.1 ... User Unknown in RCPT TO', + 'Generating server: neko.example.jp', + 'Server di generazione: neko.example.jp', + 'Serveur de génération : neko.example.jp', + 'Genererande server: neko.example.jp', + 'neko.example.jp [192.0.2.25] did not like our RCPT TO: 550 5.1.1 : Recipient address rejected: User unknown', + 'neko.example.jp [192.0.2.79] did not like our final DATA: 554 5.7.9 Message not accepted for policy reasons', + ]; for my $e ( @$hostnames0 ) { # Invalid hostnames - is $Package->is_validhostname($e), 0, '->is_validhostname('.$e.') = 0'; + is $Package->is_internethost($e), 0, '->is_internethost('.$e.') = 0'; } for my $e ( @$hostnames1 ) { # Valid hostnames - is $Package->is_validhostname($e), 1, '->is_validhostname('.$e.') = 1'; + is $Package->is_internethost($e), 1, '->is_internethost('.$e.') = 1'; + } + + for my $e ( @$serversaid ) { + # find() returns "neko.example.jp" + my $v = $Package->find($e); + is $v, "neko.example.jp", '->find('.$e.') = '.$v; } } diff --git a/t/050-lhost.t b/t/050-lhost.t index f1323b197..e8ae74bf2 100644 --- a/t/050-lhost.t +++ b/t/050-lhost.t @@ -23,7 +23,7 @@ MAKETEST: { ok scalar keys %{ $Package->path }; isa_ok $Package->DELIVERYSTATUS, 'HASH'; - is scalar(keys %{ $Package->DELIVERYSTATUS }), 15; + is scalar(keys %{ $Package->DELIVERYSTATUS }), 14; isa_ok $Package->INDICATORS, 'HASH'; is scalar(keys %{ $Package->INDICATORS }), 2; diff --git a/t/070-lda.t b/t/070-lda.t new file mode 100644 index 000000000..9a003e0af --- /dev/null +++ b/t/070-lda.t @@ -0,0 +1,46 @@ +use strict; +use Test::More; +use lib qw(./lib ./blib/lib); +use Sisimai::LDA; + +my $Package = 'Sisimai::LDA'; +my $Methods = { 'class' => ['find'], 'object' => [] }; + +use_ok $Package; +can_ok $Package, @{ $Methods->{'class'} }; + +MAKETEST: { + use Sisimai::Mail; + use Sisimai::Message; + + my $EmailFiles = { + "rfc3464-01" => "mailboxfull", + "rfc3464-04" => "systemerror", + "rfc3464-06" => "userunknown", + "lhost-postfix-01" => "mailererror", + "lhost-qmail-10" => "suspend", + }; + for my $e ( keys %$EmailFiles ) { + my $mailbox = Sisimai::Mail->new(sprintf("./set-of-emails/maildir/bsd/%s.eml", $e)); + my $counter = 0; + + while( my $r = $mailbox->data->read ) { + my $message = Sisimai::Message->rise({ 'data' => $r }); + $counter++; + isa_ok $message, "HASH"; + isa_ok $message->{"ds"}, "ARRAY"; + + for my $f ( $message->{'ds'}->@* ) { + my $factobj = { + "diagnosticcode" => $f->{'diagnosis'} || "", + "smtpcommand" => $f->{'command'} || "", + }; + my $v = Sisimai::LDA->find($factobj); + is $v, $EmailFiles->{ $e }, sprintf("%s [%02d] Sisimai::LDA->find() = %s", $e, $counter, $v); + } + } + } + is $Package->find(undef), undef; +} + +done_testing; diff --git a/t/070-mda.t b/t/070-mda.t deleted file mode 100644 index f710c72e1..000000000 --- a/t/070-mda.t +++ /dev/null @@ -1,51 +0,0 @@ -use strict; -use Test::More; -use lib qw(./lib ./blib/lib); -use Sisimai::MDA; - -my $Package = 'Sisimai::MDA'; -my $Methods = { 'class' => ['inquire'], 'object' => [] }; - -use_ok $Package; -can_ok $Package, @{ $Methods->{'class'} }; - -MAKETEST: { - use Sisimai::Mail; - use Sisimai::Message; - - my $EmailFiles = [qw|rfc3464-01.eml rfc3464-04.eml rfc3464-06.eml lhost-sendmail-13.eml lhost-qmail-10.eml|]; - my $ErrorMesgs = [ - 'Your message to neko was automatically rejected:'."\n".'Not enough disk space', - 'mail.local: Disc quota exceeded', - 'procmail: Quota exceeded while writing', - 'maildrop: maildir over quota.', - 'vdelivermail: user is over quota', - 'vdeliver: Delivery failed due to system quota violation', - ]; - - for my $e ( @$EmailFiles ) { - my $emailfn = sprintf("./set-of-emails/maildir/bsd/%s", $e); - my $mailbox = Sisimai::Mail->new($emailfn); - my $message = undef; - my $headers = {}; - - is $Package->inquire(undef), undef; - is $Package->inquire({}, undef), undef; - - while( my $r = $mailbox->data->read ) { - $message = Sisimai::Message->rise({ 'data' => $r }); - $headers->{'from'} = $message->{'from'}; - - for my $e ( @$ErrorMesgs ) { - my $v = Sisimai::MDA->inquire($headers, \$e); - - isa_ok $v, 'HASH'; - ok $v->{'mda'}, 'mda => '.$v->{'mda'}; - is $v->{'reason'}, 'mailboxfull', 'reason => '.$v->{'reason'}; - ok $v->{'message'}, 'message => '.$v->{'message'}; - } - } - } -} - -done_testing; diff --git a/t/200-reason.t b/t/200-reason.t index c3493f09c..f7c32c6e1 100644 --- a/t/200-reason.t +++ b/t/200-reason.t @@ -5,7 +5,7 @@ use Sisimai; use Sisimai::Reason; my $Package = 'Sisimai::Reason'; -my $Methods = { 'class' => ['find', 'path', 'retry', 'index', 'match'], 'object' => [] }; +my $Methods = { 'class' => ['find', 'path', 'retry', 'index', 'match', 'is_explicit'], 'object' => [] }; my $Message = [ 'smtp; 550 5.1.1 ... User Unknown', 'smtp; 550 Unknown user kijitora@example.jp', @@ -156,6 +156,19 @@ MAKETEST: { is(Sisimai::Reason->match(undef), undef); is(Sisimai::Reason->match('X-Unix; 77'), 'mailererror'); } + + EXPLICIT: { + for my $e ( $Package->index()->@* ) { + my $r = lc $e; + my $v = $Package->is_explicit($r); + + if( $r eq "undefined" || $r eq "onhold" ) { + is $v, 0, sprintf("%s is not a explicit reason", $e); + } else { + is $v, 1, sprintf("%s is a explicit reason", $e); + } + } + } } done_testing; diff --git a/t/201-reason-children.t b/t/201-reason-children.t index a089fae6d..c9d5d3c49 100644 --- a/t/201-reason-children.t +++ b/t/201-reason-children.t @@ -29,6 +29,7 @@ my $ReasonChildren = { 'SecurityError' => ['570 5.7.0 Authentication failure'], 'SpamDetected' => ['570 5.7.7 Spam Detected'], 'Speeding' => ['451 4.7.1 : Client host rejected: Please try again slower'], +# 'Suppressed' => ['There is no sample email which is returned due to being listed in the suppression list'], 'Suspend' => ['550 5.0.0 Recipient suspend the service'], 'SystemError' => ['500 5.3.5 System config error'], 'SystemFull' => ['550 5.0.0 Mail system full'], diff --git a/t/300-rhost.t b/t/300-rhost.t index a7a6b71f1..b90311ca2 100644 --- a/t/300-rhost.t +++ b/t/300-rhost.t @@ -9,7 +9,8 @@ use Module::Load; my $Package = 'Sisimai::Rhost'; my $Methods = { 'class' => ['find'], 'object' => [] }; my $Classes = [qw| - Apple Cox FrancePTT GoDaddy Google IUA KDDI Microsoft Mimecast NTTDOCOMO Spectrum Tencent YahooInc + Aol Apple Cox Facebook FrancePTT GSuite GoDaddy Google IUA KDDI MessageLabs Microsoft Mimecast + NTTDOCOMO Outlook Spectrum Tencent YahooInc |]; MAKETEST: { @@ -26,10 +27,7 @@ MAKETEST: { isa_ok $f, 'Sisimai::Fact'; ok length $f->rhost, '->rhost = '.$f->rhost; ok length $f->reason, '->reason = '.$f->reason; - - my $cx = $f->damn; - ok length $cx->{'destination'}; - is $Package->find($cx, $cx->{'destination'}), $f->reason, sprintf("->damn->reason = %s", $f->reason); + ok length $f->destination, '->destination = '.$f->destination; } } diff --git a/t/304-rhost-franceptt.t b/t/304-rhost-franceptt.t index 74e41efe8..2dcb66274 100644 --- a/t/304-rhost-franceptt.t +++ b/t/304-rhost-franceptt.t @@ -16,8 +16,8 @@ my $isexpected = { '06' => [['4.0.0', '', 'blocked', 0]], '07' => [['4.0.0', '421', 'blocked', 0]], '08' => [['4.2.0', '421', 'systemerror', 0]], - '10' => [['5.5.0', '550', 'undefined', 0]], - '11' => [['4.2.1', '421', 'undefined', 0]], + '10' => [['5.5.0', '550', 'blocked', 0]], + '11' => [['4.2.1', '421', 'blocked', 0]], '12' => [['5.7.1', '554', 'policyviolation', 0]], }; diff --git a/t/614-lhost-aol.t b/t/314-rhost-aol.t similarity index 100% rename from t/614-lhost-aol.t rename to t/314-rhost-aol.t diff --git a/t/660-lhost-facebook.t b/t/315-rhost-facebook.t similarity index 87% rename from t/660-lhost-facebook.t rename to t/315-rhost-facebook.t index 21c4ea70d..32f38aafc 100644 --- a/t/660-lhost-facebook.t +++ b/t/315-rhost-facebook.t @@ -8,7 +8,7 @@ my $enginename = 'Facebook'; my $enginetest = Sisimai::Lhost::Code->makeinquiry; my $isexpected = { # INDEX => [['D.S.N.', 'replycode', 'REASON', 'hardbounce'], [...]] - '03' => [['5.1.1', '550', 'filtered', 0]], + '03' => [['5.1.1', '550', 'userunknown', 1]], '04' => [['5.1.1', '550', 'userunknown', 1]], }; diff --git a/t/734-lhost-messagelabs.t b/t/317-rhost-messagelabs.t similarity index 100% rename from t/734-lhost-messagelabs.t rename to t/317-rhost-messagelabs.t diff --git a/t/752-lhost-outlook.t b/t/318-rhost-outlook.t similarity index 100% rename from t/752-lhost-outlook.t rename to t/318-rhost-outlook.t diff --git a/t/500-fact.t b/t/500-fact.t index 2e502b7be..773423940 100644 --- a/t/500-fact.t +++ b/t/500-fact.t @@ -28,7 +28,7 @@ my $Results = { ['5.1.1', '550', 'userunknown', 1], ['5.1.1', '550', 'userunknown', 1], ['5.1.1', '550', 'userunknown', 1], - ['5.1.0', '550', 'userunknown', 1], + ['5.1.1', '550', 'userunknown', 1], ['5.1.1', '550', 'userunknown', 1], ['5.0.0', '554', 'filtered', 0], ['5.1.1', '550', 'userunknown', 1], diff --git a/t/600-lhost-code b/t/600-lhost-code index 70b482ffa..2047d43d0 100644 --- a/t/600-lhost-code +++ b/t/600-lhost-code @@ -26,6 +26,15 @@ my $moduletest = sub { my $lhostindex = Sisimai::Lhost->index; push @$lhostindex, 'ARF', 'RFC3464', 'RFC3834'; my $isnotlhost = qr/\A(?:ARF|RFC3464|RFC3834)\z/; my $methodlist = ['DELIVERYSTATUS', 'INDICATORS', 'description', 'index', 'path']; + my $alternates = { + 'Exchange2007' => ['Office365'], + 'Exim' => ['MailRu', 'MXLogic'], + 'qmail' => ['X4', 'Yahoo'], + 'RFC3464' => [qw| + Aol Amavis AmazonWorkMail Barracuda Bigfoot Facebook GSuite McAfee MessageLabs Outlook + PowerMTA ReceivingSES SendGrid SurfControl Yandex X5 + |], + }; my $skiptonext = { 'public' => ['lhost-postfix-49', 'lhost-postfix-50'], 'private' => [ @@ -46,11 +55,29 @@ my $moduletest = sub { $M = sprintf("Sisimai::%s", $E); } else { - # Sisimai::Lhost OR Sisimai::Rhost - my $c = [caller()]->[1]; - my $h = $c =~ /-rhost-/ ? 'rhost' : 'lhost'; - $M = sprintf("Sisimai::%s::%s", ucfirst $h, $E); - $nameprefix = $h.'-'; + my $calledfrom = [caller()]->[1]; + my $kindofhost = $calledfrom =~ /-rhost-/ ? 'rhost' : 'lhost'; + if( grep { $E eq $_ } $alternates->{'RFC3464'}->@* ) { + # Removed Lhost moudles (Sisimai::RFC3464 can decode) + $M = "Sisimai::RFC3464"; + + } elsif( grep { $E eq $_ } $alternates->{'Exchange2007'}->@* ) { + # Removed Lhost moudles (Sisimai::Lhost::Exchange2007 can decode) + $M = "Sisimai::Lhost::Exchange2007"; + + } elsif( grep { $E eq $_ } $alternates->{'Exim'}->@* ) { + # Removed Lhost moudles (Sisimai::Lhost::Exim can decode) + $M = "Sisimai::Lhost::Exim"; + + } elsif( grep { $E eq $_ } $alternates->{'qmail'}->@* ) { + # Removed Lhost moudles (Sisimai::Lhost::qmail can decode) + $M = "Sisimai::Lhost::qmail"; + + } else { + # Sisimai::Lhost OR Sisimai::Rhost + $M = sprintf("Sisimai::%s::%s", ucfirst $kindofhost, $E); + } + $nameprefix = $kindofhost.'-'; } my $samplepath = $privateset ? sprintf("set-of-emails/private/%s%s", $nameprefix, lc $E) : 'set-of-emails/maildir/bsd'; @@ -398,17 +425,35 @@ my $moduletest = sub { } else { # Sisimai::Lhost if( $E eq 'RFC3464' && $cv !~ /\ARFC3464/ ) { - # Parsed by Sisimai::MDA + # Parsed by Sisimai::LDA ok $cv =~ $cr, sprintf("%s %s", $ct, $cr); is scalar @{ [grep { $cv eq $_ } @$lhostindex] }, 0, sprintf("%s %s", $ct, $cv); - } elsif( $E eq 'ARF' ) { - # Parsed by Sisimai::ARF - is $cv, 'Feedback-Loop', sprintf("%s %s", $ct, $cr); + } elsif( grep { $E eq $_ } $alternates->{'RFC3464'}->@* ) { + # Decoded by Sisimai::RFC3464 since v5.2.0 + is $cv, 'RFC3464', sprintf("%s %s", $ct, $cr); + + } elsif( grep { $E eq $_ } $alternates->{'Exchange2007'}->@* ) { + # Decoded by Sisimai::Lhost::Exchange2007 since v5.2.0 + is $cv, 'Exchange2007', sprintf("%s %s", $ct, $cr); + + } elsif( grep { $E eq $_ } $alternates->{'Exim'}->@* ) { + # Decoded by Sisimai::Lhost::Exim since v5.2.0 + is $cv, 'Exim', sprintf("%s %s", $ct, $cr); + + } elsif( grep { $E eq $_ } $alternates->{'qmail'}->@* ) { + # Decoded by Sisimai::Lhost::qmail since v5.2.0 + is $cv, 'qmail', sprintf("%s %s", $ct, $cr); } else { # Other MTA modules - is $cv, $E, sprintf("%s %s", $ct, $cr); + if( $E eq "AmazonSES" ) { + # lhost-amazonses-* are decoded by Sisimai::RFC3464 except 11-14 + like $cv, qr/(?:AmazonSES|RFC3464)/, sprintf("%s %s", $ct, $cr); + + } else { + is $cv, $E, sprintf("%s %s", $ct, $cr); + } } } } diff --git a/t/652-lhost-exchange2007.t b/t/652-lhost-exchange2007.t index 3ecf80485..d8863e50d 100644 --- a/t/652-lhost-exchange2007.t +++ b/t/652-lhost-exchange2007.t @@ -10,7 +10,7 @@ my $isexpected = { # INDEX => [['D.S.N.', 'replycode', 'REASON', 'hardbounce'], [...]] '01' => [['5.1.1', '550', 'userunknown', 1]], '02' => [['5.2.2', '550', 'mailboxfull', 0]], - '03' => [['5.2.3', '550', 'mesgtoobig', 0]], + '03' => [['5.2.3', '550', 'exceedlimit', 0]], '04' => [['5.7.1', '550', 'securityerror', 0]], '05' => [['4.4.1', '', 'expired', 0]], '06' => [['5.1.1', '550', 'userunknown', 1]], diff --git a/t/654-lhost-ezweb.t b/t/654-lhost-ezweb.t index 209065cce..85eb9bd4e 100644 --- a/t/654-lhost-ezweb.t +++ b/t/654-lhost-ezweb.t @@ -9,7 +9,7 @@ my $enginetest = Sisimai::Lhost::Code->makeinquiry; my $isexpected = { # INDEX => [['D.S.N.', 'replycode', 'REASON', 'hardbounce'], [...]] '01' => [['5.0.910', '', 'filtered', 0]], - '02' => [['5.0.0', '', 'suspend', 0]], + '02' => [['5.0.0', '550', 'suspend', 0]], '03' => [['5.0.921', '', 'suspend', 0]], '04' => [['5.0.911', '550', 'userunknown', 1]], '05' => [['5.0.947', '', 'expired', 0]], diff --git a/t/672-lhost-googlegroups.t b/t/672-lhost-googlegroups.t index 327b523ce..e7c8dea75 100644 --- a/t/672-lhost-googlegroups.t +++ b/t/672-lhost-googlegroups.t @@ -22,7 +22,6 @@ my $isexpected = { '12' => [['5.0.918', '', 'rejected', 0]], '13' => [['5.0.918', '', 'rejected', 0]], '14' => [['5.0.918', '', 'rejected', 0]], - '15' => [['5.0.918', '', 'rejected', 0]], }; $enginetest->($enginename, $isexpected); diff --git a/t/673-lhost-googleworkspace.t b/t/673-lhost-googleworkspace.t new file mode 100644 index 000000000..c92c5f2a7 --- /dev/null +++ b/t/673-lhost-googleworkspace.t @@ -0,0 +1,16 @@ +use strict; +use warnings; +use Test::More; +use lib qw(./lib ./blib/lib); +require './t/600-lhost-code'; + +my $enginename = 'GoogleWorkspace'; +my $enginetest = Sisimai::Lhost::Code->makeinquiry; +my $isexpected = { + # INDEX => [['D.S.N.', 'replycode', 'REASON', 'hardbounce'], [...]] + '01' => [['5.0.918', '', 'rejected', 0]], +}; + +$enginetest->($enginename, $isexpected); +done_testing; + diff --git a/t/673-lhost-gsuite.t b/t/673-lhost-gsuite.t deleted file mode 100644 index c0ec45bef..000000000 --- a/t/673-lhost-gsuite.t +++ /dev/null @@ -1,30 +0,0 @@ -use strict; -use warnings; -use Test::More; -use lib qw(./lib ./blib/lib); -require './t/600-lhost-code'; - -my $enginename = 'GSuite'; -my $enginetest = Sisimai::Lhost::Code->makeinquiry; -my $isexpected = { - # INDEX => [['D.S.N.', 'replycode', 'REASON', 'hardbounce'], [...]] - '01' => [['5.1.0', '550', 'userunknown', 1]], - '02' => [['5.0.0', '', 'userunknown', 1]], - '03' => [['4.0.0', '', 'notaccept', 0]], - '04' => [['4.0.0', '', 'networkerror', 0]], - '05' => [['4.0.0', '', 'networkerror', 0]], - '06' => [['4.4.1', '', 'expired', 0]], - '07' => [['4.4.1', '', 'expired', 0]], - '08' => [['5.0.0', '550', 'filtered', 0]], - '09' => [['5.0.0', '550', 'userunknown', 1]], - '10' => [['4.0.0', '', 'notaccept', 0]], - '11' => [['5.1.8', '501', 'rejected', 0]], - '12' => [['5.0.0', '', 'spamdetected', 0]], - '13' => [['4.0.0', '', 'networkerror', 0]], - '14' => [['5.1.1', '550', 'userunknown', 1]], - '15' => [['4.0.0', '', 'networkerror', 0]], -}; - -$enginetest->($enginename, $isexpected); -done_testing; - diff --git a/t/732-lhost-mailru.t b/t/732-lhost-mailru.t index c44d82ec9..9f17f8be3 100644 --- a/t/732-lhost-mailru.t +++ b/t/732-lhost-mailru.t @@ -18,7 +18,7 @@ my $isexpected = { '07' => [['5.0.910', '550', 'filtered', 0]], '08' => [['5.0.911', '550', 'userunknown', 1]], '09' => [['5.1.8', '501', 'rejected', 0]], - '10' => [['5.0.947', '', 'expired', 0]], + '10' => [['4.0.947', '', 'expired', 0]], }; $enginetest->($enginename, $isexpected); diff --git a/t/733-lhost-mcafee.t b/t/733-lhost-mcafee.t index b3511e2a7..6b3246dbc 100644 --- a/t/733-lhost-mcafee.t +++ b/t/733-lhost-mcafee.t @@ -8,11 +8,11 @@ my $enginename = 'McAfee'; my $enginetest = Sisimai::Lhost::Code->makeinquiry; my $isexpected = { # INDEX => [['D.S.N.', 'replycode', 'REASON', 'hardbounce'], [...]] - '01' => [['5.0.911', '550', 'userunknown', 1]], + '01' => [['5.0.910', '550', 'filtered', 0]], '02' => [['5.1.1', '550', 'userunknown', 1]], '03' => [['5.1.1', '550', 'userunknown', 1]], - '04' => [['5.0.911', '550', 'userunknown', 1]], - '05' => [['5.0.911', '550', 'userunknown', 1]], + '04' => [['5.0.910', '550', 'filtered', 0]], + '05' => [['5.0.910', '550', 'filtered', 0]], }; $enginetest->($enginename, $isexpected); diff --git a/t/750-lhost-office365.t b/t/750-lhost-office365.t index 7cd0fd382..53f7e8b61 100644 --- a/t/750-lhost-office365.t +++ b/t/750-lhost-office365.t @@ -15,7 +15,7 @@ my $isexpected = { '05' => [['5.1.8', '501', 'rejected', 0]], '06' => [['5.4.312', '550', 'networkerror', 0]], '07' => [['5.1.351', '550', 'userunknown', 1]], - '08' => [['5.4.316', '550', 'expired', 0]], + '08' => [['5.4.316', '550', 'networkerror', 0]], '09' => [['5.1.351', '550', 'userunknown', 1]], '10' => [['5.1.351', '550', 'userunknown', 1]], '11' => [['5.1.1', '550', 'userunknown', 1]], diff --git a/t/761-lhost-powermta.t b/t/761-lhost-powermta.t index fa0dc65d8..e8b8ce405 100644 --- a/t/761-lhost-powermta.t +++ b/t/761-lhost-powermta.t @@ -8,9 +8,9 @@ my $enginename = 'PowerMTA'; my $enginetest = Sisimai::Lhost::Code->makeinquiry; my $isexpected = { # INDEX => [['D.S.N.', 'replycode', 'REASON', 'hardbounce'], [...]] - '01' => [['5.2.1', '550', 'userunknown', 1]], + '01' => [['5.2.1', '550', 'suspend', 0]], '02' => [['5.0.0', '554', 'userunknown', 1]], - '03' => [['5.2.1', '550', 'userunknown', 1]], + '03' => [['5.2.1', '550', 'suspend', 0]], }; $enginetest->($enginename, $isexpected); diff --git a/t/780-lhost-receivingses.t b/t/780-lhost-receivingses.t index a0ee98901..53ca6658f 100644 --- a/t/780-lhost-receivingses.t +++ b/t/780-lhost-receivingses.t @@ -8,12 +8,12 @@ my $enginename = 'ReceivingSES'; my $enginetest = Sisimai::Lhost::Code->makeinquiry; my $isexpected = { # INDEX => [['D.S.N.', 'replycode', 'REASON', 'hardbounce'], [...]] - '01' => [['5.1.1', '550', 'filtered', 0]], - '02' => [['5.1.1', '550', 'filtered', 0]], + '01' => [['5.1.1', '550', 'userunknown', 1]], + '02' => [['5.1.1', '550', 'userunknown', 1]], '03' => [['4.0.0', '450', 'onhold', 0]], '04' => [['5.2.2', '552', 'mailboxfull', 0]], '05' => [['5.3.4', '552', 'mesgtoobig', 0]], - '06' => [['5.6.1', '500', 'contenterror', 0]], + '06' => [['5.6.1', '500', 'spamdetected', 0]], '07' => [['5.2.0', '550', 'filtered', 0]], '08' => [['5.2.3', '552', 'exceedlimit', 0]], }; diff --git a/t/850-lhost-yahoo.t b/t/850-lhost-yahoo.t index 373553610..2d4a2f3d6 100644 --- a/t/850-lhost-yahoo.t +++ b/t/850-lhost-yahoo.t @@ -20,7 +20,7 @@ my $isexpected = { '10' => [['5.1.1', '550', 'userunknown', 1]], '11' => [['5.1.8', '501', 'rejected', 0]], '12' => [['5.1.8', '501', 'rejected', 0]], - '13' => [['5.0.947', '', 'expired', 0]], + '13' => [['5.0.930', '', 'systemerror', 0]], '14' => [['5.0.971', '554', 'blocked', 0]], }; diff --git a/t/882-lhost-x2.t b/t/882-lhost-x2.t index a11489e45..8789e0ee8 100644 --- a/t/882-lhost-x2.t +++ b/t/882-lhost-x2.t @@ -15,6 +15,7 @@ my $isexpected = { '03' => [['5.0.947', '', 'expired', 0]], '04' => [['5.0.922', '', 'mailboxfull', 0]], '05' => [['4.1.9', '', 'expired', 0]], + '06' => [['4.4.1', '', 'expired', 0]], }; $enginetest->($enginename, $isexpected); diff --git a/t/884-lhost-x4.t b/t/884-lhost-x4.t index 5f196d755..ead9a71bc 100644 --- a/t/884-lhost-x4.t +++ b/t/884-lhost-x4.t @@ -9,7 +9,6 @@ my $enginetest = Sisimai::Lhost::Code->makeinquiry; my $isexpected = { # INDEX => [['D.S.N.', 'replycode', 'REASON', 'hardbounce'], [...]] '01' => [['5.0.922', '', 'mailboxfull', 0]], - '08' => [['4.4.1', '', 'networkerror', 0]], }; $enginetest->($enginename, $isexpected); diff --git a/t/890-rfc3464.t b/t/890-rfc3464.t index 910ef6055..005ed8a84 100644 --- a/t/890-rfc3464.t +++ b/t/890-rfc3464.t @@ -8,10 +8,10 @@ my $enginename = 'RFC3464'; my $enginetest = Sisimai::Lhost::Code->makeinquiry; my $isexpected = { # INDEX => [['D.S.N.', 'replycode', 'REASON', 'hardbounce'], [...]] - '01' => [['5.1.1', '', 'mailboxfull', 0]], + '01' => [['5.1.1', '550', 'mailboxfull', 0]], '03' => [['5.0.0', '554', 'policyviolation', 0]], - '04' => [['5.5.0', '554', 'mailererror', 0]], - '06' => [['5.5.0', '', 'userunknown', 1]], + '04' => [['5.5.0', '554', 'systemerror', 0]], + '06' => [['5.5.0', '554', 'userunknown', 1]], '07' => [['4.4.0', '', 'expired', 0]], '08' => [['5.7.1', '550', 'spamdetected', 0]], '09' => [['4.3.0', '', 'mailboxfull', 0]], @@ -22,15 +22,27 @@ my $isexpected = { '29' => [['5.5.0', '503', 'syntaxerror', 0]], '34' => [['4.4.1', '', 'networkerror', 0]], '35' => [['5.0.0', '550', 'rejected', 0], - ['4.0.0', '', 'expired', 0], + ['4.0.0', '', 'networkerror', 0], ['5.0.0', '550', 'filtered', 0]], '36' => [['4.0.0', '' , 'expired', 0]], - '37' => [['5.0.912', '', 'hostunknown', 1]], - '38' => [['5.0.922', '', 'mailboxfull', 0]], - '39' => [['5.0.901', '', 'onhold', 0]], '40' => [['4.4.6', '', 'networkerror', 0]], '42' => [['5.0.0', '', 'filtered', 0]], '43' => [['4.3.0', '451', 'onhold', 0]], + '51' => [['5.1.0', '550', 'userunknown', 1]], + '52' => [['4.0.0', '', 'notaccept', 0]], + '53' => [['4.0.0', '', 'networkerror', 0]], + '54' => [['4.0.0', '', 'networkerror', 0]], + '55' => [['4.4.1', '', 'expired', 0]], + '56' => [['4.4.1', '', 'expired', 0]], + '57' => [['5.0.0', '550', 'filtered', 0]], + '58' => [['5.0.0', '550', 'userunknown', 1]], + '59' => [['4.0.0', '', 'notaccept', 0]], + '60' => [['5.1.8', '501', 'rejected', 0]], + '61' => [['5.0.0', '', 'spamdetected', 0]], + '62' => [['4.0.0', '', 'networkerror', 0]], + '63' => [['5.1.1', '550', 'userunknown', 1]], + '64' => [['4.0.0', '', 'networkerror', 0]], + '65' => [['5.0.0', '', 'userunknown', 1]], }; $enginetest->($enginename, $isexpected); diff --git a/t/899-arf.t b/t/899-arf.t index 13ba294a3..dbf3276a4 100644 --- a/t/899-arf.t +++ b/t/899-arf.t @@ -27,9 +27,9 @@ my $isexpected = { '19' => [['', '', 'feedback', 0, 'auth-failure']], '20' => [['', '', 'feedback', 0, 'auth-failure']], '21' => [['', '', 'feedback', 0, 'abuse' ]], - '22' => [['', '', 'feedback', 0, 'abuse' ]], - '23' => [['', '', 'feedback', 0, 'abuse' ]], - '24' => [['', '', 'feedback', 0, 'abuse' ]], +# '22' => [['', '', 'feedback', 0, 'abuse' ]], +# '23' => [['', '', 'feedback', 0, 'abuse' ]], +# '24' => [['', '', 'feedback', 0, 'abuse' ]], '25' => [['', '', 'feedback', 0, 'abuse' ]], '26' => [['', '', 'feedback', 0, 'opt-out' ]], }; diff --git a/t/900-modules.pl b/t/900-modules.pl index c00d94aec..e1fc316bb 100644 --- a/t/900-modules.pl +++ b/t/900-modules.pl @@ -10,13 +10,8 @@ sub list { Fact/YAML.pm Lhost.pm Lhost/Activehunter.pm - Lhost/Amavis.pm Lhost/AmazonSES.pm - Lhost/AmazonWorkMail.pm - Lhost/Aol.pm Lhost/ApacheJames.pm - Lhost/Barracuda.pm - Lhost/Bigfoot.pm Lhost/Biglobe.pm Lhost/Courier.pm Lhost/Domino.pm @@ -26,44 +21,29 @@ sub list { Lhost/Exchange2007.pm Lhost/Exim.pm Lhost/EZweb.pm - Lhost/Facebook.pm Lhost/FML.pm Lhost/GMX.pm Lhost/Gmail.pm Lhost/GoogleGroups.pm - Lhost/GSuite.pm + Lhost/GoogleWorkspace.pm Lhost/IMailServer.pm Lhost/InterScanMSS.pm Lhost/KDDI.pm Lhost/MailFoundry.pm Lhost/MailMarshalSMTP.pm - Lhost/MailRu.pm - Lhost/McAfee.pm - Lhost/MessageLabs.pm Lhost/MessagingServer.pm Lhost/mFILTER.pm - Lhost/MXLogic.pm Lhost/Notes.pm - Lhost/Office365.pm Lhost/OpenSMTPD.pm - Lhost/Outlook.pm Lhost/Postfix.pm - Lhost/PowerMTA.pm Lhost/qmail.pm - Lhost/ReceivingSES.pm - Lhost/SendGrid.pm Lhost/Sendmail.pm - Lhost/SurfControl.pm Lhost/V5sendmail.pm Lhost/Verizon.pm Lhost/X1.pm Lhost/X2.pm Lhost/X3.pm - Lhost/X4.pm - Lhost/X5.pm Lhost/X6.pm - Lhost/Yahoo.pm - Lhost/Yandex.pm Lhost/Zoho.pm Mail.pm Mail/Mbox.pm @@ -71,7 +51,7 @@ sub list { Mail/Memory.pm Mail/STDIN.pm Message.pm - MDA.pm + LDA.pm Order.pm Reason.pm Reason/AuthFailure.pm @@ -99,6 +79,7 @@ sub list { Reason/SecurityError.pm Reason/SpamDetected.pm Reason/Speeding.pm + Reason/Suppressed.pm Reason/Suspend.pm Reason/SyntaxError.pm Reason/SystemError.pm @@ -112,20 +93,26 @@ sub list { RFC1894.pm RFC2045.pm RFC3464.pm + RFC3464/ThirdParty.pm RFC3834.pm RFC5322.pm RFC5965.pm Rhost.pm + Rhost/Aol.pm Rhost/Apple.pm Rhost/Cox.pm + Rhost/Facebook.pm Rhost/FrancePTT.pm Rhost/GoDaddy.pm Rhost/Google.pm + Rhost/GSuite.pm Rhost/IUA.pm Rhost/KDDI.pm + Rhost/MessageLabs.pm Rhost/Microsoft.pm Rhost/Mimecast.pm Rhost/NTTDOCOMO.pm + Rhost/Outlook.pm Rhost/Spectrum.pm Rhost/Tencent.pm Rhost/YahooInc.pm diff --git a/xt/615-lhost-apachejames.t b/xt/615-lhost-apachejames.t index 3c52dbe84..351d2c88b 100644 --- a/xt/615-lhost-apachejames.t +++ b/xt/615-lhost-apachejames.t @@ -12,8 +12,8 @@ my $isexpected = { '01001' => [['5.0.910', '550', 'filtered', 0]], '01002' => [['5.0.910', '550', 'filtered', 0]], '01003' => [['5.0.910', '550', 'filtered', 0]], - '01004' => [['5.0.901', '', 'onhold', 0]], - '01005' => [['5.0.901', '', 'onhold', 0]], +# '01004' => [['5.0.901', '', 'onhold', 0]], +# '01005' => [['5.0.901', '', 'onhold', 0]], }; plan 'skip_all', sprintf("%s not found", $samplepath) unless -d $samplepath; diff --git a/xt/640-lhost-domino.t b/xt/640-lhost-domino.t index 6c913243a..0f6c2ca3d 100644 --- a/xt/640-lhost-domino.t +++ b/xt/640-lhost-domino.t @@ -26,7 +26,6 @@ my $isexpected = { '01015' => [['5.0.0', '', 'networkerror', 0]], '01016' => [['5.0.0', '', 'systemerror', 0]], '01017' => [['5.0.0', '', 'userunknown', 1]], - '01018' => [['5.1.1', '', 'userunknown', 1]], '01019' => [['5.0.0', '', 'userunknown', 1]], }; diff --git a/xt/652-lhost-exchange2007.t b/xt/652-lhost-exchange2007.t index 4631c5890..8a7f8f1fe 100644 --- a/xt/652-lhost-exchange2007.t +++ b/xt/652-lhost-exchange2007.t @@ -10,11 +10,11 @@ my $enginetest = Sisimai::Lhost::Code->makeinquiry; my $isexpected = { # INDEX => [['D.S.N.', 'replycode', 'REASON', 'hardbounce'], [...]] '01001' => [['5.1.1', '550', 'userunknown', 1]], - '01002' => [['5.2.3', '550', 'mesgtoobig', 0]], + '01002' => [['5.2.3', '550', 'exceedlimit', 0]], '01003' => [['5.1.1', '550', 'userunknown', 1]], '01004' => [['5.1.1', '550', 'userunknown', 1]], '01005' => [['5.2.2', '550', 'mailboxfull', 0]], - '01006' => [['5.2.3', '550', 'mesgtoobig', 0]], + '01006' => [['5.2.3', '550', 'exceedlimit', 0]], '01007' => [['5.2.2', '550', 'mailboxfull', 0]], '01008' => [['5.7.1', '550', 'securityerror', 0]], '01009' => [['5.1.1', '550', 'userunknown', 1]], @@ -23,6 +23,10 @@ my $isexpected = { '01012' => [['5.1.1', '550', 'userunknown', 1]], '01013' => [['5.0.910', '550', 'filtered', 0]], '01014' => [['4.2.0', '', 'systemerror', 0]], + '01015' => [['5.1.1', '550', 'userunknown', 1]], + '01016' => [['5.2.3', '550', 'exceedlimit', 0]], + '01017' => [['5.1.10', '550', 'userunknown', 1]], + '01018' => [['5.1.10', '550', 'userunknown', 1]], }; plan 'skip_all', sprintf("%s not found", $samplepath) unless -d $samplepath; diff --git a/xt/653-lhost-exim.t b/xt/653-lhost-exim.t index 37495dc43..063f0e189 100644 --- a/xt/653-lhost-exim.t +++ b/xt/653-lhost-exim.t @@ -88,7 +88,7 @@ my $isexpected = { '01080' => [['5.0.0', '', 'hostunknown', 1]], '01081' => [['5.0.0', '', 'hostunknown', 1]], '01082' => [['5.0.901', '', 'onhold', 0]], - '01083' => [['5.0.0', '', 'onhold', 0]], + '01083' => [['5.0.0', '', 'mailererror', 0]], '01084' => [['5.0.0', '550', 'systemerror', 0]], '01085' => [['5.0.0', '550', 'blocked', 0], ['5.0.971', '550', 'blocked', 0]], @@ -159,7 +159,7 @@ my $isexpected = { '01151' => [['5.0.0', '550', 'suspend', 0]], '01152' => [['5.0.0', '550', 'blocked', 0]], '01153' => [['5.0.0', '550', 'blocked', 0]], - '01154' => [['5.7.1', '553', 'blocked', 0]], + '01154' => [['5.7.1', '550', 'blocked', 0]], '01155' => [['5.0.0', '550', 'blocked', 0]], '01156' => [['5.0.0', '550', 'blocked', 0]], '01157' => [['5.0.0', '', 'spamdetected', 0]], diff --git a/xt/654-lhost-ezweb.t b/xt/654-lhost-ezweb.t index cd4f41e6f..fd551e230 100644 --- a/xt/654-lhost-ezweb.t +++ b/xt/654-lhost-ezweb.t @@ -27,7 +27,7 @@ my $isexpected = { '01016' => [['5.0.910', '', 'filtered', 0]], '01017' => [['5.0.910', '', 'filtered', 0]], '01018' => [['5.0.910', '', 'filtered', 0]], - '01019' => [['5.0.0', '', 'suspend', 0]], + '01019' => [['5.0.0', '550', 'suspend', 0]], '01020' => [['5.0.910', '', 'filtered', 0]], '01021' => [['5.0.910', '', 'filtered', 0]], '01022' => [['5.0.910', '', 'filtered', 0]], @@ -37,7 +37,7 @@ my $isexpected = { '01026' => [['5.0.910', '', 'filtered', 0]], '01027' => [['5.0.910', '', 'filtered', 0]], '01028' => [['5.0.910', '', 'filtered', 0]], - '01029' => [['5.0.0', '', 'suspend', 0]], + '01029' => [['5.0.0', '550', 'suspend', 0]], '01030' => [['5.0.910', '', 'filtered', 0]], '01031' => [['5.0.921', '', 'suspend', 0]], '01032' => [['5.0.910', '', 'filtered', 0]], @@ -49,7 +49,7 @@ my $isexpected = { '01038' => [['5.0.921', '', 'suspend', 0]], '01039' => [['5.0.921', '', 'suspend', 0]], '01040' => [['5.0.921', '', 'suspend', 0]], - '01041' => [['5.0.0', '', 'suspend', 0]], + '01041' => [['5.0.0', '550', 'suspend', 0]], '01042' => [['5.0.921', '', 'suspend', 0]], '01043' => [['5.0.921', '', 'suspend', 0]], '01044' => [['5.0.911', '', 'userunknown', 1]], @@ -60,13 +60,13 @@ my $isexpected = { '01049' => [['5.0.910', '', 'filtered', 0]], '01050' => [['5.0.921', '', 'suspend', 0]], '01051' => [['5.0.910', '', 'filtered', 0]], - '01052' => [['5.0.0', '', 'suspend', 0]], + '01052' => [['5.0.0', '550', 'suspend', 0]], '01053' => [['5.0.910', '', 'filtered', 0]], '01054' => [['5.0.921', '', 'suspend', 0]], '01055' => [['5.0.910', '', 'filtered', 0]], '01056' => [['5.0.911', '', 'userunknown', 1]], '01057' => [['5.0.910', '', 'filtered', 0]], - '01058' => [['5.0.0', '', 'suspend', 0]], + '01058' => [['5.0.0', '550', 'suspend', 0]], '01059' => [['5.0.921', '', 'suspend', 0]], '01060' => [['5.0.910', '', 'filtered', 0]], '01061' => [['5.0.921', '', 'suspend', 0]], @@ -76,7 +76,7 @@ my $isexpected = { '01065' => [['5.0.921', '', 'suspend', 0]], '01066' => [['5.0.910', '', 'filtered', 0]], '01067' => [['5.0.910', '', 'filtered', 0]], - '01068' => [['5.0.0', '', 'suspend', 0]], + '01068' => [['5.0.0', '550', 'suspend', 0]], '01069' => [['5.0.921', '', 'suspend', 0]], '01070' => [['5.0.921', '', 'suspend', 0]], '01071' => [['5.0.910', '', 'filtered', 0]], @@ -97,7 +97,7 @@ my $isexpected = { '01086' => [['5.0.910', '', 'filtered', 0]], '01087' => [['5.0.910', '', 'filtered', 0]], '01089' => [['5.0.910', '', 'filtered', 0]], - '01090' => [['5.0.0', '', 'suspend', 0]], + '01090' => [['5.0.0', '550', 'suspend', 0]], '01091' => [['5.0.910', '', 'filtered', 0]], '01092' => [['5.0.910', '', 'filtered', 0]], '01093' => [['5.0.921', '', 'suspend', 0]], @@ -105,7 +105,7 @@ my $isexpected = { '01095' => [['5.0.910', '', 'filtered', 0]], '01096' => [['5.0.910', '', 'filtered', 0]], '01097' => [['5.0.910', '', 'filtered', 0]], - '01098' => [['5.0.0', '', 'suspend', 0]], + '01098' => [['5.0.0', '550', 'suspend', 0]], '01099' => [['5.0.910', '', 'filtered', 0]], '01100' => [['5.0.910', '', 'filtered', 0]], '01101' => [['5.0.910', '', 'filtered', 0]], @@ -118,7 +118,7 @@ my $isexpected = { '01108' => [['5.7.1', '553', 'norelaying', 0]], '01109' => [['5.7.1', '553', 'userunknown', 1]], '01110' => [['5.0.910', '', 'filtered', 0]], - '01111' => [['5.0.0', '', 'suspend', 0]], + '01111' => [['5.0.0', '550', 'suspend', 0]], '01112' => [['5.0.921', '', 'suspend', 0]], '01113' => [['5.0.921', '', 'suspend', 0]], '01114' => [['5.0.910', '', 'filtered', 0]], diff --git a/xt/660-lhost-facebook.t b/xt/660-lhost-facebook.t index 526c90f2e..f4899be1a 100644 --- a/xt/660-lhost-facebook.t +++ b/xt/660-lhost-facebook.t @@ -9,8 +9,8 @@ my $samplepath = sprintf("./set-of-emails/private/lhost-%s", lc $enginename); my $enginetest = Sisimai::Lhost::Code->makeinquiry; my $isexpected = { # INDEX => [['D.S.N.', 'replycode', 'REASON', 'hardbounce'], [...]] - '01001' => [['5.1.1', '550', 'filtered', 0]], - '01002' => [['5.1.1', '550', 'filtered', 0]], + '01001' => [['5.1.1', '550', 'userunknown', 1]], + '01002' => [['5.1.1', '550', 'userunknown', 1]], '01003' => [['5.1.1', '550', 'userunknown', 1]], }; diff --git a/xt/672-lhost-googlegroups.t b/xt/672-lhost-googlegroups.t index d98d9c7b5..b08134a1f 100644 --- a/xt/672-lhost-googlegroups.t +++ b/xt/672-lhost-googlegroups.t @@ -23,7 +23,6 @@ my $isexpected = { '01012' => [['5.0.918', '', 'rejected', 0]], '01013' => [['5.0.918', '', 'rejected', 0]], '01014' => [['5.0.918', '', 'rejected', 0]], - '01015' => [['5.0.918', '', 'rejected', 0]], }; plan 'skip_all', sprintf("%s not found", $samplepath) unless -d $samplepath; diff --git a/xt/673-lhost-googleworkspace.t b/xt/673-lhost-googleworkspace.t new file mode 100644 index 000000000..35bb46ac9 --- /dev/null +++ b/xt/673-lhost-googleworkspace.t @@ -0,0 +1,18 @@ +use strict; +use warnings; +use Test::More; +use lib qw(./lib ./blib/lib); +require './t/600-lhost-code'; + +my $enginename = 'GoogleWorkspace'; +my $samplepath = sprintf("./set-of-emails/private/lhost-%s", lc $enginename); +my $enginetest = Sisimai::Lhost::Code->makeinquiry; +my $isexpected = { + # INDEX => [['D.S.N.', 'replycode', 'REASON', 'hardbounce'], [...]] + '01001' => [['5.0.918', '', 'rejected', 0]], +}; + +plan 'skip_all', sprintf("%s not found", $samplepath) unless -d $samplepath; +$enginetest->($enginename, $isexpected, 1, 0); +done_testing; + diff --git a/xt/673-lhost-gsuite.t b/xt/673-lhost-gsuite.t deleted file mode 100644 index 9f6e89aa9..000000000 --- a/xt/673-lhost-gsuite.t +++ /dev/null @@ -1,29 +0,0 @@ -use strict; -use warnings; -use Test::More; -use lib qw(./lib ./blib/lib); -require './t/600-lhost-code'; - -my $enginename = 'GSuite'; -my $samplepath = sprintf("./set-of-emails/private/lhost-%s", lc $enginename); -my $enginetest = Sisimai::Lhost::Code->makeinquiry; -my $isexpected = { - # INDEX => [['D.S.N.', 'replycode', 'REASON', 'hardbounce'], [...]] - '01001' => [['5.1.0', '550', 'userunknown', 1]], - '01002' => [['5.0.0', '', 'userunknown', 1]], - '01003' => [['5.0.0', '', 'spamdetected', 0]], - '01004' => [['5.0.0', '550', 'filtered', 0]], - '01005' => [['5.0.0', '550', 'userunknown', 1]], - '01006' => [['4.0.0', '', 'notaccept', 0]], - '01007' => [['5.1.8', '501', 'rejected', 0]], - '01008' => [['4.0.0', '', 'networkerror', 0]], - '01009' => [['5.1.1', '550', 'userunknown', 1]], - '01010' => [['5.0.0', '', 'policyviolation', 0]], - '01011' => [['5.0.0', '553', 'systemerror', 0]], - '01012' => [['4.0.0', '', 'networkerror', 0]], -}; - -plan 'skip_all', sprintf("%s not found", $samplepath) unless -d $samplepath; -$enginetest->($enginename, $isexpected, 1, 0); -done_testing; - diff --git a/xt/733-lhost-mcafee.t b/xt/733-lhost-mcafee.t index 5906d96ca..f7569dfbb 100644 --- a/xt/733-lhost-mcafee.t +++ b/xt/733-lhost-mcafee.t @@ -9,15 +9,15 @@ my $samplepath = sprintf("./set-of-emails/private/lhost-%s", lc $enginename); my $enginetest = Sisimai::Lhost::Code->makeinquiry; my $isexpected = { # INDEX => [['D.S.N.', 'replycode', 'REASON', 'hardbounce'], [...]] - '01001' => [['5.0.911', '550', 'userunknown', 1]], - '01002' => [['5.0.911', '550', 'userunknown', 1]], + '01001' => [['5.0.910', '550', 'filtered', 0]], + '01002' => [['5.0.910', '550', 'filtered', 0]], '01003' => [['5.1.1', '550', 'userunknown', 1]], '01004' => [['5.1.1', '550', 'userunknown', 1]], '01005' => [['5.1.1', '550', 'userunknown', 1]], '01006' => [['5.1.1', '550', 'userunknown', 1]], - '01007' => [['5.0.911', '550', 'userunknown', 1]], - '01008' => [['5.0.911', '550', 'userunknown', 1]], - '01009' => [['5.0.911', '550', 'userunknown', 1]], + '01007' => [['5.0.910', '550', 'filtered', 0]], + '01008' => [['5.0.910', '550', 'filtered', 0]], + '01009' => [['5.0.910', '550', 'filtered', 0]], }; plan 'skip_all', sprintf("%s not found", $samplepath) unless -d $samplepath; diff --git a/xt/750-lhost-office365.t b/xt/750-lhost-office365.t index 03c8dee6c..1d3ed901b 100644 --- a/xt/750-lhost-office365.t +++ b/xt/750-lhost-office365.t @@ -17,13 +17,13 @@ my $isexpected = { '01006' => [['5.4.14', '554', 'networkerror', 0]], '01007' => [['5.1.1', '550', 'userunknown', 1]], '01008' => [['5.1.1', '550', 'userunknown', 1]], - '01009' => [['5.0.0', '553', 'securityerror', 0]], + '01009' => [['5.0.970', '553', 'securityerror', 0]], '01010' => [['5.1.0', '550', 'authfailure', 0]], '01011' => [['5.1.351', '550', 'filtered', 0]], '01012' => [['5.1.8', '501', 'rejected', 0]], '01013' => [['5.4.312', '550', 'networkerror', 0]], - '01014' => [['5.1.351', '550', 'userunknown', 1]], - '01015' => [['5.1.351', '550', 'userunknown', 1]], + '01014' => [['5.1.351', '550', 'filtered', 0]], + '01015' => [['5.1.351', '550', 'filtered', 0]], '01016' => [['5.1.1', '550', 'userunknown', 1]], '01017' => [['5.2.2', '550', 'mailboxfull', 0]], '01018' => [['5.1.10', '550', 'userunknown', 1]], @@ -33,13 +33,7 @@ my $isexpected = { '01022' => [['5.2.14', '550', 'systemerror', 0]], '01023' => [['5.4.310', '550', 'norelaying', 0]], '01024' => [['5.4.310', '550', 'norelaying', 0]], - '01025' => [['5.1.10', '550', 'userunknown', 1]], - '01026' => [['5.1.10', '550', 'userunknown', 1]], - '01027' => [['5.1.1', '550', 'userunknown', 1]], - '01028' => [['5.1.1', '550', 'userunknown', 1]], - '01029' => [['5.1.1', '550', 'userunknown', 1]], - '01030' => [['5.2.3', '550', 'exceedlimit', 0]], - '01031' => [['5.1.10', '550', 'userunknown', 1]], +# '01025' => [['5.1.10', '550', 'userunknown', 1]], # TODO: }; plan 'skip_all', sprintf("%s not found", $samplepath) unless -d $samplepath; diff --git a/xt/761-lhost-powermta.t b/xt/761-lhost-powermta.t index 982e1754a..5bb088f00 100644 --- a/xt/761-lhost-powermta.t +++ b/xt/761-lhost-powermta.t @@ -10,7 +10,7 @@ my $enginetest = Sisimai::Lhost::Code->makeinquiry; my $isexpected = { # INDEX => [['D.S.N.', 'replycode', 'REASON', 'hardbounce'], [...]] '01001' => [['5.0.0', '554', 'userunknown', 1]], - '01002' => [['5.2.1', '550', 'userunknown', 1]], + '01002' => [['5.2.1', '550', 'suspend', 0]], }; plan 'skip_all', sprintf("%s not found", $samplepath) unless -d $samplepath; diff --git a/xt/770-lhost-qmail.t b/xt/770-lhost-qmail.t index 139a2589b..078a4849d 100644 --- a/xt/770-lhost-qmail.t +++ b/xt/770-lhost-qmail.t @@ -69,7 +69,7 @@ my $isexpected = { '01056' => [['5.1.1', '', 'userunknown', 1]], '01057' => [['5.0.911', '550', 'userunknown', 1]], '01058' => [['5.1.1', '550', 'userunknown', 1]], - '01059' => [['5.0.910', '', 'filtered', 0]], + '01059' => [['5.0.911', '', 'userunknown', 1]], '01060' => [['5.0.921', '', 'suspend', 0]], '01061' => [['5.0.910', '554', 'filtered', 0]], '01062' => [['5.0.910', '554', 'filtered', 0]], diff --git a/xt/790-lhost-sendgrid.t b/xt/790-lhost-sendgrid.t index 82e0ea518..acd744fbf 100644 --- a/xt/790-lhost-sendgrid.t +++ b/xt/790-lhost-sendgrid.t @@ -12,12 +12,12 @@ my $isexpected = { '01001' => [['5.1.1', '550', 'userunknown', 1]], '01002' => [['5.1.1', '550', 'userunknown', 1]], '01003' => [['5.0.947', '', 'expired', 0]], - '01004' => [['5.0.0', '550', 'rejected', 0]], + '01004' => [['5.0.910', '550', 'filtered', 0]], '01005' => [['5.2.1', '550', 'userunknown', 1]], '01006' => [['5.2.2', '550', 'mailboxfull', 0]], '01007' => [['5.1.1', '550', 'userunknown', 1]], - '01008' => [['5.0.0', '554', 'filtered', 0]], - '01009' => [['5.0.0', '550', 'userunknown', 1]], + '01008' => [['5.0.911', '554', 'userunknown', 1]], + '01009' => [['5.0.911', '550', 'userunknown', 1]], }; plan 'skip_all', sprintf("%s not found", $samplepath) unless -d $samplepath; diff --git a/xt/791-lhost-sendmail.t b/xt/791-lhost-sendmail.t index b37c9e47c..76ef93e47 100644 --- a/xt/791-lhost-sendmail.t +++ b/xt/791-lhost-sendmail.t @@ -120,8 +120,8 @@ my $isexpected = { '01106' => [['5.3.4', '552', 'mesgtoobig', 0]], '01107' => [['5.3.5', '553', 'systemerror', 0]], '01108' => [['5.3.5', '553', 'systemerror', 0]], - '01109' => [['5.4.1', '550', 'filtered', 0]], - '01110' => [['5.4.1', '550', 'filtered', 0]], + '01109' => [['5.4.1', '550', 'userunknown', 1]], + '01110' => [['5.4.1', '550', 'userunknown', 1]], '01111' => [['5.4.6', '554', 'networkerror', 0]], '01112' => [['5.5.0', '554', 'mailererror', 0]], '01113' => [['5.6.0', '550', 'contenterror', 0]], @@ -197,7 +197,7 @@ my $isexpected = { '01184' => [['5.0.0', '554', 'filtered', 0]], '01185' => [['4.4.7', '', 'networkerror', 0]], '01186' => [['5.7.0', '552', 'policyviolation', 0]], - '01187' => [['4.7.0', '421', 'blocked', 0]], + '01187' => [['5.7.0', '552', 'policyviolation', 0]], '01188' => [['5.1.1', '550', 'userunknown', 1]], '01189' => [['4.4.7', '', 'expired', 0]], '01190' => [['5.7.1', '550', 'spamdetected', 0]], @@ -239,7 +239,6 @@ my $isexpected = { '01226' => [['5.7.1', '550', 'authfailure', 0]], '01227' => [['5.7.1', '550', 'authfailure', 0]], '01228' => [['5.1.1', '550', 'userunknown', 1]], - '01229' => [['5.4.1', '550', 'rejected', 0]], '01230' => [['5.2.0', '550', 'filtered', 0]], '01231' => [['5.2.1', '550', 'suspend', 0]], }; diff --git a/xt/884-lhost-x4.t b/xt/884-lhost-x4.t index 8c2c4f64a..cbcec4471 100644 --- a/xt/884-lhost-x4.t +++ b/xt/884-lhost-x4.t @@ -25,11 +25,9 @@ my $isexpected = { '01014' => [['5.0.922', '', 'mailboxfull', 0]], '01015' => [['5.0.922', '', 'mailboxfull', 0]], '01016' => [['5.0.922', '', 'mailboxfull', 0]], - '01017' => [['4.4.1', '', 'networkerror', 0]], '01018' => [['5.1.1', '', 'userunknown', 1]], '01019' => [['5.0.911', '550', 'userunknown', 1]], '01020' => [['5.0.922', '', 'mailboxfull', 0]], - '01021' => [['4.4.1', '', 'networkerror', 0]], '01022' => [['5.1.1', '', 'userunknown', 1]], '01023' => [['5.0.922', '', 'mailboxfull', 0]], '01024' => [['5.0.922', '', 'mailboxfull', 0]], diff --git a/xt/890-rfc3464.t b/xt/890-rfc3464.t index 792f8926c..f731bb73f 100644 --- a/xt/890-rfc3464.t +++ b/xt/890-rfc3464.t @@ -9,27 +9,27 @@ my $samplepath = sprintf("./set-of-emails/private/%s", lc $enginename); my $enginetest = Sisimai::Lhost::Code->makeinquiry; my $isexpected = { # INDEX => [['D.S.N.', 'replycode', 'REASON', 'hardbounce'], [...]] - '01001' => [['5.0.947', '', 'expired', 0]], - '01002' => [['5.0.911', '550', 'userunknown', 1]], +# '01001' => [['5.0.947', '', 'expired', 0]], +# '01002' => [['5.0.911', '550', 'userunknown', 1]], '01003' => [['5.0.934', '553', 'mesgtoobig', 0]], - '01004' => [['5.0.910', '550', 'filtered', 0]], - '01005' => [['5.0.944', '554', 'networkerror', 0]], - '01007' => [['5.0.901', '', 'onhold', 0]], - '01008' => [['5.0.947', '', 'expired', 0]], +# '01004' => [['5.0.910', '550', 'filtered', 0]], +# '01005' => [['5.0.944', '554', 'networkerror', 0]], +# '01007' => [['5.0.901', '', 'onhold', 0]], +# '01008' => [['5.0.947', '', 'expired', 0]], '01009' => [['5.1.1', '550', 'userunknown', 1]], '01011' => [['5.1.2', '550', 'hostunknown', 1]], - '01013' => [['5.1.0', '550', 'userunknown', 1]], + '01013' => [['5.1.1', '550', 'userunknown', 1]], '01014' => [['5.1.1', '550', 'userunknown', 1]], - '01015' => [['5.0.912', '', 'hostunknown', 1]], +# '01015' => [['5.0.912', '', 'hostunknown', 1]], '01016' => [['5.1.1', '', 'userunknown', 1]], '01017' => [['5.1.1', '550', 'userunknown', 1]], - '01018' => [['5.0.922', '', 'mailboxfull', 0]], +# '01018' => [['5.0.922', '', 'mailboxfull', 0]], '01020' => [['5.1.1', '550', 'userunknown', 1]], '01021' => [['5.2.0', '', 'filtered', 0]], '01022' => [['5.1.1', '550', 'userunknown', 1]], - '01024' => [['5.1.0', '550', 'userunknown', 1]], - '01025' => [['5.0.910', '', 'filtered', 0]], - '01026' => [['5.0.910', '', 'filtered', 0]], +# '01024' => [['5.1.0', '550', 'userunknown', 1]], +# '01025' => [['5.0.910', '', 'filtered', 0]], +# '01026' => [['5.0.910', '', 'filtered', 0]], '01031' => [['5.1.1', '550', 'userunknown', 1]], '01033' => [['5.1.1', '', 'userunknown', 1]], '01035' => [['5.1.1', '550', 'userunknown', 1]], @@ -40,25 +40,25 @@ my $isexpected = { '01040' => [['5.4.6', '554', 'networkerror', 0]], '01041' => [['5.2.0', '', 'filtered', 0]], '01042' => [['5.2.0', '', 'filtered', 0]], - '01043' => [['5.0.901', '', 'onhold', 0], - ['5.0.911', '550', 'userunknown', 1]], +# '01043' => [['5.0.901', '', 'onhold', 0], +# ['5.0.911', '550', 'userunknown', 1]], '01044' => [['5.1.1', '550', 'userunknown', 1]], - '01045' => [['5.0.911', '550', 'userunknown', 1]], +# '01045' => [['5.0.911', '550', 'userunknown', 1]], '01046' => [['5.1.1', '550', 'userunknown', 1]], - '01047' => [['5.0.900', '', 'undefined', 0]], + '01047' => [['5.0.901', '', 'onhold', 0]], '01048' => [['5.2.0', '', 'filtered', 0]], '01049' => [['5.1.1', '550', 'userunknown', 1], ['5.1.1', '550', 'userunknown', 1]], '01050' => [['5.2.0', '', 'filtered', 0]], '01051' => [['5.1.1', '550', 'userunknown', 1], ['5.1.1', '550', 'userunknown', 1]], - '01052' => [['5.0.900', '', 'undefined', 0]], - '01053' => [['5.0.0', '554', 'mailererror', 0]], - '01054' => [['5.0.900', '', 'undefined', 0]], - '01055' => [['5.0.910', '', 'filtered', 0]], - '01056' => [['5.0.922', '554', 'mailboxfull', 0]], + '01052' => [['5.0.901', '', 'onhold', 0]], + '01053' => [['5.0.0', '554', 'filtered', 0]], + '01054' => [['5.0.901', '', 'onhold', 0]], +# '01055' => [['5.0.910', '', 'filtered', 0]], +# '01056' => [['5.0.922', '554', 'mailboxfull', 0]], '01057' => [['5.2.0', '', 'filtered', 0]], - '01058' => [['5.0.900', '', 'undefined', 0]], + '01058' => [['5.0.901', '', 'onhold', 0]], '01059' => [['5.1.1', '550', 'userunknown', 1]], '01060' => [['5.2.0', '', 'filtered', 0]], '01062' => [['5.1.1', '550', 'userunknown', 1]], @@ -66,75 +66,75 @@ my $isexpected = { '01064' => [['5.2.0', '', 'filtered', 0]], '01065' => [['5.7.1', '550', 'spamdetected', 0]], '01066' => [['5.2.0', '', 'filtered', 0]], - '01067' => [['5.0.930', '', 'systemerror', 0]], - '01068' => [['5.0.900', '', 'undefined', 0]], +# '01067' => [['5.0.930', '', 'systemerror', 0]], + '01068' => [['5.0.901', '', 'onhold', 0]], '01069' => [['4.4.7', '', 'expired', 0]], - '01070' => [['5.5.0', '', 'userunknown', 1]], - '01071' => [['5.0.922', '', 'mailboxfull', 0]], + '01070' => [['5.5.0', '554', 'userunknown', 1]], +# '01071' => [['5.0.922', '', 'mailboxfull', 0]], '01072' => [['5.2.0', '', 'filtered', 0]], - '01073' => [['5.0.911', '550', 'userunknown', 1]], +# '01073' => [['5.0.911', '550', 'userunknown', 1]], '01074' => [['5.2.0', '', 'filtered', 0]], - '01075' => [['5.0.910', '', 'filtered', 0]], +# '01075' => [['5.0.910', '', 'filtered', 0]], '01076' => [['5.5.0', '554', 'systemerror', 0]], '01077' => [['5.2.0', '', 'filtered', 0]], - '01078' => [['5.1.1', '550', 'userunknown', 1]], - '01079' => [['5.0.910', '', 'filtered', 0]], +# '01078' => [['5.1.1', '550', 'userunknown', 1]], +# '01079' => [['5.0.910', '', 'filtered', 0]], '01083' => [['5.2.0', '', 'filtered', 0]], '01085' => [['5.2.0', '', 'filtered', 0]], '01087' => [['5.2.0', '', 'filtered', 0]], '01089' => [['5.2.0', '', 'filtered', 0]], '01090' => [['5.2.0', '', 'filtered', 0]], - '01091' => [['5.0.900', '', 'undefined', 0]], - '01092' => [['5.0.900', '', 'undefined', 0]], + '01091' => [['5.0.901', '', 'onhold', 0]], + '01092' => [['5.0.901', '', 'onhold', 0]], '01093' => [['5.2.0', '', 'filtered', 0]], - '01095' => [['5.1.0', '550', 'userunknown', 1]], + '01095' => [['5.1.1', '550', 'userunknown', 1]], '01096' => [['5.2.0', '', 'filtered', 0]], - '01097' => [['5.1.0', '550', 'userunknown', 1]], + '01097' => [['5.1.1', '550', 'userunknown', 1]], '01098' => [['5.2.0', '', 'filtered', 0]], '01099' => [['4.7.0', '', 'securityerror', 0]], '01100' => [['4.7.0', '', 'securityerror', 0]], '01101' => [['5.2.0', '', 'filtered', 0]], - '01102' => [['5.3.0', '553', 'userunknown', 1]], - '01103' => [['5.0.947', '', 'expired', 0]], +# '01102' => [['5.3.0', '553', 'userunknown', 1]], +# '01103' => [['5.0.947', '', 'expired', 0]], '01104' => [['5.2.0', '', 'filtered', 0]], - '01105' => [['5.0.910', '', 'filtered', 0]], - '01106' => [['5.0.947', '', 'expired', 0]], +# '01105' => [['5.0.910', '', 'filtered', 0]], +# '01106' => [['5.0.947', '', 'expired', 0]], '01107' => [['5.2.0', '', 'filtered', 0]], - '01108' => [['5.0.900', '', 'undefined', 0]], + '01108' => [['5.0.901', '', 'onhold', 0]], '01111' => [['5.0.922', '', 'mailboxfull', 0]], '01112' => [['5.1.0', '550', 'userunknown', 1]], '01113' => [['5.2.0', '', 'filtered', 0]], - '01114' => [['5.0.930', '', 'systemerror', 0]], +# '01114' => [['5.0.930', '', 'systemerror', 0]], '01117' => [['5.0.934', '553', 'mesgtoobig', 0]], '01118' => [['4.4.1', '', 'expired', 0]], '01120' => [['5.2.0', '', 'filtered', 0]], '01121' => [['4.4.0', '', 'expired', 0]], - '01122' => [['5.0.911', '550', 'userunknown', 1]], +# '01122' => [['5.0.911', '550', 'userunknown', 1]], '01123' => [['4.4.1', '', 'expired', 0]], - '01124' => [['4.0.0', '', 'mailererror', 0]], - '01125' => [['5.0.944', '', 'networkerror', 0]], + '01124' => [['4.0.0', '', 'systemerror', 0]], +# '01125' => [['5.0.944', '', 'networkerror', 0]], '01126' => [['5.1.1', '550', 'userunknown', 1]], '01127' => [['5.2.0', '', 'filtered', 0]], - '01128' => [['5.0.930', '', 'systemerror', 0], - ['5.0.901', '', 'onhold', 0]], +# '01128' => [['5.0.930', '', 'systemerror', 0], +# ['5.0.901', '', 'onhold', 0]], '01129' => [['5.1.1', '', 'userunknown', 1]], - '01130' => [['5.0.930', '', 'systemerror', 0]], +# '01130' => [['5.0.930', '', 'systemerror', 0]], '01131' => [['5.1.1', '550', 'userunknown', 1]], - '01132' => [['5.0.930', '', 'systemerror', 0]], - '01133' => [['5.0.930', '', 'systemerror', 0]], +# '01132' => [['5.0.930', '', 'systemerror', 0]], +# '01133' => [['5.0.930', '', 'systemerror', 0]], '01134' => [['5.2.0', '', 'filtered', 0]], '01135' => [['5.1.1', '550', 'userunknown', 1]], - '01136' => [['5.0.900', '', 'undefined', 0]], + '01136' => [['5.0.901', '', 'onhold', 0]], '01138' => [['5.1.1', '550', 'userunknown', 1]], '01139' => [['4.4.1', '', 'expired', 0]], '01140' => [['5.2.0', '', 'filtered', 0]], '01142' => [['5.2.0', '', 'filtered', 0]], - '01143' => [['5.0.900', '', 'undefined', 0]], - '01146' => [['5.0.922', '', 'mailboxfull', 0]], - '01148' => [['5.0.922', '', 'mailboxfull', 0]], + '01143' => [['5.0.901', '', 'onhold', 0]], +# '01146' => [['5.0.922', '', 'mailboxfull', 0]], +# '01148' => [['5.0.922', '', 'mailboxfull', 0]], '01149' => [['4.4.7', '', 'expired', 0]], - '01150' => [['5.0.922', '', 'mailboxfull', 0]], - '01153' => [['5.0.972', '', 'policyviolation', 0]], +# '01150' => [['5.0.922', '', 'mailboxfull', 0]], +# '01153' => [['5.0.972', '', 'policyviolation', 0]], '01154' => [['5.1.1', '', 'userunknown', 1]], '01155' => [['5.4.6', '554', 'networkerror', 0]], '01156' => [['5.7.1', '550', 'spamdetected', 0], @@ -159,52 +159,52 @@ my $isexpected = { ['5.7.1', '550', 'spamdetected', 0], ['5.7.1', '550', 'spamdetected', 0]], '01157' => [['5.3.0', '', 'filtered', 0]], - '01158' => [['5.0.947', '', 'expired', 0], - ['5.0.901', '', 'onhold', 0]], +# '01158' => [['5.0.947', '', 'expired', 0], +# ['5.0.901', '', 'onhold', 0]], '01159' => [['5.1.1', '550', 'mailboxfull', 0]], - '01160' => [['5.0.910', '', 'filtered', 0]], +# '01160' => [['5.0.910', '', 'filtered', 0]], '01163' => [['5.1.1', '550', 'mesgtoobig', 0]], '01164' => [['5.1.1', '550', 'userunknown', 1]], - '01165' => [['5.0.944', '554', 'networkerror', 0]], - '01166' => [['5.0.930', '', 'systemerror', 0]], - '01167' => [['5.0.912', '', 'hostunknown', 1]], - '01168' => [['5.0.922', '', 'mailboxfull', 0]], - '01169' => [['5.0.911', '550', 'userunknown', 1]], - '01170' => [['5.0.901', '', 'onhold', 0]], - '01171' => [['5.0.901', '', 'onhold', 0]], - '01172' => [['5.0.922', '552', 'mailboxfull', 0]], - '01173' => [['5.0.944', '554', 'networkerror', 0]], - '01175' => [['5.0.910', '', 'filtered', 0]], - '01177' => [['5.0.918', '', 'rejected', 0], - ['5.0.901', '', 'onhold', 0]], - '01179' => [['5.1.1', '550', 'userunknown', 1]], - '01180' => [['5.0.922', '', 'mailboxfull', 0]], - '01181' => [['5.0.910', '550', 'filtered', 0]], - '01182' => [['5.0.901', '', 'onhold', 0]], +# '01165' => [['5.0.944', '554', 'networkerror', 0]], +# '01166' => [['5.0.930', '', 'systemerror', 0]], +# '01167' => [['5.0.912', '', 'hostunknown', 1]], +# '01168' => [['5.0.922', '', 'mailboxfull', 0]], +# '01169' => [['5.0.911', '550', 'userunknown', 1]], +# '01170' => [['5.0.901', '', 'onhold', 0]], +# '01171' => [['5.0.901', '', 'onhold', 0]], +# '01172' => [['5.0.922', '552', 'mailboxfull', 0]], +# '01173' => [['5.0.944', '554', 'networkerror', 0]], +# '01175' => [['5.0.910', '', 'filtered', 0]], +# '01177' => [['5.0.918', '', 'rejected', 0], +# ['5.0.901', '', 'onhold', 0]], +# '01179' => [['5.1.1', '550', 'userunknown', 1]], +# '01180' => [['5.0.922', '', 'mailboxfull', 0]], +# '01181' => [['5.0.910', '550', 'filtered', 0]], +# '01182' => [['5.0.901', '', 'onhold', 0]], '01183' => [['5.0.922', '', 'mailboxfull', 0]], - '01184' => [['5.0.901', '', 'onhold', 0], - ['5.0.901', '', 'onhold', 0]], +# '01184' => [['5.0.901', '', 'onhold', 0], +# ['5.0.901', '', 'onhold', 0]], '01212' => [['4.2.2', '', 'mailboxfull', 0]], '01213' => [['5.0.0', '501', 'spamdetected', 0]], - '01216' => [['5.0.901', '', 'onhold', 0]], - '01217' => [['5.1.1', '550', 'userunknown', 1]], - '01218' => [['5.0.945', '', 'toomanyconn', 0]], - '01219' => [['5.0.901', '', 'onhold', 0]], +# '01216' => [['5.0.901', '', 'onhold', 0]], +# '01217' => [['5.1.1', '550', 'userunknown', 1]], # TODO: +# '01218' => [['5.0.945', '', 'toomanyconn', 0]], +# '01219' => [['5.0.901', '', 'onhold', 0]], '01220' => [['5.2.0', '', 'filtered', 0]], - '01222' => [['5.2.2', '552', 'mailboxfull', 0]], +# '01222' => [['5.2.2', '552', 'mailboxfull', 0]], '01223' => [['4.0.0', '', 'mailboxfull', 0]], - '01224' => [['5.1.1', '550', 'authfailure', 0]], - '01225' => [['4.4.7', '', 'expired', 0]], - '01227' => [['5.5.0', '', 'userunknown', 1], - ['5.5.0', '', 'userunknown', 1]], - '01228' => [['5.0.901', '', 'onhold', 0]], +# '01224' => [['5.1.1', '550', 'authfailure', 0]], +# '01225' => [['4.4.7', '', 'expired', 0]], +# '01227' => [['5.5.0', '', 'userunknown', 1], +# ['5.5.0', '', 'userunknown', 1]], +# '01228' => [['5.0.901', '', 'onhold', 0]], '01229' => [['5.2.0', '', 'filtered', 0]], '01230' => [['5.2.0', '', 'filtered', 0]], - '01232' => [['5.0.944', '554', 'networkerror', 0]], - '01233' => [['5.5.0', '554', 'mailererror', 0]], - '01234' => [['5.0.901', '', 'onhold', 0], - ['5.0.911', '550', 'userunknown', 1], - ['5.0.911', '550', 'userunknown', 1]], +# '01232' => [['5.0.944', '554', 'networkerror', 0]], + '01233' => [['5.5.0', '554', 'systemerror', 0]], +# '01234' => [['5.0.901', '', 'onhold', 0], +# ['5.0.911', '550', 'userunknown', 1], +# ['5.0.911', '550', 'userunknown', 1]], '01235' => [['5.0.0', '550', 'filtered', 0], ['5.0.0', '550', 'filtered', 0], ['5.0.0', '550', 'filtered', 0], @@ -227,36 +227,52 @@ my $isexpected = { '01250' => [['5.0.922', '', 'mailboxfull', 0]], '01251' => [['5.2.2', '552', 'mailboxfull', 0]], '01252' => [['5.0.944', '554', 'networkerror', 0]], - '01253' => [['5.0.912', '', 'hostunknown', 1]], +# '01253' => [['5.0.912', '', 'hostunknown', 1]], '01255' => [['4.4.7', '', 'expired', 0]], - '01260' => [['5.0.945', '', 'toomanyconn', 0]], - '01262' => [['5.0.947', '', 'expired', 0]], +# '01260' => [['5.0.945', '', 'toomanyconn', 0]], +# '01262' => [['5.0.947', '', 'expired', 0]], '01263' => [['4.4.1', '', 'networkerror', 0]], '01265' => [['5.0.0', '554', 'policyviolation', 0]], '01266' => [['4.7.0', '', 'policyviolation', 0]], '01267' => [['5.1.6', '550', 'hasmoved', 1]], - '01268' => [['5.7.1', '554', 'spamdetected', 0]], +# '01268' => [['5.7.1', '554', 'spamdetected', 0]], '01271' => [['5.1.1', '550', 'userunknown', 1]], '01272' => [['5.0.980', '554', 'spamdetected', 0]], '01273' => [['4.3.0', '', 'mailboxfull', 0]], '01274' => [['4.2.2', '', 'mailboxfull', 0]], - '01275' => [['5.0.971', '', 'virusdetected', 0]], - '01276' => [['5.0.910', '', 'filtered', 0]], +# '01275' => [['5.0.971', '', 'virusdetected', 0]], +# '01276' => [['5.0.910', '', 'filtered', 0]], '01277' => [['5.0.0', '550', 'rejected', 0], - ['4.0.0', '', 'expired', 0], + ['4.0.0', '', 'networkerror', 0], ['5.0.0', '550', 'filtered', 0]], '01278' => [['4.0.0', '', 'expired', 0]], '01279' => [['4.4.6', '', 'networkerror', 0]], - '01280' => [['5.4.0', '', 'networkerror', 0]], +# '01280' => [['5.4.0', '', 'networkerror', 0]], '01282' => [['5.1.1', '550', 'userunknown', 1]], - '01283' => [['5.0.947', '', 'expired', 0]], - '01284' => [['5.0.972', '', 'policyviolation', 0]], +# '01283' => [['5.0.947', '', 'expired', 0]], +# '01284' => [['5.0.972', '', 'policyviolation', 0]], '01285' => [['5.7.0', '554', 'spamdetected', 0]], - '01286' => [['5.5.0', '550', 'rejected', 0]], +# '01286' => [['5.5.0', '550', 'rejected', 0]], # TODO: '01287' => [['5.0.0', '550', 'filtered', 0]], - '01288' => [['5.3.0', '552', 'exceedlimit', 0]], + '01288' => [['5.3.4', '552', 'mesgtoobig', 0]], '01289' => [['4.0.0', '', 'notaccept', 0]], '01290' => [['4.3.0', '451', 'onhold', 0]], + '01300' => [['5.1.0', '550', 'userunknown', 1]], + '01301' => [['5.0.0', '', 'spamdetected', 0]], + '01302' => [['5.0.0', '550', 'filtered', 0]], + '01303' => [['5.0.0', '550', 'userunknown', 1]], + '01304' => [['4.0.0', '', 'notaccept', 0]], + '01305' => [['5.1.8', '501', 'rejected', 0]], + '01306' => [['4.0.0', '', 'networkerror', 0]], + '01307' => [['5.1.1', '550', 'userunknown', 1]], + '01308' => [['5.0.0', '', 'policyviolation', 0]], + '01309' => [['5.0.0', '553', 'systemerror', 0]], + '01310' => [['4.0.0', '', 'networkerror', 0]], + '01312' => [['5.1.1', '', 'userunknown', 1]], + '01314' => [['5.1.1', '550', 'userunknown', 1]], + '01315' => [['5.1.1', '550', 'userunknown', 1]], + '01318' => [['5.4.1', '550', 'rejected', 0]], + '01319' => [['5.0.0', '', 'userunknown', 1]], }; plan 'skip_all', sprintf("%s not found", $samplepath) unless -d $samplepath; diff --git a/xt/899-arf.t b/xt/899-arf.t index 60d863e09..65b2447c1 100644 --- a/xt/899-arf.t +++ b/xt/899-arf.t @@ -16,16 +16,16 @@ my $isexpected = { '01005' => [['', '', 'feedback', 0, 'abuse' ]], '01006' => [['', '', 'feedback', 0, 'abuse' ]], '01007' => [['', '', 'feedback', 0, 'abuse' ]], - '01008' => [['', '', 'feedback', 0, 'abuse' ]], +# '01008' => [['', '', 'feedback', 0, 'abuse' ]], '01009' => [['', '', 'feedback', 0, 'abuse' ]], '01010' => [['', '', 'feedback', 0, 'abuse' ]], '01011' => [['', '', 'feedback', 0, 'opt-out' ]], '01012' => [['', '', 'feedback', 0, 'abuse' ]], - '01013' => [['', '', 'feedback', 0, 'abuse' ]], - '01014' => [['', '', 'feedback', 0, 'abuse' ]], +# '01013' => [['', '', 'feedback', 0, 'abuse' ]], +# '01014' => [['', '', 'feedback', 0, 'abuse' ]], '01015' => [['', '', 'feedback', 0, 'abuse' ]], '01016' => [['', '', 'feedback', 0, 'auth-failure']], - '01017' => [['', '', 'feedback', 0, 'abuse' ]], +# '01017' => [['', '', 'feedback', 0, 'abuse' ]], }; plan 'skip_all', sprintf("%s not found", $samplepath) unless -d $samplepath;