Skip to main content

Sends bulk email messages using Tasks Parallel.ForEach.

using System;
using System.Collections.Generic;
using System.Net.Mail;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Messenger
{
    /// <summary>
    /// Sends bulk email messages using Tasks Parallel.ForEach.
    ///
    /// Partially taken from:
    /// https://talkdotnet.wordpress.com/2014/03/20/sending-bulk-emails-with-tasks-parallel-foreach/
    /// </summary>
    public class Mailer
    {
        private readonly SmtpClient[] _smtpClients;

        private CancellationTokenSource _canellationTokenSource;

        /// <summary>
        /// Initializes a new instance of the <see cref="Mailer" /> class.
        /// </summary>
        /// <param name="smtpClientCount">The number of SMTP connections
        /// to create to the server.</param>
        public Mailer(int smtpClientCount)
        {
            SmtpClientCount = smtpClientCount;
            _smtpClients = new SmtpClient[smtpClientCount + 1];

            CreateSmtpClients();
        }

        /// <summary>
        /// Create "x" number SMTP connections to the server. (recommended 15)
        /// You don't want indicate some sort of DoS (Denial-of-service) attack
        /// by creating new SMTP connections for every email being sent.
        /// </summary>
        /// <value>The SMTP client count.</value>
        public int SmtpClientCount { get; private set; }

        public void Run(List<MailRecipient> recipients)
        {
            try
            {
                var parallelOptions = new ParallelOptions();

                // Create a cancellation token so you can cancel the task.
                _canellationTokenSource = new CancellationTokenSource();
                parallelOptions.CancellationToken = _canellationTokenSource.Token;

                // Manage the MaxDegreeOfParallelism instead of .NET Managing this.
                // We dont need 500 threads spawning for this.
                parallelOptions.MaxDegreeOfParallelism = System.Environment.ProcessorCount * 2;

                try
                {
                    Parallel.ForEach(recipients, parallelOptions, (MailRecipient recipient) =>
                    {
                        try
                        {
                            var mailMessage = new MailMessage();
                            mailMessage.To.Add(recipient.EmailAddress);
                            mailMessage.Subject = recipient.Subject;
                            mailMessage.Body = recipient.MessageBody;
                            mailMessage.BodyEncoding = Encoding.UTF8;
                            mailMessage.IsBodyHtml = true;
                            mailMessage.Priority = MailPriority.Normal;

                            SendMessage(mailMessage);
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine(ex.Message);
                        }
                    });
                }
                catch (OperationCanceledException ex)
                {
                    //User has cancelled this request.
                    Console.WriteLine(ex.Message);
                }
            }
            finally
            {
                DisposeSmtpClients();
            }
        }

        public void CancelRun()
        {
            _canellationTokenSource.Cancel();
        }

        private void SendMessage(MailMessage msg)
        {
            bool isLocked = false;

            while (!isLocked)
            {
                // Keep looping through all smtp client connections
                // until one becomes available:
                for (int i = 0; i <= SmtpClientCount; i++)
                {
                    if (System.Threading.Monitor.TryEnter(_smtpClients[i]))
                    {
                        try
                        {
                            _smtpClients[i].Send(msg);
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine(ex.Message);
                        }
                        finally
                        {
                            System.Threading.Monitor.Exit(_smtpClients[i]);
                        }

                        isLocked = true;

                        // TODO: might not be correct. Was : Exit For
                        break;
                    }
                }

                // make sure CPU doesn't ramp up to 100%:
                System.Threading.Thread.Sleep(1);
            }

            if (msg != null)
            {
                msg.Dispose();
            }
        }

        private void CreateSmtpClients()
        {
            for (int i = 0; i <= SmtpClientCount; i++)
            {
                var smtpClient = new SmtpClient();

                _smtpClients[i] = smtpClient;
            }
        }

        private void DisposeSmtpClients()
        {
            for (int i = 0; i <= SmtpClientCount; i++)
            {
                _smtpClients[i].Dispose();
            }
        }
    }

    public class MailRecipient
    {
        public string UserName { get; set; }

        public string Subject { get; set; }

        public string EmailAddress { get; set; }

        public string MessageBody { get; set; }
    }

}