Grepular

DNS Validation of Email Addresses in Perl

Written 10 years ago by Mike Cardwell

I’ve been writing a typical web application with a signup form, in Perl. One of the fields is for email addresses. Along with the typical syntax checking I wanted to make sure that the hostname part of the email address was potentially routeable, i.e. it at least had suitable DNS records. There’s no point in letting somebody submit an email address which is obviously invalid. After parsing the hostname out of the email address, the Perl to do this seems like it should be quite simple:

use Net::DNS;
if (mx($hostname)) {
  # Has MX records. Routeable.
  $routeable = 1;
}

Those of you who know SMTP will be saying to yourselves, what about A records?!? That’s right, if a hostname doesn’t have MX records, we’re supposed to fall back to an A record lookup, and a AAAA record lookup. We could do something to look up all three:

use Net::DNS;

my $res = Net::DNS::Resolver->new;

if (mx($res, $hostname)) {
  $routeable = 1;
} else {
  foreach my $type (qw( A AAAA )) {
    if (my $reply = $res->query($hostname, $type)) {
      $routeable = 1
        if grep($_->type eq $type, $reply->answer);
    }
    last if $routeable;
  }
}

This approach would work. But the trouble is, I’ve been playing with NodeJS recently and I’ve been spoilt by the way that everything in NodeJS is non-blocking and runs concurrently. The idea of stopping everything to wait for the result of three DNS lookups to happen one after the other is… off-putting to me. Not just that, but DNS lookups take time, and anything which slows down a web application response is off-putting to the user too.

The ideal situation would be one where I could just fire off three DNS requests at the same time, then carry on doing other work related to the request, whilst I’m waiting for any of those three responses to come back. A model based on promises/futures:

1. my $promise = async_routeable($hostname);
2. Do some potentially long running work here
3. my $routeable = $promise->();

Line 1 triggers the DNS lookups and immediately returns a subroutine which can be called later when we want to collect the results, without stopping to wait for the responses.

Line 3 gets the results by calling the subroutine which was previously returned by line 1. If the responses haven’t come back yet, it will block until they do, or a timeout is reached.

So how do we implement “async_routeable” in Perl? Net::DNS gives us some handy methods for treating DNS requests as sockets. You can use these with IO::Select to detect when DNS requests complete:

use Net::DNS;
use IO::Select;
use constant DNS_TIMEOUT => 5;

my $res = Net::DNS::Resolver->new;

sub async_routeable {
  my $hostname = pop;

  my $sel = IO::Select->new;

  foreach my $type (qw( MX A AAAA )) {
    my $socket = $res->bgsend($hostname, $type);
    $sel->add($socket);
  }

  my $end_time = time + DNS_TIMEOUT;

  return sub {
    my $routeable = 0;
    while (!$routeable && $sel->handles) {
      my $wait_for = time - $end_time;
      $wait_for = 0 if $wait_for < 0;
      foreach my $socket ($sel->can_read($wait_for)) {
        $sel->remove($socket);
        my $packet = $res->bgread($socket);
        $routeable = 1
          if $packet->header->tc || $packet->answer;
      }
      last if $wait_for < 1;
    }
    $sel->remove($sel->handles);
    return $routeable;
  };
}

Something to note about this implementation. Notice this line:

if $packet->header->tc || $packet->answer;

If the packet is truncated I assume that the hostname is routeable. Strictly, when this happens you’re supposed to repeat the query using TCP, but there’s no way of doing that using bgsend with Net::DNS. I don’t want to repeat the querys in a blocking fashion, so I work under the assumption that a large packet means it has records in it. I decided to err on the side of caution and assume that mail is routeable rather than not. After all, just because I know the hostname has DNS, doesn’t mean I know that there is a server listening, or that it will like the bit before the @ symbol.

Want to leave a tip?BitcoinMoneroZcashPaypalYou can follow this Blog using RSS or Mastodon. To read more, visit my blog index.