I recently transitioned mail.netisland.net from 0.28-tcpserver to
0.32-forkserver, and in doing so ended up making a number of small
changes. The attached diff does the following:
o lib/Qpsmtpd/SMTP.pm
- The code in fault() which displays error messages looks like
it wanted to prefix the message with the name of the process
and its PID, however the "$0[$$]" expression was being
parsed as an array lookup on @0. I also modified the
expression to only display the first word of $0, as in many
cases $0 has been modified to include information
identifying the current SMTP conversation.
- Treat DENY_DISCONNECT from connect hooks the same as DENY.
Previously, a return code of DENY_DISCONNECT was being
ignored, resulting in the default DECLINED action being
taken.
- Rather than always blindly appending " ESMTP" to the
smtpgreeting, only append when the smtpgreeting does not
already contain the word ESMTP.
- Make the space after "mail from:" once again be optional,
initialize $from_parameter to "" in order to avoid "Use of
uninitialized value" warnings.
- In RCPT TO, perform more careful error checking on the
object returned by Qpsmtpd::Address->parse. Previously,
"RCPT TO:<mct>" resulted in SMTP.pm not realizing that the
address was not properly parsed, and passing the plugins a
Qpsmtpd::Address object where both the user and host were
undef.
- The HELP command previously never returning a response
to the client due to missing parenthesis in the ternary
operation used to construct the response string.
o qpsmtpd-forkserver
- Add a new command line option, "-H" (same as tcpserver),
to avoid reverse DNS lookups on the client's IP address.
- If not running with "-H", modify $0 to display the name of
the IP address currently being examined, and restore the
original value when completed.
- Rather than having $USER default to "smtpd", have it instead
default to the name of the user who invoked it, and falling
back to "smtpd" only when that user was root. Previously,
if I wanted to run qpsmtpd-forkserver as myself on port
2525, I had to run "./qpsmtpd-forkserver -u mct" to avoid
permission denied errors.
- Add a call to endgrent() once we're done with getgrent()
to close the open file descriptor reading from /etc/group,
which otherwise would then be inherited by every child.
o STATUS
- Remove the wishlist item for supporting config/smtpgreeting;
the feature has been implemented.
Matt pointed out to me in the #qpsmtpd IRC channel that this is a
patch against the 0.32 distribution rather than the 0.3x SVN branch,
but as all of these changes are about three lines or less, hopefully
they won't present much of a problem when applying. If that turns out
not to be true, please let me know, and I'll be happy to put in the
grunt work to produce another diff.
Thanks,
-mct
diff -br -U20 qpsmtpd-0.32.old/lib/Qpsmtpd/SMTP.pm
qpsmtpd-0.32/lib/Qpsmtpd/SMTP.pm
--- qpsmtpd-0.32.old/lib/Qpsmtpd/SMTP.pm 2006-02-26 07:22:16.000000000
-0500
+++ qpsmtpd-0.32/lib/Qpsmtpd/SMTP.pm 2006-12-23 23:52:27.000000000 -0500
@@ -65,65 +65,68 @@
else {
$self->respond(500, "Unrecognized command");
}
return 1
}
$cmd = $1;
if (1 or $self->{_commands}->{$cmd} and $self->can($cmd)) {
my ($result) = eval { $self->$cmd(@_) };
$self->log(LOGERROR, "XX: $@") if $@;
return $result if defined $result;
return $self->fault("command '$cmd' failed unexpectedly");
}
return;
}
sub fault {
my $self = shift;
my ($msg) = shift || "program fault - command not performed";
- print STDERR "$0[$$]: $msg ($!)\n";
+ my ($name) = split /\s+/, $0, 2;
+
+ print STDERR "${name}[$$]: $msg ($!)\n";
return $self->respond(451, "Internal error - try again later - " . $msg);
}
sub start_conversation {
my $self = shift;
# this should maybe be called something else than "connect", see
# lib/Qpsmtpd/TcpServer.pm for more confusion.
my ($rc, $msg) = $self->run_hooks("connect");
- if ($rc == DENY) {
+ if ($rc == DENY || $rc == DENY_DISCONNECT) {
$self->respond(550, ($msg || 'Connection from you denied, bye bye.'));
return $rc;
}
elsif ($rc == DENYSOFT) {
$self->respond(450, ($msg || 'Connection from you temporarily denied,
bye bye.'));
return $rc;
}
elsif ($rc == DONE) {
return $rc;
}
elsif ($rc != DONE) {
my $greets = $self->config('smtpgreeting');
if ( $greets ) {
- $greets .= " ESMTP";
+ $greets .= " ESMTP"
+ unless $greets =~ /(^|\W)ESMTP(\W|$)/;
}
else {
$greets = $self->config('me')
. " ESMTP qpsmtpd "
. $self->version
. " ready; send us your mail, but not your spam.";
}
$self->respond(220, $greets);
return DONE;
}
}
sub transaction {
my $self = shift;
return $self->{_transaction} || $self->reset_transaction();
}
sub reset_transaction {
my $self = shift;
@@ -250,46 +253,47 @@
# may be aborted by the RSET (or a new EHLO) command. There may be
# zero or more transactions in a session. MAIL (or SEND, SOML, or
# SAML) MUST NOT be sent if a mail transaction is already open,
# i.e., it should be sent only if no mail transaction had been
# started in the session, or it the previous one successfully
# concluded with a successful DATA command, or if the previous one
# was aborted with a RSET.
# sendmail (8.11) rejects a second MAIL command.
# qmail-smtpd (1.03) accepts it and just starts a new transaction.
# Since we are a qmail-smtpd thing we will do the same.
$self->reset_transaction;
unless ($self->connection->hello) {
return $self->respond(503, "please say hello first ...");
}
else {
my $from_parameter = join " ", @_;
+ $from_parameter ||= "";
$self->log(LOGINFO, "full from_parameter: $from_parameter");
my ($from) = ($from_parameter =~ m/^from:\s*(<[^>]*>)/i)[0];
# support addresses without <> ... maybe we shouldn't?
- ($from) = "<" . ($from_parameter =~ m/^from:\s*(\S+)/i)[0] . ">"
+ ($from) = "<" . ($from_parameter =~ m/^from:\s*(\S*)/i)[0] . ">"
unless $from;
$self->log(LOGALERT, "from email address : [$from]");
if ($from eq "<>" or $from =~ m/\[undefined\]/ or $from eq "<[EMAIL
PROTECTED]>") {
$from = Qpsmtpd::Address->new("<>");
}
else {
$from = (Qpsmtpd::Address->parse($from))[0];
}
return $self->respond(501, "could not parse your mail from command")
unless $from;
my ($rc, $msg) = $self->run_hooks("mail", $from);
if ($rc == DONE) {
return 1;
}
elsif ($rc == DENY) {
$msg ||= $from->format . ', denied';
$self->log(LOGINFO, "deny mail from " . $from->format . " ($msg)");
$self->respond(550, $msg);
@@ -312,41 +316,42 @@
$self->disconnect;
}
else { # includes OK
$self->log(LOGINFO, "getting mail from ".$from->format);
$self->respond(250, $from->format . ", sender OK - how exciting to get
mail from you!");
$self->transaction->sender($from);
}
}
}
sub rcpt {
my $self = shift;
return $self->respond(501, "syntax error in parameters") unless $_[0] and
$_[0] =~ m/^to:/i;
return $self->respond(503, "Use MAIL before RCPT") unless
$self->transaction->sender;
my ($rcpt) = ($_[0] =~ m/to:(.*)/i)[0];
$rcpt = $_[1] unless $rcpt;
$self->log(LOGALERT, "to email address : [$rcpt]");
$rcpt = (Qpsmtpd::Address->parse($rcpt))[0];
- return $self->respond(501, "could not parse recipient") unless $rcpt;
+ return $self->respond(501, "could not parse recipient")
+ unless $rcpt && (defined($rcpt->user) || defined($rcpt->host));
my ($rc, $msg) = $self->run_hooks("rcpt", $rcpt);
if ($rc == DONE) {
return 1;
}
elsif ($rc == DENY) {
$msg ||= 'relaying denied';
$self->respond(550, $msg);
}
elsif ($rc == DENYSOFT) {
$msg ||= 'relaying denied';
return $self->respond(450, $msg);
}
elsif ($rc == DENY_DISCONNECT) {
$msg ||= 'delivery denied';
$self->log(LOGINFO, "delivery denied ($msg)");
$self->respond(550, $msg);
$self->disconnect;
}
elsif ($rc == DENYSOFT_DISCONNECT) {
@@ -354,41 +359,41 @@
$self->log(LOGINFO, "delivery denied ($msg)");
$self->respond(421, $msg);
$self->disconnect;
}
elsif ($rc == OK) {
$self->respond(250, $rcpt->format . ", recipient ok");
return $self->transaction->add_recipient($rcpt);
}
else {
return $self->respond(450, "No plugin decided if relaying is allowed");
}
return 0;
}
sub help {
my $self = shift;
$self->respond(214,
"This is qpsmtpd " .
- $self->config('smtpgreeting') ? '' : $self->version,
+ ($self->config('smtpgreeting') ? '' : $self->version),
"See http://smtpd.develooper.com/",
'To report bugs or send comments, mail to <[EMAIL PROTECTED]>.');
}
sub noop {
my $self = shift;
$self->respond(250, "OK");
}
sub vrfy {
my $self = shift;
# Note, this doesn't support the multiple ambiguous results
# documented in RFC2821#3.5.1
# I also don't think it provides all the proper result codes.
my ($rc, $msg) = $self->run_hooks("vrfy");
if ($rc == DONE) {
return 1;
}
diff -br -U20 qpsmtpd-0.32.old/qpsmtpd-forkserver
qpsmtpd-0.32/qpsmtpd-forkserver
--- qpsmtpd-0.32.old/qpsmtpd-forkserver 2006-02-26 07:22:16.000000000 -0500
+++ qpsmtpd-0.32/qpsmtpd-forkserver 2006-12-24 11:24:05.000000000 -0500
@@ -1,72 +1,77 @@
#!/usr/bin/perl -Tw
# Copyright (c) 2001 Ask Bjoern Hansen. See the LICENSE file for details.
# The "command dispatch" system is taken from colobus -
http://trainedmonkey.com/colobus/
#
# For more information see http://develooper.com/code/qpsmtpd/
#
#
use lib 'lib';
use Qpsmtpd::TcpServer;
use Qpsmtpd::Constants;
use IO::Socket;
use IO::Select;
use Socket;
-use Getopt::Long;
+use Getopt::Long qw(:config no_ignore_case);
use POSIX qw(:sys_wait_h :errno_h :signal_h);
use strict;
$| = 1;
# Configuration
my $MAXCONN = 15; # max simultaneous connections
my $PORT = 2525; # port number
my @LOCALADDR; # ip address(es) to bind to
-my $USER = 'smtpd'; # user to suid to
my $MAXCONNIP = 5; # max simultaneous connections from one IP
my $PID_FILE = '';
my $DETACH; # daemonize on startup
+my $NORDNS;
+
+my $USER = (getpwuid $>)[0]; # user to suid to
+$USER = "smtpd" if $USER eq "root";
sub usage {
print <<"EOT";
usage: qpsmtpd-forkserver [ options ]
-l, --listen-address addr : listen on specific address(es); can be specified
multiple times for multiple bindings. Default is
0.0.0.0 (all interfaces).
-p, --port P : listen on a specific port; default 2525
-c, --limit-connections N : limit concurrent connections to N; default 15
-u, --user U : run as a particular user (default 'smtpd')
-m, --max-from-ip M : limit connections from a single IP; default 5
--pid-file P : print main servers PID to file P
-d, --detach : detach from controlling terminal (daemonize)
+ -H, --no-rdns : don't perform reverse DNS lookups
EOT
exit 0;
}
GetOptions('h|help' => \&usage,
'l|listen-address=s' => [EMAIL PROTECTED],
'c|limit-connections=i' => \$MAXCONN,
'm|max-from-ip=i' => \$MAXCONNIP,
'p|port=i' => \$PORT,
'u|user=s' => \$USER,
'pid-file=s' => \$PID_FILE,
'd|detach' => \$DETACH,
+ 'H|no-rdns' => \$NORDNS,
) || &usage;
# detaint the commandline
if ($PORT =~ /^(\d+)$/) { $PORT = $1 } else { &usage }
@LOCALADDR = ( '0.0.0.0' ) if [EMAIL PROTECTED];
for (0..$#LOCALADDR) {
if ($LOCALADDR[$_] =~ /^([\d\w\-.]+)$/) {
$LOCALADDR[$_] = $1;
} else {
&usage;
}
}
if ($USER =~ /^([\w\-]+)$/) { $USER = $1 } else { &usage }
if ($MAXCONN =~ /^(\d+)$/) { $MAXCONN = $1 } else { &usage }
delete $ENV{ENV};
$ENV{PATH} = '/bin:/usr/bin:/var/qmail/bin';
my %childstatus = ();
@@ -123,40 +128,41 @@
or die "Could not truncate $PID_FILE at 0: $!";
} else {
open PID, ">$PID_FILE"
or die "open pid_file: $!\n";
}
}
# Load plugins here
my $qpsmtpd = Qpsmtpd::TcpServer->new();
# Drop privileges
my (undef, undef, $quid, $qgid) = getpwnam $USER or
die "unable to determine uid/gid for $USER\n";
my $groups = "$qgid $qgid";
while (my ($name,$passwd,$gid,$members) = getgrent()) {
my @m = split(/ /, $members);
if (grep {$_ eq $USER} @m) {
$groups .= " $gid";
}
}
+endgrent;
$) = $groups;
POSIX::setgid($qgid) or
die "unable to change gid: $!\n";
POSIX::setuid($quid) or
die "unable to change uid: $!\n";
$> = $quid;
$qpsmtpd->load_plugins;
::log(LOGINFO,"Listening on port $PORT");
::log(LOGINFO, 'Running as user '.
(getpwuid($>) || $>) .
', group '.
(getgrgid($)) || $)));
if ($DETACH) {
open STDIN, '/dev/null' or die "/dev/null: $!";
open STDOUT, '>/dev/null' or die "/dev/null: $!";
open STDERR, '>&STDOUT' or die "open(stderr): $!";
defined (my $pid = fork) or die "fork: $!";
@@ -228,41 +234,49 @@
$running++;
close($client);
next;
}
# otherwise child
# all children should have different seeds, to prevent conflicts
srand( time ^ ($$ + ($$ << 15)) );
close($server);
$SIG{$_} = 'DEFAULT' for keys %SIG;
$SIG{ALRM} = sub {
print $client "421 Connection Timed Out\n";
::log(LOGINFO, "Connection Timed Out");
exit; };
$ENV{TCPLOCALIP} = inet_ntoa($laddr);
# my ($port, $iaddr) = sockaddr_in($hisaddr);
$ENV{TCPREMOTEIP} = inet_ntoa($iaddr);
+
+ if ($NORDNS) {
+ $ENV{TCPREMOTEHOST} = $ENV{TCPREMOTEIP} ? "[$ENV{TCPREMOTEIP}]" :
"[noip!]";
+ } else {
+ my $zero = $0;
+ $0 = "$zero (gethostbyname $ENV{TCPREMOTEIP})";
$ENV{TCPREMOTEHOST} = gethostbyaddr($iaddr, AF_INET) || "Unknown";
+ $0 = $zero;
+ }
# don't do this!
#$0 = "qpsmtpd-forkserver: $ENV{TCPREMOTEIP} / $ENV{TCPREMOTEHOST}";
::log(LOGINFO, "Accepted connection $running/$MAXCONN from
$ENV{TCPREMOTEIP} / $ENV{TCPREMOTEHOST}");
# dup to STDIN/STDOUT
POSIX::dup2(fileno($client), 0);
POSIX::dup2(fileno($client), 1);
$qpsmtpd->start_connection
(
local_ip => $ENV{TCPLOCALIP},
local_port => $lport,
remote_ip => $ENV{TCPREMOTEIP},
remote_port => $port,
);
$qpsmtpd->run();
$qpsmtpd->run_hooks("post-connection");
diff -br -U20 qpsmtpd-0.32.old/STATUS qpsmtpd-0.32/STATUS
--- qpsmtpd-0.32.old/STATUS 2006-02-26 07:22:16.000000000 -0500
+++ qpsmtpd-0.32/STATUS 2006-12-23 23:35:57.000000000 -0500
@@ -66,44 +66,40 @@
join them to one for SMTP?)
support plugins for the rest of the commands.
specify a priority in register_hook. ("LAST", "FIRST", "MIDDLE", or
maybe a number)
plugin access to the data line by line during the DATA phase
(instead of just after)
if qmail-queue can't be loaded we still return 250 ?!
Make a system for configuring the plugins per user/domain/...
support databytes per user / domain
plugin to reject mails from <> if it has multiple recipients.
localiphost - support [EMAIL PROTECTED] addresses
-support smtpgreeting (?)
-
-
-
TRACE in Constants.pm is not actually being used. Should it be?
Move dispatch() etc from SMTP.pm to Qpsmtpd.pm to allow other similar
protocols to use the qpsmtpd framework.
Future Ideas
============
Methods to create a bounce message easily; partly so we can accept a
mail for one user but bounce it right away for another RCPT'er.
The data_post hook should be able to put in the notes what addresses
should go through, bounce and get rejected respectively, and qpsmtpd
should just do the right thing. See also
http://nntp.perl.org/group/perl.qpsmtpd/170
David Carraway has some thoughts for "user filters"
http://nntp.perl.org/group/perl.qpsmtpd/2