File Coverage

blib/lib/Mail/SpamAssassin/AutoWhitelist.pm
Criterion Covered Total %
statement 80 131 61.0
branch 28 64 43.7
condition 7 20 35.0
subroutine 14 18 77.7
pod 5 11 45.4
total 134 244 54.9


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::AutoWhitelist - auto-whitelist handler for SpamAssassin
21              
22             =head1 SYNOPSIS
23              
24             (see Mail::SpamAssassin)
25              
26              
27             =head1 DESCRIPTION
28              
29             Mail::SpamAssassin is a module to identify spam using text analysis and
30             several internet-based realtime blacklists.
31              
32             This class is used internally by SpamAssassin to manage the automatic
33             whitelisting functionality. Please refer to the C<Mail::SpamAssassin>
34             documentation for public interfaces.
35              
36             =head1 METHODS
37              
38             =over 4
39              
40             =cut
41              
42             package Mail::SpamAssassin::AutoWhitelist;
43              
44 21     21   130 use strict;
  21         47  
  21         636  
45 21     21   109 use warnings;
  21         63  
  21         765  
46             # use bytes;
47 21     21   130 use re 'taint';
  21         58  
  21         784  
48              
49 21     21   139 use NetAddr::IP 4.000;
  21         597  
  21         193  
50              
51 21     21   3548 use Mail::SpamAssassin;
  21         68  
  21         566  
52 21     21   129 use Mail::SpamAssassin::Logger;
  21         46  
  21         1447  
53 21     21   162 use Mail::SpamAssassin::Util qw(untaint_var);
  21         44  
  21         32566  
