This commit is contained in:
2024-10-14 00:08:40 +02:00
parent dbfba56f66
commit 1462d52e13
4572 changed files with 2658864 additions and 0 deletions

View File

@@ -0,0 +1,206 @@
package Sisimai::Rhost::ExchangeOnline;
use feature ':5.10';
use strict;
use warnings;
# https://technet.microsoft.com/en-us/library/bb232118
my $StatusList = {
'4.3.1' => [{ 'reason' => 'systemfull', 'string' => 'Insufficient system resources' }],
'4.3.2' => [{ 'reason' => 'notaccept', 'string' => 'System not accepting network messages' }],
'4.4.2' => [{ 'reason' => 'blocked', 'string' => 'Connection dropped' }],
'4.7.26' => [{
'reason' => 'securityerror',
'string' => 'must pass either SPF or DKIM validation, this message is not signed'
}],
'5.0.0' => [{ 'reason' => 'blocked', 'string' => 'HELO / EHLO requires domain address' }],
'5.1.4' => [{ 'reason' => 'systemerror', 'string' => 'Destination mailbox address ambiguous' }],
'5.2.1' => [{ 'reason' => 'suspend', 'string' => 'Mailbox cannot be accessed' }],
'5.2.2' => [{ 'reason' => 'mailboxfull', 'string' => 'Mailbox full' }],
'5.2.3' => [{ 'reason' => 'exceedlimit', 'string' => 'Message too large' }],
'5.2.4' => [{ 'reason' => 'systemerror', 'string' => 'Mailing list expansion problem' }],
'5.3.3' => [{ 'reason' => 'systemfull', 'string' => 'Unrecognized command' }],
'5.3.4' => [{ 'reason' => 'mesgtoobig', 'string' => 'Message too big for system' }],
'5.3.5' => [{ 'reason' => 'systemerror', 'string' => 'System incorrectly configured' }],
'5.4.1' => [{ 'reason' => 'userunknown', 'string' => 'Recipient address rejected: Access denied' }],
'5.4.14' => [{ 'reason' => 'networkerror','string' => 'Hop count exceeded' }],
'5.5.2' => [{ 'reason' => 'syntaxerror', 'string' => 'Send hello first' }],
'5.5.3' => [{ 'reason' => 'syntaxerror', 'string' => 'Too many recipients' }],
'5.5.4' => [{ 'reason' => 'filtered', 'string' => 'Invalid domain name' }],
'5.5.6' => [{ 'reason' => 'contenterror','string' => 'Invalid message content' }],
'5.7.1' => [
{ 'reason' => 'securityerror', 'string' => 'Delivery not authorized' },
{ 'reason' => 'securityerror', 'string' => 'Client was not authenticated' },
{ 'reason' => 'norelaying', 'string' => 'Unable to relay' },
],
'5.7.25' => [{ 'reason' => 'blocked', 'string' => 'must have a reverse DNS record' }],
'5.7.506' => [{ 'reason' => 'blocked', 'string' => 'Bad HELO' }],
'5.7.508' => [{ 'reason' => 'toomanyconn', 'string' => 'has exceeded permitted limits within ' }],
'5.7.509' => [{ 'reason' => 'rejected', 'string' => 'does not pass DMARC verification' }],
'5.7.510' => [{ 'reason' => 'notaccept', 'string' => 'does not accept email over IPv6' }],
'5.7.511' => [{ 'reason' => 'blocked', 'string' => 'banned sender' }],
'5.7.512' => [{ 'reason' => 'contenterror', 'string' => 'message must be RFC 5322' }],
};
my $ReStatuses = {
qr/\A4[.]4[.][17]\z/ => [
{ 'reason' => 'expired', 'string' => ['Connection timed out', 'Message expired'] }
],
qr/\A4[.]7[.][568]\d\d\z/ => [
{ 'reason' => 'securityerror', 'string' => ['Access denied, please try again later'] }
],
qr/\A5[.]1[.][07]\z/ => [
{ 'reason' => 'rejected', 'string' => ['Sender denied', 'Invalid address'] }
],
qr/\A5[.]1[.][123]\z/ => [{
'reason' => 'userunknown',
'string' => [
'Bad destination mailbox address',
'Invalid X.400 address',
'Invalid recipient address',
]
}],
qr/\A5[.]4[.][46]\z/ => [{
'reason' => 'networkerror',
'string' => ['Invalid arguments', 'Routing loop detected'],
}],
qr/\A5[.]7[.][13]\z/ => [{
'reason' => 'securityerror',
'string' => ['Delivery not authorized', 'Not Authorized'],
}],
qr/\A5[.]7[.]50[1-3]\z/ => [{
'reason' => 'spamdetected',
'string' => [
'Access denied, spam abuse detected',
'Access denied, banned sender'
],
}],
qr/\A5[.]7[.]50[457]\z/ => [{
'reason' => 'filtered',
'string' => [
'Recipient address rejected: Access denied',
'Access denied, banned recipient',
'Access denied, rejected by recipient'
]
}],
qr/\A5[.]7[.]6\d\d\z/ => [
{ 'reason' => 'blocked', 'string' => ['Access denied, banned sending IP '] }
],
qr/\A5[.]7[.]7\d\d\z/ => [
{ 'reason' => 'toomanyconn', 'string' => ['Access denied, tenant has exceeded threshold'] }
],
};
my $MessagesOf = {
# Copied and converted from Sisimai::Bite::Email::Exchange2007
'expired' => ['QUEUE.Expired'],
'hostunknown' => ['SMTPSEND.DNS.NonExistentDomain'],
'mesgtoobig' => ['RESOLVER.RST.RecipSizeLimit', 'RESOLVER.RST.RecipientSizeLimit'],
'networkerror' => ['SMTPSEND.DNS.MxLoopback'],
'rejected' => ['RESOLVER.RST.NotAuthorized'],
'securityerror' => ['RESOLVER.RST.AuthRequired'],
'systemerror' => ['RESOLVER.ADR.Ambiguous', 'RESOLVER.ADR.BadPrimary', 'RESOLVER.ADR.InvalidInSmtp'],
'toomanyconn' => ['RESOLVER.ADR.RecipLimit', 'RESOLVER.ADR.RecipientLimit'],
'userunknown' => [
'RESOLVER.ADR.RecipNotFound',
'RESOLVER.ADR.RecipientNotFound',
'RESOLVER.ADR.ExRecipNotFound',
'RESOLVER.ADR.ExRecipientNotFound',
],
};
sub get {
# Detect bounce reason from Exchange 2013 and Office 365
# @param [Sisimai::Data] argvs Parsed email object
# @return [String] The bounce reason for Exchange Online
# @see https://technet.microsoft.com/en-us/library/bb232118
my $class = shift;
my $argvs = shift // return undef;
return $argvs->reason if $argvs->reason;
my $statuscode = $argvs->deliverystatus;
my $statusmesg = $argvs->diagnosticcode;
my $reasontext = '';
for my $e ( keys %$StatusList ) {
# Try to compare with each status code as a key
next unless $statuscode eq $e;
for my $f ( @{ $StatusList->{ $e } } ) {
# Try to compare with each string of error messages
next if index($statusmesg, $f->{'string'}) == -1;
$reasontext = $f->{'reason'};
last;
}
last if $reasontext;
}
if( not $reasontext ) {
for my $e ( keys %$ReStatuses ) {
# Try to compare with each string of delivery status codes
next unless $statuscode =~ $e;
for my $f ( @{ $ReStatuses->{ $e } } ) {
# Try to compare with each string of error messages
for my $g ( @{ $f->{'string'} } ) {
next if index($statusmesg, $g) == -1;
$reasontext = $f->{'reason'};
last;
}
last if $reasontext;
}
last if $reasontext;
}
if( not $reasontext ) {
# D.S.N. included in the error message did not matched with any key
# in $StatusList, $ReStatuses
for my $e ( keys %$MessagesOf ) {
# Try to compare with error messages defined in MessagesOf
for my $f ( @{ $MessagesOf->{ $e } } ) {
next if index($statusmesg, $f) == -1;
$reasontext = $e;
last;
}
last if $reasontext;
}
}
}
return $reasontext;
}
1;
__END__
=encoding utf-8
=head1 NAME
Sisimai::Rhost::ExchangeOnline - Detect the bounce reason returned from on-premises
Exchange 2013 and Office 365.
=head1 SYNOPSIS
use Sisimai::Rhost;
=head1 DESCRIPTION
Sisimai::Rhost detects the bounce reason from the content of Sisimai::Data
object as an argument of get() method when the value of C<rhost> of the object
is "*.protection.outlook.com". This class is called only Sisimai::Data class.
=head1 CLASS METHODS
=head2 C<B<get(I<Sisimai::Data Object>)>>
C<get()> detects the bounce reason.
=head1 AUTHOR
azumakuniyuki
=head1 COPYRIGHT
Copyright (C) 2016-2018 azumakuniyuki, All rights reserved.
=head1 LICENSE
This software is distributed under The BSD 2-Clause License.
=cut

