File Coverage

lib/Sisimai/Lhost/MXLogic.pm
Criterion Covered Total %
statement 77 85 90.5
branch 40 56 71.4
condition 12 22 54.5
subroutine 6 6 100.0
pod 2 2 100.0
total 137 171 80.1


line stmt bran cond sub pod time code
1             package Sisimai::Lhost::MXLogic;
2 15     15   5458 use parent 'Sisimai::Lhost';
  15         23  
  15         115  
3 15     15   783 use feature ':5.10';
  15         25  
  15         896  
4 15     15   75 use strict;
  15         21  
  15         283  
5 15     15   64 use warnings;
  15         30  
  15         16666  
6              
7             # Based on Sisimai::Lhost::Exim
8 2     2 1 1016 sub description { 'McAfee SaaS' }
9             sub make {
10             # Detect an error from MXLogic
11             # @param [Hash] mhead Message headers of a bounce email
12             # @param [String] mbody Message body of a bounce email
13             # @return [Hash] Bounce data list and message/rfc822 part
14             # @return [Undef] failed to parse or the arguments are missing
15             # @since v4.1.1
16 203     203 1 586 my $class = shift;
17 203   100     417 my $mhead = shift // return undef;
18 202   50     404 my $mbody = shift // return undef;
19 202         229 my $match = 0;
20              
21             # X-MX-Bounce: mta/src/queue/bounce
22             # X-MXL-NoteHash: ffffffffffffffff-0000000000000000000000000000000000000000
23             # X-MXL-Hash: 4c9d4d411993da17-bbd4212b6c887f6c23bab7db4bd87ef5edc00758
24 202 100 50     535 $match ||= 1 if defined $mhead->{'x-mx-bounce'};
25 202 50 0     573 $match ||= 1 if defined $mhead->{'x-mxl-hash'};
26 202 100 50     469 $match ||= 1 if defined $mhead->{'x-mxl-notehash'};
27 202 100 50     721 $match ||= 1 if index($mhead->{'from'}, 'Mail Delivery System') == 0;
28 202 100 100     1539 $match ||= 1 if $mhead->{'subject'} =~ qr{(?:
29             Mail[ ]delivery[ ]failed(:[ ]returning[ ]message[ ]to[ ]sender)?
30             |Warning:[ ]message[ ][^ ]+[ ]delayed[ ]+
31             |Delivery[ ]Status[ ]Notification
32             )
33             }x;
34 202 100       713 return undef unless $match;
35              
36 40         91 state $indicators = __PACKAGE__->INDICATORS;
37 40         79 state $rebackbone = qr|^Included is a copy of the message header:|m;
38 40         51 state $startingof = { 'message' => ['This message was created automatically by mail delivery software.'] };
39 40         60 state $recommands = [
40             qr/SMTP error from remote (?:mail server|mailer) after ([A-Za-z]{4})/,
41             qr/SMTP error from remote (?:mail server|mailer) after end of ([A-Za-z]{4})/,
42             ];
43 40         99 state $messagesof = {
44             'userunknown' => ['user not found'],
45             'hostunknown' => [
46             'all host address lookups failed permanently',
47             'all relevant MX records point to non-existent hosts',
48             'Unrouteable address',
49             ],
50             'mailboxfull' => [
51             'mailbox is full',
52             'error: quota exceed',
53             ],
54             'notaccept' => [
55             'an MX or SRV record indicated no SMTP service',
56             'no host found for existing SMTP connection',
57             ],
58             'syntaxerror' => [
59             'angle-brackets nested too deep',
60             'expected word or "<"',
61             'domain missing in source-routed address',
62             'malformed address:',
63             ],
64             'systemerror' => [
65             'delivery to file forbidden',
66             'delivery to pipe forbidden',
67             'local delivery failed',
68             'LMTP error after ',
69             ],
70             'contenterror' => ['Too many "Received" headers'],
71             };
72 40         83 state $delayedfor = [
73             'retry timeout exceeded',
74             'No action is required on your part',
75             'retry time not reached for any host after a long failure period',
76             'all hosts have been failing for a long time and were last tried',
77             'Delay reason: ',
78             'has been frozen',
79             'was frozen on arrival by ',
80             ];
81              
82 40         129 my $dscontents = [__PACKAGE__->DELIVERYSTATUS];
83 40         193 my $emailsteak = Sisimai::RFC5322->fillet($mbody, $rebackbone);
84 40         135 my $readcursor = 0; # (Integer) Points the current cursor position
85 40         60 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
86 40         66 my $localhost0 = ''; # (String) Local MTA
87 40         53 my $v = undef;
88              
89 40         479 for my $e ( split("\n", $emailsteak->[0]) ) {
90             # Read error messages and delivery status lines from the head of the email
91             # to the previous line of the beginning of the original message.
92 1512 100       1846 unless( $readcursor ) {
93             # Beginning of the bounce message or message/delivery-status part
94 1427 100       1801 $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0;
95 1427         1236 next;
96             }
97 85 50       133 next unless $readcursor & $indicators->{'deliverystatus'};
98 85 100       115 next unless length $e;
99              
100             # This message was created automatically by mail delivery software.
101             #
102             # A message that you sent could not be delivered to one or more of its
103             # recipients. This is a permanent error. The following address(es) failed:
104             #
105             # kijitora@example.jp
106             # SMTP error from remote mail server after RCPT TO::
107             # host neko.example.jp [192.0.2.222]: 550 5.1.1 ... User Unknown
108 53         61 $v = $dscontents->[-1];
109              
110 53 100       215 if( $e =~ /\A[ \t]*[<]([^ ]+[@][^ ]+)[>]:(.+)\z/ ) {
    100          
111             # A message that you have sent could not be delivered to one or more
112             # recipients. This is a permanent error. The following address failed:
113             #
114             # : 550 5.1.1 ...
115 16 50       56 if( $v->{'recipient'} ) {
116             # There are multiple recipient addresses in the message body.
117 0         0 push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
118 0         0 $v = $dscontents->[-1];
119             }
120 16         50 $v->{'recipient'} = $1;
121 16         48 $v->{'diagnosis'} = $2;
122 16         29 $recipients++;
123              
124             } elsif( scalar @$dscontents == $recipients ) {
125             # Error message
126 5 50       16 next unless length $e;
127 5         19 $v->{'diagnosis'} .= $e.' ';
128             }
129             }
130 40 100       270 return undef unless $recipients;
131              
132 16 50       21 if( scalar @{ $mhead->{'received'} } ) {
  16         54  
133             # Get the name of local MTA
134             # Received: from marutamachi.example.org (c192128.example.net [192.0.2.128])
135 16 100       81 $localhost0 = $1 if $mhead->{'received'}->[-1] =~ /from[ \t]([^ ]+) /;
136             }
137              
138 16         37 for my $e ( @$dscontents ) {
139             # Set default values if each value is empty.
140 16   66     77 $e->{'lhost'} ||= $localhost0;
141 16         37 $e->{'diagnosis'} =~ s/[-]{2}.*\z//g;
142 16         117 $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'});
143              
144 16 50       66 unless( $e->{'rhost'} ) {
145             # Get the remote host name
146             # host neko.example.jp [192.0.2.222]: 550 5.1.1 ... User Unknown
147 16 50       70 $e->{'rhost'} = $1 if $e->{'diagnosis'} =~ /host[ \t]+([^ \t]+)[ \t]\[.+\]:[ \t]/;
148              
149 16 50       44 unless( $e->{'rhost'} ) {
150 16 50       29 if( scalar @{ $mhead->{'received'} } ) {
  16         44  
151             # Get localhost and remote host name from Received header.
152 16         19 $e->{'rhost'} = pop @{ Sisimai::RFC5322->received($mhead->{'received'}->[-1]) };
  16         66  
153             }
154             }
155             }
156              
157 16 50       54 unless( $e->{'command'} ) {
158             # Get the SMTP command name for the session
159 16         35 SMTP: for my $r ( @$recommands ) {
160             # Verify each regular expression of SMTP commands
161 32 50       144 next unless $e->{'diagnosis'} =~ $r;
162 0         0 $e->{'command'} = uc $1;
163 0         0 last;
164             }
165              
166             # Detect the reason of bounce
167 16 50 33     89 if( $e->{'command'} eq 'MAIL' ) {
    50          
168             # MAIL | Connected to 192.0.2.135 but sender was rejected.
169 0         0 $e->{'reason'} = 'rejected';
170              
171             } elsif( $e->{'command'} eq 'HELO' || $e->{'command'} eq 'EHLO' ) {
172             # HELO | Connected to 192.0.2.135 but my name was rejected.
173 0         0 $e->{'reason'} = 'blocked';
174              
175             } else {
176             # Verify each regular expression of session errors
177 16         58 SESSION: for my $r ( keys %$messagesof ) {
178             # Check each regular expression
179 112 50       109 next unless grep { index($e->{'diagnosis'}, $_) > -1 } @{ $messagesof->{ $r } };
  272         537  
  112         144  
180 0         0 $e->{'reason'} = $r;
181 0         0 last;
182             }
183              
184 16 50       42 unless( $e->{'reason'} ) {
185             # The reason "expired"
186 16 50       26 $e->{'reason'} = 'expired' if grep { index($e->{'diagnosis'}, $_) > -1 } @$delayedfor;
  112         195  
187             }
188             }
189             }
190 16   50     84 $e->{'command'} ||= '';
191             }
192 16         81 return { 'ds' => $dscontents, 'rfc822' => $emailsteak->[1] };
193             }
194              
195             1;
196             __END__