#!/usr/bin/perl -w use Qpsmtpd::DSN; use ParaDNS; use Socket; use Qpsmtpd::TcpServer; 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; } } $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->qp->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->qp->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; } # vim: ts=4 sw=4 expandtab syn=perl