View File

@@ -0,0 +1,90 @@
package Sisimai::Rhost::FrancePTT;
use feature ':5.10';
use strict;
use warnings;
my $ErrorCodes = {
'103' => 'blocked', # Service refuse. Veuillez essayer plus tard.
'104' => 'toomanyconn', # Too many connections, slow down. LPN105_104
'105' => undef, # Veuillez essayer plus tard.
'109' => undef, # Veuillez essayer plus tard. LPN003_109
'201' => undef, # Veuillez essayer plus tard. OFR004_201
'305' => 'securityerror', # 550 5.7.0 Code d'authentification invalide OFR_305
'401' => 'blocked', # 550 5.5.0 SPF: *** is not allowed to send mail. LPN004_401
'402' => 'securityerror', # 550 5.5.0 Authentification requise. Authentication Required. LPN105_402
'403' => 'rejected', # 5.0.1 Emetteur invalide. Invalid Sender.
'405' => 'rejected', # 5.0.1 Emetteur invalide. Invalid Sender. LPN105_405
'415' => 'rejected', # Emetteur invalide. Invalid Sender. OFR_415
'416' => 'userunknown', # 550 5.1.1 Adresse d au moins un destinataire invalide. Invalid recipient. LPN416
'417' => 'mailboxfull', # 552 5.1.1 Boite du destinataire pleine. Recipient overquota.
'418' => 'userunknown', # Adresse d au moins un destinataire invalide
'420' => 'suspend', # Boite du destinataire archivee. Archived recipient.
'421' => 'rejected', # 5.5.3 Mail from not owned by user. LPN105_421.
'423' => undef, # Service refused, please try later. LPN105_423
'424' => undef, # Veuillez essayer plus tard. LPN105_424
'506' => 'spamdetected', # Mail rejete. Mail rejected. OFR_506 [506]
'510' => 'blocked', # Veuillez essayer plus tard. service refused, please try later. LPN004_510
'513' => undef, # Mail rejete. Mail rejected. OUK_513
'514' => 'mesgtoobig', # Taille limite du message atteinte
};
sub get {
# Detect bounce reason from Orange and La Poste
# @param [Sisimai::Data] argvs Parsed email object
# @return [String] The bounce reason for Orange, La Poste
my $class = shift;
my $argvs = shift // return undef;
return $argvs->reason if $argvs->reason;
my $statusmesg = $argvs->diagnosticcode;
my $reasontext = '';
if( $statusmesg =~ /\b(LPN|OFR|OUK)(_[0-9]{3}|[0-9]{3}[-_][0-9]{3})\b/ ) {
# OUK_513, LPN105-104, OFR102-104
my $v = sprintf("%03d", substr($1.$2, -3, 3));
$reasontext = $ErrorCodes->{ $v } || 'undefined';
}
return $reasontext;
}
1;
__END__
=encoding utf-8
=head1 NAME
Sisimai::Rhost::FrancePTT - Detect the bounce reason returned from Orange and
La Poste.
=head1 SYNOPSIS
use Sisimai::Rhost;
=head1 DESCRIPTION
Sisimai::Rhost detects the bounce reason from the content of Sisimai::Data
object as an argument of get() method when the value of C<rhost> of the object
end with "laposte.net" or "orange.fr".
This class is called only Sisimai::Data class.
=head1 CLASS METHODS
=head2 C<B<get(I<Sisimai::Data Object>)>>
C<get()> detects the bounce reason.
=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

