Campaign banner

POP before SMTP

How to restrict relaying through your mailserver to only local users that have authenticated using Post Office Protocol.

From an idea by John Levine <johnl@abuse.net>, described by Scott Hazen Mueller and implemented by Neil Harkins <nharkins@well.com> and John Levine
Updated January 2000

The overall idea is that normally you only permit relay from within your own network. But some users travel and connect from other places, and you want to let those users relay mail through your server. Whenever someone logs in for POP3 mail, this trick notes the IP address from which the connection was made, and permits relay from the IP for a limited time.

Travelling users need only check their mail to ``unlock'' the mail server, no changes need be made to the client mail software.

For sendmail users, this implementation is in two parts. The first part modifies the POP3 daemon to log a message identifying the IP address used for each successful login. The second part reads the log entries and builds a dbm file listing the permissible relay addresses.

Earlier implementations expired the IP addresses after 15 minutes or so. This one expires them after one day, which still seems to be plenty short to deter spammers, but puts a much lower load on the system.

Modify the POP3 daemon

Here are modifications to qpopper version 2.4, the most popular Post Office Protocol daemon. It's available in source from at no charge from www.eudora.com. (The current version is now 2.5, but this piece of code hasn't changed.)

These changes log a message like this each time a user logs in:

POP login for "username" at (remote.host.name) 000.000.000.000

The patches go at the end of the source file pop_pass.c.

*** pop_pass.c.orig	Wed Dec 17 23:05:42 1997
--- pop_pass.c	Thu Nov 20 01:14:59 1997
***************
*** 630,635 ****
--- 630,640 ----
      p->last_msg = 0;
  
      /*  Authorization completed successfully */
+ /* begin pop-before-smtp patch */
+     pop_log(p,POP_PRIORITY,
+ 	    "(v%s) POP login for \"%s\" at (%s) %s", 
+            VERSION,p->user,p->client,p->ipaddr);
+ /* end pop-before-smtp patch */
      return (pop_msg (p,POP_SUCCESS,
          "%s has %d message%s (%d octets).",
              p->user,p->msg_count, p->msg_count == 1 ? "" : "s", p->drop_size));
You may want to also compile the daemon to use a separate log facility, such as LOCAL1. Of course it would also be simple to modify the daemon to update the list of POP authenticated addresses itself, but we wanted to have compatibilty across different machines and hardware platforms.

In some mailserver configurations, there are multiple mailservers, some which only relay SMTP mail, and one which only serves as a POP server. In order to propagate the list of POP authenticated addresses to the relaying mailservers, we decided on using syslog to log all the POP daemon messages to the relaying hosts, where a log watcher can update its own version of the list. This could be solved by a small read-only NFS mount from the POP server, but we tend to use NFS only when absolutely necessary.

Modify the sendmail.cf configuration file

