File Coverage

lib/Sisimai/Lhost/qmail.pm
Criterion Covered Total %
statement 88 90 97.7
branch 44 46 95.6
condition 15 21 71.4
subroutine 6 6 100.0
pod 2 2 100.0
total 155 165 93.9


line stmt bran cond sub pod time code
1             package Sisimai::Lhost::qmail;
2 42     42   6455 use parent 'Sisimai::Lhost';
  42         90  
  42         322  
3 42     42   4114 use v5.26;
  42         151  
4 42     42   239 use strict;
  42         82  
  42         1120  
5 42     42   175 use warnings;
  42         72  
  42         62338  
6              
7 1     1 1 10 sub description { 'qmail: https://cr.yp.to/qmail.html' }
8             sub inquire {
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 decode or the arguments are missing
14             # @since v4.0.0
15 1129     1129 1 8544 my $class = shift;
16 1129   100     6580 my $mhead = shift // return undef;
17 1126   100     4391 my $mbody = shift // return undef;
18              
19             # Pre process email headers and the body part of the message which generated by qmail.
20             # see https://cr.yp.to/qmail.html
21             # e.g.) Received: (qmail 12345 invoked for bounce); 29 Apr 2009 12:34:56 -0000
22             # Subject: failure notice
23 1123         3393 my $emailtitle = [
24             "failure notice", # qmail-send.c:Subject: failure notice\n\
25             "Failure Notice", # Yahoo
26             ];
27 1123 100       1943 my $proceedsto = 0; $proceedsto++ if grep { $mhead->{"subject"} eq $_ } @$emailtitle;
  1123         2840  
  2246         6591  
28              
29 1123         3698 for my $e ( $mhead->{"received"}->@* ) {
30             # Received: (qmail 2222 invoked for bounce);29 Apr 2017 23:34:45 +0900
31             # Received: (qmail 2202 invoked from network); 29 Apr 2018 00:00:00 +0900
32 2154 100 50     11205 $proceedsto ||= 1 if Sisimai::String->aligned(\$e, ["(qmail", " invoked "]);
33             }
34 1123 100       8673 return undef if $proceedsto == 0;
35              
36 242         1102 require Sisimai::SMTP::Command;
37 242         539 state $indicators = __PACKAGE__->INDICATORS;
38 242         379 state $boundaries = [
39             # qmail-send.c:qmail_puts(&qqt,*sender.s ? "--- Below this line is a copy of the message.\n\n" :...
40             "--- Below this line is a copy of the message.", # qmail-1.03
41             "--- Below this line is a copy of the mail header.",
42             "--- Below the next line is a copy of the message.", # The followings are the qmail clone
43             "--- Mensaje original adjunto.",
44             "Content-Type: message/rfc822",
45             "Original message follows.",
46             ];
47 242         776 state $startingof = {
48             # qmail-remote.c:248| if (code >= 500) {
49             # qmail-remote.c:249| out("h"); outhost(); out(" does not like recipient.\n");
50             # qmail-remote.c:265| if (code >= 500) quit("D"," failed on DATA command");
51             # qmail-remote.c:271| if (code >= 500) quit("D"," failed after I sent the message");
52             #
53             # Characters: K,Z,D in qmail-qmqpc.c, qmail-send.c, qmail-rspawn.c
54             # K = success, Z = temporary error, D = permanent error
55             "error" => ["Remote host said:"],
56             "message" => [
57             "Hi. This is the qmail", # qmail-send.c:Hi. This is the qmail-send program at ");
58             "He/Her is not ", # The followings are the qmail clone
59             "unable to deliver your message to the following addresses",
60             "Su mensaje no pudo ser entregado",
61             "Sorry, we were unable to deliver your message to the following address",
62             "This is the machine generated message from mail service",
63             "This is the mail delivery agent at",
64             "Unable to deliver message to the following address",
65             "unable to deliver your message to the following addresses",
66             "Unfortunately, your mail was not delivered to the following address:",
67             "Your mail message to the following address",
68             "Your message to the following addresses",
69             "We're sorry.",
70             ],
71             "rhost" => ['Giving up on ', 'Connected to ', 'remote host '],
72             };
73 242         461 state $commandset = {
74             # Error text regular expressions which defined in qmail-remote.c
75             # qmail-remote.c:225| if (smtpcode() != 220) quit("ZConnected to "," but greeting failed");
76             "CONN" => [" but greeting failed."],
77             # qmail-remote.c:231| if (smtpcode() != 250) quit("ZConnected to "," but my name was rejected");
78             "EHLO" => [" but my name was rejected."],
79             # qmail-remote.c:238| if (code >= 500) quit("DConnected to "," but sender was rejected");
80             # reason = rejected
81             "MAIL" => [" but sender was rejected."],
82             # qmail-remote.c:249| out("h"); outhost(); out(" does not like recipient.\n");
83             # qmail-remote.c:253| out("s"); outhost(); out(" does not like recipient.\n");
84             # reason = userunknown
85             "RCPT" => [" does not like recipient."],
86             # qmail-remote.c:265| if (code >= 500) quit("D"," failed on DATA command");
87             # qmail-remote.c:266| if (code >= 400) quit("Z"," failed on DATA command");
88             # qmail-remote.c:271| if (code >= 500) quit("D"," failed after I sent the message");
89             # qmail-remote.c:272| if (code >= 400) quit("Z"," failed after I sent the message");
90             "DATA" => [" failed on DATA command", " failed after I sent the message"],
91             };
92              
93             # qmail-send.c:922| ... (&dline[c],"I'm not going to try again; this message has been in the queue too long.\n")) nomem();
94             # qmail-remote-fallback.patch
95 242         450 state $hasexpired = "this message has been in the queue too long.";
96 242         329 state $onholdpair = [" does not like recipient.", "this message has been in the queue too long."];
97 242         601 state $failonldap = {
98             # qmail-ldap-1.03-20040101.patch:19817 - 19866
99             "emailtoolarge" => ["The message exeeded the maximum size the user accepts"], # 5.2.3
100             "userunknown" => ["Sorry, no mailbox here by that name"], # 5.1.1
101             "suspend" => [ # 5.2.1
102             "Mailaddress is administrativly disabled",
103             "Mailaddress is administrativley disabled",
104             "Mailaddress is administratively disabled",
105             "Mailaddress is administrativeley disabled",
106             ],
107             "systemerror" => [
108             "Automatic homedir creator crashed", # 4.3.0
109             "Illegal value in LDAP attribute", # 5.3.5
110             "LDAP attribute is not given but mandatory", # 5.3.5
111             "Timeout while performing search on LDAP server", # 4.4.3
112             "Too many results returned but needs to be unique", # 5.3.5
113             "Permanent error while executing qmail-forward", # 5.4.4
114             "Temporary error in automatic homedir creation", # 4.3.0 or 5.3.0
115             "Temporary error while executing qmail-forward", # 4.4.4
116             "Temporary failure in LDAP lookup", # 4.4.3
117             "Unable to contact LDAP server", # 4.4.3
118             "Unable to login into LDAP server, bad credentials",# 4.4.3
119             ],
120             };
121 242         538 state $messagesof = {
122             "emailtoolarge" => ["Message size exceeds fixed maximum message size:"],
123             # qmail-remote.c:68| Sorry, I couldn't find any host by that name. (#4.1.2)\n"); zerodie();
124             # qmail-remote.c:78| Sorry, I couldn't find any host named ");
125             "hostunknown" => ["Sorry, I couldn't find any host "],
126             # error_str.c:192| X(EDQUOT,"disk quota exceeded")
127             "mailboxfull" => ["disk quota exceeded"],
128             # qmail-qmtpd.c:233| ... result = "Dsorry, that message size exceeds my databytes limit (#5.3.4)";
129             # qmail-smtpd.c:391| ... out("552 sorry, that message size exceeds my databytes limit (#5.3.4)\r\n"); return;
130             "networkerror" => [
131             "Sorry, I wasn't able to establish an SMTP connection",
132             "Sorry. Although I'm listed as a best-preference MX or A for that host",
133             ],
134             "notaccept" => [
135             # notqmail 1.08 returns the following error message when the destination MX is NullMX
136             "Sorry, I couldn't find a mail exchanger or IP address",
137             ],
138             "systemerror" => [
139             "bad interpreter: No such file or directory",
140             "system error",
141             "Unable to",
142             ],
143             "systemfull" => ["Requested action not taken: mailbox unavailable (not enough free space)"],
144             # qmail-local.c:589| strerr_die1x(100,"Sorry, no mailbox here by that name. (#5.1.1)");
145             # qmail-remote.c:253| out("s"); outhost(); out(" does not like recipient.\n");
146             "userunknown" => ["no mailbox here by that name"],
147             };
148              
149 242         1502 my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; my $v = undef;
  242         624  
150 242         1259 my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries);
151 242         496 my $readcursor = 0; # (Integer) Points the current cursor position
152 242         473 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
153              
154 242         1648 for my $e ( split("\n", $emailparts->[0]) ) {
155             # Read error messages and delivery status lines from the head of the email to the previous
156             # line of the beginning of the original message.
157 2265 100       3831 unless( $readcursor ) {
158             # Beginning of the bounce message or message/delivery-status part
159 928 100       1745 $readcursor |= $indicators->{'deliverystatus'} if grep { index($e, $_) > -1 } $startingof->{'message'}->@*;
  12064         18149  
160 928         1294 next;
161             }
162 1337 100 66     4682 next if ($readcursor & $indicators->{'deliverystatus'}) == 0 || $e eq "";
163              
164             # :
165             # 192.0.2.153 does not like recipient.
166             # Remote host said: 550 5.1.1 ... User Unknown
167             # Giving up on 192.0.2.153.
168 1082         1771 $v = $dscontents->[-1];
169              
170 1082 100 66     4425 if( index($e, '<') == 0 && Sisimai::String->aligned(\$e, ['<', '@', '>:']) ) {
    100          
171             # :
172 230 100       740 if( $v->{'recipient'} ) {
173             # There are multiple recipient addresses in the message body.
174 15         68 push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
175 15         34 $v = $dscontents->[-1];
176             }
177 230         2203 $v->{'recipient'} = Sisimai::Address->s3s4(substr($e, index($e, '<'),));
178 230         1049 $recipients++;
179              
180             } elsif( scalar @$dscontents == $recipients ) {
181             # Append error message
182 558         1346 $v->{'diagnosis'} .= $e.' ';
183 558 100       1764 $v->{'alterrors'} = $e if index($e, $startingof->{'error'}->[0]) == 0;
184              
185 558 100       1199 next if $v->{'rhost'};
186 501         903 for my $r ( $startingof->{'rhost'}->@* ) {
187             # Find a remote host name
188 1348 100       2028 my $p1 = index($e, $r); next if $p1 == -1;
  1348         2554  
189 91         133 my $cm = length $r;
190 91 100       254 my $p2 = index($e, ' ', $p1 + $cm + 1); $p2 = rindex($e, '.') if $p2 == -1;
  91         238  
191              
192 91         563 $v->{'rhost'} = Sisimai::String->sweep(substr($e, $p1 + $cm, $p2 - $p1 - $cm));
193 91         247 last;
194             }
195             }
196             }
197 242 100       1309 return undef unless $recipients;
198              
199 215         588 for my $e ( @$dscontents ) {
200             # Tidy up the error message in $e->{'diagnosis'}, Try to detect the bounce reason.
201 230         1303 $e->{"diagnosis"} = Sisimai::String->sweep($e->{"diagnosis"});
202              
203             # Get the SMTP command name for the session
204 230         990 SMTP: for my $r ( keys %$commandset ) {
205             # Get the last SMTP Command
206 903 100       1653 next unless grep { index($e->{"diagnosis"}, $_) > 0 } $commandset->{ $r }->@*;
  1053         3051  
207 111         329 $e->{"command"} = $r;
208 111         197 last;
209             }
210 230 100       855 if( index($e->{"diagnosis"}, "no SMTP connection got far enough") > -1 ) {
211             # Sorry, no SMTP connection got far enough; most progress was RCPT TO response; ...
212 6   33     70 $e->{"command"} ||= Sisimai::SMTP::Command->find($e->{"diagnosis"});
213             }
214              
215             # Detect the reason of bounce
216 230 100 66     1319 if( $e->{"command"} eq "HELO" || $e->{"command"} eq "EHLO" ) {
217             # HELO | Connected to 192.0.2.135 but my name was rejected.
218 5         12 $e->{"reason"} = "blocked";
219              
220             } else {
221             # The error message includes any of patterns defined in the variable avobe
222 225 100       1000 if( Sisimai::String->aligned(\$e->{"diagnosis"}, $onholdpair) ) {
223             # Need to be matched with error message pattens defined in Sisimai/Reason/*
224 5         16 $e->{"reason"} = "onhold";
225              
226             } else {
227             # Check that the error message includes any of message patterns or not
228 220         691 FINDREASON: for my $f ( $e->{"alterrors"}, $e->{"diagnosis"} ) {
229             # Try to detect an error reason
230 435 50       1020 last if $e->{"reason"};
231 435 100       1109 next unless $f;
232 352         1461 MESG: for my $r ( keys %$messagesof ) {
233             # The key is a bounce reason name
234 2715 100       4089 next unless grep { index($f, $_) > -1 } $messagesof->{ $r }->@*;
  3727         9887  
235 40         117 $e->{"reason"} = $r;
236 40         93 last FINDREASON;
237             }
238 312         1089 LDAP: for my $r ( keys %$failonldap ) {
239             # The key is a bounce reason name
240 1248 50       2070 next unless grep { index($f, $_) > -1 } $failonldap->{ $r }->@*;
  5304         9680  
241 0         0 $e->{"reason"} = $r;
242 0         0 last FINDREASON;
243             }
244 312 100       1043 $e->{"reason"} = "expired" if index($f, $hasexpired) > -1;
245             }
246             }
247             }
248 230   100     2029 $e->{"command"} ||= Sisimai::SMTP::Command->find($e->{"diagnosis"});
249             }
250 215         1820 return {"ds" => $dscontents, "rfc822" => $emailparts->[1]};
251             }
252              
253             1;
254             __END__