/// <summary> /// Examines an email to try to identify detailed bounce information from it. /// </summary> /// <param name="filename">Path and filename of the file being processed.</param> /// <param name="message">The entire text content for an email (headers and body).</param> /// <returns>An EmailProcessingResult value indicating whether the the email was /// successfully processed or not.</returns> public EmailProcessingDetails ProcessBounceEmail(string message) { EmailProcessingDetails bounceDetails = new EmailProcessingDetails(); MimeMessage msg = MimeMessage.Parse(message); if (msg == null) { bounceDetails.ProcessingResult = EmailProcessingResult.ErrorContent; bounceDetails.BounceIdentifier = BounceIdentifier.NotIdentifiedAsABounce; return(bounceDetails); } // "X-Recipient" should contain what Manta originally set as the "return-path" when sending. MessageHeader returnPath = msg.Headers.GetFirstOrDefault("X-Recipient"); if (returnPath == null) { bounceDetails.ProcessingResult = EmailProcessingResult.ErrorNoReturnPath; bounceDetails.BounceIdentifier = BounceIdentifier.UnknownReturnPath; return(bounceDetails); } string rcptTo = string.Empty; int internalSendID = 0; if (!ReturnPathManager.TryDecode(returnPath.Value, out rcptTo, out internalSendID)) { // Not a valid Return-Path so can't process. bounceDetails.ProcessingResult = EmailProcessingResult.ErrorNoReturnPath; bounceDetails.BounceIdentifier = BounceIdentifier.UnknownReturnPath; return(bounceDetails); } MantaBounceEvent bounceEvent = new MantaBounceEvent(); bounceEvent.EmailAddress = rcptTo; bounceEvent.SendID = SendDB.GetSend(internalSendID).ID; // TODO: Might be good to get the DateTime found in the email. bounceEvent.EventTime = DateTime.UtcNow; // These properties are both set according to the SMTP code we find, if any. bounceEvent.BounceInfo.BounceCode = MantaBounceCode.Unknown; bounceEvent.BounceInfo.BounceType = MantaBounceType.Unknown; bounceEvent.EventType = MantaEventType.Bounce; // First, try to find a NonDeliveryReport body part as that's the proper way for an MTA // to tell us there was an issue sending the email. BouncePair bouncePair; string bounceMsg; BodyPart deliveryReportBodyPart; string deliveryReport = string.Empty; if (FindFirstBodyPartByMediaType(msg.BodyParts, "message/delivery-status", out deliveryReportBodyPart)) { // If we've got a delivery report, check it for info. // Abuse report content may have long lines whitespace folded. deliveryReport = MimeMessage.UnfoldHeaders(deliveryReportBodyPart.GetDecodedBody()); if (ParseNdr(deliveryReport, out bouncePair, out bounceMsg, out bounceDetails)) { // Successfully parsed. bounceEvent.BounceInfo = bouncePair; bounceEvent.Message = bounceMsg; // Write BounceEvent to DB. Save(bounceEvent); bounceDetails.ProcessingResult = EmailProcessingResult.SuccessBounce; return(bounceDetails); } } // We're still here so there was either no NDR part or nothing contained within it that we could // interpret so have to check _all_ body parts for something useful. if (FindBounceReason(msg.BodyParts, out bouncePair, out bounceMsg, out bounceDetails)) { bounceEvent.BounceInfo = bouncePair; bounceEvent.Message = bounceMsg; // Write BounceEvent to DB. Save(bounceEvent); bounceDetails.ProcessingResult = EmailProcessingResult.SuccessBounce; return(bounceDetails); } // Nope - no clues relating to why the bounce occurred. bounceEvent.BounceInfo.BounceType = MantaBounceType.Unknown; bounceEvent.BounceInfo.BounceCode = MantaBounceCode.Unknown; bounceEvent.Message = string.Empty; bounceDetails.BounceIdentifier = BounceIdentifier.NotIdentifiedAsABounce; bounceDetails.ProcessingResult = EmailProcessingResult.Unknown; return(bounceDetails); }
/// <summary> /// Looks through a feedback loop email looking for something to identify it as an abuse report and who it relates to. /// If found, logs the event. /// /// How to get the info depending on the ESP (and this is likely to be the best order to check for each too): /// Abuse Report Original-Mail-From. [Yahoo] /// Message-ID from body part child with content-type of message/rfc822. [AOL] /// Return-Path in main message headers. [Hotmail] /// </summary> /// <param name="message">The feedback look email.</param> public EmailProcessingDetails ProcessFeedbackLoop(string content) { EmailProcessingDetails processingDetails = new EmailProcessingDetails(); MimeMessage message = MimeMessage.Parse(content); if (message == null) { processingDetails.ProcessingResult = EmailProcessingResult.ErrorContent; return(processingDetails); } try { // Step 1: Yahoo! provide useable Abuse Reports (AOL's are all redacted). //Look for abuse report BodyPart abuseBodyPart = null; if (this.FindFirstBodyPartByMediaType(message.BodyParts, "message/feedback-report", out abuseBodyPart)) { // Found an abuse report body part to examine. // Abuse report content may have long lines whitespace folded. string abuseReportBody = MimeMessage.UnfoldHeaders(abuseBodyPart.GetDecodedBody()); using (StringReader reader = new StringReader(abuseReportBody)) { while (reader.Peek() > -1) { string line = reader.ReadToCrLf(); // The original mail from value will be the return-path we'd set so we should be able to get all the values we need from that. if (line.StartsWith("Original-Mail-From:", StringComparison.OrdinalIgnoreCase)) { string tmp = line.Substring("Original-Mail-From: ".Length - 1); try { int internalSendID = -1; string rcptTo = string.Empty; if (ReturnPathManager.TryDecode(tmp, out rcptTo, out internalSendID)) { // NEED TO LOG TO DB HERE!!!!! Send snd = SendDB.GetSend(internalSendID); Save(new MantaAbuseEvent { EmailAddress = rcptTo, EventTime = DateTime.UtcNow, EventType = MantaEventType.Abuse, SendID = (snd == null ? string.Empty : snd.ID) }); processingDetails.ProcessingResult = EmailProcessingResult.SuccessAbuse; return(processingDetails); } } catch (Exception) { // Must be redacted break; } } } } } // Function to use against BodyParts to find a return-path header. Func <MessageHeaderCollection, bool> checkForReturnPathHeaders = delegate(MessageHeaderCollection headers) { MessageHeader returnPathHeader = headers.GetFirstOrDefault("Return-Path"); if (returnPathHeader != null && !string.IsNullOrWhiteSpace(returnPathHeader.Value)) { int internalSendID = -1; string rcptTo = string.Empty; if (ReturnPathManager.TryDecode(returnPathHeader.Value, out rcptTo, out internalSendID)) { if (!rcptTo.StartsWith("redacted@", StringComparison.OrdinalIgnoreCase)) { // NEED TO LOG TO DB HERE!!!!! Send snd = SendDB.GetSend(internalSendID); Save(new MantaAbuseEvent { EmailAddress = rcptTo, EventTime = DateTime.UtcNow, EventType = MantaEventType.Abuse, SendID = (snd == null ? string.Empty : snd.ID) }); return(true); } } } MessageHeader messageIdHeader = headers.GetFirstOrDefault("Message-ID"); if (messageIdHeader != null && messageIdHeader.Value.Length > 33) { string tmp = messageIdHeader.Value.Substring(1, 32); Guid messageID; if (Guid.TryParse(tmp, out messageID)) { int internalSendID = -1; string rcptTo = string.Empty; tmp = ReturnPathManager.GetReturnPathFromMessageID(messageID); if (ReturnPathManager.TryDecode(tmp, out rcptTo, out internalSendID)) { // NEED TO LOG TO DB HERE!!!!! Send snd = SendDB.GetSend(internalSendID); Save(new MantaAbuseEvent { EmailAddress = rcptTo, EventTime = DateTime.UtcNow, EventType = MantaEventType.Abuse, SendID = (snd == null ? string.Empty : snd.ID) }); return(true); } } } return(false); } ; // Step 2: AOL give redacted Abuse Reports but include the original email as a bodypart; find that. BodyPart childMessageBodyPart; if (FindFirstBodyPartByMediaType(message.BodyParts, "message/rfc822", out childMessageBodyPart)) { if (childMessageBodyPart.ChildMimeMessage != null) { if (checkForReturnPathHeaders(childMessageBodyPart.ChildMimeMessage.Headers)) { processingDetails.ProcessingResult = EmailProcessingResult.SuccessAbuse; return(processingDetails); } } } // Step 3: Hotmail don't do Abuse Reports, they just return our email to us exactly as we sent it. if (checkForReturnPathHeaders(message.Headers)) { processingDetails.ProcessingResult = EmailProcessingResult.SuccessAbuse; return(processingDetails); } } catch (Exception) { } Logging.Debug("Failed to find return path!"); processingDetails.ProcessingResult = EmailProcessingResult.ErrorNoReturnPath; return(processingDetails); }
/// <summary> /// Queues the email for relaying. /// </summary> private async Task <SmtpServerTransactionAsyncResult> QueueForRelayingAsync() { // The email is for relaying. Guid messageID = Guid.NewGuid(); // Look for any MTA control headers. MessageHeaderCollection headers = MessageManager.GetMessageHeaders(Data); // Will not be null if the SendGroupID header was present. MessageHeader ipGroupHeader = headers.SingleOrDefault(m => m.Name.Equals(MessageHeaderNames.SendGroupID, StringComparison.OrdinalIgnoreCase)); // Parameter will hold the MtaIPGroup that will be used to relay this message. VirtualMtaGroup mtaGroup = null; int ipGroupID = 0; if (ipGroupHeader != null) { if (int.TryParse(ipGroupHeader.Value, out ipGroupID)) { mtaGroup = VirtualMtaManager.GetVirtualMtaGroup(ipGroupID); } } #region Look for a send id, if one doesn't exist create it. MessageHeader sendIdHeader = headers.SingleOrDefault(h => h.Name.Equals(MessageHeaderNames.SendID, StringComparison.OrdinalIgnoreCase)); int internalSendId = -1; if (sendIdHeader != null) { Send sndID = await SendManager.Instance.GetSendAsync(sendIdHeader.Value); if (sndID.SendStatus == SendStatus.Discard) { return(SmtpServerTransactionAsyncResult.FailedSendDiscarding); } internalSendId = sndID.InternalID; } else { Send sndID = await SendManager.Instance.GetDefaultInternalSendIdAsync(); if (sndID.SendStatus == SendStatus.Discard) { return(SmtpServerTransactionAsyncResult.FailedSendDiscarding); } internalSendId = sndID.InternalID; } #endregion #region Generate Return Path string returnPath = string.Empty; // Can only return path to messages with one rcpt to if (RcptTo.Count == 1) { // Need to check to see if the message contains a return path overide domain. MessageHeader returnPathDomainOverrideHeader = headers.SingleOrDefault(h => h.Name.Equals(MessageHeaderNames.ReturnPathDomain, StringComparison.OrdinalIgnoreCase)); if (returnPathDomainOverrideHeader != null && MtaParameters.LocalDomains.Count(d => d.Hostname.Equals(returnPathDomainOverrideHeader.Value, StringComparison.OrdinalIgnoreCase)) > 0) { // The message contained a local domain in the returnpathdomain // header so use it instead of the default. returnPath = ReturnPathManager.GenerateReturnPath(RcptTo[0], internalSendId, returnPathDomainOverrideHeader.Value); } else { // The message didn't specify a return path overide or it didn't // contain a localdomain so use the default. returnPath = ReturnPathManager.GenerateReturnPath(RcptTo[0], internalSendId); } // Insert the return path header. Data = MessageManager.AddHeader(Data, new MessageHeader("Return-Path", "<" + returnPath + ">")); } else { // multiple rcpt's so can't have unique return paths, use generic mail from. returnPath = MailFrom; } #endregion #region Generate a message ID header string msgIDHeaderVal = "<" + messageID.ToString("N") + MailFrom.Substring(MailFrom.LastIndexOf("@")) + ">"; // If there is already a message header, remove it and add our own. required for feedback loop processing. if (headers.Count(h => h.Name.Equals("Message-ID", StringComparison.OrdinalIgnoreCase)) > 0) { Data = MessageManager.RemoveHeader(Data, "Message-ID"); } // Add the new message-id header. Data = MessageManager.AddHeader(Data, new MessageHeader("Message-ID", msgIDHeaderVal)); #endregion #region Get message priority var msgPriority = RabbitMqPriority.Low; var priorityHeader = headers.GetFirstOrDefault(MessageHeaderNames.Priority); if (priorityHeader != null) { var outVal = 0; if (int.TryParse(priorityHeader.Value, out outVal)) { if (outVal >= 0) { msgPriority = outVal < 3 ? (RabbitMqPriority)(byte)outVal : RabbitMqPriority.High; } } } #endregion // Remove any control headers. headers = new MessageHeaderCollection(headers.Where(h => h.Name.StartsWith(MessageHeaderNames.HeaderNamePrefix, StringComparison.OrdinalIgnoreCase))); foreach (MessageHeader header in headers) { Data = MessageManager.RemoveHeader(Data, header.Name); } // If the MTA group doesn't exist or it's not got any IPs, use the default. if (mtaGroup == null || mtaGroup.VirtualMtaCollection.Count == 0) { ipGroupID = VirtualMtaManager.GetDefaultVirtualMtaGroup().ID; } // Attempt to Enqueue the Email for Relaying. var enqueued = await QueueManager.Instance.Enqueue(messageID, ipGroupID, internalSendId, returnPath, RcptTo.ToArray(), Data, msgPriority); return(enqueued ? SmtpServerTransactionAsyncResult.SuccessMessageQueued : SmtpServerTransactionAsyncResult.FailedToEnqueue); }