2006-12-08 20:46:18 +01:00
|
|
|
# $Id: Server.pm,v 1.10 2005/02/14 22:04:48 msergeant Exp $
|
|
|
|
|
|
|
|
package Qpsmtpd::PollServer;
|
|
|
|
|
|
|
|
use base ('Danga::Client', 'Qpsmtpd::SMTP');
|
|
|
|
# use fields required to be a subclass of Danga::Client. Have to include
|
|
|
|
# all fields used by Qpsmtpd.pm here too.
|
|
|
|
use fields qw(
|
|
|
|
input_sock
|
|
|
|
mode
|
|
|
|
header_lines
|
|
|
|
in_header
|
|
|
|
data_size
|
|
|
|
max_size
|
|
|
|
hooks
|
|
|
|
start_time
|
|
|
|
cmd_timeout
|
2007-02-07 23:25:44 +01:00
|
|
|
conn
|
2007-02-07 22:49:45 +01:00
|
|
|
_auth
|
2006-12-08 20:46:18 +01:00
|
|
|
_auth_mechanism
|
|
|
|
_auth_state
|
|
|
|
_auth_ticket
|
|
|
|
_auth_user
|
|
|
|
_commands
|
|
|
|
_config_cache
|
|
|
|
_connection
|
|
|
|
_transaction
|
|
|
|
_test_mode
|
|
|
|
_extras
|
|
|
|
_continuation
|
|
|
|
);
|
|
|
|
use Qpsmtpd::Constants;
|
|
|
|
use Qpsmtpd::Address;
|
|
|
|
use ParaDNS;
|
|
|
|
use Mail::Header;
|
|
|
|
use POSIX qw(strftime);
|
|
|
|
use Socket qw(inet_aton AF_INET CRLF);
|
|
|
|
use Time::HiRes qw(time);
|
|
|
|
use strict;
|
|
|
|
|
|
|
|
sub max_idle_time { 60 }
|
|
|
|
sub max_connect_time { 1200 }
|
|
|
|
|
|
|
|
sub input_sock {
|
|
|
|
my $self = shift;
|
|
|
|
@_ and $self->{input_sock} = shift;
|
|
|
|
$self->{input_sock} || $self;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub new {
|
|
|
|
my Qpsmtpd::PollServer $self = shift;
|
|
|
|
|
|
|
|
$self = fields::new($self) unless ref $self;
|
|
|
|
$self->SUPER::new( @_ );
|
|
|
|
$self->{cmd_timeout} = 5;
|
|
|
|
$self->{start_time} = time;
|
|
|
|
$self->{mode} = 'connect';
|
|
|
|
$self->load_plugins;
|
|
|
|
$self->load_logging;
|
|
|
|
return $self;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub uptime {
|
|
|
|
my Qpsmtpd::PollServer $self = shift;
|
|
|
|
|
|
|
|
return (time() - $self->{start_time});
|
|
|
|
}
|
|
|
|
|
|
|
|
sub reset_for_next_message {
|
|
|
|
my Qpsmtpd::PollServer $self = shift;
|
|
|
|
$self->SUPER::reset_for_next_message(@_);
|
|
|
|
|
|
|
|
$self->{_commands} = {
|
|
|
|
ehlo => 1,
|
|
|
|
helo => 1,
|
|
|
|
rset => 1,
|
|
|
|
mail => 1,
|
|
|
|
rcpt => 1,
|
|
|
|
data => 1,
|
|
|
|
help => 1,
|
|
|
|
vrfy => 1,
|
|
|
|
noop => 1,
|
|
|
|
quit => 1,
|
|
|
|
auth => 0, # disabled by default
|
|
|
|
};
|
|
|
|
$self->{mode} = 'cmd';
|
|
|
|
$self->{_extras} = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
sub respond {
|
|
|
|
my Qpsmtpd::PollServer $self = shift;
|
|
|
|
my ($code, @messages) = @_;
|
|
|
|
while (my $msg = shift @messages) {
|
|
|
|
my $line = $code . (@messages ? "-" : " ") . $msg;
|
|
|
|
$self->write("$line\r\n");
|
|
|
|
}
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub fault {
|
|
|
|
my Qpsmtpd::PollServer $self = shift;
|
|
|
|
$self->SUPER::fault(@_);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub process_line {
|
|
|
|
my Qpsmtpd::PollServer $self = shift;
|
|
|
|
my $line = shift || return;
|
|
|
|
if ($::DEBUG > 1) { print "$$:".($self+0)."C($self->{mode}): $line"; }
|
|
|
|
eval { $self->_process_line($line) };
|
|
|
|
if ($@) {
|
|
|
|
print STDERR "Error: $@\n";
|
|
|
|
return $self->fault("command failed unexpectedly") if $self->{mode} eq 'cmd';
|
|
|
|
return $self->fault("unknown error");
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub _process_line {
|
|
|
|
my Qpsmtpd::PollServer $self = shift;
|
|
|
|
my $line = shift;
|
|
|
|
|
|
|
|
if ($self->{mode} eq 'connect') {
|
|
|
|
$self->{mode} = 'cmd';
|
|
|
|
my $rc = $self->start_conversation;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
elsif ($self->{mode} eq 'cmd') {
|
|
|
|
$line =~ s/\r?\n//;
|
|
|
|
return $self->process_cmd($line);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
die "Unknown mode";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sub process_cmd {
|
|
|
|
my Qpsmtpd::PollServer $self = shift;
|
|
|
|
my $line = shift;
|
2007-03-19 22:13:17 +01:00
|
|
|
my ($cmd, @params) = split(/ +/, $line, 2);
|
2006-12-08 20:46:18 +01:00
|
|
|
my $meth = lc($cmd);
|
|
|
|
if (my $lookup = $self->{_commands}->{$meth} && $self->can($meth)) {
|
|
|
|
my $resp = eval {
|
|
|
|
$lookup->($self, @params);
|
|
|
|
};
|
|
|
|
if ($@) {
|
|
|
|
my $error = $@;
|
|
|
|
chomp($error);
|
|
|
|
$self->log(LOGERROR, "Command Error: $error");
|
|
|
|
return $self->fault("command '$cmd' failed unexpectedly");
|
|
|
|
}
|
|
|
|
return $resp;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
# No such method - i.e. unrecognized command
|
|
|
|
my ($rc, $msg) = $self->run_hooks("unrecognized_command", $meth, @params);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sub disconnect {
|
|
|
|
my Qpsmtpd::PollServer $self = shift;
|
|
|
|
$self->SUPER::disconnect(@_);
|
|
|
|
$self->close;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub start_conversation {
|
|
|
|
my Qpsmtpd::PollServer $self = shift;
|
|
|
|
|
|
|
|
my $conn = $self->connection;
|
|
|
|
# set remote_host, remote_ip and remote_port
|
|
|
|
my ($ip, $port) = split(':', $self->peer_addr_string);
|
2007-03-21 14:42:57 +01:00
|
|
|
return $self->close() unless $ip;
|
2006-12-08 20:46:18 +01:00
|
|
|
$conn->remote_ip($ip);
|
|
|
|
$conn->remote_port($port);
|
|
|
|
$conn->remote_info("[$ip]");
|
|
|
|
ParaDNS->new(
|
|
|
|
finished => sub { $self->run_hooks("connect") },
|
|
|
|
# NB: Setting remote_info to the same as remote_host
|
|
|
|
callback => sub { $conn->remote_info($conn->remote_host($_[0])) },
|
|
|
|
host => $ip,
|
|
|
|
);
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub data {
|
|
|
|
my Qpsmtpd::PollServer $self = shift;
|
|
|
|
|
|
|
|
my ($rc, $msg) = $self->run_hooks("data");
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub data_respond {
|
|
|
|
my Qpsmtpd::PollServer $self = shift;
|
|
|
|
my ($rc, $msg) = @_;
|
|
|
|
if ($rc == DONE) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
elsif ($rc == DENY) {
|
|
|
|
$self->respond(554, $msg || "Message denied");
|
|
|
|
$self->reset_transaction();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
elsif ($rc == DENYSOFT) {
|
|
|
|
$self->respond(451, $msg || "Message denied temporarily");
|
|
|
|
$self->reset_transaction();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
elsif ($rc == DENY_DISCONNECT) {
|
|
|
|
$self->respond(554, $msg || "Message denied");
|
|
|
|
$self->disconnect;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
elsif ($rc == DENYSOFT_DISCONNECT) {
|
|
|
|
$self->respond(451, $msg || "Message denied temporarily");
|
|
|
|
$self->disconnect;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
return $self->respond(503, "MAIL first") unless $self->transaction->sender;
|
|
|
|
return $self->respond(503, "RCPT first") unless $self->transaction->recipients;
|
|
|
|
|
|
|
|
$self->{header_lines} = '';
|
|
|
|
$self->{data_size} = 0;
|
|
|
|
$self->{in_header} = 1;
|
|
|
|
$self->{max_size} = ($self->config('databytes'))[0] || 0;
|
|
|
|
|
|
|
|
$self->log(LOGDEBUG, "max_size: $self->{max_size} / size: $self->{data_size}");
|
|
|
|
|
|
|
|
$self->respond(354, "go ahead");
|
|
|
|
|
|
|
|
my $max_get = $self->{max_size} || 1048576;
|
|
|
|
$self->get_chunks($max_get, sub { $self->got_data($_[0]) });
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub got_data {
|
|
|
|
my Qpsmtpd::PollServer $self = shift;
|
|
|
|
my $data = shift;
|
|
|
|
|
|
|
|
my $done = 0;
|
|
|
|
my $remainder;
|
2007-03-19 22:13:17 +01:00
|
|
|
if ($data =~ s/^\.\r\n(.*)\z//ms) {
|
2006-12-08 20:46:18 +01:00
|
|
|
$remainder = $1;
|
|
|
|
$done = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
# add a transaction->blocked check back here when we have line by line plugin access...
|
|
|
|
unless (($self->{max_size} and $self->{data_size} > $self->{max_size})) {
|
|
|
|
$data =~ s/\r\n/\n/mg;
|
|
|
|
$data =~ s/^\.\./\./mg;
|
|
|
|
|
2007-03-19 22:40:56 +01:00
|
|
|
if ($self->{in_header} and $data =~ s/\A(.*?\n)\n/\n/ms) {
|
2006-12-08 20:46:18 +01:00
|
|
|
$self->{header_lines} .= $1;
|
|
|
|
# end of headers
|
|
|
|
$self->{in_header} = 0;
|
|
|
|
|
|
|
|
# ... need to check that we don't reformat any of the received lines.
|
|
|
|
#
|
|
|
|
# 3.8.2 Received Lines in Gatewaying
|
|
|
|
# When forwarding a message into or out of the Internet environment, a
|
|
|
|
# gateway MUST prepend a Received: line, but it MUST NOT alter in any
|
|
|
|
# way a Received: line that is already in the header.
|
2007-03-19 22:40:56 +01:00
|
|
|
my @header_lines = split(/^/m, $self->{header_lines});
|
2006-12-08 20:46:18 +01:00
|
|
|
|
|
|
|
my $header = Mail::Header->new(\@header_lines,
|
|
|
|
Modify => 0, MailFrom => "COERCE");
|
|
|
|
$self->transaction->header($header);
|
|
|
|
$self->{header_lines} = '';
|
|
|
|
|
|
|
|
#$header->add("X-SMTPD", "qpsmtpd/".$self->version.", http://smtpd.develooper.com/");
|
|
|
|
|
|
|
|
# FIXME - call plugins to work on just the header here; can
|
|
|
|
# save us buffering the mail content.
|
2007-03-19 22:40:56 +01:00
|
|
|
|
|
|
|
# Save the start of just the body itself
|
|
|
|
$self->transaction->set_body_start();
|
2006-12-08 20:46:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($self->{in_header}) {
|
|
|
|
$self->{header_lines} .= $data;
|
|
|
|
}
|
2007-03-19 22:40:56 +01:00
|
|
|
|
|
|
|
$self->transaction->body_write(\$data);
|
2006-12-08 20:46:18 +01:00
|
|
|
$self->{data_size} += length $data;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($done) {
|
|
|
|
$self->end_of_data;
|
|
|
|
$self->end_get_chunks($remainder);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
sub end_of_data {
|
|
|
|
my Qpsmtpd::PollServer $self = shift;
|
|
|
|
|
|
|
|
#$self->log(LOGDEBUG, "size is at $size\n") unless ($i % 300);
|
|
|
|
|
|
|
|
$self->log(LOGDEBUG, "max_size: $self->{max_size} / size: $self->{data_size}");
|
|
|
|
|
|
|
|
my $smtp = $self->connection->hello eq "ehlo" ? "ESMTP" : "SMTP";
|
|
|
|
|
|
|
|
my $header = $self->transaction->header;
|
|
|
|
if (!$header) {
|
|
|
|
$header = Mail::Header->new(Modify => 0, MailFrom => "COERCE");
|
|
|
|
$self->transaction->header($header);
|
|
|
|
}
|
|
|
|
|
|
|
|
# only true if client authenticated
|
|
|
|
if ( $self->authenticated == OK ) {
|
|
|
|
$header->add("X-Qpsmtpd-Auth","True");
|
|
|
|
}
|
|
|
|
|
|
|
|
$header->add("Received", "from ".$self->connection->remote_info
|
|
|
|
." (HELO ".$self->connection->hello_host . ") (".$self->connection->remote_ip
|
|
|
|
. ")\n by ".$self->config('me')." (qpsmtpd/".$self->version
|
|
|
|
.") with $smtp; ". (strftime('%a, %d %b %Y %H:%M:%S %z', localtime)),
|
|
|
|
0);
|
|
|
|
|
|
|
|
return $self->respond(552, "Message too big!") if $self->{max_size} and $self->{data_size} > $self->{max_size};
|
|
|
|
|
|
|
|
my ($rc, $msg) = $self->run_hooks("data_post");
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
1;
|
|
|
|
|