1package Sisimai::Lhost::Notes;
2use parent 'Sisimai::Lhost';
3use feature ':5.10';
4use strict;
5use warnings;
6use Encode;
7
8sub description { 'Lotus Notes' }
9sub make {
10    # Detect an error from Lotus Notes
11    # @param    [Hash] mhead    Message headers of a bounce email
12    # @param    [String] mbody  Message body of a bounce email
13    # @return   [Hash]          Bounce data list and message/rfc822 part
14    # @return   [Undef]         failed to parse or the arguments are missing
15    # @since v4.1.1
16    my $class = shift;
17    my $mhead = shift // return undef;
18    my $mbody = shift // return undef;
19    return undef unless index($mhead->{'subject'}, 'Undeliverable message') == 0;
20
21    state $indicators = __PACKAGE__->INDICATORS;
22    state $rebackbone = qr|^-------[ ]Returned[ ]Message[ ]--------|m;
23    state $startingof = { 'message' => ['------- Failure Reasons '] };
24    state $messagesof = {
25        'userunknown' => [
26            'User not listed in public Name & Address Book',
27            'ディレクトリのリストにありません',
28        ],
29        'networkerror' => ['Message has exceeded maximum hop count'],
30    };
31
32    my $dscontents = [__PACKAGE__->DELIVERYSTATUS];
33    my $emailsteak = Sisimai::RFC5322->fillet($mbody, $rebackbone);
34    my $readcursor = 0;     # (Integer) Points the current cursor position
35    my $recipients = 0;     # (Integer) The number of 'Final-Recipient' header
36    my $removedmsg = 'MULTIBYTE CHARACTERS HAVE BEEN REMOVED';
37    my $encodedmsg = '';
38    my $v = undef;
39
40    # Get character set name, Content-Type: text/plain; charset=ISO-2022-JP
41    my $characters = $mhead->{'content-type'} =~ /\A.+;[ ]*charset=(.+)\z/ ? lc $1 : '';
42
43    for my $e ( split("\n", $emailsteak->[0]) ) {
44        # Read error messages and delivery status lines from the head of the email
45        # to the previous line of the beginning of the original message.
46        unless( $readcursor ) {
47            # Beginning of the bounce message or message/delivery-status part
48            $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) == 0;
49            next;
50        }
51        next unless $readcursor & $indicators->{'deliverystatus'};
52
53        # ------- Failure Reasons  --------
54        #
55        # User not listed in public Name & Address Book
56        # kijitora@notes.example.jp
57        #
58        # ------- Returned Message --------
59        $v = $dscontents->[-1];
60        if( $e =~ /\A[^ ]+[@][^ ]+/ ) {
61            # kijitora@notes.example.jp
62            if( $v->{'recipient'} ) {
63                # There are multiple recipient addresses in the message body.
64                push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
65                $v = $dscontents->[-1];
66            }
67            $v->{'recipient'} ||= $e;
68            $recipients++;
69
70        } else {
71            next if $e eq '';
72            next if index($e, '-') == 0;
73
74            if( $e =~ /[^\x20-\x7e]/ ) {
75                # Error message is not ISO-8859-1
76                $encodedmsg = $e;
77                if( $characters ) {
78                    # Try to convert string
79                    eval { Encode::from_to($encodedmsg, $characters, 'utf8'); };
80                    $encodedmsg = $removedmsg if $@;    # Failed to convert
81
82                } else {
83                    # No character set in Content-Type header
84                    $encodedmsg = $removedmsg;
85                }
86                $v->{'diagnosis'} .= $encodedmsg;
87
88            } else {
89                # Error message does not include multi-byte character
90                $v->{'diagnosis'} .= $e;
91            }
92        }
93    }
94
95    unless( $recipients ) {
96        # Fallback: Get the recpient address from RFC822 part
97        if( $emailsteak->[1] =~ /^To:[ ]*(.+)$/m ) {
98            $v->{'recipient'} = Sisimai::Address->s3s4($1);
99            $recipients++ if $v->{'recipient'};
100        }
101    }
102    return undef unless $recipients;
103
104    for my $e ( @$dscontents ) {
105        $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'});
106        $e->{'recipient'} = Sisimai::Address->s3s4($e->{'recipient'});
107
108        for my $r ( keys %$messagesof ) {
109            # Check each regular expression of Notes error messages
110            next unless grep { index($e->{'diagnosis'}, $_) > -1 } @{ $messagesof->{ $r } };
111            $e->{'reason'} = $r;
112            $e->{'status'} = Sisimai::SMTP::Status->code($r) || '';
113            last;
114        }
115    }
116    return { 'ds' => $dscontents, 'rfc822' => $emailsteak->[1] };
117}
118
1191;
120__END__
121
122=encoding utf-8
123
124=head1 NAME
125
126Sisimai::Lhost::Notes - bounce mail parser class for C<Lotus Notes Server>.
127
128=head1 SYNOPSIS
129
130    use Sisimai::Lhost::Notes;
131
132=head1 DESCRIPTION
133
134Sisimai::Lhost::Notes parses a bounce email which created by
135C<Lotus Notes Server>.
136Methods in the module are called from only Sisimai::Message.
137
138=head1 CLASS METHODS
139
140=head2 C<B<description()>>
141
142C<description()> returns description string of this module.
143
144    print Sisimai::Lhost::Notes->description;
145
146=head2 C<B<make(I<header data>, I<reference to body string>)>>
147
148C<make()> method parses a bounced email and return results as a array reference.
149See Sisimai::Message for more details.
150
151=head1 AUTHOR
152
153azumakuniyuki
154
155=head1 COPYRIGHT
156
157Copyright (C) 2014-2020 azumakuniyuki, All rights reserved.
158
159=head1 LICENSE
160
161This software is distributed under The BSD 2-Clause License.
162
163=cut
164
165