File Coverage

lib/Sisimai/Lhost/OpenSMTPD.pm
Criterion Covered Total %
statement 50 50 100.0
branch 18 20 90.0
condition 6 7 85.7
subroutine 6 6 100.0
pod 2 2 100.0
total 82 85 96.4


line stmt bran cond sub pod time code
1             package Sisimai::Lhost::OpenSMTPD;
2 40     40   4353 use parent 'Sisimai::Lhost';
  40         82  
  40         289  
3 40     40   3537 use v5.26;
  40         132  
4 40     40   204 use strict;
  40         111  
  40         1231  
5 40     40   172 use warnings;
  40         98  
  40         28257  
6              
7 1     1 1 4 sub description { 'OpenSMTPD: https://www.opensmtpd.org/' }
8             sub inquire {
9             # Detect an error from OpenSMTPD
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 1121     1121 1 4833 my $class = shift;
16 1121   100     3828 my $mhead = shift // return undef;
17 1120   100     3448 my $mbody = shift // return undef;
18              
19 1119 100       5962 return undef unless index($mhead->{'subject'}, 'Delivery status notification') > -1;
20 97 100       637 return undef unless index($mhead->{'from'}, 'Mailer Daemon <') > -1;
21 71 50       249 return undef unless grep { rindex($_, ' (OpenSMTPD) with ') > -1 } $mhead->{'received'}->@*;
  97         476  
22              
23 71         188 state $indicators = __PACKAGE__->INDICATORS;
24 71         141 state $boundaries = [' Below is a copy of the original message:'];
25 71         152 state $startingof = {
26             # http://www.openbsd.org/cgi-bin/man.cgi?query=smtpd&sektion=8
27             # opensmtpd-5.4.2p1/smtpd/
28             # bounce.c/317:#define NOTICE_INTRO \
29             # bounce.c/318: " Hi!\n\n" \
30             # bounce.c/319: " This is the MAILER-DAEMON, please DO NOT REPLY to this e-mail.\n"
31             # bounce.c/320:
32             # bounce.c/321:const char *notice_error =
33             # bounce.c/322: " An error has occurred while attempting to deliver a message for\n"
34             # bounce.c/323: " the following list of recipients:\n\n";
35             # bounce.c/324:
36             # bounce.c/325:const char *notice_warning =
37             # bounce.c/326: " A message is delayed for more than %s for the following\n"
38             # bounce.c/327: " list of recipients:\n\n";
39             # bounce.c/328:
40             # bounce.c/329:const char *notice_warning2 =
41             # bounce.c/330: " Please note that this is only a temporary failure report.\n"
42             # bounce.c/331: " The message is kept in the queue for up to %s.\n"
43             # bounce.c/332: " You DO NOT NEED to re-send the message to these recipients.\n\n";
44             # bounce.c/333:
45             # bounce.c/334:const char *notice_success =
46             # bounce.c/335: " Your message was successfully delivered to these recipients.\n\n";
47             # bounce.c/336:
48             # bounce.c/337:const char *notice_relay =
49             # bounce.c/338: " Your message was relayed to these recipients.\n\n";
50             # bounce.c/339:
51             'message' => [' This is the MAILER-DAEMON, please DO NOT REPLY to this'],
52             };
53 71         196 state $messagesof = {
54             # smtpd/queue.c:221| envelope_set_errormsg(&evp, "Envelope expired");
55             'expired' => ['Envelope expired'],
56             'hostunknown' => [
57             # smtpd/mta.c:976| relay->failstr = "Invalid domain name";
58             # smtpd/mta.c:980| relay->failstr = "Domain does not exist";
59             'Invalid domain name',
60             'Domain does not exist',
61             ],
62             # smtp/mta.c:1085| relay->failstr = "Destination seem to reject all mails";
63             'notaccept' => [
64             'Destination seem to reject all mails',
65             'No MX found for domain',
66             'No MX found for destination',
67             ],
68             'networkerror'=> [
69             # smtpd/mta.c:972| relay->failstr = "Temporary failure in MX lookup";
70             'Address family mismatch on destination MXs',
71             'All routes to destination blocked',
72             'bad DNS lookup error code',
73             'Could not retrieve source address',
74             'Loop detected',
75             'Network error on destination MXs',
76             'No valid route to remote MX',
77             'No valid route to destination',
78             'Temporary failure in MX lookup',
79             ],
80             # smtpd/mta.c:1013| relay->failstr = "Could not retrieve credentials";
81             'securityerror' => ['Could not retrieve credentials'],
82             };
83              
84 71         423 my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; my $v = undef;
  71         208  
85 71         400 my $emailparts = Sisimai::RFC5322->part($mbody, $boundaries);
86 71         153 my $readcursor = 0; # (Integer) Points the current cursor position
87 71         147 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
88              
89 71         693 for my $e ( split("\n", $emailparts->[0]) ) {
90             # Read error messages and delivery status lines from the head of the email to the previous
91             # line of the beginning of the original message.
92 788 100       1450 unless( $readcursor ) {
93             # Beginning of the bounce message or message/delivery-status part
94 303 100       970 $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0;
95 303         448 next;
96             }
97 485 100 66     1823 next if ($readcursor & $indicators->{'deliverystatus'}) == 0 || $e eq "";
98              
99             # Hi!
100             #
101             # This is the MAILER-DAEMON, please DO NOT REPLY to this e-mail.
102             #
103             # An error has occurred while attempting to deliver a message for
104             # the following list of recipients:
105             #
106             # kijitora@example.jp: 550 5.2.2 ... Mailbox Full
107             #
108             # Below is a copy of the original message:
109 313         482 $v = $dscontents->[-1];
110              
111 313 100       1472 if( Sisimai::String->aligned(\$e, ['@', ' ']) ) {
112             # kijitora@example.jp: 550 5.2.2 ... Mailbox Full
113 81 100       288 if( $v->{'recipient'} ) {
114             # There are multiple recipient addresses in the message body.
115 10         46 push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
116 10         28 $v = $dscontents->[-1];
117             }
118 81         318 $v->{'recipient'} = substr($e, 0, index($e, ':'));
119 81         309 $v->{'diagnosis'} = substr($e, index($e, ':') + 1, );
120 81         210 $recipients++;
121             }
122             }
123 71 50       350 return undef unless $recipients;
124              
125 71         243 for my $e ( @$dscontents ) {
126 81         347 $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'});
127              
128 81         369 SESSION: for my $r ( keys %$messagesof ) {
129             # Verify each regular expression of session errors
130 308 100       710 next unless grep { index($e->{'diagnosis'}, $_) > -1 } $messagesof->{ $r }->@*;
  959         2158  
131 40         123 $e->{'reason'} = $r;
132 40         140 last;
133             }
134             }
135 71         489 return {"ds" => $dscontents, "rfc822" => $emailparts->[1]};
136             }
137              
138             1;
139             __END__