internal MailSenderSendFailureEventArgs(Exception error, int failureCounter, SmtpClientConfig smtpClientConfig, MimeMessage mimeMessage) { Error = error; FailureCounter = failureCounter; SmtpClientConfig = smtpClientConfig; MimeMessage = mimeMessage; }
internal MailSenderBeforeSendEventArgs(SmtpClientConfig smtpConfig, MimeMessage mimeMessage, DateTime startTime, Exception error, bool cancelled) { Error = error; Cancelled = cancelled; MimeMessage = mimeMessage; SmtpClientConfig = smtpConfig; StartTime = startTime; }
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<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; } }
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<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; } }
/// <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; } }