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
|
karma: sprinkled karma awards throughout other plugins
|
||||||
- limit poor karma hosts to 1 concurrent connection
|
- limit poor karma hosts to 1 concurrent connection
|
||||||
- allow +3 conncurrent connections to hosts with good karma
|
- 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
|
Sanitize spamd_sock path for perl taint mode - Markus Ullmann
|
||||||
|
|
||||||
|
@ -36,11 +36,11 @@ sub MY::libscan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sub MY::postamble {
|
sub MY::postamble {
|
||||||
qq[
|
qq[
|
||||||
testcover :
|
testcover :
|
||||||
\t cover -delete && \\
|
\t cover -delete && \\
|
||||||
HARNESS_PERL_SWITCHES=-MDevel::Cover \$(MAKE) test && \\
|
HARNESS_PERL_SWITCHES=-MDevel::Cover \$(MAKE) test && \\
|
||||||
cover
|
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
|
So far, the main changes between the release and dev branches have focused
|
||||||
on these goals:
|
on these goals:
|
||||||
|
|
||||||
|
- plugins use is_immune and is_naughty instead of a local methods
|
||||||
- plugins log a single entry summarizing their disposition
|
- plugins log a single entry summarizing their disposition
|
||||||
- plugin logs prefixed with keywords: pass, fail, skip, error
|
- plugin logs prefixed with keywords: pass, fail, skip, error
|
||||||
- plugins use 'reject' and 'reject_type' settings
|
- 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.
|
# plugins/http_config for details.
|
||||||
# http_config http://localhost/~smtpd/config/ http://www.example.com/smtp.pl?config=
|
# 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!
|
# hosts_allow does not work with the tcpserver deployment model!
|
||||||
# perldoc plugins/hosts_allow for an alternative.
|
# perldoc plugins/hosts_allow for an alternative.
|
||||||
#
|
#
|
||||||
@ -23,8 +27,6 @@ ident/geoip
|
|||||||
fcrdns
|
fcrdns
|
||||||
|
|
||||||
quit_fortune
|
quit_fortune
|
||||||
# tls should load before count_unrecognized_commands
|
|
||||||
#tls
|
|
||||||
earlytalker
|
earlytalker
|
||||||
count_unrecognized_commands 4
|
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
|
L<plugins/logging/adaptive> files for examples of how to write your own
|
||||||
logging plugins.
|
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
|
=head1 Internal support for pluggable logging
|
||||||
|
|
||||||
Any code in the core can call C<$self->log()> and those log lines will be
|
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};
|
my $reject = $self->{_args}{reject};
|
||||||
if (defined $reject && !$reject) {
|
if (defined $reject && !$reject) {
|
||||||
$self->log(LOGINFO, "fail, reject disabled" . $log_mess);
|
$self->log(LOGINFO, "fail, tolerated" . $log_mess);
|
||||||
return DECLINED;
|
return DECLINED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,6 +303,12 @@ sub is_immune {
|
|||||||
$self->log(LOGINFO, "skip, whitelisted sender");
|
$self->log(LOGINFO, "skip, whitelisted sender");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub is_naughty {
|
||||||
|
my $self = shift;
|
||||||
|
|
||||||
if ($self->connection->notes('naughty')) {
|
if ($self->connection->notes('naughty')) {
|
||||||
|
|
||||||
# see plugins/naughty
|
# see plugins/naughty
|
||||||
@ -323,7 +329,7 @@ sub adjust_karma {
|
|||||||
|
|
||||||
my $karma = $self->connection->notes('karma') || 0;
|
my $karma = $self->connection->notes('karma') || 0;
|
||||||
$karma += $value;
|
$karma += $value;
|
||||||
$self->log(LOGDEBUG, "karma adjust: $value ($karma)");
|
$self->log(LOGDEBUG, "karma $value ($karma)");
|
||||||
$self->connection->notes('karma', $karma);
|
$self->connection->notes('karma', $karma);
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
74
log/log2sql
74
log/log2sql
@ -6,21 +6,19 @@ use warnings;
|
|||||||
use Cwd;
|
use Cwd;
|
||||||
use Data::Dumper;
|
use Data::Dumper;
|
||||||
use DBIx::Simple;
|
use DBIx::Simple;
|
||||||
|
use IO::File;
|
||||||
use File::stat;
|
use File::stat;
|
||||||
use Time::TAI64 qw/ tai2unix /;
|
use Time::TAI64 qw/ tai2unix /;
|
||||||
|
|
||||||
$Data::Dumper::Sortkeys = 1;
|
$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 $logdir = get_log_dir();
|
||||||
my @logfiles = get_logfiles($logdir);
|
my @logfiles = get_logfiles($logdir);
|
||||||
|
|
||||||
my (%plugins, %os, %message_ids);
|
my (%plugins, %os, %message_ids);
|
||||||
my $has_cleanup;
|
my $has_cleanup;
|
||||||
my $db = get_db();
|
my $db = get_db();
|
||||||
|
check_plugins_table();
|
||||||
|
|
||||||
foreach my $file (@logfiles) {
|
foreach my $file (@logfiles) {
|
||||||
my ($fid, $offset) = check_logfile($file);
|
my ($fid, $offset) = check_logfile($file);
|
||||||
@ -208,6 +206,7 @@ sub parse_logfile {
|
|||||||
#warn "type: $type\n";
|
#warn "type: $type\n";
|
||||||
if ($type eq 'plugin') {
|
if ($type eq 'plugin') {
|
||||||
next if $plugin eq 'naughty'; # housekeeping only
|
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);
|
insert_plugin($msg_id, $plugin, $message);
|
||||||
}
|
}
|
||||||
elsif ($type eq 'queue') {
|
elsif ($type eq 'queue') {
|
||||||
@ -529,12 +528,70 @@ sub get_score {
|
|||||||
|
|
||||||
sub get_db {
|
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;
|
or die DBIx::Simple->error;
|
||||||
|
|
||||||
return $db;
|
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 {
|
sub exec_query {
|
||||||
my $query = shift;
|
my $query = shift;
|
||||||
my $params = shift;
|
my $params = shift;
|
||||||
@ -550,10 +607,11 @@ sub exec_query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#warn "err: $err\n";
|
#warn "err: $err\n";
|
||||||
if ($query =~ /INSERT INTO/) {
|
if ($query =~ /(?:REPLACE|INSERT) INTO/) {
|
||||||
my ($table) = $query =~ /INSERT INTO (\w+)\s/;
|
my ($table) = $query =~ /(?:REPLACE|INSERT) INTO (\w+)\s/;
|
||||||
$db->query($query, @params);
|
$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;
|
my $id = $db->last_insert_id(undef, undef, $table, undef) or die $err;
|
||||||
return $id;
|
return $id;
|
||||||
}
|
}
|
||||||
|
146
log/log2sql.sql
146
log/log2sql.sql
@ -13,35 +13,34 @@
|
|||||||
DROP TABLE IF EXISTS `log`;
|
DROP TABLE IF EXISTS `log`;
|
||||||
|
|
||||||
CREATE TABLE `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,
|
`inode` int(11) unsigned NOT NULL,
|
||||||
`size` int(11) unsigned NOT NULL,
|
`size` int(11) unsigned NOT NULL,
|
||||||
`name` varchar(30) NOT NULL default '',
|
`name` varchar(30) NOT NULL DEFAULT '',
|
||||||
`created` datetime default NULL,
|
`created` datetime DEFAULT NULL,
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||||
|
|
||||||
|
|
||||||
# Dump of table message
|
# Dump of table message
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
DROP TABLE IF EXISTS `message`;
|
DROP TABLE IF EXISTS `message`;
|
||||||
|
|
||||||
CREATE TABLE `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,
|
`file_id` int(10) unsigned NOT NULL,
|
||||||
`connect_start` datetime NOT NULL,
|
`connect_start` datetime NOT NULL,
|
||||||
`ip` int(10) unsigned NOT NULL,
|
`ip` int(10) unsigned NOT NULL,
|
||||||
`qp_pid` int(10) unsigned NOT NULL,
|
`qp_pid` int(10) unsigned NOT NULL,
|
||||||
`result` tinyint(3) NOT NULL default '0',
|
`result` tinyint(3) NOT NULL DEFAULT '0',
|
||||||
`distance` mediumint(8) unsigned default NULL,
|
`distance` mediumint(8) unsigned DEFAULT NULL,
|
||||||
`time` decimal(3,2) unsigned default NULL,
|
`time` decimal(3,2) unsigned DEFAULT NULL,
|
||||||
`os_id` tinyint(3) unsigned default NULL,
|
`os_id` tinyint(3) unsigned DEFAULT NULL,
|
||||||
`hostname` varchar(128) default NULL,
|
`hostname` varchar(128) DEFAULT NULL,
|
||||||
`helo` varchar(128) default NULL,
|
`helo` varchar(128) DEFAULT NULL,
|
||||||
`mail_from` varchar(128) default NULL,
|
`mail_from` varchar(128) DEFAULT NULL,
|
||||||
`rcpt_to` varchar(128) default NULL,
|
`rcpt_to` varchar(128) DEFAULT NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `file_id` (`file_id`),
|
KEY `file_id` (`file_id`),
|
||||||
CONSTRAINT `message_ibfk_1` FOREIGN KEY (`file_id`) REFERENCES `log` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
CONSTRAINT `message_ibfk_1` FOREIGN KEY (`file_id`) REFERENCES `log` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||||
@ -54,12 +53,12 @@ CREATE TABLE `message` (
|
|||||||
DROP TABLE IF EXISTS `message_plugin`;
|
DROP TABLE IF EXISTS `message_plugin`;
|
||||||
|
|
||||||
CREATE TABLE `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,
|
`msg_id` int(11) unsigned NOT NULL,
|
||||||
`plugin_id` int(4) unsigned NOT NULL,
|
`plugin_id` int(4) unsigned NOT NULL,
|
||||||
`result` tinyint(4) NOT NULL,
|
`result` tinyint(4) NOT NULL,
|
||||||
`string` varchar(128) default NULL,
|
`string` varchar(128) DEFAULT NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `msg_id` (`msg_id`),
|
KEY `msg_id` (`msg_id`),
|
||||||
KEY `plugin_id` (`plugin_id`),
|
KEY `plugin_id` (`plugin_id`),
|
||||||
CONSTRAINT `message_plugin_ibfk_1` FOREIGN KEY (`plugin_id`) REFERENCES `plugin` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
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;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Dump of table os
|
# Dump of table os
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
DROP TABLE IF EXISTS `os`;
|
DROP TABLE IF EXISTS `os`;
|
||||||
|
|
||||||
CREATE TABLE `os` (
|
CREATE TABLE `os` (
|
||||||
`id` tinyint(3) unsigned NOT NULL auto_increment,
|
`id` tinyint(3) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
`name` varchar(36) default NULL,
|
`name` varchar(36) DEFAULT NULL,
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||||
|
|
||||||
LOCK TABLES `os` WRITE;
|
LOCK TABLES `os` WRITE;
|
||||||
@ -114,81 +112,14 @@ UNLOCK TABLES;
|
|||||||
DROP TABLE IF EXISTS `plugin`;
|
DROP TABLE IF EXISTS `plugin`;
|
||||||
|
|
||||||
CREATE TABLE `plugin` (
|
CREATE TABLE `plugin` (
|
||||||
`id` int(4) unsigned NOT NULL auto_increment,
|
`id` int(4) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
`name` varchar(35) character set utf8 NOT NULL default '',
|
`name` varchar(35) CHARACTER SET utf8 NOT NULL DEFAULT '',
|
||||||
`abb3` char(3) character set utf8 default NULL,
|
`abb3` char(3) CHARACTER SET utf8 DEFAULT NULL,
|
||||||
`abb5` char(5) character set utf8 default NULL,
|
`abb5` char(5) CHARACTER SET utf8 DEFAULT NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `abb3` (`abb3`),
|
|
||||||
UNIQUE KEY `abb5` (`abb5`)
|
UNIQUE KEY `abb5` (`abb5`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
|
) 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
|
# Dump of table plugin_aliases
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
@ -197,33 +128,10 @@ DROP TABLE IF EXISTS `plugin_aliases`;
|
|||||||
|
|
||||||
CREATE TABLE `plugin_aliases` (
|
CREATE TABLE `plugin_aliases` (
|
||||||
`plugin_id` int(11) unsigned NOT NULL,
|
`plugin_id` int(11) unsigned NOT NULL,
|
||||||
`name` varchar(35) character set utf8 NOT NULL default '',
|
`name` varchar(35) CHARACTER SET utf8 NOT NULL DEFAULT '',
|
||||||
KEY `plugin_id` (`plugin_id`),
|
UNIQUE KEY `plugin_id` (`plugin_id`,`name`)
|
||||||
CONSTRAINT `plugin_id` FOREIGN KEY (`plugin_id`) REFERENCES `plugin` (`id`) ON UPDATE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
|
) 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 */;
|
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||||
|
@ -6,9 +6,13 @@ use warnings;
|
|||||||
use Cwd;
|
use Cwd;
|
||||||
use Data::Dumper;
|
use Data::Dumper;
|
||||||
use File::Tail;
|
use File::Tail;
|
||||||
|
use Getopt::Std;
|
||||||
|
|
||||||
$Data::Dumper::Sortkeys = 1;
|
$Data::Dumper::Sortkeys = 1;
|
||||||
|
|
||||||
|
our $opt_l = 0;
|
||||||
|
getopts('l');
|
||||||
|
|
||||||
my (%plugins, %plugin_aliases, %seen_plugins, %pids);
|
my (%plugins, %plugin_aliases, %seen_plugins, %pids);
|
||||||
my %hide_plugins = map { $_ => 1 } qw/ hostname /;
|
my %hide_plugins = map { $_ => 1 } qw/ hostname /;
|
||||||
|
|
||||||
@ -32,7 +36,7 @@ my %formats = (
|
|||||||
ip => "%-15.15s",
|
ip => "%-15.15s",
|
||||||
hostname => "%-20.20s",
|
hostname => "%-20.20s",
|
||||||
distance => "%5.5s",
|
distance => "%5.5s",
|
||||||
'ident::geoip' => "%-20.20s",
|
'ident::geoip' => $opt_l ? "%-20.20s" : "%-6.6s",
|
||||||
'ident::p0f' => "%-10.10s",
|
'ident::p0f' => "%-10.10s",
|
||||||
count_unrecognized_commands => "%-5.5s",
|
count_unrecognized_commands => "%-5.5s",
|
||||||
unrecognized_commands => "%-5.5s",
|
unrecognized_commands => "%-5.5s",
|
||||||
@ -269,18 +273,20 @@ sub print_auto_format {
|
|||||||
next;
|
next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
my $wide = $opt_l ? 20 : 8;
|
||||||
|
|
||||||
if (defined $pids{$pid}{helo_host} && $plugin =~ /helo/) {
|
if (defined $pids{$pid}{helo_host} && $plugin =~ /helo/) {
|
||||||
$format .= " %-18.18s";
|
$format .= " %-$wide.${wide}s";
|
||||||
push @values, substr(delete $pids{$pid}{helo_host}, -18, 18);
|
push @values, substr(delete $pids{$pid}{helo_host}, -$wide, $wide);
|
||||||
push @headers, 'HELO';
|
push @headers, 'HELO';
|
||||||
}
|
}
|
||||||
elsif (defined $pids{$pid}{from} && $plugin =~ /from/) {
|
elsif (defined $pids{$pid}{from} && $plugin =~ /from/) {
|
||||||
$format .= " %-20.20s";
|
$format .= " %-$wide.${wide}s";
|
||||||
push @values, substr(delete $pids{$pid}{from}, -20, 20);
|
push @values, substr(delete $pids{$pid}{from}, -$wide, $wide);
|
||||||
push @headers, 'MAIL FROM';
|
push @headers, 'MAIL FROM';
|
||||||
}
|
}
|
||||||
elsif (defined $pids{$pid}{to} && $plugin =~ /to|rcpt|recipient/) {
|
elsif (defined $pids{$pid}{to} && $plugin =~ /to|rcpt|recipient/) {
|
||||||
$format .= " %-20.20s";
|
$format .= " %-$wide.${wide}s";
|
||||||
push @values, delete $pids{$pid}{to};
|
push @values, delete $pids{$pid}{to};
|
||||||
push @headers, 'RCPT TO';
|
push @headers, 'RCPT TO';
|
||||||
}
|
}
|
||||||
@ -299,7 +305,7 @@ sub print_auto_format {
|
|||||||
$format .= "\n";
|
$format .= "\n";
|
||||||
printf("\n$format", @headers) if (!$printed || $printed % 20 == 0);
|
printf("\n$format", @headers) if (!$printed || $printed % 20 == 0);
|
||||||
printf($format, @values);
|
printf($format, @values);
|
||||||
print Data::Dumper::Dumper($pids{$pid}) if keys %{$pids{$pid}};
|
#print Data::Dumper::Dumper($pids{$pid}) if keys %{$pids{$pid}};
|
||||||
$printed++;
|
$printed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,6 +314,7 @@ sub show_symbol {
|
|||||||
return ' o' if $mess eq 'TLS setup returning';
|
return ' o' if $mess eq 'TLS setup returning';
|
||||||
return ' o' if $mess eq 'pass';
|
return ' o' if $mess eq 'pass';
|
||||||
return ' -' if $mess eq 'skip';
|
return ' -' if $mess eq 'skip';
|
||||||
|
return ' x' if 'fail, tolerated' eq substr($mess, 0, 15);
|
||||||
return ' X' if $mess eq 'fail';
|
return ' X' if $mess eq 'fail';
|
||||||
return ' -' if $mess =~ /^skip[,:\s]/i;
|
return ' -' if $mess =~ /^skip[,:\s]/i;
|
||||||
return ' o' if $mess =~ /^pass[,:\s]/i;
|
return ' o' if $mess =~ /^pass[,:\s]/i;
|
||||||
@ -347,6 +354,8 @@ sub populate_plugins_from_registry {
|
|||||||
open my $F, '<', $file;
|
open my $F, '<', $file;
|
||||||
while (defined(my $line = <$F>)) {
|
while (defined(my $line = <$F>)) {
|
||||||
next if $line =~ /^#/; # discard comments
|
next if $line =~ /^#/; # discard comments
|
||||||
|
chomp $line;
|
||||||
|
next if ! $line;
|
||||||
my ($id, $name, $abb3, $abb5, $aliases) = split /\s+/, $line;
|
my ($id, $name, $abb3, $abb5, $aliases) = split /\s+/, $line;
|
||||||
next if !defined $name;
|
next if !defined $name;
|
||||||
$plugins{$name} = {id => $id, abb3 => $abb3, abb5 => $abb5};
|
$plugins{$name} = {id => $id, abb3 => $abb3, abb5 => $abb5};
|
||||||
|
@ -136,11 +136,12 @@ sub auth_checkpassword {
|
|||||||
my $status = $?;
|
my $status = $?;
|
||||||
|
|
||||||
if ($status != 0) {
|
if ($status != 0) {
|
||||||
$self->log(LOGNOTICE, "authentication failed ($status)");
|
$self->log(LOGNOTICE, "fail, auth failed: $status");
|
||||||
return (DECLINED);
|
return (DECLINED);
|
||||||
}
|
}
|
||||||
|
|
||||||
$self->connection->notes('authuser', $user);
|
$self->connection->notes('authuser', $user);
|
||||||
|
$self->log(LOGINFO, "pass, auth success with $method");
|
||||||
return (OK, "auth_checkpassword");
|
return (OK, "auth_checkpassword");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
17
plugins/dkim
17
plugins/dkim
@ -353,7 +353,7 @@ sub handle_sig_pass {
|
|||||||
elsif ($prs->{neutral}) {
|
elsif ($prs->{neutral}) {
|
||||||
$self->add_header($mess);
|
$self->add_header($mess);
|
||||||
$self->log(LOGINFO, "pass, valid signature, neutral policy");
|
$self->log(LOGINFO, "pass, valid signature, neutral policy");
|
||||||
$self->log(LOGINFO, $mess);
|
$self->log(LOGDEBUG, $mess);
|
||||||
return DECLINED;
|
return DECLINED;
|
||||||
}
|
}
|
||||||
elsif ($prs->{reject}) {
|
elsif ($prs->{reject}) {
|
||||||
@ -364,7 +364,7 @@ sub handle_sig_pass {
|
|||||||
"fail, valid sig, reject policy");
|
"fail, valid sig, reject policy");
|
||||||
}
|
}
|
||||||
|
|
||||||
# this should never happen
|
# this should never happen,
|
||||||
$self->add_header($mess);
|
$self->add_header($mess);
|
||||||
$self->log(LOGERROR, "pass, valid sig, no policy results");
|
$self->log(LOGERROR, "pass, valid sig, no policy results");
|
||||||
$self->log(LOGINFO, $mess);
|
$self->log(LOGINFO, $mess);
|
||||||
@ -449,14 +449,17 @@ sub get_keydir {
|
|||||||
sub save_signatures_to_note {
|
sub save_signatures_to_note {
|
||||||
my ($self, $dkim) = @_;
|
my ($self, $dkim) = @_;
|
||||||
|
|
||||||
|
my %domains;
|
||||||
foreach my $sig ($dkim->signatures) {
|
foreach my $sig ($dkim->signatures) {
|
||||||
next if $sig->result ne 'pass';
|
next if $sig->result ne 'pass';
|
||||||
my $doms = $self->connection->notes('dkim_pass_domains') || [];
|
$domains{$sig->domain} = 1;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
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 {
|
sub send_message_to_dkim {
|
||||||
|
102
plugins/dmarc
102
plugins/dmarc
@ -6,6 +6,10 @@ Domain-based Message Authentication, Reporting and Conformance
|
|||||||
|
|
||||||
=head1 SYNOPSIS
|
=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."
|
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.
|
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.
|
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
|
=head1 HOWTO
|
||||||
|
|
||||||
|
=head2 Protect a domain with DMARC
|
||||||
|
|
||||||
See Section 10 of the draft: Domain Owner Actions
|
See Section 10 of the draft: Domain Owner Actions
|
||||||
|
|
||||||
1. Deploy DKIM & SPF
|
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
|
3. Publish a "monitor" record, ask for data reports
|
||||||
4. Roll policies from monitor to reject
|
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)
|
v=DMARC1; (version)
|
||||||
p=none; (disposition policy : reject, quarantine, none (monitor))
|
p=none; (disposition policy : reject, quarantine, none (monitor))
|
||||||
sp=reject; (subdomain policy: default, same as p)
|
sp=reject; (subdomain policy: default, same as p)
|
||||||
rua
|
|
||||||
adkim=s; (dkim alignment: s=strict, r=relaxed)
|
adkim=s; (dkim alignment: s=strict, r=relaxed)
|
||||||
aspf=r; (spf alignment: s=strict, r=relaxed)
|
aspf=r; (spf alignment: s=strict, r=relaxed)
|
||||||
rua=mailto: dmarc-feedback\@$zone; (aggregate reports)
|
rua=mailto: dmarc-feedback@example.com; (aggregate reports)
|
||||||
ruf=mailto: dmarc-feedback\@$zone.com; (forensic reports)
|
ruf=mailto: dmarc-feedback@example.com; (forensic reports)
|
||||||
rf=afrf; (report format: afrf, iodef)
|
rf=afrf; (report format: afrf, iodef)
|
||||||
ri=8400; (report interval)
|
ri=8400; (report interval)
|
||||||
pct=50; (percent of messages to filter)
|
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
|
http://www.dmarc.org/draft-dmarc-base-00-02.txt
|
||||||
|
|
||||||
|
https://github.com/qpsmtpd-dev/qpsmtpd-dev/wiki/DMARC-FAQ
|
||||||
|
|
||||||
=head1 TODO
|
=head1 TODO
|
||||||
|
|
||||||
2. provide dmarc feedback to domains that request it
|
2. provide dmarc feedback to domains that request it
|
||||||
|
|
||||||
3. If a message has multiple 'From' recipients, reject it
|
3. If a message has multiple 'From' recipients, reject it
|
||||||
|
|
||||||
4. Rejections with a 550 (perm) or 450 (temp)
|
|
||||||
|
|
||||||
=head1 IMPLEMENTATION
|
=head1 IMPLEMENTATION
|
||||||
|
|
||||||
1. Primary identifier is RFC5322.From field (From: header)
|
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 $from_host = $self->get_from_host($transaction) or return DECLINED;
|
||||||
my $org_host = $self->get_organizational_domain($from_host);
|
my $org_host = $self->get_organizational_domain($from_host);
|
||||||
|
|
||||||
if (!$self->exists_in_dns($from_host)) {
|
# 6. Receivers should reject email if the domain appears to not exist
|
||||||
if (!$self->exists_in_dns($org_host)) {
|
if (!$self->exists_in_dns($from_host) && !$self->exists_in_dns($org_host)) {
|
||||||
$self->log(LOGINFO, "fail, $from_host not in DNS");
|
$self->log(LOGINFO, "fail, $from_host not in DNS");
|
||||||
return $self->get_reject("RFC5322.From host does not exist");
|
return $self->get_reject("RFC5322.From host appears non-existent");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# 11.2. Determine Handling Policy
|
# 11.2. Determine Handling Policy
|
||||||
@ -129,6 +146,12 @@ sub data_post_handler {
|
|||||||
# Domain Owner. See Section 6.2 for details.
|
# Domain Owner. See Section 6.2 for details.
|
||||||
return DECLINED if lc $policy->{p} eq 'none';
|
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");
|
return $self->get_reject("failed DMARC policy");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,9 +302,16 @@ sub get_organizational_domain {
|
|||||||
|
|
||||||
# $self->log( LOGINFO, "i: $i, $tld" );
|
# $self->log( LOGINFO, "i: $i, $tld" );
|
||||||
#warn "i: $i - tld: $tld\n";
|
#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;
|
$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
|
return $from_host if $greatest == scalar @labels; # same
|
||||||
@ -295,37 +325,49 @@ sub get_organizational_domain {
|
|||||||
|
|
||||||
sub exists_in_dns {
|
sub exists_in_dns {
|
||||||
my ($self, $domain) = @_;
|
my ($self, $domain) = @_;
|
||||||
# the DMARC draft suggests rejecting messages whose From: domain does not
|
# 6. Receivers should endeavour to reject or quarantine email if the
|
||||||
# exist in DNS. That's as far as it goes. So I went back to the ADSP (from
|
# RFC5322.From purports to be from a domain that appears to be
|
||||||
# where DMARC this originated, which in turn led me to the ietf-dkim email
|
# either non-existent or incapable of receiving mail.
|
||||||
# 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.
|
|
||||||
|
|
||||||
# I chose to query the name and match NS,MX,A,or AAAA records. Since it gets
|
# I went back to the ADSP (from where DMARC this originated, which in turn
|
||||||
# repeated for the for the Organizational Name, if it fails, there's no
|
# led me to the ietf-dkim email list where a handful of 'experts' failed to
|
||||||
# delegation from the TLD.
|
# 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 $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') {
|
if ($res->errorstring eq 'NXDOMAIN') {
|
||||||
$self->log(LOGDEBUG, "fail, non-existent domain: $domain");
|
$self->log(LOGDEBUG, "fail, non-existent domain: $domain");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
return if $res->errorstring eq 'NOERROR';
|
||||||
$self->log(LOGINFO, "error, looking up $domain: " . $res->errorstring);
|
$self->log(LOGINFO, "error, looking up $domain: " . $res->errorstring);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
my $matches = 0;
|
my $matches = 0;
|
||||||
for my $rr ($query->answer) {
|
for my $rr ($query->answer) {
|
||||||
next if $rr->type !~ /(?:NS|MX|A|AAAA)/;
|
next if $rr->type ne $type;
|
||||||
$matches++;
|
$matches++;
|
||||||
}
|
}
|
||||||
if (0 == $matches) {
|
if (0 == $matches) {
|
||||||
$self->log(LOGDEBUG, "fail, no records for $domain");
|
$self->log(LOGDEBUG, "no $type records for $domain");
|
||||||
}
|
}
|
||||||
return $matches;
|
return $matches;
|
||||||
}
|
};
|
||||||
|
|
||||||
sub fetch_dmarc_record {
|
sub fetch_dmarc_record {
|
||||||
my ($self, $zone) = @_;
|
my ($self, $zone) = @_;
|
||||||
|
@ -246,7 +246,7 @@ sub helo_handler {
|
|||||||
my ($self, $transaction, $host) = @_;
|
my ($self, $transaction, $host) = @_;
|
||||||
|
|
||||||
if (!$host) {
|
if (!$host) {
|
||||||
$self->log(LOGINFO, "fail, no helo host");
|
$self->log(LOGINFO, "fail, tolerated, no helo host");
|
||||||
return DECLINED;
|
return DECLINED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,22 +20,22 @@ Karma provides other plugins with a karma value they can use to be more
|
|||||||
lenient, strict, or skip processing entirely.
|
lenient, strict, or skip processing entirely.
|
||||||
|
|
||||||
Karma is small, fast, and ruthlessly efficient. Karma can be used to craft
|
Karma is small, fast, and ruthlessly efficient. Karma can be used to craft
|
||||||
custom connection policies such as these two examples:
|
custom connection policies such as these two examples:
|
||||||
|
|
||||||
=over 4
|
=over 4
|
||||||
|
|
||||||
Hi there, well known and well behaved sender. Please help yourself to greater concurrency (hosts_allow), multiple recipients (karma), and no delays (early_sender).
|
Hi there, well known and well behaved sender. Please help yourself to greater concurrency (hosts_allow), multiple recipients (karma), and no delays (early_sender).
|
||||||
|
|
||||||
Hi there, naughty sender. You get a max concurrency of 1, max recipients of 2, and SMTP delays.
|
Hi there, naughty sender. You get a max concurrency of 1, max recipients of 2, and SMTP delays.
|
||||||
|
|
||||||
=back
|
=back
|
||||||
|
|
||||||
=head1 CONFIG
|
=head1 CONFIG
|
||||||
|
|
||||||
=head2 negative <integer>
|
=head2 negative <integer>
|
||||||
|
|
||||||
How negative a senders karma can get before we penalize them for sending a
|
How negative a senders karma can get before we penalize them for sending a
|
||||||
naughty message. Karma is the number of nice - naughty connections.
|
naughty message. Karma is the number of nice - naughty connections.
|
||||||
|
|
||||||
Default: 1
|
Default: 1
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ I<0> will not reject any connections.
|
|||||||
|
|
||||||
I<1> will reject naughty senders.
|
I<1> will reject naughty senders.
|
||||||
|
|
||||||
I<connect> is the most efficient setting.
|
I<connect> is the most efficient setting.
|
||||||
|
|
||||||
To reject at any other connection hook, use the I<naughty> setting and the
|
To reject at any other connection hook, use the I<naughty> setting and the
|
||||||
B<naughty> plugin.
|
B<naughty> plugin.
|
||||||
@ -104,7 +104,7 @@ sending a virus, early talking, or sending messages with a very high spam
|
|||||||
score.
|
score.
|
||||||
|
|
||||||
This plugin does not penalize connections with transaction notes I<relayclient>
|
This plugin does not penalize connections with transaction notes I<relayclient>
|
||||||
or I<whitelisthost> set. These notes would have been set by the B<relay>,
|
or I<whitelisthost> set. These notes would have been set by the B<relay>,
|
||||||
B<whitelist>, and B<dns_whitelist_soft> plugins. Obviously, those plugins must
|
B<whitelist>, and B<dns_whitelist_soft> plugins. Obviously, those plugins must
|
||||||
run before B<karma> for that to work.
|
run before B<karma> for that to work.
|
||||||
|
|
||||||
@ -244,9 +244,10 @@ sub register {
|
|||||||
|
|
||||||
#$self->prune_db(); # keep the DB compact
|
#$self->prune_db(); # keep the DB compact
|
||||||
$self->register_hook('connect', 'connect_handler');
|
$self->register_hook('connect', 'connect_handler');
|
||||||
|
$self->register_hook('rcpt_pre', 'rcpt_handler');
|
||||||
$self->register_hook('data', 'data_handler');
|
$self->register_hook('data', 'data_handler');
|
||||||
|
$self->register_hook('data_post', 'data_handler');
|
||||||
$self->register_hook('disconnect', 'disconnect_handler');
|
$self->register_hook('disconnect', 'disconnect_handler');
|
||||||
$self->register_hook('received_line', 'rcpt_handler');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sub hook_pre_connection {
|
sub hook_pre_connection {
|
||||||
@ -256,8 +257,6 @@ sub hook_pre_connection {
|
|||||||
|
|
||||||
my $remote_ip = $args{remote_ip};
|
my $remote_ip = $args{remote_ip};
|
||||||
|
|
||||||
#my $max_conn = $args{max_conn_ip};
|
|
||||||
|
|
||||||
my $db = $self->get_db_location();
|
my $db = $self->get_db_location();
|
||||||
my $lock = $self->get_db_lock($db) or return DECLINED;
|
my $lock = $self->get_db_lock($db) or return DECLINED;
|
||||||
my $tied = $self->get_db_tie($db, $lock) or return DECLINED;
|
my $tied = $self->get_db_tie($db, $lock) or return DECLINED;
|
||||||
@ -323,23 +322,43 @@ sub connect_handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sub rcpt_handler {
|
sub rcpt_handler {
|
||||||
my ($self, $transaction, $recipient, %args) = @_;
|
my ($self, $transaction, $addr) = @_;
|
||||||
|
|
||||||
my $recipients = scalar $self->transaction->recipients;
|
return DECLINED if $self->is_immune();
|
||||||
return DECLINED if $recipients < 2; # only one recipient
|
|
||||||
|
|
||||||
my $karma = $self->connection->notes('karma_history');
|
my $recipients = scalar $self->transaction->recipients or do {
|
||||||
return DECLINED if $karma > 0; # good karma, no limit
|
$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
|
# 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 {
|
sub data_handler {
|
||||||
my ($self, $transaction) = @_;
|
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;
|
return DECLINED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,8 +201,10 @@ sub rcpt_handler {
|
|||||||
$self->log(LOGINFO, "pass, bouncesaying with program"), $k++ if $rv == 0x13;
|
$self->log(LOGINFO, "pass, bouncesaying with program"), $k++ if $rv == 0x13;
|
||||||
if ($rv == 0x14) {
|
if ($rv == 0x14) {
|
||||||
my $s = $transaction->sender->address;
|
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");
|
$self->log(LOGINFO, "pass, ezmlm list");
|
||||||
$k++;
|
$k++;
|
||||||
}
|
}
|
||||||
|
@ -6,18 +6,18 @@ rcpt_ok
|
|||||||
|
|
||||||
=head1 SYNOPSIS
|
=head1 SYNOPSIS
|
||||||
|
|
||||||
this plugin checks the standard rcpthosts config
|
Validate that we accept mail for a recipient using a qmail rcpthosts file
|
||||||
|
|
||||||
=head1 DESCRIPTION
|
=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
|
This is functionally identical to qmail's rcpthosts implementation, consulting
|
||||||
both rcpthosts and morercpthosts.cdb.
|
both rcpthosts and morercpthosts.cdb.
|
||||||
|
|
||||||
=head1 CONFIGURATION
|
=head1 CONFIGURATION
|
||||||
|
|
||||||
It should be configured to be run _LAST_!
|
It should be configured as the _LAST_ recipient plugin!
|
||||||
|
|
||||||
=cut
|
=cut
|
||||||
|
|
||||||
@ -30,6 +30,8 @@ use Qpsmtpd::DSN;
|
|||||||
sub hook_rcpt {
|
sub hook_rcpt {
|
||||||
my ($self, $transaction, $recipient, %param) = @_;
|
my ($self, $transaction, $recipient, %param) = @_;
|
||||||
|
|
||||||
|
return (OK) if $self->is_immune(); # relay_client or whitelist
|
||||||
|
|
||||||
# Allow 'no @' addresses for 'postmaster' and 'abuse'
|
# Allow 'no @' addresses for 'postmaster' and 'abuse'
|
||||||
# qmail-smtpd will do this for all users without a domain, but we'll
|
# 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.
|
# 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_rcpthosts($host);
|
||||||
return (OK) if $self->is_in_morercpthosts($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,
|
# default of relaying_denied is obviously DENY,
|
||||||
# we use the default "Relaying denied" message...
|
# we use the default "Relaying denied" message...
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
# This file contains a list of every plugin used on this server. If you have
|
# This file contains a list of every plugin used on this server. If you have
|
||||||
# additional plugins running, add them here.
|
# additional plugins running, add them here.
|
||||||
# Fields are whitespace delimited. Columns are ordered by numeric plugin ID.
|
# 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
|
#id name abb3 abb5 aliases
|
||||||
#
|
#
|
||||||
201 hosts_allow alw allow
|
201 hosts_allow alw allow
|
||||||
202 ident::geoip geo geoip
|
202 ident::geoip geo geoip
|
||||||
203 ident::p0f p0f p0f
|
203 ident::p0f p0f p0f ident::p0f_3a0,ident::p0f_3a1
|
||||||
204 ident::p0f_3a0 p0f p0f
|
|
||||||
205 karma krm karma
|
205 karma krm karma
|
||||||
206 dnsbl dbl dnsbl
|
206 dnsbl dbl dnsbl
|
||||||
207 relay rly relay check_relay,check_norelay,relay_only
|
207 relay rly relay check_relay,check_norelay,relay_only
|
||||||
@ -26,13 +29,13 @@
|
|||||||
#
|
#
|
||||||
# Authentication
|
# Authentication
|
||||||
#
|
#
|
||||||
400 auth::auth_vpopmail_sql aut vpsql
|
400 auth::auth_vpopmail_sql avq avsql
|
||||||
401 auth::auth_vpopmaild vpd vpopd
|
401 auth::auth_vpopmaild avd vpopd
|
||||||
402 auth::auth_vpopmail vpo vpop
|
402 auth::auth_vpopmail avp vpop
|
||||||
403 auth::auth_checkpasswd ckp chkpw
|
403 auth::auth_checkpassword ack chkpw
|
||||||
404 auth::auth_cvs_unix_local cvs cvsul
|
404 auth::auth_cvs_unix_local acv cvsul
|
||||||
405 auth::auth_flat_file flt aflat
|
405 auth::auth_flat_file aff aflat
|
||||||
406 auth::auth_ldap_bind ldp aldap
|
406 auth::auth_ldap_bind ald aldap
|
||||||
407 auth::authdeny dny adeny
|
407 auth::authdeny dny adeny
|
||||||
#
|
#
|
||||||
# Sender / Envelope From
|
# Sender / Envelope From
|
||||||
@ -80,11 +83,11 @@
|
|||||||
#
|
#
|
||||||
# Queue Plugins
|
# Queue Plugins
|
||||||
#
|
#
|
||||||
800 queue::qmail-queue qqm queue
|
800 queue::qmail-queue qqm queue queue::qmail_2dqueue
|
||||||
801 queue::maildir qdr qudir
|
801 queue::maildir qdr qudir
|
||||||
802 queue::postfix-queue qpf qupfx
|
802 queue::postfix-queue qpf qupfx queue::postfix_2dqueue
|
||||||
803 queue::smtp-forward qfw qufwd
|
803 queue::smtp-forward qfw qufwd queue::smtp_2dqueue
|
||||||
804 queue::exim-bsmtp qxm qexim
|
804 queue::exim-bsmtp qxm qexim queue::exim_2dbsmtp
|
||||||
|
|
||||||
900 quit_fortune for fortu
|
900 quit_fortune for fortu
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ sub hook_mail {
|
|||||||
return Qpsmtpd::DSN->temp_resolver_failed($self->get_reject_type(),
|
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;
|
return DECLINED;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -127,7 +127,7 @@ sub hook_mail {
|
|||||||
|
|
||||||
if (!$self->{_args}{reject}) {
|
if (!$self->{_args}{reject}) {
|
||||||
;
|
;
|
||||||
$self->log(LOGINFO, "fail, reject disabled, $result");
|
$self->log(LOGINFO, "fail, tolerated, $result");
|
||||||
return DECLINED;
|
return DECLINED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,28 +96,17 @@ sub mail_handler {
|
|||||||
return (DECLINED, "SPF - null sender");
|
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 $from = $sender->user . '@' . lc($sender->host);
|
||||||
my $helo = $self->qp->connection->hello_host;
|
my $helo = $self->qp->connection->hello_host;
|
||||||
my $scope = $from ? 'mfrom' : 'helo';
|
my $scope = $from ? 'mfrom' : 'helo';
|
||||||
my %req_params = (
|
my %req_params = (
|
||||||
versions => [1, 2], # optional
|
versions => [1, 2], # optional
|
||||||
scope => $scope,
|
scope => $scope,
|
||||||
ip_address => $client_ip,
|
ip_address => $self->qp->connection->remote_ip,
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($scope =~ /^mfrom|pra$/) {
|
if ($scope =~ /^mfrom|pra$/) {
|
||||||
$req_params{identity} = $from;
|
$req_params{identity} = $from;
|
||||||
$req_params{helo_identity} = $helo if $helo;
|
$req_params{helo_identity} = $helo if $helo;
|
||||||
}
|
}
|
||||||
elsif ($scope eq 'helo') {
|
elsif ($scope eq 'helo') {
|
||||||
@ -144,44 +133,43 @@ sub mail_handler {
|
|||||||
return (DECLINED, "SPF - no response");
|
return (DECLINED, "SPF - no response");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$reject) {
|
if ($code eq 'pass') {
|
||||||
$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') {
|
|
||||||
$self->adjust_karma(1);
|
$self->adjust_karma(1);
|
||||||
$transaction->notes('spf_pass_host', lc $sender->host);
|
$transaction->notes('spf_pass_host', lc $sender->host);
|
||||||
$self->log(LOGINFO, "pass, $code: $why");
|
$self->log(LOGINFO, "pass, $code: $why");
|
||||||
return (DECLINED);
|
return (DECLINED);
|
||||||
}
|
}
|
||||||
elsif ($code eq 'neutral') {
|
|
||||||
$self->log(LOGINFO, "fail, $code, $why");
|
if (!$reject) {
|
||||||
return (DENY, "SPF - $code: $why") if $reject >= 5;
|
$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 (DENY, "SPF - $code: $why") if $reject >= 6;
|
||||||
return (DENYSOFT, "SPF - $code: $why") if $reject > 3;
|
return (DENYSOFT, "SPF - $code: $why") if $reject > 3;
|
||||||
|
$self->log(LOGINFO, "fail, tolerated, $code, $why");
|
||||||
|
return (DECLINED);
|
||||||
}
|
}
|
||||||
elsif ($code eq 'permerror') {
|
if ($code eq 'temperror') {
|
||||||
$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') {
|
|
||||||
$self->log(LOGINFO, "fail, $code, $why");
|
$self->log(LOGINFO, "fail, $code, $why");
|
||||||
return (DENYSOFT, "SPF - $code: $why") if $reject >= 2;
|
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");
|
$self->log(LOGINFO, "SPF from $from was $code: $why");
|
||||||
@ -196,33 +184,37 @@ sub handle_code_none {
|
|||||||
return (DENY, "SPF - none: $why");
|
return (DENY, "SPF - none: $why");
|
||||||
}
|
}
|
||||||
|
|
||||||
$self->log(LOGINFO, "pass, none, $why");
|
$self->log(LOGINFO, "skip, tolerated, none, $why");
|
||||||
return DECLINED;
|
return DECLINED;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub handle_code_fail {
|
sub handle_code_fail {
|
||||||
my ($self, $reject, $why) = @_;
|
my ($self, $reject, $why) = @_;
|
||||||
|
|
||||||
|
$self->adjust_karma(-1);
|
||||||
|
|
||||||
if ($reject >= 2) {
|
if ($reject >= 2) {
|
||||||
$self->log(LOGINFO, "fail, $why");
|
$self->log(LOGINFO, "fail, $why");
|
||||||
return (DENY, "SPF - forgery: $why") if $reject >= 3;
|
return (DENY, "SPF - forgery: $why") if $reject >= 3;
|
||||||
return (DENYSOFT, "SPF - fail: $why");
|
return (DENYSOFT, "SPF - fail: $why");
|
||||||
}
|
}
|
||||||
|
|
||||||
$self->log(LOGINFO, "pass, fail tolerated, $why");
|
$self->log(LOGINFO, "fail, tolerated, $why");
|
||||||
return DECLINED;
|
return DECLINED;
|
||||||
}
|
}
|
||||||
|
|
||||||
sub handle_code_softfail {
|
sub handle_code_softfail {
|
||||||
my ($self, $reject, $why) = @_;
|
my ($self, $reject, $why) = @_;
|
||||||
|
|
||||||
|
$self->adjust_karma(-1);
|
||||||
|
|
||||||
if ($reject >= 3) {
|
if ($reject >= 3) {
|
||||||
$self->log(LOGINFO, "fail, soft, $why");
|
$self->log(LOGINFO, "fail, soft, $why");
|
||||||
return (DENY, "SPF - fail: $why") if $reject >= 4;
|
return (DENY, "SPF - fail: $why") if $reject >= 4;
|
||||||
return (DENYSOFT, "SPF - fail: $why") if $reject >= 3;
|
return (DENYSOFT, "SPF - fail: $why") if $reject >= 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
$self->log(LOGINFO, "pass, softfail tolerated, $why");
|
$self->log(LOGINFO, "fail, tolerated, soft, $why");
|
||||||
return DECLINED;
|
return DECLINED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,7 +178,7 @@ sub data_post_handler {
|
|||||||
|
|
||||||
if ($transaction->data_size > 500_000) {
|
if ($transaction->data_size > 500_000) {
|
||||||
$self->log(LOGINFO,
|
$self->log(LOGINFO,
|
||||||
"skip: too large (" . $transaction->data_size . ")");
|
"skip, too large (" . $transaction->data_size . ")");
|
||||||
return (DECLINED);
|
return (DECLINED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -424,7 +424,7 @@ sub reject {
|
|||||||
|
|
||||||
if ($score < $reject) {
|
if ($score < $reject) {
|
||||||
if ($ham_or_spam eq 'Spam') {
|
if ($ham_or_spam eq 'Spam') {
|
||||||
$self->log(LOGINFO, "fail, $status < $reject, $learn");
|
$self->log(LOGINFO, "fail, tolerated, $status < $reject, $learn");
|
||||||
return DECLINED;
|
return DECLINED;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -149,12 +149,16 @@ sub hook_connect {
|
|||||||
my ($self, $transaction) = @_;
|
my ($self, $transaction) = @_;
|
||||||
|
|
||||||
my $local_port = $self->qp->connection->local_port;
|
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)) {
|
unless (_convert_to_ssl($self)) {
|
||||||
|
$self->log(LOGINFO, "fail, unable to establish SSL");
|
||||||
return (DENY_DISCONNECT, "Cannot establish SSL session");
|
return (DENY_DISCONNECT, "Cannot establish SSL session");
|
||||||
}
|
}
|
||||||
$self->log(LOGWARN, "Connected via SMTPS");
|
$self->log(LOGINFO, "pass, connect via SMTPS");
|
||||||
return DECLINED;
|
return DECLINED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
4
run
4
run
@ -2,8 +2,8 @@
|
|||||||
#
|
#
|
||||||
# You might want/need to to edit these settings
|
# You might want/need to to edit these settings
|
||||||
QPUSER=smtpd
|
QPUSER=smtpd
|
||||||
# limit qpsmtpd to 150MB memory, should be several times what is needed.
|
# limit qpsmtpd to 300MB memory
|
||||||
MAXRAM=150000000
|
MAXRAM=300000000
|
||||||
BIN=/usr/local/bin
|
BIN=/usr/local/bin
|
||||||
PERL=/usr/bin/perl
|
PERL=/usr/bin/perl
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ my $test_email = 'matt@tnpi.net';
|
|||||||
sub register_tests {
|
sub register_tests {
|
||||||
my $self = shift;
|
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_fetch_dmarc_record", 3);
|
||||||
$self->register_test("test_discover_policy", 1);
|
$self->register_test("test_discover_policy", 1);
|
||||||
}
|
}
|
||||||
@ -55,7 +55,8 @@ sub test_get_organizational_domain {
|
|||||||
my $transaction = $self->qp->transaction;
|
my $transaction = $self->qp->transaction;
|
||||||
|
|
||||||
cmp_ok( $self->get_organizational_domain('test.www.tnpi.net'), 'eq', 'tnpi.net' );
|
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 {
|
sub test_discover_policy {
|
||||||
|
Loading…
Reference in New Issue
Block a user