Verify e-mail addresses with Address Validator


Introduction

EmailAddressValidator component lets you verify e-mail addresses for correct syntax and, optionally, for existence. The component can check both single addresses and bulks of hundreds of thousands.

Address Validator supports multi-threading (up to 60 threads) which greatly improves performance and enables up to 100.000 address checks per hour in the most accurate validation mode, SendAttempt. You can enable it with setting EmailAddressValidator.MaxThreadCount property. Another important performance optimization, DNS caching, is also supported, and enabled by default. As for memory usage, it stays stable and won't increase even if you run the check for many hours or even days validating millions of addresses.

In .NET Core and UWP editions, only single addresses can be checked at a time. Bulk operations are not supported yet.

The following validation levels can be set with EmailAddressValidator.ValidationLevel property (defined by AddressValidationLevel enum):

To start with Address Validator, you need to add a reference to MailBee.NET.dll, import namespaces and unlock the component.


Import namespaces and set license key

To learn how to add a reference to MailBee.NET.dll (or MailBee.NET.45.dll in .NET 4.5+ case), see similar topic in MailBee.NET SMTP guide.

Then, import the required namespaces at the top of your code file:

using MailBee;
using MailBee.AddressCheck;
using MailBee.SmtpMail;
Imports MailBee
Imports MailBee.AddressCheck
Imports MailBee.SmtpMail

Note that MailBee and MailBee.SmtpMail namespaces do not directly belong to Address Validator but the classes in these namespaces are commonly used with Address Validator classes.

After importing namespaces, unlock the component with your trial or permanent license key. The key can be specified in the code, in app.config/web.config, in the registry.

Option A - specify license key directly in code