View File

@@ -0,0 +1,106 @@
package Sisimai::Rhost::GoDaddy;
use feature ':5.10';
use strict;
use warnings;
# https://www.godaddy.com/help/what-does-my-email-bounceback-mean-3568
my $ErrorCodes = {
'IB103' => 'blocked', # 554 Connection refused. This IP has a poor reputation on Cloudmark Sender Intelligence (CSI). IB103
'IB104' => 'blocked', # 554 Connection refused. This IP is listed on the Spamhaus Block List (SBL). IB104
'IB105' => 'blocked', # 554 Connection refused. This IP is listed on the Exploits Block List (XBL). IB105
'IB106' => 'blocked', # 554 Connection refused. This IP is listed on the Policy Block List (PBL). IB106
'IB007' => 'toomanyconn', # 421 Connection refused, too many sessions from This IP. Please lower the number of concurrent sessions. IB007
'IB101' => 'expired', # 421 Server temporarily unavailable. Try again later. IB101
'IB108' => 'blocked', # 421 Temporarily rejected. Reverse DNS for this IP failed. IB108
'IB110' => 'blocked', # 554 This IP has been temporarily blocked for attempting to send too many messages containing content judged to be spam by the Internet community. IB110
'IB111' => 'blocked', # 554 This IP has been blocked for the day, for attempting to send too many messages containing content judged to be spam by the Internet community. IB111
'IB112' => 'blocked', # 554 This IP has been temporarily blocked for attempting to mail too many invalid recipients. IB112
'IB113' => 'blocked', # 554 This IP has been blocked for the day, for attempting to mail too many invalid recipients. IB113
'IB212' => 'spamdetected', # 552 This message has been rejected due to content judged to be spam by the Internet community. IB212
'IB401' => 'securityerror', # 535 Authentication not allowed on IBSMTP Servers. IB401
'IB501' => 'rejected', # 550 holly@coolexample.com Blank From: addresses are not allowed. Please provide a valid From. IB501
'IB502' => 'rejected', # 550 holly@coolexample.com IP addresses are not allowed as a From: Address. Please provide a valid From. IB502
'IB504' => 'toomanyconn', # 550 This IP has sent too many messages this hour. IB504
'IB506' => 'rejected', # 550 coolexample.com From: Domain is invalid. Please provide a valid From: IB506
'IB508' => 'rejected', # 550 holly@coolexample.com Invalid SPF record. Please inspect your SPF settings, and try again. IB508
'IB510' => 'toomanyconn', # 550 This message has exceeded the max number of messages per session. Please open a new session and try again. IB510
'IB607' => 'toomanyconn', # 550 This IP has sent too many to too many recipients this hour. IB607
'IB705' => 'virusdetected', # 552 Virus infected message rejected. IB705
};
my $MessagesOf = {
'blocked' => ['553 http://www.spamhaus.org/query/bl?ip=', '554 RBL Reject.'],
'expired' => ['Delivery timeout', "451 Sorry, I wasn't able to establish an SMTP connection."],
'suspend' => ['Account disabled'],
'mailboxfull' => ['Account storage limit'],
'userunknown' => ['Account does not exist', '550 Recipient not found.'],
};
sub get {
# Detect bounce reason from GoDaddy (smtp.secureserver.net)
# @param [Sisimai::Data] argvs Parsed email object
# @return [String] The bounce reason for GoDaddy
# @see https://www.godaddy.com/help/what-does-my-email-bounceback-mean-3568
my $class = shift;
my $argvs = shift // return undef;
return $argvs->reason if $argvs->reason;
my $statusmesg = $argvs->diagnosticcode;
my $reasontext = '';
if( $statusmesg =~ /\s(IB\d{3})\b/ ) {
# 192.0.2.22 has sent to too many recipients this hour. IB607 ...
$reasontext = $ErrorCodes->{ $1 };
} else {
# 553 http://www.spamhaus.org/query/bl?ip=192.0.0.222
for my $e ( keys %$MessagesOf ) {
for my $f ( @{ $MessagesOf->{ $e } } ) {
next if index($statusmesg, $f) == -1;
$reasontext = $e;
last
}
last if $reasontext;
}
}
return $reasontext;
}
1;
__END__
=encoding utf-8
=head1 NAME
Sisimai::Rhost::GoDaddy - Detect the bounce reason returned from GoDaddy.
=head1 SYNOPSIS
use Sisimai::Rhost;
=head1 DESCRIPTION
Sisimai::Rhost detects the bounce reason from the content of Sisimai::Data
object as an argument of get() method when the value of C<rhost> of the object
end with "secureserver.net". This class is called only Sisimai::Data class.
=head1 CLASS METHODS
=head2 C<B<get(I<Sisimai::Data Object>)>>
C<get()> detects the bounce reason.
=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

