/// <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();
                }
            }
        }