Attached is a plugin that checks to see if a message was sent to a lower preference MX when a higher preference (lower number) MX was probably up at the time. The code is currently set ($MAXTIMEDIFF) to +/- 30 seconds. That is, if a message is passed to a higher preference MX within 30 seconds it is assumed that the higher preference MX was up and the mail should have been sent there instead of the lower preference MX. Of course this relies on the MX servers having their time set correctly.

If someone could run this plugin in a masscheck, and then suggest a score for the eval rule, it would be greatly appreciated. Unfortunately I don't have a large enough corpus to test it myself.

The attached .cf file has tflags net set, so you may need to comment that out if you normally only do local tests.

The plugin requires Net::DNS, of course.

Thanks!

Daryl
loadplugin WrongMX wrongmx.pm
header          WrongMX eval:wrongmx()
describe        WrongMX Sent to lower pref MX when higher pref MX was up.
tflags          WrongMX net
score           WrongMX 0.1
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;

  # avoid FPs by not checking when all_trusted
  return 0 if $permsgstatus->check_all_trusted;

  # if there is only one recieved header we can bail
  my $times = $permsgstatus->{received_header_times};
  return 0 if (scalar(@$times) < 2); # if it only hit one server were done

  # next we need the recipient domain's MX records... who's the recipient
  my $recipient;
  $recipient = $self->{main}->{username} if ($self->{main}->{username} =~ 
/[EMAIL PROTECTED],3}/);
  unless ($recipient) {
    foreach my $address ($permsgstatus->all_to_addrs) {
      if ($address =~ /[EMAIL PROTECTED],3}/) {
        $recipient = $address;
        last;
      }
    }
  }
  my $recipient_domain = $recipient;
  $recipient_domain =~ s/.*@//;
  return 0 unless ($recipient_domain);  # no domain means no MX records

  # now we need to get the recipient domain's MX records
  my $res = Net::DNS::Resolver->new;
  my @rmx = mx($res, $recipient_domain);

  # build some hashes to use when we check to see how fast mail was passed
  # between MXes
  my %mx_prefs;
  my %mx_times;
  if (@rmx) {
    foreach my $rr (@rmx) {
      $mx_prefs{$rr->exchange} = $rr->preference;
      $mx_times{$rr->exchange} = shift @$times;
    }
  } else {
     return 0; # no recipient domain MX records found, no way to check
  }

  # If we could trust users to set their (NATed) trusted relays properly
  # (which apparently we can't judging by the users' list) we could just
  # push the following two arrays together (we'd use untrusted also incase
  # someone doesn't have control over their secondary MX and doesn't want to
  # trust it).  Instead we'll reparse the headers ourself for the by host,
  # which lucky for us is each, and always the same.

  #my @relays = @{$permsgstatus->{relays_trusted}};     # better way
  #push @relays, @{$permsgstatus->{relays_untrusted}};  # better way

  my @received;
  my $received = $permsgstatus->get('Received');
  if (defined($received) && length($received)) {
    @received = grep {$_ =~ m/\S/} (split(/\n/,$received));
  }
  return 0 if (!scalar(@received)); # this shouldn't happen, but whatever

  my @relays;
  foreach my $header (@received) {
    push @relays, $1 if ($header =~ / by (\S+) /);
  }
  return 0 if (!scalar(@relays)); # this probably won't happen, but whatever

  # Check to see if a higher preference relay passes mail to a lower
  # preference relay within $MAXDELAY seconds.  If we do decide that a message
  # has done this, wait till AFTER we lookup the sender domain's MX records to
  # see if there are any overlaps since we'll bail if there are MX overlaps.
  # 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.
  my $hits = 0;
  my $last_pref = -1;
  my $last_time = 0;
  foreach my $relay (@relays) {
    if (defined($mx_prefs{$relay}) && defined($mx_times{$relay})) {
      $hits++ if ($mx_prefs{$relay} > $last_pref && ($mx_times{$relay} + 
$MAXTIMEDIFF > $last_time && $last_time > $mx_times{$relay} - $MAXTIMEDIFF) );
      $last_pref = $mx_prefs{$relay};
      $last_time = $mx_times{$relay};
    }
  }

  # Determine the sender's domain.
  # Don't bail if we can't determine the sender since it's probably spam.
  my $envelope_from = $permsgstatus->get("EnvelopeFrom");
  my $sending_domain = $envelope_from;
  $sending_domain =~ s/.*@//;

  if ($sending_domain) {
    my @smx = mx($res, $sending_domain);
    if (@smx) {
      
      # Bail if the receiving and sending domains share the same MX servers.
      # If the sender's primary MX is the receiver's secondary MX we don't want
      # to penalize them.  (Comparing SPF records might also be a good idea.)
      foreach my $rrr (@rmx) {
        foreach my $srr (@smx) {
          return 0 if ($rrr->exchange eq $srr->exchange);
        }
      }
    }
  }
  return 1 if $hits;
  return 0;
}

1;

Reply via email to