File Coverage

lib/Sisimai/Lhost/qmail.pm
Criterion Covered Total %
statement 78 78 100.0
branch 39 40 97.5
condition 15 21 71.4
subroutine 6 6 100.0
pod 2 2 100.0
total 140 147 95.2


line stmt bran cond sub pod time code
1             package Sisimai::Lhost::qmail;
2 43     43   4497 use parent 'Sisimai::Lhost';
  43         65  
  43         258  
3 43     43   2860 use v5.26;
  43         118  
4 43     43   189 use strict;
  43         80  
  43         987  
5 43     43   163 use warnings;
  43         104  
  43         37655  
6              
7 1     1 1 5 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 1127     1127 1 5731 my $class = shift;
16 1127   100     2583 my $mhead = shift // return undef;
17 1124   100     1949 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 1121         2156 my $emailtitle = [
24             "failure notice", # qmail-send.c:Subject: failure notice\n\
25             "Failure Notice", # Yahoo
26             ];
27 1121 100       1552 my $proceedsto = 0; $proceedsto++ if grep { $mhead->{"subject"} eq $_ } @$emailtitle;
  1121         1529  
  2242         3994  
28              
29 1121         2094 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 2141 100 50     12408 $proceedsto ||= 1 if Sisimai::String->aligned(\$e, ["(qmail", " invoked "]);
33             }
34 1121 100       2558 return undef if $proceedsto == 0;
35              
36 242         846 require Sisimai::SMTP::Command;
37 242         408 state $indicators = __PACKAGE__->INDICATORS;
38 242         298 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         347 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         328 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 242         270 state $messagesof = {
94             # notqmail 1.08 returns the following error message when the destination MX is NullMX
95             "notaccept" => ["Sorry, I couldn't find a mail exchanger or IP address"],
96             "userunknown" => ["no mailbox here by that name"],
97             };
98              
99 242         793 my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; my $v = undef;
  242         340  
100 242         818 my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries);
101 242         337 my $readcursor = 0; # (Integer) Points the current cursor position
102 242         291 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
103              
104 242         1059 for my $e ( split("\n", $emailparts->[0]) ) {
105             # Read error messages and delivery status lines from the head of the email to the previous
106             # line of the beginning of the original message.
107 2265 100       2435 unless( $readcursor ) {
108             # Beginning of the bounce message or message/delivery-status part
109 928 100       1080 $readcursor |= $indicators->{'deliverystatus'} if grep { index($e, $_) > -1 } $startingof->{'message'}->@*;
  12064         11866  
110 928         936 next;
111             }
112 1337 100 66     3305 next if ($readcursor & $indicators->{'deliverystatus'}) == 0 || $e eq "";
113              
114             # :
115             # 192.0.2.153 does not like recipient.
116             # Remote host said: 550 5.1.1 ... User Unknown
117             # Giving up on 192.0.2.153.
118 1082         1024 $v = $dscontents->[-1];
119              
120 1082 100 66     3074 if( index($e, '<') == 0 && Sisimai::String->aligned(\$e, ['<', '@', '>:']) ) {
    100          
121             # :
122 230 100       482 if( $v->{'recipient'} ) {
123             # There are multiple recipient addresses in the message body.
124 15         51 push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
125 15         29 $v = $dscontents->[-1];
126             }
127 230         1480 $v->{'recipient'} = Sisimai::Address->s3s4(substr($e, index($e, '<'),));
128 230         543 $recipients++;
129              
130             } elsif( scalar @$dscontents == $recipients ) {
131             # Append error message
132 558         938 $v->{'diagnosis'} .= $e.' ';
133 558 100       1257 $v->{'alterrors'} = $e if index($e, $startingof->{'error'}->[0]) == 0;
134              
135 558 100       818 next if $v->{'rhost'};
136 501         646 for my $r ( $startingof->{'rhost'}->@* ) {
137             # Find a remote host name
138 1348 100       1389 my $p1 = index($e, $r); next if $p1 == -1;
  1348         1672  
139 91         91 my $cm = length $r;
140 91 100       152 my $p2 = index($e, ' ', $p1 + $cm + 1); $p2 = rindex($e, '.') if $p2 == -1;
  91         155  
141              
142 91         197 $v->{'rhost'} = substr($e, $p1 + $cm, $p2 - $p1 - $cm);
143 91         123 last;
144             }
145             }
146             }
147 242 100       843 return undef unless $recipients;
148              
149 215         338 for my $e ( @$dscontents ) {
150             # Get the SMTP command name for the session
151 230         645 SMTP: for my $r ( keys %$commandset ) {
152             # Get the last SMTP Command
153 845 100       1073 next unless grep { index($e->{"diagnosis"}, $_) > 0 } $commandset->{ $r }->@*;
  1015         1957  
154 111         162 $e->{"command"} = $r;
155 111         126 last;
156             }
157 230 100       640 if( index($e->{"diagnosis"}, "no SMTP connection got far enough") > -1 ) {
158             # Sorry, no SMTP connection got far enough; most progress was RCPT TO response; ...
159 6   33     51 $e->{"command"} ||= Sisimai::SMTP::Command->find($e->{"diagnosis"});
160             }
161              
162             # Detect the reason of bounce
163 230 100 66     854 if( $e->{"command"} eq "HELO" || $e->{"command"} eq "EHLO" ) {
164             # HELO | Connected to 192.0.2.135 but my name was rejected.
165 5         12 $e->{"reason"} = "blocked";
166              
167             } else {
168             # The error message includes any of patterns defined in the variable avobe
169 225         415 FINDREASON: for my $f ( $e->{"alterrors"}, $e->{"diagnosis"} ) {
170             # Check that the error message includes any of message patterns or not
171 450 50       591 last if $e->{"reason"};
172 450 100       619 next unless $f;
173 367         591 MESG: for my $r ( keys %$messagesof ) {
174             # The key is a bounce reason name
175 728 100       766 next unless grep { index($f, $_) > -1 } $messagesof->{ $r }->@*;
  728         1317  
176 15         50 $e->{"reason"} = $r;
177 15         24 last FINDREASON;
178             }
179             }
180             }
181 230   100     1156 $e->{"command"} ||= Sisimai::SMTP::Command->find($e->{"diagnosis"});
182             }
183 215         1093 return {"ds" => $dscontents, "rfc822" => $emailparts->[1]};
184             }
185              
186             1;
187             __END__