1package Sisimai::Lhost::AmazonWorkMail; 2use parent 'Sisimai::Lhost'; 3use feature ':5.10'; 4use strict; 5use warnings; 6 7# https://aws.amazon.com/workmail/ 8sub description { 'Amazon WorkMail: https://aws.amazon.com/workmail/' } 9sub make { 10 # Detect an error from Amazon WorkMail 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.29 16 my $class = shift; 17 my $mhead = shift // return undef; 18 my $mbody = shift // return undef; 19 my $match = 0; 20 my $xmail = $mhead->{'x-original-mailer'} || $mhead->{'x-mailer'} || ''; 21 22 # X-Mailer: Amazon WorkMail 23 # X-Original-Mailer: Amazon WorkMail 24 # X-Ses-Outgoing: 2016.01.14-54.240.27.159 25 $match++ if $mhead->{'x-ses-outgoing'}; 26 if( $xmail ) { 27 # X-Mailer: Amazon WorkMail 28 # X-Original-Mailer: Amazon WorkMail 29 $match++ if $xmail eq 'Amazon WorkMail'; 30 } 31 return undef if $match < 2; 32 33 state $indicators = __PACKAGE__->INDICATORS; 34 state $rebackbone = qr|^content-type:[ ]message/rfc822|m; 35 state $startingof = { 'message' => ['Technical report:'] }; 36 37 require Sisimai::RFC1894; 38 my $fieldtable = Sisimai::RFC1894->FIELDTABLE; 39 my $permessage = {}; # (Hash) Store values of each Per-Message field 40 41 my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; 42 my $emailsteak = Sisimai::RFC5322->fillet($mbody, $rebackbone); 43 my $readcursor = 0; # (Integer) Points the current cursor position 44 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header 45 my $v = undef; 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]) == 0; 53 next; 54 } 55 next unless $readcursor & $indicators->{'deliverystatus'}; 56 next unless length $e; 57 58 if( my $f = Sisimai::RFC1894->match($e) ) { 59 # $e matched with any field defined in RFC3464 60 next unless my $o = Sisimai::RFC1894->field($e); 61 $v = $dscontents->[-1]; 62 63 if( $o->[-1] eq 'addr' ) { 64 # Final-Recipient: rfc822; kijitora@example.jp 65 # X-Actual-Recipient: rfc822; kijitora@example.co.jp 66 if( $o->[0] eq 'final-recipient' ) { 67 # Final-Recipient: rfc822; kijitora@example.jp 68 if( $v->{'recipient'} ) { 69 # There are multiple recipient addresses in the message body. 70 push @$dscontents, __PACKAGE__->DELIVERYSTATUS; 71 $v = $dscontents->[-1]; 72 } 73 $v->{'recipient'} = $o->[2]; 74 $recipients++; 75 76 } else { 77 # X-Actual-Recipient: rfc822; kijitora@example.co.jp 78 $v->{'alias'} = $o->[2]; 79 } 80 } elsif( $o->[-1] eq 'code' ) { 81 # Diagnostic-Code: SMTP; 550 5.1.1 <userunknown@example.jp>... User Unknown 82 $v->{'spec'} = $o->[1]; 83 $v->{'diagnosis'} = $o->[2]; 84 85 } else { 86 # Other DSN fields defined in RFC3464 87 next unless exists $fieldtable->{ $o->[0] }; 88 $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; 89 90 next unless $f == 1; 91 $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2]; 92 } 93 } 94 95 # <!DOCTYPE HTML><html> 96 # <head> 97 # <meta name="Generator" content="Amazon WorkMail v3.0-2023.77"> 98 # <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 99 last if index($e, '<!DOCTYPE HTML><html>') == 0; 100 } 101 return undef unless $recipients; 102 103 for my $e ( @$dscontents ) { 104 # Set default values if each value is empty. 105 $e->{'lhost'} ||= $permessage->{'rhost'}; 106 $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage; 107 $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); 108 109 if( $e->{'status'} =~ /\A[45][.][01][.]0\z/ ) { 110 # Get other D.S.N. value from the error message 111 # 5.1.0 - Unknown address error 550-'5.7.1 ... 112 my $errormessage = $e->{'diagnosis'}; 113 $errormessage = $1 if $e->{'diagnosis'} =~ /["'](\d[.]\d[.]\d.+)['"]/; 114 $e->{'status'} = Sisimai::SMTP::Status->find($errormessage) || $e->{'status'}; 115 } 116 117 # 554 4.4.7 Message expired: unable to deliver in 840 minutes. 118 # <421 4.4.2 Connection timed out> 119 $e->{'replycode'} = $1 if $e->{'diagnosis'} =~ /[<]([245]\d\d)[ ].+[>]/; 120 $e->{'reason'} ||= Sisimai::SMTP::Status->name($e->{'status'}) || ''; 121 } 122 return { 'ds' => $dscontents, 'rfc822' => $emailsteak->[1] }; 123} 124 1251; 126__END__ 127 128=encoding utf-8 129 130=head1 NAME 131 132Sisimai::Lhost::AmazonWorkMail - bounce mail parser class for C<Amazon WorkMail>. 133 134=head1 SYNOPSIS 135 136 use Sisimai::Lhost::AmazonWorkMail; 137 138=head1 DESCRIPTION 139 140Sisimai::Lhost::AmazonWorkMail parses a bounce email which created by C<Amazon WorkMail>. 141Methods in the module are called from only Sisimai::Message. 142 143=head1 CLASS METHODS 144 145=head2 C<B<description()>> 146 147C<description()> returns description string of this module. 148 149 print Sisimai::Lhost::AmazonWorkMail->description; 150 151=head2 C<B<make(I<header data>, I<reference to body string>)>> 152 153C<make()> method parses a bounced email and return results as a array reference. 154See Sisimai::Message for more details. 155 156=head1 AUTHOR 157 158azumakuniyuki 159 160=head1 COPYRIGHT 161 162Copyright (C) 2016-2020 azumakuniyuki, All rights reserved. 163 164=head1 LICENSE 165 166This software is distributed under The BSD 2-Clause License. 167 168=cut 169 170