Пример #1
0
        /// <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);
        }
Пример #2
0
        /// <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);
        }
Пример #3
0
        /// <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);
        }
Пример #4
0
        /// <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);
        }
Пример #5
0
        /// <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);
        }
Пример #6
0
        /// <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);
        }
Пример #7
0
        /// <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);
        }