File Coverage

blib/lib/Mail/SpamAssassin/Plugin/ReplaceTags.pm
Criterion Covered Total %
statement 47 107 43.9
branch 0 40 0.0
condition 1 12 8.3
subroutine 11 13 84.6
pod 4 5 80.0
total 63 177 35.5


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             Mail::SpamAssassin::Plugin::ReplaceTags - tags for SpamAssassin rules
21              
22             The plugin allows rules to contain regular expression tags to be used in
23             regular expression rules. The tags make it much easier to maintain
24             complicated rules.
25              
26             Warning: This plugin relies on data structures specific to this version of
27             SpamAssassin; it is not guaranteed to work with other versions of SpamAssassin.
28              
29             =head1 SYNOPSIS
30              
31             loadplugin Mail::SpamAssassin::Plugin::ReplaceTags
32              
33             replace_start <
34             replace_end >
35              
36             replace_tag A [a@]
37             replace_tag G [gk]
38             replace_tag I [il|!1y\?\xcc\xcd\xce\xcf\xec\xed\xee\xef]
39             replace_tag R [r3]
40             replace_tag V (?:[vu]|\\\/)
41             replace_tag SP [\s~_-]
42              
43             body VIAGRA_OBFU /(?!viagra)<V>+<SP>*<I>+<SP>*<A>+<SP>*<G>+<SP>*<R>+<SP>*<A>+/i
44             describe VIAGRA_OBFU Attempt to obfuscate "viagra"
45              
46             replace_rules VIAGRA_OBFU
47              
48             =cut
49              
50              
51             use Mail::SpamAssassin;
52 22     22   167 use Mail::SpamAssassin::Plugin;
  22         48  
  22         635  
53 22     22   140 use Mail::SpamAssassin::Logger;
  22         49  
  22         546  
54 22     22   141 use Mail::SpamAssassin::Util qw(compile_regexp qr_to_string);
  22         43  
  22         1366  
55 22     22   140  
  22         42  
  22         1259  
56             use strict;
57 22     22   139 use warnings;
  22         55  
  22         564  
58 22     22   112 # use bytes;
  22         223  
  22         687  
59             use re 'taint';
60 22     22   148  
  22         50  
  22         21809  
