public async Task InsertAsync(Message message, Stream stream) { var messageFullPath = Path.Combine(_dataDirectory, Path.ChangeExtension(Guid.NewGuid().ToString(), ".eml")); message.Filename = Path.GetFileName(messageFullPath); using (var fileStream = File.Open(messageFullPath, FileMode.CreateNew)) { stream.CopyTo(fileStream); } using (var sqlConnection = new MySqlConnection(_connectionString)) { sqlConnection.Open(); var messageId = await sqlConnection.InsertAsync<long>(message); message.Id = messageId; } foreach (var recipient in message.Recipients) { recipient.MessageId = message.Id; await InsertAsync(recipient); } }
public async Task UpdateAsync(Message message) { using (var sqlConnection = new MySqlConnection(_connectionString)) { sqlConnection.Open(); await sqlConnection.UpdateAsync(message); } }
public async Task<SmtpCommandReply> HandleData(Stream stream) { var message = new Message { From = _state.FromAddress, State = MessageState.Delivering, Recipients = _state.Recipients, Size = stream.Length }; var messageRepository = _container.GetInstance<IMessageRepository>(); await messageRepository.InsertAsync(message, stream); return SmtpCommandReply.CreateDefault250Success(); }
public async Task DeleteAsync(Message message) { if (message.AccountId > 0) throw new InvalidOperationException(string.Format("Use DeleteAsync(account, message)")); var filename = Path.Combine(_dataDirectory, message.Filename); using (var sqlConnection = new MySqlConnection(_connectionString)) { sqlConnection.Open(); await sqlConnection.DeleteAsync(message); await DeleteRecipientsAsync(message, sqlConnection); } File.Delete(filename); }
public async Task<List<DeliveryResult>> DeliverAsync(Message message, List<Recipient> recipients) { if (message == null) throw new ArgumentNullException(nameof(message)); if (message.Recipients == null) throw new ArgumentNullException(nameof(message.Recipients)); var result = new List<DeliveryResult>(); foreach (var localRecipient in recipients) { _log.LogInfo(new LogEvent() { EventType = LogEventType.Application, LogLevel = LogLevel.Info, Message = $"Delivering message from {message.From} to {localRecipient.Address}", Protocol = "SMTPD", }); var localAccount = await _accountRepository.GetByIdAsync(localRecipient.AccountId); // TODO: Check quotas var inbox = await _folderRepository.GetInbox(localAccount.Id); var accountLevelMessage = await _messageRepository.CreateAccountLevelMessageAsync(message, localAccount, inbox); // TODO: Execute rules // TODO: Perform forwarding // TODO: Add Trace headers. // TODO: Etc accountLevelMessage.State = MessageState.Delivered; await _messageRepository.InsertAsync(accountLevelMessage); // Delete the recipient right away, so that if there is a crash we don't end up sending to this recipient again. await _messageRepository.DeleteRecipientAsync(localRecipient); result.Add(new DeliveryResult(localRecipient.Address, ReplyCodeSeverity.Positive, "Message delivered.")); } return result; }
public async Task InsertAsync(Message message) { using (var sqlConnection = new MySqlConnection(_connectionString)) { sqlConnection.Open(); var messageId = await sqlConnection.InsertAsync<long>(message); message.Id = messageId; } foreach (var recipient in message.Recipients) { recipient.MessageId = message.Id; await InsertAsync(recipient); } }
private static async Task DeleteRecipientsAsync(Message message, MySqlConnection sqlConnection) { foreach (var recipient in message.Recipients) { await sqlConnection.DeleteAsync(recipient); } }
private string GetMessageFullFileName(Account account, Message message) { var accountMessageDirectory = GetAccountMessageDirectory(account); var messageDirectory = Path.Combine(accountMessageDirectory, message.Filename.Substring(0, 2)); var messageFileFullPath = Path.Combine(messageDirectory, message.Filename); return messageFileFullPath; }
public Stream GetMessageData(Message message) { var messageFullPath = Path.Combine(_dataDirectory, message.Filename); return File.OpenRead(messageFullPath); }
public Stream GetMessageData(Account account, Message message) { var messageFileFullPath = GetMessageFullFileName(account, message); return File.OpenRead(messageFileFullPath); }
public Task<Message> CreateAccountLevelMessageAsync(Message message, Account account, Folder folder) { if (message == null) throw new ArgumentNullException(nameof(message)); if (account == null) throw new ArgumentNullException(nameof(account)); var clonedMessage = message.Clone(); clonedMessage.Id = 0; clonedMessage.AccountId = account.Id; clonedMessage.FolderId = folder.Id; clonedMessage.Filename = Path.ChangeExtension(Guid.NewGuid().ToString(), ".eml"); clonedMessage.Recipients = new List<Recipient>(); var messageFileFullPath = GetMessageFullFileName(account, clonedMessage); var messageDirectory = Path.GetDirectoryName(messageFileFullPath); // TODO: Should be possible to do this asynchronously. Directory.CreateDirectory(messageDirectory); File.Copy(Path.Combine(_dataDirectory, message.Filename), messageFileFullPath); return Task.FromResult(clonedMessage); }
public async Task DeleteAsync(Account account, Message message) { if (account == null) throw new ArgumentNullException(nameof(account)); if (message == null) throw new ArgumentNullException(nameof(message)); var filename = GetMessageFullFileName(account, message); using (var sqlConnection = new MySqlConnection(_connectionString)) { sqlConnection.Open(); await sqlConnection.DeleteAsync(message); await DeleteRecipientsAsync(message, sqlConnection); } File.Delete(filename); }
private async Task DeliverMessageAsync(Message message) { var accountRepository = _container.GetInstance<IAccountRepository>(); var messageRepository = _container.GetInstance<IMessageRepository>(); var folderRepository = _container.GetInstance<IFolderRepository>(); var dnsClient = _container.GetInstance<IDnsClient>(); message.NumberOfDeliveryAttempts++; bool isLastAttempt = message.NumberOfDeliveryAttempts >= 3; var deliveryResults = new List<DeliveryResult>(); try { var remainingRecipients = new List<Recipient>(message.Recipients); var localDelivery = new LocalDelivery(accountRepository, messageRepository, folderRepository, _log); deliveryResults.AddRange(await localDelivery.DeliverAsync(message, remainingRecipients.Where(recipient => recipient.AccountId != 0).ToList())); var externalDelivery = new ExternalDelivery(messageRepository, dnsClient, _log); deliveryResults.AddRange(await externalDelivery.DeliverAsync(message, remainingRecipients.Where(recipient => recipient.AccountId == 0).ToList())); var failedRecipients = deliveryResults.Where(result => result.ReplyCodeSeverity == ReplyCodeSeverity.PermanentNegative || (isLastAttempt && result.ReplyCodeSeverity == ReplyCodeSeverity.TransientNegative)); await SubmitBounceMessageAsync(message, failedRecipients); var deliveryCompleted = deliveryResults.Any(result => result.ReplyCodeSeverity == ReplyCodeSeverity.TransientNegative); if (isLastAttempt || !deliveryCompleted) { await messageRepository.DeleteAsync(message); } } catch (Exception ex) { var logEvent = new LogEvent() { EventType = LogEventType.Application, LogLevel = LogLevel.Error, Protocol = "SMTPD", }; if (isLastAttempt) logEvent.Message = "Failed delivering message due to an error. Giving up."; else logEvent.Message = "Failed delivering message due to an error. Will retry later."; _log.LogException(logEvent, ex); if (isLastAttempt) { await messageRepository.DeleteAsync(message); } else { await messageRepository.UpdateAsync(message); } } }
private async Task SubmitBounceMessageAsync(Message message, IEnumerable<DeliveryResult> failedRecipients) { if (string.IsNullOrWhiteSpace(message.From)) return; if (IsMailerDaemonAddress(message.From)) return; // TODO: Dont' hardcode this. string bounceMessage = string.Format(@"Your message did not reach some or all of the intended recipients. Sent: %MACRO_SENT% Subject: %MACRO_SUBJECT% The following recipient(s)could not be reached: %MACRO_RECIPIENTS% hMailServer"); throw new NotImplementedException(); }
public async Task<List<DeliveryResult>> DeliverAsync(Message message, List<Recipient> recipients) { if (message == null) throw new ArgumentNullException(nameof(message)); if (message.Recipients == null) throw new ArgumentNullException(nameof(message.Recipients)); var commaSeparatedRecipientList = string.Join(", ", recipients.Select(item => item.Address)); _log.LogInfo(new LogEvent() { EventType = LogEventType.Application, LogLevel = LogLevel.Info, Message = $"Delivering message from {message.From} to {commaSeparatedRecipientList}", Protocol = "SMTPD", }); var recipientsByDomain = recipients.GroupBy(recipient => EmailAddressParser.GetDomain(recipient.Address)).Distinct().ToList(); var result = new List<DeliveryResult>(); foreach (var domainWithRecipients in recipientsByDomain) { var ipAddresses = await _dnsClient.ResolveMxIpAddressesAsync(domainWithRecipients.Key); var remainingRecipientsOnDomain = domainWithRecipients.Select(item => item.Address).ToList(); foreach (var ipAddress in ipAddresses) { var client = new TcpClient(); await client.ConnectAsync(ipAddress, 25); var connection = new Connection(client, CancellationToken.None); using (var messageStream = _messageRepository.GetMessageData(message)) { var messageData = new MessageData() { From = message.From, Recipients = remainingRecipientsOnDomain, Data = messageStream }; var clientSession = new SmtpClientSession(_log, new SmtpClientSessionConfiguration(), messageData); await clientSession.HandleConnection(connection); foreach (var deliveryResult in clientSession.DeliveryResult) { var matchingRecipient = recipients.Single(recipient => string.Equals(recipient.Address, deliveryResult.Recipient, StringComparison.InvariantCultureIgnoreCase)); switch (deliveryResult.ReplyCodeSeverity) { case ReplyCodeSeverity.Positive: // Delete the recipient right away, so that if there is a crash we don't end up sending to this recipient again. await _messageRepository.DeleteRecipientAsync(matchingRecipient); result.Add(deliveryResult); remainingRecipientsOnDomain.Remove(deliveryResult.Recipient); _log.LogInfo(new LogEvent() { EventType = LogEventType.Application, LogLevel = LogLevel.Info, Message = $"Message delivery from {message.From} to {deliveryResult.Recipient} completed", Protocol = "SMTPD", }); break; case ReplyCodeSeverity.PermanentNegative: // Let this recipient be deleted after we've submitted bounce message. This is delayed so that we don't // lose the information if there's a crash. result.Add(deliveryResult); remainingRecipientsOnDomain.Remove(deliveryResult.Recipient); _log.LogInfo(new LogEvent() { EventType = LogEventType.Application, LogLevel = LogLevel.Info, Message = $"Message delivery from {message.From} to {deliveryResult.Recipient} failed permanently: {deliveryResult.ResultMessage}", Protocol = "SMTPD", }); break; case ReplyCodeSeverity.TransientNegative: _log.LogInfo(new LogEvent() { EventType = LogEventType.Application, LogLevel = LogLevel.Info, Message = $"Message delivery from {message.From} to {deliveryResult.Recipient} failed temporarily: {deliveryResult.ResultMessage}", Protocol = "SMTPD", }); break; } } } if (!remainingRecipientsOnDomain.Any()) { // No more recipients remaining. break; } } } return result; }