Dale Fraser
Dale Fraser

Reputation: 4758

Send mass emails from .NET

We have a fairly large application and part of that application requires the sending of around 300k emails a day.

Doing this in .net is insanely slow, to fix this we did it uses threads which has the effect of making the entire server come to a grind.

Im sure there are bigger apps out there sending more emails.

What is the best way to send a large volume of emails in a reasonable time frame.

Here is the code

Part 1 thread the send

System.Threading.Tasks.Task.Run(() =>
{
    System.Net.Mail.SmtpClient smtp = new System.Net.Mail.SmtpClient();
    Utility.sendTemplatedEmail(model.email, "[email protected]", "New Jobs!", fullHtml: fullHtml, checkUnsubscribed: false, smtpClient: smtp);
    smtp.Dispose();

    lock (syncObject) 
    {
        emailCounter++;
        writer.WriteLine(string.Format("sent and smtp client closed: {0} -- email number: {1}", DateTime.UtcNow.ToString("dd/MM/yyyy hh:mm:ss.fff tt"), emailCounter.ToString()));

        writer.Flush();
        if (emailCounter == numberOfGroupedMatches) 
        {
            writer.Close();
            System.IO.File.Move(Server.MapPath("~/Temp/") + "emailLog.txt", Server.MapPath("~/Temp/") + "emailLog-Finished.txt");
        }
    }
});

Part 2 actual send

fullHtml = (fullHtml == null ? ViewToString("~/View/Shared/_DocumentTemplate.cshtml", controllerContext, new emailModel { body = body, subject = subject }) : fullHtml);
messageMail.Body = fullHtml;
System.Net.Mail.SmtpClient smtp = (smtpClient == null ? new System.Net.Mail.SmtpClient() : smtpClient);
smtp.Send(messageMail);

Upvotes: 1

Views: 6303

Answers (2)

Yuval Itzchakov
Yuval Itzchakov

Reputation: 149538

Using threads to do async IO is unnecessary (most of time) and will cause your program to not really scale out well, as you're probably noticing right now.

First, im not sure why you're using threads if you eventually have to lock on a mutual object, that is usually a sign of a code smell, i suggest you think about that again.

A good idea would be to use SmtpClient.SendMailAsync with a combination of async-await feature of C#5:

public async Task SendSmtpMailAsync()
{
    fullHtml = (fullHtml == null ? ViewToString("~/View/Shared/_DocumentTemplate.cshtml", controllerContext, new emailModel { body = body, subject = subject }) : fullHtml);
    messageMail.Body = fullHtml;
    System.Net.Mail.SmtpClient smtp = smtpClient ?? new SmtpClient();
    await smtp.SendMailAsync(messageMail);
}

You can use it inside the first piece of code like this:

using (System.Net.Mail.SmtpClient smtp = new System.Net.Mail.SmtpClient())
{
    await Utility.SendTemplatedMailAsync(model.email, "[email protected]", "New Jobs!", fullHtml: fullHtml, checkUnsubscribed: false, smtpClient: smtp);
}

// Note i removed the lock from this piece of code. If you have to execute multiple async methods concurrently (using Task.WhenAll), then maybe it should be re-added 
emailCounter++;
writer.WriteLine(string.Format("sent and smtp client closed: {0} -- email number: {1}", DateTime.UtcNow.ToString("dd/MM/yyyy hh:mm:ss.fff tt"), emailCounter.ToString()));

writer.Flush();
if (emailCounter == numberOfGroupedMatches) 
{
    writer.Close();
    System.IO.File.Move(Server.MapPath("~/Temp/") + "emailLog.txt", Server.MapPath("~/Temp/") + "emailLog-Finished.txt");
}

Upvotes: 1

Ty H.
Ty H.

Reputation: 953

Assuming you are using the built in .Net System.Net.Mail.SmtpClient library, I suggest that you look into making your own or using another third party library. I also deal with large volume of email generated from a .Net Windows Service... and my mileage increased dramatically when I stopped letting Microsoft send my mail for me.

  1. The LumiSoft.Net.SMTP.Client is a reliable library, and performs well
  2. The following class is one I use as a "first choice", it is very lightweight... but may or may not work with the mail server you are sending to. In my case I pass all my SMTP traffic to SendGrid, so it saves me a lot of CPU cycles. Original author here: http://www.codeproject.com/Articles/9637/SMTP-MailMessage-done-right

    using System; using System.Collections; using System.Diagnostics; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions; using System.Net.Mail;

    namespace General.Utilities.Mail { public class SMTPSendRawMIME { #region Constructor public SMTPSendRawMIME() {

    }
    #endregion
    
    //The following methods are KNOWN to be flawed... they work only with some SMTP Servers... not many.... 
    //On the other hand, they run very fast... so they are worth using when possible like with SendGrid
    #region Send Raw Mime
    
    #region Static Send Methods
    public static void SendEmail(SmtpClient Server, string FromEmail, string ToEmail, byte[] MIMEMessage) //bool SSL, 
    {
        SMTPSendRawMIME objMailClient = new SMTPSendRawMIME();
        objMailClient.Send(Server, FromEmail, ToEmail, MIMEMessage);
    }
    
    public static void SendEmail(General.Utilities.Mail.MailTools.MailServerTypes enuUseServerType, string FromEmail, string ToEmail, byte[] MIMEMessage) //bool SSL, 
    {
        SmtpClient objServer = General.Utilities.Mail.MailTools.GetMailServer(enuUseServerType);
        SMTPSendRawMIME objMailClient = new SMTPSendRawMIME();
        objMailClient.Send(objServer, FromEmail, ToEmail, MIMEMessage);
    }
    #endregion
    
    public void Send(SmtpClient Server, string FromEmail, string ToEmail, byte[] MIMEMessage) //bool SSL, 
    {
        if (Server.Credentials != null)
        {
            //if (SSL)
                //SendSSL(Server.Host, Server.Port, ((NetworkCredential)Server.Credentials).UserName, ((NetworkCredential)Server.Credentials).Password, FromEmail, ToEmail, MIMEMessage);
            //else
                Send(Server.Host, Server.Port, ((NetworkCredential)Server.Credentials).UserName, ((NetworkCredential)Server.Credentials).Password, FromEmail, ToEmail, MIMEMessage);
        }
        else
        {
            SendToOpenRelay(Server.Host, Server.Port, FromEmail, ToEmail, MIMEMessage);
        }
    
    }
    
    #region BASE Send
    /// <summary>
    /// Sends the message via a socket connection to an SMTP relay host. 
    /// </summary>
    /// <param name="hostname">Friendly-name or IP address of SMTP relay host</param>
    /// <param name="port">Port on which to connect to SMTP relay host</param>
    /// <param name="username"></param>
    /// <param name="password"></param>
    public void Send(string HostName, int Port, string UserName, string Password, string FromEmail, string ToEmail, byte[] MIMEMessage)
    {
        const int bufsize = 1000;
        TcpClient smtp;
        NetworkStream ns;
        int cb, startOfBlock;
        byte[] recv = new byte[bufsize];
        byte[] data;
        string message, block;
    
        try
        {
    
            smtp = new TcpClient(HostName, Port);
            ns = smtp.GetStream();
            cb = ns.Read(recv, 0, recv.Length);
            message = Encoding.ASCII.GetString(recv);
            System.Diagnostics.Debug.WriteLine(message, "Server");
            message = "EHLO\r\n";
            System.Diagnostics.Debug.WriteLine(message, "Client");
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
        }
        catch(Exception ex)
        {
            throw new Exception(string.Format("Unable to establish SMTP session with {0}:{1}", HostName, Port), ex);
        }
    
        try
        {
    
            //figure out the line containing 250-AUTH
            cb = ns.Read(recv, 0, recv.Length);
            message = Encoding.ASCII.GetString(recv);
            //System.Diagnostics.Debug.WriteLine(message, "Server");
            startOfBlock = message.IndexOf("250-AUTH");
            block = message.Substring(startOfBlock, message.IndexOf("\n", startOfBlock) - startOfBlock);
            //check the auth protocols
            if (-1 == block.IndexOf("LOGIN"))
                throw new Exception("Mailhost does not support LOGIN authentication");
    
            message = "AUTH LOGIN\r\n";
            //System.Diagnostics.Debug.WriteLine(message, "Client");
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
            clearBuf(recv);
            cb = ns.Read(recv, 0, recv.Length);
            message = Encoding.ASCII.GetString(recv);
            //System.Diagnostics.Debug.WriteLine(message, "Server");
            if (!message.StartsWith("334"))
                throw new Exception(string.Format("Unexpected reply to AUTH LOGIN:\n{0}", message));
    
            message = string.Format("{0}\r\n", Convert.ToBase64String(Encoding.ASCII.GetBytes(UserName)));
            //System.Diagnostics.Debug.WriteLine(message, "Client (username)");
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
            clearBuf(recv);
            cb = ns.Read(recv, 0, recv.Length);
            message = Encoding.ASCII.GetString(recv);
            //System.Diagnostics.Debug.WriteLine(message, "Server");
            if (!message.StartsWith("334"))
                throw new Exception(string.Format("Unexpected reply to username:\n{0}", message));
    
            message = string.Format("{0}\r\n", Convert.ToBase64String(Encoding.ASCII.GetBytes(Password)));
            //System.Diagnostics.Debug.WriteLine(message, "Client (password)");
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
            clearBuf(recv);
            cb = ns.Read(recv, 0, recv.Length);
            message = Encoding.ASCII.GetString(recv);
            //System.Diagnostics.Debug.WriteLine(message, "Server");
            if (message.StartsWith("535"))
                throw new Exception("Authentication unsuccessful");
            if (!message.StartsWith("2"))
                throw new Exception(string.Format("Unexpected reply to password:\n{0}", message));
    
            message = string.Format("MAIL FROM: <{0}>\r\n", FromEmail);
            //System.Diagnostics.Debug.WriteLine(message, "Client");
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
            clearBuf(recv);
            cb = ns.Read(recv, 0, recv.Length);
            message = Encoding.ASCII.GetString(recv);
            //System.Diagnostics.Debug.WriteLine(message, "Server");
            if (!message.StartsWith("250"))
                throw new Exception(string.Format("Unexpected reply to MAIL FROM:\n{0}", message));
    
            message = string.Format("RCPT TO: <{0}>\r\n", ToEmail);
            string strRcptToMessage = message;
            //System.Diagnostics.Debug.WriteLine(message, "Client");
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
            clearBuf(recv);
            cb = ns.Read(recv, 0, recv.Length);
            message = Encoding.ASCII.GetString(recv);
            //System.Diagnostics.Debug.WriteLine(message, "Server");
            if (!message.StartsWith("250"))
                throw new Exception(string.Format("Unexpected reply to RCPT TO:\n{0}", message + " (" + strRcptToMessage + ")"));
    
            message = "DATA\r\n";
            //System.Diagnostics.Debug.WriteLine(message, "Client");
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
            clearBuf(recv);
            cb = ns.Read(recv, 0, recv.Length);
            message = Encoding.ASCII.GetString(recv);
            //System.Diagnostics.Debug.WriteLine(message, "Server");
            if (!message.StartsWith("354"))
                throw new Exception(string.Format("Unexpected reply to DATA:\n{0}", message));
    
            //message = payload + "\r\n.\r\n";
            //data = Encoding.ASCII.GetBytes(Encoding.UTF8.GetString(MIMEMessage) + "\r\n.\r\n");
            data = MIMEMessage;
            ns.Write(data, 0, data.Length);
    
            message = "\r\n.\r\n";
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
    
            clearBuf(recv);
            cb = ns.Read(recv, 0, recv.Length);
            message = Encoding.ASCII.GetString(recv);
            //System.Diagnostics.Debug.WriteLine(message, "Server");
            if (!message.StartsWith("250"))
                throw new Exception(string.Format("Unexpected reply to end of data marker (\\r\\n.\\r\\n):\n{0}", message));
    
            message = "QUIT\r\n";
            //System.Diagnostics.Debug.WriteLine(message, "Client");
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
    
        }
        catch (Exception ex)
        {
            General.Utilities.Debugging.Report.SendError("SMTP Communication Error", ex);
            throw new Exception(string.Format("SMTP Communication Error: {0}", message), ex);
        }
        finally
        {
            if (null != smtp) smtp.Close();
        }
    }
    #endregion
    
    #region BASE SendSSL
    /// <summary>
    /// Sends the message via a socket connection to an SMTP relay host. 
    /// </summary>
    /// <param name="hostname">Friendly-name or IP address of SMTP relay host</param>
    /// <param name="port">Port on which to connect to SMTP relay host</param>
    /// <param name="username"></param>
    /// <param name="password"></param>
    private void SendSSL(string HostName, int Port, string UserName, string Password, string FromEmail, string ToEmail, byte[] MIMEMessage)
    {
        //This Doesn't Work Yet... :(
    
        const int bufsize = 1000;
        TcpClient smtp;
        System.Net.Security.SslStream ns;
        int cb, startOfBlock;
        byte[] recv = new byte[bufsize];
        byte[] data;
        string message, block;
    
        try
        {
    
            smtp = new TcpClient(HostName, Port);
            ns = new System.Net.Security.SslStream(smtp.GetStream(), true);
            cb = ns.Read(recv, 0, recv.Length);
            message = Encoding.ASCII.GetString(recv);
            System.Diagnostics.Debug.WriteLine(message, "Server");
            message = "EHLO\r\n";
            System.Diagnostics.Debug.WriteLine(message, "Client");
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
        }
        catch(Exception ex)
        {
            throw new Exception(string.Format("Unable to establish SMTP session with {0}:{1}", HostName, Port), ex);
        }
    
        try
        {
    
            //figure out the line containing 250-AUTH
            cb = ns.Read(recv, 0, recv.Length);
            message = Encoding.ASCII.GetString(recv);
            System.Diagnostics.Debug.WriteLine(message, "Server");
            startOfBlock = message.IndexOf("250-AUTH");
            block = message.Substring(startOfBlock, message.IndexOf("\n", startOfBlock) - startOfBlock);
            //check the auth protocols
            if (-1 == block.IndexOf("LOGIN"))
                throw new Exception("Mailhost does not support LOGIN authentication");
    
            message = "AUTH LOGIN\r\n";
            System.Diagnostics.Debug.WriteLine(message, "Client");
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
            clearBuf(recv);
            cb = ns.Read(recv, 0, recv.Length);
            message = Encoding.ASCII.GetString(recv);
            System.Diagnostics.Debug.WriteLine(message, "Server");
            if (!message.StartsWith("334"))
                throw new Exception(string.Format("Unexpected reply to AUTH LOGIN:\n{0}", message));
    
            message = string.Format("{0}\r\n", Convert.ToBase64String(Encoding.ASCII.GetBytes(UserName)));
            System.Diagnostics.Debug.WriteLine(message, "Client (username)");
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
            clearBuf(recv);
            cb = ns.Read(recv, 0, recv.Length);
            message = Encoding.ASCII.GetString(recv);
            System.Diagnostics.Debug.WriteLine(message, "Server");
            if (!message.StartsWith("334"))
                throw new Exception(string.Format("Unexpected reply to username:\n{0}", message));
    
            message = string.Format("{0}\r\n", Convert.ToBase64String(Encoding.ASCII.GetBytes(Password)));
            System.Diagnostics.Debug.WriteLine(message, "Client (password)");
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
            clearBuf(recv);
            cb = ns.Read(recv, 0, recv.Length);
            message = Encoding.ASCII.GetString(recv);
            System.Diagnostics.Debug.WriteLine(message, "Server");
            if (message.StartsWith("535"))
                throw new Exception("Authentication unsuccessful");
            if (!message.StartsWith("2"))
                throw new Exception(string.Format("Unexpected reply to password:\n{0}", message));
    
            message = string.Format("MAIL FROM: <{0}>\r\n", FromEmail);
            System.Diagnostics.Debug.WriteLine(message, "Client");
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
            clearBuf(recv);
            cb = ns.Read(recv, 0, recv.Length);
            message = Encoding.ASCII.GetString(recv);
            System.Diagnostics.Debug.WriteLine(message, "Server");
            if (!message.StartsWith("250"))
                throw new Exception(string.Format("Unexpected reply to MAIL FROM:\n{0}", message));
    
            message = string.Format("RCPT TO: <{0}>\r\n", ToEmail);
            System.Diagnostics.Debug.WriteLine(message, "Client");
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
            clearBuf(recv);
            cb = ns.Read(recv, 0, recv.Length);
            message = Encoding.ASCII.GetString(recv);
            System.Diagnostics.Debug.WriteLine(message, "Server");
            if (!message.StartsWith("250"))
                throw new Exception(string.Format("Unexpected reply to RCPT TO:\n{0}", message));
    
            message = "DATA\r\n";
            System.Diagnostics.Debug.WriteLine(message, "Client");
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
            clearBuf(recv);
            cb = ns.Read(recv, 0, recv.Length);
            message = Encoding.ASCII.GetString(recv);
            System.Diagnostics.Debug.WriteLine(message, "Server");
            if (!message.StartsWith("354"))
                throw new Exception(string.Format("Unexpected reply to DATA:\n{0}", message));
    
            data = MIMEMessage;
            ns.Write(data, 0, data.Length);
            message = "\r\n.\r\n";
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
    
            clearBuf(recv);
            cb = ns.Read(recv, 0, recv.Length);
            message = Encoding.ASCII.GetString(recv);
            System.Diagnostics.Debug.WriteLine(message, "Server");
            if (!message.StartsWith("250"))
                throw new Exception(string.Format("Unexpected reply to end of data marker (\\r\\n.\\r\\n):\n{0}", message));
    
            message = "QUIT\r\n";
            System.Diagnostics.Debug.WriteLine(message, "Client");
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
    
        }
        catch (Exception ex)
        {
            throw new Exception(string.Format("SMTP Communication Error: {0}", message), ex);
        }
        finally
        {
            if (null != smtp) smtp.Close();
        }
    }
    #endregion
    
    #region BASE SendToOpenRelay
    /// <summary>
    /// Sends the message via a socket connection to an SMTP relay host. 
    /// </summary>
    /// <param name="hostname">Friendly-name or IP address of SMTP relay host</param>
    /// <param name="port">Port on which to connect to SMTP relay host</param>
    public void SendToOpenRelay(string HostName, int Port, string FromEmail, string ToEmail, byte[] MIMEMessage)
    {
    
        TcpClient smtp;
        NetworkStream ns;
        int cb;
        byte[] recv = new byte[256];
        byte[] data;
        string message;
    
        try
        {
            smtp = new TcpClient(HostName, Port);
            ns = smtp.GetStream();
            message = "HELO\r\n";
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
            cb = ns.Read(recv, 0, recv.Length);
            System.Diagnostics.Debug.WriteLine(Encoding.ASCII.GetString(recv), "Server");
        }
        catch(Exception ex)
        {
            throw new Exception(string.Format("Unable to establish SMTP session with {0}:{1}", HostName, Port), ex);
        }
    
        try
        {
            message = string.Format("MAIL FROM: <{0}>\r\n", FromEmail);
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
            cb = ns.Read(recv, 0, recv.Length);
            if (!Convert.ToString(recv).StartsWith("501"))
                throw new Exception("Malformed sender address");
            if (!Convert.ToString(recv).StartsWith("250"))
                throw new Exception(string.Format("SMTP host responded incorrectly to MAIL FROM:, response was:\n{0}", Convert.ToString(recv)));
            message = string.Format("RCPT TO: <{0}>\r\n", ToEmail);
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
            cb = ns.Read(recv, 0, recv.Length);
            if (Convert.ToString(recv).StartsWith("501"))
                throw new Exception("Malformed recipient address");
            if (!Convert.ToString(recv).StartsWith("250"))
                throw new Exception(string.Format("SMTP host responded incorrectly to RCPT TO:, response was:\n{0}", Convert.ToString(recv)));
            message = "DATA\r\n";
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
            cb = ns.Read(recv, 0, recv.Length);
            if (!Convert.ToString(recv).StartsWith("354"))
                throw new Exception(string.Format("SMTP host responded incorrectly to DATA, response was:\n{0}", Convert.ToString(recv)));
            data = MIMEMessage;
            ns.Write(data, 0, data.Length);
            message = "\r\n.\r\n";
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
            cb = ns.Read(recv, 0, recv.Length);
            message = "QUIT\r\n";
            data = Encoding.ASCII.GetBytes(message);
            ns.Write(data, 0, data.Length);
        }
        catch (Exception ex)
        {
    
    
    
                throw new Exception(string.Format("SMTP Communication Error: {0}", message), ex);
            }
            finally
            {
                if (null != smtp) smtp.Close();
            }
        }
        #endregion
    
        #region clearBuf
        private void clearBuf(byte[] buf)
        {
            for (int i = 0; i < buf.Length; i++) buf[i] = 0;
        }
        #endregion
    
        #endregion
    
    }
    

    }

Upvotes: 1

Related Questions