/// <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> /// Compares two EmailProcessingDetails objects to see if they are equal. /// </summary> /// <param name="obj">Another EmailProcessingDetails object to compare to this one.</param> /// <returns>true if the two objects have the same value, else false.</returns> public override bool Equals(object obj) { if (!(obj is EmailProcessingDetails)) { return(false); } EmailProcessingDetails otherObj = obj as EmailProcessingDetails; if (this.ProcessingResult == otherObj.ProcessingResult && this.BounceIdentifier == otherObj.BounceIdentifier && this.MatchingBounceRuleID == otherObj.MatchingBounceRuleID && this.MatchingValue == otherObj.MatchingValue) { return(true); } else { return(false); } }
/// <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!!!!! Sends.Send snd = MantaMTA.Core.DAL.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!!!!! Sends.Send snd = MantaMTA.Core.DAL.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!!!!! Sends.Send snd = MantaMTA.Core.DAL.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> /// 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 = Core.Enums.BounceIdentifier.NotIdentifiedAsABounce; return(bounceDetails); } // "x-receiver" should contain what Manta originally set as the "return-path" when sending. MessageHeader returnPath = msg.Headers.GetFirstOrDefault("x-receiver"); if (returnPath == null) { bounceDetails.ProcessingResult = EmailProcessingResult.ErrorNoReturnPath; bounceDetails.BounceIdentifier = Core.Enums.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 = Core.Enums.BounceIdentifier.UnknownReturnPath; return(bounceDetails); } MantaBounceEvent bounceEvent = new MantaBounceEvent(); bounceEvent.EmailAddress = rcptTo; bounceEvent.SendID = MantaMTA.Core.DAL.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 = Core.Enums.BounceIdentifier.NotIdentifiedAsABounce; bounceDetails.ProcessingResult = EmailProcessingResult.Unknown; return(bounceDetails); }
/// <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 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> /// 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); }
/// <summary> /// DirectoryHandler provides a standard way of processing a directory of email files. /// </summary> /// <param name="path">The filepath to operate on.</param> /// <param name="fileProcessor">A delegate method that will be used to process each file found in <paramref name="path"/>.</param> /// <param name="logger">A delegate method that will be used to return information to an interface, e.g. to /// display messages to a user.</param> private void DirectoryHandler(string path, Func <string, EmailProcessingDetails> fileProcessor) { // A filter to use when pulling out files to process; likely to be "*.eml". string fileSearchPattern = "*.eml"; FileInfo[] files = new DirectoryInfo(path).GetFiles(fileSearchPattern); // Keep going until there aren't any more files to process, then we wait for the FileSystemWatcher to nudge us // back into life again. do { // Loop through and process all the files we've picked up. Parallel.ForEach <FileInfo>(files, delegate(FileInfo f) { if (_IsStopping) { return; } if (!File.Exists(f.FullName)) { Logging.Debug(String.Format("File not found: \"{0}\".", f.FullName)); return; } // If a file's not accessible, skip it so we'll pick it up the next time. if (IsFileLocked(f)) { return; } string content = File.ReadAllText(f.FullName); // Send the content to the delegate method that'll process its contents. EmailProcessingDetails processingDetails = fileProcessor(content); switch (processingDetails.ProcessingResult) { case EmailProcessingResult.SuccessAbuse: // All good. Nothing to do other than delete the file. File.Delete(f.FullName); break; case EmailProcessingResult.SuccessBounce: // To enable reviewing of bounce emails and how Manta has processed them, a flag can be set that // keeps processed files. Files that result in an error when being processed are always kept. if (MtaParameters.KeepBounceFiles) { // Retain files. string keepPath = Path.Combine(Path.GetDirectoryName(f.FullName), _SubdirectoryForProcessedBounceEmails, processingDetails.BounceIdentifier.ToString()); switch (processingDetails.BounceIdentifier) { case MantaMTA.Core.Enums.BounceIdentifier.BounceRule: keepPath = Path.Combine(keepPath, processingDetails.MatchingBounceRuleID.ToString()); break; case MantaMTA.Core.Enums.BounceIdentifier.NdrCode: case MantaMTA.Core.Enums.BounceIdentifier.SmtpCode: keepPath = Path.Combine(keepPath, processingDetails.MatchingValue); break; } Directory.CreateDirectory(keepPath); File.Move(f.FullName, Path.Combine(keepPath, f.Name)); } else { // All good. Nothing to do other than delete the file. File.Delete(f.FullName); } break; case EmailProcessingResult.ErrorNoFile: throw new FileNotFoundException("Failed to locate file to process: \"" + f.Name + "\"."); case EmailProcessingResult.Unknown: case EmailProcessingResult.ErrorContent: case EmailProcessingResult.ErrorNoReason: default: // Move the file into a separate directory, handling any issues of duplicate filenames. bool moved = false; int version = 0; Stopwatch sw = new Stopwatch(); sw.Start(); while (!moved) { try { if (sw.Elapsed > TimeSpan.FromSeconds(10)) { throw new TimeoutException(); } if (version > 0) { File.Move(f.FullName, Path.Combine(Path.GetDirectoryName(f.FullName), _SubdirectoryForProblemEmails, Path.GetFileNameWithoutExtension(f.Name) + "_" + version.ToString() + f.Extension)); } else { File.Move(f.FullName, Path.Combine(Path.GetDirectoryName(f.FullName), _SubdirectoryForProblemEmails, f.Name)); } moved = true; } catch (TimeoutException) { Logging.Fatal("Tried to move file " + f.Name + " for 10 seconds but failed."); Logging.Fatal("This is a FATAL failure."); Environment.Exit(-1); } catch (Exception) { Logging.Debug("Attempt " + version.ToString() + " Failed."); version++; } } break; } }); if (!_IsStopping) { // Get any new files that have turned up. files = new DirectoryInfo(path).GetFiles(fileSearchPattern); } }while (files.Count() > 0); }