Exemple #1
0
 internal MailSenderSendFailureEventArgs(Exception error, int failureCounter, SmtpClientConfig smtpClientConfig,
                                         MimeMessage mimeMessage)
 {
     Error            = error;
     FailureCounter   = failureCounter;
     SmtpClientConfig = smtpClientConfig;
     MimeMessage      = mimeMessage;
 }
Exemple #2
0
 internal MailSenderBeforeSendEventArgs(SmtpClientConfig smtpConfig, MimeMessage mimeMessage, DateTime startTime, Exception error,
                                        bool cancelled)
 {
     Error            = error;
     Cancelled        = cancelled;
     MimeMessage      = mimeMessage;
     SmtpClientConfig = smtpConfig;
     StartTime        = startTime;
 }
Exemple #3
0
 internal MailSenderAfterSendEventArgs(SmtpClientConfig smtpConfig, MimeMessage mailMergeMessage, DateTime startTime, DateTime endTime,
                                       Exception error, bool cancelled)
 {
     Error            = error;
     Cancelled        = cancelled;
     SmtpClientConfig = smtpConfig;
     MimeMessage      = mailMergeMessage;
     StartTime        = startTime;
     EndTime          = endTime;
 }
        /// <summary>
        /// Get pre-configured SmtpClient
        /// </summary>
        private static SmtpClient GetInitializedSmtpClient(SmtpClientConfig config)
        {
            //var smtpClient = new SmtpClient(new ProtocolLogger(@"C:\temp\mail\SmtpLog_" + System.IO.Path.GetRandomFileName() + ".txt"));
            var smtpClient = config.EnableLogOutput ? new SmtpClient(config.GetProtocolLogger()) : new SmtpClient();

            SetConfigForSmtpClient(smtpClient, config);

            // smtpClient.AuthenticationMechanisms.Remove("XOAUTH2");
            return(smtpClient);
        }
 /// <summary>
 /// Determines whether the specified object is equal to the current object.
 /// </summary>
 /// <param name="other"></param>
 /// <returns></returns>
 /// <remarks>
 /// Excluding those properties which are not serialized:
 /// ClientCertificates, ServerCertificateValidationCallback, NetworkCredential, ProtocolLoggerDelegate
 /// </remarks>
 protected bool Equals(SmtpClientConfig other)
 {
     return(MaxFailures == other.MaxFailures && RetryDelayTime == other.RetryDelayTime &&
            string.Equals(MailOutputDirectory, other.MailOutputDirectory) &&
            string.Equals(Name, other.Name) &&
            string.Equals(SmtpHost, other.SmtpHost) && SmtpPort == other.SmtpPort &&
            string.Equals(ClientDomain, other.ClientDomain) && Equals(LocalEndPoint, other.LocalEndPoint) &&
            MessageOutput == other.MessageOutput &&
            SslProtocols == other.SslProtocols && SecureSocketOptions == other.SecureSocketOptions &&
            Timeout == other.Timeout && DelayBetweenMessages == other.DelayBetweenMessages);
 }
        /// <summary>
        /// Get a new instance of a pre-configured SmtpClient
        /// </summary>
        private SmtpClient GetInitializedSmtpClient(SmtpClientConfig config)
        {
            var smtpClient = config.ProtocolLoggerDelegate != null ? new SmtpClient(config.ProtocolLoggerDelegate?.Invoke()) : new SmtpClient();

            smtpClient.Timeout            = config.Timeout;
            smtpClient.LocalDomain        = config.ClientDomain;
            smtpClient.LocalEndPoint      = config.LocalEndPoint;
            smtpClient.ClientCertificates = config.ClientCertificates;
            smtpClient.ServerCertificateValidationCallback = config.ServerCertificateValidationCallback;
            smtpClient.SslProtocols = config.SslProtocols;

            // redirect SmtpClient events
            smtpClient.Connected     += (sender, args) => { OnSmtpConnected?.Invoke(smtpClient, new MailSenderSmtpClientEventArgs(config)); };
            smtpClient.Authenticated += (sender, args) => { OnSmtpAuthenticated?.Invoke(smtpClient, new MailSenderSmtpClientEventArgs(config)); };
            smtpClient.Disconnected  += (sender, args) => { OnSmtpDisconnected?.Invoke(smtpClient, new MailSenderSmtpClientEventArgs(config)); };
            return(smtpClient);
        }
        /// <summary>
        /// Disconnects the SmtpClient if connected, and sets the new configuration.
        /// </summary>
        /// <remarks>
        /// Note:
        /// Part of configuration will only be used by SmtpClient during Connect() or Authorize().
        /// Protocol logger settings do not change.
        /// </remarks>
        /// <param name="smtpClient"></param>
        /// <param name="config"></param>
        private static void SetConfigForSmtpClient(SmtpClient smtpClient, SmtpClientConfig config)
        {
            try
            {
                if (smtpClient.IsConnected)
                {
                    smtpClient.Disconnect(false);
                }
            }
            catch (Exception)
            {}

            smtpClient.Timeout = config.Timeout;
            smtpClient.LocalDomain = config.ClientDomain;
            smtpClient.LocalEndPoint = config.LocalEndPoint;
            smtpClient.ServerCertificateValidationCallback = config.ServerCertificateValidationCallback;
        }
        /// <summary>
        /// Sends the MimeMessage to an SMTP server. This is the lowest level of sending a message.
        /// Connects and authenticates if necessary, but leaves the connection open.
        /// </summary>
        /// <param name="smtpClient"></param>
        /// <param name="message"></param>
        /// <param name="config"></param>
        internal void SendMimeMessageToSmtpServer(SmtpClient smtpClient, MimeMessage message, SmtpClientConfig config)
        {
            var          hostPortConfig = $"{config.SmtpHost}:{config.SmtpPort} using configuration '{config.Name}'";
            const string errorConnect   = "Error trying to connect";
            const string errorAuth      = "Error trying to authenticate on";

            try
            {
                if (!smtpClient.IsConnected)
                {
                    smtpClient.Connect(config.SmtpHost, config.SmtpPort, config.SecureSocketOptions,
                                       _cancellationTokenSource.Token);
                }
            }
            catch (SmtpCommandException ex)
            {
                throw new SmtpCommandException(ex.ErrorCode, ex.StatusCode, ex.Mailbox,
                                               $"{errorConnect} {hostPortConfig}'. " + ex.Message);
            }
            catch (SmtpProtocolException ex)
            {
                throw new SmtpProtocolException(
                          $"{errorConnect} {hostPortConfig}'. " + ex.Message);
            }
            catch (System.IO.IOException ex)
            {
                throw new System.IO.IOException($"{errorConnect} {hostPortConfig}'. " + ex.Message);
            }

            if (config.NetworkCredential != null && !smtpClient.IsAuthenticated && smtpClient.Capabilities.HasFlag(SmtpCapabilities.Authentication))
            {
                try
                {
                    smtpClient.Authenticate(config.NetworkCredential, _cancellationTokenSource.Token);
                }
                catch (AuthenticationException ex)
                {
                    throw new AuthenticationException($"{errorAuth} {hostPortConfig}. " + ex.Message);
                }
                catch (SmtpCommandException ex)
                {
                    throw new SmtpCommandException(ex.ErrorCode, ex.StatusCode, ex.Mailbox,
                                                   $"{errorAuth} {hostPortConfig}. " + ex.Message);
                }
                catch (SmtpProtocolException ex)
                {
                    throw new SmtpProtocolException($"{errorAuth} {hostPortConfig}. " + ex.Message);
                }
            }

            try
            {
                smtpClient.Send(message, _cancellationTokenSource.Token);
            }
            catch (SmtpCommandException ex)
            {
                throw ex.ErrorCode switch
                      {
                          SmtpErrorCode.RecipientNotAccepted => new SmtpCommandException(ex.ErrorCode, ex.StatusCode, ex.Mailbox,
                                                                                         $"Recipient not accepted by {hostPortConfig}. " + ex.Message),
                          SmtpErrorCode.SenderNotAccepted => new SmtpCommandException(ex.ErrorCode, ex.StatusCode, ex.Mailbox,
                                                                                      $"Sender not accepted by {hostPortConfig}. " + ex.Message),
                          SmtpErrorCode.MessageNotAccepted => new SmtpCommandException(ex.ErrorCode, ex.StatusCode, ex.Mailbox,
                                                                                       $"Message not accepted by {hostPortConfig}. " + ex.Message),
                          _ => new SmtpCommandException(ex.ErrorCode, ex.StatusCode, ex.Mailbox,
                                                        $"Error sending message to {hostPortConfig}. " + ex.Message),
                      };
            }
            catch (SmtpProtocolException ex)
            {
                throw new SmtpProtocolException($"Error while sending message to {hostPortConfig}. " + ex.Message, ex);
            }
        }
        /// <summary>
        /// Sends mail messages asynchronously to all recipients supplied in the data source
        /// of the mail merge message.
        /// </summary>
        /// <param name="mailMergeMessage">Mail merge message.</param>
        /// <param name="dataSource">IEnumerable data source with values for the placeholders of the MailMergeMessage.
        /// IEnumerable&lt;T&gt; where T can be the following types:
        /// Dictionary&lt;string,object&gt;, ExpandoObject, DataRow, class instances or anonymous types.
        /// The named placeholders can be the name of a Property, Field, or a parameterless method.
        /// They can also be chained together by using &quot;dot-notation&quot;.
        /// </param>
        /// <remarks>
        /// In order to use a DataTable as a dataSource, use System.Data.DataSetExtensions and convert it with DataTable.AsEnumerable()
        /// </remarks>
        /// <exception>
        /// If the SMTP transaction is the cause, SmtpFailedRecipientsException, SmtpFailedRecipientException or SmtpException can be expected.
        /// These exceptions throw after re-trying to send after failures (i.e. after MaxFailures * RetryDelayTime).
        /// </exception>
        /// <exception cref="InvalidOperationException">A send operation is pending.</exception>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="Exception">The first exception found in one of the async tasks.</exception>
        /// <exception cref="MailMergeMessage.MailMergeMessageException"></exception>
        public async Task SendAsync <T>(MailMergeMessage mailMergeMessage, IEnumerable <T> dataSource)
        {
            if (mailMergeMessage == null)
            {
                throw new ArgumentNullException($"{nameof(SendAsync)}: {nameof(mailMergeMessage)} is null.");
            }

            if (dataSource == null)
            {
                throw new ArgumentNullException($"{nameof(dataSource)} is null.");
            }

            if (IsBusy)
            {
                throw new InvalidOperationException($"{nameof(SendAsync)}: A send operation is pending in this instance of {nameof(MailMergeSender)}.");
            }

            IsBusy = true;
            var sentMsgCount  = 0;
            var errorMsgCount = 0;

            var tasksUsed = new HashSet <int>();

            void AfterSend(object obj, MailSenderAfterSendEventArgs args)
            {
                if (args.Error == null)
                {
                    Interlocked.Increment(ref sentMsgCount);
                }
                else
                {
                    Interlocked.Increment(ref errorMsgCount);
                }
            }

            OnAfterSend += AfterSend;

            var startTime = DateTime.Now;

            var queue = new ConcurrentQueue <T>(dataSource);

            var numOfRecords = queue.Count;
            var sendTasks    = new Task[Config.MaxNumOfSmtpClients];

            // The max. number of configurations used is the number of parallel smtp clients
            var smtpConfigForTask = new SmtpClientConfig[Config.MaxNumOfSmtpClients];

            // Set as many smtp configs as we have for each task
            // Example: 5 tasks with 2 configs: task 0 => config 0, task 1 => config 1, task 2 => config 0, task 3 => config 1, task 4 => config 0, task 5 => config 1
            for (var i = 0; i < Config.MaxNumOfSmtpClients; i++)
            {
                smtpConfigForTask[i] = Config.SmtpClientConfig[i % Config.SmtpClientConfig.Length];
            }

            for (var i = 0; i < sendTasks.Length; i++)
            {
                var taskNo = i;
                sendTasks[taskNo] = Task.Run(async() =>
                {
                    using var smtpClient = GetInitializedSmtpClientDelegate(smtpConfigForTask[taskNo]);
                    while (queue.TryDequeue(out var dataItem))
                    {
                        lock (tasksUsed)
                        {
                            tasksUsed.Add(taskNo);
                        }

                        // Delay between messages is also the delay until the first message will be sent
                        await Task.Delay(smtpConfigForTask[taskNo].DelayBetweenMessages, _cancellationTokenSource.Token).ConfigureAwait(false);

                        var localDataItem       = dataItem; // no modified enclosure
                        MimeMessage mimeMessage = null;
                        try
                        {
                            mimeMessage = await Task.Run(() => mailMergeMessage.GetMimeMessage(localDataItem), _cancellationTokenSource.Token).ConfigureAwait(false);
                        }
                        catch (Exception exception)
                        {
                            OnMergeProgress?.Invoke(this,
                                                    new MailSenderMergeProgressEventArgs(startTime, numOfRecords, sentMsgCount, errorMsgCount));

                            var mmFailureEventArgs = new MailMessageFailureEventArgs(exception, mailMergeMessage, dataItem, mimeMessage, true);
                            if (exception is MailMergeMessage.MailMergeMessageException ex)
                            {
                                mmFailureEventArgs = new MailMessageFailureEventArgs(ex, mailMergeMessage, dataItem,
                                                                                     ex.MimeMessage, true);
                            }

                            OnMessageFailure?.Invoke(this, mmFailureEventArgs);

                            // event delegate may have modified the mimeMessage and decided not to throw an exception
                            if (mmFailureEventArgs.ThrowException || mmFailureEventArgs.MimeMessage == null)
                            {
                                Interlocked.Increment(ref errorMsgCount);

                                // Fire promised events
                                OnMergeProgress?.Invoke(this,
                                                        new MailSenderMergeProgressEventArgs(startTime, numOfRecords, sentMsgCount,
                                                                                             errorMsgCount));
                                throw;
                            }

                            // set MimeMessage from OnMessageFailure delegate
                            mimeMessage = mmFailureEventArgs.MimeMessage;
                        }

                        OnMergeProgress?.Invoke(this,
                                                new MailSenderMergeProgressEventArgs(startTime, numOfRecords, sentMsgCount, errorMsgCount));

                        await SendMimeMessageAsync(smtpClient, mimeMessage, smtpConfigForTask[taskNo]).ConfigureAwait(false);

                        OnMergeProgress?.Invoke(this,
                                                new MailSenderMergeProgressEventArgs(startTime, numOfRecords, sentMsgCount, errorMsgCount));
                    }

                    smtpClient.ProtocolLogger?.Dispose();
                    smtpClient.Disconnect(true, _cancellationTokenSource.Token);
                }, _cancellationTokenSource.Token);
            }

            try
            {
                OnMergeBegin?.Invoke(this, new MailSenderMergeBeginEventArgs(startTime, numOfRecords));

                // Note await Task.WhenAll will only throw the FIRST exception of the aggregate exception!
                await Task.WhenAll(sendTasks.AsEnumerable()).ConfigureAwait(false);
            }
            finally
            {
                OnMergeComplete?.Invoke(this, new MailSenderMergeCompleteEventArgs(startTime, DateTime.Now, numOfRecords, sentMsgCount, errorMsgCount, tasksUsed.Count));

                OnAfterSend -= AfterSend;

                RenewCancellationTokenSource();
                IsBusy = false;
            }
        }
        /// <summary>
        /// This is the procedure taking care of sending the message (or saving to a file, respectively).
        /// </summary>
        /// <param name="smtpClient">The fully configures SmtpClient used to send the MimeMessate</param>
        /// <param name="mimeMsg">Mime message to send.</param>
        /// <param name="config"></param>
        /// <exception>
        /// If the SMTP transaction is the cause, SmtpFailedRecipientsException, SmtpFailedRecipientException or SmtpException can be expected.
        /// These exceptions throw after re-trying to send after failures (i.e. after MaxFailures * RetryDelayTime).
        /// </exception>
        /// <exception cref="SmtpCommandException"></exception>
        /// <exception cref="SmtpProtocolException"></exception>
        /// <exception cref="AuthenticationException"></exception>
        /// <exception cref="System.Net.Sockets.SocketException"></exception>
        internal void SendMimeMessage(SmtpClient smtpClient, MimeMessage mimeMsg, SmtpClientConfig config)
        {
            var       startTime = DateTime.Now;
            Exception sendException;

            // the client can rely on the sequence of events: OnBeforeSend, OnSendFailure (if any), OnAfterSend
            OnBeforeSend?.Invoke(smtpClient, new MailSenderBeforeSendEventArgs(config, mimeMsg, startTime, null, _cancellationTokenSource.Token.IsCancellationRequested));

            var failureCounter = 0;

            do
            {
                try
                {
                    sendException = null;
                    const string mailExt = ".eml";
                    switch (config.MessageOutput)
                    {
                    case MessageOutput.None:
                        break;

                    case MessageOutput.Directory:
                        mimeMsg.WriteTo(System.IO.Path.Combine(config.MailOutputDirectory, Guid.NewGuid().ToString("N") + mailExt), _cancellationTokenSource.Token);
                        break;

#if NETFRAMEWORK
                    case MessageOutput.PickupDirectoryFromIis:
                        // for requirements of message format see: https://technet.microsoft.com/en-us/library/bb124230(v=exchg.150).aspx
                        // and here http://www.vsysad.com/2014/01/iis-smtp-folders-and-domains-explained/
                        mimeMsg.WriteTo(System.IO.Path.Combine(config.MailOutputDirectory, Guid.NewGuid().ToString("N") + mailExt), _cancellationTokenSource.Token);
                        break;
#endif
                    default:
                        SendMimeMessageToSmtpServer(smtpClient, mimeMsg, config);
                        break;     // break switch
                    }
                    // when SendMimeMessageToSmtpServer throws less than _maxFailures exceptions,
                    // and succeeds after an exception, we MUST break the while loop here (else: infinite)
                    break;
                }
                catch (Exception ex)
                {
                    sendException = ex;
                    // exceptions which are thrown by SmtpClient:
                    if (ex is SmtpCommandException || ex is SmtpProtocolException ||
                        ex is AuthenticationException || ex is System.Net.Sockets.SocketException || ex is System.IO.IOException)
                    {
                        failureCounter++;
                        OnSendFailure?.Invoke(smtpClient,
                                              new MailSenderSendFailureEventArgs(sendException, failureCounter, config, mimeMsg));

                        Thread.Sleep(config.RetryDelayTime);

                        // on first SMTP failure switch to the backup configuration, if one exists
                        if (failureCounter == 1 && config.MaxFailures > 1)
                        {
                            var backupConfig = Config.SmtpClientConfig.FirstOrDefault(c => !c.Equals(config));
                            if (backupConfig == null)
                            {
                                continue;
                            }

                            backupConfig.MaxFailures = config.MaxFailures; // keep the logic within the current loop unchanged
                            config = backupConfig;
                            smtpClient.Disconnect(false);
                            smtpClient = GetInitializedSmtpClientDelegate(config);
                        }

                        if (failureCounter == config.MaxFailures && smtpClient.IsConnected)
                        {
                            smtpClient.Disconnect(false);
                        }
                    }
                    else
                    {
                        failureCounter = config.MaxFailures;
                        OnSendFailure?.Invoke(smtpClient, new MailSenderSendFailureEventArgs(sendException, 1, config, mimeMsg));
                    }
                }
            } while (failureCounter < config.MaxFailures && failureCounter > 0);

            OnAfterSend?.Invoke(smtpClient,
                                new MailSenderAfterSendEventArgs(config, mimeMsg, startTime, DateTime.Now, sendException, _cancellationTokenSource.Token.IsCancellationRequested));

            // Dispose the streams of file attachments and inline file attachments
            MailMergeMessage.DisposeFileStreams(mimeMsg);

            if (sendException != null)
            {
                throw sendException;
            }
        }
