1package Sisimai::Lhost::OpenSMTPD;
2use parent 'Sisimai::Lhost';
3use feature ':5.10';
4use strict;
5use warnings;
6
7sub description { 'OpenSMTPD' }
8sub make {
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 parse or the arguments are missing
14
15    # @since v4.0.0
16    my $class = shift;
17    my $mhead = shift // return undef;
18    my $mbody = shift // return undef;
19
20    return undef unless index($mhead->{'subject'}, 'Delivery status notification') > -1;
21    return undef unless index($mhead->{'from'}, 'Mailer Daemon <') > -1;
22    return undef unless grep { rindex($_, ' (OpenSMTPD) with ') > -1 } @{ $mhead->{'received'} };
23
24    state $indicators = __PACKAGE__->INDICATORS;
25    state $rebackbone = qr|^[ ]+Below is a copy of the original message:|m;
26    state $startingof = {
27        # http://www.openbsd.org/cgi-bin/man.cgi?query=smtpd&sektion=8
28        # opensmtpd-5.4.2p1/smtpd/
29        #   bounce.c/317:#define NOTICE_INTRO \
30        #   bounce.c/318:    "    Hi!\n\n"    \
31        #   bounce.c/319:    "    This is the MAILER-DAEMON, please DO NOT REPLY to this e-mail.\n"
32        #   bounce.c/320:
33        #   bounce.c/321:const char *notice_error =
34        #   bounce.c/322:    "    An error has occurred while attempting to deliver a message for\n"
35        #   bounce.c/323:    "    the following list of recipients:\n\n";
36        #   bounce.c/324:
37        #   bounce.c/325:const char *notice_warning =
38        #   bounce.c/326:    "    A message is delayed for more than %s for the following\n"
39        #   bounce.c/327:    "    list of recipients:\n\n";
40        #   bounce.c/328:
41        #   bounce.c/329:const char *notice_warning2 =
42        #   bounce.c/330:    "    Please note that this is only a temporary failure report.\n"
43        #   bounce.c/331:    "    The message is kept in the queue for up to %s.\n"
44        #   bounce.c/332:    "    You DO NOT NEED to re-send the message to these recipients.\n\n";
45        #   bounce.c/333:
46        #   bounce.c/334:const char *notice_success =
47        #   bounce.c/335:    "    Your message was successfully delivered to these recipients.\n\n";
48        #   bounce.c/336:
49        #   bounce.c/337:const char *notice_relay =
50        #   bounce.c/338:    "    Your message was relayed to these recipients.\n\n";
51        #   bounce.c/339:
52        'message' => ['    This is the MAILER-DAEMON, please DO NOT REPLY to this'],
53    };
54    state $messagesof = {
55        # smtpd/queue.c:221|  envelope_set_errormsg(&evp, "Envelope expired");
56        'expired'     => ['Envelope expired'],
57        'hostunknown' => [
58            # smtpd/mta.c:976|  relay->failstr = "Invalid domain name";
59            # smtpd/mta.c:980|  relay->failstr = "Domain does not exist";
60            'Invalid domain name',
61            'Domain does not exist',
62        ],
63        # smtp/mta.c:1085|  relay->failstr = "Destination seem to reject all mails";
64        'notaccept'   => ['Destination seem to reject all mails'],
65        'networkerror'=> [
66            #  smtpd/mta.c:972|  relay->failstr = "Temporary failure in MX lookup";
67            'Address family mismatch on destination MXs',
68            'All routes to destination blocked',
69            'bad DNS lookup error code',
70            'Could not retrieve source address',
71            'Loop detected',
72            'Network error on destination MXs',
73            'No MX found for domain',
74            'No MX found for destination',
75            'No valid route to remote MX',
76            'No valid route to destination',
77            'Temporary failure in MX lookup',
78        ],
79        # smtpd/mta.c:1013|  relay->failstr = "Could not retrieve credentials";
80        'securityerror' => ['Could not retrieve credentials'],
81    };
82
83    my $dscontents = [__PACKAGE__->DELIVERYSTATUS];
84    my $emailsteak = Sisimai::RFC5322->fillet($mbody, $rebackbone);
85    my $readcursor = 0;     # (Integer) Points the current cursor position
86    my $recipients = 0;     # (Integer) The number of 'Final-Recipient' header
87    my $v = undef;
88
89    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        unless( $readcursor ) {
93            # Beginning of the bounce message or message/delivery-status part
94            $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0;
95            next;
96        }
97        next unless $readcursor & $indicators->{'deliverystatus'};
98        next unless length $e;
99
100        #    Hi!
101        #
102        #    This is the MAILER-DAEMON, please DO NOT REPLY to this e-mail.
103        #
104        #    An error has occurred while attempting to deliver a message for
105        #    the following list of recipients:
106        #
107        # kijitora@example.jp: 550 5.2.2 <kijitora@example>... Mailbox Full
108        #
109        #    Below is a copy of the original message:
110        $v = $dscontents->[-1];
111
112        if( $e =~ /\A([^ ]+?[@][^ ]+?):?[ ](.+)\z/ ) {
113            # kijitora@example.jp: 550 5.2.2 <kijitora@example>... Mailbox Full
114            if( $v->{'recipient'} ) {
115                # There are multiple recipient addresses in the message body.
116                push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
117                $v = $dscontents->[-1];
118            }
119            $v->{'recipient'} = $1;
120            $v->{'diagnosis'} = $2;
121            $recipients++;
122        }
123    }
124    return undef unless $recipients;
125
126    for my $e ( @$dscontents ) {
127        $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'});
128
129        SESSION: for my $r ( keys %$messagesof ) {
130            # Verify each regular expression of session errors
131            next unless grep { index($e->{'diagnosis'}, $_) > -1 } @{ $messagesof->{ $r } };
132            $e->{'reason'} = $r;
133            last;
134        }
135    }
136    return { 'ds' => $dscontents, 'rfc822' => $emailsteak->[1] };
137}
138
1391;
140__END__
141
142=encoding utf-8
143
144=head1 NAME
145
146Sisimai::Lhost::OpenSMTPD - bounce mail parser class for C<OpenSMTPD>.
147
148=head1 SYNOPSIS
149
150    use Sisimai::Lhost::OpenSMTPD;
151
152=head1 DESCRIPTION
153
154Sisimai::Lhost::OpenSMTPD parses a bounce email which created by C<OpenSMTPD>.
155Methods in the module are called from only Sisimai::Message.
156
157=head1 CLASS METHODS
158
159=head2 C<B<description()>>
160
161C<description()> returns description string of this module.
162
163    print Sisimai::Lhost::OpenSMTPD->description;
164
165=head2 C<B<make(I<header data>, I<reference to body string>)>>
166
167C<make()> method parses a bounced email and return results as a array reference.
168See Sisimai::Message for more details.
169
170=head1 AUTHOR
171
172azumakuniyuki
173
174=head1 COPYRIGHT
175
176Copyright (C) 2014-2020 azumakuniyuki, All rights reserved.
177
178=head1 LICENSE
179
180This software is distributed under The BSD 2-Clause License.
181
182=cut
183