File Coverage

lib/Sisimai/Lhost/Exchange2007.pm
Criterion Covered Total %
statement 80 82 97.5
branch 33 40 82.5
condition 18 20 90.0
subroutine 6 6 100.0
pod 2 2 100.0
total 139 150 92.6


line stmt bran cond sub pod time code
1             package Sisimai::Lhost::Exchange2007;
2 45     45   4997 use parent 'Sisimai::Lhost';
  45         117  
  45         394  
3 45     45   4032 use v5.26;
  45         155  
4 45     45   225 use strict;
  45         75  
  45         1211  
5 45     45   192 use warnings;
  45         95  
  45         57805  
6              
7 1     1 1 7 sub description { 'Microsoft Exchange Server 2007: https://www.microsoft.com/microsoft-365/exchange/email' }
8             sub inquire {
9             # Detect an error from Microsoft Exchange Server 2007
10             # @param [Hash] mhead Message headers of a bounce email
11             # @param [String] mbody Message body of a bounce email
12             # @return [Hash] Bounce data list and message/rfc822 part
13             # @return [undef] failed to decode or the arguments are missing
14             # @since v4.1.1
15 1062     1062 1 5621 my $class = shift;
16 1062   100     3548 my $mhead = shift // return undef;
17 1060   100     2965 my $mbody = shift // return undef;
18              
19 1058         2987 my $mailsender = ['postmaster@outlook.com', ".onmicrosoft.com"];
20 1058         3831 my $emailtitle = [
21             # "Subject:" "Content-Language:"
22             "Undeliverable", # en-US
23             "Non_remis_", # fr-FR
24             "Non remis ", # fr-FR
25             "Non recapitabile", # it-CH
26             "Olevererbart", # sv-SE
27             ];
28 1058 100       1703 my $proceedsto = 0; $proceedsto++ if grep { index($mhead->{"subject"}, $_) > -1 } @$emailtitle;
  1058         4221  
  5290         11998  
29 1058 100       2173 $proceedsto++ if grep { index($mhead->{"from"}, $_) > 1 } @$mailsender;
  2116         6037  
30 1058 100       4128 $proceedsto++ if defined $mhead->{"content-language"};
31 1058 100       5166 return undef if $proceedsto < 2;
32              
33 108         677 require Sisimai::RFC1123;
34 108         322 state $indicators = __PACKAGE__->INDICATORS;
35 108         226 state $boundaries = [
36             "Original Message Headers",
37             "Original message headers:", # en-US
38             "tes de message d'origine :", # fr-FR/En-têtes de message d'origine
39             "Intestazioni originali del messaggio:", # it-CH
40             "Ursprungshuvuden:", # sv-SE
41             ];
42 108         262 state $startingof = {
43             "error" => [" RESOLVER.", " QUEUE."],
44             "message" => [
45             "Error Details",
46             "Diagnostic information for administrators:", # en-US
47             "Informations de diagnostic pour les administrateurs", # fr-FR
48             "Informazioni di diagnostica per gli amministratori", # it-CH
49             "Diagnostisk information f", # sv-SE
50             ],
51             "rhost" => [
52             "DSN generated by:",
53             "Generating server", # en-US
54             "Serveur de g", # fr-FR/Serveur de gènèration
55             "Server di generazione", # it-CH
56             "Genererande server", # sv-SE
57             ],
58             };
59 108         295 state $ndrsubject = {
60             "SMTPSEND.DNS.NonExistentDomain" => "hostunknown", # 554 5.4.4 SMTPSEND.DNS.NonExistentDomain
61             "SMTPSEND.DNS.MxLoopback" => "networkerror", # 554 5.4.4 SMTPSEND.DNS.MxLoopback
62             "RESOLVER.ADR.BadPrimary" => "systemerror", # 550 5.2.0 RESOLVER.ADR.BadPrimary
63             "RESOLVER.ADR.RecipNotFound" => "userunknown", # 550 5.1.1 RESOLVER.ADR.RecipNotFound
64             "RESOLVER.ADR.RecipientNotFound" => "userunknown", # 550 5.1.1 RESOLVER.ADR.RecipientNotFound
65             "RESOLVER.ADR.ExRecipNotFound" => "userunknown", # 550 5.1.1 RESOLVER.ADR.ExRecipNotFound
66             "RESOLVER.ADR.RecipLimit" => "ratelimited", # 550 5.5.3 RESOLVER.ADR.RecipLimit
67             "RESOLVER.ADR.InvalidInSmtp" => "systemerror", # 550 5.1.0 RESOLVER.ADR.InvalidInSmtp
68             "RESOLVER.ADR.Ambiguous" => "systemerror", # 550 5.1.4 RESOLVER.ADR.Ambiguous, 420 4.2.0 RESOLVER.ADR.Ambiguous
69             "RESOLVER.RST.AuthRequired" => "securityerror", # 550 5.7.1 RESOLVER.RST.AuthRequired
70             "RESOLVER.RST.NotAuthorized" => "rejected", # 550 5.7.1 RESOLVER.RST.NotAuthorized
71             "RESOLVER.RST.RecipSizeLimit" => "emailtoolarge", # 550 5.2.3 RESOLVER.RST.RecipSizeLimit
72             "QUEUE.Expired" => "expired", # 550 4.4.7 QUEUE.Expired
73             };
74              
75 108         655 my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; my $v = undef;
  108         276  
76 108         666 my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries);
77 108         276 my $readcursor = 0; # (Integer) Points the current cursor position
78 108         217 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
79              
80 108         1432 for my $e ( split("\n", $emailparts->[0]) ) {
81             # Read error messages and delivery status lines from the head of the email to the previous
82             # line of the beginning of the original message.
83 3131 100       5358 unless( $readcursor ) {
84             # Beginning of the bounce message or message/delivery-status part
85 2436 100       3505 $readcursor |= $indicators->{"deliverystatus"} if grep { index($e, $_) == 0 } $startingof->{"message"}->@*;
  12180         20507  
86 2436         3346 next;
87             }
88 695 100 66     3184 next if ($readcursor & $indicators->{'deliverystatus'}) == 0 || $e eq "";
89              
90             # Diagnostic information for administrators:
91             #
92             # Generating server: mta2.neko.example.jp
93             #
94             # kijitora@example.jp
95             # #550 5.1.1 RESOLVER.ADR.RecipNotFound; not found ##
96             #
97             # Original message headers:
98 474         926 $v = $dscontents->[-1];
99              
100 474 100 100     1844 if( index($e, " ") < 0 && index($e, '@') > 1 ) {
101             # kijitora@example.jp
102 97 50       318 if( $v->{"recipient"} ) {
103             # There are multiple recipient addresses in the message body.
104 0         0 push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
105 0         0 $v = $dscontents->[-1];
106             }
107 97         875 $v->{"recipient"} = Sisimai::Address->s3s4($e);
108 97         253 $recipients++;
109              
110             } else {
111             # Try to pick the remote hostname and status code, reply code from the error message
112 377 100       1048 if( grep { index($e, $_) == 0 } $startingof->{"rhost"}->@* ) {
  1885         3929  
113             # Generating server: SG2APC01HT234.mail.protection.outlook.com
114             # DSN generated by: NEKONYAAN0022.apcprd01.prod.exchangelabs.com
115 108         1206 my $cv = Sisimai::RFC1123->find($e);
116 108 50       461 $v->{"rhost"} = $cv if Sisimai::RFC1123->is_internethost($cv);
117              
118             } else {
119             # #550 5.1.1 RESOLVER.ADR.RecipNotFound; not found ##
120             # #550 5.2.3 RESOLVER.RST.RecipSizeLimit; message too large for this recipient ##
121 269   100     1852 my $cr = Sisimai::SMTP::Reply->find($e) || "";
122 269   100     1623 my $cs = Sisimai::SMTP::Status->find($e) || "";
123 269 100 100     1655 if( $cr ne "" || $cs ne "" || index($e, "Remote Server ") > -1 ) {
      66        
124             # Remote Server returned '550 5.1.1 RESOLVER.ADR.RecipNotFound; not found'
125             # 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'
126 123         380 $v->{"replycode"} = $cr;
127 123         256 $v->{"status"} = $cs;
128 123         569 $v->{"diagnosis"} .= $e." ";
129             }
130             }
131             }
132             }
133              
134 108         899 while( $recipients == 0 ) {
135             # Try to pick the recipient address from the following formatted bounce message:
136             #
137             # Original Message Details
138             # Created Date: 4/29/2017 11:23:34 PM
139             # Sender Address: neko@example.com
140             # Recipient Address: kijitora-nyaan@neko.kyoto.example.jp
141             # Subject: Nyaan?
142 11 50       83 my $p1 = index($emailparts->[0], "Original Message Details"); last if $p1 < 0;
  11         49  
143 11 50       76 my $p2 = index($emailparts->[0], "\nRecipient Address: "); last if $p2 < 0;
  11         49  
144 11 50       36 my $p3 = index($emailparts->[0], "\n", $p2 + 20); last if $p3 < 0;
  11         44  
145 11         170 my $cv = Sisimai::Address->s3s4(substr($emailparts->[0], $p2 + 20, $p3 - $p2 - 20));
146              
147 11 50       77 last unless Sisimai::Address->is_emailaddress($cv);
148 11         61 $dscontents->[0]->{"recipient"} = $cv;
149 11         49 $recipients++;
150             }
151 108 50       306 return undef unless $recipients;
152              
153 108         312 for my $e ( @$dscontents ) {
154             # Tidy up the error message in $e->{'diagnosis'}, Try to detect the bounce reason.
155 108         543 $e->{"diagnosis"} = Sisimai::String->sweep($e->{"diagnosis"});
156              
157 108         296 my $p0 = -1; for my $r ( $startingof->{"error"}->@* ) {
  108         366  
158             # Try to find the NDR subject string such as "RESOLVER.ADR.RecipientNotFound" from the
159             # error message
160 168 100       389 $p0 = index($e->{"diagnosis"}, $r); last if $p0 > -1;
  168         448  
161             }
162 108 100       285 next if $p0 < 0;
163              
164 53         242 my $cv = substr($e->{"diagnosis"}, $p0 + 1, index($e->{"diagnosis"}, ";") - $p0 - 1);
165 53         480 for my $r ( keys %$ndrsubject ) {
166             # Try to match with error subject strings such as "RESOLVER.ADR.RecipNotFound"
167 381 100       750 next unless $cv eq $r;
168 53         174 $e->{"reason"} = $ndrsubject->{ $r };
169 53         184 last;
170             }
171             }
172 108         995 return {"ds" => $dscontents, "rfc822" => $emailparts->[1]};
173             }
174              
175             1;
176             __END__