/// <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 = 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 = BounceIdentifier.NotIdentifiedAsABounce; return(false); }
/// <summary> /// Converts an SMTP status code into a MantaBounceType and MantaBounceCode pairing. /// </summary> /// <param name="smtpCode">A standard SMTP code, e.g. "550".</param> /// <returns>The appropriate MantaBounceCode for the SMTP code provided in <paramref name="smtpCode"/>.</returns> internal BouncePair ConvertSmtpCodeToMantaBouncePair(int smtpCode) { BouncePair bp = new BouncePair(); // Based on the first character of the SMTP code, we can tell if it's not actually a bounce // or at least get the BounceType (whether it's a permanent or temporary problem). char codeClass = smtpCode.ToString()[0]; if (codeClass == '2' || codeClass == '3') { // All done - not a bounce. bp.BounceType = MantaBounceType.Unknown; bp.BounceCode = MantaBounceCode.NotABounce; return(bp); } else if (codeClass == '4') { // Temporary problem. bp.BounceType = MantaBounceType.Soft; } else if (codeClass == '5') { // Permanent problem. bp.BounceType = MantaBounceType.Hard; } else { // Perhaps not a valid SMTP code at all. bp.BounceType = MantaBounceType.Unknown; bp.BounceCode = MantaBounceCode.Unknown; return(bp); } switch (smtpCode) { case 200: // (nonstandard success response, see rfc876) case 211: // System status, or system help reply case 214: // Help message case 220: // SMTP Service ready. case 221: // Service closing transmission channel case 250: // Requested mail action okay, completed case 251: // The recipient is not local to the server, but the server will accept and forward the message. case 252: // The recipient cannot be VRFYed, but the server accepts the message and attempts delivery. case 354: // Start mail input; end with <CRLF>.<CRLF> bp.BounceType = MantaBounceType.Unknown; bp.BounceCode = MantaBounceCode.NotABounce; return(bp); case 420: // Timeout communication problem encountered during transmission case 421: // Service not available, closing transmission channel case 521: // <domain> does not accept mail (see rfc1846) case 530: // Access denied (???a Sendmailism) bp.BounceCode = MantaBounceCode.ServiceUnavailable; break; case 431: // Receiving mail server's disk is full case 452: // Requested action not taken: insufficient system storage case 552: // Requested mail action aborted: exceeded storage allocation bp.BounceCode = MantaBounceCode.MailboxFull; break; case 450: // Requested mail action not taken: mailbox unavailable case 550: // Requested action not taken: mailbox unavailable case 551: // User not local; please try <forward-path> case 553: // Requested action not taken: mailbox name not allowed bp.BounceCode = MantaBounceCode.BadEmailAddress; break; case 571: // Message refused. bp.BounceCode = MantaBounceCode.RelayDenied; break; case 451: // Requested action aborted: local error in processing case 500: // Syntax error, command unrecognised case 501: // Syntax error in parameters or arguments case 502: // Command not implemented case 503: // Bad sequence of commands case 504: // Command parameter not implemented case 554: // Transaction failed bp.BounceCode = MantaBounceCode.General; break; default: bp.BounceCode = MantaBounceCode.Unknown; break; } return(bp); }
/// <summary> /// Converts a Non-Delivery Report (NDR) code to a MantaBounceType and MantaBounceCode. /// </summary> /// <param name="smtpCode">An NDR code, e.g. "4.4.7". See here for more: /// http://tools.ietf.org/html/rfc3463.</param> /// <returns>A BouncePair object with the appropriate MantaBounceCode and MantaBounceType values /// for the NDR code provided in <paramref name="ndrCode"/>.</returns> internal BouncePair ConvertNdrCodeToMantaBouncePair(string ndrCode) { BouncePair bp = new BouncePair(); int firstDotPos = ndrCode.IndexOf('.'); // If it ain't got no dots, it ain't a proper NDR code. if (firstDotPos == -1) { bp.BounceType = MantaBounceType.Unknown; bp.BounceCode = MantaBounceCode.Unknown; return(bp); } // Identify if it's a temporary or permanent bounce (or even not one at all). if (ndrCode.StartsWith("2") || ndrCode.StartsWith("3")) { // All done - not a bounce. bp.BounceType = MantaBounceType.Unknown; bp.BounceCode = MantaBounceCode.NotABounce; return(bp); } if (ndrCode.StartsWith("4.")) { bp.BounceType = MantaBounceType.Soft; } else if (ndrCode.StartsWith("5.")) { bp.BounceType = MantaBounceType.Hard; } else { bp.BounceType = MantaBounceType.Unknown; bp.BounceCode = MantaBounceCode.Unknown; return(bp); } // Check the rest of the code. string endPart = ndrCode.Substring(firstDotPos); // List of status codes as per RFC 3463 (). // // TODO BenC (2013-07-08): Needs refining/reviewing as just did a rough first pass through. switch (endPart) { case ".0.0": // "Other undefined Status". Should be used for all errors for which only the class of the error is known. bp.BounceCode = MantaBounceCode.BounceUnknown; break; case ".1.5": // Destination mailbox address valid bp.BounceCode = MantaBounceCode.NotABounce; break; case ".1.0": // Other address status case ".1.1": // Bad destination mailbox address case ".1.2": // Bad destination system address case ".1.3": // Bad destination mailbox address syntax case ".1.4": // Destination mailbox address ambiguous case ".1.6": // Mailbox has moved case ".2.0": // Other or undefined mailbox status case ".2.1": // Mailbox disabled, not accepting messages bp.BounceCode = MantaBounceCode.BadEmailAddress; break; case ".2.2": // Mailbox full case ".3.1": // Mail system full bp.BounceCode = MantaBounceCode.MailboxFull; break; case ".2.3": // Message length exceeds administrative limit. case ".3.4": // Message too big for system bp.BounceCode = MantaBounceCode.MessageSizeTooLarge; break; case ".2.4": // Mailing list expansion problem case ".3.0": // Other or undefined mail system status case ".3.2": // System not accepting network messages case ".3.3": // System not capable of selected features case ".4.0": // Other or undefined network or routing status case ".4.1": // No answer from host case ".4.2": // Bad connection case ".4.3": // Routing server failure case ".4.4": // Unable to route case ".4.5": // Network congestion case ".4.6": // Routing loop detected case ".4.7": // Delivery time expired case ".5.0": // Other or undefined protocol status bp.BounceCode = MantaBounceCode.UnableToConnect; break; case ".1.7": // Bad sender's mailbox address syntax case ".1.8": // Bad sender's system address bp.BounceCode = MantaBounceCode.ConfigurationErrorWithSendingAddress; break; // BenC (2013-08-21): Don't know why these are commented out (doesn't affect things as they're handled by default anyway), but leaving for now. /* * case ".5.1": // Invalid command * case ".5.2": // Syntax error * case ".5.3": // Too many recipients * case ".5.4": // Invalid command arguments * case ".5.5": // Wrong protocol version * case ".6.0": // Other or undefined media error * case ".6.1": // Media not supported * case ".6.2": // Conversion required and prohibited * case ".6.3": // Conversion required but not supported * case ".6.4": // Conversion with loss performed * case ".6.5": // Conversion failed * case ".7.0": // Other or undefined security status * case ".7.1": // Delivery not authorized, message refused * case ".7.2": // Mailing list expansion prohibited * case ".7.3": // Security conversion required but not possible * case ".7.4": // Security features not supported * case ".7.5": // Cryptographic failure * case ".7.6": // Cryptographic algorithm not supported * case ".7.7": // Message integrity failure */ default: // Do additional processing if no matches above. bp.BounceCode = MantaBounceCode.General; break; } return(bp); }
/// <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 = 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 = 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 = 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 = 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 = 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 = 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 = BounceIdentifier.NotIdentifiedAsABounce; bounceIdentification.MatchingValue = string.Empty; return(false); }