1package Sisimai::Lhost::GSuite; 2use parent 'Sisimai::Lhost'; 3use feature ':5.10'; 4use strict; 5use warnings; 6 7sub description { 'G Suite: https://gsuite.google.com/' } 8sub make { 9 # Detect an error from G Suite (Transfer from G Suite to a destination host) 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.21.0 15 my $class = shift; 16 my $mhead = shift // return undef; 17 my $mbody = shift // return undef; 18 19 return undef unless rindex($mhead->{'from'}, '<mailer-daemon@googlemail.com>') > -1; 20 return undef unless index($mhead->{'subject'}, 'Delivery Status Notification') > -1; 21 return undef unless $mhead->{'x-gm-message-state'}; 22 23 state $indicators = __PACKAGE__->INDICATORS; 24 state $rebackbone = qr<^Content-Type:[ ](?:message/rfc822|text/rfc822-headers)>m; 25 state $markingsof = { 26 'message' => qr/\A[*][*][ ].+[ ][*][*]\z/, 27 'error' => qr/\AThe[ ]response([ ]from[ ]the[ ]remote[ ]server)?[ ]was:\z/, 28 'html' => qr{\AContent-Type:[ ]*text/html;[ ]*charset=['"]?(?:UTF|utf)[-]8['"]?\z}, 29 }; 30 state $messagesof = { 31 'userunknown' => ["because the address couldn't be found. Check for typos or unnecessary spaces and try again."], 32 'notaccept' => ['Null MX'], 33 'networkerror' => [' had no relevant answers.', ' responded with code NXDOMAIN'], 34 }; 35 36 require Sisimai::RFC1894; 37 my $fieldtable = Sisimai::RFC1894->FIELDTABLE; 38 my $permessage = {}; # (Hash) Store values of each Per-Message field 39 40 my $dscontents = [__PACKAGE__->DELIVERYSTATUS]; 41 my $emailsteak = Sisimai::RFC5322->fillet($mbody, $rebackbone); 42 my $readcursor = 0; # (Integer) Points the current cursor position 43 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header 44 my $endoferror = 0; # (Integer) Flag for a blank line after error messages 45 my $emptylines = 0; # (Integer) The number of empty lines 46 my $anotherset = { # (Hash) Another error information 47 'diagnosis' => '', 48 }; 49 my $v = undef; 50 51 for my $e ( split("\n", $emailsteak->[0]) ) { 52 # Read error messages and delivery status lines from the head of the email 53 # to the previous line of the beginning of the original message. 54 unless( $readcursor ) { 55 # Beginning of the bounce message or message/delivery-status part 56 $readcursor |= $indicators->{'deliverystatus'} if $e =~ $markingsof->{'message'}; 57 } 58 next unless $readcursor & $indicators->{'deliverystatus'}; 59 60 if( my $f = Sisimai::RFC1894->match($e) ) { 61 # $e matched with any field defined in RFC3464 62 next unless my $o = Sisimai::RFC1894->field($e); 63 $v = $dscontents->[-1]; 64 65 if( $o->[-1] eq 'addr' ) { 66 # Final-Recipient: rfc822; kijitora@example.jp 67 # X-Actual-Recipient: rfc822; kijitora@example.co.jp 68 if( $o->[0] eq 'final-recipient' ) { 69 # Final-Recipient: rfc822; kijitora@example.jp 70 if( $v->{'recipient'} ) { 71 # There are multiple recipient addresses in the message body. 72 push @$dscontents, __PACKAGE__->DELIVERYSTATUS; 73 $v = $dscontents->[-1]; 74 } 75 $v->{'recipient'} = $o->[2]; 76 $recipients++; 77 78 } else { 79 # X-Actual-Recipient: rfc822; kijitora@example.co.jp 80 $v->{'alias'} = $o->[2]; 81 } 82 } elsif( $o->[-1] eq 'code' ) { 83 # Diagnostic-Code: SMTP; 550 5.1.1 <userunknown@example.jp>... User Unknown 84 $v->{'spec'} = $o->[1]; 85 $v->{'diagnosis'} = $o->[2]; 86 87 } else { 88 # Other DSN fields defined in RFC3464 89 next unless exists $fieldtable->{ $o->[0] }; 90 $v->{ $fieldtable->{ $o->[0] } } = $o->[2]; 91 92 if( $fieldtable->{ $o->[0] } eq 'lhost' ) { 93 # Do not set an email address as a hostname in "lhost" value 94 $v->{'lhost'} = '' if index($v->{'lhost'}, '@'); 95 } 96 97 next unless $f == 1; 98 $permessage->{ $fieldtable->{ $o->[0] } } = $o->[2]; 99 } 100 } else { 101 # The line does not begin with a DSN field defined in RFC3464 102 if( ! $endoferror && $v->{'diagnosis'} ) { 103 # Append error messages continued from the previous line 104 $endoferror ||= 1 if $e eq ''; 105 next if $endoferror; 106 $v->{'diagnosis'} .= $e; 107 108 } elsif( $e =~ $markingsof->{'error'} ) { 109 # The response from the remote server was: 110 $anotherset->{'diagnosis'} .= $e; 111 112 } else { 113 # ** Address not found ** 114 # 115 # Your message wasn't delivered to * because the address couldn't be found. 116 # Check for typos or unnecessary spaces and try again. 117 # 118 # The response from the remote server was: 119 # 550 #5.1.0 Address rejected. 120 next if $e =~ /\AContent-Type:/; 121 if( $anotherset->{'diagnosis'} ) { 122 # Continued error messages from the previous line like 123 # "550 #5.1.0 Address rejected." 124 next if $emptylines > 5; 125 unless( length $e ) { 126 # Count and next() 127 $emptylines += 1; 128 next; 129 } 130 $anotherset->{'diagnosis'} .= ' '.$e 131 132 } else { 133 # ** Address not found ** 134 # 135 # Your message wasn't delivered to * because the address couldn't be found. 136 # Check for typos or unnecessary spaces and try again. 137 next unless $e =~ $markingsof->{'message'}; 138 $anotherset->{'diagnosis'} = $e; 139 } 140 } 141 } # End of message/delivery-status 142 } 143 return undef unless $recipients; 144 145 for my $e ( @$dscontents ) { 146 # Set default values if each value is empty. 147 $e->{'lhost'} ||= $permessage->{'rhost'}; 148 $e->{ $_ } ||= $permessage->{ $_ } || '' for keys %$permessage; 149 150 if( exists $anotherset->{'diagnosis'} && $anotherset->{'diagnosis'} ) { 151 # Copy alternative error message 152 $e->{'diagnosis'} ||= $anotherset->{'diagnosis'}; 153 if( $e->{'diagnosis'} =~ /\A\d+\z/ ) { 154 # Override the value of diagnostic code message 155 $e->{'diagnosis'} = $anotherset->{'diagnosis'}; 156 157 } else { 158 # More detailed error message is in "$anotherset" 159 my $as = undef; # status 160 my $ar = undef; # replycode 161 162 if( $e->{'status'} eq '' || $e->{'status'} eq '5.0.0' || $e->{'status'} eq '4.0.0' ) { 163 # Check the value of D.S.N. in $anotherset 164 $as = Sisimai::SMTP::Status->find($anotherset->{'diagnosis'}) || ''; 165 if( length($as) > 0 && substr($as, -4, 4) ne '.0.0' ) { 166 # The D.S.N. is neither an empty nor *.0.0 167 $e->{'status'} = $as; 168 } 169 } 170 171 if( $e->{'replycode'} eq '' || $e->{'replycode'} eq '500' || $e->{'replycode'} eq '400' ) { 172 # Check the value of SMTP reply code in $anotherset 173 $ar = Sisimai::SMTP::Reply->find($anotherset->{'diagnosis'}) || ''; 174 if( length($ar) > 0 && substr($ar, -2, 2) ne '00' ) { 175 # The SMTP reply code is neither an empty nor *00 176 $e->{'replycode'} = $ar; 177 } 178 } 179 180 if( $as || $ar && ( length($anotherset->{'diagnosis'}) > length($e->{'diagnosis'}) ) ) { 181 # Update the error message in $e->{'diagnosis'} 182 $e->{'diagnosis'} = $anotherset->{'diagnosis'}; 183 } 184 } 185 } 186 $e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'}); 187 188 for my $q ( keys %$messagesof ) { 189 # Guess an reason of the bounce 190 next unless grep { index($e->{'diagnosis'}, $_) > -1 } @{ $messagesof->{ $q } }; 191 $e->{'reason'} = $q; 192 last; 193 } 194 } 195 return { 'ds' => $dscontents, 'rfc822' => $emailsteak->[1] }; 196} 197 1981; 199__END__ 200 201=encoding utf-8 202 203=head1 NAME 204 205Sisimai::Lhost::GSuite - bounce mail parser class for C<G Suite>. 206 207=head1 SYNOPSIS 208 209 use Sisimai::Lhost::GSuite; 210 211=head1 DESCRIPTION 212 213Sisimai::Lhost::GSuite parses a bounce email which created by C<G Suite>. 214Methods in the module are called from only Sisimai::Message. 215 216=head1 CLASS METHODS 217 218=head2 C<B<description()>> 219 220C<description()> returns description string of this module. 221 222 print Sisimai::Lhost::GSuite->description; 223 224=head2 C<B<make(I<header data>, I<reference to body string>)>> 225 226C<make()> method parses a bounced email and return results as a array reference. 227See Sisimai::Message for more details. 228 229=head1 AUTHOR 230 231azumakuniyuki 232 233=head1 COPYRIGHT 234 235Copyright (C) 2017-2021 azumakuniyuki, All rights reserved. 236 237=head1 LICENSE 238 239This software is distributed under The BSD 2-Clause License. 240 241=cut 242 243