File Coverage

lib/Sisimai/Lhost/qmail.pm
Criterion Covered Total %
statement 93 95 97.8
branch 55 60 91.6
condition 12 15 80.0
subroutine 6 6 100.0
pod 2 2 100.0
total 168 178 94.3


line stmt bran cond sub pod time code
1             package Sisimai::Lhost::qmail;
2 19     19   6499 use parent 'Sisimai::Lhost';
  19         52  
  19         131  
3 19     19   1149 use feature ':5.10';
  19         31  
  19         1284  
4 19     19   122 use strict;
  19         39  
  19         361  
5 19     19   150 use warnings;
  19         47  
  19         31329  
6              
7 2     2 1 1345 sub description { 'qmail' }
8             sub make {
9             # Detect an error from qmail
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 parse or the arguments are missing
14             # @since v4.0.0
15 280     280 1 750 my $class = shift;
16 280   100     733 my $mhead = shift // return undef;
17 279   50     579 my $mbody = shift // return undef;
18 279         352 my $match = 0;
19 279         1049 my $tryto = qr/\A[(]qmail[ ]+\d+[ ]+invoked[ ]+(?:for[ ]+bounce|from[ ]+network)[)]/;
20              
21             # Pre process email headers and the body part of the message which generated
22             # by qmail, see https://cr.yp.to/qmail.html
23             # e.g.) Received: (qmail 12345 invoked for bounce); 29 Apr 2009 12:34:56 -0000
24             # Subject: failure notice
25 279 100 50     1167 $match ||= 1 if $mhead->{'subject'} eq 'failure notice';
26 279 100 100     396 $match ||= 1 if grep { $_ =~ $tryto } @{ $mhead->{'received'} };
  438         2505  
  279         719  
27 279 100       1113 return undef unless $match;
28              
29 83         203 state $indicators = __PACKAGE__->INDICATORS;
30 83         171 state $rebackbone = qr|^--- Below this line is a copy of the message[.]|m;
31 83         131 state $startingof = {
32             # qmail-remote.c:248| if (code >= 500) {
33             # qmail-remote.c:249| out("h"); outhost(); out(" does not like recipient.\n");
34             # qmail-remote.c:265| if (code >= 500) quit("D"," failed on DATA command");
35             # qmail-remote.c:271| if (code >= 500) quit("D"," failed after I sent the message");
36             #
37             # Characters: K,Z,D in qmail-qmqpc.c, qmail-send.c, qmail-rspawn.c
38             # K = success, Z = temporary error, D = permanent error
39             'message' => ['Hi. This is the qmail'],
40             'error' => ['Remote host said:'],
41             };
42              
43 83         202 state $resmtp = {
44             # Error text regular expressions which defined in qmail-remote.c
45             # qmail-remote.c:225| if (smtpcode() != 220) quit("ZConnected to "," but greeting failed");
46             'conn' => qr/(?:Error:)?Connected to [^ ]+ but greeting failed[.]/,
47             # qmail-remote.c:231| if (smtpcode() != 250) quit("ZConnected to "," but my name was rejected");
48             'ehlo' => qr/(?:Error:)?Connected to [^ ]+ but my name was rejected[.]/,
49             # qmail-remote.c:238| if (code >= 500) quit("DConnected to "," but sender was rejected");
50             # reason = rejected
51             'mail' => qr/(?:Error:)?Connected to [^ ]+ but sender was rejected[.]/,
52             # qmail-remote.c:249| out("h"); outhost(); out(" does not like recipient.\n");
53             # qmail-remote.c:253| out("s"); outhost(); out(" does not like recipient.\n");
54             # reason = userunknown
55             'rcpt' => qr/(?:Error:)?[^ ]+ does not like recipient[.]/,
56             # qmail-remote.c:265| if (code >= 500) quit("D"," failed on DATA command");
57             # qmail-remote.c:266| if (code >= 400) quit("Z"," failed on DATA command");
58             # qmail-remote.c:271| if (code >= 500) quit("D"," failed after I sent the message");
59             # qmail-remote.c:272| if (code >= 400) quit("Z"," failed after I sent the message");
60             'data' => qr{(?:
61             (?:Error:)?[^ ]+[ ]failed[ ]on[ ]DATA[ ]command[.]
62             |(?:Error:)?[^ ]+[ ]failed[ ]after[ ]I[ ]sent[ ]the[ ]message[.]
63             )
64             }x,
65             };
66 83         122 state $rehost = qr{(?:
67             # qmail-remote.c:261| if (!flagbother) quit("DGiving up on ","");
68             Giving[ ]up[ ]on[ ]([^ ]+[0-9a-zA-Z])[.]?\z
69             |Connected[ ]to[ ]([-0-9a-zA-Z.]+[0-9a-zA-Z])[ ]
70             |remote[ ]host[ ]([-0-9a-zA-Z.]+[0-9a-zA-Z])[ ]said:
71             )
72             }x;
73              
74             # qmail-send.c:922| ... (&dline[c],"I'm not going to try again; this message has been in the queue too long.\n")) nomem();
75 83         107 state $hasexpired = 'this message has been in the queue too long.';
76             # qmail-remote-fallback.patch
77 83         118 state $recommands = qr/Sorry, no SMTP connection got far enough; most progress was ([A-Z]{4}) /;
78 83         111 state $reisonhold = qr/\A[^ ]+ does not like recipient[.][ \t]+.+this message has been in the queue too long[.]\z/;
79 83         154 state $failonldap = {
80             # qmail-ldap-1.03-20040101.patch:19817 - 19866
81             'suspend' => ['Mailaddress is administrative?le?y disabled'], # 5.2.1
82             'userunknown' => ['Sorry, no mailbox here by that name'], # 5.1.1
83             'exceedlimit' => ['The message exeeded the maximum size the user accepts'], # 5.2.3
84             'systemerror' => [
85             'Automatic homedir creator crashed', # 4.3.0
86             'Illegal value in LDAP attribute', # 5.3.5
87             'LDAP attribute is not given but mandatory', # 5.3.5
88             'Timeout while performing search on LDAP server', # 4.4.3
89             'Too many results returned but needs to be unique', # 5.3.5
90             'Permanent error while executing qmail-forward', # 5.4.4
91             'Temporary error in automatic homedir creation', # 4.3.0 or 5.3.0
92             'Temporary error while executing qmail-forward', # 4.4.4
93             'Temporary failure in LDAP lookup', # 4.4.3
94             'Unable to contact LDAP server', # 4.4.3
95             'Unable to login into LDAP server, bad credentials',# 4.4.3
96             ],
97             };
98 83         153 state $messagesof = {
99             # qmail-local.c:589| strerr_die1x(100,"Sorry, no mailbox here by that name. (#5.1.1)");
100             # qmail-remote.c:253| out("s"); outhost(); out(" does not like recipient.\n");
101             'userunknown' => [
102             'no mailbox here by that name',
103             'does not like recipient.',
104             ],
105             # error_str.c:192| X(EDQUOT,"disk quota exceeded")
106             'mailboxfull' => ['disk quota exceeded'],
107             # qmail-qmtpd.c:233| ... result = "Dsorry, that message size exceeds my databytes limit (#5.3.4)";
108             # qmail-smtpd.c:391| ... out("552 sorry, that message size exceeds my databytes limit (#5.3.4)\r\n"); return;
109             'mesgtoobig' => ['Message size exceeds fixed maximum message size:'],
110             # qmail-remote.c:68| Sorry, I couldn't find any host by that name. (#4.1.2)\n"); zerodie();
111             # qmail-remote.c:78| Sorry, I couldn't find any host named ");
112             'hostunknown' => ["Sorry, I couldn't find any host "],
113             'systemfull' => ['Requested action not taken: mailbox unavailable (not enough free space)'],
114             'systemerror' => [
115             'bad interpreter: No such file or directory',
116             'system error',
117             'Unable to',
118             ],
119             'networkerror'=> [
120             "Sorry, I wasn't able to establish an SMTP connection",
121             "Sorry, I couldn't find a mail exchanger or IP address",
122             "Sorry. Although I'm listed as a best-preference MX or A for that host",
123             ],
124             };
125              
126 83         283 my $dscontents = [__PACKAGE__->DELIVERYSTATUS];
127 83         589 my $emailsteak = Sisimai::RFC5322->fillet($mbody, $rebackbone);
128 83         259 my $readcursor = 0; # (Integer) Points the current cursor position
129 83         156 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
130 83         132 my $v = undef;
131              
132 83         651 for my $e ( split("\n", $emailsteak->[0]) ) {
133             # Read error messages and delivery status lines from the head of the email
134             # to the previous line of the beginning of the original message.
135 1130 100       1593 unless( $readcursor ) {
136             # Beginning of the bounce message or message/delivery-status part
137 745 100       1406 $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0;
138 745         842 next;
139             }
140 385 50       710 next unless $readcursor & $indicators->{'deliverystatus'};
141 385 100       569 next unless length $e;
142              
143             # :
144             # 192.0.2.153 does not like recipient.
145             # Remote host said: 550 5.1.1 ... User Unknown
146             # Giving up on 192.0.2.153.
147 330         370 $v = $dscontents->[-1];
148              
149 330 100       1161 if( $e =~ /\A(?:To[ ]*:)?[<](.+[@].+)[>]:[ \t]*\z/ ) {
    100          
150             # :
151 60 100       218 if( $v->{'recipient'} ) {
152             # There are multiple recipient addresses in the message body.
153 5         37 push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
154 5         28 $v = $dscontents->[-1];
155             }
156 60         189 $v->{'recipient'} = $1;
157 60         125 $recipients++;
158              
159             } elsif( scalar @$dscontents == $recipients ) {
160             # Append error message
161 160 50       325 next unless length $e;
162 160         398 $v->{'diagnosis'} .= $e.' ';
163 160 100       502 $v->{'alterrors'} = $e if index($e, $startingof->{'error'}->[0]) == 0;
164              
165 160 100       275 next if $v->{'rhost'};
166 155 100       1173 $v->{'rhost'} = $1 if $e =~ $rehost;
167             }
168             }
169 83 100       493 return undef unless $recipients;
170              
171 55         167 for my $e ( @$dscontents ) {
172 60         363 $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'});
173              
174 60 50       371 if( ! $e->{'command'} ) {
175             # Get the SMTP command name for the session
176 60         280 SMTP: for my $r ( keys %$resmtp ) {
177             # Verify each regular expression of SMTP commands
178 229 100       3294 next unless $e->{'diagnosis'} =~ $resmtp->{ $r };
179 39         166 $e->{'command'} = uc $r;
180 39         90 last;
181             }
182              
183 60 100       254 unless( $e->{'command'} ) {
184             # Verify each regular expression of patches
185 21 100       217 $e->{'command'} = uc $1 if $e->{'diagnosis'} =~ $recommands;
186             }
187             }
188              
189             # Detect the reason of bounce
190 60 100 66     353 if( $e->{'command'} eq 'MAIL' ) {
    100          
191             # MAIL | Connected to 192.0.2.135 but sender was rejected.
192 5         13 $e->{'reason'} = 'rejected';
193              
194             } elsif( $e->{'command'} eq 'HELO' || $e->{'command'} eq 'EHLO' ) {
195             # HELO | Connected to 192.0.2.135 but my name was rejected.
196 5         17 $e->{'reason'} = 'blocked';
197              
198             } else {
199             # Try to match with each error message in the table
200 50 100       273 if( $e->{'diagnosis'} =~ $reisonhold ) {
201             # To decide the reason require pattern match with
202             # Sisimai::Reason::* modules
203 5         18 $e->{'reason'} = 'onhold';
204              
205             } else {
206 45         177 SESSION: for my $r ( keys %$messagesof ) {
207             # Verify each regular expression of session errors
208 269 100       473 if( $e->{'alterrors'} ) {
209             # Check the value of "alterrors"
210 163 100       161 next unless grep { index($e->{'alterrors'}, $_) > -1 } @{ $messagesof->{ $r } };
  283         863  
  163         264  
211 5         28 $e->{'reason'} = $r;
212             }
213 111 100       201 last if $e->{'reason'};
214              
215 106 100       115 next unless grep { index($e->{'diagnosis'}, $_) > -1 } @{ $messagesof->{ $r } };
  196         605  
  106         205  
216 10         39 $e->{'reason'} = $r;
217 10         21 last;
218             }
219              
220 45 100       211 unless( $e->{'reason'} ) {
221 30         104 LDAP: for my $r ( keys %$failonldap ) {
222             # Verify each regular expression of LDAP errors
223 120 50       172 next unless grep { index($e->{'diagnosis'}, $_) > -1 } @{ $failonldap->{ $r } };
  420         1021  
  120         216  
224 0         0 $e->{'reason'} = $r;
225 0         0 last;
226             }
227             }
228              
229 45 100       173 unless( $e->{'reason'} ) {
230 30 50       127 $e->{'reason'} = 'expired' if index($e->{'diagnosis'}, $hasexpired) > -1;
231             }
232             }
233             }
234 60   100     207 $e->{'command'} ||= '';
235 60   100     327 $e->{'status'} = Sisimai::SMTP::Status->find($e->{'diagnosis'}) || '';
236             }
237 55         483 return { 'ds' => $dscontents, 'rfc822' => $emailsteak->[1] };
238             }
239              
240             1;
241             __END__