/// <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<T> where T can be the following types: /// Dictionary<string,object>, 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 "dot-notation". /// </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<T> where T can be the following types: /// Dictionary<string,object>, 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 "dot-notation". /// </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<T> where T can be the following types: /// Dictionary<string,object>, 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 "dot-notation". /// </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<T> where T can be the following types: /// Dictionary<string,object>, 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 "dot-notation". /// </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; } }