File Coverage

lib/Sisimai/RFC3834.pm
Criterion Covered Total %
statement 82 82 100.0
branch 39 44 88.6
condition 6 10 60.0
subroutine 5 5 100.0
pod 2 2 100.0
total 134 143 93.7


line stmt bran cond sub pod time code
1             package Sisimai::RFC3834;
2 8     8   1862 use v5.26;
  8         27  
3 8     8   34 use strict;
  8         12  
  8         190  
4 8     8   26 use warnings;
  8         12  
  8         7531  
5              
6             # http://tools.ietf.org/html/rfc3834
7 1     1 1 3 sub description { 'Detector for auto replied message' }
8             sub inquire {
9             # Detect auto reply message as RFC3834
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 decode or the arguments are missing
14             # @since v4.1.28
15 93     93 1 1817 my $class = shift;
16 93 100 100     184 my $mhead = shift // return undef; return undef unless keys %$mhead;
  92         230  
17 90 100 50     180 my $mbody = shift // return undef; return undef unless ref $mbody eq 'SCALAR';
  90         206  
18 89         109 my $lower = {};
19              
20 89         196 my $markingsof = {'boundary' => '__SISIMAI_PSEUDO_BOUNDARY__'};
21 89         251 my $lowerlabel = ['from', 'to', 'subject', 'auto-submitted', 'precedence', 'x-apple-action'];
22              
23 89         118 for my $e ( @$lowerlabel ) {
24             # Set lower-cased value of each header related to auto-response
25 534 100       735 next unless exists $mhead->{ $e };
26 279         561 $lower->{ $e } = lc $mhead->{ $e };
27             }
28              
29 89         140 state $donotparse = {
30             'from' => ['root@', 'postmaster@', 'mailer-daemon@'],
31             'to' => ['root@'],
32             'subject' => [
33             'security information for', # sudo(1)
34             'mail failure -', # Exim
35             ],
36             };
37 89         117 state $autoreply0 = {
38             # http://www.iana.org/assignments/auto-submitted-keywords/auto-submitted-keywords.xhtml
39             'auto-submitted' => ['auto-generated', 'auto-replied', 'auto-notified'],
40             'precedence' => ['auto_reply'],
41             'subject' => ['auto:', 'auto response:', 'automatic reply:', 'out of office:', 'out of the office:'],
42             'x-apple-action' => ['vacation'],
43             };
44 89         102 state $subjectset = qr{\A(?>
45             (?:.+?)?re:
46             |auto(?:[ ]response):
47             |automatic[ ]reply:
48             |out[ ]of[ ]office:
49             )
50             [ ]*(.+)\z
51             }x;
52 89         110 state $suspending = [
53             ["this email inbox", " is no longer in use."],
54             ];
55              
56 89         99 my $leave = 0; DETECT_EXCLUSION_MESSAGE: for my $e ( keys %$donotparse ) {
  89         203  
57             # Exclude message from root@
58 263 100       325 next unless exists $lower->{ $e };
59 245 100       365 next unless grep { index($lower->{ $e }, $_) > -1 } $donotparse->{ $e }->@*;
  511         793  
60 18         43 $leave = 1;
61 18         28 last;
62             }
63 89 100       241 return undef if $leave;
64              
65 71         95 my $match = 0; DETECT_AUTO_REPLY_MESSAGE0: for my $e ( keys %$autoreply0 ) {
  71         135  
66             # RFC3834 Auto-Submitted and other headers
67 226 100       318 next unless exists $lower->{ $e };
68 75 100       97 next unless grep { index($lower->{ $e }, $_) == 0 } $autoreply0->{ $e }->@*;
  319         426  
69              
70 31         37 $match++;
71 31         34 last;
72             }
73 71 100       252 return undef unless $match;
74              
75 31         97 require Sisimai::Lhost;
76 31         98 my $dscontents = [Sisimai::Lhost->DELIVERYSTATUS]; my $v = $dscontents->[-1];
  31         43  
77 31         38 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
78 31         35 my $maxmsgline = 5; # (Integer) Max message length(lines)
79 31         31 my $haveloaded = 0; # (Integer) The number of lines loaded from message body
80 31         27 my $blanklines = 0; # (Integer) Counter for countinuous blank lines
81 31         28 my $countuntil = 1; # (Integer) Maximun value of blank lines in the body part
82              
83             RECIPIENT_ADDRESS: {
84             # Try to get the address of the recipient
85 31         27 for my $e ('reply-to', 'from', 'return-path') {
  31         40  
86             # Get the recipient address
87 57 100       91 next unless exists $mhead->{ $e };
88              
89 31         45 $v->{'recipient'} = $mhead->{ $e };
90 31         24 last;
91             }
92              
93 31 50       72 if( $v->{'recipient'} ) {
94             # Clean-up the recipient address
95 31         137 $v->{'recipient'} = Sisimai::Address->s3s4($v->{'recipient'});
96 31         39 $recipients++;
97             }
98             }
99 31 50       59 return undef unless $recipients;
100              
101 31 100       57 if( $mhead->{'content-type'} ) {
102             # Get the boundary string and set regular expression for matching with the boundary string.
103 25         75 my $q = Sisimai::RFC2045->boundary($mhead->{'content-type'}, 0);
104 25 100       70 $markingsof->{'boundary'} = $q if $q;
105             }
106              
107             MESSAGE_BODY: {
108             # Get vacation message
109 31         28 for my $e ( split("\n", $$mbody) ) {
  31         85  
110             # Read the first 5 lines except a blank line
111 73 50       137 $countuntil += 1 if index($e, $markingsof->{'boundary'}) > -1;
112              
113 73 100       115 unless( length $e ) {
114             # Check a blank line
115 21 100       31 last if ++$blanklines > $countuntil;
116 16         23 next;
117             }
118 52 100       94 next if rindex($e, ' ') < 0;
119 37 50 33     172 next if index($e, 'Content-Type') == 0 || index($e, 'Content-Transfer') == 0;
120              
121 37         62 $v->{'diagnosis'} .= $e.' ';
122 37         44 $haveloaded++;
123 37 50       74 last if $haveloaded >= $maxmsgline;
124             }
125 31   66     87 $v->{'diagnosis'} ||= $mhead->{'subject'};
126             }
127 31         38 $v->{'reason'} = 'vacation';
128              
129 31         54 my $cv = lc $v->{'diagnosis'}; for my $e ( @$suspending ) {
  31         37  
130             # Check that the auto-replied message indicates the "Suspend" reason or not.
131 31 100       98 next unless Sisimai::String->aligned(\$cv, $e);
132 5         10 $v->{'reason'} = 'suspend'; last;
  5         5  
133             }
134              
135 31         39 $v->{'date'} = $mhead->{'date'};
136 31         58 $v->{'status'} = '';
137              
138             # Get the Subject header from the original message
139 31 100       346 my $rfc822part = $lower->{'subject'} =~ $subjectset ? 'Subject: '.$1."\n" : '';
140 31         171 return {"ds" => $dscontents, "rfc822" => $rfc822part};
141             }
142              
143             1;
144             __END__