Update your sendmail.cf file to look at the list of addresses provided by the POP3 server. (Actually, by a daemon that reads the POP3 server log info, which we describe in the next section.

The exact changes depend on your version of sendmail. For 8.9 and later, use the POPAUTH hack provided at http://www.sendmail.org/~ca/email/chk-89n.html.

For 8.8, it would be a good idea to update to 8.9 since there are many security holes fixed. If that's not possible, there are rulesets at http://www.sendmail.org/~ca/email/check.html. Look for "Relay control for roaming users".

For versions older than 8.8, you must update to something newer. Older versions are too buggy to support this feature.

Create the file /etc/mail/backupmx and place in it any fully-qualified domains for which you provide backup MX service. (NOTE: the above ruleset is very simple, and will not match and OK subdomains. They must be expicitly listed in /etc/mail/backupmx. However, if this is needed, it is easy to change with the addition of a $* before the $={BackupMX}.)

Create /etc/mail/localip and place in it all of your local network address ranges, each on a line by themselves, omiting any host bits. (i.e. "206.169" to denote 206.169.0.0/16 or "206.80.6" to denote 206.80.6.0/24.)

Maintaining the database of authorized relay IP addresses

First, create the directory /var/spool/popauth.

Create a popper syslog watcher script similar to the following ``popwatch'' script. This script watches for valid POP-authenticated IP addresses, creates a temporary file for each address as the filename, and updates the popauth database each time a new address is seen.

Here are two versions of the script.

Original script

#!/usr/local/bin/perl5
# Change the following for your system:
 
$popauthspool = "/var/spool/popauth";
$poppersyslog = "/var/log/popper";
$watcherlog = "/var/log/popwatch";
$popwatcherpidfile = "/etc/popwatch.pid";
$TAIL = "/usr/local/bin/tail";
$date = `/usr/local/bin/date`; chop($date);

# make database of IPs seen so far

@ips = `ls $popauthspool`;
#print @ips;
foreach $ip (@ips) {chop($ip);
   $ipok{$ip} = "OK";
}
 
# now watch log file and add new IPs as encountered
# performance buglet: this will also add IPs in the local range as well
# as travellers, but it's probably not worth the effort to filter them
# out since each IP will be added a maximum of once per day.

open(LOG,">>$watcherlog") || die("Can't open $watcherlog");
print LOG "\n$date Starting log for popauth.watcher at pid $$\n";
 
select(LOG);
$| = 1;
 
select(STDOUT);
$| = 1;
 
$SIG{'INT'} = 'handler';
$SIG{'QUIT'} = 'handler';
$SIG{'KILL'} = 'handler';
 
open(PID,">$popwatcherpidfile");
print PID "$$\n";
close(PID);
 
open(POPPER,"$TAIL -f $poppersyslog |") || die("Can't $TAIL -f $poppersyslog");
while(<POPPER>) {
   if(/^([A-Za-z]+\s+\d+\s+\d+\:\d+\:\d+).+POP login for \"(.+)\".+\s(\d+\.\d+\.\d+.\d+).*$/) {
       $time   = $1;
       $user   = $2;
       $ip     = $3;
       if ($ipok{$ip} eq "OK") {
#          print LOG "$time $user $ip $ipok{$ip} already exists\n";
       } else {
          print LOG "$time $user $ip $ipok{$ip}\n";
          $ipok{$ip} = "OK";
          open(TEMP,"> $popauthspool/$ip");
          close(TEMP);
 
         open (OUT,">/etc/mail/pophash.tmp");
         foreach $key (keys %ipok) {
            print OUT "$key OK\n";
         }
         close (OUT);
 
         $rc = system ("cd /etc/mail; /etc/makemap hash pophash.junk < pophash.tmp");
         $rc = system ("mv /etc/mail/pophash.junk.db /etc/mail/pophash.db");
 
       }
   }
}
close(POPPER);
close(LOG);
exit(1);

sub handler {
  local($sig) = @_;
  close(POPPER);
  close(LOG);
  exit(0);
}

Spiffier script

If you already have a check_rcpt ruleset, this simplified perl daemon simply concatenates to the .cR file. The daemon doesn't write local IPs or repeated IPs.

#!/usr/bin/perl5
# Change the following for your system:

$popauthspool = "/etc/sendmail.cR";
$poppersyslog = "/var/log/messages";
$watcherlog = "/var/log/popwatch";
$popwatcherpidfile = "/var/run/popwatch.pid";
$TAIL = "/usr/bin/tail";
$date = `/bin/date`; chop($date);

# make database of IPs seen so far

open(PREV,"$popauthspool")|| die "Can't open $popauthspool";
while() {
#       print $_;
        $prevIp{chop($_)} = "GOOOOOD";
}
close(PREV);

# now watch log file and add new IPs as encountered

open(LOG,">>$watcherlog") || die("Can't open $watcherlog");
print LOG "\n$date Starting log for popauth.watcher at pid $$\n";

select(LOG);
$| = 1;

select(STDOUT);
$| = 1;

$SIG{'INT'} = 'handler';
$SIG{'QUIT'} = 'handler';
$SIG{'KILL'} = 'handler';

open(PID,">$popwatcherpidfile");
print PID "$$\n";
close(PID);

open(POPPER,"$TAIL -f $poppersyslog |") || die("Can't $TAIL -f
$poppersyslog");
while() {
   if(/^([A-Za-z]+\s+\d+\s+\d+\:\d+\:\d+).+POP login for
\"(.+)\".+\s(\d+\.\d+\.
\d+.\d+).*$/) {
       $time   = $1;
       $user   = $2;
       $ip     = $3;
        # in the following line, change the pattern to match your
        # local known good IP range
       if ($ip =~ /999\.000\.[0-31]/){
           #print LOG "$time native $user \t$ip\n";
       } elsif ($prev{$ip} eq "GOOOOOD") {
           #print LOG "$time $user \t$ip is already logged\n";
       } else {
           #print LOG "$time $user $ip LOGGED TO .cR\n";
           $prev{$ip} = "GOOOOOD";
           open(SPOOL,">>$popauthspool");
           print SPOOL $ip . "\n";
           close (SPOOL);
       }
   }
}
close(POPPER);
close(LOG);
exit(1);

sub handler {
  local($sig) = @_;
  close(POPPER);
  close(LOG);
  exit(0);
}
Run this watcher in the background, killing and restarting it from cron whenever the syslog (and its own log) files are rotated. At the time you kill and restart it, run through the directory of IP addresses and delete any that are stale. Here's a snippet of shell script:

# after logs are rotated
kill `cat /etc/popwatch.pid`

# delete stale relay IPs
( cd /var/spool/popauth; find . -mtime +0 -print | xargs rm -f )

# restart popwatch

popwatch &

And that should be it!


Neil Harkins <nharkins@well.com>

John Levine <johnl@abuse.net>


(Also see an alternative implmentation of a POP3 Authenticated Relaying mechanism.)