BillForney.com

A day in the life of a geek...

A better direct SMTP mailer

Wayne posted an interesting little SMTP snippet recently and I had been poking around at that, so I thought I'd share this... It is an adaptation of several articles scattered around through the years, his code, and several other things I wrote a while ago. Enjoy...

namespace SMTP
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.IO;
    using System.Net;
    using System.Net.Mail;
    using System.Net.Sockets;
    using System.Runtime.InteropServices;
    using System.Text;
    using System.Text.RegularExpressions;
    using System.Threading;

    /// <summary>
    /// provides methods to send email via smtp direct to mail server
    /// </summary>
    public class SmtpDirect
    {
        /// <summary>
        /// Determines whether the specified input email is email.
        /// </summary>
        /// <param name="inputEmail">The input email.</param>
        /// <param name="fromEmail">From email.</param>
        /// <param name="tryLookup">if set to <c>true</c> [try lookup].</param>
        /// <param name="tryConnect">if set to <c>true</c> [try connect].</param>
        /// <returns>
        ///     <c>true</c> if the specified input email is email; otherwise, <c>false</c>.
        /// </returns>
        public static bool IsEmail(string inputEmail, string fromEmail, bool tryLookup, bool tryConnect)
        {
            // WLF: Check for null or empty input

            if (string.IsNullOrEmpty(inputEmail))
            {
                return false;
            }

            // WLF: RegEx check

            const string strRegex = @"^(([a-zA-Z0-9_\-\+/\^]+)([\.]?)([a-zA-Z0-9_\-\+/\^]+))+@((\[[0-9]{1,3}" +
                                    @"\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\" +
                                    @".)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$";
            var re = new Regex(strRegex);

            if (!re.IsMatch(inputEmail))
            {
                return false;
            }

            // WWB: Check To Make Sure The Email Address Parses

            MailAddress mailAddress;
            try
            {
                mailAddress = new MailAddress(inputEmail);
            }
            catch
            {
                return false;
            }

            if (tryLookup)
            {
                // WWB: Check To Make Sure There Is a SMTP Server To Recieve The Email Address

                if (DnsMx.GetMXRecords(mailAddress.Host).Length == 0)
                {
                    return false;
                }
            }

            if (tryConnect)
            {
                // WLF: Try to connect to the SMTP server

                try
                {
                    var mxRecords = DnsMx.GetMXRecords(mailAddress.Host);
                    if (mxRecords.Length == 0)
                    {
                        return false;
                    }

                    var ipHostEntry = Dns.GetHostEntry(mxRecords[0]);
                    var endPoint = new IPEndPoint(ipHostEntry.AddressList[0], 25);
                    var s = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
                    s.Connect(endPoint);

                    // WLF: Attempting to connect

                    if (!CheckResponse(s, SmtpResponse.ConnectSuccess))
                    {
                        s.Close();
                        return false;
                    }

                    // WLF: HELO server

                    Senddata(s, string.Format("HELO {0}\r\n", Dns.GetHostName()));
                    if (!CheckResponse(s, SmtpResponse.GenericSuccess))
                    {
                        s.Close();
                        return false;
                    }

                    // WLF: Say who it is from 

                    Senddata(s, string.Format("MAIL From: {0}\r\n", fromEmail));
                    if (!CheckResponse(s, SmtpResponse.GenericSuccess))
                    {
                        s.Close();
                        return false;
                    }

                    // WLF: Try to send 

                    Senddata(s, string.Format("RCPT TO: {0}\r\n", inputEmail));
                    if (!CheckResponse(s, SmtpResponse.GenericSuccess))
                    {
                        s.Close();
                        return false;
                    }
                }
                catch
                {
                    return false;
                }
            }

            // WWB: Success

            return true;
        }

        /// <summary>
        /// Sends the mail directly to the receiver's server.
        /// </summary>
        /// <param name="message">The message.</param>
        /// <param name="errors">The errors.</param>
        /// <returns></returns>
        public static bool SendDirect(MailMessage message, out List<string> errors)
        {
            errors = new List<string>();
            var deliveryAddresses = new List<MailAddress>();

            // WLF: Add To's
            foreach (var address in message.To)
            {
                deliveryAddresses.Add(address);
            }

            // WLF: Add CC's
            foreach (var address in message.CC)
            {
                deliveryAddresses.Add(address);
            }

            foreach (var to in deliveryAddresses)
            {
                var mxRecords = DnsMx.GetMXRecords(to.Host);
                if (mxRecords.Length == 0)
                {
                    errors.Add(to.Host);
                }
                else
                {
                    var ipHostEntry = Dns.GetHostEntry(mxRecords[0]);
                    var endPoint = new IPEndPoint(ipHostEntry.AddressList[0], 25);
                    var s = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
                    s.Connect(endPoint);

                    if (!CheckResponse(s, SmtpResponse.ConnectSuccess))
                    {
                        s.Close();
                        return false;
                    }

                    Senddata(s, string.Format("HELO {0}\r\n", Dns.GetHostName()));
                    if (!CheckResponse(s, SmtpResponse.GenericSuccess))
                    {
                        s.Close();
                        return false;
                    }

                    Senddata(s, string.Format("MAIL From: {0}\r\n", message.From));
                    if (!CheckResponse(s, SmtpResponse.GenericSuccess))
                    {
                        s.Close();
                        return false;
                    }

                    Senddata(s, string.Format("RCPT TO: {0}\r\n", to.Address));
                    if (!CheckResponse(s, SmtpResponse.GenericSuccess))
                    {
                        s.Close();
                        return false;
                    }

                    #region Build message body

                    var header = GetHeader(message);

                    var msgBody = message.Body;
                    if (!msgBody.EndsWith("\r\n"))
                        msgBody += "\r\n";

                    #region Handle attachments

                    if (message.Attachments.Count > 0)
                    {
                        header.Append("MIME-Version: 1.0\r\n");
                        header.Append("Content-Type: multipart/mixed; boundary=unique-boundary-1\r\n");
                        header.Append("\r\n");
                        header.Append("This is a multi-part message in MIME format.\r\n");
                        var sb = new StringBuilder();
                        sb.Append("--unique-boundary-1\r\n");
                        sb.Append("Content-Type: text/plain\r\n");
                        sb.Append("Content-Transfer-Encoding: 7Bit\r\n");
                        sb.Append("\r\n");
                        sb.Append(msgBody + "\r\n");
                        sb.Append("\r\n");

                        foreach (var a in message.Attachments)
                        {
                            if (a == null) continue;

                            var f = new FileInfo(a.Name);
                            sb.Append("--unique-boundary-1\r\n");
                            sb.Append("Content-Type: application/octet-stream; file=" + f.Name + "\r\n");
                            sb.Append("Content-Transfer-Encoding: base64\r\n");
                            sb.Append("Content-Disposition: attachment; filename=" + f.Name + "\r\n");
                            sb.Append("\r\n");
                            var fs = f.OpenRead();
                            var binaryData = new Byte[fs.Length];
                            long bytesRead = fs.Read(binaryData, 0, (int) fs.Length);
                            Trace.WriteLine(string.Format("Bytes read: {0}", bytesRead));
                            fs.Close();
                            var base64String = Convert.ToBase64String(binaryData, 0, binaryData.Length);

                            for (var i = 0; i < base64String.Length;)
                            {
                                var nextchunk = 100;
                                if (base64String.Length - (i + nextchunk) < 0)
                                    nextchunk = base64String.Length - i;
                                sb.Append(base64String.Substring(i, nextchunk));
                                sb.Append("\r\n");
                                i += nextchunk;
                            }
                            sb.Append("\r\n");
                        }

                        msgBody = sb.ToString();
                    }

                    #endregion

                    #endregion

                    #region Send message body

                    Senddata(s, ("DATA\r\n"));
                    if (!CheckResponse(s, SmtpResponse.DataSuccess))
                    {
                        s.Close();
                        return false;
                    }
                    header.Append("\r\n");
                    header.Append(msgBody);
                    header.Append(".\r\n");
                    header.Append("\r\n");
                    header.Append("\r\n");
                    Senddata(s, header.ToString());
                    if (!CheckResponse(s, SmtpResponse.GenericSuccess))
                    {
                        s.Close();
                        return false;
                    }

                    Senddata(s, "QUIT\r\n");
                    CheckResponse(s, SmtpResponse.QuitSuccess);
                    s.Close();

                    #endregion
                }
            }

            return errors.Count == 0;
        }

        /// <summary>
        /// Sends the specified message.
        /// </summary>
        /// <param name="message">The message.</param>
        /// <param name="outboundSmtpServer">The outbound SMTP server.</param>
        /// <returns></returns>
        public static bool Send(MailMessage message, string outboundSmtpServer)
        {
            var ipHostEntry = Dns.GetHostEntry(outboundSmtpServer);
            var endPoint = new IPEndPoint(ipHostEntry.AddressList[0], 25);
            var s = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            s.Connect(endPoint);

            if (!CheckResponse(s, SmtpResponse.ConnectSuccess))
            {
                s.Close();
                return false;
            }

            Senddata(s, string.Format("HELO {0}\r\n", Dns.GetHostName()));
            if (!CheckResponse(s, SmtpResponse.GenericSuccess))
            {
                s.Close();
                return false;
            }

            Senddata(s, string.Format("MAIL From: {0}\r\n", message.From));
            if (!CheckResponse(s, SmtpResponse.GenericSuccess))
            {
                s.Close();
                return false;
            }

            foreach (var to in message.To)
            {
                Senddata(s, string.Format("RCPT TO: {0}\r\n", to.Address));
                if (!CheckResponse(s, SmtpResponse.GenericSuccess))
                {
                    s.Close();
                    return false;
                }
            }

            foreach (var cc in message.CC)
            {
                Senddata(s, string.Format("RCPT TO: {0}\r\n", cc.Address));
                if (!CheckResponse(s, SmtpResponse.GenericSuccess))
                {
                    s.Close();
                    return false;
                }
            }

            var header = GetHeader(message);

            var msgBody = message.Body;
            if (!msgBody.EndsWith("\r\n"))
                msgBody += "\r\n";

            #region Handle attachments

            if (message.Attachments.Count > 0)
            {
                header.Append("MIME-Version: 1.0\r\n");
                header.Append("Content-Type: multipart/mixed; boundary=unique-boundary-1\r\n");
                header.Append("\r\n");
                header.Append("This is a multi-part message in MIME format.\r\n");
                var sb = new StringBuilder();
                sb.Append("--unique-boundary-1\r\n");
                sb.Append("Content-Type: text/plain\r\n");
                sb.Append("Content-Transfer-Encoding: 7Bit\r\n");
                sb.Append("\r\n");
                sb.Append(msgBody + "\r\n");
                sb.Append("\r\n");

                foreach (var a in message.Attachments)
                {
                    if (a == null) continue;

                    var f = new FileInfo(a.Name);
                    sb.Append("--unique-boundary-1\r\n");
                    sb.Append("Content-Type: application/octet-stream; file=" + f.Name + "\r\n");
                    sb.Append("Content-Transfer-Encoding: base64\r\n");
                    sb.Append("Content-Disposition: attachment; filename=" + f.Name + "\r\n");
                    sb.Append("\r\n");
                    var fs = f.OpenRead();
                    var binaryData = new Byte[fs.Length];
                    long bytesRead = fs.Read(binaryData, 0, (int) fs.Length);
                    Trace.WriteLine(string.Format("Bytes read: {0}", bytesRead));
                    fs.Close();
                    var base64String = Convert.ToBase64String(binaryData, 0, binaryData.Length);

                    for (var i = 0; i < base64String.Length;)
                    {
                        var nextchunk = 100;
                        if (base64String.Length - (i + nextchunk) < 0)
                            nextchunk = base64String.Length - i;
                        sb.Append(base64String.Substring(i, nextchunk));
                        sb.Append("\r\n");
                        i += nextchunk;
                    }
                    sb.Append("\r\n");
                }

                msgBody = sb.ToString();
            }

            #endregion

            Senddata(s, ("DATA\r\n"));
            if (!CheckResponse(s, SmtpResponse.DataSuccess))
            {
                s.Close();
                return false;
            }
            header.Append("\r\n");
            header.Append(msgBody);
            header.Append(".\r\n");
            header.Append("\r\n");
            header.Append("\r\n");
            Senddata(s, header.ToString());
            if (!CheckResponse(s, SmtpResponse.GenericSuccess))
            {
                s.Close();
                return false;
            }

            Senddata(s, "QUIT\r\n");
            CheckResponse(s, SmtpResponse.QuitSuccess);
            s.Close();
            return true;
        }

        /// <summary>
        /// Gets the header.
        /// </summary>
        /// <param name="message">The message.</param>
        /// <returns></returns>
        private static StringBuilder GetHeader(MailMessage message)
        {
            var header = new StringBuilder();

            header.Append("From: " + message.From + "\r\n");

            var tos = string.Empty;
            foreach (var to in message.To)
            {
                tos = to.Address + ",";
            }
            header.AppendFormat("To: {0}\r\n", tos.TrimEnd(','));

            if (message.CC.Count > 0)
            {
                var ccs = string.Empty;
                foreach (var cc in message.CC)
                {
                    ccs = cc.Address + ",";
                }
                header.AppendFormat("Cc: {0}\r\n", ccs.TrimEnd(','));
            }

            header.AppendFormat("Date: {0}\r\n", DateTime.Now.ToString("ddd, d M y H:m:s z"));
            header.AppendFormat("Subject: {0}\r\n", message.Subject);
            header.Append("X-Mailer: SMTPDirect v1\r\n");
            return header;
        }

        #region Socket Operations

        /// <summary>
        /// Sends data to the specified socket.
        /// </summary>
        /// <param name="socket">The socket.</param>
        /// <param name="message">The message.</param>
        private static void Senddata(Socket socket, string message)
        {
            var messageBytes = Encoding.ASCII.GetBytes(message);
            socket.Send(messageBytes, 0, messageBytes.Length, SocketFlags.None);
        }

        /// <summary>
        /// Checks the response.
        /// </summary>
        /// <param name="socket">The socket.</param>
        /// <param name="responseExpected">The response expected.</param>
        /// <returns></returns>
        private static bool CheckResponse(Socket socket, SmtpResponse responseExpected)
        {
            var bytes = new byte[1024];
            while (socket.Available == 0)
            {
                Thread.Sleep(100);
            }

            socket.Receive(bytes, 0, socket.Available, SocketFlags.None);
            var sResponse = Encoding.ASCII.GetString(bytes);
            var response = Convert.ToInt32(sResponse.Substring(0, 3));
            return response == (int) responseExpected;
        }

        #endregion

        #region DNS

        /// <summary>
        /// DNS MX Lookup
        /// </summary>
        public class DnsMx
        {
            /// <summary>
            /// Executes a DNS Query
            /// </summary>
            /// <param name="pszName">Name of the PSZ.</param>
            /// <param name="wType">Type of the query.</param>
            /// <param name="options">The query options.</param>
            /// <param name="aipServers">The aip servers.</param>
            /// <param name="ppQueryResults">The pp query results.</param>
            /// <param name="pReserved">The p reserved.</param>
            /// <returns></returns>
            [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);

            /// <summary>
            /// Grabs the DNS Record List
            /// </summary>
            /// <param name="pRecordList">The p record list.</param>
            /// <param name="FreeType">Type of the free.</param>
            [DllImport("dnsapi", CharSet = CharSet.Auto, SetLastError = true)]
            private static extern void DnsRecordListFree(IntPtr pRecordList, int FreeType);

            /// <summary>
            /// Gets the MX records.
            /// </summary>
            /// <param name="domain">The domain.</param>
            /// <returns></returns>
            public static string[] GetMXRecords(string domain)
            {
                var ptr1 = IntPtr.Zero;
                IntPtr ptr2;
                MXRecord recMx;

                if (Environment.OSVersion.Platform != PlatformID.Win32NT)
                {
                    throw new NotSupportedException();
                }

                var list1 = new List<string>();
                var num1 = DnsQuery(ref domain, QueryTypes.DnsTypeMX, QueryOptions.DnsQueryBypassCache, 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) continue;

                    var text1 = Marshal.PtrToStringAuto(recMx.PNameExchange);
                    list1.Add(text1);
                }

                DnsRecordListFree(ptr1, 0);
                return list1.ToArray();
            }

            #region Nested type: MXRecord

            [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;
            }

            #endregion

            #region Nested type: QueryOptions

            /// <summary>
            /// Query Options
            /// </summary>
            private enum QueryOptions
            {
                //DnsQueryAcceptTruncatedResponse = 1,
                /// <summary>
                /// DNS Query Bypass Cache
                /// </summary>
                DnsQueryBypassCache = 8,
                //DnsQueryDontResetTtlValues = 0x100000,
                //DnsQueryNoHostsFile = 0x40,
                //DnsQueryNoLocalName = 0x20,
                //DnsQueryNoNetbt = 0x80,
                //DnsQueryNoRecursion = 4,
                //DnsQueryNoWireQuery = 0x10,
                //DnsQueryReserved = -16777216,
                //DnsQueryReturnMessage = 0x200,
                //DnsQueryStandard = 0,
                //DnsQueryTreatAsFqdn = 0x1000,
                //DnsQueryUseTcpOnly = 2,
                //DnsQueryWireOnly = 0x100
            }

            #endregion

            #region Nested type: QueryTypes

            /// <summary>
            /// Query Types
            /// </summary>
            private enum QueryTypes
            {
                //DnsTypeA = 1,
                //DnsTypeNs = 2,
                //DnsTypeCname = 5,
                //DnsTypeSoa = 6,
                //DnsTypePtr = 12,
                //DnsTypeHinfo = 13,
                /// <summary>
                /// DNS Type MX
                /// </summary>
                DnsTypeMX = 15,
                //DnsTypeTxt = 16,
                //DnsTypeAaaa = 28
            }

            #endregion
        }

        #endregion

        #region Nested type: SmtpResponse

        /// <summary>
        /// Smtp Response
        /// </summary>
        private enum SmtpResponse
        {
            /// <summary>
            /// Connect Success
            /// </summary>
            ConnectSuccess = 220,
            /// <summary>
            /// Generic Success
            /// </summary>
            GenericSuccess = 250,
            /// <summary>
            /// Data Success
            /// </summary>
            DataSuccess = 354,
            /// <summary>
            /// Quit Success
            /// </summary>
            QuitSuccess = 221
        }

        #endregion
    }
}