Files
scripts/Perl OTRS/Kernel/cpan-lib/Sisimai/Bite/Email/GSuite.pm
2024-10-14 00:08:40 +02:00

311 lines
12 KiB
Perl

package Sisimai::Bite::Email::GSuite;
use parent 'Sisimai::Bite::Email';
use feature ':5.10';
use strict;
use warnings;
my $Indicators = __PACKAGE__->INDICATORS;
my $MarkingsOf = {
'message' => qr/\A[*][*][ ].+[ ][*][*]\z/,
'error' => qr/\AThe[ ]response([ ]from[ ]the[ ]remote[ ]server)?[ ]was:\z/,
'html' => qr{\AContent-Type:[ ]*text/html;[ ]*charset=['"]?(?:UTF|utf)[-]8['"]?\z},
'rfc822' => qr{\AContent-Type:[ ]*(?:message/rfc822|text/rfc822-headers)\z},
};
my $MessagesOf = {
'userunknown' => ["because the address couldn't be found. Check for typos or unnecessary spaces and try again."],
'notaccept' => ['Null MX'],
'networkerror' => [' responded with code NXDOMAIN'],
};
sub headerlist { return ['X-Gm-Message-State'] }
sub description { 'G Suite: https://gsuite.google.com/' }
sub scan {
# Detect an error from G Suite (Transfer from G Suite to a destination host)
# @param [Hash] mhead Message headers of a bounce email
# @options mhead [String] from From header
# @options mhead [String] date Date header
# @options mhead [String] subject Subject header
# @options mhead [Array] received Received headers
# @options mhead [String] others Other required headers
# @param [String] mbody Message body of a bounce email
# @return [Hash, Undef] Bounce data list and message/rfc822 part
# or Undef if it failed to parse or the
# arguments are missing
# @since v4.21.0
my $class = shift;
my $mhead = shift // return undef;
my $mbody = shift // return undef;
return undef unless rindex($mhead->{'from'}, '<mailer-daemon@googlemail.com>') > -1;
return undef unless index($mhead->{'subject'}, 'Delivery Status Notification') > -1;
return undef unless $mhead->{'x-gm-message-state'};
my $dscontents = [__PACKAGE__->DELIVERYSTATUS];
my @hasdivided = split("\n", $$mbody);
my $rfc822part = ''; # (String) message/rfc822-headers part
my $rfc822list = []; # (Array) Each line in message/rfc822 part string
my $blanklines = 0; # (Integer) The number of blank lines
my $readcursor = 0; # (Integer) Points the current cursor position
my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
my $endoferror = 0; # (Integer) Flag for a blank line after error messages
my $emptylines = 0; # (Integer) The number of empty lines
my $connvalues = 0; # (Integer) Flag, 1 if all the value of $connheader have been set
my $anotherset = { # (Hash) Another error information
'diagnosis' => '',
};
my $connheader = {
'date' => '', # The value of Arrival-Date header
'lhost' => '', # The value of Reporting-MTA header
};
my $v = undef;
for my $e ( @hasdivided ) {
# Read each line between the start of the message and the start of rfc822 part.
unless( $readcursor ) {
# Beginning of the bounce message or delivery status part
$readcursor |= $Indicators->{'deliverystatus'} if $e =~ $MarkingsOf->{'message'};
}
unless( $readcursor & $Indicators->{'message-rfc822'} ) {
# Beginning of the original message part
if( $e =~ $MarkingsOf->{'rfc822'} ) {
$readcursor |= $Indicators->{'message-rfc822'};
next;
}
}
if( $readcursor & $Indicators->{'message-rfc822'} ) {
# After "message/rfc822"
unless( length $e ) {
$blanklines++;
last if $blanklines > 1;
next;
}
push @$rfc822list, $e;
} else {
# Before "message/rfc822"
next unless $readcursor & $Indicators->{'deliverystatus'};
if( $connvalues == scalar(keys %$connheader) ) {
# Final-Recipient: rfc822; kijitora@example.de
# Action: failed
# Status: 5.0.0
# Remote-MTA: dns; 192.0.2.222 (192.0.2.222, the server for the domain.)
# Diagnostic-Code: smtp; 550 #5.1.0 Address rejected.
# Last-Attempt-Date: Fri, 24 Mar 2017 23:34:10 -0700 (PDT)
$v = $dscontents->[-1];
if( $e =~ /\AFinal-Recipient:[ ]*(?:RFC|rfc)822;[ ]*([^ ]+)\z/ ) {
# Final-Recipient: rfc822; kijitora@example.de
if( $v->{'recipient'} ) {
# There are multiple recipient addresses in the message body.
push @$dscontents, __PACKAGE__->DELIVERYSTATUS;
$v = $dscontents->[-1];
}
$v->{'recipient'} = $1;
$recipients++;
} elsif( $e =~ /\AAction:[ ]*(.+)\z/ ) {
# Action: failed
$v->{'action'} = lc $1;
} elsif( $e =~ /\AStatus:[ ]*(\d[.]\d+[.]\d+)/ ) {
# Status: 5.0.0
$v->{'status'} = $1;
} elsif( $e =~ /\ARemote-MTA:[ ]*(?:DNS|dns);[ ]*(.+)\z/ ) {
# Remote-MTA: dns; 192.0.2.222 (192.0.2.222, the server for the domain.)
$v->{'rhost'} = lc $1;
$v->{'rhost'} = '' if $v->{'rhost'} =~ /\A\s+\z/; # Remote-MTA: DNS;
} elsif( $e =~ /\ALast-Attempt-Date:[ ]*(.+)\z/ ) {
# Last-Attempt-Date: Fri, 24 Mar 2017 23:34:10 -0700 (PDT)
$v->{'date'} = $1;
} else {
if( $e =~ /\ADiagnostic-Code:[ ]*(.+?);[ ]*(.+)\z/ ) {
# Diagnostic-Code: smtp; 550 #5.1.0 Address rejected.
$v->{'spec'} = uc $1;
$v->{'diagnosis'} = $2;
} else {
# Append error messages continued from the previous line
if( ! $endoferror && $v->{'diagnosis'} ) {
$endoferror ||= 1 if $e eq '';
$endoferror ||= 1 if index($e, '--') == 0;
next if $endoferror;
next unless index($e, ' ') == 0;
$v->{'diagnosis'} .= $e;
}
}
}
} else {
# Reporting-MTA: dns; googlemail.com
# Received-From-MTA: dns; sironeko@example.jp
# Arrival-Date: Fri, 24 Mar 2017 23:34:07 -0700 (PDT)
# X-Original-Message-ID: <06C1ED5C-7E02-4036-AEE1-AA448067FB2C@example.jp>
if( $e =~ /\AReporting-MTA:[ ]*(?:DNS|dns);[ ]*(.+)\z/ ) {
# Reporting-MTA: dns; googlemail.com
next if $connheader->{'lhost'};
$connheader->{'lhost'} = lc $1;
$connvalues++;
} elsif( $e =~ /\AArrival-Date:[ ]*(.+)\z/ ) {
# Arrival-Date: Wed, 29 Apr 2009 16:03:18 +0900
next if $connheader->{'date'};
$connheader->{'date'} = $1;
$connvalues++;
} else {
# Detect SMTP session error or connection error
if( $e =~ $MarkingsOf->{'error'} ) {
# The response from the remote server was:
$anotherset->{'diagnosis'} .= $e;
} else {
# ** Address not found **
#
# Your message wasn't delivered to * because the address couldn't be found.
# Check for typos or unnecessary spaces and try again.
#
# The response from the remote server was:
# 550 #5.1.0 Address rejected.
next if $e =~ $MarkingsOf->{'html'};
if( $anotherset->{'diagnosis'} ) {
# Continued error messages from the previous line like
# "550 #5.1.0 Address rejected."
next if $e =~ /\AContent-Type:/;
next if $emptylines > 5;
unless( length $e ) {
# Count and next()
$emptylines += 1;
next;
}
$anotherset->{'diagnosis'} .= ' '.$e
} else {
# ** Address not found **
#
# Your message wasn't delivered to * because the address couldn't be found.
# Check for typos or unnecessary spaces and try again.
next unless $e;
next unless $e =~ $MarkingsOf->{'message'};
$anotherset->{'diagnosis'} = $e;
}
}
}
}
} # End of if: rfc822
}
return undef unless $recipients;
for my $e ( @$dscontents ) {
# Set default values if each value is empty.
map { $e->{ $_ } ||= $connheader->{ $_ } || '' } keys %$connheader;
if( exists $anotherset->{'diagnosis'} && $anotherset->{'diagnosis'} ) {
# Copy alternative error message
$e->{'diagnosis'} ||= $anotherset->{'diagnosis'};
if( $e->{'diagnosis'} =~ /\A\d+\z/ ) {
# Override the value of diagnostic code message
$e->{'diagnosis'} = $anotherset->{'diagnosis'};
} else {
# More detailed error message is in "$anotherset"
my $as = undef; # status
my $ar = undef; # replycode
if( $e->{'status'} eq '' || $e->{'status'} eq '5.0.0' || $e->{'status'} eq '4.0.0' ) {
# Check the value of D.S.N. in $anotherset
$as = Sisimai::SMTP::Status->find($anotherset->{'diagnosis'});
if( length($as) > 0 && substr($as, -4, 4) ne '.0.0' ) {
# The D.S.N. is neither an empty nor *.0.0
$e->{'status'} = $as;
}
}
if( $e->{'replycode'} eq '' || $e->{'replycode'} eq '500' || $e->{'replycode'} eq '400' ) {
# Check the value of SMTP reply code in $anotherset
$ar = Sisimai::SMTP::Reply->find($anotherset->{'diagnosis'});
if( length($ar) > 0 && substr($ar, -2, 2) ne '00' ) {
# The SMTP reply code is neither an empty nor *00
$e->{'replycode'} = $ar;
}
}
if( $as || $ar && ( length($anotherset->{'diagnosis'}) > length($e->{'diagnosis'}) ) ) {
# Update the error message in $e->{'diagnosis'}
$e->{'diagnosis'} = $anotherset->{'diagnosis'};
}
}
}
$e->{'diagnosis'} = Sisimai::String->sweep($e->{'diagnosis'});
$e->{'agent'} = __PACKAGE__->smtpagent;
for my $q ( keys %$MessagesOf ) {
# Guess an reason of the bounce
next unless grep { index($e->{'diagnosis'}, $_) > -1 } @{ $MessagesOf->{ $q } };
$e->{'reason'} = $q;
last;
}
}
$rfc822part = Sisimai::RFC5322->weedout($rfc822list);
return { 'ds' => $dscontents, 'rfc822' => $$rfc822part };
}
1;
__END__
=encoding utf-8
=head1 NAME
Sisimai::Bite::Email::GSuite - bounce mail parser class for C<G Suite>.
=head1 SYNOPSIS
use Sisimai::Bite::Email::GSuite;
=head1 DESCRIPTION
Sisimai::Bite::Email::GSuite parses a bounce email which created by C<G Suite>.
Methods in the module are called from only Sisimai::Message.
=head1 CLASS METHODS
=head2 C<B<description()>>
C<description()> returns description string of this module.
print Sisimai::Bite::Email::GSuite->description;
=head2 C<B<smtpagent()>>
C<smtpagent()> returns MTA name.
print Sisimai::Bite::Email::GSuite->smtpagent;
=head2 C<B<scan(I<header data>, I<reference to body string>)>>
C<scan()> method parses a bounced email and return results as a array reference.
See Sisimai::Message for more details.
=head1 AUTHOR
azumakuniyuki
=head1 COPYRIGHT
Copyright (C) 2017-2018 azumakuniyuki, All rights reserved.
=head1 LICENSE
This software is distributed under The BSD 2-Clause License.
=cut