File Coverage

blib/lib/Email/Received.pm
Criterion Covered Total %
statement 109 553 19.7
branch 47 294 15.9
condition 2 12 16.6
subroutine 12 14 85.7
pod 0 4 0.0
total 170 877 19.3


line stmt bran cond sub pod time code
1             package Email::Received;
2 2     2   8938 use 5.006;
  2         7  
  2         86  
3 2     2   13 use strict;
  2         3  
  2         73  
4 2     2   12 use warnings;
  2         15  
  2         74  
5 2     2   11 use constant DEBUG => 0;
  2         5  
  2         171  
6              
7             require Exporter;
8 2     2   22 use base 'Exporter';
  2         3  
  2         312  
9             our @EXPORT = qw( parse_received);
10             our $VERSION = '1.00';
11 2     2   1380 use Email::Received::Constants;
  2         6  
  2         209  
12             my $IP_ADDRESS = IP_ADDRESS;
13             my $LOCALHOST = LOCALHOST;
14 2     2   2072 use Regexp::Common qw/net/;
  2         10092  
  2         12  
15              
16             # So the plan - man, this is so evil - is to make parse_received on the
17             # fly from the rules below.
18             *parse_received = generate_parse_received( unparse_rules(parse_rules()));
19              
20             sub tidy_up {
21 0     0 0 0 my $r = shift;
22 2     2   6926 no warnings;
  2         4  
  2         735  
23             #print "Tidy up called for $_\n";
24 0         0 $r->{envfrom} =~ s/^\s*<*//gs; $r->{envfrom} =~ s/>*\s*$//gs;
  0         0  
25 0         0 $r->{by} =~ s/\;$//;
26 0 0       0 if ($r->{ip} =~ /($RE{net}{IPv4})/) { $r->{ip} = $1 } else { return }
  0         0  
  0         0  
27              
28 0   0     0 exists $r->{$_} and $r->{$_} =~ s/[\s\0\#\[\]\(\)\<\>\|]/!/gs for qw/ip rdns helo by ident envfrom /;
29 0 0       0 delete $r->{rdns} if lc $r->{rdns} eq "unknown";
30 0         0 return $r;
31             }
32              
33             sub generate_parse_received {
34 2     2 0 6 my $code = shift;
35 2         105 $code = q|sub {
36             local $_ = shift;
37             s/\s+/ /gs;
38             #print "Got $_\n";
39             my $r = {};
40             |.
41             $code. q/
42             #print "Dropped off the end\n";
43             return tidy_up($r);
44             }
45             /;
46 2 0 0 0   9580 my $subref = eval $code;
  0 0 0     0  
  0 0 0     0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0 0       0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
47 2 50       12 die "Couldn't create subroutine: $@" if $@;
48 2         41 return $subref;
49             }
50              
51             =head1 NAME
52              
53             Email::Received - Parse an email Received: header
54              
55             =head1 SYNOPSIS
56              
57             use Email::Received;
58              
59             for ($mail->header("Received")) {
60             my $data = parse_received($_);
61             return "SPAM" if rbl_lookup($data->{ip});
62             }
63              
64             =head1 DESCRIPTION
65              
66             This module is a Perl Email Project rewrite of SpamAssassin's email
67             header parser. We did this so that the great work they did in analysing
68             pretty much every possible Received header format could be used in
69             applications other than SpamAssassin itself.
70              
71             The module provides one function, C, which takes a
72             single Received line. It then produces either nothing, if the line is
73             unparsable, a hash reference like this:
74              
75             { reason => "gateway noise" }
76              
77             if the line should be ignored for some good reason, and one like this:
78              
79             { ip => '64.12.136.4', id => '875522', by => 'xxx.com',
80             helo => 'imo-m01.mx.aol.com' }
81              
82             if it parsed the message. Possible keys are:
83              
84             ip rdns helo ident envfrom auth by id
85              
86             =head1 RULE FORMAT
87              
88             Where SpamAssassin used a big static subroutine full of regular expressions
89             to parse the data, we build up a big subroutine full of regular expressions
90             dynamically from a set of rules. The rules are stored at the bottom of
91             this module. The basic format for a rule looks like this:
92              
93             ((var=~)?/REGEXP/)? [ACTION; ]+
94              
95             The C is either C, C,
96             C or C.
97              
98             One control structure is provided, which is basically an C statement:
99              
100             GIVEN (NOT)? /REGEXP/ {
101             ACTION+
102             }
103              
104             =head2 EXPORT
105              
106             parse_received
107              
108             =head1 SEE ALSO
109              
110             L, from which the
111             rules and some of the IP address matching constants were blatantly
112             stolen. Thanks, guys, for doing such a comprehensive job!
113              
114             =head1 AUTHOR
115              
116             simon, Esimon@E
117              
118             =head1 COPYRIGHT AND LICENSE
119              
120             Copyright (C) 2006 by simon
121              
122             This library is free software; you can redistribute it and/or modify
123             it under the same terms as Perl itself, either Perl version 5.8.7 or,
124             at your option, any later version of Perl 5 you may have available.
125              
126              
127             =cut
128              
129 2     2   4999 use Text::Balanced qw(extract_quotelike);
  2         45363  
  2         2777  
130             sub parse_rules {
131 30     30 0 38 my $in_given = shift;
132 30         52 my $tree = [];
133 30         424 while () {
134 410         533 chomp; s/^\s+//;
  410         992  
135 410 100       525 s/^#.*//; next unless /\S/;
  410         1904  
136 288 100       605 if (/^}\s*$/) {
137 28 50       83 return $tree if $in_given;
138 0         0 die "Syntax error on line $.: superfluous close bracket\n";
139             }
140 260 100       547 if (s/^GIVEN\s+//) {
141 28         53 my $inverse = s/^NOT\s+//;
142 28 50       77 my $referent = s/^(\S+)=~//?$1:"";
143 28         29 my $re;
144 28 50       69 unless ($re= extract_quotelike($_)) {
145 0         0 die "Syntax error on line $.: given has no expression\n";
146             }
147 28 50       1689 die "Syntax error on line $.: improper given\n" unless /\s*{\s*$/;
148 28         81 my $subtree = parse_rules(1); # Let the reader understand!
149 28 50       142 push @$tree, {given => $re,
150             ($referent ? (referent => $referent) : ()),
151             subtree => $subtree, inverse => $inverse };
152 28         118 next;
153             }
154 232         232 my $referent;
155 232 100       570 $referent = $1 if s/^(\S+)=~//;
156 232         338 my $current = {};
157 232         240 if (DEBUG) { $current->{line} = "$.: $_"; }
158 232 100       565 if (my $re= extract_quotelike($_)) {
    50          
159 202         13791 $current->{regexp} = $re;
160 202 100       440 $current->{referent} = $referent if $referent;
161 0         0 } elsif($referent) { die "Syntax error on line $.: Referent with no regexp!\n"; }
162             # At this point we want a set of commands delimited with
163             # semicolons
164 232         1591 my @actions;
165 232         427 while ($_) {
166 1156 100       3381 s/^\s+// and next;
167 590 100       2592 if (s/^SET (\w+)\s*((?:\|\|)?=)\s*(.*?);//) {
    100          
    50          
168 426         2217 push @actions, { action => "SET", variable => $1, value => $3, operator => $2 };
169 426         968 next;
170             }elsif (s/^IGNORE\s*//) {
171 46         114 my $reason = extract_quotelike($_);
172 46 50       3137 die "No semicolon after reason? on line $.\n" unless s/^\s*;//;
173 46         158 push @actions, { action => "IGNORE", reason => $reason};
174 46         125 next;
175             } elsif (s/^(DONE|UNPARSABLE)\s*;//) {
176 118         428 push @actions, { action => $1 };
177 118         265 next;
178             }
179 0         0 die "Can't parse action '$_' at line $.\n";
180             }
181 232 50       422 if (@actions) { $current->{actions} = \@actions }
  232         443  
182 232         1482 push @$tree, $current;
183             }
184 2         11 return $tree;
185             }
186              
187             sub unparse_rules {
188 30     30 0 40 my $tree = shift;
189 30   100     67 my $level = shift||0;
190 30         26 my $output;
191 30         51 for (@$tree) {
192 260         325 $output .= " " x ($level * 5);
193 260 100       538 if ($_->{given}) {
194             #$output .= "print q{Trying given $_->{given} against |}.\$_.qq{|\n};\n";
195 28 100       55 $output .= $_->{inverse} ? "unless " : "if ";
196 28         29 $output .= "(";
197 28 50       55 $output .= '$r->{'.$_->{referent}."}=~" if $_->{referent};
198 28         43 $output .= $_->{given}.") {\n";
199             #$output .= "print q{In given\n};\n";
200 28         117 $output .= unparse_rules($_->{subtree}, $level+1);
201 28         62 $output .= " " x ($level * 5);
202 28         28 $output .= "}\n";
203             #$output .= "print qq{Given over\n};\n";
204 28         42 next;
205             }
206 232 100       477 if ($_->{regexp}) {
207             #$output .= "print qq{Trying regexp }.q{$_->{regexp}}.qq{\n};";
208 202         206 $output .= 'if (';
209 202 100       488 $output .= '$r->{'.$_->{referent}."}=~" if $_->{referent};
210 202         366 $output .= $_->{regexp}.") {\n";
211 30         34 } else { $output .= "do { \n"; }
212 232         200 $level++;
213 232         181 if (DEBUG) {
214             $output .= " " x ($level * 5);
215             $output .= 'push @{$r->{rules_fired}}, q{'.$_->{line}."};\n";
216             }
217 232 50       217 for (@{$_->{actions}||[]}) {
  232         642  
218 590         803 $output .= " " x ($level * 5);
219 590 100       1827 if ($_->{action} eq "DONE") { $output .= "return tidy_up(\$r)" }
  114 100       135  
    100          
    50          
220 4         6 elsif ($_->{action} eq "UNPARSABLE") { $output .= "return" }
221             elsif ($_->{action} eq "IGNORE") {
222 46         103 $output .= 'return ';
223 46 100       95 if ($_->{reason}) { $output .= "{reason => $_->{reason} }" };
  34         69  
224             } elsif ($_->{action} eq "SET") {
225 426         945 $output .= '$r->{'.$_->{variable}."} ".$_->{operator} . " ".$_->{value} }
226 0         0 else { die "Couldn't unparse action!\n" }
227 590         862 $output .= ";\n";
228             }
229 232         278 $level--;
230 232         378 $output .= " " x ($level * 5);
231 232         313 $output .= "};\n";
232             }
233 30         225 return $output;
234             }
235             __DATA__