54              
55             our @ISA = qw();
56              
57             ###########################################################################
58              
59             sub new {
60 6     6 0 15 my $class = shift;
61 6   33     45 $class = ref($class) || $class;
62 6         20 my ($main, $msg) = @_;
63              
64 6         31 my $conf = $main->{conf};
65             my $self = {
66             main => $main,
67             factor => $conf->{auto_whitelist_factor},
68             ipv4_mask_len => $conf->{auto_whitelist_ipv4_mask_len},
69             ipv6_mask_len => $conf->{auto_whitelist_ipv6_mask_len},
70 6         54 };
71              
72 6         17 my $factory;
73 6 50       56 if ($main->{pers_addr_list_factory}) {
74 0         0 $factory = $main->{pers_addr_list_factory};
75             }
76             else {
77 6         18 my $type = $conf->{auto_whitelist_factory};
78 6 50       41 if ($type =~ /^([_A-Za-z0-9:]+)$/) {
79 6         28 $type = untaint_var($type);
80             eval '
81             require '.$type.';
82             $factory = '.$type.'->new();
83             1;
84             '
85 6 50       533 or do {
86 0 0       0 my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0         0  
87 0         0 warn "auto-whitelist: $eval_stat\n";
88 0         0 undef $factory;
89             };
90 6 50       70 $main->set_persistent_address_list_factory($factory) if $factory;
91             }
92             else {
93 0         0 warn "auto-whitelist: illegal auto_whitelist_factory setting\n";
94             }
95             }
96              
97 6 50       26 if (!defined $factory) {
98 0         0 $self->{checker} = undef;
99             } else {
100 6         29 $self->{checker} = $factory->new_checker($self->{main});
101             }
102              
103 6         33 bless ($self, $class);
104 6         25 $self;
105             }
106              
107             ###########################################################################
108              
109             =item $meanscore = awl->check_address($addr, $originating_ip, $signedby);
110              
111             This method will return the mean score of all messages associated with the
112             given address, or undef if the address hasn't been seen before.
113              
114             If B<$originating_ip> is supplied, it will be used in the lookup.
115              
116             =cut
117              
118             sub check_address {
119 6     6 1 26 my ($self, $addr, $origip, $signedby) = @_;
120              
121 6 50       38 if (!defined $self->{checker}) {
122 0         0 return; # no factory defined; we can't check
123             }
124              
125 6         18 $self->{entry} = undef;
126              
127 6         29 my $fulladdr = $self->pack_addr ($addr, $origip);
128 6         31 my $entry = $self->{checker}->get_addr_entry ($fulladdr, $signedby);
129 6         17 $self->{entry} = $entry;
130              
131 6 100       23 if (!$entry->{count}) {
132             # no entry found
133 5 100       27 if (defined $origip) {
134             # try upgrading a default entry (probably from "add-addr-to-foo")
135 1         3 my $noipaddr = $self->pack_addr ($addr, undef);
136 1         4 my $noipent = $self->{checker}->get_addr_entry ($noipaddr, undef);
137              
138 1 50 33     7 if (defined $noipent->{count} && $noipent->{count} > 0) {
139 0         0 dbg("auto-whitelist: found entry w/o IP address for $addr: replacing with $origip");
140 0         0 $self->{checker}->remove_entry($noipent);
141             # Now assign proper entry the count and totscore values of the
142             # no-IP entry instead of assigning the whole value to avoid
143             # wiping out any information added to the previous entry.
144 0         0 $entry->{count} = $noipent->{count};
145 0         0 $entry->{totscore} = $noipent->{totscore};
146             }
147             }
148             }
149              
150 6 50 33     70 if ($entry->{count} < 0 ||
      33        
151             $entry->{count} != $entry->{count} || # test for NaN
152             $entry->{totscore} != $entry->{totscore})
153             {
154 0         0 warn "auto-whitelist: resetting bad data for ($addr, $origip), ".
155             "count: $entry->{count}, totscore: $entry->{totscore}\n";
156 0         0 $entry->{count} = $entry->{totscore} = 0;
157             }
158              
159 6 100       38 return !$entry->{count} ? undef : $entry->{totscore} / $entry->{count};
160             }
161              
162             ###########################################################################
163              
164             =item awl->count();
165              
166             This method will return the count of messages used in determining the
167             whitelist correction.
168              
169             =cut
170              
171             sub count {
172 1     1 1 2 my $self = shift;
173 1         8 return $self->{entry}->{count};
174             }
175              
176              
177             ###########################################################################
178              
179             =item awl->add_score($score);
180              
181             This method will add half the score to the current entry. Half the
182             score is used, so that repeated use of the same From and IP address
183             combination will gradually reduce the score.
184              
185             =cut
186              
187             sub add_score {
188 5     5 1 17 my ($self,$score) = @_;
189              
190 5 50       33 if (!defined $self->{checker}) {
191 0         0 return; # no factory defined; we can't check
192             }
193 5 50       22 if ($score != $score) {
194 0         0 warn "auto-whitelist: attempt to add a $score to AWL entry ignored\n";
195 0         0 return; # don't try to add a NaN
196             }
197              
198 5   100     43 $self->{entry}->{count} ||= 0;
199 5         25 $self->{checker}->add_score($self->{entry}, $score);
200             }
201              
202             ###########################################################################
203              
204             =item awl->add_known_good_address($addr);
205              
206             This method will add a score of -100 to the given address -- effectively
207             "bootstrapping" the address as being one that should be whitelisted.
208              
209             =cut
210              
211             sub add_known_good_address {
212 0     0 1 0 my ($self, $addr, $signedby) = @_;
213              
214 0         0 return $self->modify_address($addr, -100, $signedby);
215             }
216              
217              
218             ###########################################################################
219              
220             =item awl->add_known_bad_address($addr);
221              
222             This method will add a score of 100 to the given address -- effectively
223             "bootstrapping" the address as being one that should be blacklisted.
224              
225             =cut
226              
227             sub add_known_bad_address {
228 0     0 1 0 my ($self, $addr, $signedby) = @_;
229              
230 0         0 return $self->modify_address($addr, 100, $signedby);
231             }
232              
233             ###########################################################################
234              
235             sub remove_address {
236 0     0 0 0 my ($self, $addr, $signedby) = @_;
237              
238 0         0 return $self->modify_address($addr, undef, $signedby);
239             }
240              
241             ###########################################################################
242              
243             sub modify_address {
244 0     0 0 0 my ($self, $addr, $score, $signedby) = @_;
245              
246 0 0       0 if (!defined $self->{checker}) {
247 0         0 return; # no factory defined; we can't check
248             }
249              
250 0         0 my $fulladdr = $self->pack_addr ($addr, undef);
251 0         0 my $entry = $self->{checker}->get_addr_entry ($fulladdr, $signedby);
252              
253             # remove any old entries (will remove per-ip entries as well)
254             # always call this regardless, as the current entry may have 0
255             # scores, but the per-ip one may have more
256 0         0 $self->{checker}->remove_entry($entry);
257              
258             # remove address only, no new score to add
259 0 0       0 if (!defined $score) { return 1; }
  0         0  
260 0 0       0 if ($score != $score) { return 1; } # don't try to add a NaN
  0         0  
261              
262             # else add score. get a new entry first
263 0         0 $entry = $self->{checker}->get_addr_entry ($fulladdr, $signedby);
264 0         0 $self->{checker}->add_score($entry, $score);
265              
266 0         0 return 1;
267             }
268              
269             ###########################################################################
270              
271             sub finish {
272 6     6 0 15 my $self = shift;
273              
274 6 50       25 return if !defined $self->{checker};
275 6         24 $self->{checker}->finish();
276             }
277              
278             ###########################################################################
279              
280             sub ip_to_awl_key {
281 1     1 0 2 my ($self, $origip) = @_;
282              
283 1         2 my $result;
284 1         2 local $1;
285 1 50 0     8 if (!defined $origip) {
    50          
    0          
286             # could not find an IP address to use
287             } elsif ($origip =~ /^ (\d{1,3} \. \d{1,3}) \. \d{1,3} \. \d{1,3} $/xs) {
288 1         2 my $mask_len = $self->{ipv4_mask_len};
289 1 50       3 $mask_len = 16 if !defined $mask_len;
290             # handle the default and easy cases manually
291 1 50       4 if ($mask_len == 32) {
    50          
292 0         0 $result = $origip;
293             } elsif ($mask_len == 16) {
294 1         3 $result = $1;
295             } else {
296 0         0 my $origip_obj = NetAddr::IP->new($origip . '/' . $mask_len);
297 0 0       0 if (!defined $origip_obj) { # invalid IPv4 address
298 0         0 dbg("auto-whitelist: bad IPv4 address $origip");
299             } else {
300 0         0 $result = $origip_obj->network->addr;
301 0         0 $result =~s/(\.0){1,3}\z//; # truncate zero tail
302             }
303             }
304             } elsif ($origip =~ /:/ && # triage
305             $origip =~
306             /^ [0-9a-f]{0,4} (?: : [0-9a-f]{0,4} | \. [0-9]{1,3} ){2,9} $/xsi) {
307             # looks like an IPv6 address
308 0         0 my $mask_len = $self->{ipv6_mask_len};
309 0 0       0 $mask_len = 48 if !defined $mask_len;
310 0         0 my $origip_obj = NetAddr::IP->new6($origip . '/' . $mask_len);
311 0 0       0 if (!defined $origip_obj) { # invalid IPv6 address
    0          
312 0         0 dbg("auto-whitelist: bad IPv6 address $origip");
313             } elsif (NetAddr::IP->can('full6')) { # since NetAddr::IP 4.010
314 0         0 $result = $origip_obj->network->full6; # string in a canonical form
315 0         0 $result =~ s/(:0000){1,7}\z/::/; # compress zero tail
316             }
317             } else {
318 0         0 dbg("auto-whitelist: bad IP address $origip");
319             }
320 1 50 33     11 if (defined $result && length($result) > 39) { # just in case, keep under
321 0         0 $result = substr($result,0,39); # the awl.ip field size
322             }
323 1 50       3 if (defined $result) {
324 1         4 dbg("auto-whitelist: IP masking %s -> %s", $origip,$result);
325             }
326 1         4 return $result;
327             }
328              
329             ###########################################################################
330              
331             sub pack_addr {
332 7     7 0 20 my ($self, $addr, $origip) = @_;
333              
334 7         29 $addr = lc $addr;
335 7         29 $addr =~ s/[\000\;\'\"\!\|]/_/gs; # paranoia
336              
337 7 100       23 if (defined $origip) {
338 1         3 $origip = $self->ip_to_awl_key($origip);
339             }
340 7 100       25 if (!defined $origip) {
341             # could not find an IP address to use, could be localhost mail
342             # or from the user running "add-addr-to-*".
343 6         30 $origip = 'none';
344             }
345 7         33 return $addr . "|ip=" . $origip;
346             }
347              
348             ###########################################################################
349              
350             1;
351              
352             =back
353              
354             =cut