b00f4c7793
which will merge into the main branch fairly easily
350 lines
9.6 KiB
Perl
350 lines
9.6 KiB
Perl
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
|
|
conn
|
|
_auth
|
|
_auth_mechanism
|
|
_auth_state
|
|
_auth_ticket
|
|
_auth_user
|
|
_commands
|
|
_config_cache
|
|
_connection
|
|
_continuation
|
|
_extras
|
|
_test_mode
|
|
_transaction
|
|
);
|
|
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;
|
|
|
|
my ($rc, @msg) = $self->run_hooks_no_respond("pre-connection");
|
|
if ($rc == DENYSOFT || $rc == DENYSOFT_DISCONNECT) {
|
|
@msg = ("Sorry, try again later")
|
|
unless @msg;
|
|
$self->respond(451, @msg);
|
|
$self->disconnect;
|
|
}
|
|
elsif ($rc == DENY || $rc == DENY_DISCONNECT) {
|
|
@msg = ("Sorry, service not available for you")
|
|
unless @msg;
|
|
$self->respond(550, @msg);
|
|
$self->disconnect;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
my %cmd_cache;
|
|
|
|
sub process_line {
|
|
my Qpsmtpd::PollServer $self = shift;
|
|
my $line = shift || return;
|
|
if ($::DEBUG > 1) { print "$$:".($self+0)."C($self->{mode}): $line"; }
|
|
if ($self->{mode} eq 'cmd') {
|
|
$line =~ s/\r?\n$//s;
|
|
$self->connection->notes('original_string', $line);
|
|
my ($cmd, @params) = split(/ +/, $line, 2);
|
|
my $meth = lc($cmd);
|
|
if (my $lookup = $cmd_cache{$meth} || $self->{_commands}->{$meth} && $self->can($meth)) {
|
|
$cmd_cache{$meth} = $lookup;
|
|
eval {
|
|
$lookup->($self, @params);
|
|
};
|
|
if ($@) {
|
|
my $error = $@;
|
|
chomp($error);
|
|
$self->log(LOGERROR, "Command Error: $error");
|
|
$self->fault("command '$cmd' failed unexpectedly");
|
|
}
|
|
}
|
|
else {
|
|
# No such method - i.e. unrecognized command
|
|
my ($rc, $msg) = $self->run_hooks("unrecognized_command", $meth, @params);
|
|
}
|
|
}
|
|
elsif ($self->{mode} eq 'connect') {
|
|
$self->{mode} = 'cmd';
|
|
# I've removed an eval{} from around this. It shouldn't ever die()
|
|
# but if it does we're a bit screwed... Ah well :-)
|
|
$self->start_conversation;
|
|
}
|
|
else {
|
|
die "Unknown mode";
|
|
}
|
|
return;
|
|
}
|
|
|
|
sub disconnect {
|
|
my Qpsmtpd::PollServer $self = shift;
|
|
$self->SUPER::disconnect(@_);
|
|
$self->close;
|
|
}
|
|
|
|
sub close {
|
|
my Qpsmtpd::PollServer $self = shift;
|
|
$self->run_hooks_no_respond("post-connection");
|
|
$self->connection->reset;
|
|
$self->SUPER::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);
|
|
return $self->close() unless $ip;
|
|
$conn->remote_ip($ip);
|
|
$conn->remote_port($port);
|
|
$conn->remote_info("[$ip]");
|
|
my ($lip,$lport) = split(':', $self->local_addr_string);
|
|
$conn->local_ip($lip);
|
|
$conn->local_port($lport);
|
|
|
|
ParaDNS->new(
|
|
finished => sub { $self->continue_read(); $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) {
|
|
$msg->[0] ||= "Message denied";
|
|
$self->respond(554, @$msg);
|
|
$self->reset_transaction();
|
|
return;
|
|
}
|
|
elsif ($rc == DENYSOFT) {
|
|
$msg->[0] ||= "Message denied temporarily";
|
|
$self->respond(451, @$msg);
|
|
$self->reset_transaction();
|
|
return;
|
|
}
|
|
elsif ($rc == DENY_DISCONNECT) {
|
|
$msg->[0] ||= "Message denied";
|
|
$self->respond(554, @$msg);
|
|
$self->disconnect;
|
|
return;
|
|
}
|
|
elsif ($rc == DENYSOFT_DISCONNECT) {
|
|
$msg->[0] ||= "Message denied temporarily";
|
|
$self->respond(451, @$msg);
|
|
$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;
|
|
if ($data =~ s/^\.\r\n(.*)\z//ms) {
|
|
$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;
|
|
|
|
if ($self->{in_header}) {
|
|
$self->{header_lines} .= $data;
|
|
|
|
if ($self->{header_lines} =~ s/\n(\n.*)\z/\n/ms) {
|
|
$data = $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.
|
|
my @header_lines = split(/^/m, $self->{header_lines});
|
|
|
|
my $header = Mail::Header->new(\@header_lines,
|
|
Modify => 0, MailFrom => "COERCE");
|
|
$self->transaction->header($header);
|
|
$self->transaction->body_write($self->{header_lines});
|
|
$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.
|
|
|
|
# Save the start of just the body itself
|
|
$self->transaction->set_body_start();
|
|
}
|
|
}
|
|
|
|
$self->transaction->body_write(\$data);
|
|
$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 $header = $self->transaction->header;
|
|
if (!$header) {
|
|
$header = Mail::Header->new(Modify => 0, MailFrom => "COERCE");
|
|
$self->transaction->header($header);
|
|
}
|
|
|
|
my $smtp = $self->connection->hello eq "ehlo" ? "ESMTP" : "SMTP";
|
|
my $esmtp = substr($smtp,0,1) eq "E";
|
|
my $authheader;
|
|
my $sslheader;
|
|
|
|
if (defined $self->connection->notes('tls_enabled')
|
|
and $self->connection->notes('tls_enabled'))
|
|
{
|
|
$smtp .= "S" if $esmtp; # RFC3848
|
|
$sslheader = "(".$self->connection->notes('tls_socket')->get_cipher()." encrypted) ";
|
|
}
|
|
|
|
if (defined $self->{_auth} and $self->{_auth} == OK) {
|
|
$smtp .= "A" if $esmtp; # RFC3848
|
|
$authheader = "(smtp-auth username $self->{_auth_user}, mechanism $self->{_auth_mechanism})\n";
|
|
}
|
|
|
|
$header->add("Received", $self->received_line($smtp, $authheader, $sslheader), 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;
|
|
|