File Coverage

lib/Sisimai/RFC3834.pm
Criterion Covered Total %
statement 75 75 100.0
branch 33 48 68.7
condition 4 7 57.1
subroutine 5 5 100.0
pod 2 2 100.0
total 119 137 86.8


line stmt bran cond sub pod time code
1             package Sisimai::RFC3834;
2 6     6   4496 use feature ':5.10';
  6         13  
  6         507  
3 6     6   42 use strict;
  6         11  
  6         127  
4 6     6   31 use warnings;
  6         11  
  6         6128  
5              
6             # http://tools.ietf.org/html/rfc3834
7 2     2 1 771 sub description { 'Detector for auto replied message' }
8             sub make {
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 parse or the arguments are missing
14             # @since v4.1.28
15 31     31 1 396 my $class = shift;
16 31   100     109 my $mhead = shift // return undef;
17 30   50     74 my $mbody = shift // return undef;
18 30         53 my $leave = 0;
19 30         50 my $match = 0;
20              
21 30 50       93 return undef unless keys %$mhead;
22 30 50       108 return undef unless ref $mbody eq 'SCALAR';
23              
24 30         228 my $markingsof = { 'boundary' => qr/\A__SISIMAI_PSEUDO_BOUNDARY__\z/ };
25 30         105 state $autoreply1 = {
26             # http://www.iana.org/assignments/auto-submitted-keywords/auto-submitted-keywords.xhtml
27             'auto-submitted' => qr/\Aauto-(?:generated|replied|notified)/,
28             'x-apple-action' => qr/\Avacation\z/,
29             'precedence' => qr/\Aauto_reply\z/,
30             'subject' => qr/\A(?>
31             auto:
32             |auto[ ]response:
33             |automatic[ ]reply:
34             |out[ ]of[ ](?:the[ ])*office:
35             )
36             /x,
37             };
38 30         80 state $excludings = {
39             'subject' => qr{(?:
40             security[ ]information[ ]for # sudo
41             |mail[ ]failure[ ][-] # Exim
42             )
43             }x,
44             'from' => qr/(?:root|postmaster|mailer-daemon)[@]/,
45             'to' => qr/root[@]/,
46             };
47 30         56 state $subjectset = qr{\A(?>
48             (?:.+?)?re:
49             |auto(?:[ ]response):
50             |automatic[ ]reply:
51             |out[ ]of[ ]office:
52             )
53             [ ]*(.+)\z
54             }x;
55              
56 30         101 DETECT_EXCLUSION_MESSAGE: for my $e ( keys %$excludings ) {
57             # Exclude message from root@
58 84 50       205 next unless exists $mhead->{ $e };
59 84 50       158 next unless defined $mhead->{ $e };
60 84 100       591 next unless lc($mhead->{ $e }) =~ $excludings->{ $e };
61 3         6 $leave = 1;
62 3         6 last;
63             }
64 30 100       95 return undef if $leave;
65              
66 27         91 DETECT_AUTO_REPLY_MESSAGE: for my $e ( keys %$autoreply1 ) {
67             # RFC3834 Auto-Submitted and other headers
68 78 100       147 next unless exists $mhead->{ $e };
69 30 50       82 next unless defined $mhead->{ $e };
70 30 100       251 next unless lc($mhead->{ $e }) =~ $autoreply1->{ $e };
71 21         40 $match++;
72 21         45 last;
73             }
74 27 100       84 return undef unless $match;
75              
76 21         102 require Sisimai::Lhost;
77 21         111 my $dscontents = [Sisimai::Lhost->DELIVERYSTATUS];
78 21         54 my $recipients = 0; # (Integer) The number of 'Final-Recipient' header
79 21         49 my $maxmsgline = 5; # (Integer) Max message length(lines)
80 21         46 my $haveloaded = 0; # (Integer) The number of lines loaded from message body
81 21         40 my $blanklines = 0; # (Integer) Counter for countinuous blank lines
82 21         25 my $countuntil = 1; # (Integer) Maximun value of blank lines in the body part
83 21         56 my $v = $dscontents->[-1];
84              
85             RECIPIENT_ADDRESS: {
86             # Try to get the address of the recipient
87 21         34 for my $e ('from', 'return-path') {
  21         44  
88             # Get the recipient address
89 21 50       56 next unless exists $mhead->{ $e };
90 21 50       56 next unless defined $mhead->{ $e };
91              
92 21         51 $v->{'recipient'} = $mhead->{ $e };
93 21         35 last;
94             }
95              
96 21 50       60 if( $v->{'recipient'} ) {
97             # Clean-up the recipient address
98 21         114 $v->{'recipient'} = Sisimai::Address->s3s4($v->{'recipient'});
99 21         64 $recipients++;
100             }
101             }
102 21 50       89 return undef unless $recipients;
103              
104 21 100       77 if( $mhead->{'content-type'} ) {
105             # Get the boundary string and set regular expression for matching with
106             # the boundary string.
107 15         55 my $b0 = Sisimai::MIME->boundary($mhead->{'content-type'}, 0);
108 15 50       59 $markingsof->{'boundary'} = qr/\A\Q$b0\E\z/ if length $b0;
109             }
110              
111             BODY_PARSER: {
112             # Get vacation message
113 21         33 for my $e ( split("\n", $$mbody) ) {
  21         112  
114             # Read the first 5 lines except a blank line
115 48 50       213 $countuntil += 1 if $e =~ $markingsof->{'boundary'};
116              
117 48 100       96 unless( length $e ) {
118             # Check a blank line
119 16 100       49 last if ++$blanklines > $countuntil;
120 11         16 next;
121             }
122 32 50       101 next unless rindex($e, ' ') > -1;
123 32 50       95 next if index($e, 'Content-Type') == 0;
124 32 50       89 next if index($e, 'Content-Transfer') == 0;
125              
126 32         101 $v->{'diagnosis'} .= $e.' ';
127 32         77 $haveloaded++;
128 32 50       82 last if $haveloaded >= $maxmsgline;
129             }
130 21   33     65 $v->{'diagnosis'} ||= $mhead->{'subject'};
131             }
132              
133 21         154 $v->{'diagnosis'} = Sisimai::String->sweep($v->{'diagnosis'});
134 21         80 $v->{'reason'} = 'vacation';
135 21         94 $v->{'date'} = $mhead->{'date'};
136 21         48 $v->{'status'} = '';
137              
138             # Get the Subject header from the original message
139 21 100       289 my $rfc822part = lc($mhead->{'subject'}) =~ $subjectset ? 'Subject: '.$1."\n" : '';
140 21         146 return { 'ds' => $dscontents, 'rfc822' => $rfc822part };
141             }
142              
143             1;
144             __END__