/// <summary> /// Processes input stream as archive and scans archive file entries (checks the archive contents). /// </summary> /// <remarks>Calls itself recursively when another archive is found in current archive.</remarks> /// <param name="archiveStream">Stream containing archive data.</param> /// <returns></returns> public AttachmentFilterStatus ProcessArchiveStream(Stream archiveStream) { var result = new AttachmentFilterStatus(AttachmentFilterStatusEnum.Accept, "Archive OK"); try { using (var zipFile = new ZipFile(archiveStream)) { foreach (ZipEntry entry in zipFile) { var fileResult = FilenameFilterStatus(entry.Name); if (fileResult.Status > result.Status) { result = fileResult; } if (IsArchive(entry.Name)) { var archiveResult = ProcessArchiveStream(zipFile.GetInputStream(entry)); if (archiveResult.Status > result.Status) { result = archiveResult; } } if (IsOpenXmlDocument(entry.Name)) { var openXmlDocumentStatus = ProcessOpenXmlDocumentStream(zipFile.GetInputStream(entry)); if (openXmlDocumentStatus.Status > result.Status) { result = openXmlDocumentStatus; } } } } } catch (Exception ex) { // Processing usually fails because of: // - password protected archive (when we need to access it's contents) // - unsupported archive (version, format, ...) // - corrupted archive // - spoofed file extension (looks like some kind of archive but it is of different type) // We remove such attchments (recommended, might be dangerous, maybe not... just let the recipient know) return(new AttachmentFilterStatus(AttachmentFilterStatusEnum.RemoveAttachment, $"Archive ERROR [{ex.GetType()}]: {ex.Message}")); } return(result); }
private void OnOnSubmittedMessage(SubmittedMessageEventSource source, QueuedMessageEventArgs queuedMessageEventArgs) { lock (_fileLock) { AgentAsyncContext agentAsyncContext = null; try { var mailItem = queuedMessageEventArgs.MailItem; agentAsyncContext = GetAgentAsyncContext(); // check the sender whitelist if (_exchangeAttachmentFilterConfig.SendersWhitelist.Any( f => Regex.IsMatch(mailItem.FromAddress.ToString(), WildcardToRegex(f)))) { return; } // maybe we will need list of recipients on single line... var recipientList = new StringBuilder(); for (var i = 0; i < mailItem.Recipients.Count; i++) { recipientList.Append(i == 0 ? mailItem.Recipients[i].Address.ToString() : "; " + mailItem.Recipients[i].Address); } var removedAttachments = new List <Attachment>(); var strippedAttachments = new List <Attachment>(); var messageRejected = false; var messageLogStringBuilder = new SysLogBuilder(); var mailItemStatusText = $"[from: {mailItem.FromAddress}] [to: {recipientList}] [method: {mailItem.InboundDeliveryMethod}] [subject: {mailItem.Message.Subject}] [size: {mailItem.MimeStreamLength.ToString("N0")}]"; messageLogStringBuilder.Log(mailItemStatusText); if (mailItem.Message.Attachments.Count == 0 && _exchangeAttachmentFilterConfig.LogAccepted) { messageLogStringBuilder.LogPadded( "ACCEPTED: [reason: no attachments]"); } else { foreach (var attachment in mailItem.Message.Attachments) { // It would be great idea to process only attachments with size greater // than some threshold, 'cos infected files are always quite small (only few kB) // But I am not sure how to get the attachment size here, ... // if (any previous) attachment rejected the message then break the processing now if (messageRejected) { break; } AttachmentFilterStatus attachmentStatus = null; if (_exchangeAttachmentFilterConfig.DsnStripOriginalMessage) { // DSN has InboundDeliveryMethod equal to DeliveryMethod.File and FromAddress is equal to <> // and DSN's original message is included as message/rfc822 attachment if (mailItem.InboundDeliveryMethod == DeliveryMethod.File && mailItem.FromAddress.ToString() == "<>" && attachment.ContentType.ToLower().Equals("message/rfc822")) { attachmentStatus = new AttachmentFilterStatus(AttachmentFilterStatusEnum.StripAttachment, "DSN original message"); } } if (attachmentStatus == null) { // default file status (by extension) attachmentStatus = FilenameFilterStatus(attachment.FileName); // is it archive? if (_exchangeAttachmentFilterConfig.ScanArchives && IsArchive(attachment.FileName)) { var archiveStatus = ProcessArchiveStream(attachment.GetContentReadStream()); if (archiveStatus.Status > attachmentStatus.Status) { attachmentStatus = archiveStatus; } } // is it OpenXml document? if (_exchangeAttachmentFilterConfig.ScanOpenXmlDocuments && IsOpenXmlDocument(attachment.FileName)) { var openXmlDocumentStatus = ProcessOpenXmlDocumentStream(attachment.GetContentReadStream()); if (openXmlDocumentStatus.Status > attachmentStatus.Status) { attachmentStatus = openXmlDocumentStatus; } } } var attachmentStatusText = $"[file: {attachment.FileName}] [type: {attachment.AttachmentType}] [content type:{attachment.ContentType}] [reason: {attachmentStatus.Reason}]"; switch (attachmentStatus.Status) { case AttachmentFilterStatusEnum.Accept: if (_exchangeAttachmentFilterConfig.LogAccepted) { messageLogStringBuilder.LogPadded($"ACCEPTED: {attachmentStatusText}"); } break; case AttachmentFilterStatusEnum.RemoveAttachment: // just mark this attachment for removement, but do not touch attachments collection now // (we are in foreach loop and need to process them all) removedAttachments.Add(attachment); if (_exchangeAttachmentFilterConfig.LogRejectedOrRemoved) { messageLogStringBuilder.LogPadded($"REMOVED: {attachmentStatusText}"); } break; case AttachmentFilterStatusEnum.StripAttachment: // just mark this attachment for removement, but do not touch attachments collection now // (we are in foreach loop and need to process them all) strippedAttachments.Add(attachment); if (_exchangeAttachmentFilterConfig.LogRejectedOrRemoved) { messageLogStringBuilder.LogPadded($"STRIPPED: {attachmentStatusText}"); } break; case AttachmentFilterStatusEnum.RejectMessage: // reject whole message if (_exchangeAttachmentFilterConfig.LogRejectedOrRemoved) { messageLogStringBuilder.LogPadded($"REJECTED: {attachmentStatusText}"); } messageRejected = true; break; } } } if (messageLogStringBuilder.MessageCount > 1) { SysLog.Log(messageLogStringBuilder); } // reject the message? if (messageRejected) { // delete the source message and do nothing more (we do not send DSN)... source.Delete(); return; } // for every attachment we marked as being removed create new txt attachment with some info why it happened... foreach (var removedAttachment in removedAttachments) { // new attachment filename var newFileName = $"{_exchangeAttachmentFilterConfig.RemovedAttachmentPrefix}{removedAttachment.FileName}.txt"; // add new attachment to the message... var newAttachment = mailItem.Message.Attachments.Add(newFileName); // ...and write content into it (info message) var newAttachmentWriter = new StreamWriter(newAttachment.GetContentWriteStream()); newAttachmentWriter.WriteLine(removedAttachment.FileName); newAttachmentWriter.WriteLine(); newAttachmentWriter.WriteLine(_exchangeAttachmentFilterConfig.RemovedAttachmentNewContent); newAttachmentWriter.Flush(); newAttachmentWriter.Close(); } // finally remove all attachments marked for removal foreach (var removedAttachment in removedAttachments) { mailItem.Message.Attachments.Remove(removedAttachment); } // ...and stripped attachments, too foreach (var strippedAttachment in strippedAttachments) { mailItem.Message.Attachments.Remove(strippedAttachment); } } catch (IOException ex) { SysLog.Log("IOException: " + ex.Message); } catch (Exception ex) { SysLog.Log("Exception: " + ex.Message); } finally { agentAsyncContext?.Complete(); } } }