/// <summary> /// Attempts to find and process the reason an email bounced by digging through all body parts. /// </summary> /// <param name="bodyParts">Array of BodyParts to dig through; may include child body parts.</param> /// <param name="bouncePair">out. A BouncePair object containing details of the bounce (if a reason was found).</param> /// <param name="bounceMessage">out. The text to use as the reason found why the bounce occurred.</param> /// <returns>true if a reason was found, else false.</returns> internal bool FindBounceReason(BodyPart[] bodyParts, out BouncePair bouncePair, out string bounceMessage, out EmailProcessingDetails bounceIdentification) { foreach (BodyPart b in bodyParts) { if (b.ContentType.MediaType.StartsWith("multipart/", StringComparison.OrdinalIgnoreCase)) { // Just a container for other body parts so check them. if (FindBounceReason(b.BodyParts, out bouncePair, out bounceMessage, out bounceIdentification)) { return(true); } } else if (b.ContentType.MediaType.Equals("text/plain", StringComparison.OrdinalIgnoreCase)) { // Only useful to examine text/plain body parts (so not "image/gif" etc). if (ParseBounceMessage(b.GetDecodedBody(), out bouncePair, out bounceMessage, out bounceIdentification)) { return(true); } } // else // Console.WriteLine("\tSkipped bodypart \"" + b.ContentType.MediaType + "\"."); } // Still here so haven't found anything useful. bouncePair.BounceCode = MantaBounceCode.Unknown; bouncePair.BounceType = MantaBounceType.Unknown; bounceMessage = string.Empty; bounceIdentification = new EmailProcessingDetails(); bounceIdentification.BounceIdentifier = Core.Enums.BounceIdentifier.NotIdentifiedAsABounce; return(false); }
/// <summary> /// Examines an SMTP response that is thought to relate to delivery of an email failing. /// </summary> /// <param name="message">An SMTP response either found as the "Diagnostic-Code" value in a Non-Delivery Report /// or received directly from another MTA in an SMTP session.</param> /// <param name="bouncePair">out. Details of the Bounce (or not) based on details found in <paramref name="message"/>.</param> /// <param name="bounceMessage">out. The message found that indicated a Bounce or string.Empty if it wasn't /// found to indicate a bounce.</param> /// <returns>true if a bounce was positively identified, else false.</returns> internal bool ParseSmtpDiagnosticCode(string message, out BouncePair bouncePair, out string bounceMessage, out EmailProcessingDetails bounceIdentification) { // Remove "smtp;[possible whitespace]" if it appears at the beginning of the message. if (message.StartsWith("smtp;", StringComparison.OrdinalIgnoreCase)) { message = message.Substring("smtp;".Length).Trim(); } if (ParseBounceMessage(message, out bouncePair, out bounceMessage, out bounceIdentification)) { return(true); } bounceIdentification.BounceIdentifier = Core.Enums.BounceIdentifier.NotIdentifiedAsABounce; return(false); }
/// <summary> /// Attempts to find the reason for the bounce by running Bounce Rules, then checking for Non-Delivery Report codes, /// and finally checking for SMTP codes. /// </summary> /// <param name="message">Could either be an email body part or a single or multiple line response from another MTA.</param> /// <param name="bouncePair">out.</param> /// <param name="bounceMessage">out.</param> /// <returns>true if a positive match in <paramref name="message"/> was found indicating a bounce, else false.</returns> internal bool ParseBounceMessage(string message, out BouncePair bouncePair, out string bounceMessage, out EmailProcessingDetails bounceIdentification) { bounceIdentification = new EmailProcessingDetails(); // Check all Bounce Rules for a match. foreach (BounceRule r in BounceRulesManager.BounceRules) { // If we get a match, we're done processing Rules. if (r.IsMatch(message, out bounceMessage)) { bouncePair.BounceType = r.BounceTypeIndicated; bouncePair.BounceCode = r.BounceCodeIndicated; bounceMessage = message; bounceIdentification.BounceIdentifier = Core.Enums.BounceIdentifier.BounceRule; bounceIdentification.MatchingBounceRuleID = r.RuleID; bounceIdentification.MatchingValue = r.Criteria; return(true); } } // No Bounce Rules match the message so try to get a match on an NDR code ("5.1.1") or an SMTP code ("550"). // TODO: Handle several matches being found - somehow find The Best? // Pattern: Should match like this: // [anything at the beginning if present][then either an SMTP code or an NDR code, but both should be grabbed if // they exist][then the rest of the content (if any)] Match match = Regex.Match(message, RegexPatterns.SmtpResponse, RegexOptions.Singleline | RegexOptions.ExplicitCapture); if (match.Success) { bounceMessage = match.Value; // Check for anything useful with the NDR code first as it contains more specific detail than the SMTP code. if (match.Groups["NdrCode"].Success && match.Groups["NdrCode"].Length > 0) { bouncePair = BounceRulesManager.Instance.ConvertNdrCodeToMantaBouncePair(match.Groups["NdrCode"].Value); if (bouncePair.BounceType != MantaBounceType.Unknown) { bounceIdentification.BounceIdentifier = Core.Enums.BounceIdentifier.NdrCode; bounceIdentification.MatchingValue = match.Groups["NdrCode"].Value; return(true); } } // Try the SMTP code as there wasn't an NDR. if (match.Groups["SmtpCode"].Success && match.Groups["SmtpCode"].Length > 0) { bouncePair = BounceRulesManager.Instance.ConvertSmtpCodeToMantaBouncePair(Int32.Parse(match.Groups["SmtpCode"].Value)); bounceIdentification.BounceIdentifier = Core.Enums.BounceIdentifier.SmtpCode; bounceIdentification.MatchingValue = match.Groups["SmtpCode"].Value; return(true); } } // Failed to identify a reason so shouldn't be a bounce. bouncePair.BounceCode = MantaBounceCode.Unknown; bouncePair.BounceType = MantaBounceType.Unknown; bounceMessage = string.Empty; bounceIdentification.BounceIdentifier = Core.Enums.BounceIdentifier.NotIdentifiedAsABounce; bounceIdentification.MatchingValue = string.Empty; return(false); }
/// <summary> /// Examines an SMTP response message to identify detailed bounce information from it. /// </summary> /// <param name="response">The message that's come back from an external MTA when attempting to send an email.</param> /// <param name="rcptTo">The email address that was being sent to.</param> /// <param name="internalSendID">The internal Manta SendID.</param> /// <returns>True if a bounce was found and recorded, false if not.</returns> internal bool ProcessSmtpResponseMessage(string response, string rcptTo, int internalSendID, out EmailProcessingDetails bounceIdentification) { bounceIdentification = new EmailProcessingDetails(); // Check for TimedOutInQueue message first. if (response.Equals(MtaParameters.TIMED_OUT_IN_QUEUE_MESSAGE, StringComparison.OrdinalIgnoreCase)) { bounceIdentification = null; MantaTimedOutInQueueEvent timeOut = new MantaTimedOutInQueueEvent { EventType = MantaEventType.TimedOutInQueue, EmailAddress = rcptTo, SendID = SendDB.GetSend(internalSendID).ID, EventTime = DateTime.UtcNow }; // Log to DB. Save(timeOut); // All done return true. return(true); } BouncePair bouncePair = new BouncePair(); string bounceMessage = string.Empty; if (ParseBounceMessage(response, out bouncePair, out bounceMessage, out bounceIdentification)) { // Were able to find the bounce so create the bounce event. MantaBounceEvent bounceEvent = new MantaBounceEvent { EventType = MantaEventType.Bounce, EmailAddress = rcptTo, BounceInfo = bouncePair, SendID = SendDB.GetSend(internalSendID).ID, // It is possible that the bounce was generated a while back, but we're assuming "now" for the moment. // Might be good to get the DateTime found in the email at a later point. EventTime = DateTime.UtcNow, Message = response }; // Log to DB. Save(bounceEvent); // All done return true. return(true); } // Couldn't identify the bounce. bounceIdentification.BounceIdentifier = Core.Enums.BounceIdentifier.NotIdentifiedAsABounce; return(false); }
/// <summary> /// Examines a non-delivery report for detailed bounce information. /// </summary> /// <param name="message"></param> /// <param name="bounceType"></param> /// <param name="bounceCode"></param> /// <param name="bounceMessage"></param> /// <returns></returns> internal bool ParseNdr(string message, out BouncePair bouncePair, out string bounceMessage, out EmailProcessingDetails bounceIdentification) { bounceIdentification = new EmailProcessingDetails(); // Check for the Diagnostic-Code as hopefully contains more information about the error. const string DiagnosticCodeFieldName = "Diagnostic-Code: "; const string StatusFieldName = "Status: "; StringBuilder diagnosticCode = new StringBuilder(string.Empty); string status = string.Empty; using (StringReader sr = new StringReader(message)) { string line = sr.ReadToCrLf(); // While the string reader has stuff to read keep looping through each line. while (!string.IsNullOrWhiteSpace(line) || sr.Peek() > -1) { if (line.StartsWith(DiagnosticCodeFieldName, StringComparison.OrdinalIgnoreCase)) { // Found the diagnostic code. // Remove the field name. line = line.Substring(DiagnosticCodeFieldName.Length); // Check to see if the disagnostic-code contains an SMTP response. bool isSmtpResponse = line.StartsWith("smtp;", StringComparison.OrdinalIgnoreCase); // Add the first line of the diagnostic-code. diagnosticCode.AppendLine(line); // Will be set to true when we find the next non diagnostic-code line. bool foundNextLine = false; // Loop to read multiline diagnostic-code. while (!foundNextLine) { if (sr.Peek() == -1) { break; // We've reached the end of the string! } // Read the next line. line = sr.ReadToCrLf(); if (isSmtpResponse) { // Diagnostic code is an SMTP response so look for SMTP response line. if (Regex.IsMatch(line, @"\d{3}(-|\s)")) { diagnosticCode.AppendLine(line); } else // Not a SMTP response line so must be next NDR line. { foundNextLine = true; } } else { // Non SMTP response. If first char is whitespace then it's part of the disagnostic-code otherwise it isn't. if (char.IsWhiteSpace(line[0])) { diagnosticCode.AppendLine(line); } else { foundNextLine = true; } } } } else { // We haven't found a diagnostic-code line. // Check to see if we have found a status field. if (line.StartsWith(StatusFieldName, StringComparison.OrdinalIgnoreCase)) { status = line.Substring(StatusFieldName.Length).TrimEnd(); } // If there is more of the string to read then read the next line, otherwise set line to string.empty. if (sr.Peek() > -1) { line = sr.ReadToCrLf(); } else { line = string.Empty; } } } } // Process what we've managed to find... // Diagnostic-Code if (!string.IsNullOrWhiteSpace(diagnosticCode.ToString())) { if (ParseSmtpDiagnosticCode(diagnosticCode.ToString(), out bouncePair, out bounceMessage, out bounceIdentification)) { return(true); } } // Status if (!string.IsNullOrWhiteSpace(status)) { // If there's an NDR code in the Status value, use it. Match m = Regex.Match(status, RegexPatterns.NonDeliveryReportCode, RegexOptions.ExplicitCapture); if (m.Success) { bouncePair = BounceRulesManager.Instance.ConvertNdrCodeToMantaBouncePair(m.Value); bounceMessage = m.Value; bounceIdentification.BounceIdentifier = Core.Enums.BounceIdentifier.NdrCode; bounceIdentification.MatchingValue = m.Value; return(true); } } // If we've not already returned from this method, then we're still looking for an explanation // for the bounce so parse the entire message as a string. if (ParseBounceMessage(message, out bouncePair, out bounceMessage, out bounceIdentification)) { return(true); } // Nope - no clues relating to why the bounce occurred. bouncePair.BounceType = MantaBounceType.Unknown; bouncePair.BounceCode = MantaBounceCode.Unknown; bounceMessage = string.Empty; bounceIdentification.BounceIdentifier = Core.Enums.BounceIdentifier.NotIdentifiedAsABounce; bounceIdentification.MatchingValue = string.Empty; return(false); }