Before the first use and creating any instances of EmailAddressValidator class, set the static MailBee.Global.LicenseKey property to assign the license key (if you haven't done this before). It's assumed you've already imported MailBee.AddressCheck namespace with using (C#) or Imports (VB) directives.

MailBee.Global.LicenseKey = "MN110-0123456789ABCDEF-0123";
MailBee.Global.LicenseKey = "MN110-0123456789ABCDEF-0123"

You can also create and unlock an instance of EmailAddressValidator class, passing the license key as a parameter of EmailAddressValidator(string) constructor. Again, it's assumed you've already imported MailBee.AddressCheck namespace.

EmailAddressValidator valid = new EmailAddressValidator("MN110-0123456789ABCDEF-0123");
Dim valid As New EmailAddressValidator("MN110-0123456789ABCDEF-0123")

Option B - put license key in app.config or web.config

Alternatively, in app.config locate <appSettings> entry in <configuration> section (if it's not there, create it), and add MailBee.NET license key as follows:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="MailBee.Global.LicenseKey" value="MN110-0123456789ABCDEF-0123"/>
  </appSettings>
</configuration>

If <appSettings> originally looked as <appSettings/> (self-closing tag syntax), you'll need to unfold it to <appSettings></appSettings> so that you could insert <add key .../> there.

The above also applies to ASP.NET application and web.config.

If you do not yet have app.config or web.config, see Import namespaces and set license key in MailBee.NET SMTP guide on how to add it.

You can also specify the key in machine.config file. This will enable access to the license key to all applications on the computer so that there will be no need to specify the license key in each application separately. For instance, this is the preferred way if you a hosting provider willing to let your hosting clients create and run applications which use MailBee.NET without disclosing your license key to them.


Usage basics

The topics below assume you have already referenced MailBee.NET assembly. Code snippets which are not complete programs assume you have imported MailBee and MailBee.AddressCheck namespace, unlocked the license and created a new EmailAddressValidator instance named valid.

To verify a single e-mail address, use Verify(string) overload.

By default, EmailAddressValidator.ValidationLevel is already SendAttempt (the most advanced e-mail address check available). Therefore we don't need to explicitly set this mode to perform all the checks possible against a single e-mail address.

using System;
using MailBee;
using MailBee.AddressCheck;

class Program
{
    static void Main(string[] args)
    {
        EmailAddressValidator valid = new EmailAddressValidator("MN100-0123456789ABCDEF-0123");

        // To perform DNS MX lookup queries, we need some DNS servers for that.
        valid.DnsServers.Autodetect();

        // Logging into a file is not required for the component to work
        // but helpful for debugging and understanding things under the hood.
        valid.Log.Enabled = true;
        valid.Log.Filename = @"C:\Temp\log.txt";
        valid.Log.Clear();

        AddressValidationLevel result = valid.Verify("jdoe@company.com");
        Console.WriteLine(result.ToString());
    }
}
Imports MailBee
Imports MailBee.AddressCheck

Class Program
    Shared Sub Main()
        Dim valid As New EmailAddressValidator("MN100-0123456789ABCDEF-0123")

        ' To perform DNS MX lookup queries, we need some DNS servers for that.
        valid.DnsServers.Autodetect()

        ' Logging into a file is not required for the component to work
        ' but helpful for debugging and understanding things under the hood.
        valid.Log.Enabled = True
        valid.Log.Filename = "C:\Temp\log.txt"
        valid.Log.Clear()

        Dim result As AddressValidationLevel = valid.Verify("jdoe@company.com")
        Console.WriteLine(result.ToString())
    End Sub
End Class

To verify a bulk of addresses, subscribe to Verified event, start verification with Verify(DataTable, string) or Verify(string[]) overload, and examine event arguments (the event raises each time an address is verified, with either positive or negative result).

using System;
using MailBee;
using MailBee.AddressCheck;

class Program
{
    // This method will be executed for each e-mail address in the bulk,
    // whenever it gets verified, with either positive or negative result.
    private static void valid_Verified(object sender, VerifiedEventArgs e)
    {
        Console.WriteLine("*** Verified ***");
        Console.WriteLine("Email: " + e.Email);
        Console.WriteLine("Result: " + e.Result.ToString());
        if (e.Reason != null)
        {
            Console.WriteLine("Reason: " + e.Reason.Message);
        }
        Console.WriteLine();
    }

    static void Main(string[] args)
    {
        EmailAddressValidator valid = new EmailAddressValidator("MN100-0123456789ABCDEF-0123");

        // To perform DNS MX lookup queries, we need some DNS servers for that.
        valid.DnsServers.Autodetect();

        // Logging into a file is not required for the component to work
        // but helpful for debugging and understanding things under the hood.
        valid.Log.Enabled = true;
        valid.Log.Filename = @"C:\Temp\log.txt";
        valid.Log.Clear();

        // Subscribe to event.
        valid.Verified += new VerifiedEventHandler(valid_Verified);

        // Populate an array with some e-mails. You can also use DataTable as a source.
        string[] emails = new string[] { "jdoe@company.com", "bob@example.com", "alice@domain.com" };

        // Run the verification in single-threaded mode.
        valid.Verify(emails);
    }
}
Imports MailBee
Imports MailBee.AddressCheck

Class Program
    ' This method will be executed for each e-mail address in the bulk,
    ' whenever it gets verified, with either positive or negative result.
    Private Shared Sub valid_Verified(ByVal sender As Object, ByVal e As VerifiedEventArgs)
        Console.WriteLine("*** Verified ***")
        Console.WriteLine(("Email: " & e.Email))
        Console.WriteLine(("Result: " & e.Result.ToString))
        If (Not (e.Reason) Is Nothing) Then
            Console.WriteLine(("Reason: " & e.Reason.Message))
        End If
        Console.WriteLine()
    End Sub

    Shared Sub Main(ByVal args() As String)
        Dim valid As EmailAddressValidator = New EmailAddressValidator("MN100-0123456789ABCDEF-0123")

        ' To perform DNS MX lookup queries, we need some DNS servers for that.
        valid.DnsServers.Autodetect()

        ' Logging into a file is not required for the component to work
        ' but helpful for debugging and understanding things under the hood.
        valid.Log.Enabled = True
        valid.Log.Filename = "C:\Temp\log.txt"
        valid.Log.Clear()

        ' Subscribe to event.
        AddHandler valid.Verified, AddressOf valid_Verified

        ' Populate an array with some e-mails. You can also use DataTable as a source.
        Dim emails() As String = New String() {"jdoe@company.com", "bob@example.com", "alice@domain.com"}

        ' Run the verification in single-threaded mode.
        valid.Verify(emails)
    End Sub
End Class

The previous sample checked addresses one by one. Now let's enable multi-threading to make things faster.

The next sample processes 10 addresses instead of 3, but completes faster. Also, it demonstrates using a DataTable rather than a string array. Actually, in this sample we build this DataTable from a string array but you can populate it from elsewhere, such as database.

Although the sample below uses multi-threading, it does not do any thread synchronizing in Verified event handler (despite the fact that in multi-threaded mode this event can occur simultaneously on many threads). This is because we're not using any thread-unsafe things in event handlers. But most .NET classes are thread-unsafe. If you need to use thread-unsafe class there, see Verify sample on how to synchronize threads.

Note that if you're using Visual Studio 2010 or earlier, you may be experiencing sudden lockups when debugging a multi-threaded application. Upgrading to Visual Studio 2012 or newer solves the issue. SeeEmailAddressValidator.MaxThreadCount topic for other options.

using System;
using System.Data;
using MailBee;
using MailBee.AddressCheck;

class Program
{
    // This method will be executed for each e-mail address in the bulk,
    // whenever it gets verified, with either positive or negative result.
    private static void valid_Verified(object sender, VerifiedEventArgs e)
    {
        Console.WriteLine("*** Verified ***");
        Console.WriteLine("Email: " + e.Email);
        Console.WriteLine("Result: " + e.Result.ToString());
        if (e.Reason != null)
        {
            Console.WriteLine("Reason: " + e.Reason.Message);
        }
        Console.WriteLine();
    }

    static void Main(string[] args)
    {
        EmailAddressValidator valid = new EmailAddressValidator("MN100-0123456789ABCDEF-0123");

        // To perform DNS MX lookup queries, we need some DNS servers for that.
        valid.DnsServers.Autodetect();

        // Logging into a file is not required for the component to work
        // but helpful for debugging and understanding things under the hood.
        // Since this sample is multi-threaded, we also set AddContextInfo option
        // to make it easier analyze the log (each record will be tagged with a thread ID).
        valid.Log.Enabled = true;
        valid.Log.Format = LogFormatOptions.AddContextInfo;
        valid.Log.Filename = @"C:\Temp\log.txt";
        valid.Log.Clear();

        // Subscribe to event.
        valid.Verified += new VerifiedEventHandler(valid_Verified);

        // In this sample, we'll build DataTable manually from a string array.
        string[] emails = new string[10];
        for (int i = 0; i < emails.Length; i++)
        {
            // Make user0@domain0.com, user1@domain1.com, ..., user9@domain9.com.
            emails[i] = string.Format("user{0}@domain{0}.com", i.ToString());
        }

        // In real apps, you'll get data from database. For now, populate data from
        // string array by putting all the data into a table with one column "email".
        DataTable data = valid.ArrayToDataTable(emails);

        // If you comment the next code line out, the whole thing will be dead slow.
        // Checking each address usually involves a test connection, and the time required to understand
        // that there is no living host at the other side (for e-mail addresses with non-existent domains)
        // may exceed 20 seconds. For 10 addresses in this sample, if run one-by-one, this will take minutes.
        // Multi-threading, however, will make all the e-mail addresses in list list be checked
        // simultaneously (MailBee can do up to 60 of simultaneous queries). The total time won't exceed
        // the time required for a single address be checked (which maxes out at 20 seconds or so).
        valid.MaxThreadCount = -1;

        // Run the verification in multi-threaded mode.
        valid.Verify(data, "email");
    }
}
Imports System.Data
Imports MailBee
Imports MailBee.AddressCheck

Class Program
    ' This method will be executed for each e-mail address in the bulk,
    ' whenever it gets verified, with either positive or negative result.
    Private Shared Sub valid_Verified(ByVal sender As Object, ByVal e As VerifiedEventArgs)
        Console.WriteLine("*** Verified ***")
        Console.WriteLine("Email: " & e.Email)
        Console.WriteLine("Result: " & e.Result.ToString())
        If e.Reason IsNot Nothing Then
            Console.WriteLine("Reason: " & e.Reason.Message)
        End If
        Console.WriteLine()
    End Sub

    Shared Sub Main(ByVal args As String())
        Dim valid As New EmailAddressValidator("MN100-0123456789ABCDEF-0123")

        ' To perform DNS MX lookup queries, we need some DNS servers for that.
        valid.DnsServers.Autodetect()

        ' Logging into a file is not required for the component to work
        ' but helpful for debugging and understanding things under the hood.
        ' Since this sample is multi-threaded, we also set AddContextInfo option
        ' to make it easier analyze the log (each record will be tagged with a thread ID).
        valid.Log.Enabled = True
        valid.Log.Format = LogFormatOptions.AddContextInfo
        valid.Log.Filename = "C:\Temp\log.txt"
        valid.Log.Clear()

        ' Subscribe to event.
        AddHandler valid.Verified, AddressOf valid_Verified

        ' In this sample, we'll build DataTable manually from a string array.
        Dim emails As String() = New String(9) {}
        For i As Integer = 0 To emails.Length - 1
            ' Make user0@domain0.com, user1@domain1.com, ..., user9@domain9.com.
            emails(i) = String.Format("user{0}@domain{0}.com", i.ToString())
        Next

        ' In real apps, you'll get data from database. For now, populate data from
        ' string array by putting all the data into a table with one column "email".
        Dim data As DataTable = valid.ArrayToDataTable(emails)

        ' If you comment the next code line out, the whole thing will be dead slow.
        ' Checking each address usually involves a test connection, and the time required to understand
        ' that there is no living host at the other side (for e-mail addresses with non-existent domains)
        ' may exceed 20 seconds. For 10 addresses in this sample, if run one-by-one, this will take minutes.
        ' Multi-threading, however, will make all the e-mail addresses in list list be checked
        ' simultaneously (MailBee can do up to 60 of simultaneous queries). The total time won't exceed
        ' the time required for a single address be checked (which maxes out at 20 seconds or so).
        valid.MaxThreadCount = -1

        ' Run the verification in multi-threaded mode.
        valid.Verify(data, "email")
    End Sub
End Class

As all the addresses in these samples are fake, the verification of them all will deliver negative results (not AddressValidationLevel.OK).

In general case, however, If the result of an e-mail address verification is negative, you should analyze it in more detail. This is because the failed check does not necessarily indicate that the e-mail address is invalid, see Examining the results to see why this happens and what you can do with this.

All the samples above used the maximum (and default) validation level available, AddressValidationLevel.SendAttempt. In real apps, it's not always the best choice, especially if for some reason you cannot run the check on a machine which has a public IP address with a properly configured rDNS record (and, ideally, MX and SPF), running SMTP service, and so on. Read the next topic to learn which validation method better suits your case.


Understanding validation levels

Each next level includes all the previous levels. The higher level is, the more accurate check is. However, for SmtpConnection and SendAttempt levels be sure that the host from which you're running Address Validator has a public IP address with a valid reverse DNS record, not black-listed by popular RBLs and so on. Otherwise, many MX hosts will reject submissions of e-mail addresses even if they are correct. This is because the receiving host may think you're a spammer willing to send spam to its users. Read below for more details.

RegexCheck

Syntax check is the basic level. It does regular expression test to check if the address is at least syntactically correct. If required, the regular expression can be adjusted (EmailAddressValidator.RegexPattern property). It's very fast as no network connections are made, and very simple.

On the one hand, this check filters out only certainly invalid addresses which have broken syntax, no checking of actual existence of the domain or the account is made - this is drawback. On the other hand, if this check says the address is invalid - it's invalid (at least if the regular expression stays the same). It won't change in two hours or tomorrow. It does not depend on anything. More advanced validation levels are not that simple and may require multiple checks to be made in different days to prove that the address is really non-existent.

DnsQuery

DNS MX lookup, in addition to syntax check, also extracts the domain part from the e-mail address string and makes a query to the DNS servers listed in EmailAddressValidator.DnsServers collection, to find out which SMTP MX hosts accept e-mail for that domain. This lets MailBee find out that the given domain actually exists and can, at least in theory, accept e-mail. Still, there is no guarantee that the SMTP MX hosts found are actually online. Neither it's known if the account part of the e-mail address denotes the valid mailbox on that server.

The DNS MX lookup check involves making network queries, and thus heavily utilizes network resources. For better performance and reliability, you can specify multiple servers in EmailAddressValidator.DnsServers, MailBee will automatically distribute load between them. Also, MailBee extensively uses DNS caching to reduce load on DNS servers (because many domains may appear in e-mail address many times, like gmail.com, yahoo.com, and others). If you need to turn DNS caching off for some reason, use MailBee.DnsMX.DnsCache.Enabled property.

In many cases, DNS MX lookup represents reasonable trade between the rate of false positives and false negatives. On the one hand, this method cannot actually tell if MX hosts are dead or alive or if the given account actually exists. On the other hand, this method won't tell you that some address is invalid just because it's SMTP MX host is temporarily down or currently rejects connections from you as you somehow got into some RBL blacklist.

In terms of reproducibility, DNS MX lookup is pretty close to syntax check - the same e-mail address will likely deliver the same result, no matter what day you're running the test. This assumes, however, the DNS servers being used are properly configured and do not report false data to the application.

SmtpConnection

In addition to DNS MX lookup, this method will also do a connection to port 25 of the SMTP MX server found during the DNS MX lookup. As a domain can have more than one MX servers with different priorities, MailBee can try them accordingly their priorities in case if higher-priority servers seem to be down.

A test connection does the following: connects to the server, receives the initial response, executes EHLO command (HELO for very old servers which don't understand EHLO), then executes QUIT command to close the connection. It's recommended to set the appropriate domain name MailBee will pass as EHLO argument. Use the external domain name of the machine running MailBee. For instance, if the check is being run on afterlogic.com server, set EmailAddressValidator.HelloDomain to "afterlogic.com" value.

If the test connection succeeds, this means the host can actually accept e-mail for the given domain. This ensures the corresponding SMTP MX server is actually alive. However, if your IP address is in the black-list of the given server, the connection may not go through and MailBee will report the address as "failed".

This method requires more network resources than DNS MX query check, and also your firewall and your ISP's firewall must pass outgoing connections to port 25. Many ISPs block this, so make sure it's not an issue for you.

Also, it's recommended to have a valid PTR (rDNS) record on your IP address (having MX and SPF records would be another plus), and have a working SMTP server there (some hosts only allow connections from hosts which are SMTP servers themselves). It's not necessary to have the SMTP server at the same computer. They just should have the same IP address (for instance, an SMTP server is an Internet gate either while MailBee machine is in internal network and "sees" Internet through this gate). Other working scenarios are possible too. The main idea is to make other servers think you're one of them, not just one of home computers infected by a virus and sending spam, with no public IP address, PTR record, MX record, or SMTP service running.

Even if the connection succeeds, there is no guarantee the SMTP MX host would accept e-mail for the given account name. At this point, all we know is that the e-mail address syntax is correct, the domain is valid, the SMTP MX host for this domain is online.

SendAttempt

This method not just connects to the SMTP MX host and sends QUIT like the previous method does. It also submits the sender in MAIL FROM command and the e-mail address being checked, in RCPT TO command.

As a sender, MailBee uses user@domain.com string by default but you can change it to anything you want with EmailAddressValidator.MailFrom property. Recommend setting is an e-mail address which exists on your SMTP server (which should run at the same IP address as external IP address of MailBee machine). Or, at least the domain part of should be valid. This way the receiving host will see that the sending host does not use sender's e-mail which does not belong to its domain. In case if your network does not have an SMTP server, the e-mail address checking quality will degrade a bit due to a number of false negatives.

Shortly, if you don't have an SMTP service, some hosts will tell that the e-mail address being submitted is invalid even if it's OK.

Once MailBee did RCPT TO command passing the e-mail address being verified as a parameter and the server replied OK, MailBee sends RSET command to reset the connection (because otherwise the server would wait for the DATA command and message data to be submitted). Then, MailBee sends QUIT and this closes the connection. The e-mail address is considered to be valid.

Still, false positives may exist (in addition to false negatives explained earlier). Many servers say that an address is correct even if it's not. They just accept any address and say OK (this way spammers cannot find out which addresses exist or not by trying all popular names and checking what server reply to each name).

With such servers, if the address is incorrect and you send an e-mail there. you'll get bounce later (maybe, in a minute, or in a few hours). There is no workaround unless to monitor your mailbox for bounces, parse them, and remove invalid address from your database (see DeliveryStatusParser class for this purpose). However, preliminary check for invalid addresses with Address Validator greatly reduces the amount of bounces as many bad address are still filtered out before you start bulk sending.


Examining the results

If for certain e-mail address the result was not AddressValidationLevel.OK, this alone means nothing. As explained above, there can be false negatives. This applies to AddressValidationLevel.SmtpConnection andAddressValidationLevel.SendAttempt modes.

You should not just check the return value for OK, you should analyze the result in a complex pattern:

The sample below shows the idea how to address some of these issues. We run the check 3 times. Each time, we refine the results determined during previous checks. Thus, if the server or domain for some e-mail address was not not available on certain attempt, we mark the address as "suspicious", and during the next check we either prove this (and mark the address as Bad) or restore it to Good.

We have to kinds of "Suspicious" statuses: ProbablyBad status for DNS errors and SMTP connection failures, and transient SMTP errors for which we set the status to "greylisting is possibly in action" (ProbablyGreylisted). If ProbablyBad repeats on next attempt, we mark this address as Bad, if ProbablyGreylisted repeats - mark it as ProbablyBad.

Note that in real apps the time interval between checks should be at least one day. If the server accepting e-mail for the given e-mail address was offline during the current check, it won't likely go back online in a few seconds. Should you make a second attempt immediately after the first one, you'd probably get the same results. BUT! If the server reported a transient error (ProbablyGreylisted), this often indicates greylisting. In this case, the second attempt for ProbablyGreylisted addresses should occur earlier, within an hour (10-15 minutes would be great). If the second attempt failed too, it wasn't greylisting but rather a real problem with the server, and we should try again next day or so.

using System;
using System.Data;
using MailBee;
using MailBee.AddressCheck;
using MailBee.SmtpMail;

class Program
{
    // This enum defines the status of an e-mail address in our data table.
    enum AddressStatus
    {
        Good = 0,
        ProbablyGreylisted = 1,
        ProbablyBad = 2,
        Bad = 3
    }

    const string EmailColumn = "email";
    const string StatusColumn = "status";

    // In real apps, you'll get this data table from your database.
    // In the sample, we build it directly in the code.
    private static DataTable GetDataTable()
    {
        DataTable workTable = new DataTable();
        workTable.Columns.Add(EmailColumn, typeof(string));
        workTable.Columns.Add(StatusColumn, typeof(AddressStatus));

        // Populate the data table with several addresses (some are surely wrong)
        // and initially mark them Good. We believe they are good until proven otherwise.
        string[] emails = new string[] { "jdoe@company.com", "no.dot.in@domain",
			"bob@example.com", "user@nonexistent.domain", "alice@domain.com" };
        foreach (string email in emails)
        {
            DataRow row = workTable.NewRow();
            row[EmailColumn] = email;
            row[StatusColumn] = AddressStatus.Good;
            workTable.Rows.Add(row);
        }

        return workTable;
    }

    // This method executes when the e-mail address is about to be checked.
    private static void valid_Verifying(object sender, VerifyingEventArgs e)
    {
        // In this sample, we won't check addresses which have already proven to be bad.
        // You can use the same idea to skip any other addresses you wish, such as
        // to implement black-listing or white-listing.
        if ((AddressStatus)e.Row[StatusColumn] == AddressStatus.Bad)
        {
            e.VerifyIt = false;
        }
    }

    // This method executes after the e-mail address was checked.
    private static void valid_Verified(object sender, VerifiedEventArgs e)
    {
        // Nothing new in this part.
        Console.WriteLine("*** Verified ***");
        Console.WriteLine("Email: " + e.Email);
        Console.WriteLine("Result: " + e.Result.ToString());
        if (e.Reason != null)
        {
            Console.WriteLine("Reason: " + e.Reason.Message);
        }

        // Interesting part starts here.
        AddressStatus previousStatus = (AddressStatus)e.Row[StatusColumn];
        AddressStatus newStatus = previousStatus;

        switch (e.Result)
        {
            case AddressValidationLevel.OK:
                // If the address was Good, it stays Good. If it was Probably"Something",
                // it has now cleared our suspicions, congrats!
                if (previousStatus != AddressStatus.Good)
                {
                    newStatus = AddressStatus.Good;
                }
                break;
            case AddressValidationLevel.RegexCheck:
                // If the address failed regex check, it just had bad syntax.
                // Does not make sense to give it a second chance.
                newStatus = AddressStatus.Bad;
                break;
            case AddressValidationLevel.DnsQuery:
                if (previousStatus == AddressStatus.Good ||
                    previousStatus == AddressStatus.ProbablyGreylisted)
                {
                    // Give the address which was good before, a second chance. We also do this for
                    // "probably greylisted" addresses because from DNS MX point of view they are good
                    // (valid MX host, running SMTP service) and DNS error is something new for them,
                    // so there is a chance it's a temporary issue.
                    newStatus = AddressStatus.ProbablyBad;
                }
                else
                {
                    // The address had already been given second chance before and thus had ProbablyBad status.
                    // Execute it. No mercy this time. But in real apps you better take this decision on at least
                    // 3rd failed attempt, and at least 3 days should pass since the first attempt. Give
                    // the e-mail service of the recipient some time to fix the issues they may be experiencing,
                    // or the mailbox was just full and the user had no time to get some free space there.
                    newStatus = AddressStatus.Bad;
                }
                break;
            case AddressValidationLevel.SmtpConnection:
                // In this sample, we take the same decisions in SmtpConnection case like we do for DnsQuery.
                // This is because the reasons are pretty similar in most cases. The domain could go down due
                // to the name server issue, the same can happen with a SMTP MX e-mail server. This differs
                // from the situation when the SMTP MX server does respond but says "No" (see below).
                if (previousStatus == AddressStatus.Good ||
                    previousStatus == AddressStatus.ProbablyGreylisted)
                {
                    newStatus = AddressStatus.ProbablyBad;
                }
                else
                {
                    newStatus = AddressStatus.Bad;
                }
                break;
            case AddressValidationLevel.SendAttempt:
                // Did the connection simply break or did the server actually say "No"?
                // Connection break may indicate a temporary issue with the SMTP MX server.
                IMailBeeNegativeSmtpResponseException negativeResponseEx;
                negativeResponseEx = e.Reason as IMailBeeNegativeSmtpResponseException;
                bool isNegativeReply = (negativeResponseEx != null);

                // For No errors, we need to make sure this is the final verdict of the SMTP MX server.
                // If the error is transient rather than the final verdict, it leaves us a second chance to try.
                if (isNegativeReply && !negativeResponseEx.IsTransientError)
                {
                    // The SMTP MX server which is charge for the given e-mail address said us strict "No".
                    // We don't know the exact reason (maybe, your IP address is blacklisted by some RBLs),
                    // but under the current circumstances the given e-mail address cannot receive e-mail from us.
                    newStatus = AddressStatus.Bad;
                    break;
                }

                // At this point, we have only temporary SMTP MX errors left, such as broken connections or
                // greylisting ("Try again later" responses). For such errors, giving second chances does make sense.

                if (previousStatus == AddressStatus.Good)
                {
                    if (isNegativeReply)
                    {
                        // The server replied "No, but try again later". It's legit thing, so there is a good
                        // chance there IS a real e-mail account under the given name there.
                        newStatus = AddressStatus.ProbablyGreylisted;
                    }
                    else
                    {
                        // The server suddenly broke the connection. Give it one second chance, but no more.
                        newStatus = AddressStatus.ProbablyBad;
                    }
                }
                else if (previousStatus == AddressStatus.ProbablyGreylisted)
                {
                    // The server said "Try again later" or broke the connection?
                    if (isNegativeReply)
                    {
                        // Greylisting returned the same result second time in a row. Maybe, it wasn't greylisting
                        // at all? Give this e-mail address one more chance, but our patience is nearly over.
                        newStatus = AddressStatus.ProbablyBad;
                    }
                    else
                    {
                        // Well, "If isNegativeReply" is actually redundant here. If the server broke the connection
                        // instead of saying "Better luck next time", our assumption remains the same. There is
                        // still a small chance that the server will accept the given address next time, so give it
                        // one more shot to try. In your apps, however, you may consider different logic here.
                        newStatus = AddressStatus.ProbablyBad;
                    }
                }
                else
                {
                    // We're tired of all that "Try again later" occurring 3 times in a row and repetitive
                    // disconnects during send attempts. Consider the e-mail address dead.
                    newStatus = AddressStatus.Bad;
                }
                break;
        }

        if (previousStatus != newStatus)
        {
            // Update the status in the data table. You may also need to do Commit, Update
            // or whatever is required to apply the changes to your database.
            e.Row[StatusColumn] = newStatus;
        }

        Console.WriteLine();
    }

    // Dumps the contents of the data table to console. We use it how the statuses change
    // over the time with making new attempts to verify the corresponding e-mails addresses.
    private static void DisplayDataTable(DataTable data)
    {
        foreach (DataRow row in data.Rows)
        {
            Console.WriteLine(string.Format("{0}={1}",
                row[EmailColumn].ToString(), ((AddressStatus)row[StatusColumn]).ToString()));
        }
        Console.WriteLine();
    }

    static void Main(string[] args)
    {
        // Common initialization, like in previous samples.
        EmailAddressValidator valid = new EmailAddressValidator("MN100-0123456789ABCDEF-0123");
        valid.DnsServers.Autodetect();
        valid.Log.Enabled = true;
        valid.Log.Format = LogFormatOptions.AddContextInfo;
        valid.Log.Filename = @"C:\Temp\log.txt";
        valid.Log.Clear();
        valid.Verifying += new VerifyingEventHandler(valid_Verifying);
        valid.Verified += new VerifiedEventHandler(valid_Verified);
        valid.MaxThreadCount = -1;

        DataTable data = GetDataTable();

        valid.Verify(data, EmailColumn);
        DisplayDataTable(data);

        Console.WriteLine("Press any key to continue to the 2nd run");
        Console.ReadKey(true);

        valid.Verify(data, EmailColumn);
        DisplayDataTable(data);

        Console.WriteLine("Press any key to continue to the 3rd run");
        Console.ReadKey(true);

        valid.Verify(data, EmailColumn);
        DisplayDataTable(data);

        Console.WriteLine("Press any key to exit");
        Console.ReadKey(true);
    }
}
Imports System.Data
Imports MailBee
Imports MailBee.AddressCheck
Imports MailBee.SmtpMail

Class Program
    ' This enum defines the status of an e-mail address in our data table.
    Private Enum AddressStatus
        Good = 0
        ProbablyGreylisted = 1
        ProbablyBad = 2
        Bad = 3
    End Enum

    Const EmailColumn As String = "email"
    Const StatusColumn As String = "status"

    ' In real apps, you'll get this data table from your database.
    ' In the sample, we build it directly in the code.
    Private Shared Function GetDataTable() As DataTable
        Dim workTable As New DataTable()
        workTable.Columns.Add(EmailColumn, GetType(String))
        workTable.Columns.Add(StatusColumn, GetType(AddressStatus))

        ' Populate the data table with several addresses (some are surely wrong)
        ' and initially mark them Good. We believe they are good until proven otherwise.
        Dim emails As String() = New String() {"jdoe@company.com", "no.dot.in@domain", _
         "bob@example.com", "user@nonexistent.domain", "alice@domain.com"}
        For Each email As String In emails
            Dim row As DataRow = workTable.NewRow()
            row(EmailColumn) = email
            row(StatusColumn) = AddressStatus.Good
            workTable.Rows.Add(row)
        Next

        Return workTable
    End Function

    ' This method executes when the e-mail address is about to be checked.
    Private Shared Sub valid_Verifying(ByVal sender As Object, ByVal e As VerifyingEventArgs)
        ' In this sample, we won't check addresses which have already proven to be bad.
        ' You can use the same idea to skip any other addresses you wish, such as
        ' to implement black-listing or white-listing.
        If CType(e.Row(StatusColumn), AddressStatus) = AddressStatus.Bad Then
            e.VerifyIt = False
        End If
    End Sub

    ' This method executes after the e-mail address was checked.
    Private Shared Sub valid_Verified(ByVal sender As Object, ByVal e As VerifiedEventArgs)
        ' Nothing new in this part.
        Console.WriteLine("*** Verified ***")
        Console.WriteLine("Email: " & e.Email)
        Console.WriteLine("Result: " & e.Result.ToString())
        If e.Reason IsNot Nothing Then
            Console.WriteLine("Reason: " & e.Reason.Message)
        End If

        ' Interesting part starts here.
        Dim previousStatus As AddressStatus = CType(e.Row(StatusColumn), AddressStatus)
        Dim newStatus As AddressStatus = previousStatus

        Select Case e.Result
            Case AddressValidationLevel.OK
                ' If the address was Good, it stays Good. If it was Probably"Something",
                ' it has now cleared our suspicions, congrats!
                If previousStatus <> AddressStatus.Good Then
                    newStatus = AddressStatus.Good
                End If
                Exit Select
            Case AddressValidationLevel.RegexCheck
                ' If the address failed regex check, it just had bad syntax.
                ' Does not make sense to give it a second chance.
                newStatus = AddressStatus.Bad
                Exit Select
            Case AddressValidationLevel.DnsQuery
                If previousStatus = AddressStatus.Good OrElse previousStatus = AddressStatus.ProbablyGreylisted Then
                    ' Give the address which was good before, a second chance. We also do this for
                    ' "probably greylisted" addresses because from DNS MX point of view they are good
                    ' (valid MX host, running SMTP service) and DNS error is something new for them,
                    ' so there is a chance it's a temporary issue.
                    newStatus = AddressStatus.ProbablyBad
                Else
                    ' The address had already been given second chance before and thus had ProbablyBad status.
                    ' Execute it. No mercy this time. But in real apps you better take this decision on at least
                    ' 3rd failed attempt, and at least 3 days should pass since the first attempt. Give
                    ' the e-mail service of the recipient some time to fix the issues they may be experiencing,
                    ' or the mailbox was just full and the user had no time to get some free space there.
                    newStatus = AddressStatus.Bad
                End If
                Exit Select
            Case AddressValidationLevel.SmtpConnection
                ' In this sample, we take the same decisions in SmtpConnection case like we do for DnsQuery.
                ' This is because the reasons are pretty similar in most cases. The domain could go down due
                ' to the name server issue, the same can happen with a SMTP MX e-mail server. This differs
                ' from the situation when the SMTP MX server does respond but says "No" (see below).
                If previousStatus = AddressStatus.Good OrElse previousStatus = AddressStatus.ProbablyGreylisted Then
                    newStatus = AddressStatus.ProbablyBad
                Else
                    newStatus = AddressStatus.Bad
                End If
                Exit Select
            Case AddressValidationLevel.SendAttempt
                ' Did the connection simply break or did the server actually say "No"?
                ' Connection break may indicate a temporary issue with the SMTP MX server.
                Dim negativeResponseEx As IMailBeeNegativeSmtpResponseException
                negativeResponseEx = TryCast(e.Reason, IMailBeeNegativeSmtpResponseException)
                Dim isNegativeReply As Boolean = (negativeResponseEx IsNot Nothing)

                ' For No errors, we need to make sure this is the final verdict of the SMTP MX server.
                ' If the error is transient rather than the final verdict, it leaves us a second chance to try.
                If isNegativeReply AndAlso Not negativeResponseEx.IsTransientError Then
                    ' The SMTP MX server which is charge for the given e-mail address said us strict "No".
                    ' We don't know the exact reason (maybe, your IP address is blacklisted by some RBLs),
                    ' but under the current circumstances the given e-mail address cannot receive e-mail from us.
                    newStatus = AddressStatus.Bad
                    Exit Select
                End If

                ' At this point, we have only temporary SMTP MX errors left, such as broken connections or
                ' greylisting ("Try again later" responses). For such errors, giving second chances does make sense.

                If previousStatus = AddressStatus.Good Then
                    If isNegativeReply Then
                        ' The server replied "No, but try again later". It's legit thing, so there is a good
                        ' chance there IS a real e-mail account under the given name there.
                        newStatus = AddressStatus.ProbablyGreylisted
                    Else
                        ' The server suddenly broke the connection. Give it one second chance, but no more.
                        newStatus = AddressStatus.ProbablyBad
                    End If
                ElseIf previousStatus = AddressStatus.ProbablyGreylisted Then
                    ' The server said "Try again later" or broke the connection?
                    If isNegativeReply Then
                        ' Greylisting returned the same result second time in a row. Maybe, it wasn't greylisting
                        ' at all? Give this e-mail address one more chance, but our patience is nearly over.
                        newStatus = AddressStatus.ProbablyBad
                    Else
                        ' Well, "If isNegativeReply" is actually redundant here. If the server broke the connection
                        ' instead of saying "Better luck next time", our assumption remains the same. There is
                        ' still a small chance that the server will accept the given address next time, so give it
                        ' one more shot to try. In your apps, however, you may consider different logic here.
                        newStatus = AddressStatus.ProbablyBad
                    End If
                Else
                    ' We're tired of all that "Try again later" occurring 3 times in a row and repetitive
                    ' disconnects during send attempts. Consider the e-mail address dead.
                    newStatus = AddressStatus.Bad
                End If
                Exit Select
        End Select

        If previousStatus <> newStatus Then
            ' Update the status in the data table. You may also need to do Commit, Update
            ' or whatever is required to apply the changes to your database.
            e.Row(StatusColumn) = newStatus
        End If

        Console.WriteLine()
    End Sub

    ' Dumps the contents of the data table to console. We use it how the statuses change
    ' over the time with making new attempts to verify the corresponding e-mails addresses.
    Private Shared Sub DisplayDataTable(ByVal data As DataTable)
        For Each row As DataRow In data.Rows
            Console.WriteLine(String.Format("{0}={1}", _
             row(EmailColumn).ToString(), CType(row(StatusColumn), AddressStatus).ToString()))
        Next
        Console.WriteLine()
    End Sub

    Shared Sub Main(ByVal args As String())
        ' Common initialization, like in previous samples.
        Dim valid As New EmailAddressValidator("MN100-0123456789ABCDEF-0123")
        valid.DnsServers.Autodetect()
        valid.Log.Enabled = True
        valid.Log.Format = LogFormatOptions.AddContextInfo
        valid.Log.Filename = "C:\Temp\log.txt"
        valid.Log.Clear()
        AddHandler valid.Verifying, AddressOf valid_Verifying
        AddHandler valid.Verified, AddressOf valid_Verified
        valid.MaxThreadCount = -1

        Dim data As DataTable = GetDataTable()

        valid.Verify(data, EmailColumn)
        DisplayDataTable(data)

        Console.WriteLine("Press any key to continue to the 2nd run")
        Console.ReadKey(True)

        valid.Verify(data, EmailColumn)
        DisplayDataTable(data)

        Console.WriteLine("Press any key to continue to the 3rd run")
        Console.ReadKey(True)

        valid.Verify(data, EmailColumn)
        DisplayDataTable(data)

        Console.WriteLine("Press any key to exit")
        Console.ReadKey(True)
    End Sub
End Class

Of course, if you're blacklisted, you'll get numerous false positives. Examining error messages from the servers may help to find out if it's true. For instance, a typical response can include Spamhaus hit - http://www.spamhaus.org/query/bl?ip=10.20.30.40, Please contact your Internet service provider since part of their network is on our block list., Denied by policy, etc.

MailBee cannot understand these text messages as there is no SMTP error code or anything like that to find out that you're black-listed. However, MailBee provides you with their text so you can make some assumptions based on it. The samples in this tutorial print all the error messages to the console so you can use them to learn how to access the error information.


Getting data from SqlDataReader or any other IDataReader

You can pass IDataReader instance instead of DataTable as data source. This can be handy if you're getting data from SQL table and cannot afford loading data from into DataTable (such as with DataTable.Load method) because your dataset is too big (such as millions of records).

Passing IDataReader is faster and consumes less memory as only one row at a time is loaded. But some limitations apply in this case if you're running the validation in multi-threaded mode. In event handlers, you cannot access data from IDataReader directly because other threads may already advance the reader further. However, when processing a row, MailBee copies the row fields from the current row for temporary use. If for some reason you need to access them in EmailAddressValidator events, use DataReaderRowValues property of Verifying event. The same property is available for Verified event as well.

AddressValidatorDemo WinForms sample project contains sophisticated logic utilizing SqlDataReader as a data source of e-mail addresses.


DNS servers, caching, local endpoints, SMTP settings

This topic highlights some advanced matters for specific needs.

To perform DNS MX lookup, Address Validator uses DNS servers you specified in DnsServers collection. Usually, you'll use Autodetect method to populate this collection. But you can add DNS servers there manually, if you wish. See Add method and its overloads.

By default, MailBee caches DNS MX information for the domains which have already been queried (see more in DnsQuery section of Understanding validation levels topic. DnsCache class provides a number of other options, see it reference for details. The most important thing about the cache is that it's shared by all the instances of EmailAddressValidator class if you create many of them (and even Smtp class if you send e-mail after validating addresses). This also improves performance.

If your machine has multiple network adapters, you may want to bind MailBee to certain IP address or port. This may improve performance as network packets will follow shorter route. Use LocalEndPoint property for that.

Pipelining property lets you disable SMTP pipelining  which provides better performance but can cause problems with some servers (although this occurs extremely rarely).

HelloDomain property lets you manually specify the domain argument for EHLO/EHLO commands. Setting it is highly recommended. If not set, MailBee will use the autodetected name or IP address of the current machine which in most cases will be local, not external. The hosts to which MailBee connects to are able to check only external domain name and compare it with EHLO argument, and it would be great to match that name. Name mismatch will increase the chance of being rejected by the target server.

SmtpTimeout can be increased if some SMTP MX servers you're dealing with are slow. Or, you can decrease it for debugging purposes to test how the app behaves when encounters sudden connection breaks.

Note that we're not dealing with SSL/TLS, authentication, and so on here. These matters do not apply to checking e-mail addresses. All connections between SMTP servers are made on plain port 25, without authentication (because servers do not know passwords of each other). Moreover, if you get "authentication required" in the logs, this means the given server cannot accept e-mail from you under the current circumstances (you're blacklisted, the account does not exist, or the MX record for the domain is incorrect and points to the SMTP relay server of that domain rather than to its MX SMTP server). No secure data is being transmitted either.


WinForms sample project

Sample projects which are installed in My Documents\MailBee.NET Objects\Samples\WinForms folder include AddressValidatorDemo project, available in both C# and VB versions, for .NET 2.0+ and .NET 4.0+. This project provides a complex example of bulk e-mail validation including multiple passes to refine results, blacklists and whitelists, MS SQL Server and file storage backends, live tracking of the operation's progress, dealing with multi-threading issues like race conditions and raising events from worker threads, and much more.


Send feedback to AfterLogic

Copyright © 2006-2017 AfterLogic Corporation. All rights reserved.