diff --git a/config.sample/plugins b/config.sample/plugins index 582a4fe..46e75d6 100644 --- a/config.sample/plugins +++ b/config.sample/plugins @@ -91,9 +91,9 @@ spamassassin reject 12 # dspam must run after spamassassin for the learn_from_sa feature to work dspam autolearn spamassassin reject 0.95 -# run the clamav virus checking plugin +# run the clamav virus checking plugin (max size in Kb) # virus/clamav -# virus/clamdscan deny_viruses yes scan_all 1 +# virus/clamdscan deny_viruses yes max_size 1024 naughty reject data diff --git a/plugins/dmarc b/plugins/dmarc index a44c6d6..3f5eab8 100644 --- a/plugins/dmarc +++ b/plugins/dmarc @@ -64,7 +64,9 @@ https://github.com/qpsmtpd-dev/qpsmtpd-dev/wiki/DMARC-FAQ =head1 TODO - 2. provide dmarc feedback to domains that request it + provide dmarc feedback to domains that request it + + reject messages with multiple From: headers =head1 AUTHORS diff --git a/plugins/dont_require_anglebrackets b/plugins/dont_require_anglebrackets index b81df88..c8f25fd 100644 --- a/plugins/dont_require_anglebrackets +++ b/plugins/dont_require_anglebrackets @@ -26,6 +26,7 @@ sub hook_mail_pre { unless ($addr =~ /^<.*>$/) { $self->log(LOGINFO, "added MAIL angle brackets"); $addr = '<' . $addr . '>'; + $self->adjust_karma(-1); } return (OK, $addr); } @@ -35,6 +36,7 @@ sub hook_rcpt_pre { unless ($addr =~ /^<.*>$/) { $self->log(LOGINFO, "added RCPT angle brackets"); $addr = '<' . $addr . '>'; + $self->adjust_karma(-1); } return (OK, $addr); } diff --git a/plugins/headers b/plugins/headers index 8dd0220..9c7be78 100644 --- a/plugins/headers +++ b/plugins/headers @@ -77,6 +77,12 @@ Default: perm Adjust the quantity of logging for this plugin. See docs/logging.pod +=head1 TODO + +=head1 SEE ALSO + +https://tools.ietf.org/html/rfc5322 + =head1 AUTHOR 2012 - Matt Simerson @@ -130,36 +136,59 @@ sub hook_data_post { return $self->get_reject("Headers are missing", "missing headers"); }; - return (DECLINED, "immune") if $self->is_immune(); + return DECLINED if $self->is_immune(); - foreach my $h (@required_headers) { - next if $header->get($h); - $self->adjust_karma(-1); - return $self->get_reject("We require a valid $h header", - "no $h header"); - } - - foreach my $h (@singular_headers) { - next if !$header->get($h); # doesn't exist - my @qty = $header->get($h); - next if @qty == 1; # only 1 header - $self->adjust_karma(-1); - return - $self->get_reject( - "Only one $h header allowed. See RFC 5322, Section 3.6", - "too many $h headers",); - } + my $errors = $self->has_required_headers( $header ); + $errors += $self->has_singular_headers( $header ); my $err_msg = $self->invalid_date_range(); if ($err_msg) { - $self->adjust_karma(-1); return $self->get_reject($err_msg, $err_msg); } + if ( $errors ) { + return $self->get_reject($self->get_reject_type(), + "RFC 5322 validation errors" ); + }; + $self->log(LOGINFO, 'pass'); return (DECLINED); } +sub has_required_headers { + my ($self, $header) = @_; + + my $errors; + foreach my $h (@required_headers) { + next if $header->get($h); + $errors++; + $self->adjust_karma(-1); + $self->is_naughty(1) if $self->{args}{reject}; + $self->store_deferred_reject("We require a valid $h header"); + $self->log(LOGINFO, "fail, no $h header" ); + } + return $errors; +}; + +sub has_singular_headers { + my ($self, $header) = @_; + + my $errors; + foreach my $h (@singular_headers) { + next if !$header->get($h); # doesn't exist + my @qty = $header->get($h); + next if @qty == 1; # only 1 header + $errors++; + $self->adjust_karma(-1); + $self->is_naughty(1) if $self->{args}{reject}; + $self->store_deferred_reject( + "Only one $h header allowed. See RFC 5322, Section 3.6", + ); + $self->log(LOGINFO, "fail, too many $h headers" ); + } + return $errors; +}; + sub invalid_date_range { my $self = shift; @@ -175,12 +204,14 @@ sub invalid_date_range { my $past = $self->{_args}{past}; if ($past && $ts < time - ($past * 24 * 3600)) { $self->log(LOGINFO, "fail, date too old ($date)"); + $self->adjust_karma(-1); return "The Date header is too far in the past"; } my $future = $self->{_args}{future}; if ($future && $ts > time + ($future * 24 * 3600)) { $self->log(LOGINFO, "fail, date in future ($date)"); + $self->adjust_karma(-1); return "The Date header is too far in the future"; } diff --git a/plugins/helo b/plugins/helo index 0123471..d6ab0b5 100644 --- a/plugins/helo +++ b/plugins/helo @@ -203,6 +203,12 @@ this prohibition applies to the matching of the parameter to its IP address only; see Section 7.9 for a more extensive discussion of rejecting incoming connections or mail messages. +=head1 TODO + +is_forged_literal, if the forged IP is an internal IP, it's likely one +of our clients that should have authenticated. Perhaps when we check back +later in data_post, if they have added relay_client, then give back the +karma. =head1 AUTHOR diff --git a/plugins/karma b/plugins/karma index a8f2dd6..4dd0437 100644 --- a/plugins/karma +++ b/plugins/karma @@ -244,6 +244,7 @@ sub register { #$self->prune_db(); # keep the DB compact $self->register_hook('connect', 'connect_handler'); + $self->register_hook('mail_pre', 'from_handler'); $self->register_hook('rcpt_pre', 'rcpt_handler'); $self->register_hook('data', 'data_handler'); $self->register_hook('data_post', 'data_handler'); @@ -271,7 +272,7 @@ sub hook_pre_connection { } my ($penalty_start_ts, $naughty, $nice, $connects) = - $self->parse_value($tied->{$key}); + $self->parse_db_record($tied->{$key}); $self->calc_karma($naughty, $nice); return $self->cleanup_and_return($tied, $lock); } @@ -297,7 +298,7 @@ sub connect_handler { } my ($penalty_start_ts, $naughty, $nice, $connects) = - $self->parse_value($tied->{$key}); + $self->parse_db_record($tied->{$key}); my $summary = "$naughty naughty, $nice nice, $connects connects"; my $karma = $self->calc_karma($naughty, $nice); @@ -321,25 +322,47 @@ sub connect_handler { return $self->get_reject($mess, $karma); } +sub from_handler { + my ($self, $transaction, $addr) = @_; + +# test if sender has placed an illegal (RFC (2)821) space in envelope from + my $full_from = $self->connection->notes('envelope_from'); + $self->illegal_envelope_format( $full_from ); + + return DECLINED; +}; + sub rcpt_handler { my ($self, $transaction, $addr) = @_; + $self->illegal_envelope_format( + $self->connection->notes('envelope_rcpt'), + ); + + my $count = $self->connection->notes('recipient_count') || 0; + $count++; + if ( $count > 1 ) { + $self->log(LOGINFO, "recipients c: $count ($addr)"); + $self->connection->notes('recipient_count', $count); + }; + return DECLINED if $self->is_immune(); my $recipients = scalar $self->transaction->recipients or do { $self->log(LOGDEBUG, "info, no recipient count"); return DECLINED; }; + $self->log(LOGINFO, "recipients t: $recipients ($addr)"); my $history = $self->connection->notes('karma_history'); if ( $history > 0 ) { - $self->log(LOGDEBUG, "info, good history"); + $self->log(LOGINFO, "info, good history"); return DECLINED; }; my $karma = $self->connection->notes('karma'); if ( $karma > 0 ) { - $self->log(LOGDEBUG, "info, good connection"); + $self->log(LOGINFO, "info, good connection"); return DECLINED; }; @@ -376,7 +399,7 @@ sub disconnect_handler { my $key = $self->get_db_key(); my ($penalty_start_ts, $naughty, $nice, $connects) = - $self->parse_value($tied->{$key}); + $self->parse_db_record($tied->{$key}); my $history = ($nice || 0) - $naughty; my $log_mess = ''; @@ -410,7 +433,17 @@ sub disconnect_handler { return $self->cleanup_and_return($tied, $lock); } -sub parse_value { +sub illegal_envelope_format { + my ($self, $addr) = @_; + +# test if envelope address has an illegal (RFC (2)821) space + if ( uc substr($addr,0,6) ne 'FROM:<' && uc substr($addr,0,4) ne 'TO:<' ) { + $self->log(LOGINFO, "illegal envelope address format: $addr" ); + $self->adjust_karma(-1); + }; +}; + +sub parse_db_record { my ($self, $value) = @_; my $penalty_start_ts = my $naughty = my $nice = my $connects = 0; diff --git a/plugins/virus/clamdscan b/plugins/virus/clamdscan index 00feaae..246cb1e 100644 --- a/plugins/virus/clamdscan +++ b/plugins/virus/clamdscan @@ -123,8 +123,8 @@ sub register { # Set some sensible defaults $self->{'_args'}{'deny_viruses'} ||= 'yes'; - $self->{'_args'}{'max_size'} ||= 128; - $self->{'_args'}{'scan_all'} ||= 0; + $self->{'_args'}{'max_size'} ||= 1024; + $self->{'_args'}{'scan_all'} ||= 1; for my $setting ('deny_viruses', 'defer_on_error') { next unless $self->{'_args'}{$setting}; if (lc $self->{'_args'}{$setting} eq 'no') {