/// <summary> /// Sends a single mail message asyncronously. /// </summary> /// <remarks>The method raises events before and after sending, as well as on send failure.</remarks> /// <param name="mailMergeMessage">Mail merge message.</param> /// <param name="dataItem">The following types are accepted: /// 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> /// <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="AggregateException"></exception> public async Task SendAsync(MailMergeMessage mailMergeMessage, object dataItem) { if (IsBusy) { throw new InvalidOperationException($"{nameof(SendAsync)}: A send operation is pending in this instance of {nameof(MailMergeSender)}."); } IsBusy = true; try { #if NET40 await TaskEx.Run(async() => #else await Task.Run(async() => #endif { var smtpClientConfig = Config.SmtpClientConfig[0]; // use the standard configuration using (var smtpClient = GetInitializedSmtpClient(smtpClientConfig)) { await SendMimeMessageAsync(smtpClient, mailMergeMessage.GetMimeMessage(dataItem), smtpClientConfig).ConfigureAwait(false); smtpClient.ProtocolLogger?.Dispose(); } }, _cancellationTokenSource.Token).ConfigureAwait(false); } finally { 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="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 a single mail message asynchronously. /// </summary> /// <remarks>The method raises events before and after sending, as well as on send failure.</remarks> /// <param name="mailMergeMessage">Mail merge message.</param> /// <param name="dataItem">The following types are accepted: /// 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> /// <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="AggregateException"></exception> /// <exception cref="MailMergeMessage.MailMergeMessageException"></exception> public async Task SendAsync(MailMergeMessage mailMergeMessage, object dataItem) { if (mailMergeMessage == null) { throw new ArgumentNullException($"{nameof(SendAsync)}: {nameof(mailMergeMessage)} is null."); } if (IsBusy) { throw new InvalidOperationException($"{nameof(SendAsync)}: A send operation is pending in this instance of {nameof(MailMergeSender)}."); } IsBusy = true; try { await Task.Run(async() => { var smtpClientConfig = Config.SmtpClientConfig[0]; // use the standard configuration using (var smtpClient = GetInitializedSmtpClientDelegate(smtpClientConfig)) { 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) { throw; } // set MimeMessage from OnMessageFailure delegate mimeMessage = mmFailureEventArgs.MimeMessage; } await SendMimeMessageAsync(smtpClient, mimeMessage, smtpClientConfig).ConfigureAwait(false); smtpClient.ProtocolLogger?.Dispose(); smtpClient.Disconnect(true, _cancellationTokenSource.Token); } }, _cancellationTokenSource.Token).ConfigureAwait(false); } finally { RenewCancellationTokenSource(); IsBusy = false; } }
internal MailMessageFailureEventArgs(Exception error, MailMergeMessage mailMergeMessage, object dataSource, MimeMessage mimeMessage = null, bool throwException = true) { Error = error; MailMergeMessage = mailMergeMessage; DataSource = dataSource; MimeMessage = mimeMessage; ThrowException = throwException; }
/// <summary> /// Constructor. /// </summary> /// <param name="mailMergeMessage">The parent MailMergeMessage, where HtmlBodyBuilder processes HtmlText and Subject properties.</param> /// <param name="dataItem"></param> public HtmlBodyBuilder(MailMergeMessage mailMergeMessage, object dataItem) { _docBaseUri = new Uri(string.Concat(UriScheme.File, UriScheme.SchemeDelimiter)); _mailMergeMessage = mailMergeMessage; _dataItem = dataItem; BinaryTransferEncoding = mailMergeMessage.Config.BinaryTransferEncoding; // Create a new parser front-end (can be re-used) var parser = new HtmlParser(); //Just get the DOM representation _htmlDocument = parser.Parse(mailMergeMessage.HtmlText); }
/// <summary> /// Constructor. /// </summary> /// <param name="mailMergeMessage">The parent MailMergeMessage, where HtmlBodyBuilder processes HtmlText and Subject properties.</param> /// <param name="dataItem"></param> public HtmlBodyBuilder(MailMergeMessage mailMergeMessage, object dataItem) { DocBaseUri = mailMergeMessage.Config.FileBaseDirectory; _mailMergeMessage = mailMergeMessage; _dataItem = dataItem; BinaryTransferEncoding = mailMergeMessage.Config.BinaryTransferEncoding; // Create a new parser front-end (can be re-used) var parser = new HtmlParser(); //Just get the DOM representation _htmlDocument = parser.Parse(mailMergeMessage.HtmlText); }
/// <summary> /// Sends a single mail merge message. /// </summary> /// <param name="mailMergeMessage">Message to send.</param> /// <param name="dataItem">The following types are accepted: /// 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> /// <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> /// <exception cref="MailMergeMessage.MailMergeMessageException"></exception> public void Send(MailMergeMessage mailMergeMessage, object dataItem) { if (IsBusy) { throw new InvalidOperationException($"{nameof(Send)}: A send operation is pending in this instance of {nameof(MailMergeSender)}."); } IsBusy = true; try { var smtpClientConfig = Config.SmtpClientConfig[0]; // use the standard configuration using (var smtpClient = GetInitializedSmtpClient(smtpClientConfig)) { 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) { throw; } // set MimeMessage from OnMessageFailure delegate mimeMessage = mmFailureEventArgs.MimeMessage; } SendMimeMessage(smtpClient, mimeMessage, smtpClientConfig); smtpClient.ProtocolLogger?.Dispose(); } } finally { IsBusy = false; } }
/// <summary> /// Gets the MailAddress representation of the MailMergeAddress. /// </summary> /// <returns>Returns a MailAddress ready to be used for a MailAddress, or Null if no address part exists.</returns> /// <exception cref="NullReferenceException">Throws a NullReferenceException if TextVariableManager is null.</exception> /// <exception cref="FormatException">Throws a FormatException if the computed MailAddress is not valid.</exception> internal MailboxAddress GetMailAddress(MailMergeMessage mmm, object dataItem) { var address = mmm.SearchAndReplaceVars(Address, dataItem); var displayName = mmm.SearchAndReplaceVars(DisplayName, dataItem); if (string.IsNullOrEmpty(displayName)) { displayName = null; } // Exclude invalid address from further process if (!EmailValidator.Validate(address, false, true)) { return(null); } return(displayName != null ? new MailboxAddress(DisplayNameCharacterEncoding, displayName, address) : new MailboxAddress(DisplayNameCharacterEncoding, address, address)); }
/// <summary> /// Sends a single mail merge message. /// </summary> /// <param name="mailMergeMessage">Message to send.</param> /// <param name="dataItem">The following types are accepted: /// 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> /// <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(MailMergeMessage mailMergeMessage, object dataItem) { if (IsBusy) { throw new InvalidOperationException($"{nameof(Send)}: A send operation is pending in this instance of {nameof(MailMergeSender)}."); } IsBusy = true; try { var smtpClientConfig = Config.SmtpClientConfig[0]; // use the standard configuration using (var smtpClient = GetInitializedSmtpClient(smtpClientConfig)) { SendMimeMessage(smtpClient, mailMergeMessage.GetMimeMessage(dataItem), smtpClientConfig); 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> /// 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; } }
/// <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> /// Constructor. /// </summary> internal MailMergeAddressCollection(MailMergeMessage msg) { _mailMergeMessage = msg; }
/// <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; } }
internal MailMessageFailureEventArgs(Exception error, MailMergeMessage mailMergeMessage, object dataSource) { Error = error; MailMergeMessage = mailMergeMessage; DataSource = dataSource; }
/// <summary> /// CTOR. /// Create an instance which loads the Formatters and Source extensions required by MailMergeLib. /// Error actions are SmartFormatters defaults. /// </summary> /// <param name="mailMergeMessage"></param> internal MailSmartFormatter(MailMergeMessage mailMergeMessage) : this() { ErrorAction = mailMergeMessage.Config.SmartFormatterConfig.FormatErrorAction; Parser.ErrorAction = mailMergeMessage.Config.SmartFormatterConfig.ParseErrorAction; }