Real'sHowTo AddThis Feed Button
Custom Search

Lookup using MX record to validate mail serverTag(s): Networking


The best you can do to verify if an email address is real is to verify if there is a mail server registered to the domain name.
import  java.util.Hashtable;
import  javax.naming.*;
import  javax.naming.directory.*;

public class MXLookup {
  public static void main( String args[] ) {
    if( args.length == 0 ) {
      System.err.println( "Usage: MXLookup host [...]" );
      System.exit( 99 );
    }
    for( int i = 0; i < args.length; i++ ) {
      try {
        System.out.println( args[i] + " has " +
          doLookup( args[i] ) + " mail servers" );
      }
      catch( Exception e ) {
        System.out.println(args[i] + " : " + e.getMessage());
      }
    }
  }

  static int doLookup( String hostName ) throws NamingException {
    Hashtable env = new Hashtable();
    env.put("java.naming.factory.initial",
            "com.sun.jndi.dns.DnsContextFactory");
    DirContext ictx = new InitialDirContext( env );
    Attributes attrs = 
       ictx.getAttributes( hostName, new String[] { "MX" });
    Attribute attr = attrs.get( "MX" );
    if( attr == null ) return( 0 );
    return( attr.size() );
  }
}
The output is
>java MXLookup rgagnon.com realhowto.com
rgagnon.com has 1 mail servers
realhowto.com : DNS name not found [response code 3]

From T. Orbaker, more infos on that subject (thanks to him).

There are other methods by which to validate an email address to a higher degree of than just the mail server.

One:
Use the VRFY command (see RFCs 821/2821). Because this was abused by spammers, it have typically been disabled on most mail servers. Some recent servers don't even support this command as they are so frequently shut off.

When it works, connect to the server, issue the HELO command and then send 'VRFY '. If it is enabled, and the address is valid, you should get a 250 if the address is valid and a 550 if it isn't. Note that some servers (qmail) return 252 as a means of pleading the fifth. Others will return a failure even if the address exists but the command has been disables (although this is typically a 450 error).