View File

@@ -0,0 +1,167 @@
package Sisimai::Rhost::GoogleApps;
use feature ':5.10';
use strict;
use warnings;
my $StatusList = {
# https://support.google.com/a/answer/3726730
'X.1.1' => [{ 'reason' => 'userunknown', 'string' => ['The email account that you tried to reach does not exist.'] }],
'X.1.2' => [{ 'reason' => 'hostunknown', 'string' => ["We weren't able to find the recipient domain."] }],
'X.2.1' => [
{ 'reason' => 'suspend', 'string' => ['The email account that you tried to reach is disabled.'] },
{ 'reason' => 'undefined', 'string' => ['The user you are trying to contact is receiving mail '] },
],
'X.2.2' => [{ 'reason' => 'mailboxfull', 'string' => ['The email account that you tried to reach is over quota.'] }],
'X.2.3' => [{ 'reason' => 'exceedlimit', 'string' => ["Your message exceeded Google's message size limits."] }],
'X.3.0' => [
{ 'reason' => 'syntaxerror', 'string' => ['Multiple destination domains per transaction is unsupported.'] },
{ 'reason' => 'undefined', 'string' => ['Mail server temporarily rejected message.'] },
],
'X.4.2' => [{ 'reason' => 'expired', 'string' => ['Timeout - closing connection.'] }],
'X.4.5' => [
{ 'reason' => 'exceedlimit', 'string' => ['Daily sending quota exceeded.'] },
{ 'reason' => 'undefined', 'string' => ['Server busy, try again later.'] },
],
'X.5.0' => [{ 'reason' => 'syntaxerror', 'string' => ['SMTP protocol violation'] }],
'X.5.1' => [
{ 'reason' => 'securityerror', 'string' => ['Authentication Required.'] },
{
'reason' => 'syntaxerror',
'string' => [
'STARTTLS may not be repeated',
'Too many unrecognized commands, goodbye.',
'Unimplemented command.',
'Unrecognized command.',
'EHLO/HELO first.',
'MAIL first.',
'RCPT first.',
],
},
],
'X.5.2' => [
{ 'reason' => 'securityerror', 'string' => ['Cannot Decode response.'] }, # 2FA related error, maybe.
{ 'reason' => 'syntaxerror', 'string' => ['Syntax error.'] },
],
'X.5.3' => [
{ 'reason' => 'mailboxfull', 'string' => ['Domain policy size per transaction exceeded,'] },
{ 'reason' => 'policyviolation','string' => ['Your message has too many recipients.'] },
],
'X.5.4' => [{ 'reason' => 'syntaxerror', 'string' => ['Optional Argument not permitted for that AUTH mode.'] }],
'X.6.0' => [
{ 'reason' => 'contenterror', 'string' => ['Mail message is malformed.'] },
{ 'reason' => 'networkerror', 'string' => ['Message exceeded 50 hops'] }
],
'X.7.0' => [
{
'reason' => 'blocked',
'string' => [
'IP not in whitelist for RCPT domain, closing connection.',
'Our system has detected an unusual rate of unsolicited mail originating from your IP address.',
],
},
{
'reason' => 'expired',
'string' => [
'Temporary System Problem. Try again later.',
'Try again later, closing connection.',
],
},
{
'reason' => 'securityerror',
'string' => [
'TLS required for RCPT domain, closing connection.',
'No identity changes permitted.',
'Must issue a STARTTLS command first.',
'Too Many Unauthenticated commands.',
],
},
{ 'reason' => 'systemerror', 'string' => ['Cannot authenticate due to temporary system problem.'] },
{ 'reason' => 'norelaying', 'string' => ['Mail relay denied.'] },
{ 'reason' => 'rejected', 'string' => ['Mail Sending denied.'] },
],
'X.7.1' => [
{ 'reason' => 'mailboxfull', 'string' => ['Email quota exceeded.'] },
{
'reason' => 'securityerror',
'string' => [
'Application-specific password required.',
'Please log in with your web browser and then try again.',
'Username and Password not accepted.',
],
},
{
'reason' => 'blocked',
'string' => [
'Our system has detected an unusual rate of unsolicited mail originating from your IP address.',
"The IP you're using to send mail is not authorized to send email directly to our servers.",
],
},
{ 'reason' => 'spamdetected', 'string' => ['Our system has detected that this message is likely unsolicited mail.'] },
{ 'reason' => 'policyviolation','string' => ['The user or domain that you are sending to (or from) has a policy'] },
{ 'reason' => 'rejected', 'string' => ['Unauthenticated email is not accepted from this domain.'] },
],
'X.7.4' => [{ 'reason' => 'syntaxerror', 'string' => ['Unrecognized Authentication Type.'] }],
};
sub get {
# Detect bounce reason from Google Apps
# @param [Sisimai::Data] argvs Parsed email object
# @return [String] The bounce reason for Google Apps
# @see https://support.google.com/a/answer/3726730?hl=en
my $class = shift;
my $argvs = shift // return undef;
return $argvs->reason if $argvs->reason;
my $reasontext = '';
my $statuscode = $argvs->deliverystatus;
substr($statuscode, 0, 1, 'X');
return '' unless scalar @{ $StatusList->{ $statuscode } };
for my $e ( @{ $StatusList->{ $statuscode } } ) {
# Try to match
next unless grep { rindex($argvs->diagnosticcode, $_) > -1 } @{ $e->{'string'} };
$reasontext = $e->{'reason'};
last;
}
return $reasontext;
}
1;
__END__
=encoding utf-8
=head1 NAME
Sisimai::Rhost::GoogleApps - Detect the bounce reason returned from Google Apps.
=head1 SYNOPSIS
use Sisimai::Rhost;
=head1 DESCRIPTION
Sisimai::Rhost detects the bounce reason from the content of Sisimai::Data
object as an argument of get() method when the value of C<rhost> of the object
is "aspmx.l.google.com". This class is called only Sisimai::Data class.
=head1 CLASS METHODS
=head2 C<B<get(I<Sisimai::Data Object>)>>
C<get()> detects the bounce reason.
=head1 AUTHOR
azumakuniyuki
=head1 COPYRIGHT
Copyright (C) 2014-2016,2018 azumakuniyuki, All rights reserved.
=head1 LICENSE
This software is distributed under The BSD 2-Clause License.
=cut

