File Coverage

blib/lib/Mail/SpamAssassin/Plugin/MIMEHeader.pm
Criterion Covered Total %
statement 39 93 41.9
branch 0 30 0.0
condition 1 3 33.3
subroutine 11 13 84.6
pod 2 4 50.0
total 53 143 37.0


line stmt bran cond sub pod time code
1             # <@LICENSE>
2             # Licensed to the Apache Software Foundation (ASF) under one or more
3             # contributor license agreements. See the NOTICE file distributed with
4             # this work for additional information regarding copyright ownership.
5             # The ASF licenses this file to you under the Apache License, Version 2.0
6             # (the "License"); you may not use this file except in compliance with
7             # the License. You may obtain a copy of the License at:
8             #
9             # http://www.apache.org/licenses/LICENSE-2.0
10             #
11             # Unless required by applicable law or agreed to in writing, software
12             # distributed under the License is distributed on an "AS IS" BASIS,
13             # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14             # See the License for the specific language governing permissions and
15             # limitations under the License.
16             # </@LICENSE>
17              
18             =head1 NAME
19              
20             MIMEHeader - perform regexp tests against MIME headers
21              
22             =head1 SYNOPSIS
23              
24             loadplugin Mail::SpamAssassin::Plugin::MIMEHeader
25             mimeheader NAME_OF_RULE Content-Id =~ /foo/
26              
27             =head1 DESCRIPTION
28              
29             This plugin allows regexp rules to be written against MIME headers in the
30             message.
31              
32             =head1 RULE DEFINITIONS AND PRIVILEGED SETTINGS
33              
34             =over 4
35              
36             =item mimeheader NAME_OF_RULE Header-Name =~ /pattern/modifiers
37              
38             Specify a rule. C<NAME_OF_RULE> is the name of the rule to be used,
39             C<Header-Name> is the name of the MIME header to check, and
40             C</pattern/modifiers> is the Perl regular expression to match against this.
41              
42             Note that in a message of multiple parts, each header will be checked
43             against the pattern separately. In other words, if multiple parts
44             have a 'Content-Type' header, each header's value will be tested
45             individually as a separate string.
46              
47             Header names are considered case-insensitive.
48              
49             The header values are normally cleaned up a little; for example, whitespace
50             around the newline character in "folded" headers will be replaced with a single
51             space. Append C<:raw> to the header name to retrieve the raw, undecoded value,
52             including pristine whitespace, instead.
53              
54             =back
55              
56             =cut
57              
58             package Mail::SpamAssassin::Plugin::MIMEHeader;
59              
60 21     21   160 use strict;
  21         57  
  21         677  
61 21     21   135 use warnings;
  21         62  
  21         703  
62             # use bytes;
63 21     21   153 use re 'taint';
  21         60  
  21         706  
64              
65 21     21   144 use Mail::SpamAssassin::Plugin;
  21         63  
  21         607  
66 21     21   134 use Mail::SpamAssassin::Conf;
  21         59  
  21         615  
67 21     21   134 use Mail::SpamAssassin::Logger;
  21         57  
  21         1385  
68 21     21   171 use Mail::SpamAssassin::Util qw(untaint_var compile_regexp);
  21         43  
  21         1531  
69 21     21   164 use Mail::SpamAssassin::Constants qw(:sa);
  21         50  
  21         24803  
