From e731c87789e5532c8d1ce0774f19a251a67cccba Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Tue, 17 Jan 2023 19:14:58 -0800 Subject: [PATCH 01/18] Trying to fix this branch (had to recreate and lost all commits) --- README.rst | 3 +- changelog_temp.md | 7 + extract_msg/attachment.py | 28 ++- extract_msg/attachment_base.py | 35 ++- extract_msg/custom_attachments/__init__.py | 51 +++++ .../custom_attachments/custom_handler.py | 54 +++++ .../custom_attachments/outlook_image.py | 98 +++++++++ extract_msg/custom_attachments/utils.py | 201 ++++++++++++++++++ extract_msg/enums.py | 12 ++ extract_msg/exceptions.py | 17 +- extract_msg/message_base.py | 14 +- extract_msg/msg.py | 8 +- extract_msg/ole_writer.py | 1 + 13 files changed, 499 insertions(+), 30 deletions(-) create mode 100644 changelog_temp.md create mode 100644 extract_msg/custom_attachments/__init__.py create mode 100644 extract_msg/custom_attachments/custom_handler.py create mode 100644 extract_msg/custom_attachments/outlook_image.py create mode 100644 extract_msg/custom_attachments/utils.py diff --git a/README.rst b/README.rst index 33efaec7..76e73266 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,7 @@ This module has a Discord server for general discussion. You can find it here: Changelog --------- -- `Changelog `__ +- `Changelog`_ Usage ----- @@ -252,3 +252,4 @@ your access to the newest major version of extract-msg. .. _Ko-fi: https://ko-fi.com/destructione .. _Patreon: https://www.patreon.com/DestructionE .. _msg-explorer: https://pypi.org/project/msg-explorer/ +.. _Changelog: https://github.com/TeamMsgExtractor/msg-extractor/blob/master/CHANGELOG.md diff --git a/changelog_temp.md b/changelog_temp.md new file mode 100644 index 00000000..bb6f63f4 --- /dev/null +++ b/changelog_temp.md @@ -0,0 +1,7 @@ +Temporary location for the changelog entry to ensure it doesn't conflict. + +**v0.??.??** +* Added new submodule `custom_attachments`. This submodule provides an extendable way to handle custom attachment types, attachment types whose structure and formatting are not defined in the Microsoft documentation for MSG files. +* Added new property `AttachmentBase.clsid` which returns the listed CLSID value of the data stream/storage of the attachment. +* Changed internal behavior of `MSGFile.attachments`. This should not cause any noticeable changes to the output. +* Removed some debug code that was left behind. diff --git a/extract_msg/attachment.py b/extract_msg/attachment.py index cf02bc82..0655f8e9 100644 --- a/extract_msg/attachment.py +++ b/extract_msg/attachment.py @@ -5,10 +5,11 @@ import string import zipfile -from typing import Optional, Union +from typing import Any, Dict, Optional, Union from . import constants from .attachment_base import AttachmentBase +from .custom_attachments import CustomAttachmentHandler, getHandler from .enums import AttachmentType from .exceptions import StandardViolationError from .utils import createZipOpen, inputToString, openMsg, prepareFilename @@ -33,6 +34,7 @@ def __init__(self, msg, dir_): located. """ super().__init__(msg, dir_) + self.__customHandler = None if '37050003' not in self.props: from .prop import createProp @@ -65,9 +67,11 @@ def __init__(self, msg, dir_): self.__data = self._getStream('__substg1.0_37010102') elif self.exists('__substg1.0_3701000D'): if (self.props['37050003'].value & 0x7) != 0x5: - raise NotImplementedError( - 'Current version of extract_msg does not support extraction of containers that are not embedded msg files.') - # TODO add implementation. + self.__type = AttachmentType.CUSTOM + # Check if we have any custom handlers. If not, it will raise + # an error automatically. + self.__customHandler = getHandler(self) + self.__data = self.__customHandler.data else: self.__prefix = msg.prefixList + [dir_, '__substg1.0_3701000D'] self.__type = AttachmentType.MSG @@ -103,8 +107,10 @@ def getFilename(self, **kwargs) -> str: # Check if user wants to save the file under the Content-ID. if kwargs.get('contentId', False): filename = self.cid - # If filename is None at this point, use long filename as first - # preference. + # If we are using a custom handler, prefer it's name. + if self.type is AttachmentType.CUSTOM: + filename = self.__customHandler.name + # If we are here, try to get the filename however else we can. if not filename: filename = self.name # Otherwise just make something up! @@ -198,7 +204,7 @@ def save(self, **kwargs) -> Optional[Union[str, 'MSGFile']]: fullFilename = customPath / filename - if self.type is AttachmentType.DATA: + if self.type is AttachmentType.DATA or (self.type is AttachmentType.CUSTOM and isinstance(self.__data, bytes)): if _zip: name, ext = os.path.splitext(filename) nameList = _zip.namelist() @@ -254,6 +260,14 @@ def saveEmbededMessage(self, **kwargs) -> None: """ self.data.save(**kwargs) + @property + def customHandler(self) -> Optional[CustomAttachmentHandler]: + """ + The instance of the custom handler associated with this attachment, if + it has one. + """ + return self.__customHandler + @property def data(self) -> Optional[Union[bytes, 'MSGFile']]: """ diff --git a/extract_msg/attachment_base.py b/extract_msg/attachment_base.py index 2b5a10ee..ef7c3bef 100644 --- a/extract_msg/attachment_base.py +++ b/extract_msg/attachment_base.py @@ -34,7 +34,6 @@ def __init__(self, msg, dir_): self.__props = Properties(self._getStream('__properties_version1.0'), PropertiesType.ATTACHMENT) self.__namedProperties = NamedProperties(msg.named, self) - def _ensureSet(self, variable, streamID, stringStream = True, **kwargs): """ Ensures that the variable exists, otherwise will set it using the @@ -271,9 +270,39 @@ def cid(self) -> Optional[str]: contendId = cid @property - def dir(self): + def clsid(self) -> str: + """ + Returns the CLSID for the data stream/storage of the attachment. + """ + try: + return self.__clsid + except AttributeError: + # Set some default values. + self.__clsid = '00000000-0000-0000-0000-000000000000' + dataStream = None + + # See if we can find the data stream/storage. + if self.type in (AttachmentType.CUSTOM, AttachmentType.MSG): + dataStream = [self.__dir, '__substg1.0_3701000D'] + elif self.type is AttachmentType.DATA: + dataStream = [self.__dir, '__substg1.0_37010102'] + elif self.type is AttachmentType.UNSUPPORTED: + # Special check for custom attachments. + if self.exists('__substg1.0_3701000D'): + dataStream = [self.__dir, '__substg1.0_3701000D'] + elif self.exists('__substg1.0_37010102'): + dataStream = [self.__dir, '__substg1.0_37010102'] + + # If we found the right item, get the CLSID. + if dataStream: + self.__clsid = self.__msg._getOleEntry(dataStream).clsid or '00000000-0000-0000-0000-000000000000' + + return self.__clsid + + @property + def dir(self) -> str: """ - Returns the directory inside the msg file where the attachment is + Returns the directory inside the MSG file where the attachment is located. """ return self.__dir diff --git a/extract_msg/custom_attachments/__init__.py b/extract_msg/custom_attachments/__init__.py new file mode 100644 index 00000000..1b153c6f --- /dev/null +++ b/extract_msg/custom_attachments/__init__.py @@ -0,0 +1,51 @@ +""" +Submodule designed to help with saving and using custom attachments. Custom +attachments are those follow standards not defined in the MSG documentation. Use +the function `getHandler` to get an instance of a subclass of +CustomAttachmentHandler. + +CustomAttachmentHandler subclasses will all define the following methods: + injectHtml: A method which takes HTML and inserts the + +It should hopefully be completely unnecessary for your code to know what type of +handler it is using, as the abstract base class should give all of the functions +you would typically want. + +If you would like to add your own handler, simply subclass +CustomAttachmentHandler and add it using the `registerHandler` function. +""" + +from typing import List + +from .custom_handler import CustomAttachmentHandler + + +# Create a way to register handlers. +_knownHandlers : List[CustomAttachmentHandler] = [] +# This line is cheating a little bit, but is more efficient than wrapping it in +# a function. +registerHandler = _knownHandlers.append + + +# Import built-in handler modules. THey will all automatically register their +# respecive handler(s). +from .outlook_image import OutlookImage + + + + +# Function designed to route to the correct handler. +def getHandler(attachment : 'Attachment') -> CustomAttachmentHandler: + """ + Takes an attachment and uses it to find the correct handler. Returns an + instance created using the specified attachment. + + :raises NotImplementedError: No handler could be found. + :raises ValueError: A handler was found, but something was wrong with the + attachment data. + """ + for handler in _knownHandlers: + if handler.isCorrectHandler(attachment): + return handler(attachment) + + raise NotImplementedError('No valid handler could be found for the attachment. Contact the developers for help.') diff --git a/extract_msg/custom_attachments/custom_handler.py b/extract_msg/custom_attachments/custom_handler.py new file mode 100644 index 00000000..c844151a --- /dev/null +++ b/extract_msg/custom_attachments/custom_handler.py @@ -0,0 +1,54 @@ +import abc + +from typing import List, Optional, Tuple + + +class CustomAttachmentHandler(abc.ABC): + """ + A class designed to help with custom attachments that may require parsing in + special ways that are completely different from one another. + """ + def __init__(self, attachment : 'Attachment'): + super().__init__() + self.__att = attachment + + @classmethod + @abc.abstractmethod + def isCorrectHandler(cls, attachment : 'Attachment') -> bool: + """ + Checks if this is the correct handler for the attachment. + """ + + @abc.abstractmethod + def injectHTML(self, html : bytes, renderedList : Optional[List[str]] = None) -> Tuple[bytes, Optional[List[str]]]: + """ + Adds the relevent tag, if any, to the HTML for making prepared HTML. + + If this function should do nothing, returns the two arguments without + modification. + + :param html: The HTML body to inject into (if at all). + :param renderedList: The list to use (if needed) of "rendered + characters" which will be returned and matches the HTML returned. + """ + + @property + def attachment(self): + """ + The attachment this handler is associated with. + """ + return self.__att + + @property + @abc.abstractmethod + def data(self) -> bytes: + """ + Gets the data for the attachment. + """ + + @property + @abc.abstractmethod + def name(self) -> str: + """ + Returns the name to be used when saving the attachment. + """ diff --git a/extract_msg/custom_attachments/outlook_image.py b/extract_msg/custom_attachments/outlook_image.py new file mode 100644 index 00000000..54428599 --- /dev/null +++ b/extract_msg/custom_attachments/outlook_image.py @@ -0,0 +1,98 @@ +import base64 +import struct + +from typing import List, Optional, Tuple + +from . import registerHandler +from .custom_handler import CustomAttachmentHandler +from .utils import htmlSplitRendered +from ..enums import DVAspect +from ..exceptions import CustomAttachmentError + + +_ST_OLE = struct.Struct('' + + @classmethod + def isCorrectHandler(cls, attachment : 'Attachment') -> bool: + if attachment.clsid != '00000316-0000-0000-C000-000000000046': + return False + + # Check for the required streams. + if not attachment.exists('__substg1.0_3701000D/CONTENTS'): + return False + if not attachment.exists('__substg1.0_3701000D/\x01Ole'): + return False + if not attachment.exists('__substg1.0_3701000D/\x03MailStream'): + return False + + return True + + def injectHTML(self, html : bytes, renderedList : Optional[List[str]] = None) -> Tuple[bytes, Optional[List[str]]]: + if not renderedList: + renderedList = htmlSplitRendered(html) + + rp = self.attachment.renderingPosition + + if rp >= len(renderedList): + raise CustomAttachmentError(f'Rendering position beyond calculated number of rendered characters (expected less than {len(renderedList)}, got {rp}).') + + renderedList[rp] = self.__htmlTag + renderedList[rp] + + return (''.join(renderedList).encode('utf-8'), renderedList) + + @property + def data(self) -> bytes: + return self.__data + + @property + def name(self) -> str: + return self.attachment.shortFilename + '.bmp' + + + + +registerHandler(OutlookImage) diff --git a/extract_msg/custom_attachments/utils.py b/extract_msg/custom_attachments/utils.py new file mode 100644 index 00000000..8a61e3ec --- /dev/null +++ b/extract_msg/custom_attachments/utils.py @@ -0,0 +1,201 @@ +""" +Utilities for extract-msg that are more specialized for the custom_attachments +submodule than for the main module. +""" + +import bs4 + +from typing import List + + +_WHITESPACE_BREAKERS = ( + ' bool: + """ + Helper function to indicate that a tag breaks a chain of whitespace. + """ + for x in _WHITESPACE_BREAKERS: + if token.startswith(x) and len(token) > len(x) and token[len(x)] in ('>', ' ', '/'): + return True + + return False + + +def _isWhitespaceToken(token : str) -> bool: + if token[0] == '<': + for x in _WHITESPACE_TAGS: + if token.startswith(x) and len(token) > len(x) and token[len(x)] in ('>', ' ', '/'): + return True + elif token in (' ', ' ', ' ', ' ', ' '): + return True + else: + return token.isspace() + + return False + + +def htmlSplitRendered(html : bytes) -> List[str]: + """ + Takes html bytes and returns a list of the rendered characters, with data + that is not being rendered being attached to the next rendered character. + """ + # Unfortunately bs4 didn't seem particularly great for tokenizing, so I did + # my own function that works well enough. First, let's tokenize the html. + tokens = tokenizeHtml(bs4.BeautifulSoup(html, features = 'html.parser').decode()) + + # Next, let's break things down further. + breakDown = [] + for token in tokens: + # We tell what we are looking at by checking the first character of the + # token. If it's a <, then it is an HTML tag. If it is a & then it is an + # escape. Otherwise, it is plain text. For both tags and escapes, just + # dump them into the list. + if token[0] in ('<', '&'): + breakDown.append(token) + else: + # If we are looking at plain text, add it by extending the list. + breakDown.extend(token) + + # Now that we have broken things down further, let's go through and join our + # pieces togethered into rendered tokens. Here is were we actually need to + # know what an html tag is. If it's an escape or just a non-whitespace + # character, we can just shove it onto what we currently have. + current = '' + renderedCharacters = [] + lastWhitespace = None + for item in breakDown: + if item[0] == '&': + if _isWhitespaceToken(item): + if lastWhitespace is None: + lastWhitespace == item + else: + if lastWhitespace is not None and lastWhitespace[0] != '<': + current += lastWhitespace + renderedCharacters.append(current) + current = '' + lastWhitespace = None + current += item + renderedCharacters.append(current) + current = '' + elif item[0] == '<': + if _isWhitespaceToken(item): + # If we are here, add it to current, push current, and set this + # tag as the last whitespace. + current += item + renderedCharacters.append(current) + current = '' + lastWhitespace = item + else: + + # Some tags will break whitespace chains. + if _isWhitespaceBreaker(item): + if lastWhitespace is not None and lastWhitespace[0] != '<': + current += lastWhitespace + renderedCharacters.append(current) + current = '' + lastWhitespace = None + + current += item + else: + # Here is where we handle text, which is not particularly fun. + # Basically if it is whitespace and lastWhitespace is not none, we + # set the whitespace. + if _isWhitespaceToken(item): + if lastWhitespace is None: + lastWhitespace = item + else: + if lastWhitespace is not None and lastWhitespace[0] != '<': + current += lastWhitespace + renderedCharacters.append(current) + current = '' + lastWhitespace = None + current += item + renderedCharacters.append(current) + current = '' + + if current: + renderedCharacters.append(current) + + return renderedCharacters + + +def tokenizeHtml(html : str) -> List[str]: + # Setup a few variables for state tracking. + inTag = False + # Used for tracking escapes starting with &. If your escape ends up at 100 + # characters because it is missing the semicolon, we are going to throw an + # error. + inEscape = False + inString = False + # Only used when in string. Last character was a backslash. + isBackslash = False + # Tells which quote type we are in. + isDoubleQuote = False + + tokens = [] + currentToken = '' + + # Finally, let's start breaking things up. Our rules are that if we start a + # quote while in a tag, then we acknowledge it, otherwise it is treated as + # plain text. + for character in html: + # First we need to know our state, as our state determines what how we + # process a character. + if inTag: + currentToken += character + if inString: + if character == '"' and isDoubleQuote: + # If isBackslash then we stay in the quote, otherwise... + isQuote = isBackslash + elif character == "'" and not isDoubleQuote: + # If isBackslash then we stay in the quote, otherwise... + isQuote = isBackslash + elif character == '\\': + isBackslash = not isBackslash + if character != '\\': + isBackslash = False + else: + if character == '>': + inTag = False + tokens.append(currentToken) + currentToken = '' + elif inEscape: + currentToken += character + if len(currentToken) > 99: + raise ValueError('Found escape that was too long (is a ; missing?)') + if character == ';': + tokens.append(currentToken) + currentToken = '' + inEscape = False + elif inString: + # This is an error. We should *never* be in a quote if we are not in + # a tag. + raise ValueError('Found to be inQuote when not in tag.') + else: + # We are currently processing plain text, so let's just handle. + if character == '&': + if currentToken: + tokens.append(currentToken) + currentToken = character + inEscape = True + elif character == '<': + if currentToken: + tokens.append(currentToken) + currentToken = character + inTag = True + inString = False + else: + currentToken += character + + if currentToken: + tokens.append(currentToken) + + return tokens diff --git a/extract_msg/enums.py b/extract_msg/enums.py index 3f746da4..93ea5bb1 100644 --- a/extract_msg/enums.py +++ b/extract_msg/enums.py @@ -113,6 +113,7 @@ class AttachmentType(enum.Enum): SIGNED = 3 BROKEN = 4 UNSUPPORTED = 5 + CUSTOM = 6 UNKNOWN = 0xFFFFFFFF @@ -316,6 +317,7 @@ class Color(enum.IntEnum): BLACK = 1 + class ContactAddressIndex(enum.Enum): EMAIL_1 = 0 EMAIL_2 = 1 @@ -376,6 +378,16 @@ class DisplayType(enum.Enum): +class DVAspect(enum.IntEnum): + """ + Part of the extra data for Outlook signatures. Different sources seem to + disagree on the meanings, so I'm sticking to the meanings in the official + Microsoft documentation of the DVASPECT enumeration. + """ + CONTENT = 1 + ICON = 4 + + class ElectronicAddressProperties(enum.Enum): @classmethod def fromBits(cls, value : int) -> Set['ElectronicAddressProperties']: diff --git a/extract_msg/exceptions.py b/extract_msg/exceptions.py index e835df64..1ce1fc88 100644 --- a/extract_msg/exceptions.py +++ b/extract_msg/exceptions.py @@ -17,19 +17,21 @@ class BadHtmlError(ValueError): """ HTML failed to pass validation. """ - pass class ConversionError(Exception): """ An error occured during type conversion. """ - pass + +class CustomAttachmentError(Exception): + """ + A generic error used for issues handling custom attachments. + """ class DataNotFoundError(Exception): """ Requested stream type was unavailable. """ - pass class DeencapMalformedData(Exception): """ @@ -45,7 +47,6 @@ class ExecutableNotFound(Exception): """ Could not find the specified executable. """ - pass class IncompatibleOptionsError(Exception): """ @@ -56,26 +57,22 @@ class InvalidFileFormatError(OSError): """ An Invalid File Format Error occurred. """ - pass class InvaildPropertyIdError(Exception): """ The provided property ID was invalid. """ - pass class InvalidVersionError(Exception): """ The version specified is invalid. """ - pass class StandardViolationError(Exception): """ A critical violation of the MSG standards was detected and could not be recovered from. Recoverable violations will result in log messages instead. """ - pass class TZError(Exception): """ @@ -91,13 +88,11 @@ class UnknownCodepageError(Exception): """ The codepage provided was not one we know of. """ - pass class UnknownTypeError(Exception): """ The type specified is not one that is recognized. """ - pass class UnsupportedMSGTypeError(NotImplementedError): """ @@ -110,10 +105,8 @@ class UnrecognizedMSGTypeError(TypeError): An exception that is raised when the module cannot determine how to properly open a specific class of msg file. """ - pass class WKError(RuntimeError): """ An error occured while running wkhtmltopdf. """ - pass diff --git a/extract_msg/message_base.py b/extract_msg/message_base.py index 841654d4..334e322d 100644 --- a/extract_msg/message_base.py +++ b/extract_msg/message_base.py @@ -19,7 +19,7 @@ from typing import Callable, Dict, Optional, Tuple, Union from . import constants -from .enums import DeencapType, RecipientType +from .enums import AttachmentType, DeencapType, RecipientType from .exceptions import ( DataNotFoundError, DeencapMalformedData, DeencapNotEncapsulated, IncompatibleOptionsError, WKError @@ -1178,8 +1178,18 @@ def htmlBodyPrepared(self) -> Optional[bytes]: if not self.htmlBody: return self.htmlBody + html = self.htmlBody + + renderedCharacters = None + + # Iterate through all attachments, and inject all of the custom data + # from them. + for x in self.attachments: + if x.type is AttachmentType.CUSTOM: + html, renderedCharacters = x.customHandler.injectHTML(html, renderedCharacters) + # Create the BeautifulSoup instance to use. - soup = bs4.BeautifulSoup(self.htmlBody, 'html.parser') + soup = bs4.BeautifulSoup(html, 'html.parser') # Get a list of image tags to see if we can inject into. If the source # of an image starts with "cid:" that means it is one of the attachments diff --git a/extract_msg/msg.py b/extract_msg/msg.py index 6fecb2b0..9db13122 100644 --- a/extract_msg/msg.py +++ b/extract_msg/msg.py @@ -631,10 +631,9 @@ def attachments(self) -> List: # Get the attachments. attachmentDirs = [] prefixLen = self.prefixLen - for dir_ in self.listDir(False, True): - if dir_[prefixLen].startswith('__attach') and \ - dir_[prefixLen] not in attachmentDirs: - attachmentDirs.append(dir_[prefixLen]) + for dir_ in self.listDir(False, True, False): + if dir_[0].startswith('__attach') and dir_[0] not in attachmentDirs: + attachmentDirs.append(dir_[0]) self._attachments = [] @@ -642,7 +641,6 @@ def attachments(self) -> List: try: self._attachments.append(self.attachmentClass(self, attachmentDir)) except (NotImplementedError, UnrecognizedMSGTypeError) as e: - print("Hello") if self.attachmentErrorBehavior != AttachErrorBehavior.THROW: logger.error(f'Error processing attachment at {attachmentDir}') logger.exception(e) diff --git a/extract_msg/ole_writer.py b/extract_msg/ole_writer.py index 69a9cf44..53ce1593 100644 --- a/extract_msg/ole_writer.py +++ b/extract_msg/ole_writer.py @@ -631,6 +631,7 @@ def addOleEntry(self, path, entry : OleDirectoryEntry, data : Optional[bytes] = """ path = inputToMsgPath(path) # First, find the current place in our dict to add the item. + _dir = self.__getContainingStorage(path, False) # Now, check that the item *is not* already in our dict, as that would # cause problems. From 558c61f10ca1566ec86e519348f06e12cf34bdac Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Tue, 17 Jan 2023 19:17:02 -0800 Subject: [PATCH 02/18] Fix typo --- extract_msg/custom_attachments/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extract_msg/custom_attachments/__init__.py b/extract_msg/custom_attachments/__init__.py index 1b153c6f..a8f84904 100644 --- a/extract_msg/custom_attachments/__init__.py +++ b/extract_msg/custom_attachments/__init__.py @@ -27,7 +27,7 @@ registerHandler = _knownHandlers.append -# Import built-in handler modules. THey will all automatically register their +# Import built-in handler modules. They will all automatically register their # respecive handler(s). from .outlook_image import OutlookImage From 4f01d61d7781e79b16343a07c638d61bd50ae8e4 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sun, 12 Feb 2023 23:53:24 -0800 Subject: [PATCH 03/18] Attempting to fix merge --- extract_msg/attachment_base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/extract_msg/attachment_base.py b/extract_msg/attachment_base.py index ef7c3bef..aa6066a6 100644 --- a/extract_msg/attachment_base.py +++ b/extract_msg/attachment_base.py @@ -34,6 +34,7 @@ def __init__(self, msg, dir_): self.__props = Properties(self._getStream('__properties_version1.0'), PropertiesType.ATTACHMENT) self.__namedProperties = NamedProperties(msg.named, self) + def _ensureSet(self, variable, streamID, stringStream = True, **kwargs): """ Ensures that the variable exists, otherwise will set it using the From 8384e1662adadcd3c72c980b8bc511f8e4b8b085 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sun, 12 Feb 2023 23:55:31 -0800 Subject: [PATCH 04/18] Final fix for merge --- extract_msg/enums.py | 1 - 1 file changed, 1 deletion(-) diff --git a/extract_msg/enums.py b/extract_msg/enums.py index 93ea5bb1..aeb6ca1b 100644 --- a/extract_msg/enums.py +++ b/extract_msg/enums.py @@ -113,7 +113,6 @@ class AttachmentType(enum.Enum): SIGNED = 3 BROKEN = 4 UNSUPPORTED = 5 - CUSTOM = 6 UNKNOWN = 0xFFFFFFFF From 5ce8cb2dd3d59c96b4ea7179aeea1bb03020720f Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sun, 12 Feb 2023 23:56:43 -0800 Subject: [PATCH 05/18] Add back CUSTOM to AttachmentType enum --- extract_msg/enums.py | 1 + 1 file changed, 1 insertion(+) diff --git a/extract_msg/enums.py b/extract_msg/enums.py index a2c7b420..c59a488b 100644 --- a/extract_msg/enums.py +++ b/extract_msg/enums.py @@ -124,6 +124,7 @@ class AttachmentType(enum.Enum): BROKEN = 4 UNSUPPORTED = 5 SIGNED_EMBEDDED = 6 + CUSTOM = 7 UNKNOWN = 0xFFFFFFFF From 340168b4d88d1335fb255f5db76fcc6a3cd29d94 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sat, 18 Mar 2023 16:00:35 -0700 Subject: [PATCH 06/18] Revert and adjust some changes to sync with next-release --- README.rst | 3 +-- extract_msg/message_base.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 9788fe6d..5eec4e65 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,7 @@ This module has a Discord server for general discussion. You can find it here: Changelog --------- -- `Changelog`_ +- `Changelog `__ Usage ----- @@ -252,4 +252,3 @@ your access to the newest major version of extract-msg. .. _Ko-fi: https://ko-fi.com/destructione .. _Patreon: https://www.patreon.com/DestructionE .. _msg-explorer: https://pypi.org/project/msg-explorer/ -.. _Changelog: https://github.com/TeamMsgExtractor/msg-extractor/blob/master/CHANGELOG.md diff --git a/extract_msg/message_base.py b/extract_msg/message_base.py index 334e322d..f1505a82 100644 --- a/extract_msg/message_base.py +++ b/extract_msg/message_base.py @@ -19,7 +19,7 @@ from typing import Callable, Dict, Optional, Tuple, Union from . import constants -from .enums import AttachmentType, DeencapType, RecipientType +from .enums import DeencapType, RecipientType from .exceptions import ( DataNotFoundError, DeencapMalformedData, DeencapNotEncapsulated, IncompatibleOptionsError, WKError From 1f30ab8dd90b120ce6a812f169fa2b882d79da36 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sat, 18 Mar 2023 16:06:31 -0700 Subject: [PATCH 07/18] Add back change that was blocking merge --- extract_msg/message_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extract_msg/message_base.py b/extract_msg/message_base.py index 5bc481af..a56ea225 100644 --- a/extract_msg/message_base.py +++ b/extract_msg/message_base.py @@ -21,7 +21,7 @@ from . import constants from ._rtf.create_doc import createDocument from ._rtf.inject_rtf import injectStartRTF -from .enums import DeencapType, RecipientType +from .enums import AttachmentType, DeencapType, RecipientType from .exceptions import ( DataNotFoundError, DeencapMalformedData, DeencapNotEncapsulated, IncompatibleOptionsError, WKError From d2fdda734b508935c82c2f7e1730ddaad1479851 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Tue, 9 May 2023 14:46:24 -0700 Subject: [PATCH 08/18] Remove unneded imports (also resolve merge) --- extract_msg/attachment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extract_msg/attachment.py b/extract_msg/attachment.py index 8ff0f3fb..b23cb8bf 100644 --- a/extract_msg/attachment.py +++ b/extract_msg/attachment.py @@ -5,7 +5,7 @@ import string import zipfile -from typing import Any, Dict, Optional, Union +from typing import Optional, Union from . import constants from .attachment_base import AttachmentBase From 88fd211c643fb8567770f50dced621de2bc31eba Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Tue, 9 May 2023 14:47:51 -0700 Subject: [PATCH 09/18] Attempting to resolve merge --- extract_msg/attachment.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/extract_msg/attachment.py b/extract_msg/attachment.py index b23cb8bf..6b87b1d1 100644 --- a/extract_msg/attachment.py +++ b/extract_msg/attachment.py @@ -260,14 +260,6 @@ def saveEmbededMessage(self, **kwargs) -> None: """ self.data.save(**kwargs) - @property - def customHandler(self) -> Optional[CustomAttachmentHandler]: - """ - The instance of the custom handler associated with this attachment, if - it has one. - """ - return self.__customHandler - @property def data(self) -> Optional[Union[bytes, 'MSGFile']]: """ From 3a20c74902c768fb02d4f8879b014cf034d3a74a Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Tue, 9 May 2023 14:53:09 -0700 Subject: [PATCH 10/18] More resolving merge --- extract_msg/exceptions.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/extract_msg/exceptions.py b/extract_msg/exceptions.py index 1ce1fc88..acd3567f 100644 --- a/extract_msg/exceptions.py +++ b/extract_msg/exceptions.py @@ -23,11 +23,6 @@ class ConversionError(Exception): An error occured during type conversion. """ -class CustomAttachmentError(Exception): - """ - A generic error used for issues handling custom attachments. - """ - class DataNotFoundError(Exception): """ Requested stream type was unavailable. From 2a1b0f41584ec6344dde2c6d10a8d57397d751a0 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Tue, 9 May 2023 14:57:12 -0700 Subject: [PATCH 11/18] Checking if that was one of the merge issues --- extract_msg/msg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/extract_msg/msg.py b/extract_msg/msg.py index 8dc73585..f88b5638 100644 --- a/extract_msg/msg.py +++ b/extract_msg/msg.py @@ -653,6 +653,7 @@ def attachments(self) -> List: try: self._attachments.append(self.attachmentClass(self, attachmentDir)) except (NotImplementedError, UnrecognizedMSGTypeError) as e: + print("Hello") if self.attachmentErrorBehavior != AttachErrorBehavior.THROW: logger.error(f'Error processing attachment at {attachmentDir}') logger.exception(e) From 7c1a48080051f02a399d014c881a10d26e9d93d3 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Tue, 9 May 2023 14:57:59 -0700 Subject: [PATCH 12/18] Is this the issue? --- extract_msg/exceptions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/extract_msg/exceptions.py b/extract_msg/exceptions.py index acd3567f..762a1d65 100644 --- a/extract_msg/exceptions.py +++ b/extract_msg/exceptions.py @@ -83,6 +83,7 @@ class UnknownCodepageError(Exception): """ The codepage provided was not one we know of. """ + pass class UnknownTypeError(Exception): """ From f0cfe3ecd78c53972097ca0b91c70544698d67a4 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Tue, 9 May 2023 14:59:46 -0700 Subject: [PATCH 13/18] Add changed that prevented merge back --- extract_msg/attachment.py | 8 ++++++++ extract_msg/exceptions.py | 7 ++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/extract_msg/attachment.py b/extract_msg/attachment.py index 96cd6d73..3dd65d3d 100644 --- a/extract_msg/attachment.py +++ b/extract_msg/attachment.py @@ -274,6 +274,14 @@ def saveEmbededMessage(self, **kwargs) -> None: """ self.data.save(**kwargs) + @property + def customHandler(self) -> Optional[CustomAttachmentHandler]: + """ + The instance of the custom handler associated with this attachment, if + it has one. + """ + return self.__customHandler + @property def data(self) -> Optional[Union[bytes, MSGFile]]: """ diff --git a/extract_msg/exceptions.py b/extract_msg/exceptions.py index d323746e..2223faca 100644 --- a/extract_msg/exceptions.py +++ b/extract_msg/exceptions.py @@ -46,6 +46,11 @@ class ConversionError(Exception): An error occured during type conversion. """ +class CustomAttachmentError(Exception): + """ + A generic error used for issues handling custom attachments. + """ + class DataNotFoundError(Exception): """ Requested stream type was unavailable. @@ -91,7 +96,7 @@ class StandardViolationError(InvalidFileFormatError): A critical violation of the MSG standards was detected and could not be recovered from. Recoverable violations will result in log messages instead. - Any that could reasonably be skipped, although are likely to still cause + Any that could reasonably be skipped, although are likely to still cause errors down the line, can be suppressed. """ From ac8e1b2398a99581073075e14580daec104be97c Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Tue, 9 May 2023 18:44:18 -0700 Subject: [PATCH 14/18] Update organization to match current version --- extract_msg/custom_attachments/__init__.py | 20 +++++++++++++++++-- .../custom_attachments/custom_handler.py | 18 ++++++++++++++--- .../custom_attachments/outlook_image.py | 17 +++++++++++++--- extract_msg/custom_attachments/utils.py | 6 ++++++ 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/extract_msg/custom_attachments/__init__.py b/extract_msg/custom_attachments/__init__.py index a8f84904..702bb9bf 100644 --- a/extract_msg/custom_attachments/__init__.py +++ b/extract_msg/custom_attachments/__init__.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + """ Submodule designed to help with saving and using custom attachments. Custom attachments are those follow standards not defined in the MSG documentation. Use @@ -15,7 +18,18 @@ CustomAttachmentHandler and add it using the `registerHandler` function. """ -from typing import List +__all__ = [ + # Classes. + 'CustomAttachmentHandler', + 'OutlookImage', + + # Functions. + 'getHandler', + 'registerHandler', +] + + +from typing import List, TYPE_CHECKING from .custom_handler import CustomAttachmentHandler @@ -32,10 +46,12 @@ from .outlook_image import OutlookImage +if TYPE_CHECKING: + from ..attachment import Attachment # Function designed to route to the correct handler. -def getHandler(attachment : 'Attachment') -> CustomAttachmentHandler: +def getHandler(attachment : Attachment) -> CustomAttachmentHandler: """ Takes an attachment and uses it to find the correct handler. Returns an instance created using the specified attachment. diff --git a/extract_msg/custom_attachments/custom_handler.py b/extract_msg/custom_attachments/custom_handler.py index c844151a..08dbccba 100644 --- a/extract_msg/custom_attachments/custom_handler.py +++ b/extract_msg/custom_attachments/custom_handler.py @@ -1,6 +1,18 @@ +from __future__ import annotations + + +__all__ = [ + 'CustomAttachmentHandler', +] + + import abc -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, TYPE_CHECKING + + +if TYPE_CHECKING: + from ..attachment import Attachment class CustomAttachmentHandler(abc.ABC): @@ -8,13 +20,13 @@ class CustomAttachmentHandler(abc.ABC): A class designed to help with custom attachments that may require parsing in special ways that are completely different from one another. """ - def __init__(self, attachment : 'Attachment'): + def __init__(self, attachment : Attachment): super().__init__() self.__att = attachment @classmethod @abc.abstractmethod - def isCorrectHandler(cls, attachment : 'Attachment') -> bool: + def isCorrectHandler(cls, attachment : Attachment) -> bool: """ Checks if this is the correct handler for the attachment. """ diff --git a/extract_msg/custom_attachments/outlook_image.py b/extract_msg/custom_attachments/outlook_image.py index 54428599..09b295a8 100644 --- a/extract_msg/custom_attachments/outlook_image.py +++ b/extract_msg/custom_attachments/outlook_image.py @@ -1,7 +1,15 @@ +from __future__ import annotations + + +__all__ = [ + 'OutlookImage', +] + + import base64 import struct -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, TYPE_CHECKING from . import registerHandler from .custom_handler import CustomAttachmentHandler @@ -10,12 +18,15 @@ from ..exceptions import CustomAttachmentError +if TYPE_CHECKING: + from ..attachment import Attachment + _ST_OLE = struct.Struct('' @classmethod - def isCorrectHandler(cls, attachment : 'Attachment') -> bool: + def isCorrectHandler(cls, attachment : Attachment) -> bool: if attachment.clsid != '00000316-0000-0000-C000-000000000046': return False diff --git a/extract_msg/custom_attachments/utils.py b/extract_msg/custom_attachments/utils.py index 8a61e3ec..18d8507e 100644 --- a/extract_msg/custom_attachments/utils.py +++ b/extract_msg/custom_attachments/utils.py @@ -3,6 +3,12 @@ submodule than for the main module. """ +__all__ = [ + 'htmlSplitRendered', + 'tokenizeHtml', +] + + import bs4 from typing import List From 68538adb9129143af8ae56c924fa01b97b84793a Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sat, 10 Jun 2023 15:24:04 -0700 Subject: [PATCH 15/18] Changing the way custom attachments work --- extract_msg/custom_attachments/__init__.py | 24 ++++++++++++++----- .../custom_attachments/custom_handler.py | 12 ++++------ ...{outlook_image.py => outlook_image_dib.py} | 23 +++++++----------- extract_msg/message_base.py | 14 ++--------- 4 files changed, 33 insertions(+), 40 deletions(-) rename extract_msg/custom_attachments/{outlook_image.py => outlook_image_dib.py} (82%) diff --git a/extract_msg/custom_attachments/__init__.py b/extract_msg/custom_attachments/__init__.py index 702bb9bf..13e32914 100644 --- a/extract_msg/custom_attachments/__init__.py +++ b/extract_msg/custom_attachments/__init__.py @@ -21,7 +21,7 @@ __all__ = [ # Classes. 'CustomAttachmentHandler', - 'OutlookImage', + 'OutlookImageDIB', # Functions. 'getHandler', @@ -29,21 +29,33 @@ ] -from typing import List, TYPE_CHECKING +from typing import List, Type, TYPE_CHECKING from .custom_handler import CustomAttachmentHandler # Create a way to register handlers. _knownHandlers : List[CustomAttachmentHandler] = [] -# This line is cheating a little bit, but is more efficient than wrapping it in -# a function. -registerHandler = _knownHandlers.append + +def registerHandler(handler : Type[CustomAttachmentHandler]) -> None: + """ + Registers the CustomAttachmentHandler subclass as a handler. + + :raises TypeError: The handler was not a subclass of + CustomAttachmentHandler. + """ + # Make sure it is a subclass of CustomAttachmentHandler. + if not isinstance(handler, type): + raise ValueError(':param handler: must be a class, not an instance of a class.') + if not issubclass(handler, CustomAttachmentHandler): + raise ValueError(':param handler: must be a subclass of CustomAttachmentHandler.') + _knownHandlers.append(handler) + # Import built-in handler modules. They will all automatically register their # respecive handler(s). -from .outlook_image import OutlookImage +from .outlook_image_dib import OutlookImageDIB if TYPE_CHECKING: diff --git a/extract_msg/custom_attachments/custom_handler.py b/extract_msg/custom_attachments/custom_handler.py index 08dbccba..90efa186 100644 --- a/extract_msg/custom_attachments/custom_handler.py +++ b/extract_msg/custom_attachments/custom_handler.py @@ -20,6 +20,7 @@ class CustomAttachmentHandler(abc.ABC): A class designed to help with custom attachments that may require parsing in special ways that are completely different from one another. """ + def __init__(self, attachment : Attachment): super().__init__() self.__att = attachment @@ -32,16 +33,11 @@ def isCorrectHandler(cls, attachment : Attachment) -> bool: """ @abc.abstractmethod - def injectHTML(self, html : bytes, renderedList : Optional[List[str]] = None) -> Tuple[bytes, Optional[List[str]]]: + def generateRtf(self) -> Optional[bytes]: """ - Adds the relevent tag, if any, to the HTML for making prepared HTML. - - If this function should do nothing, returns the two arguments without - modification. + Generates the RTF to inject in place of the \objattph tag. - :param html: The HTML body to inject into (if at all). - :param renderedList: The list to use (if needed) of "rendered - characters" which will be returned and matches the HTML returned. + If this function should do nothing, returns None """ @property diff --git a/extract_msg/custom_attachments/outlook_image.py b/extract_msg/custom_attachments/outlook_image_dib.py similarity index 82% rename from extract_msg/custom_attachments/outlook_image.py rename to extract_msg/custom_attachments/outlook_image_dib.py index 09b295a8..e05b71f6 100644 --- a/extract_msg/custom_attachments/outlook_image.py +++ b/extract_msg/custom_attachments/outlook_image_dib.py @@ -25,7 +25,12 @@ _ST_MAILSTREAM = struct.Struct(' bool: return True - def injectHTML(self, html : bytes, renderedList : Optional[List[str]] = None) -> Tuple[bytes, Optional[List[str]]]: - if not renderedList: - renderedList = htmlSplitRendered(html) - - rp = self.attachment.renderingPosition - - if rp >= len(renderedList): - raise CustomAttachmentError(f'Rendering position beyond calculated number of rendered characters (expected less than {len(renderedList)}, got {rp}).') - - renderedList[rp] = self.__htmlTag + renderedList[rp] - - return (''.join(renderedList).encode('utf-8'), renderedList) + def generateRtf(self) -> Optional[bytes]: + pass @property def data(self) -> bytes: @@ -106,4 +101,4 @@ def name(self) -> str: -registerHandler(OutlookImage) +registerHandler(OutlookImageDIB) diff --git a/extract_msg/message_base.py b/extract_msg/message_base.py index 381a0a4c..6c16e6ab 100644 --- a/extract_msg/message_base.py +++ b/extract_msg/message_base.py @@ -26,7 +26,7 @@ from . import constants from ._rtf.create_doc import createDocument from ._rtf.inject_rtf import injectStartRTF -from .enums import AttachmentType, DeencapType, RecipientType +from .enums import DeencapType, RecipientType from .exceptions import ( BadHtmlError, DataNotFoundError, DeencapMalformedData, DeencapNotEncapsulated, IncompatibleOptionsError, WKError @@ -1148,18 +1148,8 @@ def htmlBodyPrepared(self) -> Optional[bytes]: if not self.htmlBody: return self.htmlBody - html = self.htmlBody - - renderedCharacters = None - - # Iterate through all attachments, and inject all of the custom data - # from them. - for x in self.attachments: - if x.type is AttachmentType.CUSTOM: - html, renderedCharacters = x.customHandler.injectHTML(html, renderedCharacters) - # Create the BeautifulSoup instance to use. - soup = bs4.BeautifulSoup(html, 'html.parser') + soup = bs4.BeautifulSoup(self.htmlBody, 'html.parser') # Get a list of image tags to see if we can inject into. If the source # of an image starts with "cid:" that means it is one of the attachments From 197fc532bd7da04bff0aad278b61e42808dcaf90 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sat, 10 Jun 2023 15:26:54 -0700 Subject: [PATCH 16/18] Remove odd newline --- extract_msg/ole_writer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/extract_msg/ole_writer.py b/extract_msg/ole_writer.py index ec57f2d9..8642ca6b 100644 --- a/extract_msg/ole_writer.py +++ b/extract_msg/ole_writer.py @@ -648,7 +648,6 @@ def addOleEntry(self, path, entry : OleDirectoryEntry, data : Optional[bytes] = """ path = inputToMsgPath(path) # First, find the current place in our dict to add the item. - _dir = self.__getContainingStorage(path, False) # Now, check that the item *is not* already in our dict, as that would # cause problems. From 68cfb2531dca0b5ed163102008e85cbfa243c526 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Tue, 13 Jun 2023 15:08:53 -0700 Subject: [PATCH 17/18] Changed and finished the RTF code --- .../custom_attachments/custom_handler.py | 2 +- .../custom_attachments/outlook_image_dib.py | 42 ++++++++++++++++--- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/extract_msg/custom_attachments/custom_handler.py b/extract_msg/custom_attachments/custom_handler.py index 90efa186..e721754d 100644 --- a/extract_msg/custom_attachments/custom_handler.py +++ b/extract_msg/custom_attachments/custom_handler.py @@ -37,7 +37,7 @@ def generateRtf(self) -> Optional[bytes]: """ Generates the RTF to inject in place of the \objattph tag. - If this function should do nothing, returns None + If this function should do nothing, returns None. """ @property diff --git a/extract_msg/custom_attachments/outlook_image_dib.py b/extract_msg/custom_attachments/outlook_image_dib.py index e05b71f6..4ed48a45 100644 --- a/extract_msg/custom_attachments/outlook_image_dib.py +++ b/extract_msg/custom_attachments/outlook_image_dib.py @@ -66,11 +66,11 @@ def __init__(self, attachment : Attachment): # Unpack the mailstream and create the HTML tag. vals = _ST_MAILSTREAM.unpack(stream) self.__dvaspect = DVAspect(vals[0]) - self.__y = vals[1] - self.__x = vals[2] - hwStyle = f'height: {self.__x / 100.0:.2f}mm; width: {self.__y / 100.0:.2f}mm;' - imgData = f'data:image;base64,{base64.b64encode(self.__data).decode("ascii")}' - self.__htmlTag = f'' + self.__x = vals[1] + self.__y = vals[2] + # Convert to twips for RTF. + self.__xtwips = int(round(self.__x / 1.7639)) + self.__ytwips = int(round(self.__y / 1.7639)) @classmethod def isCorrectHandler(cls, attachment : Attachment) -> bool: @@ -88,7 +88,37 @@ def isCorrectHandler(cls, attachment : Attachment) -> bool: return True def generateRtf(self) -> Optional[bytes]: - pass + """ + Generates the RTF to inject in place of the \objattph tag. + + If this function should do nothing, returns None. + + This function requires PIL or Pillow. If neither are found, raises an + import error. + """ + try: + import PIL.Image + except ImportError: + raise ImportError('PIL or Pillow is required for inserting an Outlook Image into the body.') + + # First, convert the bitmap into a PNG so we can insert it into the + # body. + import io + + # Note, use self.data instead of self.__data to allow support for + # extensions. + with PIL.Image.open(io.BytesIO(self.data)) as img: + out = io.BytesIO() + img.save(out, 'PNG') + + hexData = out.getvalue().hex() + + inject = '{\\*\\shppict\n{\\pict\\picscalex100\\picscaley100' + inject += f'\\picw{img.width}\\pich{img.height}' + inject += f'\\picwgoal{self.__xtwips}\\pichgoal{self.__ytwips}\n' + inject += '\\pngblip ' + hexData + '}}' + + return inject.encode() @property def data(self) -> bytes: From beb9125a5bb770280da82c30b86ad666149a5100 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Tue, 13 Jun 2023 15:55:10 -0700 Subject: [PATCH 18/18] Finalize changes for transfer back to next-release --- CHANGELOG.md | 5 ++ extract_msg/attachment.py | 4 +- extract_msg/attachment_base.py | 47 +++++++++---------- .../custom_attachments/custom_handler.py | 3 ++ 4 files changed, 32 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0145e45d..39603a93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +**v0.42.0** +* Added new submodule `custom_attachments`. This submodule provides an extendable way to handle custom attachment types, attachment types whose structure and formatting are not defined in the Microsoft documentation for MSG files. +* Added new property `AttachmentBase.clsid` which returns the listed CLSID value of the data stream/storage of the attachment. +* Changed internal behavior of `MSGFile.attachments`. This should not cause any noticeable changes to the output. + **v0.41.5** * Fixed an issue from version `0.41.3` where the header being present but missing the `From` field would cause an exception. diff --git a/extract_msg/attachment.py b/extract_msg/attachment.py index 3dd65d3d..34cb797c 100644 --- a/extract_msg/attachment.py +++ b/extract_msg/attachment.py @@ -219,7 +219,7 @@ def save(self, **kwargs) -> Optional[Union[str, MSGFile]]: fullFilename = customPath / filename - if self.type is AttachmentType.DATA or (self.type is AttachmentType.CUSTOM and isinstance(self.__data, bytes)): + if isinstance(self.__data, bytes): if _zip: name, ext = os.path.splitext(filename) nameList = _zip.namelist() @@ -254,7 +254,7 @@ def save(self, **kwargs) -> Optional[Union[str, MSGFile]]: _zip.close() return str(fullFilename) - else: + elif self.__data: if kwargs.get('extractEmbedded', False): with _open(str(fullFilename), mode) as f: self.data.export(f) diff --git a/extract_msg/attachment_base.py b/extract_msg/attachment_base.py index 4659a325..1f1996a7 100644 --- a/extract_msg/attachment_base.py +++ b/extract_msg/attachment_base.py @@ -9,7 +9,7 @@ import datetime import logging -from functools import partial +from functools import cached_property, partial from typing import Optional, Tuple, TYPE_CHECKING from .enums import AttachmentType, ErrorBehavior, PropertiesType @@ -262,7 +262,7 @@ def existsTypedProperty(self, id, _type = None) -> bool: def attachmentEncoding(self) -> Optional[bytes]: """ The encoding information about the attachment object. Will return - b'*\x86H\x86\xf7\x14\x03\x0b\x01' if encoded in MacBinary format, + b'*\\x86H\\x86\\xf7\\x14\\x03\\x0b\\x01' if encoded in MacBinary format, otherwise it is unset. """ return self._ensureSet('_attachmentEncoding', '__substg1.0_37020102', False) @@ -287,35 +287,32 @@ def cid(self) -> Optional[str]: contendId = cid - @property + @cached_property def clsid(self) -> str: """ Returns the CLSID for the data stream/storage of the attachment. """ - try: - return self.__clsid - except AttributeError: - # Set some default values. - self.__clsid = '00000000-0000-0000-0000-000000000000' - dataStream = None - - # See if we can find the data stream/storage. - if self.type in (AttachmentType.CUSTOM, AttachmentType.MSG): + # Set some default values. + clsid = '00000000-0000-0000-0000-000000000000' + dataStream = None + + # See if we can find the data stream/storage. + if self.type in (AttachmentType.CUSTOM, AttachmentType.MSG): + dataStream = [self.__dir, '__substg1.0_3701000D'] + elif self.type is AttachmentType.DATA: + dataStream = [self.__dir, '__substg1.0_37010102'] + elif self.type is AttachmentType.UNSUPPORTED: + # Special check for custom attachments. + if self.exists('__substg1.0_3701000D'): dataStream = [self.__dir, '__substg1.0_3701000D'] - elif self.type is AttachmentType.DATA: + elif self.exists('__substg1.0_37010102'): dataStream = [self.__dir, '__substg1.0_37010102'] - elif self.type is AttachmentType.UNSUPPORTED: - # Special check for custom attachments. - if self.exists('__substg1.0_3701000D'): - dataStream = [self.__dir, '__substg1.0_3701000D'] - elif self.exists('__substg1.0_37010102'): - dataStream = [self.__dir, '__substg1.0_37010102'] - - # If we found the right item, get the CLSID. - if dataStream: - self.__clsid = self.__msg._getOleEntry(dataStream).clsid or '00000000-0000-0000-0000-000000000000' - - return self.__clsid + + # If we found the right item, get the CLSID. + if dataStream: + clsid = self.__msg._getOleEntry(dataStream).clsid or clsid + + return self.__clsid @property def dir(self) -> str: diff --git a/extract_msg/custom_attachments/custom_handler.py b/extract_msg/custom_attachments/custom_handler.py index e721754d..f6ebe21c 100644 --- a/extract_msg/custom_attachments/custom_handler.py +++ b/extract_msg/custom_attachments/custom_handler.py @@ -52,6 +52,9 @@ def attachment(self): def data(self) -> bytes: """ Gets the data for the attachment. + + If an attachment should do nothing when saving, return None from this + property. """ @property