Re: [exim] Perl Scripting SMTP session.

Top Page
Delete this message
Reply to this message
Author: Chris Lightfoot
Date:  
To: Marc Perkel
CC: Exim, Users
Subject: Re: [exim] Perl Scripting SMTP session.
On Wed, Oct 25, 2006 at 02:07:51PM -0700, Marc Perkel wrote:
> I was thinking it would be handy to have some perl scripts to do custom
> sender/recipient verification. Basically I want to connect, wait for the
> greeting, send a HELO, interact using the script, and return the final
> response string.
>
> Anyone have a sample of how to do this?


Relevant perl excerpt follows. Actually this has some
deficiencies (doesn't handle CNAMEs correctly; the check
for postmaster@ causes it to generate bad results for
people who don't support that alias) but it should provide
a useful starting-point. I haven't fixed the problems I
describe since, as discussed previously, sender
verification isn't a whole lot of good; I suppose if I
wanted to do recipient verification it would be worth
improving, but at the moment that's done by a downstream
server in our configuration.

# verify_address_on_host ADDRESS HOST
# Test whether ADDRESS is deliverable on HOST. Returns an array giving a status
# and an explanation; status is '+' when a positive response to the RCPT
# command is received, '-' when a permanent failure response is received, and
# '?' otherwise; the explanation is a human-readable account of the condition.
sub verify_address_on_host ($$) {
    my ($email, $host) = @_;
    my $S = new Net::SMTP($host, Timeout => 15, Hello => 'your.hostname.here');
    if (!$S) {
        Exim::log_write("verify_address_on_host: unable to connect to $host; system error: $!");
        return ('?', "unable to connect to $host: $!");
    }
    $S->mail('')
        || return ('?', "response to MAIL FROM: <> was '" . $S->code() . " " . ch($S->message()) . "'");
    if ($S->recipient($email)) {
        return ('+', "host $host accepted RCPT TO with '" . $S->code() . " " . ch($S->message()) . "'");
    } else {
        my $code = $S->code();
        if (!defined($code)) {
            return ('?', "unknown response to RCPT TO; system error $!");
        } elsif ($code =~ /^4\d\d$/) {
            return ('?', "temporary failure response to RCPT TO '" . $S->code() . " " . ch($S->message()) . "'");
        } elsif ($code =~ /^5\d\d$/) {
            return ('-', "permanent failure response to RCPT TO '" . $S->code() . " " . ch($S->message()) . "'");
        } else {
            return ('?', "unknown response to RCPT TO; code = $code");
        }
    }
    $S->quit();
}


# verify_address ADDRESS [NOCACHE]
# Test whether ADDRESS is deliverable, by consulting the appropriate servers
# for email addressed to that domain. If NOCACHE is true the cache of tested
# addresses is not consulted.
sub verify_address ($;$) {
    my ($email, $nocache) = @_;
    if ($email eq '') {
        return ('+', "blank return-path always valid");
    } elsif ($email !~ /^([^@]+)@(.+)$/) {
        return ('-', "not a valid email address");
    }


    my ($local_part, $domain) = ($1, lc($2));


    if ($domain =~ /^\[[^]]+\]$/) {
        return ('-', "we regard IP literals as invalid");
    }


    $email = $local_part . '@' . $domain;


    # An irritating new trick is for spammers to point the MX records for
    # their domains at those of some other provider, say Yahoo. This would be
    # fine, except that some servers use large numbers of failing RCPT calls
    # as evidence that a host is a source of spam, and should be blacklisted.
    # Therefore, we also check that the postmaster address is deliverable,
    # using a cached result even if NOCACHE is true, and if it is definitely
    # undeliverable, we return a negative result.
    if (lc($local_part) ne 'postmaster') {
        my ($result, $expln) = verify_address('postmaster@' . $domain);
        return ('-', "postmaster\@$domain is undeliverable, so it is not worth checking for $local_part\@$domain; result for postmaster was: $expln")
            if ($result eq '-')
    }


    if (!$nocache) {
        my $x = $T->fetch($email);
        if ($x) {
            my ($result, $expln, $when) = split(/\0/, $x);
            if ($when > time() - $cachetime) {
                return ($result, "$expln (cached)");
            } else {
                $T->delete($email);
            }
        }
    }


    our $R;
    if (!$R) {
        $R = new Net::DNS::Resolver;
        $R->tcp_timeout(10);
        $R->udp_timeout(10);
    }


    my @mailhosts = ( );


    # First try MX records.
    my @mx = mx($R, $domain);
    if (@mx) {
        # Only try the highest preference MXs. Others are likely just to be
        # relays which won't know what is or isn't a valid address.
        my $p = $mx[0]->preference();
        @mailhosts = map { $_->exchange() } grep { $_->preference() == $p } @mx;
        Exim::log_write("verify_address: $email: relevant MX hosts are: " . join(", ", @mailhosts));
    } else {
        @mailhosts = ($domain);
        Exim::log_write("verify_address: $email: no MX records");
    }


    my ($result, $expln);


    my $bad_domain = 0;
    my $found_servers = 0;
    foreach my $host (@mailhosts) {
        my $reply = $R->send($host, 'A');
        if (!defined($reply)) {
            Exim::log_write("verify_address: $email: DNS error resolving '$host' (timeout?)");
            next;
        } if ($reply->header()->rcode() eq 'NXDOMAIN') {
            $bad_domain = 1;
            Exim::log_write("verify_address: $email: no such domain '$domain'");
            next;
        } else {
            $bad_domain = 0;
        }
        foreach my $r ($reply->answer()) {
            next if (ref($r) ne 'Net::DNS::RR::A');
            ++$found_servers;
            my $addr = $r->address();
            ($result, $expln) = verify_address_on_host($email, $addr);
            if ($result ne '?') {
                goto done;
            }
        }
    }


    if ($bad_domain) {
        ($result, $expln) = ('-', "no such domain '$domain'");
    } else {
        $expln ||= 'n/a';
        ($result, $expln) = ('?', "unable to get a definitive answer; tried $found_servers servers; last result: $expln");
    }


done:
    Exim::log_write("verify_address: $email: $result $expln");
    $T->store($email => "$result\0$expln\0" . time());
    if (wantarray()) {
        return ($result, $expln);
    } else {
        return $result;
    }
}



--
``As Lord Denning said,
there's no smoke without fire:
Nil Combustibus Profumo.'' (Flanders and Swann)