# $Id$ ## Addressbook plugin for SpamAssassin ## ## Copyright (C) 2007 Karl Chen ## ## Released into the public domain ## Rationale: This provides a way to automatically give a negative score to ## all addresses in an addressbook, which may be updated dynamically. It's ## not the same as whitelisting all addresses because spammers may know about ## some of them, so we don't want to whitelist, only give a small negative ## score. It's not the same as autowhitelist (AWL), which is an unlabeled ## learner, i.e. it just smooths scores per sender over time, but doesn't help ## senders who have consistently high scores. ## ## Usage: ## ## loadplugin Addressbook addressbook.pm ## ## ifplugin Addressbook ## addressbook_path ~/.spamassassin/addressbook #default ## ## header FROM_IN_ADDRESSBOOK eval:check_from_in_addressbook() ## describe FROM_IN_ADDRESSBOOK From address in addressbook ## score FROM_IN_ADDRESSBOOK -3.0 ## endif ## ## ## addressbook should contain email addresses, one per line. ## quarl 2007-03-25 initial version package Addressbook; use strict; use warnings; use Mail::SpamAssassin; use Mail::SpamAssassin::Plugin; use Mail::SpamAssassin::Logger; # for 'dbg' use FileHandle; our @ISA = qw(Mail::SpamAssassin::Plugin); sub new { my ($class, $mailsa) = @_; $class = ref($class) || $class; my $self = $class->SUPER::new($mailsa); bless ($self, $class); $self->register_eval_rule("check_from_in_addressbook"); $self->set_config($mailsa->{conf}); return $self; } sub set_config { my($self, $conf) = @_; my @cmds = (); push (@cmds, { setting => 'addressbook_path', is_admin => 1, default => '__userstate__/addressbook', code => sub { my ($self, $key, $value, $line) = @_; unless (defined $value && $value !~ /^$/) { return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE; } if (-d $value) { return $Mail::SpamAssassin::Conf::INVALID_VALUE; } $self->{addressbook_path} = $value; } }); $conf->{parser}->register_commands(\@cmds); } my $CACHE = {}; sub get_addressbook { my ($main) = @_; my $path = $main->sed_path($main->{conf}->{addressbook_path}); my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size, $atime,$mtime,$ctime,$blksize,$blocks) = stat($path); if (!$mtime) { dbg("addressbook: $path not found"); return undef; } my $cached_mtime = $CACHE->{$path}->[0]; if ($cached_mtime && $cached_mtime == $mtime) { dbg("addressbook: $path unchanged"); return $CACHE->{$path}->[1]; } else { dbg("addressbook: reading $path, mtime=$mtime"); my $addressbook = read_addressbook($path); $CACHE->{$path} = [ $mtime, $addressbook ]; return $addressbook; } } sub read_addressbook { my ($path) = @_; # dbg("read_addressbook('$path')"); ## TODO: may want to setfsuid (or use access(2) if we have to) to read ## addressbook, in case we're running as spamd my $f = new FileHandle($path, 'r'); if (!$f) { warn "addressbook: couldn't open $path: $!\n"; return {}; } my $result = {}; local $_; while (<$f>) { chomp; s/;.*//; s/#.*//; s/^ +//; s/ +$//; if (/<(.*?)>/) { $_ = $1; } next unless $_; $result->{lc($_)} = 1; } return $result; } sub check_from_in_addressbook { my ($self, $permsgstatus) = @_; my $from = lc $permsgstatus->get('From:addr'); return 0 unless $from; my $addressbook = get_addressbook($self->{main}); if ($addressbook->{$from}) { dbg("addressbook: '$from' in addressbook"); return 1; } else { dbg("addressbook: '$from' NOT in addressbook"); return 0; } } 1;