70              
71             our @ISA = qw(Mail::SpamAssassin::Plugin);
72              
73             our @TEMPORARY_METHODS;
74              
75             my $RULENAME_RE = RULENAME_RE;
76              
77             # ---------------------------------------------------------------------------
78              
79             # constructor
80             sub new {
81 62     62 1 251 my $class = shift;
82 62         216 my $samain = shift;
83              
84             # some boilerplate...
85 62   33     426 $class = ref($class) || $class;
86 62         336 my $self = $class->SUPER::new($samain);
87 62         262 bless ($self, $class);
88              
89 62         365 $self->set_config($samain->{conf});
90              
91 62         908 return $self;
92             }
93              
94             # ---------------------------------------------------------------------------
95              
96             sub set_config {
97 62     62 0 218 my($self, $conf) = @_;
98 62         169 my @cmds;
99              
100 62         138 my $pluginobj = $self; # allow use inside the closure below
101              
102             push (@cmds, {
103             setting => 'mimeheader',
104             is_priv => 1,
105             code => sub {
106 0     0   0 my ($self, $key, $value, $line) = @_;
107 0         0 local ($1,$2,$3);
108 0 0       0 if ($value !~ s/^(${RULENAME_RE})\s+//) {
109 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
110             }
111 0         0 my $rulename = untaint_var($1);
112 0 0       0 if ($value eq '') {
113 0         0 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
114             }
115             # Take :raw to hdrname!
116 0 0       0 if ($value !~ /^([^:\s]+(?:\:(?:raw)?)?)\s*([=!]~)\s*(.+)$/) {
117 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
118             }
119 0         0 my $hdrname = $1;
120 0 0       0 my $negated = $2 eq '!~' ? 1 : 0;
121 0         0 my $pattern = $3;
122 0         0 $hdrname =~ s/:$//;
123 0         0 my $if_unset = '';
124 0 0       0 if ($pattern =~ s/\s+\[if-unset:\s+(.+)\]$//) {
125 0         0 $if_unset = $1;
126             }
127 0         0 my ($rec, $err) = compile_regexp($pattern, 1);
128 0 0       0 if (!$rec) {
129 0         0 info("mimeheader: invalid regexp for $rulename '$pattern': $err");
130 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
131             }
132              
133 0         0 $self->{mimeheader_tests}->{$rulename} = {
134             hdr => $hdrname,
135             negated => $negated,
136             if_unset => $if_unset,
137             pattern => $rec
138             };
139              
140             # now here's a hack; generate a fake eval rule function to
141             # call this rule's _real_ code!
142             # TODO: we should have a more elegant way for new rule types to
143             # be defined
144 0         0 my $evalfn = "_mimeheader_eval_$rulename";
145              
146             # don't redefine the subroutine if it already exists!
147             # this causes lots of annoying warnings and such during things like
148             # "make test".
149 0 0       0 return if (defined &{'Mail::SpamAssassin::Plugin::MIMEHeader::'.$evalfn});
  0         0  
150              
151 0         0 $self->{parser}->add_test($rulename, $evalfn."()",
152             $Mail::SpamAssassin::Conf::TYPE_BODY_EVALS);
153              
154             # evalfn/rulename safe, sanitized by $RULENAME_RE
155 0         0 my $evalcode = '
156             sub Mail::SpamAssassin::Plugin::MIMEHeader::'.$evalfn.' {
157             $_[0]->eval_hook_called($_[1], q{'.$rulename.'});
158             }
159             ';
160              
161             eval
162             $evalcode . '; 1'
163 0 0       0 or do {
164 0 0       0 my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0         0  
165 0         0 warn "mimeheader: plugin error: $eval_stat\n";
166 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
167             };
168              
169 0         0 $pluginobj->register_eval_rule($evalfn);
170              
171 0         0 push @TEMPORARY_METHODS, "Mail::SpamAssassin::Plugin::MIMEHeader::${evalfn}";
172             }
173 62         771 });
174              
175 62         378 $conf->{parser}->register_commands(\@cmds);
176             }
177              
178             # ---------------------------------------------------------------------------
179              
180             sub eval_hook_called {
181 0     0 0 0 my ($pobj, $scanner, $rulename) = @_;
182              
183 0         0 my $rule = $scanner->{conf}->{mimeheader_tests}->{$rulename};
184 0         0 my $hdr = $rule->{hdr};
185 0         0 my $negated = $rule->{negated};
186 0         0 my $if_unset = $rule->{if_unset};
187 0         0 my $pattern = $rule->{pattern};
188              
189              
190 0         0 my $getraw;
191 0 0       0 if ($hdr =~ s/:raw$//) {
192 0         0 $getraw = 1;
193             } else {
194 0         0 $getraw = 0;
195             }
196              
197 0         0 foreach my $p ($scanner->{msg}->find_parts(qr/./)) {
198 0         0 my $val;
199 0 0       0 if ($getraw) {
200 0         0 $val = $p->raw_header($hdr);
201             } else {
202 0         0 $val = $p->get_header($hdr);
203             }
204 0 0       0 $val = $if_unset if !defined $val;
205              
206 0 0       0 if ($val =~ $pattern) {
207 0 0       0 return ($negated ? 0 : 1);
208             }
209             }
210              
211 0 0       0 return ($negated ? 1 : 0);
212             }
213              
214             # ---------------------------------------------------------------------------
215              
216             sub finish_tests {
217 40     40 1 135 my ($self, $params) = @_;
218              
219 40         168 foreach my $method (@TEMPORARY_METHODS) {
220 0         0 undef &{$method};
  0         0  
221             }
222 40         147 @TEMPORARY_METHODS = (); # clear for next time
223             }
224              
225             # ---------------------------------------------------------------------------
226              
227             1;