Files
zira-etc/mail/spamassassin/wrongmx.pm
2021-05-24 22:18:33 +03:00

182 lines
6.2 KiB
Perl

package WrongMX;
use strict;
use Mail::SpamAssassin;
use Mail::SpamAssassin::Plugin;
use Net::DNS;
our @ISA = qw(Mail::SpamAssassin::Plugin);
sub new {
my ($class, $mailsa) = @_;
$class = ref($class) || $class;
my $self = $class->SUPER::new($mailsa);
bless ($self, $class);
$self->register_eval_rule("wrongmx");
return $self;
}
sub wrongmx {
my ($self, $permsgstatus) = @_;
my $MAXTIMEDIFF = 30;
return 0 if $self->{main}->{local_tests_only}; # in case plugins ever get called
# if a user set dns_available to no we shouldn't be doing MX lookups
return 0 unless $permsgstatus->is_dns_available();
# avoid FPs (and wasted processing) by not checking when all_trusted
return 0 if $permsgstatus->check_all_trusted;
# if there is only one received header we can bail
my $times_ref = ($permsgstatus->{received_header_times});
return 0 if (!defined($times_ref) || scalar(@$times_ref) < 2); # if it only hit one server we're done
# next we need the recipient domain's MX records... who's the recipient
my $recipient_domain;
if ($self->{main}->{username} =~ /\@(\S+\.\S+)/) {
$recipient_domain = $1;
} else {
foreach my $to ($permsgstatus->all_to_addrs) {
next unless defined $to;
$to =~ tr/././s; # bug 3366?
if ($to =~ /\@(\S+\.\S+)/) {
$recipient_domain = $1;
last;
}
}
}
return 0 unless defined $recipient_domain; # no domain means no MX records
# Now we need to get the recipient domain's MX records.
# We'll resolve the hosts so we can look for IP overlaps.
my $res = Net::DNS::Resolver->new;
my @rmx = mx($res, $recipient_domain);
my %mx_prefs;
if (@rmx) {
foreach my $rr (@rmx) {
unless (exists $mx_prefs{$rr->exchange} && $mx_prefs{$rr->exchange} < $rr->preference) {
$mx_prefs{$rr->exchange} = $rr->preference;
}
my @ips = $permsgstatus->lookup_a($rr->exchange);
next unless @ips;
foreach my $ip (@ips) {
unless (exists $mx_prefs{$ip} && $mx_prefs{$ip} < $rr->preference) {
$mx_prefs{$ip} = $rr->preference;
}
}
}
} else {
return 0; # no recipient domain MX records found, no way to check MX flow
}
# get relay hosts
my @relays;
foreach my $rcvd (@{$permsgstatus->{relays_trusted}}, @{$permsgstatus->{relays_untrusted}}) {
push @relays, $rcvd->{by};
}
return 0 if (!scalar(@relays)); # this probably won't happen, but whatever
# Bail if we don't have the same number of relays and times, or if we have
# fewer preferences than times (or relays).
return 0 if (scalar(@relays) != scalar(@$times_ref) || scalar(@$times_ref) > scalar(keys(%mx_prefs)));
# Check to see if a higher preference relay passes mail to a lower
# preference relay within $MAXTIMEDIFF seconds. If we do decide that a message
# has done this, wait till AFTER we lookup the sender domain's MX records
# to return 1 since there may be MX overlaps that we'll bail on... see below.
# We could do the sender domain MX lookups first, but we might as well save
# the overhead if we're going to end up bailing anyway ($hits == 0).
# We'll go through backwards so that we can detect weird local configs
# that pass mail from the primary MX to the secondary MX for spam/virus
# scanning, or even final delivery. See BACKWARDS comment below.
# We'll resolve the 'by' hosts found to see if they match any of our
# resolved MX hosts' IPs.
my $hits = 0;
my $last_pref;
my $last_time;
foreach (my $i = $#relays; $i >= 0; $i--) {
my $MX = 0;
if (exists($mx_prefs{$relays[$i]})) {
$MX = $relays[$i];
} else {
my @ips = $permsgstatus->lookup_a($relays[$i]);
next unless @ips;
foreach my $ip (@ips) {
if ( exists $mx_prefs{$ip} ) {
$MX = $ip;
last;
}
}
}
if ($MX) {
if (defined ($last_pref) && defined ($last_time)) {
# BACKWARDS -- uncomment the next line if you need to pass mail from a
# higher pref MX to a lower MX (for virus scanning/etc) AND back,
# before SA sees it... this opens you up to FNs with forged headers
# last if ($mx_prefs{$MX} > $last_pref);
$hits++ if ($mx_prefs{$MX} < $last_pref
&& ($last_time - $MAXTIMEDIFF <= @$times_ref[$i] && @$times_ref[$i] <= $last_time + $MAXTIMEDIFF) ); # within max time diff
}
$last_pref = $mx_prefs{$MX};
$last_time = @$times_ref[$i];
}
last if $hits;
}
# Determine the sender's domain.
# Don't bail if we can't determine the sender since it's probably spam.
my $sender_domain;
foreach my $from ($permsgstatus->get('EnvelopeFrom:addr')) {
next unless defined $from;
$from =~ tr/././s; # bug 3366?
if ($from =~ /\@(\S+\.\S+)/) {
$sender_domain = $1;
last;
}
}
if (defined $sender_domain) {
# Until SPF is incorporated (to better define possibly shared MX servers) we
# might as well bail here, and save the MX lookup, if the sender domain is
# the same as the recipient domain, since the MX records will, obviously,
# overlap. See below.
return 0 if (lc($sender_domain) eq lc($recipient_domain));
# Bail if the recepient and sender domains share the same MX servers.
# If the sender's primary MX is the recipient's secondary MX we don't want
# to penalize them. (Comparing SPF records might also be a good idea.)
# This will FN if spam comes with a From: as your domain. SPF should
# really be implemented here!
# Ignoring the sender's MX records if an SPF lookup results in failure
# would help to avoid FNs and should do the job since anyone with an SPF
# record that shares your MX servers probably won't be spamming you.
# Again, MX hosts are resolved to look for IP overlaps.
if ($sender_domain) {
my @smx = mx($res, $sender_domain);
if (@smx) {
foreach my $srr (@smx) {
foreach my $rrr (@rmx) {
return 0 if ($rrr->exchange eq $srr->exchange);
}
my @sips = $permsgstatus->lookup_a($srr->exchange);
foreach my $sip (@sips) {
foreach my $rip (keys %mx_prefs) {
return 0 if ($rip eq $sip);
}
}
}
}
}
}
return 1 if $hits;
return 0;
}
1;