Exemple #11
0
 internal MailSenderSmtpClientEventArgs(SmtpClientConfig smtpConfig)
 {
     SmtpClientConfig = smtpConfig;
 }
        /// <summary>
        /// Sends mail messages asynchronously to all recipients supplied in the data source
        /// of the mail merge message.
        /// </summary>
        /// <param name="mailMergeMessage">Mail merge message.</param>
        /// <param name="dataSource">IEnumerable data source with values for the placeholders of the MailMergeMessage.
        /// IEnumerable&lt;T&gt; where T can be the following types:
        /// Dictionary&lt;string,object&gt;, ExpandoObject, DataRow, class instances or anonymous types.
        /// The named placeholders can be the name of a Property, Field, or a parameterless method.
        /// They can also be chained together by using &quot;dot-notation&quot;.
        /// </param>
        /// <remarks>
        /// In order to use a DataTable as a dataSource, use System.Data.DataSetExtensions and convert it with DataTable.AsEnumerable()
        /// </remarks>
        /// <exception>
        /// If the SMTP transaction is the cause, SmtpFailedRecipientsException, SmtpFailedRecipientException or SmtpException can be expected.
        /// These exceptions throw after re-trying to send after failures (i.e. after MaxFailures * RetryDelayTime).
        /// </exception>
        /// <exception cref="InvalidOperationException">A send operation is pending.</exception>
        /// <exception cref="NullReferenceException"></exception>
        /// <exception cref="Exception">The first exception found in one of the async tasks.</exception>
        public async Task SendAsync <T>(MailMergeMessage mailMergeMessage, IEnumerable <T> dataSource)
        {
            if (mailMergeMessage == null || dataSource == null)
            {
                throw new NullReferenceException($"{nameof(mailMergeMessage)} and {nameof(dataSource)} must not be null.");
            }

            if (IsBusy)
            {
                throw new InvalidOperationException($"{nameof(SendAsync)}: A send operation is pending in this instance of {nameof(MailMergeSender)}.");
            }

            IsBusy = true;
            var sentMsgCount  = 0;
            var errorMsgCount = 0;

            var tasksUsed = new HashSet <int>();

            EventHandler <MailSenderAfterSendEventArgs> afterSend = (obj, args) =>
            {
                if (args.Error == null)
                {
                    Interlocked.Increment(ref sentMsgCount);
                }
                else
                {
                    Interlocked.Increment(ref errorMsgCount);
                }
            };

            OnAfterSend += afterSend;

            var startTime = DateTime.Now;

            var queue = new ConcurrentQueue <T>(dataSource);

            var numOfRecords = queue.Count;
            var sendTasks    = new Task[Config.MaxNumOfSmtpClients];

            // The max. number of configurations used is the number of parallel smtp clients
            var smtpConfigForTask = new SmtpClientConfig[Config.MaxNumOfSmtpClients];

            // Set as many smtp configs as we have for each task
            // Example: 5 tasks with 2 configs: task 0 => config 0, task 1 => config 1, task 2 => config 0, task 3 => config 1, task 4 => config 0, task 5 => config 1
            for (var i = 0; i < Config.MaxNumOfSmtpClients; i++)
            {
                smtpConfigForTask[i] = Config.SmtpClientConfig[i % Config.SmtpClientConfig.Length];
            }

            for (var i = 0; i < sendTasks.Length; i++)
            {
                var taskNo = i;
#if NET40
                sendTasks[taskNo] = TaskEx.Run(async() =>
#else
                sendTasks[taskNo] = Task.Run(async() =>
#endif
                {
                    using (var smtpClient = GetInitializedSmtpClient(smtpConfigForTask[taskNo]))
                    {
                        T dataItem;
                        while (queue.TryDequeue(out dataItem))
                        {
                            lock (tasksUsed)
                            {
                                tasksUsed.Add(taskNo);
                            }

                            var localDataItem = dataItem;                              // no modified enclosure
                            MimeMessage mimeMessage;
                            try
                            {
#if NET40
                                mimeMessage = await TaskEx.Run(() => mailMergeMessage.GetMimeMessage(localDataItem)).ConfigureAwait(false);
#else
                                mimeMessage = await Task.Run(() => mailMergeMessage.GetMimeMessage(localDataItem)).ConfigureAwait(false);
#endif
                            }
                            catch (Exception ex)
                            {
                                OnMessageFailure?.Invoke(this, new MailMessageFailureEventArgs(ex, mailMergeMessage, dataItem));
                                return;
                            }

                            OnMergeProgress?.Invoke(this,
                                                    new MailSenderMergeProgressEventArgs(startTime, numOfRecords, sentMsgCount, errorMsgCount));

                            /*
                             * if (OnMergeProgress != null)
                             *      Task.Factory.FromAsync((asyncCallback, obj) => OnMergeProgress.BeginInvoke(this, new MailSenderMergeProgressEventArgs(startTime, numOfRecords, sentMsgCount, errorMsgCount), asyncCallback, obj), OnMergeProgress.EndInvoke, null);
                             */
                            await SendMimeMessageAsync(smtpClient, mimeMessage, smtpConfigForTask[taskNo]).ConfigureAwait(false);

                            OnMergeProgress?.Invoke(this,
                                                    new MailSenderMergeProgressEventArgs(startTime, numOfRecords, sentMsgCount, errorMsgCount));
#if NET40
                            await TaskEx.Delay(smtpConfigForTask[taskNo].DelayBetweenMessages, _cancellationTokenSource.Token).ConfigureAwait(false);
#else
                            await Task.Delay(smtpConfigForTask[taskNo].DelayBetweenMessages, _cancellationTokenSource.Token).ConfigureAwait(false);
#endif
                        }

                        try
                        {
                            smtpClient.Disconnect(true);
                        }
                        catch (Exception)
                        {
                            // don't care for exception when disconnecting,
                            // because smptClient will be disposed immediately anyway
                        }
                        smtpClient.ProtocolLogger?.Dispose();
                    }
                }, _cancellationTokenSource.Token);
            }

            try
            {
                OnMergeBegin?.Invoke(this, new MailSenderMergeBeginEventArgs(startTime, numOfRecords));

                // Note await Task.WhenAll will only throw the FIRST exception of the aggregate exception!
#if NET40
                await TaskEx.WhenAll(sendTasks.AsEnumerable()).ConfigureAwait(false);
#else
                await Task.WhenAll(sendTasks.AsEnumerable()).ConfigureAwait(false);
#endif
            }
            finally
            {
                OnMergeComplete?.Invoke(this, new MailSenderMergeCompleteEventArgs(startTime, DateTime.Now, numOfRecords, sentMsgCount, errorMsgCount, tasksUsed.Count));

                OnAfterSend -= afterSend;

                IsBusy = false;
            }
        }
        /// <summary>
        /// Sends the MimeMessage to an SMTP server. This is the lowest level of sending a message.
        /// Connects and authenficates if necessary, but leaves the connection open.
        /// </summary>
        /// <param name="smtpClient"></param>
        /// <param name="message"></param>
        /// <param name="config"></param>
        private async Task SendMimeMessageToSmtpServerAsync(SmtpClient smtpClient, MimeMessage message, SmtpClientConfig config)
        {
            var          hostPortConfig = $"{config.SmtpHost}:{config.SmtpPort} using configuration '{config.Name}'";
            const string errorConnect   = "Error trying to connect";
            const string errorAuth      = "Error trying to authenticate on";

            try
            {
                if (!smtpClient.IsConnected)
                {
                    await smtpClient.ConnectAsync(config.SmtpHost, config.SmtpPort, config.SecureSocketOptions,
                                                  _cancellationTokenSource.Token).ConfigureAwait(false);
                }
            }
            catch (SmtpCommandException ex)
            {
                throw new SmtpCommandException(ex.ErrorCode, ex.StatusCode, ex.Mailbox,
                                               $"{errorConnect} {hostPortConfig}'. " + ex.Message);
            }
            catch (SmtpProtocolException ex)
            {
                throw new SmtpProtocolException(
                          $"{errorConnect} {hostPortConfig}'. " + ex.Message);
            }

            if (config.NetworkCredential != null && !smtpClient.IsAuthenticated && smtpClient.Capabilities.HasFlag(SmtpCapabilities.Authentication))
            {
                try
                {
                    await smtpClient.AuthenticateAsync(config.NetworkCredential, _cancellationTokenSource.Token).ConfigureAwait(false);
                }
                catch (AuthenticationException ex)
                {
                    throw new AuthenticationException($"{errorAuth} {hostPortConfig}. " + ex.Message);
                }
                catch (SmtpCommandException ex)
                {
                    throw new SmtpCommandException(ex.ErrorCode, ex.StatusCode, ex.Mailbox, $"{errorAuth} {hostPortConfig}. " + ex.Message);
                }
                catch (SmtpProtocolException ex)
                {
                    throw new SmtpProtocolException($"{errorAuth} {hostPortConfig}. " + ex.Message);
                }
            }

            try
            {
                await smtpClient.SendAsync(message, _cancellationTokenSource.Token).ConfigureAwait(false);
            }
            catch (SmtpCommandException ex)
            {
                switch (ex.ErrorCode)
                {
                case SmtpErrorCode.RecipientNotAccepted:
                    throw new SmtpCommandException(ex.ErrorCode, ex.StatusCode, ex.Mailbox,
                                                   $"Recipient not accepted by {hostPortConfig}. " + ex.Message);

                case SmtpErrorCode.SenderNotAccepted:
                    throw new SmtpCommandException(ex.ErrorCode, ex.StatusCode, ex.Mailbox,
                                                   $"Sender not accepted by {hostPortConfig}. " + ex.Message);

                case SmtpErrorCode.MessageNotAccepted:
                    throw new SmtpCommandException(ex.ErrorCode, ex.StatusCode, ex.Mailbox,
                                                   $"Message not accepted by {hostPortConfig}. " + ex.Message);

                default:
                    throw new SmtpCommandException(ex.ErrorCode, ex.StatusCode, ex.Mailbox,
                                                   $"Error sending message to {hostPortConfig}. " + ex.Message);
                }
            }
            catch (SmtpProtocolException ex)
            {
                throw new SmtpProtocolException($"Error while sending message to {hostPortConfig}. " + ex.Message, ex);
            }
        }
        /// <summary>
        /// This is the procedure taking care of sending the message (or saving to a file, respectively).
        /// </summary>
        /// <param name="smtpClient">The fully configures SmtpClient used to send the MimeMessate</param>
        /// <param name="mimeMsg">Mime message to send.</param>
        /// <param name="config"></param>
        /// <exception>
        /// If the SMTP transaction is the cause, SmtpFailedRecipientsException, SmtpFailedRecipientException or SmtpException can be expected.
        /// These exceptions throw after re-trying to send after failures (i.e. after MaxFailures * RetryDelayTime).
        /// </exception>
        /// <exception cref="SmtpCommandException"></exception>
        /// <exception cref="SmtpProtocolException"></exception>
        /// <exception cref="AuthenticationException"></exception>
        /// <exception cref="System.Net.Sockets.SocketException"></exception>
        private async Task SendMimeMessageAsync(SmtpClient smtpClient, MimeMessage mimeMsg, SmtpClientConfig config)
        {
            var startTime = DateTime.Now;
            Exception sendException;

            // the client can rely on the sequence of events: OnBeforeSend, OnSendFailure (if any), OnAfterSend
            OnBeforeSend?.Invoke(smtpClient, new MailSenderBeforeSendEventArgs(config, mimeMsg, startTime, null, _cancellationTokenSource.Token.IsCancellationRequested));

            var failureCounter = 0;

            do
            {
                try
                {
                    sendException        = null;
                    const string mailExt = ".eml";
                    switch (config.MessageOutput)
                    {
                    case MessageOutput.None:
                        break;

                    case MessageOutput.Directory:
                        mimeMsg.WriteTo(System.IO.Path.Combine(config.MailOutputDirectory, Guid.NewGuid().ToString("N") + mailExt), _cancellationTokenSource.Token);
                        break;

#if NET40 || NET45
                    case MessageOutput.PickupDirectoryFromIis:
                        // for requirements of message format see: https://technet.microsoft.com/en-us/library/bb124230(v=exchg.150).aspx
                        // and here http://www.vsysad.com/2014/01/iis-smtp-folders-and-domains-explained/
                        mimeMsg.WriteTo(System.IO.Path.Combine(config.MailOutputDirectory, Guid.NewGuid().ToString("N") + mailExt), _cancellationTokenSource.Token);
                        break;
#endif
                    default:
                        await SendMimeMessageToSmtpServerAsync(smtpClient, mimeMsg, config).ConfigureAwait(false);

                        break;                                 // break switch
                    }
                    // when SendMimeMessageToSmtpServer throws less than _maxFailures exceptions,
                    // and succeeds after an exception, we MUST break the while loop here (else: infinite)
                    break;
                }
                catch (Exception ex)
                {
                    // exceptions which are thrown by SmtpClient:
                    if (ex is SmtpCommandException || ex is SmtpProtocolException ||
                        ex is AuthenticationException || ex is System.Net.Sockets.SocketException)
                    {
                        failureCounter++;
                        sendException = ex;
                        OnSendFailure?.Invoke(smtpClient,
                                              new MailSenderSendFailureEventArgs(sendException, failureCounter, config, mimeMsg));
#if NET40
                        await TaskEx.Delay(config.RetryDelayTime, _cancellationTokenSource.Token).ConfigureAwait(false);
#else
                        await Task.Delay(config.RetryDelayTime, _cancellationTokenSource.Token).ConfigureAwait(false);
#endif
                        // on first SMTP failure switch to the backup configuration, if one exists
                        if (failureCounter == 1 && config.MaxFailures > 1)
                        {
                            var backupConfig = Config.SmtpClientConfig.FirstOrDefault(c => c != config);
                            if (backupConfig == null)
                            {
                                continue;
                            }

                            backupConfig.MaxFailures = config.MaxFailures;                             // keep the logic within the current loop unchanged
                            SetConfigForSmtpClient(smtpClient, backupConfig);
                            config = backupConfig;
                        }
                    }
                    else
                    {
                        failureCounter = config.MaxFailures;
                        sendException  = ex;
                        OnSendFailure?.Invoke(smtpClient, new MailSenderSendFailureEventArgs(sendException, 1, config, mimeMsg));
                    }
                }
            } while (failureCounter < config.MaxFailures && failureCounter > 0);

            OnAfterSend?.Invoke(smtpClient,
                                new MailSenderAfterSendEventArgs(config, mimeMsg, startTime, DateTime.Now, sendException, _cancellationTokenSource.Token.IsCancellationRequested));

            // Do some clean-up with the message
            foreach (var mimeEntity in mimeMsg.Attachments)
            {
                var att = mimeEntity as MimePart;
                att?.ContentObject.Stream.Dispose();
            }

            if (sendException != null)
            {
                throw sendException;
            }
        }