File Coverage

blib/lib/Mail/SpamAssassin/AutoWhitelist.pm
Criterion Covered Total %
statement 81 131 61.8
branch 29 64 45.3
condition 7 20 35.0
subroutine 14 18 77.7
pod 5 11 45.4
total 136 244 55.7


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