#!perl -w

use strict;
use warnings;

use Qpsmtpd::Constants;
use Qpsmtpd::DSN;
use Qpsmtpd::TcpServer;

#use ParaDNS;  # moved into register
use Socket;

my %invalid  = ();
my $has_ipv6 = Qpsmtpd::TcpServer::has_ipv6();

sub register {
    my ( $self, $qp ) = @_;
    
    foreach my $i ( $self->qp->config("invalid_resolvable_fromhost") ) {
        $i =~ s/^\s*//;
        $i =~ s/\s*$//;
        if ( $i =~ m#^((\d{1,3}\.){3}\d{1,3})/(\d\d?)# ) {
            $invalid{$1} = $3;
        }
    }

    eval 'use ParaDNS';
    if ( $@ ) {
        warn "could not load ParaDNS, plugin disabled";
        return DECLINED;
    };
    $self->register_hook( mail => 'hook_mail_start' );
    $self->register_hook( mail => 'hook_mail_done' );
}

sub hook_mail_start {
    my ( $self, $transaction, $sender ) = @_;
    
    return DECLINED
      if ($self->connection->notes('whitelisthost'));

    if ( $sender ne '<>' ) {

        unless ( $sender->host ) {
            # default of addr_bad_from_system is DENY, we use DENYSOFT here to
            # get the same behaviour as without Qpsmtpd::DSN...
            return Qpsmtpd::DSN->addr_bad_from_system( DENYSOFT,
                "FQDN required in the envelope sender" );
        }

        return DECLINED if $sender->host =~ m/^\[(\d{1,3}\.){3}\d{1,3}\]$/;

        unless ($self->check_dns( $sender->host )) {
            return Qpsmtpd::DSN->temp_resolver_failed(
                "Could not resolve " . $sender->host );
        }

        return YIELD;
    }

    return DECLINED;
}

sub hook_mail_done {
    my ( $self, $transaction, $sender ) = @_;
    
    return DECLINED
      if ( $self->connection->notes('whitelisthost') );

    if ( $sender ne "<>" && !$transaction->notes('resolvable_fromhost') ) {
        # default of temp_resolver_failed is DENYSOFT
        return Qpsmtpd::DSN->temp_resolver_failed(
            "Could not resolve " . $sender->host );
    }
    return DECLINED;
}

sub check_dns {
    my ( $self, $host ) = @_;
    my @host_answers;

    my $qp = $self->qp;
    $qp->input_sock->pause_read;
    
    my $a_records = [];
    my $num_queries = 1;    # queries in progress
    my $mx_found = 0;

    ParaDNS->new(
        callback  => sub {
            my $mx = shift;
            return if $mx =~ /^[A-Z]+$/; # error

            my $addr = $mx->[0];
            $mx_found = 1;

            $num_queries++;
            ParaDNS->new(
                callback => sub { push @$a_records, $_[0] if $_[0] !~ /^[A-Z]+$/; },
                finished => sub { $num_queries--; $self->finish_up($qp, $a_records, $num_queries) },
                host     => $addr,
                type     => 'A',
            );

            if ($has_ipv6) {
                $num_queries++;
                ParaDNS->new(
                    callback => sub { push @$a_records, $_[0] if $_[0] !~ /^[A-Z]+$/; },
                    finished => sub { $num_queries--; $self->finish_up($qp, $a_records, $num_queries) },
                    host     => $addr,
                    type     => 'AAAA',
                );
            }
        },
        finished  => sub {

            unless ($mx_found) {

                $num_queries++;
                ParaDNS->new(
                    callback  => sub { push @$a_records, $_[0] if $_[0] !~ /^[A-Z]+$/; },
                    finished  => sub { $num_queries--; $self->finish_up($qp, $a_records, $num_queries) },
                    host      => $host,
                    type      => 'A',
                );

                if ($has_ipv6) {
                    $num_queries++;
                    ParaDNS->new(
                        callback  => sub { push @$a_records, $_[0] if $_[0] !~ /^[A-Z]+$/; },
                        finished  => sub { $num_queries--; $self->finish_up($qp, $a_records, $num_queries) },
                        host      => $host,
                        type      => 'AAAA',
                   );
                }

            }

            $num_queries--;
            $self->finish_up($qp, $a_records, $num_queries);
        },
        host      => $host,
        type      => 'MX',
    ) or $qp->input_sock->continue_read, return;

    return 1;
}

sub finish_up {
    my ($self, $qp, $a_records, $num_queries) = @_;

    return if defined $qp->transaction->notes('resolvable_fromhost');

    foreach my $addr (@$a_records) {
        if (is_valid($addr)) {
            $qp->transaction->notes('resolvable_fromhost', 1);
            $qp->input_sock->continue_read;
            $qp->run_continuation;
            return;
        }
    }
    
    unless ($num_queries) {
        # all queries returned no valid response
        $qp->transaction->notes('resolvable_fromhost', 0);
        $qp->input_sock->continue_read;
        $qp->run_continuation;
    }
}

sub is_valid {
    my $ip = shift;
    my ( $net, $mask );
    foreach $net ( keys %invalid ) {
        $mask = $invalid{$net};
        $mask = pack "B32", "1" x ($mask) . "0" x ( 32 - $mask );
        return 0
          if join( ".", unpack( "C4", inet_aton($ip) & $mask ) ) eq $net;
    }
    return 1;
}