Build a C# DNS MX (Mail Exchange) Record Query Class by Peter A. Bromberg, Ph.D.

Peter Bromberg

 

The .NET Framework provides wrappers to native APIs for a very large number of classes and functions, including the System.Net.Dns classes. However, it doesn't cover all of them. One useful API is the set of DNS query methods in the Dnsapi.dll library, and a particularly useful technique of note is the ability to query a domain and get back the list of MX (Mail Exchange) records of the mail servers associated with the domain.



Once you have the code completed to handle this, you will be able to validate an email address against its domain for the mail exchange ("MX") type entries. If there aren't any, then you have a pretty good idea that the email address you are dealing with is BOGUS. Obviously, this can be helpful in tasks such as verifying user registrations on your web site and to fight SPAM. The implementation I provide here is a simplified and very "narrow" one that is intended solely to deal with the MX record lookup. If you want a more complete implementation, there are a number of examples in the Net-o-sphere. One interesting one can be found here.

For starters, it might be wise to visit the Platform SDK section on DNS to familiarize ourselves with what we need to create a managed C# PInvoke call to the underlying method.

As can be seen, there are three DnsQuery calls, with DnsQuery_W (Unicode) being the most commonly used. There is an fOptions parameter where the types of queries can be combined, the wType type of record parameter, and a ppQueryResultsSet [in, out] pointer parameter.

Finally, we are told that we need to free the returned RR Sets with the DnsRecordListFree API call.

Note that in the sample code implementation below, VBByRefStr is used for the marshaling directive. The argument is treated as "byref" on the managed side, but stays "byval" on the unmanaged side.  If the unmanaged side writes into the buffer, it won't mangle the managed string, which remains immutable.  Instead, a different string is propagated to the managed side, preserving managed string immutability. In VB.Net, you could code as if the VBByRefStr is actually byval on the managed side, and the compiler will clean up after you, but in C# you must explicitly add the "ref" keyword in the right places:

namespace PAB.DnsUtils
{
    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.Runtime.InteropServices;

    public class DnsMx
    {        
        public DnsMx()
        {
        }
        [DllImport("dnsapi", EntryPoint="DnsQuery_W", CharSet=CharSet.Unicode, SetLastError=true, ExactSpelling=true)]
        private static extern int DnsQuery([MarshalAs(UnmanagedType.VBByRefStr)]ref string pszName, 
QueryTypes wType, QueryOptions options, int aipServers, ref IntPtr ppQueryResults, int pReserved); [DllImport("dnsapi", CharSet=CharSet.Auto, SetLastError=true)] private static extern void DnsRecordListFree(IntPtr pRecordList, int FreeType); public static string[] GetMXRecords(string domain) { IntPtr ptr1=IntPtr.Zero ; IntPtr ptr2=IntPtr.Zero ; MXRecord recMx; if (Environment.OSVersion.Platform != PlatformID.Win32NT) { throw new NotSupportedException(); } ArrayList list1 = new ArrayList(); int num1 = DnsMx.DnsQuery(ref domain, QueryTypes.DNS_TYPE_MX,
QueryOptions.DNS_QUERY_BYPASS_CACHE, 0, ref ptr1, 0); if (num1 != 0) { throw new Win32Exception(num1); } for (ptr2 = ptr1; !ptr2.Equals(IntPtr.Zero); ptr2 = recMx.pNext) { recMx = ( MXRecord) Marshal.PtrToStructure(ptr2, typeof(MXRecord)); if (recMx.wType == 15) { string text1 = Marshal.PtrToStringAuto(recMx.pNameExchange); list1.Add(text1); } } DnsMx.DnsRecordListFree(ptr1, 0); return (string[]) list1.ToArray(typeof(string)); } private enum QueryOptions { DNS_QUERY_ACCEPT_TRUNCATED_RESPONSE = 1, DNS_QUERY_BYPASS_CACHE = 8, DNS_QUERY_DONT_RESET_TTL_VALUES = 0x100000, DNS_QUERY_NO_HOSTS_FILE = 0x40, DNS_QUERY_NO_LOCAL_NAME = 0x20, DNS_QUERY_NO_NETBT = 0x80, DNS_QUERY_NO_RECURSION = 4, DNS_QUERY_NO_WIRE_QUERY = 0x10, DNS_QUERY_RESERVED = -16777216, DNS_QUERY_RETURN_MESSAGE = 0x200, DNS_QUERY_STANDARD = 0, DNS_QUERY_TREAT_AS_FQDN = 0x1000, DNS_QUERY_USE_TCP_ONLY = 2, DNS_QUERY_WIRE_ONLY = 0x100 } private enum QueryTypes { DNS_TYPE_MX = 15 } [StructLayout(LayoutKind.Sequential)] private struct MXRecord { public IntPtr pNext; public string pName; public short wType; public short wDataLength; public int flags; public int dwTtl; public int dwReserved; public IntPtr pNameExchange; public short wPreference; public short Pad; } } }

The downloadable sample solution includes a simple Console Tester app that makes the following call:

static void Main(string[] args)
{
string[] s= PAB.DnsUtils.DnsMx.GetMXRecords("microsoft.com");
foreach (string st in s)
Console.WriteLine("Server: {0}",st);
Console.ReadLine();

which will return the following output:

Remember, hamburger@hamburgerhal.com is a valid email address. But, that doesn't mean that you can actually send mail to it. This handy class, along with some good REGEX match strings, should be sufficient to validate most any email address that can be thrown at you. The downloadable solution has more user-friendly exception handling built in.

 

Download the Visual Studio.NET Solution that accompanies this article

 



Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.
Article Discussion: