Merge branch 'master' of github.com:msimerson/qpsmtpd-dev
Conflicts: Makefile.PL
This commit is contained in:
commit
632b42ecee
1
Changes
1
Changes
@ -17,6 +17,7 @@
|
||||
karma: sprinkled karma awards throughout other plugins
|
||||
- limit poor karma hosts to 1 concurrent connection
|
||||
- allow +3 conncurrent connections to hosts with good karma
|
||||
- limit recipients to 1 for senders with negative karma
|
||||
|
||||
Sanitize spamd_sock path for perl taint mode - Markus Ullmann
|
||||
|
||||
|
@ -36,11 +36,11 @@ sub MY::libscan {
|
||||
}
|
||||
|
||||
sub MY::postamble {
|
||||
qq[
|
||||
qq[
|
||||
testcover :
|
||||
\t cover -delete && \\
|
||||
HARNESS_PERL_SWITCHES=-MDevel::Cover \$(MAKE) test && \\
|
||||
cover
|
||||
HARNESS_PERL_SWITCHES=-MDevel::Cover \$(MAKE) test && \\
|
||||
cover
|
||||
]
|
||||
|
||||
}
|
||||
|
1
STATUS
1
STATUS
@ -15,6 +15,7 @@ in Perl Best Practices is also fair game.
|
||||
So far, the main changes between the release and dev branches have focused
|
||||
on these goals:
|
||||
|
||||
- plugins use is_immune and is_naughty instead of a local methods
|
||||
- plugins log a single entry summarizing their disposition
|
||||
- plugin logs prefixed with keywords: pass, fail, skip, error
|
||||
- plugins use 'reject' and 'reject_type' settings
|
||||
|
4
config.sample/log2sql
Normal file
4
config.sample/log2sql
Normal file
@ -0,0 +1,4 @@
|
||||
# comments are allowed
|
||||
dsn = DBI:mysql:database=qpsmtpd;host=db;port=3306
|
||||
user = qplog
|
||||
pass = can mysql have 6 spaces in a passphrase?
|
@ -6,6 +6,10 @@
|
||||
# plugins/http_config for details.
|
||||
# http_config http://localhost/~smtpd/config/ http://www.example.com/smtp.pl?config=
|
||||
|
||||
# tls should load before count_unrecognized_commands
|
||||
# to support legacy port 465, tls must load before connection plugins
|
||||
#tls
|
||||
|
||||
# hosts_allow does not work with the tcpserver deployment model!
|
||||
# perldoc plugins/hosts_allow for an alternative.
|
||||
#
|
||||
@ -23,8 +27,6 @@ ident/geoip
|
||||
fcrdns
|
||||
|
||||
quit_fortune
|
||||
# tls should load before count_unrecognized_commands
|
||||
#tls
|
||||
earlytalker
|
||||
count_unrecognized_commands 4
|
||||
|
||||
|
@ -127,6 +127,40 @@ plugins in plugins/logging, specifically the L<plugins/logging/warn> and
|
||||
L<plugins/logging/adaptive> files for examples of how to write your own
|
||||
logging plugins.
|
||||
|
||||
=head1 plugin authors
|
||||
|
||||
While plugins can log anything they like, a few logging conventions in use:
|
||||
|
||||
=over 4
|
||||
|
||||
=item * at LOGINFO, log a single entry summarizing their disposition
|
||||
|
||||
=item * log messages are prefixed with keywords: pass, fail, skip, error
|
||||
|
||||
=over 4
|
||||
|
||||
=item pass: tests were run and the message passed
|
||||
|
||||
=item fail: tests were run and the message failed
|
||||
|
||||
=item fail, tolerated: tests run, msg failed, reject disabled
|
||||
|
||||
=item skip: tests were not run
|
||||
|
||||
=item error: tried to run tests but failure(s) encountered
|
||||
|
||||
=item info: additional info, not to be used for plugin summary
|
||||
|
||||
=back
|
||||
|
||||
=item * when tests fail and reject is disabled, use the 'fail, tolerated' prefix
|
||||
|
||||
=back
|
||||
|
||||
When these conventions are adhered to, the logs/summarize tool outputs each
|
||||
message as a single row, with a small x showing failed tests and a large X
|
||||
for failed tests that caused message rejection.
|
||||
|
||||
=head1 Internal support for pluggable logging
|
||||
|
||||
Any code in the core can call C<$self->log()> and those log lines will be
|
||||
|
@ -224,7 +224,7 @@ sub get_reject {
|
||||
|
||||
my $reject = $self->{_args}{reject};
|
||||
if (defined $reject && !$reject) {
|
||||
$self->log(LOGINFO, "fail, reject disabled" . $log_mess);
|
||||
$self->log(LOGINFO, "fail, tolerated" . $log_mess);
|
||||
return DECLINED;
|
||||
}
|
||||
|
||||
@ -303,6 +303,12 @@ sub is_immune {
|
||||
$self->log(LOGINFO, "skip, whitelisted sender");
|
||||
return 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sub is_naughty {
|
||||
my $self = shift;
|
||||
|
||||
if ($self->connection->notes('naughty')) {
|
||||
|
||||
# see plugins/naughty
|
||||
@ -323,7 +329,7 @@ sub adjust_karma {
|
||||
|
||||
my $karma = $self->connection->notes('karma') || 0;
|
||||
$karma += $value;
|
||||
$self->log(LOGDEBUG, "karma adjust: $value ($karma)");
|
||||
$self->log(LOGDEBUG, "karma $value ($karma)");
|
||||
$self->connection->notes('karma', $karma);
|
||||
return $value;
|
||||
}
|
||||
|
74
log/log2sql
74
log/log2sql
@ -6,21 +6,19 @@ use warnings;
|
||||
use Cwd;
|
||||
use Data::Dumper;
|
||||
use DBIx::Simple;
|
||||
use IO::File;
|
||||
use File::stat;
|
||||
use Time::TAI64 qw/ tai2unix /;
|
||||
|
||||
$Data::Dumper::Sortkeys = 1;
|
||||
|
||||
my $dsn = 'DBI:mysql:database=qpsmtpd;host=db;port=3306';
|
||||
my $user = 'qplog';
|
||||
my $pass = 't0ps3cret';
|
||||
|
||||
my $logdir = get_log_dir();
|
||||
my @logfiles = get_logfiles($logdir);
|
||||
|
||||
my (%plugins, %os, %message_ids);
|
||||
my $has_cleanup;
|
||||
my $db = get_db();
|
||||
check_plugins_table();
|
||||
|
||||
foreach my $file (@logfiles) {
|
||||
my ($fid, $offset) = check_logfile($file);
|
||||
@ -208,6 +206,7 @@ sub parse_logfile {
|
||||
#warn "type: $type\n";
|
||||
if ($type eq 'plugin') {
|
||||
next if $plugin eq 'naughty'; # housekeeping only
|
||||
next if $plugin eq 'karma' && 'karma adjust' eq substr($message,0,12);
|
||||
insert_plugin($msg_id, $plugin, $message);
|
||||
}
|
||||
elsif ($type eq 'queue') {
|
||||
@ -529,12 +528,70 @@ sub get_score {
|
||||
|
||||
sub get_db {
|
||||
|
||||
my $db = DBIx::Simple->connect($dsn, $user, $pass)
|
||||
my %dbv = get_config('log2sql');
|
||||
|
||||
$dbv{dsn} ||= 'DBI:mysql:database=qpsmtpd;host=db;port=3306';
|
||||
$dbv{user} ||= 'qplog';
|
||||
$dbv{pass} ||= 't0ps3cret';
|
||||
|
||||
print Dumper(\%dbv);
|
||||
my $db = DBIx::Simple->connect($dbv{dsn}, $dbv{user}, $dbv{pass})
|
||||
or die DBIx::Simple->error;
|
||||
|
||||
return $db;
|
||||
}
|
||||
|
||||
sub get_config {
|
||||
my $file = shift or die "missing file name\n";
|
||||
my %values;
|
||||
foreach my $line ( get_config_contents( $file ) ) {
|
||||
next if $line =~ /^#/;
|
||||
chomp $line;
|
||||
my ($key,$val) = split /\s*=\s*/, $line, 2;
|
||||
$values{$key} = $val;
|
||||
};
|
||||
return %values;
|
||||
};
|
||||
|
||||
sub get_config_contents {
|
||||
my $name = shift;
|
||||
|
||||
my @config_dirs = qw[ config ../config log plugins ];
|
||||
foreach my $dir ( @config_dirs ) {
|
||||
next if ! -f "$dir/$name";
|
||||
|
||||
my $fh = IO::File->new();
|
||||
if ( ! $fh->open( "$dir/$name", '<' ) ) {
|
||||
warn "unable to open config file $dir/$name\n";
|
||||
next;
|
||||
};
|
||||
my @contents = <$fh>;
|
||||
return @contents;
|
||||
};
|
||||
};
|
||||
|
||||
sub check_plugins_table {
|
||||
my $rows = exec_query( 'SELECT COUNT(*) FROM plugin');
|
||||
return if scalar @$rows != 0;
|
||||
my @lines = get_config_contents('registry.txt');
|
||||
foreach my $line ( @lines ) {
|
||||
next if $line =~ /^\s*#/; # ignore comments
|
||||
chomp $line;
|
||||
next if ! $line;
|
||||
my ($id, $name, $abb3, $abb5, $aliases) = split /\s+/, $line, 5;
|
||||
my $q = "REPLACE INTO plugin (id,name,abb3,abb5) VALUES (??)";
|
||||
print "query: $q, $id, $name, $abb3, $abb5\n";
|
||||
exec_query($q, [$id, $name, $abb3, $abb5 ]);
|
||||
next if ! $aliases;
|
||||
foreach my $alias ( split /\s*,\s*/, $aliases ) {
|
||||
next if ! $alias;
|
||||
my $aq = "REPLACE INTO plugin_aliases (plugin_id,name) VALUES (??)";
|
||||
print "aqury: $aq, $id, $alias\n";
|
||||
exec_query($aq, [$id, $alias]);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
sub exec_query {
|
||||
my $query = shift;
|
||||
my $params = shift;
|
||||
@ -550,10 +607,11 @@ sub exec_query {
|
||||
}
|
||||
|
||||
#warn "err: $err\n";
|
||||
if ($query =~ /INSERT INTO/) {
|
||||
my ($table) = $query =~ /INSERT INTO (\w+)\s/;
|
||||
if ($query =~ /(?:REPLACE|INSERT) INTO/) {
|
||||
my ($table) = $query =~ /(?:REPLACE|INSERT) INTO (\w+)\s/;
|
||||
$db->query($query, @params);
|
||||
die "$db->error\n$err" if $db->error ne 'DBI error: ';
|
||||
warn "$db->error\n$err" if $db->error ne 'DBI error: ';
|
||||
return if $query =~ /^REPLACE/;
|
||||
my $id = $db->last_insert_id(undef, undef, $table, undef) or die $err;
|
||||
return $id;
|
||||
}
|
||||
|
146
log/log2sql.sql
146
log/log2sql.sql
@ -13,35 +13,34 @@
|
||||
DROP TABLE IF EXISTS `log`;
|
||||
|
||||
CREATE TABLE `log` (
|
||||
`id` int(11) unsigned NOT NULL auto_increment,
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`inode` int(11) unsigned NOT NULL,
|
||||
`size` int(11) unsigned NOT NULL,
|
||||
`name` varchar(30) NOT NULL default '',
|
||||
`created` datetime default NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
`name` varchar(30) NOT NULL DEFAULT '',
|
||||
`created` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
|
||||
# Dump of table message
|
||||
# ------------------------------------------------------------
|
||||
|
||||
DROP TABLE IF EXISTS `message`;
|
||||
|
||||
CREATE TABLE `message` (
|
||||
`id` int(11) unsigned NOT NULL auto_increment,
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`file_id` int(10) unsigned NOT NULL,
|
||||
`connect_start` datetime NOT NULL,
|
||||
`ip` int(10) unsigned NOT NULL,
|
||||
`qp_pid` int(10) unsigned NOT NULL,
|
||||
`result` tinyint(3) NOT NULL default '0',
|
||||
`distance` mediumint(8) unsigned default NULL,
|
||||
`time` decimal(3,2) unsigned default NULL,
|
||||
`os_id` tinyint(3) unsigned default NULL,
|
||||
`hostname` varchar(128) default NULL,
|
||||
`helo` varchar(128) default NULL,
|
||||
`mail_from` varchar(128) default NULL,
|
||||
`rcpt_to` varchar(128) default NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
`result` tinyint(3) NOT NULL DEFAULT '0',
|
||||
`distance` mediumint(8) unsigned DEFAULT NULL,
|
||||
`time` decimal(3,2) unsigned DEFAULT NULL,
|
||||
`os_id` tinyint(3) unsigned DEFAULT NULL,
|
||||
`hostname` varchar(128) DEFAULT NULL,
|
||||
`helo` varchar(128) DEFAULT NULL,
|
||||
`mail_from` varchar(128) DEFAULT NULL,
|
||||
`rcpt_to` varchar(128) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `file_id` (`file_id`),
|
||||
CONSTRAINT `message_ibfk_1` FOREIGN KEY (`file_id`) REFERENCES `log` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
@ -54,12 +53,12 @@ CREATE TABLE `message` (
|
||||
DROP TABLE IF EXISTS `message_plugin`;
|
||||
|
||||
CREATE TABLE `message_plugin` (
|
||||
`id` int(11) unsigned NOT NULL auto_increment,
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`msg_id` int(11) unsigned NOT NULL,
|
||||
`plugin_id` int(4) unsigned NOT NULL,
|
||||
`result` tinyint(4) NOT NULL,
|
||||
`string` varchar(128) default NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
`string` varchar(128) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `msg_id` (`msg_id`),
|
||||
KEY `plugin_id` (`plugin_id`),
|
||||
CONSTRAINT `message_plugin_ibfk_1` FOREIGN KEY (`plugin_id`) REFERENCES `plugin` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
@ -67,16 +66,15 @@ CREATE TABLE `message_plugin` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
|
||||
|
||||
# Dump of table os
|
||||
# ------------------------------------------------------------
|
||||
|
||||
DROP TABLE IF EXISTS `os`;
|
||||
|
||||
CREATE TABLE `os` (
|
||||
`id` tinyint(3) unsigned NOT NULL auto_increment,
|
||||
`name` varchar(36) default NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
`id` tinyint(3) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(36) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
LOCK TABLES `os` WRITE;
|
||||
@ -114,81 +112,14 @@ UNLOCK TABLES;
|
||||
DROP TABLE IF EXISTS `plugin`;
|
||||
|
||||
CREATE TABLE `plugin` (
|
||||
`id` int(4) unsigned NOT NULL auto_increment,
|
||||
`name` varchar(35) character set utf8 NOT NULL default '',
|
||||
`abb3` char(3) character set utf8 default NULL,
|
||||
`abb5` char(5) character set utf8 default NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `abb3` (`abb3`),
|
||||
`id` int(4) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(35) CHARACTER SET utf8 NOT NULL DEFAULT '',
|
||||
`abb3` char(3) CHARACTER SET utf8 DEFAULT NULL,
|
||||
`abb5` char(5) CHARACTER SET utf8 DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `abb5` (`abb5`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
|
||||
|
||||
LOCK TABLES `plugin` WRITE;
|
||||
/*!40000 ALTER TABLE `plugin` DISABLE KEYS */;
|
||||
|
||||
INSERT INTO `plugin` (`id`, `name`, `abb3`, `abb5`)
|
||||
VALUES
|
||||
(1,'hosts_allow','alw','allow'),
|
||||
(2,'ident::geoip','geo','geoip'),
|
||||
(3,'ident::p0f','p0f',' p0f'),
|
||||
(5,'karma','krm','karma'),
|
||||
(6,'dnsbl','dbl','dnsbl'),
|
||||
(7,'relay','rly','relay'),
|
||||
(9,'earlytalker','ear','early'),
|
||||
(15,'helo','hlo','helo'),
|
||||
(16,'tls','tls',' tls'),
|
||||
(20,'dont_require_anglebrackets','rab','drabs'),
|
||||
(21,'unrecognized_commands','cmd','uncmd'),
|
||||
(22,'noop','nop','noop'),
|
||||
(23,'random_error','rnd','rande'),
|
||||
(24,'milter','mtr','mlter'),
|
||||
(25,'content_log','log','colog'),
|
||||
(30,'auth::vpopmail_sql','aut','vpsql'),
|
||||
(31,'auth::vpopmaild','vpd','vpopd'),
|
||||
(32,'auth::vpopmail','vpo','vpop'),
|
||||
(33,'auth::checkpasswd','ckp','chkpw'),
|
||||
(34,'auth::cvs_unix_local','cvs','cvsul'),
|
||||
(35,'auth::flat_file','flt','aflat'),
|
||||
(36,'auth::ldap_bind','ldp','aldap'),
|
||||
(40,'badmailfrom','bmf','badmf'),
|
||||
(41,'badmailfromto','bmt','bfrto'),
|
||||
(42,'rhsbl','rbl','rhsbl'),
|
||||
(44,'resolvable_fromhost','rfh','rsvfh'),
|
||||
(45,'sender_permitted_from','spf',' spf'),
|
||||
(50,'badrcptto','bto','badto'),
|
||||
(51,'rcpt_map','rmp','rcmap'),
|
||||
(52,'rcpt_regex','rcx','rcrex'),
|
||||
(53,'qmail_deliverable','qmd',' qmd'),
|
||||
(55,'rcpt_ok','rok','rcpok'),
|
||||
(58,'bogus_bounce','bog','bogus'),
|
||||
(59,'greylisting','gry','greyl'),
|
||||
(60,'headers','hdr','headr'),
|
||||
(61,'loop','lop','loop'),
|
||||
(62,'uribl','uri','uribl'),
|
||||
(63,'domainkeys','dk','dkey'),
|
||||
(64,'dkim','dkm','dkim'),
|
||||
(65,'spamassassin','spm','spama'),
|
||||
(66,'dspam','dsp','dspam'),
|
||||
(70,'virus::aveclient','vav','avirs'),
|
||||
(71,'virus::bitdefender','vbd','bitdf'),
|
||||
(72,'virus::clamav','cav','clamv'),
|
||||
(73,'virus::clamdscan','cad','clamd'),
|
||||
(74,'virus::hbedv','hbv','hbedv'),
|
||||
(75,'virus::kavscanner','kav','kavsc'),
|
||||
(76,'virus::klez_filter','klz','vklez'),
|
||||
(77,'virus::sophie','sop','sophe'),
|
||||
(78,'virus::uvscan','uvs','uvscn'),
|
||||
(80,'queue::qmail-queue','qqm','queue'),
|
||||
(81,'queue::maildir','qdr','qudir'),
|
||||
(82,'queue::postfix-queue','qpf','qupfx'),
|
||||
(83,'queue::smtp-forward','qfw','qufwd'),
|
||||
(84,'queue::exim-bsmtp','qxm','qexim'),
|
||||
(98,'quit_fortune','for','fortu'),
|
||||
(99,'connection_time','tim','time');
|
||||
|
||||
/*!40000 ALTER TABLE `plugin` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
|
||||
# Dump of table plugin_aliases
|
||||
# ------------------------------------------------------------
|
||||
@ -197,33 +128,10 @@ DROP TABLE IF EXISTS `plugin_aliases`;
|
||||
|
||||
CREATE TABLE `plugin_aliases` (
|
||||
`plugin_id` int(11) unsigned NOT NULL,
|
||||
`name` varchar(35) character set utf8 NOT NULL default '',
|
||||
KEY `plugin_id` (`plugin_id`),
|
||||
CONSTRAINT `plugin_id` FOREIGN KEY (`plugin_id`) REFERENCES `plugin` (`id`) ON UPDATE CASCADE
|
||||
`name` varchar(35) CHARACTER SET utf8 NOT NULL DEFAULT '',
|
||||
UNIQUE KEY `plugin_id` (`plugin_id`,`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
|
||||
|
||||
LOCK TABLES `plugin_aliases` WRITE;
|
||||
/*!40000 ALTER TABLE `plugin_aliases` DISABLE KEYS */;
|
||||
|
||||
INSERT INTO `plugin_aliases` (`plugin_id`, `name`)
|
||||
VALUES
|
||||
(60,'check_basicheaders'),
|
||||
(44,'require_resolvable_fromhost'),
|
||||
(21,'count_unrecognized_commands'),
|
||||
(9,'check_earlytalker'),
|
||||
(40,'check_badmailfrom'),
|
||||
(50,'check_badrcptto'),
|
||||
(58,'check_bogus_bounce'),
|
||||
(15,'check_spamhelo'),
|
||||
(3,'ident::p0f_3a0'),
|
||||
(80,'queue::qmail_2dqueue'),
|
||||
(22,'noop_counter');
|
||||
|
||||
/*!40000 ALTER TABLE `plugin_aliases` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
|
||||
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
|
@ -6,9 +6,13 @@ use warnings;
|
||||
use Cwd;
|
||||
use Data::Dumper;
|
||||
use File::Tail;
|
||||
use Getopt::Std;
|
||||
|
||||
$Data::Dumper::Sortkeys = 1;
|
||||
|
||||
our $opt_l = 0;
|
||||
getopts('l');
|
||||
|
||||
my (%plugins, %plugin_aliases, %seen_plugins, %pids);
|
||||
my %hide_plugins = map { $_ => 1 } qw/ hostname /;
|
||||
|
||||
@ -32,7 +36,7 @@ my %formats = (
|
||||
ip => "%-15.15s",
|
||||
hostname => "%-20.20s",
|
||||
distance => "%5.5s",
|
||||
'ident::geoip' => "%-20.20s",
|
||||
'ident::geoip' => $opt_l ? "%-20.20s" : "%-6.6s",
|
||||
'ident::p0f' => "%-10.10s",
|
||||
count_unrecognized_commands => "%-5.5s",
|
||||
unrecognized_commands => "%-5.5s",
|
||||
@ -269,18 +273,20 @@ sub print_auto_format {
|
||||
next;
|
||||
}
|
||||
|
||||
my $wide = $opt_l ? 20 : 8;
|
||||
|
||||
if (defined $pids{$pid}{helo_host} && $plugin =~ /helo/) {
|
||||
$format .= " %-18.18s";
|
||||
push @values, substr(delete $pids{$pid}{helo_host}, -18, 18);
|
||||
$format .= " %-$wide.${wide}s";
|
||||
push @values, substr(delete $pids{$pid}{helo_host}, -$wide, $wide);
|
||||
push @headers, 'HELO';
|
||||
}
|
||||
elsif (defined $pids{$pid}{from} && $plugin =~ /from/) {
|
||||
$format .= " %-20.20s";
|
||||
push @values, substr(delete $pids{$pid}{from}, -20, 20);
|
||||
$format .= " %-$wide.${wide}s";
|
||||
push @values, substr(delete $pids{$pid}{from}, -$wide, $wide);
|
||||
push @headers, 'MAIL FROM';
|
||||
}
|
||||
elsif (defined $pids{$pid}{to} && $plugin =~ /to|rcpt|recipient/) {
|
||||
$format .= " %-20.20s";
|
||||
$format .= " %-$wide.${wide}s";
|
||||
push @values, delete $pids{$pid}{to};
|
||||
push @headers, 'RCPT TO';
|
||||
}
|
||||
@ -299,7 +305,7 @@ sub print_auto_format {
|
||||
$format .= "\n";
|
||||
printf("\n$format", @headers) if (!$printed || $printed % 20 == 0);
|
||||
printf($format, @values);
|
||||
print Data::Dumper::Dumper($pids{$pid}) if keys %{$pids{$pid}};
|
||||
#print Data::Dumper::Dumper($pids{$pid}) if keys %{$pids{$pid}};
|
||||
$printed++;
|
||||
}
|
||||
|
||||
@ -308,6 +314,7 @@ sub show_symbol {
|
||||
return ' o' if $mess eq 'TLS setup returning';
|
||||
return ' o' if $mess eq 'pass';
|
||||
return ' -' if $mess eq 'skip';
|
||||
return ' x' if 'fail, tolerated' eq substr($mess, 0, 15);
|
||||
return ' X' if $mess eq 'fail';
|
||||
return ' -' if $mess =~ /^skip[,:\s]/i;
|
||||
return ' o' if $mess =~ /^pass[,:\s]/i;
|
||||
@ -347,6 +354,8 @@ sub populate_plugins_from_registry {
|
||||
open my $F, '<', $file;
|
||||
while (defined(my $line = <$F>)) {
|
||||
next if $line =~ /^#/; # discard comments
|
||||
chomp $line;
|
||||
next if ! $line;
|
||||
my ($id, $name, $abb3, $abb5, $aliases) = split /\s+/, $line;
|
||||
next if !defined $name;
|
||||
$plugins{$name} = {id => $id, abb3 => $abb3, abb5 => $abb5};
|
||||
|
@ -136,11 +136,12 @@ sub auth_checkpassword {
|
||||
my $status = $?;
|
||||
|
||||
if ($status != 0) {
|
||||
$self->log(LOGNOTICE, "authentication failed ($status)");
|
||||
$self->log(LOGNOTICE, "fail, auth failed: $status");
|
||||
return (DECLINED);
|
||||
}
|
||||
|
||||
$self->connection->notes('authuser', $user);
|
||||
$self->log(LOGINFO, "pass, auth success with $method");
|
||||
return (OK, "auth_checkpassword");
|
||||
}
|
||||
|
||||
|
17
plugins/dkim
17
plugins/dkim
@ -353,7 +353,7 @@ sub handle_sig_pass {
|
||||
elsif ($prs->{neutral}) {
|
||||
$self->add_header($mess);
|
||||
$self->log(LOGINFO, "pass, valid signature, neutral policy");
|
||||
$self->log(LOGINFO, $mess);
|
||||
$self->log(LOGDEBUG, $mess);
|
||||
return DECLINED;
|
||||
}
|
||||
elsif ($prs->{reject}) {
|
||||
@ -364,7 +364,7 @@ sub handle_sig_pass {
|
||||
"fail, valid sig, reject policy");
|
||||
}
|
||||
|
||||
# this should never happen
|
||||
# this should never happen,
|
||||
$self->add_header($mess);
|
||||
$self->log(LOGERROR, "pass, valid sig, no policy results");
|
||||
$self->log(LOGINFO, $mess);
|
||||
@ -449,14 +449,17 @@ sub get_keydir {
|
||||
sub save_signatures_to_note {
|
||||
my ($self, $dkim) = @_;
|
||||
|
||||
my %domains;
|
||||
foreach my $sig ($dkim->signatures) {
|
||||
next if $sig->result ne 'pass';
|
||||
my $doms = $self->connection->notes('dkim_pass_domains') || [];
|
||||
next if grep /$sig->domain/, @$doms; # already in the list
|
||||
push @$doms, $sig->domain;
|
||||
$self->connection->notes('dkim_pass_domains', $doms);
|
||||
$self->log(LOGINFO, "info, added " . $sig->domain);
|
||||
$domains{$sig->domain} = 1;
|
||||
}
|
||||
return if 0 == scalar keys %domains;
|
||||
|
||||
my $doms = $self->connection->notes('dkim_pass_domains') || [];
|
||||
push @$doms, keys %domains;
|
||||
$self->log(LOGDEBUG, "info, signed by: ". join(',', keys %domains) );
|
||||
$self->connection->notes('dkim_pass_domains', $doms);
|
||||
}
|
||||
|
||||
sub send_message_to_dkim {
|
||||
|
102
plugins/dmarc
102
plugins/dmarc
@ -6,6 +6,10 @@ Domain-based Message Authentication, Reporting and Conformance
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
DMARC is an extremely reliable means to authenticate email.
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
From the DMARC Draft: "DMARC operates as a policy layer atop DKIM and SPF. These technologies are the building blocks of DMARC as each is widely deployed, supported by mature tools, and is readily available to both senders and receivers. They are complementary, as each is resilient to many of the failure modes of the other."
|
||||
|
||||
DMARC provides a way to exchange authentication information and policies among mail servers.
|
||||
@ -14,10 +18,10 @@ DMARC benefits domain owners by preventing others from impersonating them. A dom
|
||||
|
||||
DMARC benefits mail server operators by providing them with an extremely reliable (as opposed to DKIM or SPF, which both have reliability issues when used independently) means to block forged emails. Is that message really from PayPal, Chase, Gmail, or Facebook? Since those organizations, and many more, publish DMARC policies, operators have a definitive means to know.
|
||||
|
||||
=head1 HOW IT WORKS
|
||||
|
||||
=head1 HOWTO
|
||||
|
||||
=head2 Protect a domain with DMARC
|
||||
|
||||
See Section 10 of the draft: Domain Owner Actions
|
||||
|
||||
1. Deploy DKIM & SPF
|
||||
@ -25,33 +29,47 @@ See Section 10 of the draft: Domain Owner Actions
|
||||
3. Publish a "monitor" record, ask for data reports
|
||||
4. Roll policies from monitor to reject
|
||||
|
||||
=head2 Publish a DMARC policy
|
||||
=head3 Publish a DMARC policy
|
||||
|
||||
_dmarc IN TXT "v=DMARC1; p=reject; pct=100; rua=mailto:dmarc-feedback@example.com;"
|
||||
|
||||
v=DMARC1; (version)
|
||||
p=none; (disposition policy : reject, quarantine, none (monitor))
|
||||
sp=reject; (subdomain policy: default, same as p)
|
||||
rua
|
||||
adkim=s; (dkim alignment: s=strict, r=relaxed)
|
||||
aspf=r; (spf alignment: s=strict, r=relaxed)
|
||||
rua=mailto: dmarc-feedback\@$zone; (aggregate reports)
|
||||
ruf=mailto: dmarc-feedback\@$zone.com; (forensic reports)
|
||||
rua=mailto: dmarc-feedback@example.com; (aggregate reports)
|
||||
ruf=mailto: dmarc-feedback@example.com; (forensic reports)
|
||||
rf=afrf; (report format: afrf, iodef)
|
||||
ri=8400; (report interval)
|
||||
pct=50; (percent of messages to filter)
|
||||
|
||||
=head2 Validate messages with DMARC
|
||||
|
||||
=head1 DRAFT
|
||||
1. install this plugin
|
||||
|
||||
2. install a public suffix list in config/public_suffix_list. See http://publicsuffix.org/list/
|
||||
|
||||
3. activate this plugin (add to config/plugins)
|
||||
|
||||
Be sure to run the DMARC plugin after the SPF & DKIM plugins. Configure the SPF and DKIM messages to not reject mail.
|
||||
|
||||
=head2 Parse dmarc feedback reports into a database
|
||||
|
||||
See http://www.taugh.com/rddmarc/
|
||||
|
||||
=head1 MORE INFORMATION
|
||||
|
||||
http://www.dmarc.org/draft-dmarc-base-00-02.txt
|
||||
|
||||
https://github.com/qpsmtpd-dev/qpsmtpd-dev/wiki/DMARC-FAQ
|
||||
|
||||
=head1 TODO
|
||||
|
||||
2. provide dmarc feedback to domains that request it
|
||||
|
||||
3. If a message has multiple 'From' recipients, reject it
|
||||
|
||||
4. Rejections with a 550 (perm) or 450 (temp)
|
||||
|
||||
=head1 IMPLEMENTATION
|
||||
|
||||
1. Primary identifier is RFC5322.From field (From: header)
|
||||
@ -99,11 +117,10 @@ sub data_post_handler {
|
||||
my $from_host = $self->get_from_host($transaction) or return DECLINED;
|
||||
my $org_host = $self->get_organizational_domain($from_host);
|
||||
|
||||
if (!$self->exists_in_dns($from_host)) {
|
||||
if (!$self->exists_in_dns($org_host)) {
|
||||
$self->log(LOGINFO, "fail, $from_host not in DNS");
|
||||
return $self->get_reject("RFC5322.From host does not exist");
|
||||
}
|
||||
# 6. Receivers should reject email if the domain appears to not exist
|
||||
if (!$self->exists_in_dns($from_host) && !$self->exists_in_dns($org_host)) {
|
||||
$self->log(LOGINFO, "fail, $from_host not in DNS");
|
||||
return $self->get_reject("RFC5322.From host appears non-existent");
|
||||
}
|
||||
|
||||
# 11.2. Determine Handling Policy
|
||||
@ -129,6 +146,12 @@ sub data_post_handler {
|
||||
# Domain Owner. See Section 6.2 for details.
|
||||
return DECLINED if lc $policy->{p} eq 'none';
|
||||
|
||||
my $pct = $policy->{pct} || 100;
|
||||
if ( $pct != 100 && int(rand(100)) >= $pct ) {
|
||||
$self->log("fail, tolerated, policy, sampled out");
|
||||
return DECLINED;
|
||||
};
|
||||
|
||||
return $self->get_reject("failed DMARC policy");
|
||||
}
|
||||
|
||||
@ -279,9 +302,16 @@ sub get_organizational_domain {
|
||||
|
||||
# $self->log( LOGINFO, "i: $i, $tld" );
|
||||
#warn "i: $i - tld: $tld\n";
|
||||
if (grep /$tld/, $self->qp->config('public_suffix_list')) {
|
||||
if (grep /^$tld/, $self->qp->config('public_suffix_list')) {
|
||||
$greatest = $i + 1;
|
||||
next;
|
||||
}
|
||||
|
||||
# check for wildcards (ex: *.uk should match co.uk)
|
||||
$tld = join '.', '\*', reverse((@labels)[0 .. $i-1]);
|
||||
if (grep /^$tld/, $self->qp->config('public_suffix_list')) {
|
||||
$greatest = $i + 1;
|
||||
};
|
||||
}
|
||||
|
||||
return $from_host if $greatest == scalar @labels; # same
|
||||
@ -295,37 +325,49 @@ sub get_organizational_domain {
|
||||
|
||||
sub exists_in_dns {
|
||||
my ($self, $domain) = @_;
|
||||
# the DMARC draft suggests rejecting messages whose From: domain does not
|
||||
# exist in DNS. That's as far as it goes. So I went back to the ADSP (from
|
||||
# where DMARC this originated, which in turn led me to the ietf-dkim email
|
||||
# list where a handful of 'experts' failed to agree on The Right Way to
|
||||
# perform this test. And thus no direction was given.
|
||||
# As they point out:
|
||||
# MX records aren't mandatory.
|
||||
# A or AAAA records as fallback aren't reliable either.
|
||||
# 6. Receivers should endeavour to reject or quarantine email if the
|
||||
# RFC5322.From purports to be from a domain that appears to be
|
||||
# either non-existent or incapable of receiving mail.
|
||||
|
||||
# I chose to query the name and match NS,MX,A,or AAAA records. Since it gets
|
||||
# repeated for the for the Organizational Name, if it fails, there's no
|
||||
# delegation from the TLD.
|
||||
# I went back to the ADSP (from where DMARC this originated, which in turn
|
||||
# led me to the ietf-dkim email list where a handful of 'experts' failed to
|
||||
# agree on The Right Way to test domain validity. No direction was given.
|
||||
# They point out:
|
||||
# MX records aren't mandatory.
|
||||
# A or AAAA records as fallback aren't reliable.
|
||||
|
||||
# I chose to query the From: domain name and match NS,MX,A,or AAAA records.
|
||||
# Since this search gets repeated for the Organizational Name, if it
|
||||
# fails for the O.N., there's no delegation from the TLD.
|
||||
my $res = $self->init_resolver(8);
|
||||
my $query = $res->query($domain, 'NS') or do {
|
||||
return 1 if $self->host_has_rr('NS', $res, $domain);
|
||||
return 1 if $self->host_has_rr('MX', $res, $domain);
|
||||
return 1 if $self->host_has_rr('A', $res, $domain);
|
||||
return 1 if $self->host_has_rr('AAAA', $res, $domain);
|
||||
}
|
||||
|
||||
sub host_has_rr {
|
||||
my ($self, $type, $res, $domain) = @_;
|
||||
|
||||
my $query = $res->query($domain, $type) or do {
|
||||
if ($res->errorstring eq 'NXDOMAIN') {
|
||||
$self->log(LOGDEBUG, "fail, non-existent domain: $domain");
|
||||
return;
|
||||
}
|
||||
return if $res->errorstring eq 'NOERROR';
|
||||
$self->log(LOGINFO, "error, looking up $domain: " . $res->errorstring);
|
||||
return;
|
||||
};
|
||||
my $matches = 0;
|
||||
for my $rr ($query->answer) {
|
||||
next if $rr->type !~ /(?:NS|MX|A|AAAA)/;
|
||||
next if $rr->type ne $type;
|
||||
$matches++;
|
||||
}
|
||||
if (0 == $matches) {
|
||||
$self->log(LOGDEBUG, "fail, no records for $domain");
|
||||
$self->log(LOGDEBUG, "no $type records for $domain");
|
||||
}
|
||||
return $matches;
|
||||
}
|
||||
};
|
||||
|
||||
sub fetch_dmarc_record {
|
||||
my ($self, $zone) = @_;
|
||||
|
@ -246,7 +246,7 @@ sub helo_handler {
|
||||
my ($self, $transaction, $host) = @_;
|
||||
|
||||
if (!$host) {
|
||||
$self->log(LOGINFO, "fail, no helo host");
|
||||
$self->log(LOGINFO, "fail, tolerated, no helo host");
|
||||
return DECLINED;
|
||||
}
|
||||
|
||||
|
@ -244,9 +244,10 @@ sub register {
|
||||
|
||||
#$self->prune_db(); # keep the DB compact
|
||||
$self->register_hook('connect', 'connect_handler');
|
||||
$self->register_hook('rcpt_pre', 'rcpt_handler');
|
||||
$self->register_hook('data', 'data_handler');
|
||||
$self->register_hook('data_post', 'data_handler');
|
||||
$self->register_hook('disconnect', 'disconnect_handler');
|
||||
$self->register_hook('received_line', 'rcpt_handler');
|
||||
}
|
||||
|
||||
sub hook_pre_connection {
|
||||
@ -256,8 +257,6 @@ sub hook_pre_connection {
|
||||
|
||||
my $remote_ip = $args{remote_ip};
|
||||
|
||||
#my $max_conn = $args{max_conn_ip};
|
||||
|
||||
my $db = $self->get_db_location();
|
||||
my $lock = $self->get_db_lock($db) or return DECLINED;
|
||||
my $tied = $self->get_db_tie($db, $lock) or return DECLINED;
|
||||
@ -323,23 +322,43 @@ sub connect_handler {
|
||||
}
|
||||
|
||||
sub rcpt_handler {
|
||||
my ($self, $transaction, $recipient, %args) = @_;
|
||||
my ($self, $transaction, $addr) = @_;
|
||||
|
||||
my $recipients = scalar $self->transaction->recipients;
|
||||
return DECLINED if $recipients < 2; # only one recipient
|
||||
return DECLINED if $self->is_immune();
|
||||
|
||||
my $karma = $self->connection->notes('karma_history');
|
||||
return DECLINED if $karma > 0; # good karma, no limit
|
||||
my $recipients = scalar $self->transaction->recipients or do {
|
||||
$self->log(LOGDEBUG, "info, no recipient count");
|
||||
return DECLINED;
|
||||
};
|
||||
|
||||
my $history = $self->connection->notes('karma_history');
|
||||
if ( $history > 0 ) {
|
||||
$self->log(LOGDEBUG, "info, good history");
|
||||
return DECLINED;
|
||||
};
|
||||
|
||||
my $karma = $self->connection->notes('karma');
|
||||
if ( $karma > 0 ) {
|
||||
$self->log(LOGDEBUG, "info, good connection");
|
||||
return DECLINED;
|
||||
};
|
||||
|
||||
# limit # of recipients if host has negative or unknown karma
|
||||
return $self->get_reject("too many recipients");
|
||||
return (DENY, "too many recipients for karma $karma (h: $history)");
|
||||
}
|
||||
|
||||
sub data_handler {
|
||||
my ($self, $transaction) = @_;
|
||||
return DECLINED if !$self->qp->connection->relay_client;
|
||||
|
||||
$self->adjust_karma(5); # big karma boost for authenticated user/IP
|
||||
return DECLINED if $self->is_immune();
|
||||
return DECLINED if $self->is_naughty(); # let naughty do it
|
||||
|
||||
# cutting off a naughty sender at DATA prevents having to receive the message
|
||||
my $karma = $self->connection->notes('karma');
|
||||
if ( $karma < -3 ) { # bad karma
|
||||
return $self->get_reject("very bad karma: $karma");
|
||||
};
|
||||
|
||||
return DECLINED;
|
||||
}
|
||||
|
||||
|
@ -201,8 +201,10 @@ sub rcpt_handler {
|
||||
$self->log(LOGINFO, "pass, bouncesaying with program"), $k++ if $rv == 0x13;
|
||||
if ($rv == 0x14) {
|
||||
my $s = $transaction->sender->address;
|
||||
return (DENY, "mailing lists do not accept null senders")
|
||||
if (!$s || $s eq '<>');
|
||||
if (!$s || $s eq '<>') {
|
||||
$self->adjust_karma(-1);
|
||||
return (DENY, "mailing lists do not accept null senders");
|
||||
};
|
||||
$self->log(LOGINFO, "pass, ezmlm list");
|
||||
$k++;
|
||||
}
|
||||
|
@ -6,18 +6,18 @@ rcpt_ok
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
this plugin checks the standard rcpthosts config
|
||||
Validate that we accept mail for a recipient using a qmail rcpthosts file
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Check the recipient hostname and determine if we accept mail to that host.
|
||||
Check the envelope recipient hostname and determine if we accept mail to that host.
|
||||
|
||||
This is functionally identical to qmail's rcpthosts implementation, consulting
|
||||
both rcpthosts and morercpthosts.cdb.
|
||||
|
||||
=head1 CONFIGURATION
|
||||
|
||||
It should be configured to be run _LAST_!
|
||||
It should be configured as the _LAST_ recipient plugin!
|
||||
|
||||
=cut
|
||||
|
||||
@ -30,6 +30,8 @@ use Qpsmtpd::DSN;
|
||||
sub hook_rcpt {
|
||||
my ($self, $transaction, $recipient, %param) = @_;
|
||||
|
||||
return (OK) if $self->is_immune(); # relay_client or whitelist
|
||||
|
||||
# Allow 'no @' addresses for 'postmaster' and 'abuse'
|
||||
# qmail-smtpd will do this for all users without a domain, but we'll
|
||||
# be a bit more picky. Maybe that's a bad idea.
|
||||
@ -37,7 +39,6 @@ sub hook_rcpt {
|
||||
|
||||
return (OK) if $self->is_in_rcpthosts($host);
|
||||
return (OK) if $self->is_in_morercpthosts($host);
|
||||
return (OK) if $self->qp->connection->relay_client; # failsafe
|
||||
|
||||
# default of relaying_denied is obviously DENY,
|
||||
# we use the default "Relaying denied" message...
|
||||
|
@ -2,12 +2,15 @@
|
||||
# additional plugins running, add them here.
|
||||
# Fields are whitespace delimited. Columns are ordered by numeric plugin ID.
|
||||
#
|
||||
# the order of plugins in this file determines the order they appear in
|
||||
# summary output
|
||||
#
|
||||
#id name abb3 abb5 aliases
|
||||
#
|
||||
201 hosts_allow alw allow
|
||||
202 ident::geoip geo geoip
|
||||
203 ident::p0f p0f p0f
|
||||
204 ident::p0f_3a0 p0f p0f
|
||||
203 ident::p0f p0f p0f ident::p0f_3a0,ident::p0f_3a1
|
||||
|
||||
205 karma krm karma
|
||||
206 dnsbl dbl dnsbl
|
||||
207 relay rly relay check_relay,check_norelay,relay_only
|
||||
@ -26,13 +29,13 @@
|
||||
#
|
||||
# Authentication
|
||||
#
|
||||
400 auth::auth_vpopmail_sql aut vpsql
|
||||
401 auth::auth_vpopmaild vpd vpopd
|
||||
402 auth::auth_vpopmail vpo vpop
|
||||
403 auth::auth_checkpasswd ckp chkpw
|
||||
404 auth::auth_cvs_unix_local cvs cvsul
|
||||
405 auth::auth_flat_file flt aflat
|
||||
406 auth::auth_ldap_bind ldp aldap
|
||||
400 auth::auth_vpopmail_sql avq avsql
|
||||
401 auth::auth_vpopmaild avd vpopd
|
||||
402 auth::auth_vpopmail avp vpop
|
||||
403 auth::auth_checkpassword ack chkpw
|
||||
404 auth::auth_cvs_unix_local acv cvsul
|
||||
405 auth::auth_flat_file aff aflat
|
||||
406 auth::auth_ldap_bind ald aldap
|
||||
407 auth::authdeny dny adeny
|
||||
#
|
||||
# Sender / Envelope From
|
||||
@ -80,11 +83,11 @@
|
||||
#
|
||||
# Queue Plugins
|
||||
#
|
||||
800 queue::qmail-queue qqm queue
|
||||
800 queue::qmail-queue qqm queue queue::qmail_2dqueue
|
||||
801 queue::maildir qdr qudir
|
||||
802 queue::postfix-queue qpf qupfx
|
||||
803 queue::smtp-forward qfw qufwd
|
||||
804 queue::exim-bsmtp qxm qexim
|
||||
802 queue::postfix-queue qpf qupfx queue::postfix_2dqueue
|
||||
803 queue::smtp-forward qfw qufwd queue::smtp_2dqueue
|
||||
804 queue::exim-bsmtp qxm qexim queue::exim_2dbsmtp
|
||||
|
||||
900 quit_fortune for fortu
|
||||
|
||||
|
@ -116,7 +116,7 @@ sub hook_mail {
|
||||
return Qpsmtpd::DSN->temp_resolver_failed($self->get_reject_type(),
|
||||
'');
|
||||
}
|
||||
$self->log(LOGINFO, 'fail, missing result, reject disabled');
|
||||
$self->log(LOGINFO, 'fail, tolerated, missing result');
|
||||
return DECLINED;
|
||||
};
|
||||
|
||||
@ -127,7 +127,7 @@ sub hook_mail {
|
||||
|
||||
if (!$self->{_args}{reject}) {
|
||||
;
|
||||
$self->log(LOGINFO, "fail, reject disabled, $result");
|
||||
$self->log(LOGINFO, "fail, tolerated, $result");
|
||||
return DECLINED;
|
||||
}
|
||||
|
||||
|
@ -96,28 +96,17 @@ sub mail_handler {
|
||||
return (DECLINED, "SPF - null sender");
|
||||
}
|
||||
|
||||
if ($self->qp->connection->relay_client) {
|
||||
$self->log(LOGINFO, "skip, relay_client");
|
||||
return (DECLINED, "SPF - relaying permitted");
|
||||
}
|
||||
|
||||
if (!$self->{_args}{reject}) {
|
||||
$self->log(LOGINFO, "skip, reject disabled");
|
||||
return (DECLINED);
|
||||
}
|
||||
|
||||
my $client_ip = $self->qp->connection->remote_ip;
|
||||
my $from = $sender->user . '@' . lc($sender->host);
|
||||
my $helo = $self->qp->connection->hello_host;
|
||||
my $scope = $from ? 'mfrom' : 'helo';
|
||||
my %req_params = (
|
||||
versions => [1, 2], # optional
|
||||
scope => $scope,
|
||||
ip_address => $client_ip,
|
||||
ip_address => $self->qp->connection->remote_ip,
|
||||
);
|
||||
|
||||
if ($scope =~ /^mfrom|pra$/) {
|
||||
$req_params{identity} = $from;
|
||||
$req_params{identity} = $from;
|
||||
$req_params{helo_identity} = $helo if $helo;
|
||||
}
|
||||
elsif ($scope eq 'helo') {
|
||||
@ -144,44 +133,43 @@ sub mail_handler {
|
||||
return (DECLINED, "SPF - no response");
|
||||
}
|
||||
|
||||
if (!$reject) {
|
||||
$self->log(LOGINFO, "fail, no reject policy ($code: $why)");
|
||||
return (DECLINED, "SPF - $code: $why");
|
||||
}
|
||||
|
||||
# SPF result codes: pass fail softfail neutral none error permerror temperror
|
||||
return $self->handle_code_none($reject, $why) if $code eq 'none';
|
||||
if ($code eq 'fail') {
|
||||
$self->adjust_karma(-1);
|
||||
return $self->handle_code_fail($reject, $why);
|
||||
}
|
||||
elsif ($code eq 'softfail') {
|
||||
$self->adjust_karma(-1);
|
||||
return $self->handle_code_softfail($reject, $why);
|
||||
}
|
||||
elsif ($code eq 'pass') {
|
||||
if ($code eq 'pass') {
|
||||
$self->adjust_karma(1);
|
||||
$transaction->notes('spf_pass_host', lc $sender->host);
|
||||
$self->log(LOGINFO, "pass, $code: $why");
|
||||
return (DECLINED);
|
||||
}
|
||||
elsif ($code eq 'neutral') {
|
||||
$self->log(LOGINFO, "fail, $code, $why");
|
||||
return (DENY, "SPF - $code: $why") if $reject >= 5;
|
||||
|
||||
if (!$reject) {
|
||||
$self->log(LOGINFO, "skip, tolerated ($code: $why)");
|
||||
return (DECLINED, "SPF - $code: $why");
|
||||
}
|
||||
elsif ($code eq 'error') {
|
||||
$self->log(LOGINFO, "fail, $code, $why");
|
||||
|
||||
# SPF result codes: pass fail softfail neutral none error permerror temperror
|
||||
return $self->handle_code_none($reject, $why) if $code eq 'none';
|
||||
return $self->handle_code_fail($reject, $why) if $code eq 'fail';
|
||||
return $self->handle_code_softfail($reject, $why) if $code eq 'softfail';
|
||||
|
||||
if ($code eq 'neutral') {
|
||||
if ($reject >= 5 ) {
|
||||
$self->log(LOGINFO, "fail, $code, $why");
|
||||
return (DENY, "SPF - $code: $why");
|
||||
};
|
||||
$self->log(LOGINFO, "fail, tolerated, $code, $why");
|
||||
return (DECLINED);
|
||||
}
|
||||
if ($code =~ /(?:permerror|error)/ ) {
|
||||
$self->log(LOGINFO, "fail, $code, $why") if $reject > 3;
|
||||
return (DENY, "SPF - $code: $why") if $reject >= 6;
|
||||
return (DENYSOFT, "SPF - $code: $why") if $reject > 3;
|
||||
$self->log(LOGINFO, "fail, tolerated, $code, $why");
|
||||
return (DECLINED);
|
||||
}
|
||||
elsif ($code eq 'permerror') {
|
||||
$self->log(LOGINFO, "fail, $code, $why");
|
||||
return (DENY, "SPF - $code: $why") if $reject >= 6;
|
||||
return (DENYSOFT, "SPF - $code: $why") if $reject > 3;
|
||||
}
|
||||
elsif ($code eq 'temperror') {
|
||||
if ($code eq 'temperror') {
|
||||
$self->log(LOGINFO, "fail, $code, $why");
|
||||
return (DENYSOFT, "SPF - $code: $why") if $reject >= 2;
|
||||
$self->log(LOGINFO, "fail, tolerated, $code, $why");
|
||||
return (DECLINED);
|
||||
}
|
||||
|
||||
$self->log(LOGINFO, "SPF from $from was $code: $why");
|
||||
@ -196,33 +184,37 @@ sub handle_code_none {
|
||||
return (DENY, "SPF - none: $why");
|
||||
}
|
||||
|
||||
$self->log(LOGINFO, "pass, none, $why");
|
||||
$self->log(LOGINFO, "skip, tolerated, none, $why");
|
||||
return DECLINED;
|
||||
}
|
||||
|
||||
sub handle_code_fail {
|
||||
my ($self, $reject, $why) = @_;
|
||||
|
||||
$self->adjust_karma(-1);
|
||||
|
||||
if ($reject >= 2) {
|
||||
$self->log(LOGINFO, "fail, $why");
|
||||
return (DENY, "SPF - forgery: $why") if $reject >= 3;
|
||||
return (DENYSOFT, "SPF - fail: $why");
|
||||
}
|
||||
|
||||
$self->log(LOGINFO, "pass, fail tolerated, $why");
|
||||
$self->log(LOGINFO, "fail, tolerated, $why");
|
||||
return DECLINED;
|
||||
}
|
||||
|
||||
sub handle_code_softfail {
|
||||
my ($self, $reject, $why) = @_;
|
||||
|
||||
$self->adjust_karma(-1);
|
||||
|
||||
if ($reject >= 3) {
|
||||
$self->log(LOGINFO, "fail, soft, $why");
|
||||
return (DENY, "SPF - fail: $why") if $reject >= 4;
|
||||
return (DENYSOFT, "SPF - fail: $why") if $reject >= 3;
|
||||
}
|
||||
|
||||
$self->log(LOGINFO, "pass, softfail tolerated, $why");
|
||||
$self->log(LOGINFO, "fail, tolerated, soft, $why");
|
||||
return DECLINED;
|
||||
}
|
||||
|
||||
|
@ -178,7 +178,7 @@ sub data_post_handler {
|
||||
|
||||
if ($transaction->data_size > 500_000) {
|
||||
$self->log(LOGINFO,
|
||||
"skip: too large (" . $transaction->data_size . ")");
|
||||
"skip, too large (" . $transaction->data_size . ")");
|
||||
return (DECLINED);
|
||||
}
|
||||
|
||||
@ -424,7 +424,7 @@ sub reject {
|
||||
|
||||
if ($score < $reject) {
|
||||
if ($ham_or_spam eq 'Spam') {
|
||||
$self->log(LOGINFO, "fail, $status < $reject, $learn");
|
||||
$self->log(LOGINFO, "fail, tolerated, $status < $reject, $learn");
|
||||
return DECLINED;
|
||||
}
|
||||
else {
|
||||
|
@ -149,12 +149,16 @@ sub hook_connect {
|
||||
my ($self, $transaction) = @_;
|
||||
|
||||
my $local_port = $self->qp->connection->local_port;
|
||||
return DECLINED unless defined $local_port && $local_port == 465; # SMTPS
|
||||
if ( ! defined $local_port || $local_port != 465 ) { # SMTPS
|
||||
$self->log(LOGDEBUG, "skip, not SMTPS");
|
||||
return DECLINED;
|
||||
};
|
||||
|
||||
unless (_convert_to_ssl($self)) {
|
||||
$self->log(LOGINFO, "fail, unable to establish SSL");
|
||||
return (DENY_DISCONNECT, "Cannot establish SSL session");
|
||||
}
|
||||
$self->log(LOGWARN, "Connected via SMTPS");
|
||||
$self->log(LOGINFO, "pass, connect via SMTPS");
|
||||
return DECLINED;
|
||||
}
|
||||
|
||||
|
4
run
4
run
@ -2,8 +2,8 @@
|
||||
#
|
||||
# You might want/need to to edit these settings
|
||||
QPUSER=smtpd
|
||||
# limit qpsmtpd to 150MB memory, should be several times what is needed.
|
||||
MAXRAM=150000000
|
||||
# limit qpsmtpd to 300MB memory
|
||||
MAXRAM=300000000
|
||||
BIN=/usr/local/bin
|
||||
PERL=/usr/bin/perl
|
||||
|
||||
|
@ -12,7 +12,7 @@ my $test_email = 'matt@tnpi.net';
|
||||
sub register_tests {
|
||||
my $self = shift;
|
||||
|
||||
$self->register_test('test_get_organizational_domain', 2);
|
||||
$self->register_test('test_get_organizational_domain', 3);
|
||||
$self->register_test("test_fetch_dmarc_record", 3);
|
||||
$self->register_test("test_discover_policy", 1);
|
||||
}
|
||||
@ -55,7 +55,8 @@ sub test_get_organizational_domain {
|
||||
my $transaction = $self->qp->transaction;
|
||||
|
||||
cmp_ok( $self->get_organizational_domain('test.www.tnpi.net'), 'eq', 'tnpi.net' );
|
||||
cmp_ok( $self->get_organizational_domain('www.example.co.uk'), 'eq', 'example.co.uk' )
|
||||
cmp_ok( $self->get_organizational_domain('www.example.co.uk'), 'eq', 'example.co.uk' );
|
||||
cmp_ok( $self->get_organizational_domain('plus.google.com'), 'eq', 'google.com' );
|
||||
};
|
||||
|
||||
sub test_discover_policy {
|
||||
|
Loading…
Reference in New Issue
Block a user