/// <summary>
        /// Sends mail messages syncronously 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="SmtpCommandException"></exception>
        /// <exception cref="SmtpProtocolException"></exception>
        /// <exception cref="AuthenticationException"></exception>
        public void Send <T>(MailMergeMessage mailMergeMessage, IEnumerable <T> dataSource)
        {
            if (IsBusy)
            {
                throw new InvalidOperationException($"{nameof(Send)}: A send operation is pending in this instance of {nameof(MailMergeSender)}.");
            }

            IsBusy = true;

            var sentMsgCount = 0;

            try
            {
                var dataSourceList = dataSource.ToList();

                var startTime    = DateTime.Now;
                var numOfRecords = dataSourceList.Count;

                var smtpClientConfig = Config.SmtpClientConfig[0];                 // use the standard configuration
                using (var smtpClient = GetInitializedSmtpClient(smtpClientConfig))
                {
                    OnMergeBegin?.Invoke(this, new MailSenderMergeBeginEventArgs(startTime, numOfRecords));

                    foreach (var dataItem in dataSourceList)
                    {
                        OnMergeProgress?.Invoke(this,
                                                new MailSenderMergeProgressEventArgs(startTime, numOfRecords, sentMsgCount, 0));

                        var mimeMessage = mailMergeMessage.GetMimeMessage(dataItem);
                        if (_cancellationTokenSource.IsCancellationRequested)
                        {
                            break;
                        }
                        SendMimeMessage(smtpClient, mimeMessage, smtpClientConfig);
                        sentMsgCount++;
                        if (_cancellationTokenSource.IsCancellationRequested)
                        {
                            break;
                        }

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

                        Thread.Sleep(smtpClientConfig.DelayBetweenMessages);
                        if (_cancellationTokenSource.IsCancellationRequested)
                        {
                            break;
                        }
                    }

                    OnMergeComplete?.Invoke(this,
                                            new MailSenderMergeCompleteEventArgs(startTime, DateTime.Now, numOfRecords, sentMsgCount, 0, 1));
                    smtpClient.ProtocolLogger?.Dispose();
                }
            }
            finally
            {
                IsBusy = false;
            }
        }
        /// <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>
        /// Sends mail messages syncronously 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="ArgumentNullException"></exception>
        /// <exception cref="InvalidOperationException">A send operation is pending.</exception>
        /// <exception cref="SmtpCommandException"></exception>
        /// <exception cref="SmtpProtocolException"></exception>
        /// <exception cref="AuthenticationException"></exception>
        /// <exception cref="MailMergeMessage.MailMergeMessageException"></exception>
        public void Send <T>(MailMergeMessage mailMergeMessage, IEnumerable <T> dataSource)
        {
            if (mailMergeMessage == null)
            {
                throw new ArgumentNullException($"{nameof(Send)}: {nameof(mailMergeMessage)} is null.");
            }

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

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

            IsBusy = true;

            var sentMsgCount = 0;

            try
            {
                var dataSourceList = dataSource.ToList();

                var startTime    = DateTime.Now;
                var numOfRecords = dataSourceList.Count;

                var smtpClientConfig = Config.SmtpClientConfig[0]; // use the standard configuration
                using var smtpClient = GetInitializedSmtpClientDelegate(smtpClientConfig);
                OnMergeBegin?.Invoke(this, new MailSenderMergeBeginEventArgs(startTime, numOfRecords));

                foreach (var dataItem in dataSourceList)
                {
                    OnMergeProgress?.Invoke(this,
                                            new MailSenderMergeProgressEventArgs(startTime, numOfRecords, sentMsgCount, 0));

                    MimeMessage mimeMessage = null;
                    try
                    {
                        mimeMessage = mailMergeMessage.GetMimeMessage(dataItem);
                    }
                    catch (Exception exception)
                    {
                        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)
                        {
                            // Invoke promised events
                            OnMergeProgress?.Invoke(this,
                                                    new MailSenderMergeProgressEventArgs(startTime, numOfRecords, sentMsgCount, 1));
                            smtpClient.Dispose();
                            OnMergeComplete?.Invoke(this,
                                                    new MailSenderMergeCompleteEventArgs(startTime, DateTime.Now, numOfRecords,
                                                                                         sentMsgCount, 1, 1));
                            throw;
                        }

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

                    if (_cancellationTokenSource.IsCancellationRequested)
                    {
                        break;
                    }
                    SendMimeMessage(smtpClient, mimeMessage, smtpClientConfig);
                    sentMsgCount++;
                    if (_cancellationTokenSource.IsCancellationRequested)
                    {
                        break;
                    }

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

                    Thread.Sleep(smtpClientConfig.DelayBetweenMessages);
                    if (_cancellationTokenSource.IsCancellationRequested)
                    {
                        break;
                    }
                }

                smtpClient.ProtocolLogger?.Dispose();
                smtpClient.Disconnect(true); // fire OnSmtpDisconnected before OnMergeComplete
                smtpClient.Dispose();
                OnMergeComplete?.Invoke(this,
                                        new MailSenderMergeCompleteEventArgs(startTime, DateTime.Now, numOfRecords, sentMsgCount, 0, 1));
            }
            finally
            {
                RenewCancellationTokenSource();
                IsBusy = false;
            }
        }
        /// <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;
            }
        }