#!/usr/bin/perl -w use strict; # pop-before-smtp 1.0 Bennett Todd Freely Redistributable # 1.0 2000-01-04 first public release # NB: As of this writing, I don't have a website available to publish updated # versions: if you grab this much after 2000-01-07 please drop an email to # asking if there's been an update, and let me know which # version you have. Thanks! =head1 NAME pop-before-smtp --- watch log for POP/IMAP auth, update map allowing SMTP =head1 SYNOPSIS nohup pop-before-smtp [--[no]write] [--[no]debug] \ [--logfile=filename] \ [--dbfile=filename] \ [--grace=seconds] & =head1 DESCRIPTION pop-before-smtp watches /var/log/maillog for lines written by UW popd/imapd describing successful login attempts, and installs entries for them in an on-disk hash (DB) that is watched by postfix. It expires the entries after a half-hour. The hash is named /etc/postfix/pop-before-smtp.db. The name, as specified in the dbfile option, does not include the .db on the end, that's tacked on to satisfy a wired-in assumption in postfix. Internally, it keeps two data structures for all currently-allowed hosts; a queue, and a hash. The queue contains [ipaddr, time] records, while the hash contains ipaddr => time. Every time the daemon wakes up to deal with something else from the File::Tail handle, it peeks a the front of the queue, and while the timestamp of the record there has expired (is > 1800 seconds old) it tosses it, and if the timestamp in the hash is also expired and equals the timestamp in the queue it deletes the hash entry and the on-disk db file entry. Edit the source to change the wired-in filenames, grace period, logfile format, etc. When starting up, it builds an internal table of all netblocks natively permitted by Postfix (it looks at the output of "postconf mynetworks"); before adding each entry it checks to see if it would be permitted by that rule. =head1 INSTALLATION This daemon likes a couple of helpers. Here's a nice init script: #!/bin/sh progname=`basename $0` pgm=/usr/sbin/$progname log=/var/log/$progname pid=/var/run/$progname.pid die(){ echo "$progname: $*">&2; exit 1; } case "$1" in start) $pgm >$log 2>&1 & echo $! >$pid;; stop) p=`cat $pid`; test -n "$p" || exit 0 kill $p || exit 0; sleep 1 kill -9 $p 2>/dev/null || exit 0; sleep 1 kill -0 $p && die "$pid won't die" ;; esac The integration in /etc/postfix/main.cf might look like this: smtpd_recipient_restrictions = permit_mynetworks,reject_non_fqdn_recipient, check_client_access hash:/etc/postfix/pop-before-smtp, check_relay_domains =cut use File::Tail; use DB_File; use Net::Netmask; use Date::Parse; use Getopt::Long; ################################## # # # Tuneable parameters start here # # # ################################## # Flags my $write = 1; my $debug = 0; # File to watch for pop3d/imapd records my $logfile = '/var/log/maillog'; # This regex pull the lines I'm interested in out of $logfile, and yanks out # the timestamp and IP address my $pat = '^(... .. ..:..:..) \S+ (?:ipop3d|imapd)\[\d+\]: ' . 'Login user=\S+ host=\S+ \[(\d+\.\d+\.\d+\.\d+)\]'; my $dbfile = '/etc/postfix/pop-before-smtp'; # DB hash to write my $grace = 1800; # 30 minutes --- grace period GetOptions( "write!" => \$write, "debug!" => \$debug, "dbfile=s" => \$dbfile, "grace=i" => \$grace, ) or die "syntax: $0 [--[no]write] [--[no]debug] [--logfile=filename] " . "[--dbfile=filename] [--grace=seconds]\n"; # These parameters control how closely the watcher tries to follow the # logfile, which affects how much resources it consumes, and how quickly # people can smtp after they have popped. my $fi = File::Tail->new( name => $logfile, maxinterval => 10, interval => 5, adjustafter => 3, tail => -1, ); ################################ # # # Tuneable parameters end here # # # ################################ my $mynets = `postconf mynetworks`; for ($mynets) { s/^\s+//; s/\s+$//; s/\s+/ /g; s/^mynetworks\s*=\s*//; } Net::Netmask->new($_)->storeNetblock() for split /[,\s]+/, $mynets; my (%t, @q); use vars qw(%db); my $dbh = tie %db, 'DB_File', "$dbfile.db", O_CREAT|O_RDWR, 0666, $DB_HASH or die "$0: cannot dbopen $dbfile: $!\n" if $write; delete $db{$_} for keys %db; $| = 1 if $debug; while (1) { $_ = $fi->read; my ($timestamp, $ipaddr) = m/$pat/o or next; my $ts = str2time($timestamp) or next; $ts += $grace; next if $ts < time; print "read ts=$timestamp ip=$ipaddr\n" if $debug; next if findNetblock($ipaddr); print "\taccepted --- not in mynetworks\n" if $debug; push @q, [$ipaddr, $ts]; $t{$ipaddr} = $ts; $db{$ipaddr} = "ok" if $write; $dbh->sync and die "$0: sync $dbfile: $!\n" if $write; print "\twritten ok\n" if $write and $debug; while ($q[0][1] < time) { print "purging ts=".localtime($q[0][1])." ip=$q[0][0]\n" if $debug; if ($q[0][1] == $t{$q[0][0]}) { delete $t{$q[0][0]}; delete $db{$q[0][0]} if $write; $dbh->sync and die "$0: sync $dbfile: $!\n" if $write; } shift @q; } }