1package Sisimai::Lhost::McAfee;
2use parent 'Sisimai::Lhost';
3use feature ':5.10';
4use strict;
5use warnings;
6
7sub description { 'McAfee Email Appliance' }
8sub make {
9    # Detect an error from McAfee
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    # @since v4.1.1
15    my $class = shift;
16    my $mhead = shift // return undef;
17    my $mbody = shift // return undef;
18
19    # X-NAI-Header: Modified by McAfee Email and Web Security Virtual Appliance
20    return undef unless defined $mhead->{'x-nai-header'};
21    return undef unless index($mhead->{'x-nai-header'}, 'Modified by McAfee') > -1;
22    return undef unless $mhead->{'subject'} eq 'Delivery Status';
23
24    state $indicators = __PACKAGE__->INDICATORS;
25    state $rebackbone = qr|^Content-Type:[ ]message/rfc822|m;
26    state $startingof = { 'message' => ['--- The following addresses had delivery problems ---'] };
27    state $refailures = {
28        'userunknown' => qr{(?:
29             [ ]User[ ][(].+[@].+[)][ ]unknown[.][ ]
30            |550[ ]Unknown[ ]user[ ][^ ]+[@][^ ]+
31            |550[ ][<].+?[@].+?[>][.]+[ ]User[ ]not[ ]exist
32            |No[ ]such[ ]user
33            )
34        }x,
35    };
36
37    require Sisimai::RFC1894;
38    my $fieldtable = Sisimai::RFC1894->FIELDTABLE;
39    my $dscontents = [__PACKAGE__->DELIVERYSTATUS];
40    my $emailsteak = Sisimai::RFC5322->fillet($mbody, $rebackbone);
41    my $readcursor = 0;     # (Integer) Points the current cursor position
42    my $recipients = 0;     # (Integer) The number of 'Final-Recipient' header
43    my $diagnostic = '';    # (String) Alternative diagnostic message
44    my $v = undef;
45    my $p = '';
46
47    for my $e ( split("\n", $emailsteak->[0]) ) {
48        # Read error messages and delivery status lines from the head of the email
49        # to the previous line of the beginning of the original message.
50        unless( $readcursor ) {
51            # Beginning of the bounce message or message/delivery-status part
52            $readcursor |= $indicators->{'deliverystatus'} if index($e, $startingof->{'message'}->[0]) > -1;
53            next;
54        }
55        next unless $readcursor & $indicators->{'deliverystatus'};
56        next unless length $e;
57
58        # Content-Type: text/plain; name="deliveryproblems.txt"
59        #
60        #    --- The following addresses had delivery problems ---
61        #
62        # <user@example.com>   (User unknown user@example.com)
63        #
64        # --------------Boundary-00=_00000000000000000000
65        # Content-Type: message/delivery-status; name="deliverystatus.txt"
66        #
67        $v = $dscontents->[-1];
68
69        if( $e =~ /\A[<]([^ ]+[@][^ ]+)[>][ \t]+[(](.+)[)]\z/ ) {
70            # <kijitora@example.co.jp>   (Unknown user kijitora@example.co.jp)
71            if( $v->{'recipient'} ) {
72                # There are multiple recipient addresses in the message body.
73                push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
74                $v = $dscontents->[-1];
75            }
76            $v->{'recipient'} = $1;
77            $diagnostic = $2;
78            $recipients++;
79
80        } elsif( my $f = Sisimai::RFC1894->match($e) ) {
81            # $e matched with any field defined in RFC3464
82            my $o = Sisimai::RFC1894->field($e);
83            unless( $o ) {
84                # Fallback code for empty value or invalid formatted value
85                # - Original-Recipient: <kijitora@example.co.jp>
86                $v->{'alias'} = Sisimai::Address->s3s4($1) if $e =~ /\AOriginal-Recipient:[ ]*([^ ]+)\z/;
87                next;
88            }
89            next unless exists $fieldtable->{ $o->[0] };
90            $v->{ $fieldtable->{ $o->[0] } } = $o->[2];
91
92        } else {
93            # Continued line of the value of Diagnostic-Code field
94            next unless index($p, 'Diagnostic-Code:') == 0;
95            next unless $e =~ /\A[ \t]+(.+)\z/;
96            $v->{'diagnosis'} .= ' '.$1;
97        } # End of error message part
98    } continue {
99        # Save the current line for the next loop
100        $p = $e;
101    }
102    return undef unless $recipients;
103
104    for my $e ( @$dscontents ) {
105        $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'} || $diagnostic);
106
107        SESSION: for my $r ( keys %$refailures ) {
108            # Verify each regular expression of session errors
109            next unless $e->{'diagnosis'} =~ $refailures->{ $r };
110            $e->{'reason'} = $r;
111            last;
112        }
113    }
114    return { 'ds' => $dscontents, 'rfc822' => $emailsteak->[1] };
115}
116
1171;
118__END__
119
120=encoding utf-8
121
122=head1 NAME
123
124Sisimai::Lhost::McAfee - bounce mail parser class for C<McAfee Email Appliance>.
125
126=head1 SYNOPSIS
127
128    use Sisimai::Lhost::McAfee;
129
130=head1 DESCRIPTION
131
132Sisimai::Lhost::McAfee parses a bounce email which created by
133C<McAfee Email Appliance>.
134Methods in the module are called from only Sisimai::Message.
135
136=head1 CLASS METHODS
137
138=head2 C<B<description()>>
139
140C<description()> returns description string of this module.
141
142    print Sisimai::Lhost::McAfee->description;
143
144=head2 C<B<make(I<header data>, I<reference to body string>)>>
145
146C<make()> method parses a bounced email and return results as a array reference.
147See Sisimai::Message for more details.
148
149=head1 AUTHOR
150
151azumakuniyuki
152
153=head1 COPYRIGHT
154
155Copyright (C) 2014-2020 azumakuniyuki, All rights reserved.
156
157=head1 LICENSE
158
159This software is distributed under The BSD 2-Clause License.
160
161=cut
162
163