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             package Mail::SpamAssassin::AutoWhitelist;
43              
44 19     19   138 use strict;
  19         42  
  19         672  
45 19     19   125 use warnings;
  19         49  
  19         655  
46             # use bytes;
47 19     19   111 use re 'taint';
  19         59  
  19         745  
48              
49 19     19   126 use NetAddr::IP 4.000;
  19         547  
  19         164  
50              
51 19     19   4154 use Mail::SpamAssassin;
  19         56  
  19         534  
52 19     19   142 use Mail::SpamAssassin::Logger;
  19         57  
  19         1353  
53 19     19   146 use Mail::SpamAssassin::Util qw(untaint_var);
  19         42  
  19         30722  
54              
55             our @ISA = qw();
56              
57             ###########################################################################
58              
59             sub new {
60 10     10 0 28 my $class = shift;
61 10   33     74 $class = ref($class) || $class;
62 10         35 my ($main, $msg) = @_;
63              
64 10         29 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 10         78 };
71              
72 10         20 my $factory;
73 10 100       43 if ($main->{pers_addr_list_factory}) {
74 3         8 $factory = $main->{pers_addr_list_factory};
75             }
76             else {
77 7         22 my $type = $conf->{auto_whitelist_factory};
78 7 50       60 if ($type =~ /^([_A-Za-z0-9:]+)$/) {
79 7         38 $type = untaint_var($type);
80             eval '
81             require '.$type.';
82             $factory = '.$type.'->new();
83             1;
84             '
85 7 50       602 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 7 50       76 $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 10 50       40 if (!defined $factory) {
98 0         0 $self->{checker} = undef;
99             } else {
100 10         50 $self->{checker} = $factory->new_checker($self->{main});
101             }
102              
103 10         43 bless ($self, $class);
104 10         53 $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 10     10 1 53 my ($self, $addr, $origip, $signedby) = @_;
120              
121 10 50       58 if (!defined $self->{checker}) {
122 0         0 return; # no factory defined; we can't check
123             }
124              
125 10         36 $self->{entry} = undef;
126              
127 10         61 my $fulladdr = $self->pack_addr ($addr, $origip);
128 10         95 my $entry = $self->{checker}->get_addr_entry ($fulladdr, $signedby);
129 10         33 $self->{entry} = $entry;
130              
131 10 100       51 if (!$entry->{msgcount}) {
132             # no entry found
133 7 100       29 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     8 if (defined $noipent->{msgcount} && $noipent->{msgcount} > 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->{msgcount} = $noipent->{msgcount};
145 0         0 $entry->{totscore} = $noipent->{totscore};
146             }
147             }
148             }
149              
150 10 50 33     133 if ($entry->{msgcount} < 0 ||
      33        
151             $entry->{msgcount} != $entry->{msgcount} || # test for NaN
152             $entry->{totscore} != $entry->{totscore})
153             {
154 0         0 warn "auto-whitelist: resetting bad data for ($addr, $origip), ".
155             "count: $entry->{msgcount}, totscore: $entry->{totscore}\n";
156 0         0 $entry->{msgcount} = $entry->{totscore} = 0;
157             }
158              
159 10 100       102 return !$entry->{msgcount} ? undef : $entry->{totscore} / $entry->{msgcount};
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 3     3 1 8 my $self = shift;
173 3         23 return $self->{entry}->{msgcount};
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 8     8 1 39 my ($self,$score) = @_;
189              
190 8 50       33 if (!defined $self->{checker}) {
191 0         0 return; # no factory defined; we can't check
192             }
193 8 50       40 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 8   100     59 $self->{entry}->{msgcount} ||= 0;
199 8         42 $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 10     10 0 25 my $self = shift;
273              
274 10 50       40 return if !defined $self->{checker};
275 10         55 $self->{checker}->finish();
276             }
277              
278             ###########################################################################
279              
280             sub ip_to_awl_key {
281 1     1 0 3 my ($self, $origip) = @_;
282              
283 1         2 my $result;
284 1         3 local $1;
285 1 50 0     10 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         4 my $mask_len = $self->{ipv4_mask_len};
289 1 50       4 $mask_len = 16 if !defined $mask_len;
290             # handle the default and easy cases manually
291 1 50       12 if ($mask_len == 32) {
    50          
292 0         0 $result = $origip;
293             } elsif ($mask_len == 16) {
294 1         4 $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     14 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       4 if (defined $result) {
324 1         4 dbg("auto-whitelist: IP masking %s -> %s", $origip,$result);
325             }
326 1         5 return $result;
327             }
328              
329             ###########################################################################
330              
331             sub pack_addr {
332 11     11 0 55 my ($self, $addr, $origip) = @_;
333              
334 11         46 $addr = lc $addr;
335 11         47 $addr =~ s/[\000\;\'\"\!\|]/_/gs; # paranoia
336              
337 11 100       48 if (defined $origip) {
338 1         4 $origip = $self->ip_to_awl_key($origip);
339             }
340 11 100       41 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 10         26 $origip = 'none';
344             }
345 11         64 return $addr . "|ip=" . $origip;
346             }
347              
348             ###########################################################################
349              
350             1;
351              
352             =back
353              
354             =cut