lib/Exim/Grey.pm
changeset 58 27440e1334b7
parent 57 9db6f9fdba12
child 59 8088c95fac5d
equal deleted inserted replaced
57:9db6f9fdba12 58:27440e1334b7
       
     1 package Exim::Grey;
       
     2 
       
     3 use strict;
       
     4 use warnings;
       
     5 use base 'Exporter';
       
     6 use Carp;
       
     7 
       
     8 our @EXPORT_OK = qw(unseen getDBDir getDBs connectDB);
       
     9 our %EXPORT_TAGS = (
       
    10     all => \@EXPORT_OK,
       
    11 );
       
    12 our $VERSION = '2.0';
       
    13 
       
    14 # You may choose, but DB_File's footprint is smaller.
       
    15 # perl -MDB_File -e 'tie %h, ...':	real    0m0.063s
       
    16 # perl -MBerkeleyDB -e 'tie %h, ...':	real	0m0.112s
       
    17 # And DB_File is part of the Perl core distribution (?)
       
    18 # use BerkeleyDB;
       
    19 # use DB_File;
       
    20 # But we need locking! DB_File::Lock isn't part of the corelist.
       
    21 use DB_File::Lock;
       
    22 
       
    23 my %DEFAULT = (
       
    24     delay => 600,
       
    25     db    => "seen",
       
    26 );
       
    27 
       
    28 sub unseen;
       
    29 
       
    30 # some helper functions
       
    31 sub getDBDir();
       
    32 sub findExim(;$);
       
    33 sub connectDB($$);
       
    34 sub getDefault() { %DEFAULT }
       
    35 
       
    36 # Usage:
       
    37 # 	${perl{unseen}{KEY}}
       
    38 # 	${perl{unseen}{KEY}{600}}
       
    39 # 	${perl{unseen}{KEY}{600}{seen}}
       
    40 # 	${perl{unseen}{KEY}{600}{$spool_directory/grey/seen}}
       
    41 #
       
    42 # With KEY being something to identify the second delivery attempt
       
    43 # I recommend using <$sender_address>:<$local_part@$domain>
       
    44 #
       
    45 # If KEY has a /... suffix, this suffix is used for auto-whitelisting.
       
    46 # I recommend using $sender_host_address.
       
    47 #
       
    48 # defer  condition = ${perl{unseen}{<$sender_address>:<$local_part@$domain>/$sender_host_address}}
       
    49 #
       
    50 # record structure: key:   item\0
       
    51 #                   value: timestamp(creation) timestamp(usage)[ auto]\0
       
    52 # (This way we're compatible with ${lookup{...}dbm{...}})
       
    53 #
       
    54 # dbm file is relativ to $spool_directory/grey, EXCEPT its name
       
    55 # starts with "./" or "/".
       
    56 
       
    57 sub unseen {
       
    58     my $item = shift;
       
    59     my $delay = shift // $DEFAULT{delay};
       
    60     my $db = shift // $DEFAULT{db};
       
    61     my $now = time();
       
    62     my ($auto) = $item =~ /.*?\/(.+?)$/;
       
    63     my $rc;
       
    64 
       
    65     connectDB(\my %h, $db);
       
    66 
       
    67     return 'no'         # not unseen, ergo known
       
    68         if defined $auto and is_whitelisted($auto, \%h);
       
    69 
       
    70     my $key = "$item\0";
       
    71 
       
    72     # we do not know anything about the client -> unknown
       
    73     if (not exists $h{$key}) {
       
    74         $h{$key} = "$now $now 0\0";
       
    75         return 'yes';
       
    76     }
       
    77 
       
    78     my ($created, undef, $count) = split ' ', $h{$key};
       
    79 
       
    80     # we know the client, but last contact was recently (too fast)
       
    81     if ($now - $created < $delay) { 
       
    82         return 'yes';
       
    83     }
       
    84 
       
    85     # we know the client, was patiently enough
       
    86     ++$count;
       
    87     $h{$key} = "$created $now $count\0";
       
    88     whitelist($auto, \%h) if defined $auto;
       
    89     return 'no';
       
    90 }
       
    91 
       
    92 # According to a thought from "David Woodhouse <dwmw2@infradead.org>"
       
    93 # on exim-users@exim.org (Thu, 08 May 2008 13:10:21 +0100, 
       
    94 # Message-Id: <1210248621.25560.1088.camel@pmac.infradead.org>) we
       
    95 # should have the ability to "auto whitelist" hosts which are known
       
    96 # for retries, because there is no benefit in greylisting them.
       
    97 #
       
    98 # Most safe approach would be something based on message id.
       
    99 # If we see the message id a second time it indicates successful retry.
       
   100 # But we do not see the message id the first time we reject the message.
       
   101 
       
   102 # This function has to be called twice per message delivery attempt
       
   103 # <KEY> <$sender_host_address> <$sender_helo_name>
       
   104 # (Where <KEY> is something like <$sender_address>+<$local_part@$domain>
       
   105 # If we see the same message a second time (same message means here:
       
   106 # same greylist criteria
       
   107 
       
   108 sub whitelist {
       
   109     my ($item, $h) = @_;
       
   110     my $now = time;
       
   111     $h->{"$item\0"} = "$now $now 1 auto\0";
       
   112 }
       
   113 
       
   114 sub is_whitelisted {
       
   115     my ($item, $h) = @_;
       
   116     my $key = "$item\0";
       
   117 
       
   118     return 0 if not exists $h->{$key};
       
   119     
       
   120     my ($t0, undef, $cnt, $flag) = split ' ', $h->{$key};
       
   121     $h->{$key} = join(' ' => $t0, time, ++$cnt, $flag ? $flag : ()) . "\0";
       
   122 
       
   123     return 1;
       
   124 }
       
   125 
       
   126 # Get the directory where we could store the database file(s)
       
   127 # If we're running under exim it's easy, otherwise we've to find exim
       
   128 # and then ask...
       
   129 sub getDBDir() {
       
   130     my ($spooldir, $dbdir);
       
   131     eval { $spooldir = Exim::expand_string('$spool_directory') };
       
   132     if (not defined $spooldir) {
       
   133         my $exim = findExim();
       
   134         chomp($spooldir = `$exim -be '\$spool_directory'`);
       
   135         die "Can't find spooldir" if not defined $spooldir;
       
   136     }
       
   137     -d ($dbdir = "$spooldir/grey") and return $dbdir;
       
   138 
       
   139     my ($mode, $owner, $group) = (stat $spooldir)[ 2, 4, 5 ];
       
   140     {
       
   141         local $) = $group;
       
   142         local $> = $owner;
       
   143         $mode &= 0777;
       
   144         mkdir $dbdir, $mode or die "Can't create $dbdir: $!";
       
   145     }
       
   146     return $dbdir;
       
   147 }
       
   148 
       
   149 sub findExim(;$) {
       
   150     my $path = shift || $ENV{PATH};
       
   151     my $exim;
       
   152     foreach (split /:/, $ENV{PATH}) {
       
   153         -x ($exim = "$_/exim")  and return $exim;
       
   154         -x ($exim = "$_/exim4") and return $exim;
       
   155     }
       
   156     die "Can't find exim binary (missing .../sbin dirs in PATH?";
       
   157 }
       
   158 
       
   159 sub connectDB($$) {
       
   160     my ($h, $db) = @_;
       
   161     $db = getDBDir() . "/$db" unless $db =~ m(^\.?/);
       
   162 
       
   163     # Creation of DB-File if it doesn't exist
       
   164     # to avoid races we change our own uid/gid for creation of
       
   165     # this file.
       
   166     if (!-f $db) {
       
   167         (my $dir = $db) =~ s/^(.*)\/.*?$/$1/;
       
   168 
       
   169         # copy mode, uid, gid from the directory
       
   170         my ($mode, $user, $group) = (stat $dir)[ 2, 4, 5 ]
       
   171           or die "Can't stat $dir: $!";
       
   172         my $umask = umask(($mode & 0777) ^ 0777);
       
   173         local $) = $group;
       
   174         local $> = $user;
       
   175         open(X, ">>$db") or die "Can't create $db: $!";
       
   176         close(X);
       
   177         umask $umask;
       
   178     }
       
   179 
       
   180     # now test which of the DB-Modules has been loaded
       
   181 
       
   182     if (exists &BerkeleyDB::Hash::TIEHASH) {
       
   183         no strict;
       
   184         my $umask = umask 077;
       
   185         tie %$h, "BerkeleyDB::Hash", -Filename => $db
       
   186           or die "$0: $db: $!";
       
   187         return $db;
       
   188     }
       
   189 
       
   190     if (exists &DB_File::Lock::TIEHASH) {
       
   191         tie %$h, 'DB_File::Lock', [ $db ], 'write'
       
   192           or die "$0: $db: $!";
       
   193         return $db;
       
   194     }
       
   195 
       
   196     if (exists &DB_File::TIEHASH) {
       
   197         tie %$h, 'DB_File', $db or die "$0: $db: $!";
       
   198         warn "$0: using DB_File, no locking is possible!\n";
       
   199         return $db;
       
   200     }
       
   201 
       
   202     die "Can't connect to database driver";
       
   203 }
       
   204 
       
   205 1;
       
   206 
       
   207 # vim:aw et sw=4 ts=4: