File Coverage

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


line stmt bran cond sub pod time code
1             package Sisimai::RFC3834;
2 7     7   2857 use v5.26;
  7         28  
3 7     7   41 use strict;
  7         14  
  7         182  
4 7     7   30 use warnings;
  7         12  
  7         12525  
5              
6             # http://tools.ietf.org/html/rfc3834
7 1     1 1 6 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 95     95 1 1974 my $class = shift;
16 95 100 100     261 my $mhead = shift // return undef; return undef unless keys %$mhead;
  94         308  
17 92 100 50     222 my $mbody = shift // return undef; return undef unless ref $mbody eq 'SCALAR';
  92         326  
18 91         156 my $lower = {};
19              
20 91         343 my $markingsof = {'boundary' => '__SISIMAI_PSEUDO_BOUNDARY__'};
21 91         289 my $lowerlabel = ['from', 'to', 'subject', 'auto-submitted', 'precedence', 'x-apple-action'];
22              
23 91         212 for my $e ( @$lowerlabel ) {
24             # Set lower-cased value of each header related to auto-response
25 546 100       1116 next unless exists $mhead->{ $e };
26 286         823 $lower->{ $e } = lc $mhead->{ $e };
27             }
28              
29 91         180 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 91         188 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 91         159 state $subjectset = qr{\A(?>
45             (?:.+?)?re:
46             |auto(?:[ ]response):
47             |automatic[ ]reply:
48             |out[ ]of[ ]office:
49             )
50             [ ]*(.+)\z
51             }x;
52 91         126 state $suspending = [
53             ["this email inbox", " is no longer in use."],
54             ];
55              
56 91         138 my $leave = 0; DETECT_EXCLUSION_MESSAGE: for my $e ( keys %$donotparse ) {
  91         1476  
57             # Exclude message from root@
58 234 100       430 next unless exists $lower->{ $e };
59 217 100       438 next unless grep { index($lower->{ $e }, $_) > -1 } $donotparse->{ $e }->@*;
  472         1154  
60 22         37 $leave = 1;
61 22         39 last;
62             }
63 91 100       343 return undef if $leave;
64              
65 69         108 my $match = 0; DETECT_AUTO_REPLY_MESSAGE0: for my $e ( keys %$autoreply0 ) {
  69         193  
66             # RFC3834 Auto-Submitted and other headers
67 230 100       473 next unless exists $lower->{ $e };
68 91 100       546 next unless grep { index($lower->{ $e }, $_) == 0 } $autoreply0->{ $e }->@*;
  387         731  
69              
70 31         68 $match++;
71 31         51 last;
72             }
73 69 100       343 return undef unless $match;
74              
75 31         129 require Sisimai::Lhost;
76 31         130 my $dscontents = [Sisimai::Lhost->DELIVERYSTATUS]; my $v = $dscontents->[-1];
  31         58  
77 31         44 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
78 31         65 my $maxmsgline = 5; # (Integer) Max message length(lines)
79 31         37 my $haveloaded = 0; # (Integer) The number of lines loaded from message body
80 31         35 my $blanklines = 0; # (Integer) Counter for countinuous blank lines
81 31         53 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         40 for my $e ('reply-to', 'from', 'return-path') {
  31         67  
86             # Get the recipient address
87 57 100       125 next unless exists $mhead->{ $e };
88              
89 31         59 $v->{'recipient'} = $mhead->{ $e };
90 31         41 last;
91             }
92              
93 31 50       73 if( $v->{'recipient'} ) {
94             # Clean-up the recipient address
95 31         214 $v->{'recipient'} = Sisimai::Address->s3s4($v->{'recipient'});
96 31         52 $recipients++;
97             }
98             }
99 31 50       71 return undef unless $recipients;
100              
101 31 100       88 if( $mhead->{'content-type'} ) {
102             # Get the boundary string and set regular expression for matching with the boundary string.
103 25         103 my $q = Sisimai::RFC2045->boundary($mhead->{'content-type'}, 0);
104 25 100       85 $markingsof->{'boundary'} = $q if $q;
105             }
106              
107             MESSAGE_BODY: {
108             # Get vacation message
109 31         38 for my $e ( split("\n", $$mbody) ) {
  31         140  
110             # Read the first 5 lines except a blank line
111 73 50       152 $countuntil += 1 if index($e, $markingsof->{'boundary'}) > -1;
112              
113 73 100       138 unless( length $e ) {
114             # Check a blank line
115 21 100       44 last if ++$blanklines > $countuntil;
116 16         29 next;
117             }
118 52 100       97 next if rindex($e, ' ') < 0;
119 37 50 33     203 next if index($e, 'Content-Type') == 0 || index($e, 'Content-Transfer') == 0;
120              
121 37         86 $v->{'diagnosis'} .= $e.' ';
122 37         43 $haveloaded++;
123 37 50       76 last if $haveloaded >= $maxmsgline;
124             }
125 31   66     105 $v->{'diagnosis'} ||= $mhead->{'subject'};
126             }
127              
128 31         154 $v->{'diagnosis'} = Sisimai::String->sweep($v->{'diagnosis'});
129 31         56 $v->{'reason'} = 'vacation';
130              
131 31         74 my $cv = lc $v->{'diagnosis'}; for my $e ( @$suspending ) {
  31         59  
132             # Check that the auto-replied message indicates the "Suspend" reason or not.
133 31 100       119 next unless Sisimai::String->aligned(\$cv, $e);
134 5         12 $v->{'reason'} = 'suspend'; last;
  5         9  
135             }
136              
137 31         87 $v->{'date'} = $mhead->{'date'};
138 31         60 $v->{'status'} = '';
139              
140             # Get the Subject header from the original message
141 31 100       450 my $rfc822part = $lower->{'subject'} =~ $subjectset ? 'Subject: '.$1."\n" : '';
142 31         261 return {"ds" => $dscontents, "rfc822" => $rfc822part};
143             }
144              
145             1;
146             __END__