Two (better method):
Connect to the servers determined by your code snippet. BUT (and the code below doesn't do this) they must be tried from lowest preference to highest to be absolutely correct.

Once you have connected, you create the SMTP envelope, but you don't put anything in it. This is the point at which most servers will give up the dirt on whether or not an address is valid. If an envelope cannot be built, we know that the address is invalid.

The reason for connecting in order of preference:

Imagine ABC company has an Internet conneciton and runs their own mail server for abc.com. To prevent bounces and other mail errors if their connection or server should be down, their provider isp.com agrees to set up a 'store and forward' scheme for their mail. If abc.com is not available, then isp.com gets the message and when abc.com is again available, the message gets forwarded. The MX records would look something like:

    MX 1 abc.com
    MX 5 isp.com
Now, imagine that you connect to isp.com and try to send a message. The mail server at isp.com doesn't have the actual user list to know which addresses are valid, it just accepts everything and relies on abc.com to sort out the bounces.

If these are not checked in the proper order, there will be no errors for invalid addresses.

Yahoo appears to use a store and forward mechanism to its own internal servers, thus conclusively verifying a yahoo address is not possible. I suspect that hotmail is the same.

It is not possible to verify an address on a domain that uses a catch-all account as the catch account will receive the mail (it does, however, mean that someone will at least SEE the message).

import java.io.*;
import java.net.*;
import java.util.*;
import javax.naming.*;
import javax.naming.directory.*;

public class SMTP {
    private static int hear( BufferedReader in ) throws IOException {
      String line = null;
      int res = 0;

      while ( (line = in.readLine()) != null ) {
          String pfx = line.substring( 0, 3 );
          try {
             res = Integer.parseInt( pfx );
          } 
          catch (Exception ex) {
             res = -1;
          }
          if ( line.charAt( 3 ) != '-' ) break;
      }

      return res;
      }

    private static void say( BufferedWriter wr, String text ) 
       throws IOException {
      wr.write( text + "\r\n" );
      wr.flush();

      return;
      }
    
    private static ArrayList getMX( String hostName )
          throws NamingException {
      // Perform a DNS lookup for MX records in the domain
      Hashtable env = new Hashtable();
      env.put("java.naming.factory.initial",
              "com.sun.jndi.dns.DnsContextFactory");
      DirContext ictx = new InitialDirContext( env );
      Attributes attrs = ictx.getAttributes
                            ( hostName, new String[] { "MX" });
      Attribute attr = attrs.get( "MX" );

      // if we don't have an MX record, try the machine itself
      if (( attr == null ) || ( attr.size() == 0 )) {
        attrs = ictx.getAttributes( hostName, new String[] { "A" });
        attr = attrs.get( "A" );
        if( attr == null ) 
             throw new NamingException
                      ( "No match for name '" + hostName + "'" );
      }

      // Huzzah! we have machines to try. Return them as an array list
      // NOTE: We SHOULD take the preference into account to be absolutely
      //   correct. This is left as an exercise for anyone who cares.
      ArrayList res = new ArrayList();
      NamingEnumeration en = attr.getAll();

      while ( en.hasMore() ) {
         String x = (String) en.next();
         String f[] = x.split( " " );
         if ( f[1].endsWith( "." ) ) 
             f[1] = f[1].substring( 0, (f[1].length() - 1));
         res.add( f[1] );
      }
      return res;
      }

    public static boolean isAddressValid( String address ) {
      // Find the separator for the domain name
      int pos = address.indexOf( '@' );

      // If the address does not contain an '@', it's not valid
      if ( pos == -1 ) return false;

      // Isolate the domain/machine name and get a list of mail exchangers
      String domain = address.substring( ++pos );
      ArrayList mxList = null;
      try {
         mxList = getMX( domain );
      } 
      catch (NamingException ex) {
         return false;
      }

      // Just because we can send mail to the domain, doesn't mean that the
      // address is valid, but if we can't, it's a sure sign that it isn't
      if ( mxList.size() == 0 ) return false;

      // Now, do the SMTP validation, try each mail exchanger until we get
      // a positive acceptance. It *MAY* be possible for one MX to allow
      // a message [store and forwarder for example] and another [like
      // the actual mail server] to reject it. This is why we REALLY ought
      // to take the preference into account.
      for ( int mx = 0 ; mx < mxList.size() ; mx++ ) {
          boolean valid = false;
          try {
              int res;
              Socket skt = new Socket( (String) mxList.get( mx ), 25 );
              BufferedReader rdr = new BufferedReader
                 ( new InputStreamReader( skt.getInputStream() ) );
              BufferedWriter wtr = new BufferedWriter
                 ( new OutputStreamWriter( skt.getOutputStream() ) );

              res = hear( rdr );
              if ( res != 220 ) throw new Exception( "Invalid header" );
              say( wtr, "EHLO orbaker.com" );

              res = hear( rdr );
              if ( res != 250 ) throw new Exception( "Not ESMTP" );

              // validate the sender address  
              say( wtr, "MAIL FROM: <tim@orbaker.com>" );
              res = hear( rdr );
              if ( res != 250 ) throw new Exception( "Sender rejected" );

              say( wtr, "RCPT TO: <" + address + ">" );
              res = hear( rdr );

              // be polite
              say( wtr, "RSET" ); hear( rdr );
              say( wtr, "QUIT" ); hear( rdr );
              if ( res != 250 ) 
                 throw new Exception( "Address is not valid!" );

              valid = true;
              rdr.close();
              wtr.close();
              skt.close();
          } 
          catch (Exception ex) {
            // Do nothing but try next host
          } 
          finally {
            if ( valid ) return true;
          }
      }
      return false;
      }

    public static void main( String args[] ) {
      String testData[] = {
          "tim@orbaker.com", // Valid address
          "fail.me@nowhere.spam", // Invalid domain name
          "arkham@bigmeanogre.net", // Invalid address
          "nosuchaddress@yahoo.com" // Failure of this method
          };

      for ( int ctr = 0 ; ctr < testData.length ; ctr++ ) {
         System.out.println( testData[ ctr ] + " is valid? " + 
               isAddressValid( testData[ ctr ] ) );
      }
      return;
      }
}

S.Boerner has this comments about this HowTo :

The method SMTP::getMX() in the second example throws an ArrayIndexOutOfBoundsException while getting the mailhost via the "A" attribute at:

while ( en.hasMore() ) {
  String x = (String) en.next();
  String f[] = x.split( " " );
  if ( f[1].endsWith( "." ) )
The "A" attribute returns only an address list, so f.length is always 1. I used something like:
boolean hasMX = "MX".equals(attr.getID());
and later
if (hasMX)
{
  mailhost = f[1];
}
else
{
  mailhost = f[0];
}
Here my proposed fix :
import java.io.*;
import java.net.*;
import java.util.*;
import javax.naming.*;
import javax.naming.directory.*;

public class SMTPMXLookup {
   private static int hear( BufferedReader in ) throws IOException {
     String line = null;
     int res = 0;

     while ( (line = in.readLine()) != null ) {
         String pfx = line.substring( 0, 3 );
         try {
            res = Integer.parseInt( pfx );
         }
         catch (Exception ex) {
            res = -1;
         }
         if ( line.charAt( 3 ) != '-' ) break;
     }

     return res;
     }

   private static void say( BufferedWriter wr, String text )
      throws IOException {
     wr.write( text + "\r\n" );
     wr.flush();

     return;
     }
     private static ArrayList getMX( String hostName )
         throws NamingException {
     // Perform a DNS lookup for MX records in the domain
     Hashtable env = new Hashtable();
     env.put("java.naming.factory.initial",
             "com.sun.jndi.dns.DnsContextFactory");
     DirContext ictx = new InitialDirContext( env );
     Attributes attrs = ictx.getAttributes
                           ( hostName, new String[] { "MX" });
     Attribute attr = attrs.get( "MX" );

     // if we don't have an MX record, try the machine itself
     if (( attr == null ) || ( attr.size() == 0 )) {
       attrs = ictx.getAttributes( hostName, new String[] { "A" });
       attr = attrs.get( "A" );
       if( attr == null )
            throw new NamingException
                     ( "No match for name '" + hostName + "'" );
     }
         // Huzzah! we have machines to try. Return them as an array list
     // NOTE: We SHOULD take the preference into account to be absolutely
     //   correct. This is left as an exercise for anyone who cares.
     ArrayList res = new ArrayList();
     NamingEnumeration en = attr.getAll();

     while ( en.hasMore() ) {
        String mailhost;
        String x = (String) en.next();
        String f[] = x.split( " " );
        //  THE fix *************
        if (f.length == 1)
            mailhost = f[0];
        else if ( f[1].endsWith( "." ) )
            mailhost = f[1].substring( 0, (f[1].length() - 1));
        else
            mailhost = f[1];
        //  THE fix *************            
        res.add( mailhost );
     }
     return res;
     }

   public static boolean isAddressValid( String address ) {
     // Find the separator for the domain name
     int pos = address.indexOf( '@' );

     // If the address does not contain an '@', it's not valid
     if ( pos == -1 ) return false;

     // Isolate the domain/machine name and get a list of mail exchangers
     String domain = address.substring( ++pos );
     ArrayList mxList = null;
     try {
        mxList = getMX( domain );
     }
     catch (NamingException ex) {
        return false;
     }

     // Just because we can send mail to the domain, doesn't mean that the
     // address is valid, but if we can't, it's a sure sign that it isn't
     if ( mxList.size() == 0 ) return false;

     // Now, do the SMTP validation, try each mail exchanger until we get
     // a positive acceptance. It *MAY* be possible for one MX to allow
     // a message [store and forwarder for example] and another [like
     // the actual mail server] to reject it. This is why we REALLY ought
     // to take the preference into account.
     for ( int mx = 0 ; mx &lt; mxList.size() ; mx++ ) {
         boolean valid = false;
         try {
             int res;
             //
             Socket skt = new Socket( (String) mxList.get( mx ), 25 );
             BufferedReader rdr = new BufferedReader
                ( new InputStreamReader( skt.getInputStream() ) );
             BufferedWriter wtr = new BufferedWriter
                ( new OutputStreamWriter( skt.getOutputStream() ) );

             res = hear( rdr );
             if ( res != 220 ) throw new Exception( "Invalid header" );
             say( wtr, "EHLO rgagnon.com" );

             res = hear( rdr );
             if ( res != 250 ) throw new Exception( "Not ESMTP" );

             // validate the sender address              
             say( wtr, "MAIL FROM: <tim@orbaker.com>" );
             res = hear( rdr );
             if ( res != 250 ) throw new Exception( "Sender rejected" );

             say( wtr, "RCPT TO: <" + address + ">" );
             res = hear( rdr );

             // be polite
             say( wtr, "RSET" ); hear( rdr );
             say( wtr, "QUIT" ); hear( rdr );
             if ( res != 250 )
                throw new Exception( "Address is not valid!" );

             valid = true;
             rdr.close();
             wtr.close();
             skt.close();
         }
         catch (Exception ex) {
           // Do nothing but try next host
           ex.printStackTrace();
         }
         finally {
           if ( valid ) return true;
         }
     }
     return false;
     }

   public static void main( String args[] ) {
     String testData[] = {
         "real@rgagnon.com",
         "you@acquisto.net",
         "fail.me@nowhere.spam", // Invalid domain name
         "arkham@bigmeanogre.net", // Invalid address
         "nosuchaddress@yahoo.com" // Failure of this method
         };

     for ( int ctr = 0 ; ctr < testData.length ; ctr++ ) {
        System.out.println( testData[ ctr ] + " is valid? " +
              isAddressValid( testData[ ctr ] ) );
     }
     return;
     }
} 

A note sent by M. Donders.
While using the code I noticed a problem with greylisting. 
Greylisting, is an anti spam attempt which denies service 
for the first time the triple of client, sender and address 
reaches the smtp server. Greylisting will send back a 
temporary error (450) and therefore the address will be 
denied. In this case it probably is better to accept the 
address as verified, because there is no better information 
available at that moment.

A list a possible SMTP Reply Codes:

Code Description
211  System status, or system help reply.
214  Help message.
220  Domain service ready.
     Ready to start TLS.
221  Domain service closing transmission channel.
250  OK, queuing for node node started.
     Requested mail action okay, completed.
251  OK, no messages waiting for node node.
     User not local, will forward to forwardpath.
252  OK, pending messages for node node started.
     Cannot VRFY user (e.g., info is not local), 
     but will take message for this user and attempt delivery.
253  OK, messages pending messages for node node started.
354  Start mail input; end with ..
355  Octet-offset is the transaction offset.
421  Domain service not available, closing transmission channel.
432  A password transition is needed.
450  Requested mail action not taken: mailbox unavailable.
     (ex. mailbox busy)
451  Requested action aborted: local error in processing.
     Unable to process ATRN request now
452  Requested action not taken: insufficient system storage.
453  You have no mail.
454  TLS not available due to temporary reason.
     Encryption required for requested authentication mechanism.
458  Unable to queue messages for node node.
459  Node node not allowed: reason.
500  Command not recognized: command.
     Syntax error.
501  Syntax error, no parameters allowed.
502  Command not implemented.
503  Bad sequence of commands.
504  Command parameter not implemented.
521  Machine does not accept mail.
530  Must issue a STARTTLS command first.
     Encryption required for requested authentication mechanism.
534  Authentication mechanism is too weak.
538  Encryption required for requested authentication mechanism.
550  Requested action not taken: mailbox unavailable.
551  User not local; please try forwardpath.
552  Requested mail action aborted: exceeded storage allocation.
553  Requested action not taken: mailbox name not allowed.
554  Transaction failed.
RFC0821

blog comments powered by Disqus


If you find this article useful, consider making a small donation
to show your support for this Web site and its content.

Written and compiled by Réal Gagnon ©1998-2014
[ home ]