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