File Coverage

blib/lib/Mail/SpamAssassin/Plugin/ReplaceTags.pm
Criterion Covered Total %
statement 48 97 49.4
branch 2 38 5.2
condition 1 12 8.3
subroutine 9 11 81.8
pod 3 4 75.0
total 63 162 38.8


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 22     22   158 use Mail::SpamAssassin;
  22         46  
  22         649  
53 22     22   119 use Mail::SpamAssassin::Plugin;
  22         172  
  22         1799  
54 22     22   154 use Mail::SpamAssassin::Logger;
  22         489  
  22         1197  
55              
56 22     22   133 use strict;
  22         35  
  22         446  
57 22     22   101 use warnings;
  22         45  
  22         579  
58             # use bytes;
59 22     22   117 use re 'taint';
  22         55  
  22         22265  
60              
61             our @ISA = qw(Mail::SpamAssassin::Plugin);
62              
63             sub new {
64 63     63 1 230 my ($class, $mailsa) = @_;
65 63   33     389 $class = ref($class) || $class;
66              
67 63         311 my $self = $class->SUPER::new($mailsa);
68              
69 63         187 bless ($self, $class);
70              
71 63         314 $self->set_config($mailsa->{conf});
72              
73 63         693 return $self;
74             }
75              
76             sub finish_parsing_end {
77 63     63 1 218 my ($self, $opts) = @_;
78              
79 63         250 dbg("replacetags: replacing tags");
80              
81 63         169 my $conf = $opts->{conf};
82 63         212 my $start = $conf->{replace_start};
83 63         176 my $end = $conf->{replace_end};
84              
85             # this is the version-specific code
86 63         212 for my $type (qw|body_tests rawbody_tests head_tests full_tests uri_tests|) {
87 315         445 for my $priority (keys %{$conf->{$type}}) {
  315         1011  
88 186         314 while (my ($rule, $re) = each %{$conf->{$type}->{$priority}}) {
  994         2690  
89             # skip if not listed by replace_rules
90 808 50       1711 next unless $conf->{rules_to_replace}{$rule};
91              
92 0 0       0 if (would_log('dbg', 'replacetags') > 1) {
93 0         0 dbg("replacetags: replacing $rule: $re");
94             }
95              
96 0         0 my $passes = 0;
97 0         0 my $doagain;
98              
99 0   0     0 do {
100 0         0 my $pre_name;
101             my $post_name;
102 0         0 my $inter_name;
103 0         0 $doagain = 0;
104              
105             # get modifier tags
106 0 0       0 if ($re =~ s/${start}pre (.+?)${end}//) {
107 0         0 $pre_name = $1;
108             }
109 0 0       0 if ($re =~ s/${start}post (.+?)${end}//) {
110 0         0 $post_name = $1;
111             }
112 0 0       0 if ($re =~ s/${start}inter (.+?)${end}//) {
113 0         0 $inter_name = $1;
114             }
115              
116             # this will produce an array of tags to be replaced
117             # for two adjacent tags, an element of "" will be between the two
118 0         0 my @re = split(/(<[^<>]+>)/, $re);
119              
120 0 0       0 if ($pre_name) {
121 0         0 my $pre = $conf->{replace_pre}->{$pre_name};
122 0 0       0 if ($pre) {
123 0         0 s{($start.+?$end)}{$pre$1} for @re;
124             }
125             }
126 0 0       0 if ($post_name) {
127 0         0 my $post = $conf->{replace_post}->{$post_name};
128 0 0       0 if ($post) {
129 0         0 s{($start.+?$end)}{$1$post}g for @re;
130             }
131             }
132 0 0       0 if ($inter_name) {
133 0         0 my $inter = $conf->{replace_inter}->{$inter_name};
134 0 0       0 if ($inter) {
135 0         0 s{^$}{$inter} for @re;
136             }
137             }
138 0         0 for (my $i = 0; $i < @re; $i++) {
139 0 0       0 if ($re[$i] =~ m|$start(.+?)$end|g) {
140 0         0 my $tag_name = $1;
141             # if the tag exists, replace it with the corresponding phrase
142 0 0       0 if ($tag_name) {
143 0         0 my $replacement = $conf->{replace_tag}->{$tag_name};
144 0 0       0 if ($replacement) {
145 0         0 $re[$i] =~ s|$start$tag_name$end|$replacement|g;
146 0 0 0     0 $doagain = 1 if !$doagain && $replacement =~ /<[^>]+>/;
147             }
148             }
149             }
150             }
151              
152 0         0 $re = join('', @re);
153              
154             # do the actual replacement
155 0         0 $conf->{$type}->{$priority}->{$rule} = $re;
156              
157 0 0       0 if (would_log('dbg', 'replacetags') > 1) {
158 0         0 dbg("replacetags: replaced $rule: $re");
159             }
160              
161 0         0 $passes++;
162             } while $doagain && $passes <= 5;
163             }
164             }
165             }
166              
167             # free this up, if possible
168 63 50       289 if (!$conf->{allow_user_rules}) {
169 63         184 delete $conf->{rules_to_replace};
170             }
171              
172 63         242 dbg("replacetags: done replacing tags");
173             }
174              
175             sub user_conf_parsing_end {
176 0     0 1 0 my ($self, $opts) = @_;
177 0         0 return $self->finish_parsing_end($opts);
178             }
179              
180             sub set_config {
181 63     63 0 176 my ($self, $conf) = @_;
182 63         134 my @cmds;
183              
184             =head1 RULE DEFINITIONS AND PRIVILEGED SETTINGS
185              
186             =over 4
187              
188             =item replace_tag tagname expression
189              
190             Assign a valid regular expression to tagname.
191              
192             Note: It is not recommended to put quantifiers inside the tag, it's better to
193             put them inside the rule itself for greater flexibility.
194              
195             =cut
196              
197 63         336 push(@cmds, {
198             setting => 'replace_tag',
199             is_priv => 1,
200             type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
201             });
202              
203             =item replace_pre tagname expression
204              
205             Assign a valid regular expression to tagname. The expression will be
206             placed before each tag that is replaced.
207              
208             =cut
209              
210 63         249 push(@cmds, {
211             setting => 'replace_pre',
212             is_priv => 1,
213             type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
214             });
215              
216             =item replace_inter tagname expression
217              
218             Assign a valid regular expression to tagname. The expression will be
219             placed between each two immediately adjacent tags that are replaced.
220              
221             =cut
222              
223 63         236 push(@cmds, {
224             setting => 'replace_inter',
225             is_priv => 1,
226             type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
227             });
228              
229             =item replace_post tagname expression
230              
231             Assign a valid regular expression to tagname. The expression will be
232             placed after each tag that is replaced.
233              
234             =cut
235              
236 63         261 push(@cmds, {
237             setting => 'replace_post',
238             is_priv => 1,
239             type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
240             });
241              
242             =item replace_rules list_of_tests
243              
244             Specify a list of symbolic test names (separated by whitespace) of tests which
245             should be modified using replacement tags. Only simple regular expression
246             body, header, uri, full, rawbody tests are supported.
247              
248             =cut
249              
250             push(@cmds, {
251             setting => 'replace_rules',
252             is_priv => 1,
253             type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
254             code => sub {
255 0     0   0 my ($self, $key, $value, $line) = @_;
256 0 0 0     0 unless (defined $value && $value !~ /^$/) {
257 0         0 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
258             }
259 0 0       0 unless ($value =~ /\S+/) {
260 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
261             }
262 0         0 foreach my $rule (split(' ', $value)) {
263 0         0 $conf->{rules_to_replace}->{$rule} = 1;
264             }
265             }
266 63         856 });
267              
268             =item replace_start string
269              
270             =item replace_end string
271              
272             String(s) which indicate the start and end of a tag inside a rule. Only tags
273             enclosed by the start and end strings are found and replaced.
274              
275             =cut
276              
277 63         391 push(@cmds, {
278             setting => 'replace_start',
279             is_priv => 1,
280             default => '<',
281             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
282             });
283              
284 63         400 push(@cmds, {
285             setting => 'replace_end',
286             is_priv => 1,
287             default => '>',
288             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
289             });
290              
291 63         309 $conf->{parser}->register_commands(\@cmds);
292             }
293              
294             1;
295              
296             =back
297              
298             =cut