File Coverage

blib/lib/Mail/SpamAssassin/Plugin/AWL.pm
Criterion Covered Total %
statement 100 198 50.5
branch 22 96 22.9
condition 7 35 20.0
subroutine 11 17 64.7
pod 4 6 66.6
total 144 352 40.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::Plugin::AWL - Normalize scores via auto-whitelist
21              
22             =head1 SYNOPSIS
23              
24             To try this out, add this or uncomment this line in init.pre:
25              
26             loadplugin Mail::SpamAssassin::Plugin::AWL
27              
28             Use the supplied 60_awl.cf file (ie you don't have to do anything) or
29             add these lines to a .cf file:
30              
31             header AWL eval:check_from_in_auto_whitelist()
32             describe AWL From: address is in the auto white-list
33             tflags AWL userconf noautolearn
34             priority AWL 1000
35              
36             =head1 DESCRIPTION
37              
38             This plugin module provides support for the auto-whitelist. It keeps
39             track of the average SpamAssassin score for senders. Senders are
40             tracked using a combination of their From: address and their IP address.
41             It then uses that average score to reduce the variability in scoring
42             from message to message and modifies the final score by pushing the
43             result towards the historical average. This improves the accuracy of
44             filtering for most email.
45              
46             =head1 TEMPLATE TAGS
47              
48             This plugin module adds the following C<tags> that can be used as
49             placeholders in certain options. See C<Mail::SpamAssassin::Conf>
50             for more information on TEMPLATE TAGS.
51              
52             _AWL_ AWL modifier
53             _AWLMEAN_ Mean score on which AWL modification is based
54             _AWLCOUNT_ Number of messages on which AWL modification is based
55             _AWLPRESCORE_ Score before AWL
56              
57             =cut
58              
59             package Mail::SpamAssassin::Plugin::AWL;
60              
61 21     21   144 use strict;
  21         50  
  21         659  
62 21     21   108 use warnings;
  21         47  
  21         644  
63             # use bytes;
64 21     21   132 use re 'taint';
  21         50  
  21         684  
65 21     21   179 use Mail::SpamAssassin::Plugin;
  21         51  
  21         569  
66 21     21   8470 use Mail::SpamAssassin::AutoWhitelist;
  21         65  
  21         731  
67 21     21   154 use Mail::SpamAssassin::Util qw(untaint_var);
  21         58  
  21         900  
68 21     21   135 use Mail::SpamAssassin::Logger;
  21         46  
  21         43134  
69              
70             our @ISA = qw(Mail::SpamAssassin::Plugin);
71              
72             # constructor: register the eval rule
73             sub new {
74 62     62 1 265 my $class = shift;
75 62         169 my $mailsaobject = shift;
76              
77             # some boilerplate...
78 62   33     401 $class = ref($class) || $class;
79 62         357 my $self = $class->SUPER::new($mailsaobject);
80 62         185 bless ($self, $class);
81              
82             # the important bit!
83 62         291 $self->register_eval_rule("check_from_in_auto_whitelist");
84              
85 62         310 $self->set_config($mailsaobject->{conf});
86              
87 62         673 return $self;
88             }
89              
90             sub set_config {
91 62     62 0 172 my($self, $conf) = @_;
92 62         142 my @cmds;
93              
94             =head1 USER PREFERENCES
95              
96             The following options can be used in both site-wide (C<local.cf>) and
97             user-specific (C<user_prefs>) configuration files to customize how
98             SpamAssassin handles incoming email messages.
99              
100             =over 4
101              
102             =item use_auto_whitelist ( 0 | 1 ) (default: 1)
103              
104             Whether to use auto-whitelists. Auto-whitelists track the long-term
105             average score for each sender and then shift the score of new messages
106             toward that long-term average. This can increase or decrease the score
107             for messages, depending on the long-term behavior of the particular
108             correspondent.
109              
110             For more information about the auto-whitelist system, please look
111             at the the C<Automatic Whitelist System> section of the README file.
112             The auto-whitelist is not intended as a general-purpose replacement
113             for static whitelist entries added to your config files.
114              
115             Note that certain tests are ignored when determining the final
116             message score:
117              
118             - rules with tflags set to 'noautolearn'
119              
120             =cut
121              
122 62         404 push (@cmds, {
123             setting => 'use_auto_whitelist',
124             default => 1,
125             type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL
126             });
127              
128             =item auto_whitelist_factor n (default: 0.5, range [0..1])
129              
130             How much towards the long-term mean for the sender to regress a message.
131             Basically, the algorithm is to track the long-term mean score of messages for
132             the sender (C<mean>), and then once we have otherwise fully calculated the
133             score for this message (C<score>), we calculate the final score for the
134             message as:
135              
136             C<finalscore> = C<score> + (C<mean> - C<score>) * C<factor>
137              
138             So if C<factor> = 0.5, then we'll move to half way between the calculated
139             score and the mean. If C<factor> = 0.3, then we'll move about 1/3 of the way
140             from the score toward the mean. C<factor> = 1 means just use the long-term
141             mean; C<factor> = 0 mean just use the calculated score.
142              
143             =cut
144              
145 62         310 push (@cmds, {
146             setting => 'auto_whitelist_factor',
147             default => 0.5,
148             type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
149             });
150              
151             =item auto_whitelist_ipv4_mask_len n (default: 16, range [0..32])
152              
153             The AWL database keeps only the specified number of most-significant bits
154             of an IPv4 address in its fields, so that different individual IP addresses
155             within a subnet belonging to the same owner are managed under a single
156             database record. As we have no information available on the allocated
157             address ranges of senders, this CIDR mask length is only an approximation.
158             The default is 16 bits, corresponding to a former class B. Increase the
159             number if a finer granularity is desired, e.g. to 24 (class C) or 32.
160             A value 0 is allowed but is not particularly useful, as it would treat the
161             whole internet as a single organization. The number need not be a multiple
162             of 8, any split is allowed.
163              
164             =cut
165              
166             push (@cmds, {
167             setting => 'auto_whitelist_ipv4_mask_len',
168             default => 16,
169             type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
170             code => sub {
171 0     0   0 my ($self, $key, $value, $line) = @_;
172 0 0 0     0 if (!defined $value || $value eq '') {
    0 0        
      0        
173 0         0 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
174             } elsif ($value !~ /^\d+$/ || $value < 0 || $value > 32) {
175 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
176             }
177 0         0 $self->{auto_whitelist_ipv4_mask_len} = $value;
178             }
179 62         599 });
180              
181             =item auto_whitelist_ipv6_mask_len n (default: 48, range [0..128])
182              
183             The AWL database keeps only the specified number of most-significant bits
184             of an IPv6 address in its fields, so that different individual IP addresses
185             within a subnet belonging to the same owner are managed under a single
186             database record. As we have no information available on the allocated address
187             ranges of senders, this CIDR mask length is only an approximation. The default
188             is 48 bits, corresponding to an address range commonly allocated to individual
189             (smaller) organizations. Increase the number for a finer granularity, e.g.
190             to 64 or 96 or 128, or decrease for wider ranges, e.g. 32. A value 0 is
191             allowed but is not particularly useful, as it would treat the whole internet
192             as a single organization. The number need not be a multiple of 4, any split
193             is allowed.
194              
195             =cut
196              
197             push (@cmds, {
198             setting => 'auto_whitelist_ipv6_mask_len',
199             default => 48,
200             type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
201             code => sub {
202 0     0   0 my ($self, $key, $value, $line) = @_;
203 0 0 0     0 if (!defined $value || $value eq '') {
    0 0        
      0        
204 0         0 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
205             } elsif ($value !~ /^\d+$/ || $value < 0 || $value > 128) {
206 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
207             }
208 0         0 $self->{auto_whitelist_ipv6_mask_len} = $value;
209             }
210 62         484 });
211              
212             =item user_awl_sql_override_username
213              
214             Used by the SQLBasedAddrList storage implementation.
215              
216             If this option is set the SQLBasedAddrList module will override the set
217             username with the value given. This can be useful for implementing global
218             or group based auto-whitelist databases.
219              
220             =cut
221              
222 62         320 push (@cmds, {
223             setting => 'user_awl_sql_override_username',
224             default => '',
225             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING
226             });
227              
228             =item auto_whitelist_distinguish_signed
229              
230             Used by the SQLBasedAddrList storage implementation.
231              
232             If this option is set the SQLBasedAddrList module will keep separate
233             database entries for DKIM-validated e-mail addresses and for non-validated
234             ones. A pre-requisite when setting this option is that a field awl.signedby
235             exists in a SQL table, otherwise SQL operations will fail (which is why we
236             need this option at all - for compatibility with pre-3.3.0 database schema).
237             A plugin DKIM should also be enabled, as otherwise there is no benefit from
238             turning on this option.
239              
240             =cut
241              
242 62         280 push (@cmds, {
243             setting => 'auto_whitelist_distinguish_signed',
244             default => 0,
245             type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL
246             });
247              
248             =back
249              
250             =head1 ADMINISTRATOR SETTINGS
251              
252             These settings differ from the ones above, in that they are considered 'more
253             privileged' -- even more than the ones in the B<PRIVILEGED SETTINGS> section.
254             No matter what C<allow_user_rules> is set to, these can never be set from a
255             user's C<user_prefs> file.
256              
257             =over 4
258              
259             =item auto_whitelist_factory module (default: Mail::SpamAssassin::DBBasedAddrList)
260              
261             Select alternative whitelist factory module.
262              
263             =cut
264              
265 62         284 push (@cmds, {
266             setting => 'auto_whitelist_factory',
267             is_admin => 1,
268             default => 'Mail::SpamAssassin::DBBasedAddrList',
269             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING
270             });
271              
272             =item auto_whitelist_path /path/filename (default: ~/.spamassassin/auto-whitelist)
273              
274             This is the automatic-whitelist directory and filename. By default, each user
275             has their own whitelist database in their C<~/.spamassassin> directory with
276             mode 0700. For system-wide SpamAssassin use, you may want to share this
277             across all users, although that is not recommended.
278              
279             =cut
280              
281             push (@cmds, {
282             setting => 'auto_whitelist_path',
283             is_admin => 1,
284             default => '__userstate__/auto-whitelist',
285             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
286             code => sub {
287 62     62   259 my ($self, $key, $value, $line) = @_;
288 62 50 33     612 unless (defined $value && $value !~ /^$/) {
289 0         0 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
290             }
291 62 50       1001 if (-d $value) {
292 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
293             }
294 62         430 $self->{auto_whitelist_path} = $value;
295             }
296 62         497 });
297              
298             =item auto_whitelist_db_modules Module ... (default: see below)
299              
300             What database modules should be used for the auto-whitelist storage database
301             file. The first named module that can be loaded from the perl include path
302             will be used. The format is:
303              
304             PreferredModuleName SecondBest ThirdBest ...
305              
306             ie. a space-separated list of perl module names. The default is:
307              
308             DB_File GDBM_File SDBM_File
309              
310             NDBM_File is no longer supported, since it appears to have bugs that
311             preclude its use for the AWL (see SpamAssassin bug 4353).
312              
313             =cut
314              
315 62         387 push (@cmds, {
316             setting => 'auto_whitelist_db_modules',
317             is_admin => 1,
318             default => 'DB_File GDBM_File SDBM_File',
319             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING
320             });
321              
322             =item auto_whitelist_file_mode (default: 0700)
323              
324             The file mode bits used for the automatic-whitelist directory or file.
325              
326             Make sure you specify this using the 'x' mode bits set, as it may also be used
327             to create directories. However, if a file is created, the resulting file will
328             not have any execute bits set (the umask is set to 0111).
329              
330             =cut
331              
332             push (@cmds, {
333             setting => 'auto_whitelist_file_mode',
334             is_admin => 1,
335             default => '0700',
336             type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
337             code => sub {
338 0     0   0 my ($self, $key, $value, $line) = @_;
339 0 0       0 if ($value !~ /^0?[0-7]{3}$/) {
340 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
341             }
342 0         0 $self->{auto_whitelist_file_mode} = untaint_var($value);
343             }
344 62         447 });
345              
346             =item user_awl_dsn DBI:databasetype:databasename:hostname:port
347              
348             Used by the SQLBasedAddrList storage implementation.
349              
350             This will set the DSN used to connect. Example:
351             C<DBI:mysql:spamassassin:localhost>
352              
353             =cut
354              
355 62         282 push (@cmds, {
356             setting => 'user_awl_dsn',
357             is_admin => 1,
358             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING
359             });
360              
361             =item user_awl_sql_username username
362              
363             Used by the SQLBasedAddrList storage implementation.
364              
365             The authorized username to connect to the above DSN.
366              
367             =cut
368              
369 62         242 push (@cmds, {
370             setting => 'user_awl_sql_username',
371             is_admin => 1,
372             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING
373             });
374              
375             =item user_awl_sql_password password
376              
377             Used by the SQLBasedAddrList storage implementation.
378              
379             The password for the database username, for the above DSN.
380              
381             =cut
382              
383 62         236 push (@cmds, {
384             setting => 'user_awl_sql_password',
385             is_admin => 1,
386             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING
387             });
388              
389             =item user_awl_sql_table tablename
390              
391             Used by the SQLBasedAddrList storage implementation.
392              
393             The table user auto-whitelists are stored in, for the above DSN.
394              
395             =cut
396              
397 62         420 push (@cmds, {
398             setting => 'user_awl_sql_table',
399             is_admin => 1,
400             default => 'awl',
401             type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING
402             });
403              
404 62         318 $conf->{parser}->register_commands(\@cmds);
405             }
406              
407             sub check_from_in_auto_whitelist {
408 77     77 0 220 my ($self, $pms) = @_;
409              
410 77 100       1244 return 0 unless ($pms->{conf}->{use_auto_whitelist});
411              
412 13         54 my $timer = $self->{main}->time_method("total_awl");
413              
414 13         47 my $from = lc $pms->get('From:addr');
415             # dbg("auto-whitelist: From: $from");
416 13 100       147 return 0 unless $from =~ /\S/;
417              
418             # find the earliest usable "originating IP". ignore private nets
419 6         14 my $origip;
420 6         12 foreach my $rly (reverse (@{$pms->{relays_trusted}}, @{$pms->{relays_untrusted}}))
  6         20  
  6         26  
421             {
422 1 50       3 next if ($rly->{ip_private});
423 1 50       4 if ($rly->{ip}) {
424 1         9 $origip = $rly->{ip}; last;
  1         2  
425             }
426             }
427              
428 6         20 my $scores = $pms->{conf}->{scores};
429 6         14 my $tflags = $pms->{conf}->{tflags};
430 6         14 my $points = 0;
431 6         29 my $signedby = $pms->get_tag('DKIMDOMAIN');
432 6 50 33     46 undef $signedby if defined $signedby && $signedby eq '';
433              
434 6         20 foreach my $test (@{$pms->{test_names_hit}}) {
  6         23  
435             # ignore tests with 0 score in this scoreset,
436             # or if the test is marked as "noautolearn"
437 10 50       39 next if !$scores->{$test};
438 10 50 33     51 next if exists $tflags->{$test} && $tflags->{$test} =~ /\bnoautolearn\b/;
439 10 50 33     49 return 0 if exists $tflags->{$test} && $tflags->{$test} =~ /\bnoawl\b/;
440 10         30 $points += $scores->{$test};
441             }
442              
443 6         96 my $awlpoints = (sprintf "%0.3f", $points) + 0;
444              
445             # Create the AWL object
446 6         14 my $whitelist;
447             eval {
448 6         75 $whitelist = Mail::SpamAssassin::AutoWhitelist->new($pms->{main});
449              
450 6         14 my $meanscore;
451             { # check
452 6         22 my $timer = $self->{main}->time_method("check_awl");
  6         42  
453 6         35 $meanscore = $whitelist->check_address($from, $origip, $signedby);
454             }
455 6         16 my $delta = 0;
456              
457             dbg("auto-whitelist: AWL active, pre-score: %s, autolearn score: %s, ".
458             "mean: %s, IP: %s, address: %s %s",
459 6 100 100     101 $pms->{score}, $awlpoints,
    50          
460             !defined $meanscore ? 'undef' : sprintf("%.3f",$meanscore),
461             $origip || 'undef',
462             $from, $signedby ? "signed by $signedby" : '(not signed)');
463              
464 6 100       24 if (defined $meanscore) {
465 1         30 $delta = $meanscore - $awlpoints;
466 1         7 $delta *= $pms->{main}->{conf}->{auto_whitelist_factor};
467            
468 1         13 $pms->set_tag('AWL', sprintf("%2.1f",$delta));
469 1 50       5 if (defined $meanscore) {
470 1         8 $pms->set_tag('AWLMEAN', sprintf("%2.1f", $meanscore));
471             }
472 1         6 $pms->set_tag('AWLCOUNT', sprintf("%2.1f", $whitelist->count()));
473 1         9 $pms->set_tag('AWLPRESCORE', sprintf("%2.1f", $pms->{score}));
474             }
475              
476             # Update the AWL *before* adding the new score, otherwise
477             # early high-scoring messages are reinforced compared to
478             # later ones. http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=159704
479 6 100       27 if (!$pms->{disable_auto_learning}) {
480 5         21 my $timer = $self->{main}->time_method("update_awl");
481 5         24 $whitelist->add_score($awlpoints);
482             }
483              
484             # now redundant, got_hit() takes care of it
485             # for my $set (0..3) { # current AWL score changes with each hit
486             # $pms->{conf}->{scoreset}->[$set]->{"AWL"} = sprintf("%0.3f", $delta);
487             # }
488              
489 6 50       41 if ($delta != 0) {
490 0         0 $pms->got_hit("AWL", "AWL: ", ruletype => 'eval',
491             score => sprintf("%0.3f", $delta));
492             }
493              
494 6         36 $whitelist->finish();
495 6         32 1;
496 6 50       13 } or do {
497 0 0       0 my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0         0  
498 0         0 warn("auto-whitelist: open of auto-whitelist file failed: $eval_stat\n");
499             # try an unlock, in case we got that far
500 0 0       0 eval { $whitelist->finish(); } if $whitelist;
  0         0  
501 0         0 return 0;
502             };
503              
504 6         28 dbg("auto-whitelist: post auto-whitelist score: %.3f", $pms->{score});
505              
506             # test hit is above
507 6         227 return 0;
508             }
509              
510             sub blacklist_address {
511 0     0 1   my ($self, $args) = @_;
512              
513 0 0         return 0 unless ($self->{main}->{conf}->{use_auto_whitelist});
514              
515 0 0         unless ($args->{address}) {
516 0 0         print "SpamAssassin auto-whitelist: failed to add address to blacklist\n" if ($args->{cli_p});
517 0           dbg("auto-whitelist: failed to add address to blacklist");
518 0           return;
519             }
520            
521 0           my $whitelist;
522             my $status;
523              
524             eval {
525 0           $whitelist = Mail::SpamAssassin::AutoWhitelist->new($self->{main});
526              
527 0 0         if ($whitelist->add_known_bad_address($args->{address}, $args->{signedby})) {
528 0 0         print "SpamAssassin auto-whitelist: adding address to blacklist: " . $args->{address} . "\n" if ($args->{cli_p});
529 0           dbg("auto-whitelist: adding address to blacklist: " . $args->{address});
530 0           $status = 0;
531             }
532             else {
533 0 0         print "SpamAssassin auto-whitelist: error adding address to blacklist\n" if ($args->{cli_p});
534 0           dbg("auto-whitelist: error adding address to blacklist");
535 0           $status = 1;
536             }
537 0           $whitelist->finish();
538 0           1;
539 0 0         } or do {
540 0 0         my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0            
541 0           warn("auto-whitelist: open of auto-whitelist file failed: $eval_stat\n");
542 0           eval { $whitelist->finish(); };
  0            
543 0           return 0;
544             };
545              
546 0           return $status;
547             }
548              
549             sub whitelist_address {
550 0     0 1   my ($self, $args) = @_;
551              
552 0 0         return 0 unless ($self->{main}->{conf}->{use_auto_whitelist});
553              
554 0 0         unless ($args->{address}) {
555 0 0         print "SpamAssassin auto-whitelist: failed to add address to whitelist\n" if ($args->{cli_p});
556 0           dbg("auto-whitelist: failed to add address to whitelist");
557 0           return 0;
558             }
559              
560 0           my $whitelist;
561             my $status;
562              
563             eval {
564 0           $whitelist = Mail::SpamAssassin::AutoWhitelist->new($self->{main});
565              
566 0 0         if ($whitelist->add_known_good_address($args->{address}, $args->{signedby})) {
567 0 0         print "SpamAssassin auto-whitelist: adding address to whitelist: " . $args->{address} . "\n" if ($args->{cli_p});
568 0           dbg("auto-whitelist: adding address to whitelist: " . $args->{address});
569 0           $status = 1;
570             }
571             else {
572 0 0         print "SpamAssassin auto-whitelist: error adding address to whitelist\n" if ($args->{cli_p});
573 0           dbg("auto-whitelist: error adding address to whitelist");
574 0           $status = 0;
575             }
576              
577 0           $whitelist->finish();
578 0           1;
579 0 0         } or do {
580 0 0         my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0            
581 0           warn("auto-whitelist: open of auto-whitelist file failed: $eval_stat\n");
582 0           eval { $whitelist->finish(); };
  0            
583 0           return 0;
584             };
585              
586 0           return $status;
587             }
588              
589             sub remove_address {
590 0     0 1   my ($self, $args) = @_;
591              
592 0 0         return 0 unless ($self->{main}->{conf}->{use_auto_whitelist});
593              
594 0 0         unless ($args->{address}) {
595 0 0         print "SpamAssassin auto-whitelist: failed to remove address\n" if ($args->{cli_p});
596 0           dbg("auto-whitelist: failed to remove address");
597 0           return 0;
598             }
599              
600 0           my $whitelist;
601             my $status;
602              
603             eval {
604 0           $whitelist = Mail::SpamAssassin::AutoWhitelist->new($self->{main});
605              
606 0 0         if ($whitelist->remove_address($args->{address}, $args->{signedby})) {
607 0 0         print "SpamAssassin auto-whitelist: removing address: " . $args->{address} . "\n" if ($args->{cli_p});
608 0           dbg("auto-whitelist: removing address: " . $args->{address});
609 0           $status = 1;
610             }
611             else {
612 0 0         print "SpamAssassin auto-whitelist: error removing address\n" if ($args->{cli_p});
613 0           dbg("auto-whitelist: error removing address");
614 0           $status = 0;
615             }
616            
617 0           $whitelist->finish();
618 0           1;
619 0 0         } or do {
620 0 0         my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0            
621 0           warn("auto-whitelist: open of auto-whitelist file failed: $eval_stat\n");
622 0           eval { $whitelist->finish(); };
  0            
623 0           return 0;
624             };
625              
626 0           return $status;
627             }
628              
629             1;
630              
631             =back
632              
633             =cut