diff --git a/lib/Apache/Qpsmtpd.pm b/lib/Apache/Qpsmtpd.pm index d85d608..9ad82ef 100644 --- a/lib/Apache/Qpsmtpd.pm +++ b/lib/Apache/Qpsmtpd.pm @@ -7,13 +7,13 @@ use warnings FATAL => 'all'; use Apache2::ServerUtil (); use Apache2::Connection (); use Apache2::Const -compile => qw(OK MODE_GETLINE); -use APR::Const -compile => qw(SO_NONBLOCK EOF SUCCESS); -use APR::Error (); -use APR::Brigade (); -use APR::Bucket (); -use APR::Socket (); +use APR::Const -compile => qw(SO_NONBLOCK EOF SUCCESS); +use APR::Error (); +use APR::Brigade (); +use APR::Bucket (); +use APR::Socket (); use Apache2::Filter (); -use ModPerl::Util (); +use ModPerl::Util (); our $VERSION = '0.02'; @@ -22,15 +22,15 @@ sub handler { $c->client_socket->opt_set(APR::Const::SO_NONBLOCK => 0); die "\$ENV{QPSMTPD_CONFIG} must be given" unless $ENV{QPSMTPD_CONFIG}; - + my $qpsmtpd = Qpsmtpd::Apache->new(); $qpsmtpd->start_connection( - ip => $c->remote_ip, - host => $c->remote_host, - info => undef, - conn => $c, - ); - + ip => $c->remote_ip, + host => $c->remote_host, + info => undef, + conn => $c, + ); + $qpsmtpd->run($c); $qpsmtpd->run_hooks("post-connection"); $qpsmtpd->connection->reset; @@ -46,20 +46,21 @@ use base qw(Qpsmtpd::SMTP); my %cdir_memo; sub config_dir { - my ($self, $config) = @_; - if (exists $cdir_memo{$config}) { - return $cdir_memo{$config}; - } + my ($self, $config) = @_; + if (exists $cdir_memo{$config}) { + return $cdir_memo{$config}; + } - if (uc($ENV{QPSMTPD_CONFIG}) eq 'USE-VIRTUAL-DOMAINS') { - my $cdir = $self->{conn}->base_server->dir_config("qpsmtpd.config_dir"); - $cdir =~ /^(.*)$/; # detaint - my $configdir = $1 if -e "$1/$config"; - $cdir_memo{$config} = $configdir; - } else { - $cdir_memo{$config} = $self->SUPER::config_dir(@_); - } - return $cdir_memo{$config}; + if (uc($ENV{QPSMTPD_CONFIG}) eq 'USE-VIRTUAL-DOMAINS') { + my $cdir = $self->{conn}->base_server->dir_config("qpsmtpd.config_dir"); + $cdir =~ /^(.*)$/; # detaint + my $configdir = $1 if -e "$1/$config"; + $cdir_memo{$config} = $configdir; + } + else { + $cdir_memo{$config} = $self->SUPER::config_dir(@_); + } + return $cdir_memo{$config}; } sub start_connection { @@ -67,23 +68,26 @@ sub start_connection { my %opts = @_; $self->{conn} = $opts{conn}; - $self->{conn}->client_socket->timeout_set($self->config('timeout') * 1_000_000); - $self->{bb_in} = APR::Brigade->new($self->{conn}->pool, $self->{conn}->bucket_alloc); - $self->{bb_out} = APR::Brigade->new($self->{conn}->pool, $self->{conn}->bucket_alloc); + $self->{conn} + ->client_socket->timeout_set($self->config('timeout') * 1_000_000); + $self->{bb_in} = + APR::Brigade->new($self->{conn}->pool, $self->{conn}->bucket_alloc); + $self->{bb_out} = + APR::Brigade->new($self->{conn}->pool, $self->{conn}->bucket_alloc); - my $remote_host = $opts{host} || ( $opts{ip} ? "[$opts{ip}]" : "[noip!]"); + my $remote_host = $opts{host} || ($opts{ip} ? "[$opts{ip}]" : "[noip!]"); my $remote_info = $opts{info} ? "$opts{info}\@$remote_host" : $remote_host; my $remote_ip = $opts{ip}; $self->log(LOGNOTICE, "Connection from $remote_info [$remote_ip]"); $self->SUPER::connection->start( - remote_info => $remote_info, - remote_ip => $remote_ip, - remote_host => $remote_host, - local_ip => $opts{conn}->local_ip, - @_ - ); + remote_info => $remote_info, + remote_ip => $remote_ip, + remote_host => $remote_host, + local_ip => $opts{conn}->local_ip, + @_ + ); } sub config { @@ -117,31 +121,32 @@ sub getline { return if $c->aborted; my $bb = $self->{bb_in}; - + while (1) { - my $rc = $c->input_filters->get_brigade($bb, Apache2::Const::MODE_GETLINE); + my $rc = + $c->input_filters->get_brigade($bb, Apache2::Const::MODE_GETLINE); return if $rc == APR::Const::EOF; die APR::Error::strerror($rc) unless $rc == APR::Const::SUCCESS; - + next unless $bb->flatten(my $data); - + $bb->cleanup; return $data; } - + return ''; } sub read_input { my $self = shift; - my $c = $self->{conn}; + my $c = $self->{conn}; while (defined(my $data = $self->getline)) { - $data =~ s/\r?\n$//s; # advanced chomp + $data =~ s/\r?\n$//s; # advanced chomp $self->connection->notes('original_string', $data); $self->log(LOGDEBUG, "dispatching $data"); defined $self->dispatch(split / +/, $data, 2) - or $self->respond(502, "command unrecognized: '$data'"); + or $self->respond(502, "command unrecognized: '$data'"); last if $self->{_quitting}; } } @@ -151,11 +156,12 @@ sub respond { my $c = $self->{conn}; while (my $msg = shift @messages) { my $bb = $self->{bb_out}; - my $line = $code . (@messages?"-":" ").$msg; + my $line = $code . (@messages ? "-" : " ") . $msg; $self->log(LOGDEBUG, $line); my $bucket = APR::Bucket->new(($c->bucket_alloc), "$line\r\n"); $bb->insert_tail($bucket); $c->output_filters->fflush($bb); + # $bucket->remove; $bb->cleanup; } diff --git a/lib/Danga/Client.pm b/lib/Danga/Client.pm index 25fe6dd..1e10499 100644 --- a/lib/Danga/Client.pm +++ b/lib/Danga/Client.pm @@ -3,26 +3,26 @@ package Danga::Client; use base 'Danga::TimeoutSocket'; use fields qw( - line - pause_count - read_bytes - data_bytes - callback - get_chunks - reader_object - ); + line + pause_count + read_bytes + data_bytes + callback + get_chunks + reader_object + ); use Time::HiRes (); use bytes; # 30 seconds max timeout! -sub max_idle_time { 30 } -sub max_connect_time { 1200 } +sub max_idle_time { 30 } +sub max_connect_time { 1200 } sub new { my Danga::Client $self = shift; $self = fields::new($self) unless ref $self; - $self->SUPER::new( @_ ); + $self->SUPER::new(@_); $self->reset_for_next_message; return $self; @@ -30,13 +30,13 @@ sub new { sub reset_for_next_message { my Danga::Client $self = shift; - $self->{line} = ''; - $self->{pause_count} = 0; - $self->{read_bytes} = 0; - $self->{callback} = undef; + $self->{line} = ''; + $self->{pause_count} = 0; + $self->{read_bytes} = 0; + $self->{callback} = undef; $self->{reader_object} = undef; - $self->{data_bytes} = ''; - $self->{get_chunks} = 0; + $self->{data_bytes} = ''; + $self->{get_chunks} = 0; return $self; } @@ -52,10 +52,12 @@ sub get_bytes { $self->{line} = ''; if ($self->{read_bytes} <= 0) { if ($self->{read_bytes} < 0) { - $self->{line} = substr($self->{data_bytes}, - $self->{read_bytes}, # negative offset - 0 - $self->{read_bytes}, # to end of str - ""); # truncate that substr + $self->{line} = substr( + $self->{data_bytes}, + $self->{read_bytes}, # negative offset + 0 - $self->{read_bytes}, # to end of str + "" + ); # truncate that substr } $callback->($self->{data_bytes}); return; @@ -91,14 +93,14 @@ sub get_chunks { } $self->{read_bytes} = $bytes; $self->process_chunk($callback) if length($self->{line}); - $self->{callback} = $callback; + $self->{callback} = $callback; $self->{get_chunks} = 1; } sub end_get_chunks { my Danga::Client $self = shift; my $remaining = shift; - $self->{callback} = undef; + $self->{callback} = undef; $self->{get_chunks} = 0; if (defined($remaining)) { $self->process_read_buf(\$remaining); @@ -132,6 +134,7 @@ sub event_read { $self->{data_bytes} .= $$bref; } if ($self->{read_bytes} <= 0) { + # print "Erk, read too much!\n" if $self->{read_bytes} < 0; my $cb = $self->{callback}; $self->{callback} = undef; @@ -150,21 +153,29 @@ sub process_read_buf { my $bref = shift; $self->{line} .= $$bref; return if $self->{pause_count} || $self->{closed}; - + if ($self->{line} =~ s/^(.*?\n)//) { my $line = $1; $self->{alive_time} = time; my $resp = $self->process_line($line); - if ($::DEBUG > 1 and $resp) { print "$$:".($self+0)."S: $_\n" for split(/\n/, $resp) } + if ($::DEBUG > 1 and $resp) { + print "$$:" . ($self + 0) . "S: $_\n" for split(/\n/, $resp); + } $self->write($resp) if $resp; + # $self->watch_read(0) if $self->{pause_count}; return if $self->{pause_count} || $self->{closed}; + # read more in a timer, to give other clients a look in - $self->AddTimer(0, sub { - if (length($self->{line}) && !$self->paused) { - $self->process_read_buf(\""); # " for bad syntax highlighters + $self->AddTimer( + 0, + sub { + if (length($self->{line}) && !$self->paused) { + $self->process_read_buf(\"") + ; # " for bad syntax highlighters + } } - }); + ); } } @@ -188,6 +199,7 @@ sub paused { sub pause_read { my Danga::Client $self = shift; $self->{pause_count}++; + # $self->watch_read(0); } @@ -196,11 +208,15 @@ sub continue_read { $self->{pause_count}--; if ($self->{pause_count} <= 0) { $self->{pause_count} = 0; - $self->AddTimer(0, sub { - if (length($self->{line}) && !$self->paused) { - $self->process_read_buf(\""); # " for bad syntax highlighters + $self->AddTimer( + 0, + sub { + if (length($self->{line}) && !$self->paused) { + $self->process_read_buf(\"") + ; # " for bad syntax highlighters + } } - }); + ); } } @@ -216,6 +232,10 @@ sub close { } sub event_err { my Danga::Client $self = shift; $self->close("Error") } -sub event_hup { my Danga::Client $self = shift; $self->close("Disconnect (HUP)") } + +sub event_hup { + my Danga::Client $self = shift; + $self->close("Disconnect (HUP)"); +} 1; diff --git a/lib/Danga/TimeoutSocket.pm b/lib/Danga/TimeoutSocket.pm index c15aab6..030514d 100644 --- a/lib/Danga/TimeoutSocket.pm +++ b/lib/Danga/TimeoutSocket.pm @@ -22,8 +22,8 @@ sub new { } # overload these in a subclass -sub max_idle_time { 0 } -sub max_connect_time { 0 } +sub max_idle_time { 0 } +sub max_connect_time { 0 } sub Reset { Danga::Socket->Reset; @@ -32,21 +32,21 @@ sub Reset { sub _do_cleanup { my $now = time; - + Danga::Socket->AddTimer(15, \&_do_cleanup); - + my $sf = __PACKAGE__->get_sock_ref; - my %max_age; # classname -> max age (0 means forever) - my %max_connect; # classname -> max connect time + my %max_age; # classname -> max age (0 means forever) + my %max_connect; # classname -> max connect time my @to_close; while (my $k = each %$sf) { my Danga::TimeoutSocket $v = $sf->{$k}; my $ref = ref $v; next unless $v->isa('Danga::TimeoutSocket'); unless (defined $max_age{$ref}) { - $max_age{$ref} = $ref->max_idle_time || 0; - $max_connect{$ref} = $ref->max_connect_time || 0; + $max_age{$ref} = $ref->max_idle_time || 0; + $max_connect{$ref} = $ref->max_connect_time || 0; } if (my $t = $max_connect{$ref}) { if ($v->{create_time} < $now - $t) { diff --git a/lib/Qpsmtpd.pm b/lib/Qpsmtpd.pm index 6d7bc12..ec7c0ef 100644 --- a/lib/Qpsmtpd.pm +++ b/lib/Qpsmtpd.pm @@ -19,9 +19,9 @@ if (-e ".git") { my $hooks = {}; my %defaults = ( - me => hostname, - timeout => 1200, - ); + me => hostname, + timeout => 1200, + ); my $_config_cache = {}; my %config_dir_memo; @@ -30,111 +30,115 @@ my %config_dir_memo; my $LOGGING_LOADED = 0; sub _restart { - my $self = shift; - my %args = @_; - if ($args{restart}) { - # reset all global vars to defaults - $self->clear_config_cache; - $hooks = {}; - $LOGGING_LOADED = 0; - %config_dir_memo = (); - $TraceLevel = LOGWARN; - $Spool_dir = undef; - $Size_threshold = undef; - } + my $self = shift; + my %args = @_; + if ($args{restart}) { + + # reset all global vars to defaults + $self->clear_config_cache; + $hooks = {}; + $LOGGING_LOADED = 0; + %config_dir_memo = (); + $TraceLevel = LOGWARN; + $Spool_dir = undef; + $Size_threshold = undef; + } } - sub DESTROY { + #warn $_ for DashProfiler->profile_as_text("qpsmtpd"); } -sub version { $VERSION . ($git ? "/$git" : "") }; - -sub TRACE_LEVEL { $TraceLevel }; # leave for plugin compatibility +sub version { $VERSION . ($git ? "/$git" : "") } +sub TRACE_LEVEL { $TraceLevel }; # leave for plugin compatibility sub hooks { $hooks; } sub load_logging { - # need to do this differently than other plugins so as to - # not trigger logging activity - return if $LOGGING_LOADED; - my $self = shift; - return if $hooks->{"logging"}; - my $configdir = $self->config_dir("logging"); - my $configfile = "$configdir/logging"; - my @loggers = $self->_config_from_file($configfile,'logging'); - $configdir = $self->config_dir('plugin_dirs'); - $configfile = "$configdir/plugin_dirs"; - my @plugin_dirs = $self->_config_from_file($configfile,'plugin_dirs'); - unless (@plugin_dirs) { - my ($name) = ($0 =~ m!(.*?)/([^/]+)$!); - @plugin_dirs = ( "$name/plugins" ); - } + # need to do this differently than other plugins so as to + # not trigger logging activity + return if $LOGGING_LOADED; + my $self = shift; + return if $hooks->{"logging"}; + my $configdir = $self->config_dir("logging"); + my $configfile = "$configdir/logging"; + my @loggers = $self->_config_from_file($configfile, 'logging'); - my @loaded; - for my $logger (@loggers) { - push @loaded, $self->_load_plugin($logger, @plugin_dirs); - } + $configdir = $self->config_dir('plugin_dirs'); + $configfile = "$configdir/plugin_dirs"; + my @plugin_dirs = $self->_config_from_file($configfile, 'plugin_dirs'); + unless (@plugin_dirs) { + my ($name) = ($0 =~ m!(.*?)/([^/]+)$!); + @plugin_dirs = ("$name/plugins"); + } - foreach my $logger (@loaded) { - $self->log(LOGINFO, "Loaded $logger"); - } + my @loaded; + for my $logger (@loggers) { + push @loaded, $self->_load_plugin($logger, @plugin_dirs); + } - $configdir = $self->config_dir("loglevel"); - $configfile = "$configdir/loglevel"; - $TraceLevel = $self->_config_from_file($configfile,'loglevel'); + foreach my $logger (@loaded) { + $self->log(LOGINFO, "Loaded $logger"); + } - unless (defined($TraceLevel) and $TraceLevel =~ /^\d+$/) { - $TraceLevel = LOGWARN; # Default if no loglevel file found. - } + $configdir = $self->config_dir("loglevel"); + $configfile = "$configdir/loglevel"; + $TraceLevel = $self->_config_from_file($configfile, 'loglevel'); - $LOGGING_LOADED = 1; + unless (defined($TraceLevel) and $TraceLevel =~ /^\d+$/) { + $TraceLevel = LOGWARN; # Default if no loglevel file found. + } - return @loggers; + $LOGGING_LOADED = 1; + + return @loggers; } sub trace_level { - my $self = shift; - return $TraceLevel; + my $self = shift; + return $TraceLevel; } -sub init_logger { # needed for compatibility purposes - shift->trace_level(); +sub init_logger { # needed for compatibility purposes + shift->trace_level(); } sub log { - my ($self, $trace, @log) = @_; - $self->varlog($trace,join(" ",@log)); + my ($self, $trace, @log) = @_; + $self->varlog($trace, join(" ", @log)); } sub varlog { - my ($self, $trace) = (shift,shift); - my ($hook, $plugin, @log); - if ( $#_ == 0 ) { # log itself - (@log) = @_; - } - elsif ( $#_ == 1 ) { # plus the hook - ($hook, @log) = @_; - } - else { # called from plugin - ($hook, $plugin, @log) = @_; - } + my ($self, $trace) = (shift, shift); + my ($hook, $plugin, @log); + if ($#_ == 0) { # log itself + (@log) = @_; + } + elsif ($#_ == 1) { # plus the hook + ($hook, @log) = @_; + } + else { # called from plugin + ($hook, $plugin, @log) = @_; + } - $self->load_logging; # in case we don't have this loaded yet + $self->load_logging; # in case we don't have this loaded yet - my ($rc) = $self->run_hooks_no_respond("logging", $trace, $hook, $plugin, @log) - or return; + my ($rc) = + $self->run_hooks_no_respond("logging", $trace, $hook, $plugin, @log) + or return; - return if $rc == DECLINED || $rc == OK; # plugin success + return if $rc == DECLINED || $rc == OK; # plugin success return if $trace > $TraceLevel; # no logging plugins registered, fall back to STDERR - my $prefix = defined $plugin && defined $hook ? " ($hook) $plugin:" : - defined $plugin ? " $plugin:" : - defined $hook ? " ($hook) running plugin:" : ''; + my $prefix = + defined $plugin && defined $hook ? " ($hook) $plugin:" + : defined $plugin ? " $plugin:" + : defined $hook ? " ($hook) running plugin:" + : ''; warn join(' ', $$ . $prefix, @log), "\n"; } @@ -149,280 +153,301 @@ sub clear_config_cache { # database or whatever. # sub config { - my ($self, $c, $type) = @_; + my ($self, $c, $type) = @_; - $self->log(LOGDEBUG, "in config($c)"); + $self->log(LOGDEBUG, "in config($c)"); - # first try the cache - # XXX - is this always the right thing to do? what if a config hook - # can return different values on subsequent calls? - if ($_config_cache->{$c}) { - $self->log(LOGDEBUG, "config($c) returning (@{$_config_cache->{$c}}) from cache"); - return wantarray ? @{$_config_cache->{$c}} : $_config_cache->{$c}->[0]; - } + # first try the cache + # XXX - is this always the right thing to do? what if a config hook + # can return different values on subsequent calls? + if ($_config_cache->{$c}) { + $self->log(LOGDEBUG, + "config($c) returning (@{$_config_cache->{$c}}) from cache"); + return wantarray ? @{$_config_cache->{$c}} : $_config_cache->{$c}->[0]; + } - # then run the hooks - my ($rc, @config) = $self->run_hooks_no_respond("config", $c); - $self->log(LOGDEBUG, "config($c): hook returned ($rc, @config) "); - if ($rc == OK) { - $self->log(LOGDEBUG, "setting _config_cache for $c to [@config] from hooks and returning it"); - $_config_cache->{$c} = \@config; - return wantarray ? @{$_config_cache->{$c}} : $_config_cache->{$c}->[0]; - } + # then run the hooks + my ($rc, @config) = $self->run_hooks_no_respond("config", $c); + $self->log(LOGDEBUG, "config($c): hook returned ($rc, @config) "); + if ($rc == OK) { + $self->log(LOGDEBUG, +"setting _config_cache for $c to [@config] from hooks and returning it" + ); + $_config_cache->{$c} = \@config; + return wantarray ? @{$_config_cache->{$c}} : $_config_cache->{$c}->[0]; + } - # and then get_qmail_config - @config = $self->get_qmail_config($c, $type); - if (@config) { - $self->log(LOGDEBUG, "setting _config_cache for $c to [@config] from get_qmail_config and returning it"); - $_config_cache->{$c} = \@config; - return wantarray ? @{$_config_cache->{$c}} : $_config_cache->{$c}->[0]; - } + # and then get_qmail_config + @config = $self->get_qmail_config($c, $type); + if (@config) { + $self->log(LOGDEBUG, +"setting _config_cache for $c to [@config] from get_qmail_config and returning it" + ); + $_config_cache->{$c} = \@config; + return wantarray ? @{$_config_cache->{$c}} : $_config_cache->{$c}->[0]; + } - # finally we use the default if there is any: - if (exists($defaults{$c})) { - $self->log(LOGDEBUG, "setting _config_cache for $c to @{[$defaults{$c}]} from defaults and returning it"); - $_config_cache->{$c} = [$defaults{$c}]; - return wantarray ? @{$_config_cache->{$c}} : $_config_cache->{$c}->[0]; - } - return; + # finally we use the default if there is any: + if (exists($defaults{$c})) { + $self->log(LOGDEBUG, +"setting _config_cache for $c to @{[$defaults{$c}]} from defaults and returning it" + ); + $_config_cache->{$c} = [$defaults{$c}]; + return wantarray ? @{$_config_cache->{$c}} : $_config_cache->{$c}->[0]; + } + return; } sub config_dir { - my ($self, $config) = @_; - if (exists $config_dir_memo{$config}) { - return $config_dir_memo{$config}; - } - my $configdir = ($ENV{QMAIL} || '/var/qmail') . '/control'; - my ($path) = ($ENV{PROCESS} ? $ENV{PROCESS} : $0) =~ m!(.*?)/([^/]+)$!; - $configdir = "$path/config" if (-e "$path/config/$config"); - if (exists $ENV{QPSMTPD_CONFIG}) { - $ENV{QPSMTPD_CONFIG} =~ /^(.*)$/; # detaint - $configdir = $1 if -e "$1/$config"; - } - return $config_dir_memo{$config} = $configdir; + my ($self, $config) = @_; + if (exists $config_dir_memo{$config}) { + return $config_dir_memo{$config}; + } + my $configdir = ($ENV{QMAIL} || '/var/qmail') . '/control'; + my ($path) = ($ENV{PROCESS} ? $ENV{PROCESS} : $0) =~ m!(.*?)/([^/]+)$!; + $configdir = "$path/config" if (-e "$path/config/$config"); + if (exists $ENV{QPSMTPD_CONFIG}) { + $ENV{QPSMTPD_CONFIG} =~ /^(.*)$/; # detaint + $configdir = $1 if -e "$1/$config"; + } + return $config_dir_memo{$config} = $configdir; } sub plugin_dirs { - my $self = shift; + my $self = shift; my @plugin_dirs = $self->config('plugin_dirs'); unless (@plugin_dirs) { my ($path) = ($ENV{PROCESS} ? $ENV{PROCESS} : $0) =~ m!(.*?)/([^/]+)$!; - @plugin_dirs = ( "$path/plugins" ); + @plugin_dirs = ("$path/plugins"); } return @plugin_dirs; } sub get_qmail_config { - my ($self, $config, $type) = @_; - $self->log(LOGDEBUG, "trying to get config for $config"); - my $configdir = $self->config_dir($config); + my ($self, $config, $type) = @_; + $self->log(LOGDEBUG, "trying to get config for $config"); + my $configdir = $self->config_dir($config); - my $configfile = "$configdir/$config"; + my $configfile = "$configdir/$config"; - # CDB config support really should be moved to a plugin - if ($type and $type eq "map") { - unless (-e $configfile . ".cdb") { - $_config_cache->{$config} ||= []; - return +{}; - } - eval { require CDB_File }; + # CDB config support really should be moved to a plugin + if ($type and $type eq "map") { + unless (-e $configfile . ".cdb") { + $_config_cache->{$config} ||= []; + return +{}; + } + eval { require CDB_File }; - if ($@) { - $self->log(LOGERROR, "No CDB Support! Did NOT read $configfile.cdb, could not load CDB_File module: $@"); - return +{}; + if ($@) { + $self->log(LOGERROR, +"No CDB Support! Did NOT read $configfile.cdb, could not load CDB_File module: $@" + ); + return +{}; + } + + my %h; + unless (tie(%h, 'CDB_File', "$configfile.cdb")) { + $self->log(LOGERROR, "tie of $configfile.cdb failed: $!"); + return +{}; + } + + # We explicitly don't cache cdb entries. The assumption is that + # the data is in a CDB file in the first place because there's + # lots of data and the cache hit ratio would be low. + return \%h; } - my %h; - unless (tie(%h, 'CDB_File', "$configfile.cdb")) { - $self->log(LOGERROR, "tie of $configfile.cdb failed: $!"); - return +{}; - } - # We explicitly don't cache cdb entries. The assumption is that - # the data is in a CDB file in the first place because there's - # lots of data and the cache hit ratio would be low. - return \%h; - } - - return $self->_config_from_file($configfile, $config); + return $self->_config_from_file($configfile, $config); } sub _config_from_file { - my ($self, $configfile, $config, $visited) = @_; - unless (-e $configfile) { - $_config_cache->{$config} ||= []; - return; - } - - $visited ||= []; - push @{$visited}, $configfile; - - open CF, "<$configfile" or warn "$$ could not open configfile $configfile: $!" and return; - my @config = ; - chomp @config; - @config = grep { length($_) and $_ !~ m/^\s*#/ and $_ =~ m/\S/} - map {s/^\s+//; s/\s+$//; $_;} # trim leading/trailing whitespace - @config; - close CF; - - my $pos = 0; - while ($pos < @config) { - # recursively pursue an $include reference, if found. An inclusion which - # begins with a leading slash is interpreted as a path to a file and will - # supercede the usual config path resolution. Otherwise, the normal - # config_dir() lookup is employed (the location in which the inclusion - # appeared receives no special precedence; possibly it should, but it'd - # be complicated beyond justifiability for so simple a config system. - if ($config[$pos] =~ /^\s*\$include\s+(\S+)\s*$/) { - my ($includedir, $inclusion) = ('', $1); - - splice @config, $pos, 1; # remove the $include line - if ($inclusion !~ /^\//) { - $includedir = $self->config_dir($inclusion); - $inclusion = "$includedir/$inclusion"; - } - - if (grep($_ eq $inclusion, @{$visited})) { - $self->log(LOGERROR, "Circular \$include reference in config $config:"); - $self->log(LOGERROR, "From $visited->[0]:"); - $self->log(LOGERROR, " includes $_") - for (@{$visited}[1..$#{$visited}], $inclusion); - return wantarray ? () : undef; - } - push @{$visited}, $inclusion; - - for my $inc ($self->expand_inclusion_($inclusion, $configfile)) { - my @insertion = $self->_config_from_file($inc, $config, $visited); - splice @config, $pos, 0, @insertion; # insert the inclusion - $pos += @insertion; - } - } else { - $pos++; + my ($self, $configfile, $config, $visited) = @_; + unless (-e $configfile) { + $_config_cache->{$config} ||= []; + return; } - } - $_config_cache->{$config} = \@config; + $visited ||= []; + push @{$visited}, $configfile; - return wantarray ? @config : $config[0]; + open CF, "<$configfile" + or warn "$$ could not open configfile $configfile: $!" and return; + my @config = ; + chomp @config; + @config = grep { length($_) and $_ !~ m/^\s*#/ and $_ =~ m/\S/ } + map { s/^\s+//; s/\s+$//; $_; } # trim leading/trailing whitespace + @config; + close CF; + + my $pos = 0; + while ($pos < @config) { + + # recursively pursue an $include reference, if found. An inclusion which + # begins with a leading slash is interpreted as a path to a file and will + # supercede the usual config path resolution. Otherwise, the normal + # config_dir() lookup is employed (the location in which the inclusion + # appeared receives no special precedence; possibly it should, but it'd + # be complicated beyond justifiability for so simple a config system. + if ($config[$pos] =~ /^\s*\$include\s+(\S+)\s*$/) { + my ($includedir, $inclusion) = ('', $1); + + splice @config, $pos, 1; # remove the $include line + if ($inclusion !~ /^\//) { + $includedir = $self->config_dir($inclusion); + $inclusion = "$includedir/$inclusion"; + } + + if (grep($_ eq $inclusion, @{$visited})) { + $self->log(LOGERROR, + "Circular \$include reference in config $config:"); + $self->log(LOGERROR, "From $visited->[0]:"); + $self->log(LOGERROR, " includes $_") + for (@{$visited}[1 .. $#{$visited}], $inclusion); + return wantarray ? () : undef; + } + push @{$visited}, $inclusion; + + for my $inc ($self->expand_inclusion_($inclusion, $configfile)) { + my @insertion = + $self->_config_from_file($inc, $config, $visited); + splice @config, $pos, 0, @insertion; # insert the inclusion + $pos += @insertion; + } + } + else { + $pos++; + } + } + + $_config_cache->{$config} = \@config; + + return wantarray ? @config : $config[0]; } sub expand_inclusion_ { - my $self = shift; - my $inclusion = shift; - my $context = shift; - my @includes; + my $self = shift; + my $inclusion = shift; + my $context = shift; + my @includes; - if (-d $inclusion) { - $self->log(LOGDEBUG, "inclusion of directory $inclusion from $context"); + if (-d $inclusion) { + $self->log(LOGDEBUG, "inclusion of directory $inclusion from $context"); - if (opendir(INCD, $inclusion)) { - @includes = map { "$inclusion/$_" } - (grep { -f "$inclusion/$_" and !/^\./ } sort readdir INCD); - closedir INCD; - } else { - $self->log(LOGERROR, "Couldn't open directory $inclusion,". - " referenced from $context ($!)"); + if (opendir(INCD, $inclusion)) { + @includes = map { "$inclusion/$_" } + (grep { -f "$inclusion/$_" and !/^\./ } sort readdir INCD); + closedir INCD; + } + else { + $self->log(LOGERROR, + "Couldn't open directory $inclusion," + . " referenced from $context ($!)" + ); + } } - } else { - $self->log(LOGDEBUG, "inclusion of file $inclusion from $context"); - @includes = ( $inclusion ); - } - return @includes; + else { + $self->log(LOGDEBUG, "inclusion of file $inclusion from $context"); + @includes = ($inclusion); + } + return @includes; } - sub load_plugins { - my $self = shift; + my $self = shift; - my @plugins = $self->config('plugins'); - my @loaded; + my @plugins = $self->config('plugins'); + my @loaded; - if ($hooks->{queue}) { - #$self->log(LOGWARN, "Plugins already loaded"); - return @plugins; - } + if ($hooks->{queue}) { - for my $plugin_line (@plugins) { - my $this_plugin = $self->_load_plugin($plugin_line, $self->plugin_dirs); - push @loaded, $this_plugin if $this_plugin; - } + #$self->log(LOGWARN, "Plugins already loaded"); + return @plugins; + } - return @loaded; + for my $plugin_line (@plugins) { + my $this_plugin = $self->_load_plugin($plugin_line, $self->plugin_dirs); + push @loaded, $this_plugin if $this_plugin; + } + + return @loaded; } sub _load_plugin { - my $self = shift; - my ($plugin_line, @plugin_dirs) = @_; + my $self = shift; + my ($plugin_line, @plugin_dirs) = @_; - my ($plugin, @args) = split / /, $plugin_line; + my ($plugin, @args) = split / /, $plugin_line; - my $package; + my $package; - if ($plugin =~ m/::/) { - # "full" package plugin (My::Plugin) - $package = $plugin; - $package =~ s/[^_a-z0-9:]+//gi; - my $eval = qq[require $package;\n] - .qq[sub ${plugin}::plugin_name { '$plugin' }]; - $eval =~ m/(.*)/s; - $eval = $1; - eval $eval; - die "Failed loading $package - eval $@" if $@; - $self->log(LOGDEBUG, "Loading $package ($plugin_line)") - unless $plugin_line =~ /logging/; - } - else { - # regular plugins/$plugin plugin - my $plugin_name = $plugin; - $plugin =~ s/:\d+$//; # after this point, only used for filename + if ($plugin =~ m/::/) { - # Escape everything into valid perl identifiers - $plugin_name =~ s/([^A-Za-z0-9_\/])/sprintf("_%2x",unpack("C",$1))/eg; + # "full" package plugin (My::Plugin) + $package = $plugin; + $package =~ s/[^_a-z0-9:]+//gi; + my $eval = qq[require $package;\n] + . qq[sub ${plugin}::plugin_name { '$plugin' }]; + $eval =~ m/(.*)/s; + $eval = $1; + eval $eval; + die "Failed loading $package - eval $@" if $@; + $self->log(LOGDEBUG, "Loading $package ($plugin_line)") + unless $plugin_line =~ /logging/; + } + else { + # regular plugins/$plugin plugin + my $plugin_name = $plugin; + $plugin =~ s/:\d+$//; # after this point, only used for filename - # second pass cares for slashes and words starting with a digit - $plugin_name =~ s{ + # Escape everything into valid perl identifiers + $plugin_name =~ s/([^A-Za-z0-9_\/])/sprintf("_%2x",unpack("C",$1))/eg; + + # second pass cares for slashes and words starting with a digit + $plugin_name =~ s{ (/+) # directory (\d?) # package's first character }[ "::" . (length $2 ? sprintf("_%2x",unpack("C",$2)) : "") ]egx; - $package = "Qpsmtpd::Plugin::$plugin_name"; + $package = "Qpsmtpd::Plugin::$plugin_name"; - # don't reload plugins if they are already loaded - unless ( defined &{"${package}::plugin_name"} ) { - PLUGIN_DIR: for my $dir (@plugin_dirs) { - if (-e "$dir/$plugin") { - Qpsmtpd::Plugin->compile($plugin_name, $package, - "$dir/$plugin", $self->{_test_mode}, $plugin); - $self->log(LOGDEBUG, "Loading $plugin_line from $dir/$plugin") - unless $plugin_line =~ /logging/; - last PLUGIN_DIR; + # don't reload plugins if they are already loaded + unless (defined &{"${package}::plugin_name"}) { + PLUGIN_DIR: for my $dir (@plugin_dirs) { + if (-e "$dir/$plugin") { + Qpsmtpd::Plugin->compile($plugin_name, $package, + "$dir/$plugin", $self->{_test_mode}, $plugin); + $self->log(LOGDEBUG, + "Loading $plugin_line from $dir/$plugin") + unless $plugin_line =~ /logging/; + last PLUGIN_DIR; + } + } + die "Plugin $plugin_name not found in our plugin dirs (", + join(", ", @plugin_dirs), ")" + unless defined &{"${package}::plugin_name"}; } - } - die "Plugin $plugin_name not found in our plugin dirs (", - join(", ", @plugin_dirs),")" - unless defined &{"${package}::plugin_name"}; } - } - my $plug = $package->new(); - $plug->_register($self, @args); + my $plug = $package->new(); + $plug->_register($self, @args); - return $plug; + return $plug; } -sub transaction { return {}; } # base class implements empty transaction +sub transaction { return {}; } # base class implements empty transaction sub run_hooks { - my ($self, $hook) = (shift, shift); - if ($hooks->{$hook}) { - my @r; - my @local_hooks = @{$hooks->{$hook}}; - $self->{_continuation} = [$hook, [@_], @local_hooks]; - return $self->run_continuation(); - } - return $self->hook_responder($hook, [0, ''], [@_]); + my ($self, $hook) = (shift, shift); + if ($hooks->{$hook}) { + my @r; + my @local_hooks = @{$hooks->{$hook}}; + $self->{_continuation} = [$hook, [@_], @local_hooks]; + return $self->run_continuation(); + } + return $self->hook_responder($hook, [0, ''], [@_]); } sub run_hooks_no_respond { @@ -431,7 +456,9 @@ sub run_hooks_no_respond { my @r; for my $code (@{$hooks->{$hook}}) { eval { (@r) = $code->{code}->($self, $self->transaction, @_); }; - $@ and warn("FATAL PLUGIN ERROR [" . $code->{name} . "]: ", $@) and next; + $@ + and warn("FATAL PLUGIN ERROR [" . $code->{name} . "]: ", $@) + and next; if ($r[0] == YIELD) { die "YIELD not valid from $hook hook"; } @@ -443,125 +470,151 @@ sub run_hooks_no_respond { return (0, ''); } -sub continue_read {} # subclassed in -async +sub continue_read { } # subclassed in -async sub pause_read { die "Continuations only work in qpsmtpd-async" } sub run_continuation { - my $self = shift; - #my $t1 = $SAMPLER->("run_hooks", undef, 1); - die "No continuation in progress" unless $self->{_continuation}; - $self->continue_read(); - my $todo = $self->{_continuation}; - $self->{_continuation} = undef; - my $hook = shift @$todo || die "No hook in the continuation"; - my $args = shift @$todo || die "No hook args in the continuation"; - my @r; - while (@$todo) { - my $code = shift @$todo; - #my $t2 = $SAMPLER->($hook . "_" . $code->{name}, undef, 1); - #warn("Got sampler called: ${hook}_$code->{name}\n"); - $self->varlog(LOGDEBUG, $hook, $code->{name}); - my $tran = $self->transaction; - eval { (@r) = $code->{code}->($self, $tran, @$args); }; - $@ and $self->log(LOGCRIT, "FATAL PLUGIN ERROR [" . $code->{name} . "]: ", $@) and next; + my $self = shift; - !defined $r[0] - and $self->log(LOGERROR, "plugin ".$code->{name} - ." running the $hook hook returned undef!") - and next; + #my $t1 = $SAMPLER->("run_hooks", undef, 1); + die "No continuation in progress" unless $self->{_continuation}; + $self->continue_read(); + my $todo = $self->{_continuation}; + $self->{_continuation} = undef; + my $hook = shift @$todo || die "No hook in the continuation"; + my $args = shift @$todo || die "No hook args in the continuation"; + my @r; - # note this is wrong as $tran is always true in the - # current code... - if ($tran) { - my $tnotes = $tran->notes( $code->{name} ); - $tnotes->{"hook_$hook"}->{'return'} = $r[0] - if (!defined $tnotes || ref $tnotes eq "HASH"); - } - else { - my $cnotes = $self->connection->notes( $code->{name} ); - $cnotes->{"hook_$hook"}->{'return'} = $r[0] - if (!defined $cnotes || ref $cnotes eq "HASH"); - } + while (@$todo) { + my $code = shift @$todo; - if ($r[0] == YIELD) { - $self->pause_read(); - $self->{_continuation} = [$hook, $args, @$todo]; - return @r; - } - elsif ($r[0] == DENY or $r[0] == DENYSOFT or - $r[0] == DENY_DISCONNECT or $r[0] == DENYSOFT_DISCONNECT) - { - $r[1] = "" if not defined $r[1]; - $self->log(LOGDEBUG, "Plugin ".$code->{name}. - ", hook $hook returned ".return_code($r[0]).", $r[1]"); - $self->run_hooks_no_respond("deny", $code->{name}, $r[0], $r[1]) unless ($hook eq "deny"); - } - else { - $r[1] = "" if not defined $r[1]; - $self->log(LOGDEBUG, "Plugin ".$code->{name}. - ", hook $hook returned ".return_code($r[0]).", $r[1]"); - $self->run_hooks_no_respond("ok", $code->{name}, $r[0], $r[1]) unless ($hook eq "ok"); - } + #my $t2 = $SAMPLER->($hook . "_" . $code->{name}, undef, 1); + #warn("Got sampler called: ${hook}_$code->{name}\n"); + $self->varlog(LOGDEBUG, $hook, $code->{name}); + my $tran = $self->transaction; + eval { (@r) = $code->{code}->($self, $tran, @$args); }; + $@ + and + $self->log(LOGCRIT, "FATAL PLUGIN ERROR [" . $code->{name} . "]: ", + $@) + and next; - last unless $r[0] == DECLINED; - } - $r[0] = DECLINED if not defined $r[0]; - # hook_*_parse() may return a CODE ref.. - # ... which breaks when splitting as string: - @r = map { split /\n/ } @r unless (ref($r[1]) eq "CODE"); - return $self->hook_responder($hook, \@r, $args); + !defined $r[0] + and $self->log(LOGERROR, + "plugin " + . $code->{name} + . " running the $hook hook returned undef!" + ) + and next; + + # note this is wrong as $tran is always true in the + # current code... + if ($tran) { + my $tnotes = $tran->notes($code->{name}); + $tnotes->{"hook_$hook"}->{'return'} = $r[0] + if (!defined $tnotes || ref $tnotes eq "HASH"); + } + else { + my $cnotes = $self->connection->notes($code->{name}); + $cnotes->{"hook_$hook"}->{'return'} = $r[0] + if (!defined $cnotes || ref $cnotes eq "HASH"); + } + + if ($r[0] == YIELD) { + $self->pause_read(); + $self->{_continuation} = [$hook, $args, @$todo]; + return @r; + } + elsif ( $r[0] == DENY + or $r[0] == DENYSOFT + or $r[0] == DENY_DISCONNECT + or $r[0] == DENYSOFT_DISCONNECT) + { + $r[1] = "" if not defined $r[1]; + $self->log(LOGDEBUG, + "Plugin " + . $code->{name} + . ", hook $hook returned " + . return_code($r[0]) + . ", $r[1]" + ); + $self->run_hooks_no_respond("deny", $code->{name}, $r[0], $r[1]) + unless ($hook eq "deny"); + } + else { + $r[1] = "" if not defined $r[1]; + $self->log(LOGDEBUG, + "Plugin " + . $code->{name} + . ", hook $hook returned " + . return_code($r[0]) + . ", $r[1]" + ); + $self->run_hooks_no_respond("ok", $code->{name}, $r[0], $r[1]) + unless ($hook eq "ok"); + } + + last unless $r[0] == DECLINED; + } + $r[0] = DECLINED if not defined $r[0]; + + # hook_*_parse() may return a CODE ref.. + # ... which breaks when splitting as string: + @r = map { split /\n/ } @r unless (ref($r[1]) eq "CODE"); + return $self->hook_responder($hook, \@r, $args); } sub hook_responder { - my ($self, $hook, $msg, $args) = @_; + my ($self, $hook, $msg, $args) = @_; - #my $t1 = $SAMPLER->("hook_responder", undef, 1); - my $code = shift @$msg; + #my $t1 = $SAMPLER->("hook_responder", undef, 1); + my $code = shift @$msg; - my $responder = $hook . '_respond'; - if (my $meth = $self->can($responder)) { - return $meth->($self, $code, $msg, $args); - } - return $code, @$msg; + my $responder = $hook . '_respond'; + if (my $meth = $self->can($responder)) { + return $meth->($self, $code, $msg, $args); + } + return $code, @$msg; } sub _register_hook { - my $self = shift; - my ($hook, $code, $unshift) = @_; + my $self = shift; + my ($hook, $code, $unshift) = @_; - if ($unshift) { - unshift @{$hooks->{$hook}}, $code; - } - else { - push @{$hooks->{$hook}}, $code; - } + if ($unshift) { + unshift @{$hooks->{$hook}}, $code; + } + else { + push @{$hooks->{$hook}}, $code; + } } sub spool_dir { - my $self = shift; + my $self = shift; - unless ( $Spool_dir ) { # first time through - $self->log(LOGDEBUG, "Initializing spool_dir"); - $Spool_dir = $self->config('spool_dir') - || Qpsmtpd::Utils::tildeexp('~/tmp/'); + unless ($Spool_dir) { # first time through + $self->log(LOGDEBUG, "Initializing spool_dir"); + $Spool_dir = $self->config('spool_dir') + || Qpsmtpd::Utils::tildeexp('~/tmp/'); - $Spool_dir .= "/" unless ($Spool_dir =~ m!/$!); + $Spool_dir .= "/" unless ($Spool_dir =~ m!/$!); - $Spool_dir =~ /^(.+)$/ or die "spool_dir not configured properly"; - $Spool_dir = $1; # cleanse the taint - my $Spool_perms = $self->config('spool_perms') || '0700'; + $Spool_dir =~ /^(.+)$/ or die "spool_dir not configured properly"; + $Spool_dir = $1; # cleanse the taint + my $Spool_perms = $self->config('spool_perms') || '0700'; - if (! -d $Spool_dir) { # create it if it doesn't exist - mkdir($Spool_dir,oct($Spool_perms)) - or die "Could not create spool_dir $Spool_dir: $!"; - }; - # Make sure the spool dir has appropriate rights - $self->log(LOGWARN, - "Permissions on spool_dir $Spool_dir are not $Spool_perms") - unless ((stat $Spool_dir)[2] & 07777) == oct($Spool_perms); - } + if (!-d $Spool_dir) { # create it if it doesn't exist + mkdir($Spool_dir, oct($Spool_perms)) + or die "Could not create spool_dir $Spool_dir: $!"; + } - return $Spool_dir; + # Make sure the spool dir has appropriate rights + $self->log(LOGWARN, + "Permissions on spool_dir $Spool_dir are not $Spool_perms") + unless ((stat $Spool_dir)[2] & 07777) == oct($Spool_perms); + } + + return $Spool_dir; } # For unique filenames. We write to a local tmp dir so we don't need @@ -569,43 +622,44 @@ sub spool_dir { my $transaction_counter = 0; sub temp_file { - my $self = shift; - my $filename = $self->spool_dir() - . join(":", time, $$, $transaction_counter++); - return $filename; + my $self = shift; + my $filename = + $self->spool_dir() . join(":", time, $$, $transaction_counter++); + return $filename; } sub temp_dir { - my $self = shift; - my $mask = shift || 0700; - my $dirname = $self->temp_file(); - -d $dirname or mkdir($dirname, $mask) - or die "Could not create temporary directory $dirname: $!"; - return $dirname; + my $self = shift; + my $mask = shift || 0700; + my $dirname = $self->temp_file(); + -d $dirname + or mkdir($dirname, $mask) + or die "Could not create temporary directory $dirname: $!"; + return $dirname; } sub size_threshold { - my $self = shift; - unless ( defined $Size_threshold ) { - $Size_threshold = $self->config('size_threshold') || 0; - $self->log(LOGDEBUG, "size_threshold set to $Size_threshold"); - } - return $Size_threshold; + my $self = shift; + unless (defined $Size_threshold) { + $Size_threshold = $self->config('size_threshold') || 0; + $self->log(LOGDEBUG, "size_threshold set to $Size_threshold"); + } + return $Size_threshold; } sub authenticated { - my $self = shift; - return (defined $self->{_auth} ? $self->{_auth} : "" ); + my $self = shift; + return (defined $self->{_auth} ? $self->{_auth} : ""); } sub auth_user { - my $self = shift; - return (defined $self->{_auth_user} ? $self->{_auth_user} : "" ); + my $self = shift; + return (defined $self->{_auth_user} ? $self->{_auth_user} : ""); } sub auth_mechanism { - my $self = shift; - return (defined $self->{_auth_mechanism} ? $self->{_auth_mechanism} : "" ); + my $self = shift; + return (defined $self->{_auth_mechanism} ? $self->{_auth_mechanism} : ""); } 1; diff --git a/lib/Qpsmtpd/Address.pm b/lib/Qpsmtpd/Address.pm index 5800be2..a0f6b50 100644 --- a/lib/Qpsmtpd/Address.pm +++ b/lib/Qpsmtpd/Address.pm @@ -25,9 +25,9 @@ for easy testing of values. =cut use overload ( - '""' => \&format, - 'cmp' => \&_addr_cmp, -); + '""' => \&format, + 'cmp' => \&_addr_cmp, + ); =head2 new() @@ -59,13 +59,13 @@ test for equality (like in badmailfrom). sub new { my ($class, $user, $host) = @_; my $self = {}; - if ($user =~ /^<(.*)>$/ ) { - ($user, $host) = $class->canonify($user); - return undef unless defined $user; + if ($user =~ /^<(.*)>$/) { + ($user, $host) = $class->canonify($user); + return undef unless defined $user; } - elsif ( not defined $host ) { - my $address = $user; - ($user, $host) = $address =~ m/(.*)(?:\@(.*))/; + elsif (not defined $host) { + my $address = $user; + ($user, $host) = $address =~ m/(.*)(?:\@(.*))/; } $self->{_user} = $user; $self->{_host} = $host; @@ -84,35 +84,35 @@ sub new { # At-domain = "@" domain # # Mailbox = Local-part "@" Domain -# +# # Local-part = Dot-string / Quoted-string # ; MAY be case-sensitive -# +# # Dot-string = Atom *("." Atom) -# +# # Atom = 1*atext -# +# # Quoted-string = DQUOTE *qcontent DQUOTE -# +# # Domain = (sub-domain 1*("." sub-domain)) / address-literal # sub-domain = Let-dig [Ldh-str] -# +# # address-literal = "[" IPv4-address-literal / # IPv6-address-literal / # General-address-literal "]" -# +# # IPv4-address-literal = Snum 3("." Snum) # IPv6-address-literal = "IPv6:" IPv6-addr # General-address-literal = Standardized-tag ":" 1*dcontent # Standardized-tag = Ldh-str # ; MUST be specified in a standards-track RFC # ; and registered with IANA -# +# # Snum = 1*3DIGIT ; representing a decimal integer # ; value in the range 0 through 255 # Let-dig = ALPHA / DIGIT # Ldh-str = *( ALPHA / DIGIT / "-" ) Let-dig -# +# # IPv6-addr = IPv6-full / IPv6-comp / IPv6v4-full / IPv6v4-comp # IPv6-hex = 1*4HEXDIG # IPv6-full = IPv6-hex 7(":" IPv6-hex) @@ -127,12 +127,12 @@ sub new { # ; The "::" represents at least 2 16-bit groups of zeros # ; No more than 4 groups in addition to the "::" and # ; IPv4-address-literal may be present -# -# -# +# +# +# # atext and qcontent are not defined in RFC 2821. # From RFC 2822: -# +# # atext = ALPHA / DIGIT / ; Any character except controls, # "!" / "#" / ; SP, and specials. # "$" / "%" / ; Used for atoms @@ -145,21 +145,21 @@ sub new { # "|" / "}" / # "~" # qtext = NO-WS-CTL / ; Non white space controls -# +# # %d33 / ; The rest of the US-ASCII # %d35-91 / ; characters not including "\" # %d93-126 ; or the quote character -# +# # qcontent = qtext / quoted-pair -# +# # NO-WS-CTL = %d1-8 / ; US-ASCII control characters # %d11 / ; that do not include the # %d12 / ; carriage return, line feed, # %d14-31 / ; and white space characters # %d127 -# +# # quoted-pair = ("\" text) / obs-qp -# +# # text = %d1-9 / ; Characters excluding CR and LF # %d11 / # %d12 / @@ -196,8 +196,11 @@ sub canonify { return undef unless ($path =~ /^<(.*)>$/); $path = $1; - my $domain = $domain_expr ? $domain_expr - : "$subdomain_expr(?:\.$subdomain_expr)*"; + my $domain = + $domain_expr + ? $domain_expr + : "$subdomain_expr(?:\.$subdomain_expr)*"; + # it is possible for $address_literal_expr to be empty, if a site # doesn't want to allow them $domain = "(?:$address_literal_expr|$domain)" @@ -216,14 +219,15 @@ sub canonify { return (undef) unless defined $localpart; if ($localpart =~ /^$atom_expr(\.$atom_expr)*/) { + # simple case, we are done return ($localpart, $domainpart); - } + } if ($localpart =~ /^"(($qtext_expr|\\$text_expr)*)"$/) { $localpart = $1; $localpart =~ s/\\($text_expr)/$1/g; return ($localpart, $domainpart); - } + } return (undef); } @@ -234,7 +238,7 @@ to new() called with a single parameter. =cut -sub parse { # retain for compatibility only +sub parse { # retain for compatibility only return shift->new(shift); } @@ -252,14 +256,14 @@ L. sub address { my ($self, $val) = @_; - if ( defined($val) ) { - $val = "<$val>" unless $val =~ /^<.+>$/; - my ($user, $host) = $self->canonify($val); - $self->{_user} = $user; - $self->{_host} = $host; + if (defined($val)) { + $val = "<$val>" unless $val =~ /^<.+>$/; + my ($user, $host) = $self->canonify($val); + $self->{_user} = $user; + $self->{_host} = $host; } - return ( defined $self->{_user} ? $self->{_user} : '' ) - . ( defined $self->{_host} ? '@'.$self->{_host} : '' ); + return (defined $self->{_user} ? $self->{_user} : '') + . (defined $self->{_host} ? '@' . $self->{_host} : ''); } =head2 format() @@ -278,11 +282,12 @@ sub format { my ($self) = @_; my $qchar = '[^a-zA-Z0-9!#\$\%\&\x27\*\+\x2D\/=\?\^_`{\|}~.]'; return '<>' unless defined $self->{_user}; - if ( ( my $user = $self->{_user}) =~ s/($qchar)/\\$1/g) { - return qq(<"$user") - . ( defined $self->{_host} ? '@'.$self->{_host} : '' ). ">"; - } - return "<".$self->address().">"; + if ((my $user = $self->{_user}) =~ s/($qchar)/\\$1/g) { + return + qq(<"$user") + . (defined $self->{_host} ? '@' . $self->{_host} : '') . ">"; + } + return "<" . $self->address() . ">"; } =head2 user([$user]) @@ -326,10 +331,11 @@ use this to pass data between plugins. =cut sub notes { - my ($self,$key) = (shift,shift); - # Check for any additional arguments passed by the caller -- including undef - return $self->{_notes}->{$key} unless @_; - return $self->{_notes}->{$key} = shift; + my ($self, $key) = (shift, shift); + + # Check for any additional arguments passed by the caller -- including undef + return $self->{_notes}->{$key} unless @_; + return $self->{_notes}->{$key} = shift; } sub _addr_cmp { @@ -337,16 +343,16 @@ sub _addr_cmp { my ($left, $right, $swap) = @_; my $class = ref($left); - unless ( UNIVERSAL::isa($right, $class) ) { - $right = $class->new($right); + unless (UNIVERSAL::isa($right, $class)) { + $right = $class->new($right); } - #invert the address so we can sort by domain then user - ($left = join( '=', reverse( split(/@/, $left->format))) ) =~ tr/[<>]//d; - ($right = join( '=', reverse( split(/@/,$right->format))) ) =~ tr/[<>]//d; + #invert the address so we can sort by domain then user + ($left = join('=', reverse(split(/@/, $left->format)))) =~ tr/[<>]//d; + ($right = join('=', reverse(split(/@/, $right->format)))) =~ tr/[<>]//d; - if ( $swap ) { - ($right, $left) = ($left, $right); + if ($swap) { + ($right, $left) = ($left, $right); } return ($left cmp $right); diff --git a/lib/Qpsmtpd/Auth.pm b/lib/Qpsmtpd/Auth.pm index 509069c..c0a03e1 100644 --- a/lib/Qpsmtpd/Auth.pm +++ b/lib/Qpsmtpd/Auth.pm @@ -1,5 +1,6 @@ package Qpsmtpd::Auth; -# See the documentation in 'perldoc docs/authentication.pod' + +# See the documentation in 'perldoc docs/authentication.pod' use strict; use warnings; @@ -10,167 +11,172 @@ use Digest::HMAC_MD5 qw(hmac_md5_hex); use MIME::Base64; sub e64 { - my ($arg) = @_; - my $res = encode_base64($arg); - chomp($res); - return($res); + my ($arg) = @_; + my $res = encode_base64($arg); + chomp($res); + return ($res); } sub SASL { # $DB::single = 1; - my ( $session, $mechanism, $prekey ) = @_; - my ( $user, $passClear, $passHash, $ticket, $loginas ); + my ($session, $mechanism, $prekey) = @_; + my ($user, $passClear, $passHash, $ticket, $loginas); - if ( $mechanism eq 'plain' ) { - ($loginas, $user, $passClear) = get_auth_details_plain($session,$prekey); - return DECLINED if ! $user || ! $passClear; + if ($mechanism eq 'plain') { + ($loginas, $user, $passClear) = + get_auth_details_plain($session, $prekey); + return DECLINED if !$user || !$passClear; } - elsif ( $mechanism eq 'login' ) { - ($user, $passClear) = get_auth_details_login($session,$prekey); - return DECLINED if ! $user || ! $passClear; + elsif ($mechanism eq 'login') { + ($user, $passClear) = get_auth_details_login($session, $prekey); + return DECLINED if !$user || !$passClear; } - elsif ( $mechanism eq 'cram-md5' ) { - ( $ticket, $user, $passHash ) = get_auth_details_cram_md5($session); - return DECLINED if ! $user || ! $passHash; + elsif ($mechanism eq 'cram-md5') { + ($ticket, $user, $passHash) = get_auth_details_cram_md5($session); + return DECLINED if !$user || !$passHash; } else { #this error is now caught in SMTP.pm's sub auth - $session->respond( 500, "Internal server error" ); + $session->respond(500, "Internal server error"); return DECLINED; } # try running the specific hooks first - my ( $rc, $msg ) = - $session->run_hooks( "auth-$mechanism", $mechanism, $user, $passClear, - $passHash, $ticket ); + my ($rc, $msg) = + $session->run_hooks("auth-$mechanism", $mechanism, $user, $passClear, + $passHash, $ticket); # try running the polymorphous hooks next - if ( !$rc || $rc == DECLINED ) { - ( $rc, $msg ) = - $session->run_hooks( "auth", $mechanism, $user, $passClear, - $passHash, $ticket ); + if (!$rc || $rc == DECLINED) { + ($rc, $msg) = + $session->run_hooks("auth", $mechanism, $user, + $passClear, $passHash, $ticket); } - if ( $rc == OK ) { - $msg = uc($mechanism) . " authentication successful for $user" . - ( $msg ? " - $msg" : ''); - $session->respond( 235, $msg ); + if ($rc == OK) { + $msg = + uc($mechanism) + . " authentication successful for $user" + . ($msg ? " - $msg" : ''); + $session->respond(235, $msg); $session->connection->relay_client(1); - if ( $session->connection->notes('naughty' ) ) { - $session->log( LOGINFO, "auth success cleared naughty" ); - $session->connection->notes('naughty',0); - }; - $session->log( LOGDEBUG, $msg ); # already logged by $session->respond + if ($session->connection->notes('naughty')) { + $session->log(LOGINFO, "auth success cleared naughty"); + $session->connection->notes('naughty', 0); + } + $session->log(LOGDEBUG, $msg); # already logged by $session->respond - $session->{_auth_user} = $user; + $session->{_auth_user} = $user; $session->{_auth_mechanism} = $mechanism; - s/[\r\n].*//s for ($session->{_auth_user}, $session->{_auth_mechanism}); + s/[\r\n].*//s for ($session->{_auth_user}, $session->{_auth_mechanism}); return OK; } else { - $msg = uc($mechanism) . " authentication failed for $user" . - ( $msg ? " - $msg" : ''); - $session->respond( 535, $msg ); - $session->log( LOGDEBUG, $msg ); # already logged by $session->respond + $msg = + uc($mechanism) + . " authentication failed for $user" + . ($msg ? " - $msg" : ''); + $session->respond(535, $msg); + $session->log(LOGDEBUG, $msg); # already logged by $session->respond return DENY; } } sub get_auth_details_plain { - my ( $session, $prekey ) = @_; + my ($session, $prekey) = @_; - if ( ! $prekey) { - $session->respond( 334, ' ' ); - $prekey= ; + if (!$prekey) { + $session->respond(334, ' '); + $prekey = ; } - my ( $loginas, $user, $passClear ) = split /\x0/, decode_base64($prekey); + my ($loginas, $user, $passClear) = split /\x0/, decode_base64($prekey); - if ( ! $user ) { - if ( $loginas ) { + if (!$user) { + if ($loginas) { $session->respond(535, "Authentication invalid ($loginas)"); } else { $session->respond(535, "Authentication invalid"); } return; - }; + } # Authorization ID must not be different from Authentication ID - if ( $loginas ne '' && $loginas ne $user ) { + if ($loginas ne '' && $loginas ne $user) { $session->respond(535, "Authentication invalid for $user"); return; } return ($loginas, $user, $passClear); -}; +} sub get_auth_details_login { - my ( $session, $prekey ) = @_; + my ($session, $prekey) = @_; my $user; - if ( $prekey ) { + if ($prekey) { $user = decode_base64($prekey); } else { - $user = get_base64_response($session,'Username:') or return; + $user = get_base64_response($session, 'Username:') or return; } - my $passClear = get_base64_response($session,'Password:') or return; + my $passClear = get_base64_response($session, 'Password:') or return; return ($user, $passClear); -}; +} sub get_auth_details_cram_md5 { - my ( $session, $ticket ) = @_; + my ($session, $ticket) = @_; - if ( ! $ticket ) { # ticket is only passed in during testing - # rand() is not cryptographic, but we only need to generate a globally - # unique number. The rand() is there in case the user logs in more than - # once in the same second, or if the clock is skewed. - $ticket = sprintf( '<%x.%x@%s>', - rand(1000000), time(), $session->config('me') ); - }; + if (!$ticket) { # ticket is only passed in during testing + # rand() is not cryptographic, but we only need to generate a globally + # unique number. The rand() is there in case the user logs in more than + # once in the same second, or if the clock is skewed. + $ticket = + sprintf('<%x.%x@%s>', rand(1000000), time(), $session->config('me')); + } # send the base64 encoded ticket - $session->respond( 334, encode_base64( $ticket, '' ) ); + $session->respond(334, encode_base64($ticket, '')); my $line = ; - if ( $line eq '*' ) { - $session->respond( 501, "Authentication canceled" ); + if ($line eq '*') { + $session->respond(501, "Authentication canceled"); return; - }; + } - my ( $user, $passHash ) = split( / /, decode_base64($line) ); - unless ( $user && $passHash ) { + my ($user, $passHash) = split(/ /, decode_base64($line)); + unless ($user && $passHash) { $session->respond(504, "Invalid authentication string"); return; } $session->{auth}{ticket} = $ticket; return ($ticket, $user, $passHash); -}; +} sub get_base64_response { my ($session, $question) = @_; $session->respond(334, e64($question)); - my $answer = decode_base64( ); + my $answer = decode_base64(); if ($answer eq '*') { $session->respond(501, "Authentication canceled"); return; } return $answer; -}; +} sub validate_password { - my ( $self, %a ) = @_; + my ($self, %a) = @_; my ($pkg, $file, $line) = caller(); - $file = (split /\//, $file)[-1]; # strip off the path + $file = (split /\//, $file)[-1]; # strip off the path my $src_clear = $a{src_clear}; my $src_crypt = $a{src_crypt}; @@ -180,43 +186,43 @@ sub validate_password { my $ticket = $a{ticket} || $self->{auth}{ticket}; my $deny = $a{deny} || DENY; - if ( ! $src_crypt && ! $src_clear ) { + if (!$src_crypt && !$src_clear) { $self->log(LOGINFO, "fail: missing password"); - return ( $deny, "$file - no such user" ); - }; - - if ( ! $src_clear && $method =~ /CRAM-MD5/i ) { - $self->log(LOGINFO, "skip: cram-md5 not supported w/o clear pass"); - return ( DECLINED, $file ); + return ($deny, "$file - no such user"); } - if ( defined $attempt_clear ) { - if ( $src_clear && $src_clear eq $attempt_clear ) { + if (!$src_clear && $method =~ /CRAM-MD5/i) { + $self->log(LOGINFO, "skip: cram-md5 not supported w/o clear pass"); + return (DECLINED, $file); + } + + if (defined $attempt_clear) { + if ($src_clear && $src_clear eq $attempt_clear) { $self->log(LOGINFO, "pass: clear match"); - return ( OK, $file ); - }; - - if ( $src_crypt && $src_crypt eq crypt( $attempt_clear, $src_crypt ) ) { - $self->log(LOGINFO, "pass: crypt match"); - return ( OK, $file ); + return (OK, $file); } - }; - if ( defined $attempt_hash && $src_clear ) { - if ( ! $ticket ) { + if ($src_crypt && $src_crypt eq crypt($attempt_clear, $src_crypt)) { + $self->log(LOGINFO, "pass: crypt match"); + return (OK, $file); + } + } + + if (defined $attempt_hash && $src_clear) { + if (!$ticket) { $self->log(LOGERROR, "skip: missing ticket"); - return ( DECLINED, $file ); - }; + return (DECLINED, $file); + } - if ( $attempt_hash eq hmac_md5_hex( $ticket, $src_clear ) ) { + if ($attempt_hash eq hmac_md5_hex($ticket, $src_clear)) { $self->log(LOGINFO, "pass: hash match"); - return ( OK, $file ); - }; - }; + return (OK, $file); + } + } $self->log(LOGINFO, "fail: wrong password"); - return ( $deny, "$file - wrong password" ); -}; + return ($deny, "$file - wrong password"); +} # tag: qpsmtpd plugin that sets RELAYCLIENT when the user authenticates diff --git a/lib/Qpsmtpd/Command.pm b/lib/Qpsmtpd/Command.pm index e48c0f2..29a0f63 100644 --- a/lib/Qpsmtpd/Command.pm +++ b/lib/Qpsmtpd/Command.pm @@ -60,8 +60,8 @@ use vars qw(@ISA); @ISA = qw(Qpsmtpd::SMTP); sub parse { - my ($me,$cmd,$line,$sub) = @_; - return (OK) unless defined $line; # trivial case + my ($me, $cmd, $line, $sub) = @_; + return (OK) unless defined $line; # trivial case my $self = {}; bless $self, $me; $cmd = lc $cmd; @@ -77,28 +77,29 @@ sub parse { ## } ## $self->log(LOGDEBUG, "parse($cmd) => [".join("], [", @log)."]"); return @ret; - } + } my $parse = "parse_$cmd"; if ($self->can($parse)) { + # print "CMD=$cmd,line=$line\n"; my @out = eval { $self->$parse($cmd, $line); }; if ($@) { $self->log(LOGERROR, "$parse($cmd,$line) failed: $@"); - return(DENY, "Failed to parse line"); + return (DENY, "Failed to parse line"); } return @out; } - return(OK, split(/ +/, $line)); # default :) + return (OK, split(/ +/, $line)); # default :) } sub parse_rcpt { - my ($self,$cmd,$line) = @_; + my ($self, $cmd, $line) = @_; return (DENY, "Syntax error in command") unless $line =~ s/^to:\s*//i; return &_get_mail_params($cmd, $line); } sub parse_mail { - my ($self,$cmd,$line) = @_; + my ($self, $cmd, $line) = @_; return (DENY, "Syntax error in command") unless $line =~ s/^from:\s*//i; return &_get_mail_params($cmd, $line); } @@ -121,7 +122,7 @@ sub parse_mail { ## inner-esmtp-cmd ::= ("MAIL FROM:" reverse-path) / ## ("RCPT TO:" forward-path) sub _get_mail_params { - my ($cmd,$line) = @_; + my ($cmd, $line) = @_; my @params = (); $line =~ s/\s*$//; @@ -130,36 +131,37 @@ sub _get_mail_params { } @params = reverse @params; - # the above will "fail" (i.e. all of the line in @params) on + # the above will "fail" (i.e. all of the line in @params) on # some addresses without <> like # MAIL FROM: user=name@example.net # or RCPT TO: postmaster # let's see if $line contains nothing and use the first value as address: if ($line) { - # parameter syntax error, i.e. not all of the arguments were + + # parameter syntax error, i.e. not all of the arguments were # stripped by the while() loop: return (DENY, "Syntax error in parameters") - if ($line =~ /\@.*\s/); + if ($line =~ /\@.*\s/); return (OK, $line, @params); } - $line = shift @params; + $line = shift @params; if ($cmd eq "mail") { - return (OK, "<>") unless $line; # 'MAIL FROM:' --> 'MAIL FROM:<>' - return (DENY, "Syntax error in parameters") - if ($line =~ /\@.*\s/); # parameter syntax error + return (OK, "<>") unless $line; # 'MAIL FROM:' --> 'MAIL FROM:<>' + return (DENY, "Syntax error in parameters") + if ($line =~ /\@.*\s/); # parameter syntax error } else { if ($line =~ /\@/) { - return (DENY, "Syntax error in parameters") + return (DENY, "Syntax error in parameters") if ($line =~ /\@.*\s/); - } + } else { # XXX: what about 'abuse' in Qpsmtpd::Address? return (DENY, "Syntax error in parameters") if $line =~ /\s/; - return (DENY, "Syntax error in address") - unless ($line =~ /^(postmaster|abuse)$/i); + return (DENY, "Syntax error in address") + unless ($line =~ /^(postmaster|abuse)$/i); } } ## XXX: No: let this do a plugin, so it's not up to us to decide diff --git a/lib/Qpsmtpd/ConfigServer.pm b/lib/Qpsmtpd/ConfigServer.pm index a112545..16d2d12 100644 --- a/lib/Qpsmtpd/ConfigServer.pm +++ b/lib/Qpsmtpd/ConfigServer.pm @@ -6,38 +6,38 @@ use Qpsmtpd::Constants; use strict; use fields qw( - _auth - _commands - _config_cache - _connection - _transaction - _test_mode - _extras - other_fds -); + _auth + _commands + _config_cache + _connection + _transaction + _test_mode + _extras + other_fds + ); my $PROMPT = "Enter command: "; sub new { my Qpsmtpd::ConfigServer $self = shift; - + $self = fields::new($self) unless ref $self; - $self->SUPER::new( @_ ); + $self->SUPER::new(@_); $self->write($PROMPT); return $self; } -sub max_idle_time { 3600 } # one hour +sub max_idle_time { 3600 } # one hour sub process_line { my $self = shift; my $line = shift || return; - if ($::DEBUG > 1) { print "$$:".($self+0)."C($self->{mode}): $line"; } + if ($::DEBUG > 1) { print "$$:" . ($self + 0) . "C($self->{mode}): $line"; } local $SIG{ALRM} = sub { my ($pkg, $file, $line) = caller(); die "ALARM: $pkg, $file, $line"; }; - my $prev = alarm(2); # must process a command in < 2 seconds + my $prev = alarm(2); # must process a command in < 2 seconds my $resp = eval { $self->_process_line($line) }; alarm($prev); if ($@) { @@ -56,11 +56,11 @@ sub respond { } sub fault { - my $self = shift; - my ($msg) = shift || "program fault - command not performed"; - print STDERR "$0 [$$]: $msg ($!)\n"; - $self->respond("Error - " . $msg); - return $PROMPT; + my $self = shift; + my ($msg) = shift || "program fault - command not performed"; + print STDERR "$0 [$$]: $msg ($!)\n"; + $self->respond("Error - " . $msg); + return $PROMPT; } sub _process_line { @@ -71,9 +71,7 @@ sub _process_line { my ($cmd, @params) = split(/ +/, $line); my $meth = "cmd_" . lc($cmd); if (my $lookup = $self->can($meth)) { - my $resp = eval { - $lookup->($self, @params); - }; + my $resp = eval { $lookup->($self, @params); }; if ($@) { my $error = $@; chomp($error); @@ -89,28 +87,33 @@ sub _process_line { } my %helptext = ( - help => "HELP [CMD] - Get help on all commands or a specific command", + help => "HELP [CMD] - Get help on all commands or a specific command", status => "STATUS - Returns status information about current connections", - list => "LIST [LIMIT] - List the connections, specify limit or negative limit to shrink list", - kill => "KILL (\$IP | \$REF) - Disconnect all connections from \$IP or connection reference \$REF", - pause => "PAUSE - Stop accepting new connections", + list => +"LIST [LIMIT] - List the connections, specify limit or negative limit to shrink list", + kill => +"KILL (\$IP | \$REF) - Disconnect all connections from \$IP or connection reference \$REF", + pause => "PAUSE - Stop accepting new connections", continue => "CONTINUE - Resume accepting connections", - reload => "RELOAD - Reload all plugins and config", - quit => "QUIT - Exit the config server", - ); + reload => "RELOAD - Reload all plugins and config", + quit => "QUIT - Exit the config server", +); sub cmd_help { my $self = shift; my ($subcmd) = @_; - + $subcmd ||= 'help'; $subcmd = lc($subcmd); - + if ($subcmd eq 'help') { - my $txt = join("\n", map { substr($_, 0, index($_, "-")) } sort values(%helptext)); + my $txt = join("\n", + map { substr($_, 0, index($_, "-")) } + sort values(%helptext)); return "Available Commands:\n\n$txt\n"; } - my $txt = $helptext{$subcmd} || "Unrecognised help option. Try 'help' for a full list."; + my $txt = $helptext{$subcmd} + || "Unrecognised help option. Try 'help' for a full list."; return "$txt\n"; } @@ -125,47 +128,48 @@ sub cmd_shutdown { sub cmd_pause { my $self = shift; - + my $other_fds = $self->OtherFds; - - $self->{other_fds} = { %$other_fds }; + + $self->{other_fds} = {%$other_fds}; %$other_fds = (); return "PAUSED"; } sub cmd_continue { my $self = shift; - + my $other_fds = $self->{other_fds}; - - $self->OtherFds( %$other_fds ); + + $self->OtherFds(%$other_fds); %$other_fds = (); return "UNPAUSED"; } sub cmd_status { my $self = shift; - -# Status should show: -# - Total time running -# - Total number of mails received -# - Total number of mails rejected (5xx) -# - Total number of mails tempfailed (5xx) -# - Avg number of mails/minute -# - Number of current connections -# - Number of outstanding DNS queries - + + # Status should show: + # - Total time running + # - Total number of mails received + # - Total number of mails rejected (5xx) + # - Total number of mails tempfailed (5xx) + # - Avg number of mails/minute + # - Number of current connections + # - Number of outstanding DNS queries + my $output = "Current Status as of " . gmtime() . " GMT\n\n"; - + if (defined &Qpsmtpd::Plugin::stats::get_stats) { + # Stats plugin is loaded $output .= Qpsmtpd::Plugin::stats->get_stats; } - + my $descriptors = Danga::Socket->DescriptorMap; - + my $current_connections = 0; - my $current_dns = 0; + my $current_dns = 0; foreach my $fd (keys %$descriptors) { my $pob = $descriptors->{$fd}; if ($pob->isa("Qpsmtpd::PollServer")) { @@ -175,99 +179,109 @@ sub cmd_status { $current_dns = $pob->pending; } } - - $output .= "Curr Connections: $current_connections / $::MAXconn\n". - "Curr DNS Queries: $current_dns"; - + + $output .= "Curr Connections: $current_connections / $::MAXconn\n" + . "Curr DNS Queries: $current_dns"; + return $output; } sub cmd_list { my $self = shift; my ($count) = @_; - + my $descriptors = Danga::Socket->DescriptorMap; - - my $list = "Current" . ($count ? (($count > 0) ? " Oldest $count" : " Newest ".-$count) : "") . " Connections: \n\n"; + + my $list = + "Current" + . ($count ? (($count > 0) ? " Oldest $count" : " Newest " . -$count) : "") + . " Connections: \n\n"; my @all; foreach my $fd (keys %$descriptors) { my $pob = $descriptors->{$fd}; if ($pob->isa("Qpsmtpd::PollServer")) { - next unless $pob->connection->remote_ip; # haven't even started yet - push @all, [$pob+0, $pob->connection->remote_ip, - $pob->connection->remote_host, $pob->uptime]; + next unless $pob->connection->remote_ip; # haven't even started yet + push @all, + [ + $pob + 0, $pob->connection->remote_ip, + $pob->connection->remote_host, $pob->uptime + ]; } } - + @all = sort { $a->[3] <=> $b->[3] } @all; if ($count) { if ($count > 0) { - @all = @all[$#all-($count-1) .. $#all]; + @all = @all[$#all - ($count - 1) .. $#all]; } else { - @all = @all[0..(abs($count) - 1)]; + @all = @all[0 .. (abs($count) - 1)]; } } foreach my $item (@all) { - $list .= sprintf("%x : %s [%s] Connected %0.2fs\n", map { defined()?$_:'' } @$item); + $list .= sprintf("%x : %s [%s] Connected %0.2fs\n", + map { defined() ? $_ : '' } @$item); } - + return $list; } sub cmd_kill { my $self = shift; my ($match) = @_; - + return "SYNTAX: KILL (\$IP | \$REF)\n" unless $match; - + my $descriptors = Danga::Socket->DescriptorMap; - + my $killed = 0; my $is_ip = (index($match, '.') >= 0); foreach my $fd (keys %$descriptors) { my $pob = $descriptors->{$fd}; if ($pob->isa("Qpsmtpd::PollServer")) { if ($is_ip) { - next unless $pob->connection->remote_ip; # haven't even started yet + next + unless $pob->connection->remote_ip; # haven't even started yet if ($pob->connection->remote_ip eq $match) { - $pob->write("550 Your connection has been killed by an administrator\r\n"); + $pob->write( +"550 Your connection has been killed by an administrator\r\n"); $pob->disconnect; $killed++; } } else { # match by ID - if ($pob+0 == hex($match)) { - $pob->write("550 Your connection has been killed by an administrator\r\n"); + if ($pob + 0 == hex($match)) { + $pob->write( +"550 Your connection has been killed by an administrator\r\n"); $pob->disconnect; $killed++; } } } } - + return "Killed $killed connection" . ($killed > 1 ? "s" : "") . "\n"; } sub cmd_dump { my $self = shift; my ($ref) = @_; - + return "SYNTAX: DUMP \$REF\n" unless $ref; require Data::Dumper; - $Data::Dumper::Indent=1; - + $Data::Dumper::Indent = 1; + my $descriptors = Danga::Socket->DescriptorMap; foreach my $fd (keys %$descriptors) { my $pob = $descriptors->{$fd}; if ($pob->isa("Qpsmtpd::PollServer")) { - if ($pob+0 == hex($ref)) { + if ($pob + 0 == hex($ref)) { return Data::Dumper::Dumper($pob); } } } - + return "Unable to find the connection: $ref. Try the LIST command\n"; } diff --git a/lib/Qpsmtpd/Connection.pm b/lib/Qpsmtpd/Connection.pm index 99b7b38..0efa829 100644 --- a/lib/Qpsmtpd/Connection.pm +++ b/lib/Qpsmtpd/Connection.pm @@ -1,123 +1,124 @@ package Qpsmtpd::Connection; use strict; -# All of these parameters depend only on the physical connection, +# All of these parameters depend only on the physical connection, # i.e. not on anything sent from the remote machine. Hence, they # are an appropriate set to use for either start() or clone(). Do # not add parameters here unless they also meet that criteria. my @parameters = qw( - remote_host - remote_ip - remote_info - remote_port - local_ip - local_port - relay_client -); - + remote_host + remote_ip + remote_info + remote_port + local_ip + local_port + relay_client + ); sub new { - my $proto = shift; - my $class = ref($proto) || $proto; - my $self = {}; - bless ($self, $class); + my $proto = shift; + my $class = ref($proto) || $proto; + my $self = {}; + bless($self, $class); } sub start { - my $self = shift; - $self = $self->new(@_) unless ref $self; + my $self = shift; + $self = $self->new(@_) unless ref $self; - my %args = @_; + my %args = @_; - foreach my $f ( @parameters ) { - $self->$f($args{$f}) if $args{$f}; - } + foreach my $f (@parameters) { + $self->$f($args{$f}) if $args{$f}; + } - return $self; + return $self; } sub clone { - my $self = shift; - my %args = @_; - my $new = $self->new(); - foreach my $f ( @parameters ) { - $new->$f($self->$f()) if $self->$f(); - } - $new->{_notes} = $self->{_notes} if defined $self->{_notes}; - # reset the old connection object like it's done at the end of a connection - # to prevent leaks (like prefork/tls problem with the old SSL file handle - # still around) - $self->reset unless $args{no_reset}; - # should we generate a new id here? - return $new; + my $self = shift; + my %args = @_; + my $new = $self->new(); + foreach my $f (@parameters) { + $new->$f($self->$f()) if $self->$f(); + } + $new->{_notes} = $self->{_notes} if defined $self->{_notes}; + + # reset the old connection object like it's done at the end of a connection + # to prevent leaks (like prefork/tls problem with the old SSL file handle + # still around) + $self->reset unless $args{no_reset}; + + # should we generate a new id here? + return $new; } sub remote_host { - my $self = shift; - @_ and $self->{_remote_host} = shift; - $self->{_remote_host}; + my $self = shift; + @_ and $self->{_remote_host} = shift; + $self->{_remote_host}; } sub remote_ip { - my $self = shift; - @_ and $self->{_remote_ip} = shift; - $self->{_remote_ip}; + my $self = shift; + @_ and $self->{_remote_ip} = shift; + $self->{_remote_ip}; } sub remote_port { - my $self = shift; - @_ and $self->{_remote_port} = shift; - $self->{_remote_port}; + my $self = shift; + @_ and $self->{_remote_port} = shift; + $self->{_remote_port}; } sub local_ip { - my $self = shift; - @_ and $self->{_local_ip} = shift; - $self->{_local_ip}; + my $self = shift; + @_ and $self->{_local_ip} = shift; + $self->{_local_ip}; } sub local_port { - my $self = shift; - @_ and $self->{_local_port} = shift; - $self->{_local_port}; + my $self = shift; + @_ and $self->{_local_port} = shift; + $self->{_local_port}; } - sub remote_info { - my $self = shift; - @_ and $self->{_remote_info} = shift; - $self->{_remote_info}; + my $self = shift; + @_ and $self->{_remote_info} = shift; + $self->{_remote_info}; } sub relay_client { - my $self = shift; - @_ and $self->{_relay_client} = shift; - $self->{_relay_client}; + my $self = shift; + @_ and $self->{_relay_client} = shift; + $self->{_relay_client}; } sub hello { - my $self = shift; - @_ and $self->{_hello} = shift; - $self->{_hello}; + my $self = shift; + @_ and $self->{_hello} = shift; + $self->{_hello}; } sub hello_host { - my $self = shift; - @_ and $self->{_hello_host} = shift; - $self->{_hello_host}; + my $self = shift; + @_ and $self->{_hello_host} = shift; + $self->{_hello_host}; } sub notes { - my ($self,$key) = (shift,shift); - # Check for any additional arguments passed by the caller -- including undef - return $self->{_notes}->{$key} unless @_; - return $self->{_notes}->{$key} = shift; + my ($self, $key) = (shift, shift); + + # Check for any additional arguments passed by the caller -- including undef + return $self->{_notes}->{$key} unless @_; + return $self->{_notes}->{$key} = shift; } sub reset { - my $self = shift; - $self->{_notes} = undef; - $self = $self->new; + my $self = shift; + $self->{_notes} = undef; + $self = $self->new; } 1; diff --git a/lib/Qpsmtpd/Constants.pm b/lib/Qpsmtpd/Constants.pm index ccd8440..03f0e84 100644 --- a/lib/Qpsmtpd/Constants.pm +++ b/lib/Qpsmtpd/Constants.pm @@ -4,64 +4,64 @@ require Exporter; # log levels my %log_levels = ( - LOGDEBUG => 7, - LOGINFO => 6, - LOGNOTICE => 5, - LOGWARN => 4, - LOGERROR => 3, - LOGCRIT => 2, - LOGALERT => 1, - LOGEMERG => 0, - LOGRADAR => 0, -); + LOGDEBUG => 7, + LOGINFO => 6, + LOGNOTICE => 5, + LOGWARN => 4, + LOGERROR => 3, + LOGCRIT => 2, + LOGALERT => 1, + LOGEMERG => 0, + LOGRADAR => 0, + ); # return codes my %return_codes = ( - OK => 900, - DENY => 901, # 550 - DENYSOFT => 902, # 450 - DENYHARD => 903, # 550 + disconnect (deprecated in 0.29) - DENY_DISCONNECT => 903, # 550 + disconnect - DENYSOFT_DISCONNECT => 904, # 450 + disconnect - DECLINED => 909, - DONE => 910, - CONTINUATION => 911, # deprecated - use YIELD - YIELD => 911, -); + OK => 900, + DENY => 901, # 550 + DENYSOFT => 902, # 450 + DENYHARD => 903, # 550 + disconnect (deprecated in 0.29) + DENY_DISCONNECT => 903, # 550 + disconnect + DENYSOFT_DISCONNECT => 904, # 450 + disconnect + DECLINED => 909, + DONE => 910, + CONTINUATION => 911, # deprecated - use YIELD + YIELD => 911, + ); use vars qw(@ISA @EXPORT); @ISA = qw(Exporter); @EXPORT = (keys(%return_codes), keys(%log_levels), "return_code", "log_level"); -foreach (keys %return_codes ) { - eval "use constant $_ => ".$return_codes{$_}; +foreach (keys %return_codes) { + eval "use constant $_ => " . $return_codes{$_}; } -foreach (keys %log_levels ) { - eval "use constant $_ => ".$log_levels{$_}; +foreach (keys %log_levels) { + eval "use constant $_ => " . $log_levels{$_}; } sub return_code { my $test = shift; - if ( $test =~ /^\d+$/ ) { # need to return the textural form - foreach ( keys %return_codes ) { - return $_ if $return_codes{$_} =~ /$test/; - } + if ($test =~ /^\d+$/) { # need to return the textural form + foreach (keys %return_codes) { + return $_ if $return_codes{$_} =~ /$test/; + } } - else { # just return the numeric value - return $return_codes{$test}; + else { # just return the numeric value + return $return_codes{$test}; } } sub log_level { my $test = shift; - if ( $test =~ /^\d+$/ ) { # need to return the textural form - foreach ( keys %log_levels ) { - return $_ if $log_levels{$_} =~ /$test/; - } + if ($test =~ /^\d+$/) { # need to return the textural form + foreach (keys %log_levels) { + return $_ if $log_levels{$_} =~ /$test/; + } } - else { # just return the numeric value - return $log_levels{$test}; + else { # just return the numeric value + return $log_levels{$test}; } } diff --git a/lib/Qpsmtpd/DSN.pm b/lib/Qpsmtpd/DSN.pm index d446edd..5439f0d 100644 --- a/lib/Qpsmtpd/DSN.pm +++ b/lib/Qpsmtpd/DSN.pm @@ -48,95 +48,95 @@ than the RFC message. =cut my @rfc1893 = ( - [ - "Other or Undefined Status", # x.0.x + [ + "Other or Undefined Status", # x.0.x ], [ - "Other address status.", # x.1.0 - "Bad destination mailbox address.", # x.1.1 - "Bad destination system address.", # x.1.2 - "Bad destination mailbox address syntax.", # x.1.3 - "Destination mailbox address ambiguous.", # x.1.4 - "Destination address valid.", # x.1.5 - "Destination mailbox has moved, No forwarding address.", # x.1.6 - "Bad sender's mailbox address syntax.", # x.1.7 - "Bad sender's system address.", # x.1.8 + "Other address status.", # x.1.0 + "Bad destination mailbox address.", # x.1.1 + "Bad destination system address.", # x.1.2 + "Bad destination mailbox address syntax.", # x.1.3 + "Destination mailbox address ambiguous.", # x.1.4 + "Destination address valid.", # x.1.5 + "Destination mailbox has moved, No forwarding address.", # x.1.6 + "Bad sender's mailbox address syntax.", # x.1.7 + "Bad sender's system address.", # x.1.8 ], [ - "Other or undefined mailbox status.", # x.2.0 - "Mailbox disabled, not accepting messages.", # x.2.1 - "Mailbox full.", # x.2.2 - "Message length exceeds administrative limit.", # x.2.3 - "Mailing list expansion problem.", # x.2.4 + "Other or undefined mailbox status.", # x.2.0 + "Mailbox disabled, not accepting messages.", # x.2.1 + "Mailbox full.", # x.2.2 + "Message length exceeds administrative limit.", # x.2.3 + "Mailing list expansion problem.", # x.2.4 ], [ - "Other or undefined mail system status.", # x.3.0 - "Mail system full.", # x.3.1 - "System not accepting network messages.", # x.3.2 - "System not capable of selected features.", # x.3.3 - "Message too big for system.", # x.3.4 - "System incorrectly configured.", # x.3.5 - ], - [ - "Other or undefined network or routing status.", # x.4.0 - "No answer from host.", # x.4.1 - "Bad connection.", # x.4.2 - "Directory server failure.", # x.4.3 - "Unable to route.", # x.4.4 - "Mail system congestion.", # x.4.5 - "Routing loop detected.", # x.4.6 - "Delivery time expired.", # x.4.7 + "Other or undefined mail system status.", # x.3.0 + "Mail system full.", # x.3.1 + "System not accepting network messages.", # x.3.2 + "System not capable of selected features.", # x.3.3 + "Message too big for system.", # x.3.4 + "System incorrectly configured.", # x.3.5 ], [ - "Other or undefined protocol status.", # x.5.0 - "Invalid command.", # x.5.1 - "Syntax error.", # x.5.2 - "Too many recipients.", # x.5.3 - "Invalid command arguments.", # x.5.4 - "Wrong protocol version.", # x.5.5 + "Other or undefined network or routing status.", # x.4.0 + "No answer from host.", # x.4.1 + "Bad connection.", # x.4.2 + "Directory server failure.", # x.4.3 + "Unable to route.", # x.4.4 + "Mail system congestion.", # x.4.5 + "Routing loop detected.", # x.4.6 + "Delivery time expired.", # x.4.7 ], [ - "Other or undefined media error.", # x.6.0 - "Media not supported.", # x.6.1 - "Conversion required and prohibited.", # x.6.2 - "Conversion required but not supported.", # x.6.3 - "Conversion with loss performed.", # x.6.4 - "Conversion Failed.", # x.6.5 + "Other or undefined protocol status.", # x.5.0 + "Invalid command.", # x.5.1 + "Syntax error.", # x.5.2 + "Too many recipients.", # x.5.3 + "Invalid command arguments.", # x.5.4 + "Wrong protocol version.", # x.5.5 ], [ - "Other or undefined security status.", # x.7.0 - "Delivery not authorized, message refused.", # x.7.1 - "Mailing list expansion prohibited.", # x.7.2 - "Security conversion required but not possible.", # x.7.3 - "Security features not supported.", # x.7.4 - "Cryptographic failure.", # x.7.5 - "Cryptographic algorithm not supported.", # x.7.6 - "Message integrity failure.", # x.7.7 + "Other or undefined media error.", # x.6.0 + "Media not supported.", # x.6.1 + "Conversion required and prohibited.", # x.6.2 + "Conversion required but not supported.", # x.6.3 + "Conversion with loss performed.", # x.6.4 + "Conversion Failed.", # x.6.5 + ], + [ + "Other or undefined security status.", # x.7.0 + "Delivery not authorized, message refused.", # x.7.1 + "Mailing list expansion prohibited.", # x.7.2 + "Security conversion required but not possible.", # x.7.3 + "Security features not supported.", # x.7.4 + "Cryptographic failure.", # x.7.5 + "Cryptographic algorithm not supported.", # x.7.6 + "Message integrity failure.", # x.7.7 ], ); sub _status { my $return = shift; - my $const = Qpsmtpd::Constants::return_code($return); + my $const = Qpsmtpd::Constants::return_code($return); if ($const =~ /^DENYSOFT/) { return 4; - } + } elsif ($const =~ /^DENY/) { return 5; } elsif ($const eq 'OK' or $const eq 'DONE') { return 2; } - else { # err .... no :) - return 4; # just 2,4,5 are allowed.. temp error by default + else { # err .... no :) + return 4; # just 2,4,5 are allowed.. temp error by default } } sub _dsn { - my ($self,$return,$reason,$default,$subject,$detail) = @_; + my ($self, $return, $reason, $default, $subject, $detail) = @_; if (!defined $return) { $return = $default; - } + } elsif ($return !~ /^\d+$/) { $reason = $return; $return = $default; @@ -157,7 +157,7 @@ sub _dsn { return ($return, "$msg (#$class.$subject.$detail)"); } -sub unspecified { shift->_dsn(shift,shift,DENYSOFT,0,0); } +sub unspecified { shift->_dsn(shift, shift, DENYSOFT, 0, 0); } =head1 ADDRESS STATUS @@ -170,7 +170,7 @@ default: DENYSOFT =cut -sub addr_unspecified { shift->_dsn(shift,shift,DENYSOFT,1,0); } +sub addr_unspecified { shift->_dsn(shift, shift, DENYSOFT, 1, 0); } =item no_such_user, addr_bad_dest_mbox @@ -179,8 +179,8 @@ default: DENY =cut -sub no_such_user { shift->_dsn(shift,(shift||"No such user"),DENY,1,1); } -sub addr_bad_dest_mbox { shift->_dsn(shift,shift,DENY,1,1); } +sub no_such_user { shift->_dsn(shift, (shift || "No such user"), DENY, 1, 1); } +sub addr_bad_dest_mbox { shift->_dsn(shift, shift, DENY, 1, 1); } =item addr_bad_dest_system @@ -189,7 +189,7 @@ default: DENY =cut -sub addr_bad_dest_system { shift->_dsn(shift,shift,DENY,1,2); } +sub addr_bad_dest_system { shift->_dsn(shift, shift, DENY, 1, 2); } =item addr_bad_dest_syntax @@ -198,7 +198,7 @@ default: DENY =cut -sub addr_bad_dest_syntax { shift->_dsn(shift,shift,DENY,1,3); } +sub addr_bad_dest_syntax { shift->_dsn(shift, shift, DENY, 1, 3); } =item addr_dest_ambigous @@ -207,7 +207,7 @@ default: DENYSOFT =cut -sub addr_dest_ambigous { shift->_dsn(shift,shift,DENYSOFT,1,4); } +sub addr_dest_ambigous { shift->_dsn(shift, shift, DENYSOFT, 1, 4); } =item addr_rcpt_ok @@ -217,7 +217,7 @@ default: OK =cut # XXX: do we need this? Maybe in all address verifying plugins? -sub addr_rcpt_ok { shift->_dsn(shift,shift,OK,1,5); } +sub addr_rcpt_ok { shift->_dsn(shift, shift, OK, 1, 5); } =item addr_mbox_moved @@ -226,7 +226,7 @@ default: DENY =cut -sub addr_mbox_moved { shift->_dsn(shift,shift,DENY,1,6); } +sub addr_mbox_moved { shift->_dsn(shift, shift, DENY, 1, 6); } =item addr_bad_from_syntax @@ -235,7 +235,7 @@ default: DENY =cut -sub addr_bad_from_syntax { shift->_dsn(shift,shift,DENY,1,7); } +sub addr_bad_from_syntax { shift->_dsn(shift, shift, DENY, 1, 7); } =item addr_bad_from_system @@ -246,7 +246,7 @@ default: DENY =cut -sub addr_bad_from_system { shift->_dsn(shift,shift,DENY,1,8); } +sub addr_bad_from_system { shift->_dsn(shift, shift, DENY, 1, 8); } =head1 MAILBOX STATUS @@ -259,7 +259,7 @@ default: DENYSOFT =cut -sub mbox_unspecified { shift->_dsn(shift,shift,DENYSOFT,2,0); } +sub mbox_unspecified { shift->_dsn(shift, shift, DENYSOFT, 2, 0); } =item mbox_disabled @@ -272,7 +272,7 @@ default: DENY ...but RFC says: =cut -sub mbox_disabled { shift->_dsn(shift,shift,DENY,2,1); } +sub mbox_disabled { shift->_dsn(shift, shift, DENY, 2, 1); } =item mbox_full @@ -281,7 +281,7 @@ default: DENYSOFT =cut -sub mbox_full { shift->_dsn(shift,shift,DENYSOFT,2,2); } +sub mbox_full { shift->_dsn(shift, shift, DENYSOFT, 2, 2); } =item mbox_msg_too_long @@ -290,7 +290,7 @@ default: DENY =cut -sub mbox_msg_too_long { shift->_dsn(shift,shift,DENY,2,3); } +sub mbox_msg_too_long { shift->_dsn(shift, shift, DENY, 2, 3); } =item mbox_list_expansion_problem @@ -301,7 +301,7 @@ default: DENYSOFT =cut -sub mbox_list_expansion_problem { shift->_dsn(shift,shift,DENYSOFT,2,4); } +sub mbox_list_expansion_problem { shift->_dsn(shift, shift, DENYSOFT, 2, 4); } =head1 MAIL SYSTEM STATUS @@ -314,7 +314,7 @@ default: DENYSOFT =cut -sub sys_unspecified { shift->_dsn(shift,shift,DENYSOFT,3,0); } +sub sys_unspecified { shift->_dsn(shift, shift, DENYSOFT, 3, 0); } =item sys_disk_full @@ -323,7 +323,7 @@ default: DENYSOFT =cut -sub sys_disk_full { shift->_dsn(shift,shift,DENYSOFT,3,1); } +sub sys_disk_full { shift->_dsn(shift, shift, DENYSOFT, 3, 1); } =item sys_not_accepting_mail @@ -332,7 +332,7 @@ default: DENYSOFT =cut -sub sys_not_accepting_mail { shift->_dsn(shift,shift,DENYSOFT,3,2); } +sub sys_not_accepting_mail { shift->_dsn(shift, shift, DENYSOFT, 3, 2); } =item sys_not_supported @@ -345,7 +345,7 @@ default: DENYSOFT =cut -sub sys_not_supported { shift->_dsn(shift,shift,DENYSOFT,3,3); } +sub sys_not_supported { shift->_dsn(shift, shift, DENYSOFT, 3, 3); } =item sys_msg_too_big @@ -356,7 +356,7 @@ default DENY =cut -sub sys_msg_too_big { shift->_dsn(shift,shift,DENY,3,4); } +sub sys_msg_too_big { shift->_dsn(shift, shift, DENY, 3, 4); } =head1 NETWORK AND ROUTING STATUS @@ -371,10 +371,10 @@ default: DENYSOFT =cut -sub net_unspecified { shift->_dsn(shift,shift,DENYSOFT,4,0); } +sub net_unspecified { shift->_dsn(shift, shift, DENYSOFT, 4, 0); } -# not useful # sub net_no_answer { shift->_dsn(shift,shift,4,1); } -# not useful # sub net_bad_connection { shift->_dsn(shift,shift,4,2); } +# not useful # sub net_no_answer { shift->_dsn(shift,shift,4,1); } +# not useful # sub net_bad_connection { shift->_dsn(shift,shift,4,2); } =item net_directory_server_failed, temp_resolver_failed @@ -383,12 +383,11 @@ default: DENYSOFT =cut -sub temp_resolver_failed { - shift->_dsn(shift, - (shift || "Temporary address resolution failure"), - DENYSOFT,4,3); +sub temp_resolver_failed { + shift->_dsn(shift, (shift || "Temporary address resolution failure"), + DENYSOFT, 4, 3); } -sub net_directory_server_failed { shift->_dsn(shift,shift,DENYSOFT,4,3); } +sub net_directory_server_failed { shift->_dsn(shift, shift, DENYSOFT, 4, 3); } # not useful # sub net_unable_to_route { shift->_dsn(shift,shift,4,4); } @@ -399,7 +398,7 @@ default: DENYSOFT =cut -sub net_system_congested { shift->_dsn(shift,shift,DENYSOFT,4,5); } +sub net_system_congested { shift->_dsn(shift, shift, DENYSOFT, 4, 5); } =item net_routing_loop, too_many_hops @@ -416,8 +415,11 @@ Why do we want to DENYSOFT something like this? =cut -sub net_routing_loop { shift->_dsn(shift,shift,DENY,4,6); } -sub too_many_hops { shift->_dsn(shift,(shift || "Too many hops"),DENY,4,6,); } +sub net_routing_loop { shift->_dsn(shift, shift, DENY, 4, 6); } +sub too_many_hops { + shift->_dsn(shift, (shift || "Too many hops"), DENY, 4, 6,); +} + # not useful # sub delivery_time_expired { shift->_dsn(shift,shift,4,7); } =head1 MAIL DELIVERY PROTOCOL STATUS @@ -431,7 +433,7 @@ default: DENYSOFT =cut -sub proto_unspecified { shift->_dsn(shift,shift,DENYSOFT,5,0); } +sub proto_unspecified { shift->_dsn(shift, shift, DENYSOFT, 5, 0); } =item proto_invalid_command @@ -440,7 +442,7 @@ default: DENY =cut -sub proto_invalid_command { shift->_dsn(shift,shift,DENY,5,1); } +sub proto_invalid_command { shift->_dsn(shift, shift, DENY, 5, 1); } =item proto_syntax_error @@ -449,7 +451,7 @@ default: DENY =cut -sub proto_syntax_error { shift->_dsn(shift,shift,DENY,5,2); } +sub proto_syntax_error { shift->_dsn(shift, shift, DENY, 5, 2); } =item proto_rcpt_list_too_long, too_many_rcpts @@ -458,8 +460,8 @@ default: DENYSOFT =cut -sub proto_rcpt_list_too_long { shift->_dsn(shift,shift,DENYSOFT,5,3); } -sub too_many_rcpts { shift->_dsn(shift,shift,DENYSOFT,5,3); } +sub proto_rcpt_list_too_long { shift->_dsn(shift, shift, DENYSOFT, 5, 3); } +sub too_many_rcpts { shift->_dsn(shift, shift, DENYSOFT, 5, 3); } =item proto_invalid_cmd_args @@ -468,7 +470,7 @@ default: DENY =cut -sub proto_invalid_cmd_args { shift->_dsn(shift,shift,DENY,5,4); } +sub proto_invalid_cmd_args { shift->_dsn(shift, shift, DENY, 5, 4); } =item proto_wrong_version @@ -479,7 +481,7 @@ default: DENYSOFT =cut -sub proto_wrong_version { shift->_dsn(shift,shift,DENYSOFT,5,5); } +sub proto_wrong_version { shift->_dsn(shift, shift, DENYSOFT, 5, 5); } =head1 MESSAGE CONTENT OR MESSAGE MEDIA STATUS @@ -492,7 +494,7 @@ default: DENYSOFT =cut -sub media_unspecified { shift->_dsn(shift,shift,DENYSOFT,6,0); } +sub media_unspecified { shift->_dsn(shift, shift, DENYSOFT, 6, 0); } =item media_unsupported @@ -501,7 +503,7 @@ default: DENY =cut -sub media_unsupported { shift->_dsn(shift,shift,DENY,6,1); } +sub media_unsupported { shift->_dsn(shift, shift, DENY, 6, 1); } =item media_conv_prohibited @@ -510,7 +512,7 @@ default: DENY =cut -sub media_conv_prohibited { shift->_dsn(shift,shift,DENY,6,2); } +sub media_conv_prohibited { shift->_dsn(shift, shift, DENY, 6, 2); } =item media_conv_unsupported @@ -519,7 +521,7 @@ default: DENYSOFT =cut -sub media_conv_unsupported { shift->_dsn(shift,shift,DENYSOFT,6,3); } +sub media_conv_unsupported { shift->_dsn(shift, shift, DENYSOFT, 6, 3); } =item media_conv_lossy @@ -530,7 +532,7 @@ default: DENYSOFT =cut -sub media_conv_lossy { shift->_dsn(shift,shift,DENYSOFT,6,4); } +sub media_conv_lossy { shift->_dsn(shift, shift, DENYSOFT, 6, 4); } =head1 SECURITY OR POLICY STATUS @@ -543,7 +545,7 @@ default: DENYSOFT =cut -sub sec_unspecified { shift->_dsn(shift,shift,DENYSOFT,7,0); } +sub sec_unspecified { shift->_dsn(shift, shift, DENYSOFT, 7, 0); } =item sec_sender_unauthorized, bad_sender_ip, relaying_denied @@ -552,12 +554,14 @@ default: DENY =cut -sub sec_sender_unauthorized { shift->_dsn(shift,shift,DENY,7,1); } -sub bad_sender_ip { - shift->_dsn(shift,(shift || "Bad sender's IP"),DENY,7,1,); +sub sec_sender_unauthorized { shift->_dsn(shift, shift, DENY, 7, 1); } + +sub bad_sender_ip { + shift->_dsn(shift, (shift || "Bad sender's IP"), DENY, 7, 1,); } -sub relaying_denied { - shift->_dsn(shift,(shift || "Relaying denied"),DENY,7,1); + +sub relaying_denied { + shift->_dsn(shift, (shift || "Relaying denied"), DENY, 7, 1); } =item sec_list_dest_prohibited @@ -567,7 +571,7 @@ default: DENY =cut -sub sec_list_dest_prohibited { shift->_dsn(shift,shift,DENY,7,2); } +sub sec_list_dest_prohibited { shift->_dsn(shift, shift, DENY, 7, 2); } =item sec_conv_failed @@ -576,7 +580,7 @@ default: DENY =cut -sub sec_conv_failed { shift->_dsn(shift,shift,DENY,7,3); } +sub sec_conv_failed { shift->_dsn(shift, shift, DENY, 7, 3); } =item sec_feature_unsupported @@ -585,7 +589,7 @@ default: DENY =cut -sub sec_feature_unsupported { shift->_dsn(shift,shift,DENY,7,4); } +sub sec_feature_unsupported { shift->_dsn(shift, shift, DENY, 7, 4); } =item sec_crypto_failure @@ -594,7 +598,7 @@ default: DENY =cut -sub sec_crypto_failure { shift->_dsn(shift,shift,DENY,7,5); } +sub sec_crypto_failure { shift->_dsn(shift, shift, DENY, 7, 5); } =item sec_crypto_algorithm_unsupported @@ -603,7 +607,9 @@ default: DENYSOFT =cut -sub sec_crypto_algorithm_unsupported { shift->_dsn(shift,shift,DENYSOFT,7,6); } +sub sec_crypto_algorithm_unsupported { + shift->_dsn(shift, shift, DENYSOFT, 7, 6); +} =item sec_msg_integrity_failure @@ -614,7 +620,7 @@ default: DENY =cut -sub sec_msg_integrity_failure { shift->_dsn(shift,shift,DENY,7,7); } +sub sec_msg_integrity_failure { shift->_dsn(shift, shift, DENY, 7, 7); } 1; diff --git a/lib/Qpsmtpd/Plugin.pm b/lib/Qpsmtpd/Plugin.pm index 4e3a08d..d4be038 100644 --- a/lib/Qpsmtpd/Plugin.pm +++ b/lib/Qpsmtpd/Plugin.pm @@ -9,102 +9,107 @@ use Qpsmtpd::Constants; # more or less in the order they will fire our @hooks = qw( - logging config post-fork pre-connection connect ehlo_parse ehlo - helo_parse helo auth_parse auth auth-plain auth-login auth-cram-md5 - rcpt_parse rcpt_pre rcpt mail_parse mail mail_pre - data data_headers_end data_post queue_pre queue queue_post vrfy noop - quit reset_transaction disconnect post-connection - unrecognized_command deny ok received_line help -); + logging config post-fork pre-connection connect ehlo_parse ehlo + helo_parse helo auth_parse auth auth-plain auth-login auth-cram-md5 + rcpt_parse rcpt_pre rcpt mail_parse mail mail_pre + data data_headers_end data_post queue_pre queue queue_post vrfy noop + quit reset_transaction disconnect post-connection + unrecognized_command deny ok received_line help + ); our %hooks = map { $_ => 1 } @hooks; sub new { - my $proto = shift; - my $class = ref($proto) || $proto; - bless ({}, $class); + my $proto = shift; + my $class = ref($proto) || $proto; + bless({}, $class); } sub hook_name { - return shift->{_hook}; + return shift->{_hook}; } sub register_hook { - my ($plugin, $hook, $method, $unshift) = @_; + my ($plugin, $hook, $method, $unshift) = @_; - die $plugin->plugin_name . " : Invalid hook: $hook" unless $hooks{$hook}; + die $plugin->plugin_name . " : Invalid hook: $hook" unless $hooks{$hook}; - $plugin->{_qp}->log(LOGDEBUG, $plugin->plugin_name, "hooking", $hook) - unless $hook =~ /logging/; # can't log during load_logging() + $plugin->{_qp}->log(LOGDEBUG, $plugin->plugin_name, "hooking", $hook) + unless $hook =~ /logging/; # can't log during load_logging() - # I can't quite decide if it's better to parse this code ref or if - # we should pass the plugin object and method name ... hmn. - $plugin->qp->_register_hook - ($hook, - { code => sub { local $plugin->{_qp} = shift; - local $plugin->{_hook} = $hook; - $plugin->$method(@_) - }, - name => $plugin->plugin_name, - }, - $unshift, - ); + # I can't quite decide if it's better to parse this code ref or if + # we should pass the plugin object and method name ... hmn. + $plugin->qp->_register_hook( + $hook, + { + code => sub { + local $plugin->{_qp} = shift; + local $plugin->{_hook} = $hook; + $plugin->$method(@_); + }, + name => $plugin->plugin_name, + }, + $unshift, + ); } sub _register { - my $self = shift; - my $qp = shift; - local $self->{_qp} = $qp; - $self->init($qp, @_) if $self->can('init'); - $self->_register_standard_hooks($qp, @_); - $self->register($qp, @_) if $self->can('register'); + my $self = shift; + my $qp = shift; + local $self->{_qp} = $qp; + $self->init($qp, @_) if $self->can('init'); + $self->_register_standard_hooks($qp, @_); + $self->register($qp, @_) if $self->can('register'); } sub qp { - shift->{_qp}; + shift->{_qp}; } sub log { - my $self = shift; - return if defined $self->{_hook} && $self->{_hook} eq 'logging'; - my $level = $self->adjust_log_level( shift, $self->plugin_name ); - $self->{_qp}->varlog($level, $self->{_hook}, $self->plugin_name, @_); + my $self = shift; + return if defined $self->{_hook} && $self->{_hook} eq 'logging'; + my $level = $self->adjust_log_level(shift, $self->plugin_name); + $self->{_qp}->varlog($level, $self->{_hook}, $self->plugin_name, @_); } sub adjust_log_level { - my ( $self, $cur_level, $plugin_name) = @_; + my ($self, $cur_level, $plugin_name) = @_; my $adj = $self->{_args}{loglevel} or return $cur_level; - return $adj if $adj =~ m/^[01234567]$/; # a raw syslog numeral + return $adj if $adj =~ m/^[01234567]$/; # a raw syslog numeral - if ( $adj !~ /^[\+\-][\d]$/ ) { - $self->log( LOGERROR, $self-"invalid $plugin_name loglevel setting ($adj)" ); - undef $self->{_args}{loglevel}; # only complain once per plugin + if ($adj !~ /^[\+\-][\d]$/) { + $self->log(LOGERROR, + $self - "invalid $plugin_name loglevel setting ($adj)"); + undef $self->{_args}{loglevel}; # only complain once per plugin return $cur_level; - }; + } - my $operator = substr($adj, 0, 1); - my $adjust = substr($adj, -1, 1); + my $operator = substr($adj, 0, 1); + my $adjust = substr($adj, -1, 1); - my $new_level = $operator eq '+' ? $cur_level + $adjust : $cur_level - $adjust; + my $new_level = + $operator eq '+' ? $cur_level + $adjust : $cur_level - $adjust; $new_level = 7 if $new_level > 7; $new_level = 0 if $new_level < 0; return $new_level; -}; +} sub transaction { - # not sure if this will work in a non-forking or a threaded daemon - shift->qp->transaction; + + # not sure if this will work in a non-forking or a threaded daemon + shift->qp->transaction; } sub connection { - shift->qp->connection; + shift->qp->connection; } sub spool_dir { - shift->qp->spool_dir; + shift->qp->spool_dir; } sub auth_user { @@ -116,17 +121,17 @@ sub auth_mechanism { } sub temp_file { - my $self = shift; - my $tempfile = $self->qp->temp_file; - push @{$self->qp->transaction->{_temp_files}}, $tempfile; - return $tempfile; + my $self = shift; + my $tempfile = $self->qp->temp_file; + push @{$self->qp->transaction->{_temp_files}}, $tempfile; + return $tempfile; } sub temp_dir { - my $self = shift; - my $tempdir = $self->qp->temp_dir(); - push @{$self->qp->transaction->{_temp_dirs}}, $tempdir; - return $tempdir; + my $self = shift; + my $tempdir = $self->qp->temp_dir(); + push @{$self->qp->transaction->{_temp_dirs}}, $tempdir; + return $tempdir; } # plugin inheritance: @@ -137,32 +142,31 @@ sub temp_dir { # $self->SUPER::register(@_); # } sub isa_plugin { - my ($self, $parent) = @_; - my ($currentPackage) = caller; + my ($self, $parent) = @_; + my ($currentPackage) = caller; - my $cleanParent = $parent; - $cleanParent =~ s/\W/_/g; - my $newPackage = $currentPackage."::_isa_$cleanParent"; + my $cleanParent = $parent; + $cleanParent =~ s/\W/_/g; + my $newPackage = $currentPackage . "::_isa_$cleanParent"; - # don't reload plugins if they are already loaded - return if defined &{"${newPackage}::plugin_name"}; + # don't reload plugins if they are already loaded + return if defined &{"${newPackage}::plugin_name"}; - # find $parent in plugin_dirs - my $parent_dir; - for ($self->qp->plugin_dirs) { - if (-e "$_/$parent") { - $parent_dir = $_; - last; + # find $parent in plugin_dirs + my $parent_dir; + for ($self->qp->plugin_dirs) { + if (-e "$_/$parent") { + $parent_dir = $_; + last; + } } - } - die "cannot find plugin '$parent'" unless $parent_dir; + die "cannot find plugin '$parent'" unless $parent_dir; - $self->compile($self->plugin_name . "_isa_$cleanParent", - $newPackage, - "$parent_dir/$parent"); - warn "---- $newPackage\n"; - no strict 'refs'; - push @{"${currentPackage}::ISA"}, $newPackage; + $self->compile($self->plugin_name . "_isa_$cleanParent", + $newPackage, "$parent_dir/$parent"); + warn "---- $newPackage\n"; + no strict 'refs'; + push @{"${currentPackage}::ISA"}, $newPackage; } # why isn't compile private? it's only called from Plugin and Qpsmtpd. @@ -172,8 +176,8 @@ sub compile { my $sub; open F, $file or die "could not open $file: $!"; { - local $/ = undef; - $sub = ; + local $/ = undef; + $sub = ; } close F; @@ -189,19 +193,19 @@ sub compile { } my $eval = join( - "\n", - "package $package;", - 'use Qpsmtpd::Constants;', - "require Qpsmtpd::Plugin;", - 'use vars qw(@ISA);', - 'use strict;', - '@ISA = qw(Qpsmtpd::Plugin);', - ($test_mode ? 'use Test::More;' : ''), - "sub plugin_name { qq[$plugin] }", - $line, - $sub, - "\n", # last line comment without newline? - ); + "\n", + "package $package;", + 'use Qpsmtpd::Constants;', + "require Qpsmtpd::Plugin;", + 'use vars qw(@ISA);', + 'use strict;', + '@ISA = qw(Qpsmtpd::Plugin);', + ($test_mode ? 'use Test::More;' : ''), + "sub plugin_name { qq[$plugin] }", + $line, + $sub, + "\n", # last line comment without newline? + ); #warn "eval: $eval"; @@ -213,120 +217,126 @@ sub compile { } sub get_reject { - my $self = shift; + my $self = shift; my $smtp_mess = shift || "why didn't you pass an error message?"; - my $log_mess = shift || ''; + my $log_mess = shift || ''; $log_mess = ", $log_mess" if $log_mess; my $reject = $self->{_args}{reject}; - if ( defined $reject && ! $reject ) { + if (defined $reject && !$reject) { $self->log(LOGINFO, "fail, reject disabled" . $log_mess); return DECLINED; - }; + } # the naughty plugin will reject later - if ( $reject eq 'naughty' ) { + if ($reject eq 'naughty') { $self->log(LOGINFO, "fail, NAUGHTY" . $log_mess); - return $self->store_deferred_reject( $smtp_mess ); - }; + return $self->store_deferred_reject($smtp_mess); + } # they asked for reject, we give them reject $self->log(LOGINFO, "fail" . $log_mess); - return ( $self->get_reject_type(), $smtp_mess); -}; + return ($self->get_reject_type(), $smtp_mess); +} sub get_reject_type { - my $self = shift; + my $self = shift; my $default = shift || DENY; - my $deny = shift || $self->{_args}{reject_type} or return $default; + my $deny = shift || $self->{_args}{reject_type} or return $default; - return $deny =~ /^(temp|soft)$/i ? DENYSOFT - : $deny =~ /^(perm|hard)$/i ? DENY - : $deny eq 'disconnect' ? DENY_DISCONNECT - : $deny eq 'temp_disconnect' ? DENYSOFT_DISCONNECT - : $default; -}; + return + $deny =~ /^(temp|soft)$/i ? DENYSOFT + : $deny =~ /^(perm|hard)$/i ? DENY + : $deny eq 'disconnect' ? DENY_DISCONNECT + : $deny eq 'temp_disconnect' ? DENYSOFT_DISCONNECT + : $default; +} sub store_deferred_reject { my ($self, $smtp_mess) = @_; - # store the reject message that the naughty plugin will return later - if ( ! $self->connection->notes('naughty') ) { + # store the reject message that the naughty plugin will return later + if (!$self->connection->notes('naughty')) { $self->connection->notes('naughty', $smtp_mess); } else { # append this reject message to the message my $prev = $self->connection->notes('naughty'); $self->connection->notes('naughty', "$prev\015\012$smtp_mess"); - }; - if ( ! $self->connection->notes('naughty_reject_type') ) { - $self->connection->notes('naughty_reject_type', $self->{_args}{reject_type} ); + } + if (!$self->connection->notes('naughty_reject_type')) { + $self->connection->notes('naughty_reject_type', + $self->{_args}{reject_type}); } return (DECLINED); -}; +} sub init_resolver { my $self = shift; return $self->{_resolver} if $self->{_resolver}; - $self->log( LOGDEBUG, "initializing Net::DNS::Resolver"); + $self->log(LOGDEBUG, "initializing Net::DNS::Resolver"); $self->{_resolver} = Net::DNS::Resolver->new(dnsrch => 0); my $timeout = $self->{_args}{dns_timeout} || 5; $self->{_resolver}->tcp_timeout($timeout); $self->{_resolver}->udp_timeout($timeout); return $self->{_resolver}; -}; +} sub is_immune { my $self = shift; - if ( $self->qp->connection->relay_client() ) { + if ($self->qp->connection->relay_client()) { + # set by plugins/relay, or Qpsmtpd::Auth $self->log(LOGINFO, "skip, relay client"); return 1; - }; - if ( $self->qp->connection->notes('whitelisthost') ) { + } + if ($self->qp->connection->notes('whitelisthost')) { + # set by plugins/dns_whitelist_soft or plugins/whitelist $self->log(LOGINFO, "skip, whitelisted host"); return 1; - }; - if ( $self->qp->transaction->notes('whitelistsender') ) { + } + if ($self->qp->transaction->notes('whitelistsender')) { + # set by plugins/whitelist $self->log(LOGINFO, "skip, whitelisted sender"); return 1; - }; - if ( $self->connection->notes('naughty') ) { + } + if ($self->connection->notes('naughty')) { + # see plugins/naughty $self->log(LOGINFO, "skip, naughty"); return 1; - }; - if ( $self->connection->notes('rejected') ) { + } + if ($self->connection->notes('rejected')) { + # http://www.steve.org.uk/Software/ms-lite/ $self->log(LOGINFO, "skip, already rejected"); return 1; - }; + } return; -}; +} sub adjust_karma { - my ( $self, $value ) = @_; + my ($self, $value) = @_; my $karma = $self->connection->notes('karma') || 0; $karma += $value; $self->log(LOGDEBUG, "karma adjust: $value ($karma)"); $self->connection->notes('karma', $karma); return $value; -}; - -sub _register_standard_hooks { - my ($plugin, $qp) = @_; - - for my $hook (@hooks) { - my $hooksub = "hook_$hook"; - $hooksub =~ s/\W/_/g; - $plugin->register_hook( $hook, $hooksub ) - if ($plugin->can($hooksub)); - } } +sub _register_standard_hooks { + my ($plugin, $qp) = @_; + + for my $hook (@hooks) { + my $hooksub = "hook_$hook"; + $hooksub =~ s/\W/_/g; + $plugin->register_hook($hook, $hooksub) + if ($plugin->can($hooksub)); + } +} 1; diff --git a/lib/Qpsmtpd/PollServer.pm b/lib/Qpsmtpd/PollServer.pm index f987c3f..a9e6ba0 100644 --- a/lib/Qpsmtpd/PollServer.pm +++ b/lib/Qpsmtpd/PollServer.pm @@ -1,32 +1,33 @@ 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 -); + 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; @@ -36,7 +37,7 @@ use Socket qw(inet_aton AF_INET CRLF); use Time::HiRes qw(time); use strict; -sub max_idle_time { 60 } +sub max_idle_time { 60 } sub max_connect_time { 1200 } sub input_sock { @@ -47,12 +48,12 @@ sub input_sock { sub new { my Qpsmtpd::PollServer $self = shift; - + $self = fields::new($self) unless ref $self; - $self->SUPER::new( @_ ); + $self->SUPER::new(@_); $self->{cmd_timeout} = 5; - $self->{start_time} = time; - $self->{mode} = 'connect'; + $self->{start_time} = time; + $self->{mode} = 'connect'; $self->load_plugins; $self->load_logging; @@ -75,28 +76,28 @@ sub new { 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'; + 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} = {}; } @@ -121,17 +122,18 @@ 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 ($::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)) { + if (my $lookup = + $cmd_cache{$meth} + || $self->{_commands}->{$meth} && $self->can($meth)) + { $cmd_cache{$meth} = $lookup; - eval { - $lookup->($self, @params); - }; + eval { $lookup->($self, @params); }; if ($@) { my $error = $@; chomp($error); @@ -141,11 +143,13 @@ sub process_line { } else { # No such method - i.e. unrecognized command - my ($rc, $msg) = $self->run_hooks("unrecognized_command", $meth, @params); + 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; @@ -171,31 +175,33 @@ sub 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); + 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") }, + 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, - ); - + 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; } @@ -217,7 +223,7 @@ sub data_respond { $self->respond(451, @$msg); $self->reset_transaction(); return; - } + } elsif ($rc == DENY_DISCONNECT) { $msg->[0] ||= "Message denied"; $self->respond(554, @$msg); @@ -231,14 +237,16 @@ sub data_respond { return; } return $self->respond(503, "MAIL first") unless $self->transaction->sender; - return $self->respond(503, "RCPT first") unless $self->transaction->recipients; - + 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->{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"); @@ -255,42 +263,47 @@ sub got_data { my $remainder; if ($data =~ s/^\.\r\n(.*)\z//ms) { $remainder = $1; - $done = 1; + $done = 1; } - # add a transaction->blocked check back here when we have line by line plugin access... +# 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. + + # ... 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"); + + 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/"); - +#$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 + + # Save the start of just the body itself $self->transaction->set_body_start(); } } @@ -298,7 +311,6 @@ sub got_data { $self->transaction->body_write(\$data); $self->{data_size} += length $data; } - if ($done) { $self->end_of_data; @@ -309,38 +321,44 @@ sub got_data { 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}"); - + + $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 $esmtp = substr($smtp, 0, 1) eq "E"; my $authheader; my $sslheader; - + if (defined $self->connection->notes('tls_enabled') - and $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) "; + $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"; + $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}; - + + $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; } diff --git a/lib/Qpsmtpd/Postfix.pm b/lib/Qpsmtpd/Postfix.pm index 519e5f6..2946bba 100644 --- a/lib/Qpsmtpd/Postfix.pm +++ b/lib/Qpsmtpd/Postfix.pm @@ -21,125 +21,131 @@ use vars qw(@ISA); my %rec_types; sub init { - my ($self) = @_; + my ($self) = @_; - %rec_types = ( - REC_TYPE_SIZE => 'C', # first record, created by cleanup - REC_TYPE_TIME => 'T', # time stamp, required - REC_TYPE_FULL => 'F', # full name, optional - REC_TYPE_INSP => 'I', # inspector transport - REC_TYPE_FILT => 'L', # loop filter transport - REC_TYPE_FROM => 'S', # sender, required - REC_TYPE_DONE => 'D', # delivered recipient, optional - REC_TYPE_RCPT => 'R', # todo recipient, optional - REC_TYPE_ORCP => 'O', # original recipient, optional - REC_TYPE_WARN => 'W', # warning message time - REC_TYPE_ATTR => 'A', # named attribute for extensions + %rec_types = ( + REC_TYPE_SIZE => 'C', # first record, created by cleanup + REC_TYPE_TIME => 'T', # time stamp, required + REC_TYPE_FULL => 'F', # full name, optional + REC_TYPE_INSP => 'I', # inspector transport + REC_TYPE_FILT => 'L', # loop filter transport + REC_TYPE_FROM => 'S', # sender, required + REC_TYPE_DONE => 'D', # delivered recipient, optional + REC_TYPE_RCPT => 'R', # todo recipient, optional + REC_TYPE_ORCP => 'O', # original recipient, optional + REC_TYPE_WARN => 'W', # warning message time + REC_TYPE_ATTR => 'A', # named attribute for extensions - REC_TYPE_MESG => 'M', # start message records + REC_TYPE_MESG => 'M', # start message records - REC_TYPE_CONT => 'L', # long data record - REC_TYPE_NORM => 'N', # normal data record + REC_TYPE_CONT => 'L', # long data record + REC_TYPE_NORM => 'N', # normal data record - REC_TYPE_XTRA => 'X', # start extracted records + REC_TYPE_XTRA => 'X', # start extracted records - REC_TYPE_RRTO => 'r', # return-receipt, from headers - REC_TYPE_ERTO => 'e', # errors-to, from headers - REC_TYPE_PRIO => 'P', # priority - REC_TYPE_VERP => 'V', # VERP delimiters + REC_TYPE_RRTO => 'r', # return-receipt, from headers + REC_TYPE_ERTO => 'e', # errors-to, from headers + REC_TYPE_PRIO => 'P', # priority + REC_TYPE_VERP => 'V', # VERP delimiters - REC_TYPE_END => 'E', # terminator, required + REC_TYPE_END => 'E', # terminator, required - ); + ); } sub print_rec { - my ($self, $type, @list) = @_; + my ($self, $type, @list) = @_; - die "unknown record type" unless ($rec_types{$type}); - $self->print($rec_types{$type}); + die "unknown record type" unless ($rec_types{$type}); + $self->print($rec_types{$type}); - # the length is a little endian base-128 number where each - # byte except the last has the high bit set: - my $s = "@list"; - my $ln = length($s); - while ($ln >= 0x80) { - my $lnl = $ln & 0x7F; - $ln >>= 7; - $self->print(chr($lnl | 0x80)); - } - $self->print(chr($ln)); + # the length is a little endian base-128 number where each + # byte except the last has the high bit set: + my $s = "@list"; + my $ln = length($s); + while ($ln >= 0x80) { + my $lnl = $ln & 0x7F; + $ln >>= 7; + $self->print(chr($lnl | 0x80)); + } + $self->print(chr($ln)); - $self->print($s); + $self->print($s); } sub print_rec_size { - my ($self, $content_size, $data_offset, $rcpt_count) = @_; + my ($self, $content_size, $data_offset, $rcpt_count) = @_; - my $s = sprintf("%15ld %15ld %15ld", $content_size, $data_offset, $rcpt_count); - $self->print_rec('REC_TYPE_SIZE', $s); + my $s = + sprintf("%15ld %15ld %15ld", $content_size, $data_offset, $rcpt_count); + $self->print_rec('REC_TYPE_SIZE', $s); } sub print_rec_time { - my ($self, $time) = @_; + my ($self, $time) = @_; - $time = time() unless (defined($time)); + $time = time() unless (defined($time)); - my $s = sprintf("%d", $time); - $self->print_rec('REC_TYPE_TIME', $s); + my $s = sprintf("%d", $time); + $self->print_rec('REC_TYPE_TIME', $s); } sub open_cleanup { - my ($class, $socket) = @_; + my ($class, $socket) = @_; - my $self; - if ($socket =~ m#^(/.+)#) { - $socket = $1; # un-taint socket path - $self = IO::Socket::UNIX->new(Type => SOCK_STREAM, - Peer => $socket) if $socket; - - } elsif ($socket =~ /(.*):(\d+)/) { - my ($host,$port) = ($1,$2); # un-taint address and port - $self = IO::Socket::INET->new(Proto => 'tcp', - PeerAddr => $host,PeerPort => $port) - if $host and $port; - } - unless (ref $self) { - warn "Couldn't open \"$socket\": $!"; - return; - } - # allow buffered writes - $self->autoflush(0); - bless ($self, $class); - $self->init(); - return $self; + my $self; + if ($socket =~ m#^(/.+)#) { + $socket = $1; # un-taint socket path + $self = IO::Socket::UNIX->new(Type => SOCK_STREAM, + Peer => $socket) + if $socket; + + } + elsif ($socket =~ /(.*):(\d+)/) { + my ($host, $port) = ($1, $2); # un-taint address and port + $self = IO::Socket::INET->new( + Proto => 'tcp', + PeerAddr => $host, + PeerPort => $port + ) + if $host and $port; + } + unless (ref $self) { + warn "Couldn't open \"$socket\": $!"; + return; + } + + # allow buffered writes + $self->autoflush(0); + bless($self, $class); + $self->init(); + return $self; } sub print_attr { - my ($self, @kv) = @_; - for (@kv) { - $self->print("$_\0"); - } - $self->print("\0"); + my ($self, @kv) = @_; + for (@kv) { + $self->print("$_\0"); + } + $self->print("\0"); } sub get_attr { - my ($self) = @_; - local $/ = "\0"; - my %kv; - for(;;) { - my $k = $self->getline; - chomp($k); - last unless ($k); - my $v = $self->getline; - chomp($v); - $kv{$k} = $v; - } - return %kv; + my ($self) = @_; + local $/ = "\0"; + my %kv; + for (; ;) { + my $k = $self->getline; + chomp($k); + last unless ($k); + my $v = $self->getline; + chomp($v); + $kv{$k} = $v; + } + return %kv; } - =head2 print_msg_line($line) print one line of a message to cleanup. @@ -151,17 +157,17 @@ and splits the line across several records if it is longer than =cut sub print_msg_line { - my ($self, $line) = @_; + my ($self, $line) = @_; - $line =~ s/\r?\n$//s; + $line =~ s/\r?\n$//s; - # split into 1k chunks. - while (length($line) > 1024) { - my $s = substr($line, 0, 1024); - $line = substr($line, 1024); - $self->print_rec('REC_TYPE_CONT', $s); - } - $self->print_rec('REC_TYPE_NORM', $line); + # split into 1k chunks. + while (length($line) > 1024) { + my $s = substr($line, 0, 1024); + $line = substr($line, 1024); + $self->print_rec('REC_TYPE_CONT', $s); + } + $self->print_rec('REC_TYPE_NORM', $line); } =head2 inject_mail($transaction) @@ -172,52 +178,55 @@ $transaction is supposed to be a Qpsmtpd::Transaction object. =cut sub inject_mail { - my ($class, $transaction) = @_; + my ($class, $transaction) = @_; - my @sockets = @{$transaction->notes('postfix-queue-sockets') - // ['/var/spool/postfix/public/cleanup']}; - my $strm; - $strm = $class->open_cleanup($_) and last for @sockets; - die "Unable to open any cleanup sockets!" unless $strm; + my @sockets = @{$transaction->notes('postfix-queue-sockets') + // ['/var/spool/postfix/public/cleanup']}; + my $strm; + $strm = $class->open_cleanup($_) and last for @sockets; + die "Unable to open any cleanup sockets!" unless $strm; - my %at = $strm->get_attr; - my $qid = $at{queue_id}; - print STDERR "qid=$qid\n"; - $strm->print_attr('flags' => $transaction->notes('postfix-queue-flags')); - $strm->print_rec_time(); - $strm->print_rec('REC_TYPE_FROM', $transaction->sender->address|| ""); - for (map { $_->address } $transaction->recipients) { - $strm->print_rec('REC_TYPE_RCPT', $_); - } - # add an empty message length record. - # cleanup is supposed to understand that. - # see src/pickup/pickup.c - $strm->print_rec('REC_TYPE_MESG', ""); + my %at = $strm->get_attr; + my $qid = $at{queue_id}; + print STDERR "qid=$qid\n"; + $strm->print_attr('flags' => $transaction->notes('postfix-queue-flags')); + $strm->print_rec_time(); + $strm->print_rec('REC_TYPE_FROM', $transaction->sender->address || ""); + for (map { $_->address } $transaction->recipients) { + $strm->print_rec('REC_TYPE_RCPT', $_); + } - # a received header has already been added in SMTP.pm - # so we can just copy the message: + # add an empty message length record. + # cleanup is supposed to understand that. + # see src/pickup/pickup.c + $strm->print_rec('REC_TYPE_MESG', ""); - my $hdr = $transaction->header->as_string; - for (split(/\r?\n/, $hdr)) { - print STDERR "hdr: $_\n"; - $strm->print_msg_line($_); - } - $transaction->body_resetpos; - while (my $line = $transaction->body_getline) { - # print STDERR "body: $line\n"; - $strm->print_msg_line($line); - } + # a received header has already been added in SMTP.pm + # so we can just copy the message: - # finish it. - $strm->print_rec('REC_TYPE_XTRA', ""); - $strm->print_rec('REC_TYPE_END', ""); - $strm->flush(); - %at = $strm->get_attr; - my $status = $at{status}; - my $reason = $at{reason}; - $strm->close(); - return wantarray ? ($status, $qid, $reason || "") : $status; + my $hdr = $transaction->header->as_string; + for (split(/\r?\n/, $hdr)) { + print STDERR "hdr: $_\n"; + $strm->print_msg_line($_); + } + $transaction->body_resetpos; + while (my $line = $transaction->body_getline) { + + # print STDERR "body: $line\n"; + $strm->print_msg_line($line); + } + + # finish it. + $strm->print_rec('REC_TYPE_XTRA', ""); + $strm->print_rec('REC_TYPE_END', ""); + $strm->flush(); + %at = $strm->get_attr; + my $status = $at{status}; + my $reason = $at{reason}; + $strm->close(); + return wantarray ? ($status, $qid, $reason || "") : $status; } 1; + # vim:sw=2 diff --git a/lib/Qpsmtpd/Postfix/Constants.pm b/lib/Qpsmtpd/Postfix/Constants.pm index c06ad3f..8535284 100644 --- a/lib/Qpsmtpd/Postfix/Constants.pm +++ b/lib/Qpsmtpd/Postfix/Constants.pm @@ -15,72 +15,79 @@ require Exporter; use vars qw(@ISA @EXPORT %cleanup_soft %cleanup_hard $postfix_version); use strict; -@ISA = qw(Exporter); +@ISA = qw(Exporter); @EXPORT = qw( - %cleanup_soft - %cleanup_hard - $postfix_version - CLEANUP_FLAG_NONE - CLEANUP_FLAG_BOUNCE - CLEANUP_FLAG_FILTER - CLEANUP_FLAG_HOLD - CLEANUP_FLAG_DISCARD - CLEANUP_FLAG_BCC_OK - CLEANUP_FLAG_MAP_OK - CLEANUP_FLAG_MILTER - CLEANUP_FLAG_FILTER_ALL - CLEANUP_FLAG_MASK_EXTERNAL - CLEANUP_FLAG_MASK_INTERNAL - CLEANUP_FLAG_MASK_EXTRA - CLEANUP_STAT_OK - CLEANUP_STAT_BAD - CLEANUP_STAT_WRITE - CLEANUP_STAT_SIZE - CLEANUP_STAT_CONT - CLEANUP_STAT_HOPS - CLEANUP_STAT_RCPT - CLEANUP_STAT_PROXY - CLEANUP_STAT_DEFER - CLEANUP_STAT_MASK_CANT_BOUNCE - CLEANUP_STAT_MASK_INCOMPLETE -); + %cleanup_soft + %cleanup_hard + $postfix_version + CLEANUP_FLAG_NONE + CLEANUP_FLAG_BOUNCE + CLEANUP_FLAG_FILTER + CLEANUP_FLAG_HOLD + CLEANUP_FLAG_DISCARD + CLEANUP_FLAG_BCC_OK + CLEANUP_FLAG_MAP_OK + CLEANUP_FLAG_MILTER + CLEANUP_FLAG_FILTER_ALL + CLEANUP_FLAG_MASK_EXTERNAL + CLEANUP_FLAG_MASK_INTERNAL + CLEANUP_FLAG_MASK_EXTRA + CLEANUP_STAT_OK + CLEANUP_STAT_BAD + CLEANUP_STAT_WRITE + CLEANUP_STAT_SIZE + CLEANUP_STAT_CONT + CLEANUP_STAT_HOPS + CLEANUP_STAT_RCPT + CLEANUP_STAT_PROXY + CLEANUP_STAT_DEFER + CLEANUP_STAT_MASK_CANT_BOUNCE + CLEANUP_STAT_MASK_INCOMPLETE + ); $postfix_version = "2.4"; -use constant CLEANUP_FLAG_NONE => 0; # /* No special features */ -use constant CLEANUP_FLAG_BOUNCE => (1<<0); # /* Bounce bad messages */ -use constant CLEANUP_FLAG_FILTER => (1<<1); # /* Enable header/body checks */ -use constant CLEANUP_FLAG_HOLD => (1<<2); # /* Place message on hold */ -use constant CLEANUP_FLAG_DISCARD => (1<<3); # /* Discard message silently */ -use constant CLEANUP_FLAG_BCC_OK => (1<<4); # /* Ok to add auto-BCC addresses */ -use constant CLEANUP_FLAG_MAP_OK => (1<<5); # /* Ok to map addresses */ -use constant CLEANUP_FLAG_MILTER => (1<<6); # /* Enable Milter applications */ -use constant CLEANUP_FLAG_FILTER_ALL => (CLEANUP_FLAG_FILTER | CLEANUP_FLAG_MILTER); -use constant CLEANUP_FLAG_MASK_EXTERNAL => (CLEANUP_FLAG_FILTER_ALL | CLEANUP_FLAG_BCC_OK | CLEANUP_FLAG_MAP_OK); -use constant CLEANUP_FLAG_MASK_INTERNAL => CLEANUP_FLAG_MAP_OK; -use constant CLEANUP_FLAG_MASK_EXTRA => (CLEANUP_FLAG_HOLD | CLEANUP_FLAG_DISCARD); +use constant CLEANUP_FLAG_NONE => 0; # /* No special features */ +use constant CLEANUP_FLAG_BOUNCE => (1 << 0); # /* Bounce bad messages */ +use constant CLEANUP_FLAG_FILTER => (1 << 1); # /* Enable header/body checks */ +use constant CLEANUP_FLAG_HOLD => (1 << 2); # /* Place message on hold */ +use constant CLEANUP_FLAG_DISCARD => (1 << 3); # /* Discard message silently */ +use constant CLEANUP_FLAG_BCC_OK => (1 << 4) + ; # /* Ok to add auto-BCC addresses */ +use constant CLEANUP_FLAG_MAP_OK => (1 << 5); # /* Ok to map addresses */ +use constant CLEANUP_FLAG_MILTER => (1 << 6); # /* Enable Milter applications */ +use constant CLEANUP_FLAG_FILTER_ALL => + (CLEANUP_FLAG_FILTER | CLEANUP_FLAG_MILTER); +use constant CLEANUP_FLAG_MASK_EXTERNAL => + (CLEANUP_FLAG_FILTER_ALL | CLEANUP_FLAG_BCC_OK | CLEANUP_FLAG_MAP_OK); +use constant CLEANUP_FLAG_MASK_INTERNAL => CLEANUP_FLAG_MAP_OK; +use constant CLEANUP_FLAG_MASK_EXTRA => + (CLEANUP_FLAG_HOLD | CLEANUP_FLAG_DISCARD); -use constant CLEANUP_STAT_OK => 0; # /* Success. */ -use constant CLEANUP_STAT_BAD => (1<<0); # /* Internal protocol error */ -use constant CLEANUP_STAT_WRITE => (1<<1); # /* Error writing message file */ -use constant CLEANUP_STAT_SIZE => (1<<2); # /* Message file too big */ -use constant CLEANUP_STAT_CONT => (1<<3); # /* Message content rejected */ -use constant CLEANUP_STAT_HOPS => (1<<4); # /* Too many hops */ -use constant CLEANUP_STAT_RCPT => (1<<6); # /* No recipients found */ -use constant CLEANUP_STAT_PROXY => (1<<7); # /* Proxy reject */ -use constant CLEANUP_STAT_DEFER => (1<<8); # /* Temporary reject */ -use constant CLEANUP_STAT_MASK_CANT_BOUNCE => (CLEANUP_STAT_BAD | CLEANUP_STAT_WRITE | CLEANUP_STAT_DEFER); -use constant CLEANUP_STAT_MASK_INCOMPLETE => (CLEANUP_STAT_BAD | CLEANUP_STAT_WRITE | CLEANUP_STAT_SIZE | CLEANUP_STAT_DEFER); +use constant CLEANUP_STAT_OK => 0; # /* Success. */ +use constant CLEANUP_STAT_BAD => (1 << 0); # /* Internal protocol error */ +use constant CLEANUP_STAT_WRITE => (1 << 1); # /* Error writing message file */ +use constant CLEANUP_STAT_SIZE => (1 << 2); # /* Message file too big */ +use constant CLEANUP_STAT_CONT => (1 << 3); # /* Message content rejected */ +use constant CLEANUP_STAT_HOPS => (1 << 4); # /* Too many hops */ +use constant CLEANUP_STAT_RCPT => (1 << 6); # /* No recipients found */ +use constant CLEANUP_STAT_PROXY => (1 << 7); # /* Proxy reject */ +use constant CLEANUP_STAT_DEFER => (1 << 8); # /* Temporary reject */ +use constant CLEANUP_STAT_MASK_CANT_BOUNCE => + (CLEANUP_STAT_BAD | CLEANUP_STAT_WRITE | CLEANUP_STAT_DEFER); +use constant CLEANUP_STAT_MASK_INCOMPLETE => + (CLEANUP_STAT_BAD | CLEANUP_STAT_WRITE | CLEANUP_STAT_SIZE | + CLEANUP_STAT_DEFER); %cleanup_soft = ( - CLEANUP_STAT_DEFER => "service unavailable (#4.7.1)", - CLEANUP_STAT_PROXY => "queue file write error (#4.3.0)", - CLEANUP_STAT_BAD => "internal protocol error (#4.3.0)", - CLEANUP_STAT_WRITE => "queue file write error (#4.3.0)", -); + CLEANUP_STAT_DEFER => "service unavailable (#4.7.1)", + CLEANUP_STAT_PROXY => "queue file write error (#4.3.0)", + CLEANUP_STAT_BAD => "internal protocol error (#4.3.0)", + CLEANUP_STAT_WRITE => "queue file write error (#4.3.0)", + ); %cleanup_hard = ( - CLEANUP_STAT_RCPT => "no recipients specified (#5.1.0)", - CLEANUP_STAT_HOPS => "too many hops (#5.4.0)", - CLEANUP_STAT_SIZE => "message file too big (#5.3.4)", - CLEANUP_STAT_CONT => "message content rejected (#5.7.1)", -); + CLEANUP_STAT_RCPT => "no recipients specified (#5.1.0)", + CLEANUP_STAT_HOPS => "too many hops (#5.4.0)", + CLEANUP_STAT_SIZE => "message file too big (#5.3.4)", + CLEANUP_STAT_CONT => "message content rejected (#5.7.1)", + ); 1; diff --git a/lib/Qpsmtpd/SMTP.pm b/lib/Qpsmtpd/SMTP.pm index fd6dcf4..a74dead 100644 --- a/lib/Qpsmtpd/SMTP.pm +++ b/lib/Qpsmtpd/SMTP.pm @@ -16,6 +16,7 @@ use Qpsmtpd::Address (); use Qpsmtpd::Command; use Mail::Header (); + #use Data::Dumper; use POSIX qw(strftime); use Net::DNS; @@ -26,42 +27,44 @@ use Net::DNS; #$SIG{ALRM} = sub { warn "Connection Timed Out\n"; exit; }; sub new { - my $proto = shift; - my $class = ref($proto) || $proto; + my $proto = shift; + my $class = ref($proto) || $proto; - my %args = @_; + my %args = @_; - my $self = bless ({ args => \%args }, $class); + my $self = bless({args => \%args}, $class); - my (@commands) = qw(ehlo helo rset mail rcpt data help vrfy noop quit); - my (%commands); @commands{@commands} = ('') x @commands; - # this list of valid commands should probably be a method or a set of methods - $self->{_commands} = \%commands; - $self->SUPER::_restart(%args) if $args{restart}; # calls Qpsmtpd::_restart() - $self; + my (@commands) = qw(ehlo helo rset mail rcpt data help vrfy noop quit); + my (%commands); + @commands{@commands} = ('') x @commands; + + # this list of valid commands should probably be a method or a set of methods + $self->{_commands} = \%commands; + $self->SUPER::_restart(%args) if $args{restart}; # calls Qpsmtpd::_restart() + $self; } sub command_counter { - my $self = shift; - $self->{_counter} || 0; + my $self = shift; + $self->{_counter} || 0; } sub dispatch { my $self = shift; my ($cmd) = shift; - if ( ! $cmd ) { + if (!$cmd) { $self->run_hooks("unrecognized_command", '', @_); return 1; - }; + } $cmd = lc $cmd; - $self->{_counter}++; + $self->{_counter}++; - if ($cmd !~ /^(\w{1,12})$/ or !exists $self->{_commands}->{$1}) { - $self->run_hooks("unrecognized_command", $cmd, @_); - return 1; - } - $cmd = $1; + if ($cmd !~ /^(\w{1,12})$/ or !exists $self->{_commands}->{$1}) { + $self->run_hooks("unrecognized_command", $cmd, @_); + return 1; + } + $cmd = $1; my ($result) = eval { $self->$cmd(@_) }; $self->log(LOGERROR, "XX: $@") if $@; @@ -72,28 +75,28 @@ sub dispatch { sub unrecognized_command_respond { my ($self, $rc, $msg) = @_; if ($rc == DENY_DISCONNECT) { - $self->respond(521, @$msg); - $self->disconnect; + $self->respond(521, @$msg); + $self->disconnect; } elsif ($rc == DENY) { - $self->respond(500, @$msg); + $self->respond(500, @$msg); } elsif ($rc != DONE) { - $self->respond(500, "Unrecognized command"); + $self->respond(500, "Unrecognized command"); } } sub fault { - my $self = shift; - my ($msg) = shift || "program fault - command not performed"; - my ($name) = split /\s+/, $0, 2; - print STDERR $name,"[$$]: $msg ($!)\n"; - return $self->respond(451, "Internal error - try again later - " . $msg); + my $self = shift; + my ($msg) = shift || "program fault - command not performed"; + 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. $self->run_hooks("connect"); @@ -103,153 +106,188 @@ sub start_conversation { sub connect_respond { my ($self, $rc, $msg) = @_; if ($rc == DENY || $rc == DENY_DISCONNECT) { - $msg->[0] ||= 'Connection from you denied, bye bye.'; - $self->respond(550, @$msg); - $self->disconnect; + $msg->[0] ||= 'Connection from you denied, bye bye.'; + $self->respond(550, @$msg); + $self->disconnect; } elsif ($rc == DENYSOFT || $rc == DENYSOFT_DISCONNECT) { - $msg->[0] ||= 'Connection from you temporarily denied, bye bye.'; - $self->respond(450, @$msg); - $self->disconnect; + $msg->[0] ||= 'Connection from you temporarily denied, bye bye.'; + $self->respond(450, @$msg); + $self->disconnect; } elsif ($rc != DONE) { - my $greets = $self->config('smtpgreeting'); - if ( $greets ) { - $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."; - } + my $greets = $self->config('smtpgreeting'); + if ($greets) { + $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); + $self->respond(220, $greets); } } sub transaction { - my $self = shift; - return $self->{_transaction} || $self->reset_transaction(); + my $self = shift; + return $self->{_transaction} || $self->reset_transaction(); } sub reset_transaction { - my $self = shift; - $self->run_hooks("reset_transaction") if $self->{_transaction}; - return $self->{_transaction} = Qpsmtpd::Transaction->new(); + my $self = shift; + $self->run_hooks("reset_transaction") if $self->{_transaction}; + return $self->{_transaction} = Qpsmtpd::Transaction->new(); } - sub connection { - my $self = shift; - @_ and $self->{_connection} = shift; - return $self->{_connection} || ($self->{_connection} = Qpsmtpd::Connection->new()); + my $self = shift; + @_ and $self->{_connection} = shift; + return $self->{_connection} + || ($self->{_connection} = Qpsmtpd::Connection->new()); } sub helo { - my ($self, $line) = @_; - my ($rc, @msg) = $self->run_hooks('helo_parse'); - my ($ok, $hello_host, @stuff) = Qpsmtpd::Command->parse('helo', $line, $msg[0]); + my ($self, $line) = @_; + my ($rc, @msg) = $self->run_hooks('helo_parse'); + my ($ok, $hello_host, @stuff) = + Qpsmtpd::Command->parse('helo', $line, $msg[0]); - return $self->respond (501, - "helo requires domain/address - see RFC-2821 4.1.1.1") unless $hello_host; - my $conn = $self->connection; - return $self->respond (503, "but you already said HELO ...") if $conn->hello; + return $self->respond(501, + "helo requires domain/address - see RFC-2821 4.1.1.1") + unless $hello_host; + my $conn = $self->connection; + return $self->respond(503, "but you already said HELO ...") if $conn->hello; - $self->run_hooks("helo", $hello_host, @stuff); + $self->run_hooks("helo", $hello_host, @stuff); } sub helo_respond { - my ($self, $rc, $msg, $args) = @_; - my ($hello_host) = @$args; - if ($rc == DONE) { - # do nothing: - 1; - } elsif ($rc == DENY) { - $self->respond(550, @$msg); - } elsif ($rc == DENYSOFT) { - $self->respond(450, @$msg); - } elsif ($rc == DENY_DISCONNECT) { - $self->respond(550, @$msg); - $self->disconnect; - } elsif ($rc == DENYSOFT_DISCONNECT) { - $self->respond(450, @$msg); - $self->disconnect; - } else { - my $conn = $self->connection; - $conn->hello("helo"); - $conn->hello_host($hello_host); - $self->transaction; - $self->respond(250, $self->config('me') ." Hi " . $conn->remote_info . " [" . $conn->remote_ip ."]; I am so happy to meet you."); - } + my ($self, $rc, $msg, $args) = @_; + my ($hello_host) = @$args; + if ($rc == DONE) { + + # do nothing: + 1; + } + elsif ($rc == DENY) { + $self->respond(550, @$msg); + } + elsif ($rc == DENYSOFT) { + $self->respond(450, @$msg); + } + elsif ($rc == DENY_DISCONNECT) { + $self->respond(550, @$msg); + $self->disconnect; + } + elsif ($rc == DENYSOFT_DISCONNECT) { + $self->respond(450, @$msg); + $self->disconnect; + } + else { + my $conn = $self->connection; + $conn->hello("helo"); + $conn->hello_host($hello_host); + $self->transaction; + $self->respond( + 250, + $self->config('me') . " Hi " + . $conn->remote_info . " [" + . $conn->remote_ip + . "]; I am so happy to meet you." + ); + } } sub ehlo { - my ($self, $line) = @_; - my ($rc, @msg) = $self->run_hooks('ehlo_parse'); - my ($ok, $hello_host, @stuff) = Qpsmtpd::Command->parse('ehlo', $line, $msg[0]); - return $self->respond (501, - "ehlo requires domain/address - see RFC-2821 4.1.1.1") unless $hello_host; - my $conn = $self->connection; - return $self->respond (503, "but you already said HELO ...") if $conn->hello; + my ($self, $line) = @_; + my ($rc, @msg) = $self->run_hooks('ehlo_parse'); + my ($ok, $hello_host, @stuff) = + Qpsmtpd::Command->parse('ehlo', $line, $msg[0]); + return $self->respond(501, + "ehlo requires domain/address - see RFC-2821 4.1.1.1") + unless $hello_host; + my $conn = $self->connection; + return $self->respond(503, "but you already said HELO ...") if $conn->hello; - $self->run_hooks("ehlo", $hello_host, @stuff); + $self->run_hooks("ehlo", $hello_host, @stuff); } sub ehlo_respond { - my ($self, $rc, $msg, $args) = @_; - my ($hello_host) = @$args; - if ($rc == DONE) { - # do nothing: - 1; - } elsif ($rc == DENY) { - $self->respond(550, @$msg); - } elsif ($rc == DENYSOFT) { - $self->respond(450, @$msg); - } elsif ($rc == DENY_DISCONNECT) { - $self->respond(550, @$msg); - $self->disconnect; - } elsif ($rc == DENYSOFT_DISCONNECT) { - $self->respond(450, @$msg); - $self->disconnect; - } else { - my $conn = $self->connection; - $conn->hello("ehlo"); - $conn->hello_host($hello_host); - $self->transaction; + my ($self, $rc, $msg, $args) = @_; + my ($hello_host) = @$args; + if ($rc == DONE) { - my @capabilities = $self->transaction->notes('capabilities') - ? @{ $self->transaction->notes('capabilities') } - : (); + # do nothing: + 1; + } + elsif ($rc == DENY) { + $self->respond(550, @$msg); + } + elsif ($rc == DENYSOFT) { + $self->respond(450, @$msg); + } + elsif ($rc == DENY_DISCONNECT) { + $self->respond(550, @$msg); + $self->disconnect; + } + elsif ($rc == DENYSOFT_DISCONNECT) { + $self->respond(450, @$msg); + $self->disconnect; + } + else { + my $conn = $self->connection; + $conn->hello("ehlo"); + $conn->hello_host($hello_host); + $self->transaction; - # Check for possible AUTH mechanisms -HOOK: foreach my $hook ( keys %{$self->hooks} ) { - if ( $hook =~ m/^auth-?(.+)?$/ ) { - if ( defined $1 ) { - $auth_mechanisms{uc($1)} = 1; - } - else { # at least one polymorphous auth provider - %auth_mechanisms = map {$_,1} qw(PLAIN CRAM-MD5 LOGIN); - last HOOK; + my @capabilities = + $self->transaction->notes('capabilities') + ? @{$self->transaction->notes('capabilities')} + : (); + + # Check for possible AUTH mechanisms + HOOK: foreach my $hook (keys %{$self->hooks}) { + if ($hook =~ m/^auth-?(.+)?$/) { + if (defined $1) { + $auth_mechanisms{uc($1)} = 1; + } + else { # at least one polymorphous auth provider + %auth_mechanisms = map { $_, 1 } qw(PLAIN CRAM-MD5 LOGIN); + last HOOK; + } } } - } - # Check if we should only offer AUTH after TLS is completed - my $tls_before_auth = ($self->config('tls_before_auth') ? ($self->config('tls_before_auth'))[0] && $self->transaction->notes('tls_enabled') : 0); - if ( %auth_mechanisms && !$tls_before_auth) { - push @capabilities, 'AUTH '.join(" ",keys(%auth_mechanisms)); - $self->{_commands}->{'auth'} = ""; - } + # Check if we should only offer AUTH after TLS is completed + my $tls_before_auth = + ($self->config('tls_before_auth') + ? ($self->config('tls_before_auth'))[0] + && $self->transaction->notes('tls_enabled') + : 0); + if (%auth_mechanisms && !$tls_before_auth) { + push @capabilities, 'AUTH ' . join(" ", keys(%auth_mechanisms)); + $self->{_commands}->{'auth'} = ""; + } - $self->respond(250, - $self->config("me") . " Hi " . $conn->remote_info . " [" . $conn->remote_ip ."]", - "PIPELINING", - "8BITMIME", - ($self->config('databytes') ? "SIZE ". ($self->config('databytes'))[0] : ()), - @capabilities, - ); - } + $self->respond( + 250, + $self->config("me") . " Hi " + . $conn->remote_info . " [" + . $conn->remote_ip . "]", + "PIPELINING", + "8BITMIME", + ( + $self->config('databytes') + ? "SIZE " . ($self->config('databytes'))[0] + : () + ), + @capabilities, + ); + } } sub auth { @@ -261,57 +299,59 @@ sub auth_parse_respond { my ($self, $rc, $msg, $args) = @_; my ($line) = @$args; - my ($ok, $mechanism, @stuff) = Qpsmtpd::Command->parse('auth', $line, $msg->[0]); - return $self->respond(501, $mechanism || "Syntax error in command") + my ($ok, $mechanism, @stuff) = + Qpsmtpd::Command->parse('auth', $line, $msg->[0]); + return $self->respond(501, $mechanism || "Syntax error in command") unless ($ok == OK); $mechanism = lc($mechanism); #they AUTH'd once already - return $self->respond( 503, "but you already said AUTH ..." ) - if ( defined $self->{_auth} && $self->{_auth} == OK ); + return $self->respond(503, "but you already said AUTH ...") + if (defined $self->{_auth} && $self->{_auth} == OK); - return $self->respond( 503, "AUTH not defined for HELO" ) - if ( $self->connection->hello eq "helo" ); + return $self->respond(503, "AUTH not defined for HELO") + if ($self->connection->hello eq "helo"); - return $self->respond( 503, "SSL/TLS required before AUTH" ) - if ( ($self->config('tls_before_auth'))[0] - && $self->transaction->notes('tls_enabled') ); + return $self->respond(503, "SSL/TLS required before AUTH") + if (($self->config('tls_before_auth'))[0] + && $self->transaction->notes('tls_enabled')); # we don't have a plugin implementing this auth mechanism, 504 - if( exists $auth_mechanisms{uc($mechanism)} ) { - return $self->{_auth} = Qpsmtpd::Auth::SASL( $self, $mechanism, @stuff ); - }; + if (exists $auth_mechanisms{uc($mechanism)}) { + return $self->{_auth} = Qpsmtpd::Auth::SASL($self, $mechanism, @stuff); + } - $self->respond( 504, "Unimplemented authentification mechanism: $mechanism" ); + $self->respond(504, "Unimplemented authentification mechanism: $mechanism"); return DENY; } sub mail { - my ($self, $line) = @_; - # -> from RFC2821 - # The MAIL command (or the obsolete SEND, SOML, or SAML commands) - # begins a mail transaction. Once started, a mail transaction - # consists of a transaction beginning command, one or more RCPT - # commands, and a DATA command, in that order. A mail transaction - # 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. + my ($self, $line) = @_; - # sendmail (8.11) rejects a second MAIL command. + # -> from RFC2821 + # The MAIL command (or the obsolete SEND, SOML, or SAML commands) + # begins a mail transaction. Once started, a mail transaction + # consists of a transaction beginning command, one or more RCPT + # commands, and a DATA command, in that order. A mail transaction + # 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. - # qmail-smtpd (1.03) accepts it and just starts a new transaction. - # Since we are a qmail-smtpd thing we will do the same. + # sendmail (8.11) rejects a second MAIL command. - $self->reset_transaction; - - if ( ! $self->connection->hello) { - return $self->respond(503, "please say hello first ..."); - }; + # 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; + + if (!$self->connection->hello) { + return $self->respond(503, "please say hello first ..."); + } $self->log(LOGDEBUG, "full from_parameter: $line"); $self->run_hooks("mail_parse", $line); @@ -320,17 +360,19 @@ sub mail { sub mail_parse_respond { my ($self, $rc, $msg, $args) = @_; my ($line) = @$args; - my ($ok, $from, @params) = Qpsmtpd::Command->parse('mail', $line, $msg->[0]); - return $self->respond(501, $from || "Syntax error in command") - unless ($ok == OK); + my ($ok, $from, @params) = + Qpsmtpd::Command->parse('mail', $line, $msg->[0]); + return $self->respond(501, $from || "Syntax error in command") + unless ($ok == OK); my %param; foreach (@params) { - my ($k,$v) = split /=/, $_, 2; + my ($k, $v) = split /=/, $_, 2; $param{lc $k} = $v; } + # to support addresses without <> we now require a plugin - # hooking "mail_pre" to - # return (OK, "<$from>"); + # hooking "mail_pre" to + # return (OK, "<$from>"); # (...or anything else parseable by Qpsmtpd::Address ;-)) # see also comment in sub rcpt() $self->run_hooks("mail_pre", $from, \%param); @@ -340,20 +382,21 @@ sub mail_pre_respond { my ($self, $rc, $msg, $args) = @_; my ($from, $param) = @$args; if ($rc == OK) { - $from = shift @$msg; + $from = shift @$msg; } $self->log(LOGDEBUG, "from email address : [$from]"); - return $self->respond(501, "could not parse your mail from command") + return $self->respond(501, "could not parse your mail from command") unless $from =~ /^<.*>$/; if ($from eq "<>" or $from =~ m/\[undefined\]/ or $from eq "<#@[]>") { - $from = Qpsmtpd::Address->new("<>"); - } - else { - $from = (Qpsmtpd::Address->parse($from))[0]; + $from = Qpsmtpd::Address->new("<>"); } - return $self->respond(501, "could not parse your mail from command") unless $from; + else { + $from = (Qpsmtpd::Address->parse($from))[0]; + } + return $self->respond(501, "could not parse your mail from command") + unless $from; $self->run_hooks("mail", $from, %$param); } @@ -362,300 +405,313 @@ sub mail_respond { my ($self, $rc, $msg, $args) = @_; my ($from, $param) = @$args; if ($rc == DONE) { - return 1; + return 1; } elsif ($rc == DENY) { - $msg->[0] ||= $from->format . ', denied'; - $self->log(LOGINFO, "deny mail from " . $from->format . " (@$msg)"); - $self->respond(550, @$msg); + $msg->[0] ||= $from->format . ', denied'; + $self->log(LOGINFO, "deny mail from " . $from->format . " (@$msg)"); + $self->respond(550, @$msg); } elsif ($rc == DENYSOFT) { - $msg->[0] ||= $from->format . ', temporarily denied'; - $self->log(LOGINFO, "denysoft mail from " . $from->format . " (@$msg)"); - $self->respond(450, @$msg); + $msg->[0] ||= $from->format . ', temporarily denied'; + $self->log(LOGINFO, "denysoft mail from " . $from->format . " (@$msg)"); + $self->respond(450, @$msg); } elsif ($rc == DENY_DISCONNECT) { - $msg->[0] ||= $from->format . ', denied'; - $self->log(LOGINFO, "deny mail from " . $from->format . " (@$msg)"); - $self->respond(550, @$msg); - $self->disconnect; + $msg->[0] ||= $from->format . ', denied'; + $self->log(LOGINFO, "deny mail from " . $from->format . " (@$msg)"); + $self->respond(550, @$msg); + $self->disconnect; } elsif ($rc == DENYSOFT_DISCONNECT) { - $msg->[0] ||= $from->format . ', temporarily denied'; - $self->log(LOGINFO, "denysoft mail from " . $from->format . " (@$msg)"); - $self->respond(421, @$msg); - $self->disconnect; + $msg->[0] ||= $from->format . ', temporarily denied'; + $self->log(LOGINFO, "denysoft mail from " . $from->format . " (@$msg)"); + $self->respond(421, @$msg); + $self->disconnect; } - else { # includes OK - $self->log(LOGDEBUG, "getting mail from ".$from->format); - $self->respond(250, $from->format . ", sender OK - how exciting to get mail from you!"); - $self->transaction->sender($from); + else { # includes OK + $self->log(LOGDEBUG, "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, $line) = @_; - $self->run_hooks("rcpt_parse", $line); + my ($self, $line) = @_; + $self->run_hooks("rcpt_parse", $line); } sub rcpt_parse_respond { - my ($self, $rc, $msg, $args) = @_; - my ($line) = @$args; - my ($ok, $rcpt, @param) = Qpsmtpd::Command->parse("rcpt", $line, $msg->[0]); - return $self->respond(501, $rcpt || "Syntax error in command") - unless ($ok == OK); - return $self->respond(503, "Use MAIL before RCPT") unless $self->transaction->sender; + my ($self, $rc, $msg, $args) = @_; + my ($line) = @$args; + my ($ok, $rcpt, @param) = Qpsmtpd::Command->parse("rcpt", $line, $msg->[0]); + return $self->respond(501, $rcpt || "Syntax error in command") + unless ($ok == OK); + return $self->respond(503, "Use MAIL before RCPT") + unless $self->transaction->sender; - my %param; - foreach (@param) { - my ($k,$v) = split /=/, $_, 2; - $param{lc $k} = $v; - } - # to support addresses without <> we now require a plugin - # hooking "rcpt_pre" to - # return (OK, "<$rcpt>"); - # (... or anything else parseable by Qpsmtpd::Address ;-)) - # this means, a plugin can decide to (pre-)accept - # addresses like or - # by removing the trailing "."/" " from this example... - $self->run_hooks("rcpt_pre", $rcpt, \%param); + my %param; + foreach (@param) { + my ($k, $v) = split /=/, $_, 2; + $param{lc $k} = $v; + } + + # to support addresses without <> we now require a plugin + # hooking "rcpt_pre" to + # return (OK, "<$rcpt>"); + # (... or anything else parseable by Qpsmtpd::Address ;-)) + # this means, a plugin can decide to (pre-)accept + # addresses like or + # by removing the trailing "."/" " from this example... + $self->run_hooks("rcpt_pre", $rcpt, \%param); } sub rcpt_pre_respond { - my ($self, $rc, $msg, $args) = @_; - my ($rcpt, $param) = @$args; - if ($rc == OK) { - $rcpt = shift @$msg; - } - $self->log(LOGDEBUG, "to email address : [$rcpt]"); - return $self->respond(501, "could not parse recipient") - unless $rcpt =~ /^<.*>$/; + my ($self, $rc, $msg, $args) = @_; + my ($rcpt, $param) = @$args; + if ($rc == OK) { + $rcpt = shift @$msg; + } + $self->log(LOGDEBUG, "to email address : [$rcpt]"); + return $self->respond(501, "could not parse recipient") + unless $rcpt =~ /^<.*>$/; - $rcpt = (Qpsmtpd::Address->parse($rcpt))[0]; + $rcpt = (Qpsmtpd::Address->parse($rcpt))[0]; - return $self->respond(501, "could not parse recipient") - if (!$rcpt or ($rcpt->format eq '<>')); + return $self->respond(501, "could not parse recipient") + if (!$rcpt or ($rcpt->format eq '<>')); - $self->run_hooks("rcpt", $rcpt, %$param); + $self->run_hooks("rcpt", $rcpt, %$param); } sub rcpt_respond { - my ($self, $rc, $msg, $args) = @_; - my ($rcpt, $param) = @$args; - if ($rc == DONE) { - return 1; - } - elsif ($rc == DENY) { - $msg->[0] ||= 'relaying denied'; - $self->respond(550, @$msg); - } - elsif ($rc == DENYSOFT) { - $msg->[0] ||= 'relaying denied'; - return $self->respond(450, @$msg); - } - elsif ($rc == DENY_DISCONNECT) { - $msg->[0] ||= 'delivery denied'; - $self->log(LOGDEBUG, "delivery denied (@$msg)"); - $self->respond(550, @$msg); - $self->disconnect; - } - elsif ($rc == DENYSOFT_DISCONNECT) { - $msg->[0] ||= 'relaying denied'; - $self->log(LOGDEBUG, "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; + my ($self, $rc, $msg, $args) = @_; + my ($rcpt, $param) = @$args; + if ($rc == DONE) { + return 1; + } + elsif ($rc == DENY) { + $msg->[0] ||= 'relaying denied'; + $self->respond(550, @$msg); + } + elsif ($rc == DENYSOFT) { + $msg->[0] ||= 'relaying denied'; + return $self->respond(450, @$msg); + } + elsif ($rc == DENY_DISCONNECT) { + $msg->[0] ||= 'delivery denied'; + $self->log(LOGDEBUG, "delivery denied (@$msg)"); + $self->respond(550, @$msg); + $self->disconnect; + } + elsif ($rc == DENYSOFT_DISCONNECT) { + $msg->[0] ||= 'relaying denied'; + $self->log(LOGDEBUG, "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, @args) = @_; - $self->run_hooks("help", @args); + my ($self, @args) = @_; + $self->run_hooks("help", @args); } sub help_respond { - my ($self, $rc, $msg, $args) = @_; + my ($self, $rc, $msg, $args) = @_; - return 1 - if $rc == DONE; + return 1 + if $rc == DONE; - if ($rc == DENY) { - $msg->[0] ||= "Syntax error, command not recognized"; - $self->respond(500, @$msg); - } - else { - unless ($msg->[0]) { - @$msg = ( - "This is qpsmtpd " . ($self->config('smtpgreeting') ? '' : $self->version), - "See http://smtpd.develooper.com/", - 'To report bugs or send comments, mail to .'); + if ($rc == DENY) { + $msg->[0] ||= "Syntax error, command not recognized"; + $self->respond(500, @$msg); } - $self->respond(214, @$msg); - } - return 1; + else { + unless ($msg->[0]) { + @$msg = ( + "This is qpsmtpd " + . ($self->config('smtpgreeting') ? '' : $self->version), + "See http://smtpd.develooper.com/", +'To report bugs or send comments, mail to .' + ); + } + $self->respond(214, @$msg); + } + return 1; } sub noop { - my $self = shift; - $self->run_hooks("noop"); + my $self = shift; + $self->run_hooks("noop"); } sub noop_respond { - my ($self, $rc, $msg, $args) = @_; - return 1 if $rc == DONE; + my ($self, $rc, $msg, $args) = @_; + return 1 if $rc == DONE; - if ($rc == DENY || $rc == DENY_DISCONNECT) { - $msg->[0] ||= "Stop wasting my time."; # FIXME: better default message? - $self->respond(500, @$msg); - $self->disconnect if $rc == DENY_DISCONNECT; + if ($rc == DENY || $rc == DENY_DISCONNECT) { + $msg->[0] ||= "Stop wasting my time."; # FIXME: better default message? + $self->respond(500, @$msg); + $self->disconnect if $rc == DENY_DISCONNECT; + return 1; + } + + $self->respond(250, "OK"); return 1; - } - - $self->respond(250, "OK"); - return 1; } sub vrfy { - my $self = shift; + 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. + # 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. - $self->run_hooks("vrfy"); + $self->run_hooks("vrfy"); } sub vrfy_respond { - my ($self, $rc, $msg, $args) = @_; - if ($rc == DONE) { - return 1; - } - elsif ($rc == DENY) { - $msg->[0] ||= "Access Denied"; - $self->respond(554, @$msg); - $self->reset_transaction(); - return 1; - } - elsif ($rc == OK) { - $msg->[0] ||= "User OK"; - $self->respond(250, @$msg); - return 1; - } - else { # $rc == DECLINED or anything else - $self->respond(252, "Just try sending a mail and we'll see how it turns out ..."); - return 1; - } + my ($self, $rc, $msg, $args) = @_; + if ($rc == DONE) { + return 1; + } + elsif ($rc == DENY) { + $msg->[0] ||= "Access Denied"; + $self->respond(554, @$msg); + $self->reset_transaction(); + return 1; + } + elsif ($rc == OK) { + $msg->[0] ||= "User OK"; + $self->respond(250, @$msg); + return 1; + } + else { # $rc == DECLINED or anything else + $self->respond(252, + "Just try sending a mail and we'll see how it turns out ..."); + return 1; + } } sub rset { - my $self = shift; - $self->reset_transaction; - $self->respond(250, "OK"); + my $self = shift; + $self->reset_transaction; + $self->respond(250, "OK"); } sub quit { - my $self = shift; - $self->run_hooks("quit"); + my $self = shift; + $self->run_hooks("quit"); } sub quit_respond { - my ($self, $rc, $msg, $args) = @_; - if ($rc != DONE) { - $msg->[0] ||= $self->config('me') . " closing connection. Have a wonderful day."; - $self->respond(221, @$msg); - } - $self->disconnect(); + my ($self, $rc, $msg, $args) = @_; + if ($rc != DONE) { + $msg->[0] ||= + $self->config('me') . " closing connection. Have a wonderful day."; + $self->respond(221, @$msg); + } + $self->disconnect(); } sub disconnect { - my $self = shift; - $self->run_hooks("disconnect"); - $self->connection->notes(disconnected => 1); - $self->reset_transaction; + my $self = shift; + $self->run_hooks("disconnect"); + $self->connection->notes(disconnected => 1); + $self->reset_transaction; } sub data { - my $self = shift; - $self->run_hooks("data"); + my $self = shift; + $self->run_hooks("data"); } sub data_respond { - my ($self, $rc, $msg, $args) = @_; - if ($rc == DONE) { - return 1; - } - elsif ($rc == DENY) { - $msg->[0] ||= "Message denied"; - $self->respond(554, @$msg); - $self->reset_transaction(); - return 1; - } - elsif ($rc == DENYSOFT) { - $msg->[0] ||= "Message denied temporarily"; - $self->respond(451, @$msg); - $self->reset_transaction(); - return 1; - } - elsif ($rc == DENY_DISCONNECT) { - $msg->[0] ||= "Message denied"; - $self->respond(554, @$msg); - $self->disconnect; - return 1; - } - elsif ($rc == DENYSOFT_DISCONNECT) { - $msg->[0] ||= "Message denied temporarily"; - $self->respond(421, @$msg); - $self->disconnect; - return 1; - } - $self->respond(503, "MAIL first"), return 1 unless $self->transaction->sender; - $self->respond(503, "RCPT first"), return 1 unless $self->transaction->recipients; - $self->respond(354, "go ahead"); - - my $buffer = ''; - my $size = 0; - my $i = 0; - my $max_size = ($self->config('databytes'))[0] || 0; # this should work in scalar context - my $blocked = ""; - my %matches; - my $in_header = 1; - my $complete = 0; + my ($self, $rc, $msg, $args) = @_; + if ($rc == DONE) { + return 1; + } + elsif ($rc == DENY) { + $msg->[0] ||= "Message denied"; + $self->respond(554, @$msg); + $self->reset_transaction(); + return 1; + } + elsif ($rc == DENYSOFT) { + $msg->[0] ||= "Message denied temporarily"; + $self->respond(451, @$msg); + $self->reset_transaction(); + return 1; + } + elsif ($rc == DENY_DISCONNECT) { + $msg->[0] ||= "Message denied"; + $self->respond(554, @$msg); + $self->disconnect; + return 1; + } + elsif ($rc == DENYSOFT_DISCONNECT) { + $msg->[0] ||= "Message denied temporarily"; + $self->respond(421, @$msg); + $self->disconnect; + return 1; + } + $self->respond(503, "MAIL first"), return 1 + unless $self->transaction->sender; + $self->respond(503, "RCPT first"), return 1 + unless $self->transaction->recipients; + $self->respond(354, "go ahead"); - $self->log(LOGDEBUG, "max_size: $max_size / size: $size"); + my $buffer = ''; + my $size = 0; + my $i = 0; + my $max_size = + ($self->config('databytes'))[0] || 0; # this should work in scalar context + my $blocked = ""; + my %matches; + my $in_header = 1; + my $complete = 0; - my $header = Mail::Header->new(Modify => 0, MailFrom => "COERCE"); + $self->log(LOGDEBUG, "max_size: $max_size / size: $size"); - my $timeout = $self->config('timeout'); - while (defined($_ = $self->getline($timeout))) { - if ( $_ eq ".\r\n" ) { - $complete++; - $_ = ''; - }; - $i++; + my $header = Mail::Header->new(Modify => 0, MailFrom => "COERCE"); + + my $timeout = $self->config('timeout'); + while (defined($_ = $self->getline($timeout))) { + if ($_ eq ".\r\n") { + $complete++; + $_ = ''; + } + $i++; # should probably use \012 and \015 in these checks instead of \r and \n ... - # Reject messages that have either bare LF or CR. rjkaes noticed a - # lot of spam that is malformed in the header. + # Reject messages that have either bare LF or CR. rjkaes noticed a + # lot of spam that is malformed in the header. - ($_ eq ".\n" or $_ eq ".\r") - and $self->respond(421, "See http://smtpd.develooper.com/barelf.html") - and return $self->disconnect; + ($_ eq ".\n" or $_ eq ".\r") + and $self->respond(421, "See http://smtpd.develooper.com/barelf.html") + and return $self->disconnect; - # add a transaction->blocked check back here when we have line by line plugin access... - unless (($max_size and $size > $max_size)) { - s/\r\n$/\n/; - s/^\.\./\./; - if ($in_header && (m/^$/ || $complete > 0)) { - $in_header = 0; - my @headers = split /^/m, $buffer; +# add a transaction->blocked check back here when we have line by line plugin access... + unless (($max_size and $size > $max_size)) { + s/\r\n$/\n/; + s/^\.\./\./; + if ($in_header && (m/^$/ || $complete > 0)) { + $in_header = 0; + my @headers = split /^/m, $buffer; # ... need to check that we don't reformat any of the received lines. # @@ -664,199 +720,218 @@ sub data_respond { # gateway MUST prepend a Received: line, but it MUST NOT alter in any # way a Received: line that is already in the header. - $header->extract(\@headers); - #$header->add("X-SMTPD", "qpsmtpd/".$self->version.", http://smtpd.develooper.com/"); + $header->extract(\@headers); - $buffer = ""; +#$header->add("X-SMTPD", "qpsmtpd/".$self->version.", http://smtpd.develooper.com/"); - $self->transaction->header($header); + $buffer = ""; - # NOTE: This will not work properly under async. A - # data_headers_end_respond needs to be created. - my ($rc, $msg) = $self->run_hooks('data_headers_end'); - if ($rc == DENY_DISCONNECT) { - $self->respond(554, $msg || "Message denied"); - $self->disconnect; - return 1; - } elsif ($rc == DENYSOFT_DISCONNECT) { - $self->respond(421, $msg || "Message denied temporarily"); - $self->disconnect; - return 1; + $self->transaction->header($header); + + # NOTE: This will not work properly under async. A + # data_headers_end_respond needs to be created. + my ($rc, $msg) = $self->run_hooks('data_headers_end'); + if ($rc == DENY_DISCONNECT) { + $self->respond(554, $msg || "Message denied"); + $self->disconnect; + return 1; + } + elsif ($rc == DENYSOFT_DISCONNECT) { + $self->respond(421, $msg || "Message denied temporarily"); + $self->disconnect; + return 1; + } + + # Save the start of just the body itself + $self->transaction->set_body_start(); + + } + + # grab a copy of all of the header lines + if ($in_header) { + $buffer .= $_; + } + + # copy all lines into the spool file, including the headers + # we will create a new header later before sending onwards + $self->transaction->body_write($_) if !$complete; + $size += length $_; } + last if $complete > 0; - # Save the start of just the body itself - $self->transaction->set_body_start(); - - } - - # grab a copy of all of the header lines - if ($in_header) { - $buffer .= $_; - } - - # copy all lines into the spool file, including the headers - # we will create a new header later before sending onwards - $self->transaction->body_write($_) if ! $complete; - $size += length $_; + #$self->log(LOGDEBUG, "size is at $size\n") unless ($i % 300); } - last if $complete > 0; - #$self->log(LOGDEBUG, "size is at $size\n") unless ($i % 300); - } - $self->log(LOGDEBUG, "max_size: $max_size / size: $size"); + $self->log(LOGDEBUG, "max_size: $max_size / size: $size"); - my $smtp = $self->connection->hello eq "ehlo" ? "ESMTP" : "SMTP"; - my $esmtp = substr($smtp,0,1) eq "E"; - my $authheader = ''; - my $sslheader = ''; + 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->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"; - } + 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); + $header->add("Received", + $self->received_line($smtp, $authheader, $sslheader), 0); - # if we get here without seeing a terminator, the connection is - # probably dead. - unless ( $complete ) { - $self->respond(451, "Incomplete DATA"); - $self->reset_transaction; # clean up after ourselves - return 1; - } + # if we get here without seeing a terminator, the connection is + # probably dead. + unless ($complete) { + $self->respond(451, "Incomplete DATA"); + $self->reset_transaction; # clean up after ourselves + return 1; + } - #$self->respond(550, $self->transaction->blocked),return 1 if ($self->transaction->blocked); - if ($max_size and $size > $max_size) { - $self->log(LOGALERT, "Message too big: size: $size (max size: $max_size)"); - $self->respond(552, "Message too big!"); - $self->reset_transaction; # clean up after ourselves - return 1; - } +#$self->respond(550, $self->transaction->blocked),return 1 if ($self->transaction->blocked); + if ($max_size and $size > $max_size) { + $self->log(LOGALERT, + "Message too big: size: $size (max size: $max_size)"); + $self->respond(552, "Message too big!"); + $self->reset_transaction; # clean up after ourselves + return 1; + } - $self->run_hooks("data_post"); + $self->run_hooks("data_post"); } sub received_line { - my ($self, $smtp, $authheader, $sslheader) = @_; - my ($rc, @received) = $self->run_hooks("received_line", $smtp, $authheader, $sslheader); - if ($rc == YIELD) { - die "YIELD not supported for received_line hook"; - } - elsif ($rc == OK) { - return join("\n", @received); - } - else { # assume $rc == DECLINED - return "from ".$self->connection->remote_info - ." (HELO ".$self->connection->hello_host . ") (".$self->connection->remote_ip - . ")\n $authheader by ".$self->config('me')." (qpsmtpd/".$self->version - .") with $sslheader$smtp; ". (strftime('%a, %d %b %Y %H:%M:%S %z', localtime)) - } + my ($self, $smtp, $authheader, $sslheader) = @_; + my ($rc, @received) = + $self->run_hooks("received_line", $smtp, $authheader, $sslheader); + if ($rc == YIELD) { + die "YIELD not supported for received_line hook"; + } + elsif ($rc == OK) { + return join("\n", @received); + } + else { # assume $rc == DECLINED + return + "from " + . $self->connection->remote_info + . " (HELO " + . $self->connection->hello_host . ") (" + . $self->connection->remote_ip + . ")\n $authheader by " + . $self->config('me') + . " (qpsmtpd/" + . $self->version + . ") with $sslheader$smtp; " + . (strftime('%a, %d %b %Y %H:%M:%S %z', localtime)); + } } sub data_post_respond { - my ($self, $rc, $msg, $args) = @_; - if ($rc == DONE) { - return 1; - } - elsif ($rc == DENY) { - $msg->[0] ||= "Message denied"; - $self->respond(552, @$msg); - # DATA is always the end of a "transaction" - return $self->reset_transaction; - } - elsif ($rc == DENYSOFT) { - $msg->[0] ||= "Message denied temporarily"; - $self->respond(452, @$msg); - # DATA is always the end of a "transaction" - return $self->reset_transaction; - } - elsif ($rc == DENY_DISCONNECT) { - $msg->[0] ||= "Message denied"; - $self->respond(552, @$msg); - $self->disconnect; - return 1; - } - elsif ($rc == DENYSOFT_DISCONNECT) { - $msg->[0] ||= "Message denied temporarily"; - $self->respond(452, @$msg); - $self->disconnect; - return 1; - } - else { - $self->queue($self->transaction); - } + my ($self, $rc, $msg, $args) = @_; + if ($rc == DONE) { + return 1; + } + elsif ($rc == DENY) { + $msg->[0] ||= "Message denied"; + $self->respond(552, @$msg); + + # DATA is always the end of a "transaction" + return $self->reset_transaction; + } + elsif ($rc == DENYSOFT) { + $msg->[0] ||= "Message denied temporarily"; + $self->respond(452, @$msg); + + # DATA is always the end of a "transaction" + return $self->reset_transaction; + } + elsif ($rc == DENY_DISCONNECT) { + $msg->[0] ||= "Message denied"; + $self->respond(552, @$msg); + $self->disconnect; + return 1; + } + elsif ($rc == DENYSOFT_DISCONNECT) { + $msg->[0] ||= "Message denied temporarily"; + $self->respond(452, @$msg); + $self->disconnect; + return 1; + } + else { + $self->queue($self->transaction); + } } sub getline { - my ($self, $timeout) = @_; - - alarm $timeout; - my $line = ; # default implementation - alarm 0; - return $line; + my ($self, $timeout) = @_; + + alarm $timeout; + my $line = ; # default implementation + alarm 0; + return $line; } sub queue { - my ($self, $transaction) = @_; + my ($self, $transaction) = @_; - # First fire any queue_pre hooks - $self->run_hooks("queue_pre"); + # First fire any queue_pre hooks + $self->run_hooks("queue_pre"); } sub queue_pre_respond { - my ($self, $rc, $msg, $args) = @_; - if ($rc == DONE) { - return 1; - } - elsif ($rc != OK and $rc != DECLINED and $rc != 0 ) { - return $self->log(LOGERROR, "pre plugin returned illegal value"); - return 0; - } + my ($self, $rc, $msg, $args) = @_; + if ($rc == DONE) { + return 1; + } + elsif ($rc != OK and $rc != DECLINED and $rc != 0) { + return $self->log(LOGERROR, "pre plugin returned illegal value"); + return 0; + } - # If we got this far, run the queue hooks - $self->run_hooks("queue"); + # If we got this far, run the queue hooks + $self->run_hooks("queue"); } sub queue_respond { - my ($self, $rc, $msg, $args) = @_; - - # reset transaction if we queued the mail - $self->reset_transaction; - - if ($rc == DONE) { - return 1; - } - elsif ($rc == OK) { - $msg->[0] ||= 'Queued'; - $self->respond(250, @$msg); - } - elsif ($rc == DENY) { - $msg->[0] ||= 'Message denied'; - $self->respond(552, @$msg); - } - elsif ($rc == DENYSOFT) { - $msg->[0] ||= 'Message denied temporarily'; - $self->respond(452, @$msg); - } - else { - $msg->[0] ||= 'Queuing declined or disabled; try again later'; - $self->respond(451, @$msg); - } - - # And finally run any queue_post hooks - $self->run_hooks("queue_post"); + my ($self, $rc, $msg, $args) = @_; + + # reset transaction if we queued the mail + $self->reset_transaction; + + if ($rc == DONE) { + return 1; + } + elsif ($rc == OK) { + $msg->[0] ||= 'Queued'; + $self->respond(250, @$msg); + } + elsif ($rc == DENY) { + $msg->[0] ||= 'Message denied'; + $self->respond(552, @$msg); + } + elsif ($rc == DENYSOFT) { + $msg->[0] ||= 'Message denied temporarily'; + $self->respond(452, @$msg); + } + else { + $msg->[0] ||= 'Queuing declined or disabled; try again later'; + $self->respond(451, @$msg); + } + + # And finally run any queue_post hooks + $self->run_hooks("queue_post"); } sub queue_post_respond { - my ($self, $rc, $msg, $args) = @_; - $self->log(LOGERROR, @$msg) unless ($rc == OK or $rc == 0); + my ($self, $rc, $msg, $args) = @_; + $self->log(LOGERROR, @$msg) unless ($rc == OK or $rc == 0); } - 1; diff --git a/lib/Qpsmtpd/SMTP/Prefork.pm b/lib/Qpsmtpd/SMTP/Prefork.pm index af8fb8e..20b05b7 100644 --- a/lib/Qpsmtpd/SMTP/Prefork.pm +++ b/lib/Qpsmtpd/SMTP/Prefork.pm @@ -4,27 +4,28 @@ use Qpsmtpd::Constants; @ISA = qw(Qpsmtpd::SMTP); sub dispatch { - my $self = shift; - my ($cmd) = lc shift; + my $self = shift; + my ($cmd) = lc shift; - $self->{_counter}++; + $self->{_counter}++; - if ($cmd !~ /^(\w{1,12})$/ or !exists $self->{_commands}->{$1}) { - $self->run_hooks("unrecognized_command", $cmd, @_); - return 1; - } - $cmd = $1; - - if (1 or $self->{_commands}->{$cmd} and $self->can($cmd)) { - my ($result) = eval { $self->$cmd(@_) }; - if ($@ =~ /^disconnect_tcpserver/) { - die "disconnect_tcpserver"; - } elsif ($@) { - $self->log(LOGERROR, "XX: $@") if $@; + if ($cmd !~ /^(\w{1,12})$/ or !exists $self->{_commands}->{$1}) { + $self->run_hooks("unrecognized_command", $cmd, @_); + return 1; } - return $result if defined $result; - return $self->fault("command '$cmd' failed unexpectedly"); - } + $cmd = $1; - return; + if (1 or $self->{_commands}->{$cmd} and $self->can($cmd)) { + my ($result) = eval { $self->$cmd(@_) }; + if ($@ =~ /^disconnect_tcpserver/) { + die "disconnect_tcpserver"; + } + elsif ($@) { + $self->log(LOGERROR, "XX: $@") if $@; + } + return $result if defined $result; + return $self->fault("command '$cmd' failed unexpectedly"); + } + + return; } diff --git a/lib/Qpsmtpd/TcpServer.pm b/lib/Qpsmtpd/TcpServer.pm index e4af474..8641576 100644 --- a/lib/Qpsmtpd/TcpServer.pm +++ b/lib/Qpsmtpd/TcpServer.pm @@ -10,12 +10,15 @@ use POSIX (); my $has_ipv6 = 0; if ( - eval {require Socket6;} && + eval { require Socket6; } + && + # INET6 prior to 2.01 will not work; sorry. - eval {require IO::Socket::INET6; IO::Socket::INET6->VERSION("2.00");} - ) { + eval { require IO::Socket::INET6; IO::Socket::INET6->VERSION("2.00"); } + ) +{ Socket6->import(qw(inet_ntop)); - $has_ipv6=1; + $has_ipv6 = 1; } sub has_ipv6 { @@ -33,25 +36,31 @@ sub start_connection { ); if ($ENV{TCPREMOTEIP}) { - # started from tcpserver (or some other superserver which - # exports the TCPREMOTE* variables. - $remote_ip = $ENV{TCPREMOTEIP}; - $remote_host = $ENV{TCPREMOTEHOST} || "[$remote_ip]"; - $remote_info = $ENV{TCPREMOTEINFO} ? "$ENV{TCPREMOTEINFO}\@$remote_host" : $remote_host; + + # started from tcpserver (or some other superserver which + # exports the TCPREMOTE* variables. + $remote_ip = $ENV{TCPREMOTEIP}; + $remote_host = $ENV{TCPREMOTEHOST} || "[$remote_ip]"; + $remote_info = + $ENV{TCPREMOTEINFO} + ? "$ENV{TCPREMOTEINFO}\@$remote_host" + : $remote_host; $remote_port = $ENV{TCPREMOTEPORT}; $local_ip = $ENV{TCPLOCALIP}; $local_port = $ENV{TCPLOCALPORT}; $local_host = $ENV{TCPLOCALHOST}; - } else { - # Started from inetd or similar. - # get info on the remote host from the socket. - # ignore ident/tap/... - my $hersockaddr = getpeername(STDIN) - or die "getpeername failed: $0 must be called from tcpserver, (x)inetd or a similar program which passes a socket to stdin"; - my ($port, $iaddr) = sockaddr_in($hersockaddr); - $remote_ip = inet_ntoa($iaddr); - $remote_host = gethostbyaddr($iaddr, AF_INET) || "[$remote_ip]"; - $remote_info = $remote_host; + } + else { + # Started from inetd or similar. + # get info on the remote host from the socket. + # ignore ident/tap/... + my $hersockaddr = getpeername(STDIN) + or die +"getpeername failed: $0 must be called from tcpserver, (x)inetd or a similar program which passes a socket to stdin"; + my ($port, $iaddr) = sockaddr_in($hersockaddr); + $remote_ip = inet_ntoa($iaddr); + $remote_host = gethostbyaddr($iaddr, AF_INET) || "[$remote_ip]"; + $remote_info = $remote_host; } $self->log(LOGNOTICE, "Connection from $remote_info [$remote_ip]"); @@ -64,20 +73,22 @@ sub start_connection { my $now = POSIX::strftime("%H:%M:%S %Y-%m-%d", localtime); $0 = "$first_0 [$remote_ip : $remote_host : $now]"; - $self->SUPER::connection->start(remote_info => $remote_info, + $self->SUPER::connection->start( + remote_info => $remote_info, remote_ip => $remote_ip, remote_host => $remote_host, remote_port => $remote_port, local_ip => $local_ip, local_port => $local_port, local_host => $local_host, - @_); + @_ + ); } sub run { my ($self, $client) = @_; - # Set local client_socket to passed client object for testing socket state on writes +# Set local client_socket to passed client object for testing socket state on writes $self->{__client_socket} = $client; $self->load_plugins unless $self->{hooks}; @@ -85,107 +96,121 @@ sub run { my $rc = $self->start_conversation; return if $rc != DONE; - # this should really be the loop and read_input should just get one line; I think +# this should really be the loop and read_input should just get one line; I think $self->read_input; } sub read_input { - my $self = shift; + my $self = shift; - my $timeout = - $self->config('timeoutsmtpd') # qmail smtpd control file - || $self->config('timeout') # qpsmtpd control file - || 1200; # default value + my $timeout = $self->config('timeoutsmtpd') # qmail smtpd control file + || $self->config('timeout') # qpsmtpd control file + || 1200; # default value - alarm $timeout; - while () { - alarm 0; - $_ =~ s/\r?\n$//s; # advanced chomp - $self->log(LOGINFO, "dispatching $_"); - $self->connection->notes('original_string', $_); - defined $self->dispatch(split / +/, $_, 2) - or $self->respond(502, "command unrecognized: '$_'"); alarm $timeout; - } - alarm(0); - return if $self->connection->notes('disconnected'); - $self->reset_transaction; - $self->run_hooks('disconnect'); - $self->connection->notes(disconnected => 1); + while () { + alarm 0; + $_ =~ s/\r?\n$//s; # advanced chomp + $self->log(LOGINFO, "dispatching $_"); + $self->connection->notes('original_string', $_); + defined $self->dispatch(split / +/, $_, 2) + or $self->respond(502, "command unrecognized: '$_'"); + alarm $timeout; + } + alarm(0); + return if $self->connection->notes('disconnected'); + $self->reset_transaction; + $self->run_hooks('disconnect'); + $self->connection->notes(disconnected => 1); } sub respond { - my ($self, $code, @messages) = @_; - my $buf = ''; + my ($self, $code, @messages) = @_; + my $buf = ''; - if ( !$self->check_socket() ) { - $self->log(LOGERROR, "Lost connection to client, cannot send response."); - return(0); - } + if (!$self->check_socket()) { + $self->log(LOGERROR, + "Lost connection to client, cannot send response."); + return (0); + } - while (my $msg = shift @messages) { - my $line = $code . (@messages?"-":" ").$msg; - $self->log(LOGINFO, $line); - $buf .= "$line\r\n"; - } - print $buf or ($self->log(LOGERROR, "Could not print [$buf]: $!"), return 0); - return 1; + while (my $msg = shift @messages) { + my $line = $code . (@messages ? "-" : " ") . $msg; + $self->log(LOGINFO, $line); + $buf .= "$line\r\n"; + } + print $buf + or ($self->log(LOGERROR, "Could not print [$buf]: $!"), return 0); + return 1; } sub disconnect { - my $self = shift; - $self->log(LOGINFO,"click, disconnecting"); - $self->SUPER::disconnect(@_); - $self->run_hooks("post-connection"); - $self->connection->reset; - exit; + my $self = shift; + $self->log(LOGINFO, "click, disconnecting"); + $self->SUPER::disconnect(@_); + $self->run_hooks("post-connection"); + $self->connection->reset; + exit; } # local/remote port and ip address sub lrpip { - my ($server, $client, $hisaddr) = @_; + my ($server, $client, $hisaddr) = @_; - my ($port, $iaddr) = ($server->sockdomain == AF_INET) ? (sockaddr_in($hisaddr)) : (sockaddr_in6($hisaddr)); - my $localsockaddr = getsockname($client); - my ($lport, $laddr) = ($server->sockdomain == AF_INET) ? (sockaddr_in($localsockaddr)) : (sockaddr_in6($localsockaddr)); + my ($port, $iaddr) = + ($server->sockdomain == AF_INET) + ? (sockaddr_in($hisaddr)) + : (sockaddr_in6($hisaddr)); + my $localsockaddr = getsockname($client); + my ($lport, $laddr) = + ($server->sockdomain == AF_INET) + ? (sockaddr_in($localsockaddr)) + : (sockaddr_in6($localsockaddr)); - my $nto_iaddr = ($server->sockdomain == AF_INET) ? (inet_ntoa($iaddr)) : (inet_ntop(AF_INET6(), $iaddr)); - my $nto_laddr = ($server->sockdomain == AF_INET) ? (inet_ntoa($laddr)) : (inet_ntop(AF_INET6(), $laddr)); - $nto_iaddr =~ s/::ffff://; - $nto_laddr =~ s/::ffff://; + my $nto_iaddr = + ($server->sockdomain == AF_INET) + ? (inet_ntoa($iaddr)) + : (inet_ntop(AF_INET6(), $iaddr)); + my $nto_laddr = + ($server->sockdomain == AF_INET) + ? (inet_ntoa($laddr)) + : (inet_ntop(AF_INET6(), $laddr)); + $nto_iaddr =~ s/::ffff://; + $nto_laddr =~ s/::ffff://; - return ($port, $iaddr, $lport, $laddr, $nto_iaddr, $nto_laddr); + return ($port, $iaddr, $lport, $laddr, $nto_iaddr, $nto_laddr); } sub tcpenv { - my ($nto_laddr, $nto_iaddr, $no_rdns) = @_; + my ($nto_laddr, $nto_iaddr, $no_rdns) = @_; - my $TCPLOCALIP = $nto_laddr; - my $TCPREMOTEIP = $nto_iaddr; + my $TCPLOCALIP = $nto_laddr; + my $TCPREMOTEIP = $nto_iaddr; - if ($no_rdns) { - return ($TCPLOCALIP, $TCPREMOTEIP, $TCPREMOTEIP ? "[$ENV{TCPREMOTEIP}]" : "[noip!]"); - } - my $res = new Net::DNS::Resolver; - $res->tcp_timeout(3); - $res->udp_timeout(3); - my $query = $res->query($nto_iaddr); - my $TCPREMOTEHOST; - if($query) { - foreach my $rr ($query->answer) { - next unless $rr->type eq "PTR"; - $TCPREMOTEHOST = $rr->ptrdname; + if ($no_rdns) { + return ($TCPLOCALIP, $TCPREMOTEIP, + $TCPREMOTEIP ? "[$ENV{TCPREMOTEIP}]" : "[noip!]"); } - } - return ($TCPLOCALIP, $TCPREMOTEIP, $TCPREMOTEHOST || "Unknown"); + my $res = new Net::DNS::Resolver; + $res->tcp_timeout(3); + $res->udp_timeout(3); + my $query = $res->query($nto_iaddr); + my $TCPREMOTEHOST; + if ($query) { + foreach my $rr ($query->answer) { + next unless $rr->type eq "PTR"; + $TCPREMOTEHOST = $rr->ptrdname; + } + } + return ($TCPLOCALIP, $TCPREMOTEIP, $TCPREMOTEHOST || "Unknown"); } sub check_socket() { - my $self = shift; + my $self = shift; - return 1 if ( $self->{__client_socket}->connected ); + return 1 if ($self->{__client_socket}->connected); - return 0; + return 0; } 1; diff --git a/lib/Qpsmtpd/TcpServer/Prefork.pm b/lib/Qpsmtpd/TcpServer/Prefork.pm index 2728cea..d8c814e 100644 --- a/lib/Qpsmtpd/TcpServer/Prefork.pm +++ b/lib/Qpsmtpd/TcpServer/Prefork.pm @@ -5,75 +5,77 @@ use Qpsmtpd::Constants; @ISA = qw(Qpsmtpd::SMTP::Prefork Qpsmtpd::TcpServer); -my $first_0; +my $first_0; sub start_connection { my $self = shift; #reset info - $self->{_connection} = Qpsmtpd::Connection->new(); #reset connection + $self->{_connection} = Qpsmtpd::Connection->new(); #reset connection $self->reset_transaction; $self->SUPER::start_connection(@_); } sub read_input { - my $self = shift; + my $self = shift; - my $timeout = - $self->config('timeoutsmtpd') # qmail smtpd control file - || $self->config('timeout') # qpsmtpd control file - || 1200; # default value + my $timeout = $self->config('timeoutsmtpd') # qmail smtpd control file + || $self->config('timeout') # qpsmtpd control file + || 1200; # default value - alarm $timeout; - eval { - while () { - alarm 0; - $_ =~ s/\r?\n$//s; # advanced chomp - $self->log(LOGINFO, "dispatching $_"); - $self->connection->notes('original_string', $_); - defined $self->dispatch(split / +/, $_, 2) - or $self->respond(502, "command unrecognized: '$_'"); - alarm $timeout; + alarm $timeout; + eval { + while () { + alarm 0; + $_ =~ s/\r?\n$//s; # advanced chomp + $self->log(LOGINFO, "dispatching $_"); + $self->connection->notes('original_string', $_); + defined $self->dispatch(split / +/, $_, 2) + or $self->respond(502, "command unrecognized: '$_'"); + alarm $timeout; + } + unless ($self->connection->notes('disconnected')) { + $self->reset_transaction; + $self->run_hooks('disconnect'); + $self->connection->notes(disconnected => 1); + } + }; + if ($@ =~ /^disconnect_tcpserver/) { + die "disconnect_tcpserver"; } - unless ($self->connection->notes('disconnected')) { - $self->reset_transaction; - $self->run_hooks('disconnect'); - $self->connection->notes(disconnected => 1); + else { + $self->run_hooks("post-connection"); + $self->connection->reset; + die "died while reading from STDIN (probably broken sender) - $@"; } - }; - if ($@ =~ /^disconnect_tcpserver/) { - die "disconnect_tcpserver"; - } else { - $self->run_hooks("post-connection"); - $self->connection->reset; - die "died while reading from STDIN (probably broken sender) - $@"; - } - alarm(0); + alarm(0); } sub respond { - my ($self, $code, @messages) = @_; + my ($self, $code, @messages) = @_; - if ( !$self->check_socket() ) { - $self->log(LOGERROR, "Lost connection to client, cannot send response."); - return(0); - } + if (!$self->check_socket()) { + $self->log(LOGERROR, + "Lost connection to client, cannot send response."); + return (0); + } - while (my $msg = shift @messages) { - my $line = $code . (@messages?"-":" ").$msg; - $self->log(LOGINFO, $line); - print "$line\r\n" or ($self->log(LOGERROR, "Could not print [$line]: $!"), return 0); - } - return 1; + while (my $msg = shift @messages) { + my $line = $code . (@messages ? "-" : " ") . $msg; + $self->log(LOGINFO, $line); + print "$line\r\n" + or ($self->log(LOGERROR, "Could not print [$line]: $!"), return 0); + } + return 1; } sub disconnect { - my $self = shift; - $self->log(LOGINFO,"click, disconnecting"); - $self->SUPER::disconnect(@_); - $self->run_hooks("post-connection"); - $self->connection->reset; - die "disconnect_tcpserver"; + my $self = shift; + $self->log(LOGINFO, "click, disconnecting"); + $self->SUPER::disconnect(@_); + $self->run_hooks("post-connection"); + $self->connection->reset; + die "disconnect_tcpserver"; } 1; diff --git a/lib/Qpsmtpd/Transaction.pm b/lib/Qpsmtpd/Transaction.pm index 4283d29..294fcd0 100644 --- a/lib/Qpsmtpd/Transaction.pm +++ b/lib/Qpsmtpd/Transaction.pm @@ -15,13 +15,13 @@ use Time::HiRes qw(gettimeofday); sub new { start(@_) } sub start { - my $proto = shift; - my $class = ref($proto) || $proto; - my %args = @_; - - my $self = { _rcpt => [], started => time, }; - bless ($self, $class); - return $self; + my $proto = shift; + my $class = ref($proto) || $proto; + my %args = @_; + + my $self = {_rcpt => [], started => time,}; + bless($self, $class); + return $self; } sub add_recipient { @@ -30,27 +30,28 @@ sub add_recipient { } sub remove_recipient { - my ($self,$rcpt) = @_; - $self->{_recipients} = [grep {$_->address ne $rcpt->address} - @{$self->{_recipients} || []}] if $rcpt; + my ($self, $rcpt) = @_; + $self->{_recipients} = + [grep { $_->address ne $rcpt->address } @{$self->{_recipients} || []}] + if $rcpt; } sub recipients { - my $self = shift; - @_ and $self->{_recipients} = [@_]; - ($self->{_recipients} ? @{$self->{_recipients}} : ()); + my $self = shift; + @_ and $self->{_recipients} = [@_]; + ($self->{_recipients} ? @{$self->{_recipients}} : ()); } sub sender { - my $self = shift; - @_ and $self->{_sender} = shift; - $self->{_sender}; + my $self = shift; + @_ and $self->{_sender} = shift; + $self->{_sender}; } sub header { - my $self = shift; - @_ and $self->{_header} = shift; - $self->{_header}; + my $self = shift; + @_ and $self->{_header} = shift; + $self->{_header}; } # blocked() will return when we actually can do something useful with it... @@ -63,32 +64,33 @@ sub header { #} sub notes { - my ($self,$key) = (shift,shift); - # Check for any additional arguments passed by the caller -- including undef - return $self->{_notes}->{$key} unless @_; - return $self->{_notes}->{$key} = shift; + my ($self, $key) = (shift, shift); + + # Check for any additional arguments passed by the caller -- including undef + return $self->{_notes}->{$key} unless @_; + return $self->{_notes}->{$key} = shift; } sub set_body_start { my $self = shift; $self->{_body_start} = $self->body_current_pos; if ($self->{_body_file}) { - $self->{_header_size} = $self->{_body_start}; + $self->{_header_size} = $self->{_body_start}; } else { $self->{_header_size} = 0; if ($self->{_body_array}) { - foreach my $line (@{ $self->{_body_array} }) { + foreach my $line (@{$self->{_body_array}}) { $self->{_header_size} += length($line); } } - } + } } sub body_start { - my $self = shift; - @_ and die "body_start now read only"; - $self->{_body_start}; + my $self = shift; + @_ and die "body_start now read only"; + $self->{_body_start}; } sub body_current_pos { @@ -100,110 +102,116 @@ sub body_current_pos { } sub body_filename { - my $self = shift; - $self->body_spool() unless $self->{_filename}; - $self->{_body_file}->flush(); # so contents won't be cached - return $self->{_filename}; + my $self = shift; + $self->body_spool() unless $self->{_filename}; + $self->{_body_file}->flush(); # so contents won't be cached + return $self->{_filename}; } sub body_spool { - my $self = shift; - $self->log(LOGINFO, "spooling message to disk"); - $self->{_filename} = $self->temp_file(); - $self->{_body_file} = IO::File->new($self->{_filename}, O_RDWR|O_CREAT, 0600) - or die "Could not open file $self->{_filename} - $! "; # . $self->{_body_file}->error; - if ($self->{_body_array}) { - foreach my $line (@{ $self->{_body_array} }) { - $self->{_body_file}->print($line) or die "Cannot print to temp file: $!"; + my $self = shift; + $self->log(LOGINFO, "spooling message to disk"); + $self->{_filename} = $self->temp_file(); + $self->{_body_file} = + IO::File->new($self->{_filename}, O_RDWR | O_CREAT, 0600) + or die "Could not open file $self->{_filename} - $! " + ; # . $self->{_body_file}->error; + if ($self->{_body_array}) { + foreach my $line (@{$self->{_body_array}}) { + $self->{_body_file}->print($line) + or die "Cannot print to temp file: $!"; + } + $self->{_body_start} = $self->{_header_size}; } - $self->{_body_start} = $self->{_header_size}; - } - else { - $self->log(LOGERROR, "no message body"); - } - $self->{_body_array} = undef; + else { + $self->log(LOGERROR, "no message body"); + } + $self->{_body_array} = undef; } sub body_write { - my $self = shift; - my $data = shift; - if ($self->{_body_file}) { - #warn("body_write to file\n"); - # go to the end of the file - seek($self->{_body_file},0,2) - unless $self->{_body_file_writing}; - $self->{_body_file_writing} = 1; - $self->{_body_file}->print(ref $data eq "SCALAR" ? $$data : $data) - and $self->{_body_size} += length (ref $data eq "SCALAR" ? $$data : $data); - } - else { - #warn("body_write to array\n"); - $self->{_body_array} ||= []; - my $ref = ref($data) eq "SCALAR" ? $data : \$data; - pos($$ref) = 0; - while ($$ref =~ m/\G(.*?\n)/gc) { - push @{ $self->{_body_array} }, $1; - $self->{_body_size} += length($1); - ++$self->{_body_current_pos}; + my $self = shift; + my $data = shift; + if ($self->{_body_file}) { + + #warn("body_write to file\n"); + # go to the end of the file + seek($self->{_body_file}, 0, 2) + unless $self->{_body_file_writing}; + $self->{_body_file_writing} = 1; + $self->{_body_file}->print(ref $data eq "SCALAR" ? $$data : $data) + and $self->{_body_size} += + length(ref $data eq "SCALAR" ? $$data : $data); } - if ($$ref =~ m/\G(.+)\z/gc) { - push @{ $self->{_body_array} }, $1; - $self->{_body_size} += length($1); - ++$self->{_body_current_pos}; + else { + #warn("body_write to array\n"); + $self->{_body_array} ||= []; + my $ref = ref($data) eq "SCALAR" ? $data : \$data; + pos($$ref) = 0; + while ($$ref =~ m/\G(.*?\n)/gc) { + push @{$self->{_body_array}}, $1; + $self->{_body_size} += length($1); + ++$self->{_body_current_pos}; + } + if ($$ref =~ m/\G(.+)\z/gc) { + push @{$self->{_body_array}}, $1; + $self->{_body_size} += length($1); + ++$self->{_body_current_pos}; + } + $self->body_spool if ($self->{_body_size} >= $self->size_threshold()); } - $self->body_spool if ( $self->{_body_size} >= $self->size_threshold() ); - } } -sub body_size { # depreceated, use data_size() instead - my $self = shift; - $self->log(LOGWARN, "WARNING: body_size() is depreceated, use data_size() instead"); - $self->{_body_size} || 0; +sub body_size { # depreceated, use data_size() instead + my $self = shift; + $self->log(LOGWARN, + "WARNING: body_size() is depreceated, use data_size() instead"); + $self->{_body_size} || 0; } sub data_size { - shift->{_body_size} || 0; + shift->{_body_size} || 0; } sub body_length { - my $self = shift; - $self->{_body_size} or return 0; - $self->{_header_size} or return 0; - return $self->{_body_size} - $self->{_header_size}; + my $self = shift; + $self->{_body_size} or return 0; + $self->{_header_size} or return 0; + return $self->{_body_size} - $self->{_header_size}; } sub body_resetpos { - my $self = shift; - - if ($self->{_body_file}) { - my $start = $self->{_body_start} || 0; - seek($self->{_body_file}, $start, 0); - $self->{_body_file_writing} = 0; - } - else { - $self->{_body_current_pos} = $self->{_body_start}; - } - - 1; + my $self = shift; + + if ($self->{_body_file}) { + my $start = $self->{_body_start} || 0; + seek($self->{_body_file}, $start, 0); + $self->{_body_file_writing} = 0; + } + else { + $self->{_body_current_pos} = $self->{_body_start}; + } + + 1; } sub body_getline { - my $self = shift; - if ($self->{_body_file}) { - my $start = $self->{_body_start} || 0; - seek($self->{_body_file}, $start,0) - if $self->{_body_file_writing}; - $self->{_body_file_writing} = 0; - my $line = $self->{_body_file}->getline; - return $line; - } - else { - return unless $self->{_body_array}; - $self->{_body_current_pos} ||= 0; - my $line = $self->{_body_array}->[$self->{_body_current_pos}]; - $self->{_body_current_pos}++; - return $line; - } + my $self = shift; + if ($self->{_body_file}) { + my $start = $self->{_body_start} || 0; + seek($self->{_body_file}, $start, 0) + if $self->{_body_file_writing}; + $self->{_body_file_writing} = 0; + my $line = $self->{_body_file}->getline; + return $line; + } + else { + return unless $self->{_body_array}; + $self->{_body_current_pos} ||= 0; + my $line = $self->{_body_array}->[$self->{_body_current_pos}]; + $self->{_body_current_pos}++; + return $line; + } } sub body_as_string { @@ -218,55 +226,59 @@ sub body_as_string { } sub body_fh { - return shift->{_body_file}; + return shift->{_body_file}; } sub dup_body_fh { - my ($self) = @_; - open(my $fh, '<&=', $self->body_fh); - return $fh; + my ($self) = @_; + open(my $fh, '<&=', $self->body_fh); + return $fh; } sub DESTROY { - my $self = shift; - # would we save some disk flushing if we unlinked the file before - # closing it? + my $self = shift; - $self->log(LOGDEBUG, sprintf( "DESTROY called by %s, %s, %s", (caller) ) ); + # would we save some disk flushing if we unlinked the file before + # closing it? - if ( $self->{_body_file} ) { + $self->log(LOGDEBUG, sprintf("DESTROY called by %s, %s, %s", (caller))); + + if ($self->{_body_file}) { undef $self->{_body_file}; - }; + } if ($self->{_filename} and -e $self->{_filename}) { - if ( unlink $self->{_filename} ) { - $self->log(LOGDEBUG, "unlinked ", $self->{_filename} ); + if (unlink $self->{_filename}) { + $self->log(LOGDEBUG, "unlinked ", $self->{_filename}); } else { - $self->log(LOGERROR, "Could not unlink ", $self->{_filename}, ": $!"); + $self->log(LOGERROR, "Could not unlink ", + $self->{_filename}, ": $!"); } } - # These may not exist - if ( $self->{_temp_files} ) { - $self->log(LOGDEBUG, "Cleaning up temporary transaction files"); - foreach my $file ( @{$self->{_temp_files}} ) { - next unless -e $file; - unlink $file or $self->log(LOGERROR, - "Could not unlink temporary file", $file, ": $!"); + # These may not exist + if ($self->{_temp_files}) { + $self->log(LOGDEBUG, "Cleaning up temporary transaction files"); + foreach my $file (@{$self->{_temp_files}}) { + next unless -e $file; + unlink $file + or $self->log(LOGERROR, "Could not unlink temporary file", + $file, ": $!"); + } } - } - # Ditto - if ( $self->{_temp_dirs} ) { - eval {use File::Path}; - $self->log(LOGDEBUG, "Cleaning up temporary directories"); - foreach my $dir ( @{$self->{_temp_dirs}} ) { - rmtree($dir) or $self->log(LOGERROR, - "Could not unlink temporary dir", $dir, ": $!"); - } - } -} + # Ditto + if ($self->{_temp_dirs}) { + eval { use File::Path }; + $self->log(LOGDEBUG, "Cleaning up temporary directories"); + foreach my $dir (@{$self->{_temp_dirs}}) { + rmtree($dir) + or $self->log(LOGERROR, "Could not unlink temporary dir", + $dir, ": $!"); + } + } +} 1; __END__ diff --git a/lib/Qpsmtpd/Utils.pm b/lib/Qpsmtpd/Utils.pm index 7ddc801..38c2c6f 100644 --- a/lib/Qpsmtpd/Utils.pm +++ b/lib/Qpsmtpd/Utils.pm @@ -11,5 +11,4 @@ sub tildeexp { return $path; } - 1; diff --git a/t/Test/Qpsmtpd.pm b/t/Test/Qpsmtpd.pm index 48041ee..0499ac5 100644 --- a/t/Test/Qpsmtpd.pm +++ b/t/Test/Qpsmtpd.pm @@ -9,11 +9,17 @@ use Qpsmtpd::Constants; use Test::Qpsmtpd::Plugin; sub new_conn { - ok(my $smtpd = __PACKAGE__->new(), "new"); - ok(my $conn = $smtpd->start_connection(remote_host => 'localhost', - remote_ip => '127.0.0.1'), "start_connection"); - is(($smtpd->response)[0], "220", "greetings"); - ($smtpd, $conn); + ok(my $smtpd = __PACKAGE__->new(), "new"); + ok( + my $conn = + $smtpd->start_connection( + remote_host => 'localhost', + remote_ip => '127.0.0.1' + ), + "start_connection" + ); + is(($smtpd->response)[0], "220", "greetings"); + ($smtpd, $conn); } sub start_connection { @@ -23,12 +29,14 @@ sub start_connection { my $remote_host = $args{remote_host} or croak "no remote_host parameter"; my $remote_info = "test\@$remote_host"; my $remote_ip = $args{remote_ip} or croak "no remote_ip parameter"; - - my $conn = $self->SUPER::connection->start(remote_info => $remote_info, - remote_ip => $remote_ip, - remote_host => $remote_host, - @_); + my $conn = + $self->SUPER::connection->start( + remote_info => $remote_info, + remote_ip => $remote_ip, + remote_host => $remote_host, + @_ + ); $self->load_plugins; @@ -39,33 +47,33 @@ sub start_connection { } sub respond { - my $self = shift; - $self->{_response} = [@_]; + my $self = shift; + $self->{_response} = [@_]; } sub response { - my $self = shift; - $self->{_response} ? (@{ delete $self->{_response} }) : (); + my $self = shift; + $self->{_response} ? (@{delete $self->{_response}}) : (); } sub command { - my ($self, $command) = @_; - $self->input($command); - $self->response; + my ($self, $command) = @_; + $self->input($command); + $self->response; } sub input { - my $self = shift; - my $command = shift; + my $self = shift; + my $command = shift; - my $timeout = $self->config('timeout'); - alarm $timeout; + my $timeout = $self->config('timeout'); + alarm $timeout; - $command =~ s/\r?\n$//s; # advanced chomp - $self->log(LOGDEBUG, "dispatching $command"); - defined $self->dispatch(split / +/, $command, 2) + $command =~ s/\r?\n$//s; # advanced chomp + $self->log(LOGDEBUG, "dispatching $command"); + defined $self->dispatch(split / +/, $command, 2) or $self->respond(502, "command unrecognized: '$command'"); - alarm $timeout; + alarm $timeout; } sub config_dir { @@ -95,20 +103,21 @@ sub run_plugin_tests { my $self = shift; $self->{_test_mode} = 1; my @plugins = $self->load_plugins(); + # First count test number my $num_tests = 0; foreach my $plugin (@plugins) { $plugin->register_tests(); $num_tests += $plugin->total_tests(); } - + require Test::Builder; my $Test = Test::Builder->new(); - $Test->plan( tests => $num_tests ); - + $Test->plan(tests => $num_tests); + # Now run them - + foreach my $plugin (@plugins) { $plugin->run_tests($self); } diff --git a/t/Test/Qpsmtpd/Plugin.pm b/t/Test/Qpsmtpd/Plugin.pm index 81969d1..2733f50 100644 --- a/t/Test/Qpsmtpd/Plugin.pm +++ b/t/Test/Qpsmtpd/Plugin.pm @@ -11,14 +11,16 @@ use Qpsmtpd::Constants; use Test::More; sub register_tests { + # Virtual base method - implement in plugin } sub register_test { my ($plugin, $test, $num_tests) = @_; $num_tests = 1 unless defined($num_tests); + # print STDERR "Registering test $test ($num_tests)\n"; - push @{$plugin->{_tests}}, { name => $test, num => $num_tests }; + push @{$plugin->{_tests}}, {name => $test, num => $num_tests}; } sub total_tests { @@ -34,14 +36,15 @@ sub run_tests { my ($plugin, $qp) = @_; foreach my $t (@{$plugin->{_tests}}) { my $method = $t->{name}; - print "# Running $method tests for plugin " . $plugin->plugin_name . "\n"; + print "# Running $method tests for plugin " + . $plugin->plugin_name . "\n"; local $plugin->{_qp} = $qp; $plugin->$method(); } } sub validate_password { - my ( $self, %a ) = @_; + my ($self, %a) = @_; my ($pkg, $file, $line) = caller(); @@ -53,42 +56,42 @@ sub validate_password { my $ticket = $a{ticket}; my $deny = $a{deny} || DENY; - if ( ! $src_crypt && ! $src_clear ) { + if (!$src_crypt && !$src_clear) { $self->log(LOGINFO, "fail: missing password"); - return ( $deny, "$file - no such user" ); - }; - - if ( ! $src_clear && $method =~ /CRAM-MD5/i ) { - $self->log(LOGINFO, "skip: cram-md5 not supported w/o clear pass"); - return ( DECLINED, $file ); + return ($deny, "$file - no such user"); } - if ( defined $attempt_clear ) { - if ( $src_clear && $src_clear eq $attempt_clear ) { + if (!$src_clear && $method =~ /CRAM-MD5/i) { + $self->log(LOGINFO, "skip: cram-md5 not supported w/o clear pass"); + return (DECLINED, $file); + } + + if (defined $attempt_clear) { + if ($src_clear && $src_clear eq $attempt_clear) { $self->log(LOGINFO, "pass: clear match"); - return ( OK, $file ); - }; - - if ( $src_crypt && $src_crypt eq crypt( $attempt_clear, $src_crypt ) ) { - $self->log(LOGINFO, "pass: crypt match"); - return ( OK, $file ); + return (OK, $file); } - }; - if ( defined $attempt_hash && $src_clear ) { - if ( ! $ticket ) { + if ($src_crypt && $src_crypt eq crypt($attempt_clear, $src_crypt)) { + $self->log(LOGINFO, "pass: crypt match"); + return (OK, $file); + } + } + + if (defined $attempt_hash && $src_clear) { + if (!$ticket) { $self->log(LOGERROR, "skip: missing ticket"); - return ( DECLINED, $file ); - }; + return (DECLINED, $file); + } - if ( $attempt_hash eq hmac_md5_hex( $ticket, $src_clear ) ) { + if ($attempt_hash eq hmac_md5_hex($ticket, $src_clear)) { $self->log(LOGINFO, "pass: hash match"); - return ( OK, $file ); - }; - }; + return (OK, $file); + } + } $self->log(LOGINFO, "fail: wrong password"); - return ( $deny, "$file - wrong password" ); -}; + return ($deny, "$file - wrong password"); +} 1;