61             our @ISA = qw(Mail::SpamAssassin::Plugin);
62              
63             my ($class, $mailsa) = @_;
64             $class = ref($class) || $class;
65 63     63 1 897  
66 63   33     391 my $self = $class->SUPER::new($mailsa);
67              
68 63         576 bless ($self, $class);
69              
70 63         394 $self->set_config($mailsa->{conf});
71              
72 63         334 return $self;
73             }
74 63         547  
75             my ($self, $opts) = @_;
76              
77             # keeps track of replaced rules
78 63     63 1 194 # don't have $pms in finish_parsing_end() so init this..
79             $self->{replace_rules_done} = {};
80              
81             return 1;
82 63         204 }
83              
84 63         194 my ($self, $opts) = @_;
85              
86             dbg("replacetags: replacing tags");
87              
88 63     63 1 240 my $conf = $opts->{conf};
89             my $start = $conf->{replace_start};
90 63         210 my $end = $conf->{replace_end};
91              
92 63         170 foreach my $rule (keys %{$conf->{replace_rules}}) {
93 63         190 # process rules only once, mark to replace_rules_done,
94 63         153 # do NOT delete $conf->{replace_rules}, it's used by BodyRuleExtractor
95             next if exists $self->{replace_rules_done}->{$rule};
96 63         140 $self->{replace_rules_done}->{$rule} = 1;
  63         390  
97              
98             if (!exists $conf->{test_qrs}->{$rule}) {
99 0 0       0 dbg("replacetags: replace requested for non-existing or incompatible rule: $rule\n");
100 0         0 next;
101             }
102 0 0       0  
103 0         0 my $re = qr_to_string($conf->{test_qrs}->{$rule});
104 0         0 next unless defined $re;
105             my $origre = $re;
106              
107 0         0 my $passes = 0;
108 0 0       0 my $doagain;
109 0         0  
110             do {
111 0         0 my $pre_name;
112 0         0 my $post_name;
113             my $inter_name;
114 0   0     0 $doagain = 0;
115 0         0  
116             # get modifier tags
117 0         0 if ($re =~ s/${start}pre (.+?)${end}//) {
118 0         0 $pre_name = $1;
119             }
120             if ($re =~ s/${start}post (.+?)${end}//) {
121 0 0       0 $post_name = $1;
122 0         0 }
123             if ($re =~ s/${start}inter (.+?)${end}//) {
124 0 0       0 $inter_name = $1;
125 0         0 }
126              
127 0 0       0 # this will produce an array of tags to be replaced
128 0         0 # for two adjacent tags, an element of "" will be between the two
129             my @re = split(/(<[^<>]+>)/, $re);
130              
131             if ($pre_name) {
132             my $pre = $conf->{replace_pre}->{$pre_name};
133 0         0 if ($pre) {
134             s{($start.+?$end)}{$pre$1} for @re;
135 0 0       0 }
136 0         0 }
137 0 0       0 if ($post_name) {
138 0         0 my $post = $conf->{replace_post}->{$post_name};
139             if ($post) {
140             s{($start.+?$end)}{$1$post}g for @re;
141 0 0       0 }
142 0         0 }
143 0 0       0 if ($inter_name) {
144 0         0 my $inter = $conf->{replace_inter}->{$inter_name};
145             if ($inter) {
146             s{^$}{$inter} for @re;
147 0 0       0 }
148 0         0 }
149 0 0       0 for (my $i = 0; $i < @re; $i++) {
150 0         0 if ($re[$i] =~ m|$start(.+?)$end|g) {
151             my $tag_name = $1;
152             # if the tag exists, replace it with the corresponding phrase
153 0         0 if ($tag_name) {
154 0 0       0 my $replacement = $conf->{replace_tag}->{$tag_name};
155 0         0 if ($replacement) {
156             $re[$i] =~ s|$start$tag_name$end|$replacement|g;
157 0 0       0 $doagain = 1 if !$doagain && $replacement =~ /<[^>]+>/;
158 0         0 }
159 0 0       0 }
160 0         0 }
161 0 0 0     0 }
162              
163             $re = join('', @re);
164              
165             $passes++;
166             } while $doagain && $passes <= 5;
167 0         0  
168             if ($re ne $origre) {
169 0         0 # do the actual replacement
170             my ($rec, $err) = compile_regexp($re, 0);
171             if (!$rec) {
172 0 0       0 info("replacetags: regexp compilation failed '$re': $err");
173             next;
174 0         0 }
175 0 0       0 $conf->{test_qrs}->{$rule} = $rec;
176 0         0 #dbg("replacetags: replaced $rule: '$origre' => '$re'");
177 0         0 dbg("replacetags: replaced $rule");
178             } else {
179 0         0 dbg("replacetags: nothing was replaced in $rule");
180             }
181 0         0 }
182             }
183 0         0  
184             my ($self, $opts) = @_;
185             return $self->finish_parsing_end($opts);
186             }
187              
188             my ($self, $conf) = @_;
189 0     0 1 0 my @cmds;
190 0         0  
191             =head1 RULE DEFINITIONS AND PRIVILEGED SETTINGS
192              
193             =over 4
194 63     63 0 189  
195 63         131 =item replace_tag tagname expression
196              
197             Assign a valid regular expression to tagname.
198              
199             Note: It is not recommended to put quantifiers inside the tag, it's better to
200             put them inside the rule itself for greater flexibility.
201              
202             =cut
203              
204             push(@cmds, {
205             setting => 'replace_tag',
206             is_priv => 1,
207             type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
208             });
209              
210 63         316 =item replace_pre tagname expression
211              
212             Assign a valid regular expression to tagname. The expression will be
213             placed before each tag that is replaced.
214              
215             =cut
216              
217             push(@cmds, {
218             setting => 'replace_pre',
219             is_priv => 1,
220             type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
221             });
222              
223 63         361 =item replace_inter tagname expression
224              
225             Assign a valid regular expression to tagname. The expression will be
226             placed between each two immediately adjacent tags that are replaced.
227              
228             =cut
229              
230             push(@cmds, {
231             setting => 'replace_inter',
232             is_priv => 1,
233             type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
234             });
235              
236 63         240 =item replace_post tagname expression
237              
238             Assign a valid regular expression to tagname. The expression will be
239             placed after each tag that is replaced.
240              
241             =cut
242              
243             push(@cmds, {
244             setting => 'replace_post',
245             is_priv => 1,
246             type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
247             });
248              
249 63         572 =item replace_rules list_of_tests
250              
251             Specify a list of symbolic test names (separated by whitespace) of tests which
252             should be modified using replacement tags. Only simple regular expression
253             body, header, uri, full, rawbody tests are supported.
254              
255             =cut
256              
257             push(@cmds, {
258             setting => 'replace_rules',
259             is_priv => 1,
260             default => {},
261             type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
262             code => sub {
263             my ($self, $key, $value, $line) = @_;
264             unless (defined $value && $value !~ /^$/) {
265             return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
266             }
267             unless ($value =~ /\S+/) {
268             return $Mail::SpamAssassin::Conf::INVALID_VALUE;
269 0     0   0 }
270 0 0 0     0 foreach my $rule (split(/\s+/, $value)) {
271 0         0 $self->{replace_rules}->{$rule} = 1;
272             }
273 0 0       0 }
274 0         0 });
275              
276 0         0 =item replace_start string
277 0         0  
278             =item replace_end string
279              
280 63         589 String(s) which indicate the start and end of a tag inside a rule. Only tags
281             enclosed by the start and end strings are found and replaced.
282              
283             =cut
284              
285             push(@cmds, {
286             setting => 'replace_start',
287             is_priv => 1,
288             default => '<',
289             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
290             });
291 63         371  
292             push(@cmds, {
293             setting => 'replace_end',
294             is_priv => 1,
295             default => '>',
296             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
297             });
298 63         424  
299             $conf->{parser}->register_commands(\@cmds);
300             }
301              
302             1;
303              
304             =back
305 63         276  
306             =cut