package Mail::SpamAssassin::Plugin::BadRelay;
my $VERSION = 0.32;

###
### http://sa.hege.li/BadRelay.pm (and badrelay.cf) / sa@hege.li
###
### This plugin performs similar checks as Botnet.pm/RDNS_DYNAMIC rules,
### first external relay's hostname is checked for signs of "dynamicness".
### Authenticated relays are always whitelisted. SA 3.2+ required.
###
### Differences to Botnet:
###
### - No DNS queries from plugin, MTA already did that in Received: header
### - RegistrarBoundaries used for precise matching
###
### Licensed under WTFPL
###

use Mail::SpamAssassin::Plugin;
use Mail::SpamAssassin::Util::RegistrarBoundaries;
use strict;
use vars qw(@ISA);
@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("check_badrelay");

    $self->{disabled} = 0;
    $self->{client_re} = "";
    $self->{client_fre} = "";
    $self->{client_dom} = "";
    $self->{exclude_re} = "";
    $self->{exclude_fre} = "";
    $self->{exclude_dom} = "";

    return $self;
}

sub parse_config
{
    my ($self, $opts) = @_;

    if ($opts->{key} =~ /^badrelay_(client|exclude)$/)
    {
        my $type = $1;

        foreach my $temp (split(/\s+/, $opts->{value}))
        {
            if ($temp =~ /(?:^\^|\$$)/)
            {
                $temp =~ s# \(\?: # \(    #gx;
                $temp =~ s# \(    # \(\?: #gx;
                $self->{"${type}_fre"} .= $temp."|";
            }
            elsif ($temp =~ /^\./)
            {
                $temp =~ s/\./\\./g;
                $self->{"${type}_dom"} .= $temp."|";
            }
            else
            {
                $temp =~ s# \(\?: # \(    #gx;
                $temp =~ s# \(    # \(\?: #gx;
                $self->{"${type}_re"} .= $temp."|";
            }
        }

        $self->inhibit_further_callbacks();

        return 1;
    }

    return 0;
}

sub finish_parsing_end
{
    my ($self, $opts) = @_;

    if ($self->{client_re} ne "")
    {
        chop $self->{client_re};
        $self->{client_re} = "(?:\\d|\\b|_)(?:$self->{client_re})(?:\\d|\\b|_)";
    }
    else
    {
        Mail::SpamAssassin::Plugin::dbg("BadRelay: ERROR: no badrelay_client defined, Plugin disabled");
        $self->{disabled} = 1;
        return 0;
    }

    if ($self->{client_fre} ne "")
    {
        chop $self->{client_fre};
        $self->{client_fre} = "(?:$self->{client_fre})";
    }
    else { $self->{client_fre} = "^\$"; }

    if ($self->{client_dom} ne "")
    {
        chop $self->{client_dom};
        $self->{client_dom} = qr/(?:$self->{client_dom})$/;
    }
    else { $self->{client_dom} = qr/^$/; }

    if ($self->{exclude_re} ne "")
    {
        chop $self->{exclude_re};
        $self->{exclude_re} = "(?:\\d|\\b|_)(?:$self->{exclude_re})(?:\\d|\\b|_)";
    }
    else { $self->{exclude_re} = "^\$"; }

    if ($self->{exclude_fre} ne "")
    {
        chop $self->{exclude_fre};
        $self->{exclude_fre} = "(?:$self->{exclude_fre})";
    }
    else { $self->{exclude_fre} = "^\$"; }

    if ($self->{exclude_dom} ne "")
    {
        chop $self->{exclude_dom};
        $self->{exclude_dom} = qr/(?:$self->{exclude_dom})$/;
    }
    else { $self->{exclude_dom} = qr/^$/; }

    $self->{client_re} = qr/(?:  $self->{client_re}  |  $self->{client_fre}  )/ix;
    $self->{exclude_re} = qr/(?:  $self->{exclude_re}  |  $self->{exclude_fre}  )/ix;

    delete $self->{client_fre};
    delete $self->{exclude_fre};

    Mail::SpamAssassin::Plugin::dbg("BadRelay: client_re = ".$self->{client_re});
    Mail::SpamAssassin::Plugin::dbg("BadRelay: exclude_re = ".$self->{exclude_re});

    return 0;
}

sub check_badrelay
{
    my ($self, $pms) = @_;

    return 0 if $self->{disabled};

    my $msg = $pms->get_message();
    my @relays = @{$msg->{metadata}->{relays_external}};
    
    if (scalar(@relays) < 1)
    {
        Mail::SpamAssassin::Plugin::dbg("BadRelay: no external relays found, skipping check");
        return 0;
    }

    if ($relays[0]->{auth})
    {
        Mail::SpamAssassin::Plugin::dbg("BadRelay: relay has authenticated, skipping check");
        return 0;
    }

    my ($hostpart, $domain) = Mail::SpamAssassin::Util::RegistrarBoundaries::split_domain($relays[0]->{rdns});

    if ($hostpart eq "" or $domain eq "")
    {
        Mail::SpamAssassin::Plugin::dbg("BadRelay: relay has no host part, skipping");
        return 0;
    }

    Mail::SpamAssassin::Plugin::dbg("BadRelay: first external relay: $hostpart/$domain");

    if ($hostpart =~ $self->{exclude_re} or $relays[0]->{rdns} =~ $self->{exclude_dom})
    {
        Mail::SpamAssassin::Plugin::dbg("BadRelay: relay matches exclude list, skipping");
        return 0;
    }

    if ($hostpart =~ $self->{client_re} or $relays[0]->{rdns} =~ $self->{client_dom})
    {
        Mail::SpamAssassin::Plugin::dbg("BadRelay: relay matches client list - HIT");
        return 1;
    }

    Mail::SpamAssassin::Plugin::dbg("BadRelay: relay looks ok, didn't match anything");

    return 0;
}

1;
