# <@LICENSE>
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to you under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at:
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# </@LICENSE>
=head1 NAME
Mail::SpamAssassin::SQLBasedAddrList - SpamAssassin SQL Based Auto Whitelist
=head1 SYNOPSIS
my $factory = Mail::SpamAssassin::SQLBasedAddrList->new()
$spamtest->set_persistent_addr_list_factory ($factory);
... call into SpamAssassin classes...
SpamAssassin will call:
my $addrlist = $factory->new_checker($spamtest);
$entry = $addrlist->get_addr_entry ($addr, $origip);
...
=head1 DESCRIPTION
A SQL based persistent address list implementation.
See C<Mail::SpamAssassin::PersistentAddrList> for more information.
Uses DBI::DBD module access to your favorite database (tested with
MySQL, SQLite and PostgreSQL) to store user auto-whitelists.
The default table structure looks like this:
CREATE TABLE awl (
username VARCHAR NOT NULL,
email VARCHAR NOT NULL,
ip VARCHAR NOT NULL,
count INT NOT NULL,
totscore FLOAT NOT NULL,
PRIMARY KEY (username, email, ip)
)
You're table definition may change depending on which database driver
you choose. There is a config option to override the table name.
This module introduces several new config variables:
user_awl_dsn
user_awl_sql_username
user_awl_sql_password
user_awl_sql_table
user_awl_sql_override_username
see C<Mail::SpamAssassin::Conf> for more information.
=cut
package Mail::SpamAssassin::SQLBasedAddrList;
use strict;
use warnings;
use bytes;
# Do this silliness to stop RPM from finding DBI as required
BEGIN { require DBI; import DBI; }
use Mail::SpamAssassin::PersistentAddrList;
use Mail::SpamAssassin::Logger;
use vars qw(@ISA);
@ISA = qw(Mail::SpamAssassin::PersistentAddrList);
=head2 new
public class (Mail::SpamAssassin::SQLBasedAddrList) new ()
Description:
This method creates a new instance of the SQLBasedAddrList factory and calls
the parent's (PersistentAddrList) new method.
=cut
sub new {
my ($proto) = @_;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(@_);
$self->{class} = $class;
bless ($self, $class);
$self;
}
=head2 new_checker
public instance (Mail::SpamAssassin::SQLBasedAddrList) new_checker (\% $main)
Description:
This method is called to setup a new checker interface and return a blessed
copy of itself. Here is where we setup the SQL database connection based
on the config values.
=cut
sub new_checker {
my ($self, $main) = @_;
my $class = $self->{class};
if (!$main->{conf}->{user_awl_dsn} ||
!$main->{conf}->{user_awl_sql_table}) {
dbg("auto-whitelist: sql-based invalid config");
return undef;
}
my $dsn = $main->{conf}->{user_awl_dsn};
my $dbuser = $main->{conf}->{user_awl_sql_username};
my $dbpass = $main->{conf}->{user_awl_sql_password};
my $dbh = DBI->connect($dsn, $dbuser, $dbpass, {'PrintError' => 0});
if(!$dbh) {
dbg("auto-whitelist: sql-based unable to connect to database ($dsn) : " . DBI::errstr);
return undef;
}
dbg("auto-whitelist: sql-based connected to $dsn");
$self = { 'main' => $main,
'dsn' => $dsn,
'dbh' => $dbh,
'tablename' => $main->{conf}->{user_awl_sql_table},
};
if ($main->{conf}->{user_awl_sql_override_username}) {
$self->{_username} = $main->{conf}->{user_awl_sql_override_username};
}
else {
$self->{_username} = $main->{username};
# Need to make sure that a username is set, so just in case there is
# no username set in main, set one here.
unless ($self->{_username}) {
$self->{_username} = "GLOBAL";
}
}
dbg("auto-whitelist: sql-based using username: ".$self->{_username});
return bless ($self, $class);
}
=head2 get_addr_entry
public instance (\%) get_addr_entry (String $addr)
Description:
This method takes a given C<$addr> and splits it between the email address
component and the ip component and performs a lookup in the database. If
nothing is found in the database then a blank entry hash is created and
returned, otherwise an entry containing the found information is returned.
A key, C<exists_p>, is set to 1 if an entry already exists in the database,
otherwise it is set to 0.
=cut
sub get_addr_entry {
my ($self, $addr) = @_;
my $entry = { addr => $addr,
exists_p => 0,
count => 0,
totscore => 0,
};
my ($email, $ip) = $self->_unpack_addr($addr);
return $entry unless ($email && $ip);
my $sql = "SELECT count, totscore FROM $self->{tablename}
WHERE username = ? AND email = ? AND ip = ?";
my $sth = $self->{dbh}->prepare($sql);
my $rc = $sth->execute($self->{_username}, $email, $ip);
if (!$rc) { # there was an error, but try to go on
my $err = $self->{dbh}->errstr;
dbg("auto-whitelist: sql-based get_addr_entry: SQL error: $err");
$entry->{count} = 0;
$entry->{totscore} = 0;
}
else {
my $aryref = $sth->fetchrow_arrayref();
if (defined($aryref)) { # we got some data back
$entry->{count} = $aryref->[0] || 0;
$entry->{totscore} = $aryref->[1] || 0;
$entry->{exists_p} = 1;
dbg("auto-whitelist: sql-based get_addr_entry: found existing entry for $addr");
}
else {
dbg("auto-whitelist: sql-based get_addr_entry: no entry found for $addr");
}
}
$sth->finish();
dbg("auto-whitelist: sql-based $addr scores ".$entry->{count}.'/'.$entry->{totscore});
return $entry;
}
=head2 add_score
public instance (\%) add_score (\% $entry, Integer $score)
Description:
This method adds a given C<$score> to a given C<$entry>. If the entry was
marked as not existing in the database then an entry will be inserted,
otherwise a simple update will be performed.
NOTE: This code uses a self referential SQL call (ie set foo = foo + 1) which
is supported by most modern database backends, but not everything calling
itself a SQL database.
=cut
sub add_score {
my($self, $entry, $score) = @_;
return if (!$entry->{addr});
my ($email, $ip) = $self->_unpack_addr($entry->{addr});
$entry->{count} += 1;
$entry->{totscore} += $score;
return $entry unless ($email && $ip);
if ($entry->{exists_p}) { # entry already exists, so just update
my $sql = "UPDATE $self->{tablename} SET count = count + 1,
totscore = totscore + ?
WHERE username = ? AND email = ? AND ip = ?";
my $sth = $self->{dbh}->prepare($sql);
my $rc = $sth->execute($score, $self->{_username}, $email, $ip);
if (!$rc) {
my $err = $self->{dbh}->errstr;
dbg("auto-whitelist: sql-based add_score: SQL error: $err");
}
else {
dbg("auto-whitelist: sql-based add_score: new count: ". $entry->{count} .", new totscore: ".$entry->{totscore}." for ".$entry->{addr});
}
$sth->finish();
}
else { # no entry yet, so insert a new entry
my $sql = "INSERT INTO $self->{tablename} (username,email,ip,count,totscore) VALUES (?,?,?,?,?)";
my $sth = $self->{dbh}->prepare($sql);
my $rc = $sth->execute($self->{_username},$email,$ip,1,$score);
if (!$rc) {
my $err = $self->{dbh}->errstr;
dbg("auto-whitelist: sql-based add_score: SQL error: $err");
}
$entry->{exists_p} = 1;
dbg("auto-whitelist: sql-based add_score: created new entry for ".$entry->{addr}." with totscore: $score");
$sth->finish();
}
return $entry;
}
=head2 remove_entry
public instance () remove_entry (\% $entry)
Description:
This method removes a given C<$entry> from the database. If the
ip portion of the entry address is equal to "none" then remove any
perl-IP entries for this address as well.
=cut
sub remove_entry {
my ($self, $entry) = @_;
my ($email, $ip) = $self->_unpack_addr($entry->{addr});
return unless ($email && $ip);
my $sql = "DELETE FROM $self->{tablename} WHERE username = ? AND email = ?";
my @args = ($self->{_username}, $email);
# when $ip is equal to none then attempt to delete all entries
# associated with address
if ($ip eq 'none') {
dbg("auto-whitelist: sql-based remove_entry: removing all entries matching $email");
}
else {
$sql .= " AND ip = ?";
push(@args, $ip);
dbg("auto-whitelist: sql-based remove_entry: removing single entry matching ".$entry->{addr});
}
my $sth = $self->{dbh}->prepare($sql);
my $rc = $sth->execute(@args);
if (!$rc) {
my $err = $self->{dbh}->errstr;
dbg("auto-whitelist: sql-based remove_entry: SQL error: $err");
}
else {
# We might normally have a dbg saying we removed the address
# but the common codepath already provides this in SpamAssassin.pm
}
$entry = undef; # slight cleanup since it is now gone
}
=head2 finish
public instance () finish ()
Description:
This method provides the necessary cleanup for the address list.
=cut
sub finish {
my ($self) = @_;
dbg("auto-whitelist: sql-based finish: disconnected from " . $self->{dsn});
$self->{dbh}->disconnect();
}
=head2 _unpack_addr
private instance (String, String) _unpack_addr(string $addr)
Description:
This method splits an autowhitelist address into it's two components,
email and ip address.
=cut
sub _unpack_addr {
my ($self, $addr) = @_;
my ($email, $ip) = split(/\|ip=/, $addr);
unless ($email && $ip) {
dbg("auto-whitelist: sql-based _unpack_addr: unable to decode $addr");
}
return ($email, $ip);
}
1;
syntax highlighted by Code2HTML, v. 0.9.1