View File

@@ -0,0 +1,69 @@
package Sisimai::Rhost::KDDI;
use feature ':5.10';
use strict;
use warnings;
my $MessagesOf = {
'filtered' => '550 : User unknown', # The response was: 550 : User unknown
'userunknown' => '>: User unknown', # The response was: 550 <...>: User unknown
};
sub get {
# Detect bounce reason from au(KDDI)
# @param [Sisimai::Data] argvs Parsed email object
# @return [String] The bounce reason au.com and ezweb.ne.jp
my $class = shift;
my $argvs = shift // return undef;
my $statusmesg = $argvs->diagnosticcode;
my $reasontext = '';
for my $e ( keys %$MessagesOf ) {
# Try to match the error message with message patterns defined in $MessagesOf
next unless rindex($statusmesg, $MessagesOf->{ $e }) > -1;
$reasontext = $e;
last;
}
return $reasontext;
}
1;
__END__
=encoding utf-8
=head1 NAME
Sisimai::Rhost::KDDI - Detect the bounce reason returned from au (KDDI).
=head1 SYNOPSIS
use Sisimai::Rhost;
=head1 DESCRIPTION
Sisimai::Rhost detects the bounce reason from the content of Sisimai::Data
object as an argument of get() method when the value of C<rhost> of the object
is "msmx.au.com" or "lsean.ezweb.ne.jp".
This class is called only Sisimai::Data class.
=head1 CLASS METHODS
=head2 C<B<get(I<Sisimai::Data Object>)>>
C<get()> detects the bounce reason.
=head1 AUTHOR
azumakuniyuki
=head1 COPYRIGHT
Copyright (C) 2018 azumakuniyuki, All rights reserved.
=head1 LICENSE
This software is distributed under The BSD 2-Clause License.
=cut