Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e731c87
Trying to fix this branch (had to recreate and lost all commits)
TheElementalOfDestruction Jan 18, 2023
558c61f
Fix typo
TheElementalOfDestruction Jan 18, 2023
4f01d61
Attempting to fix merge
TheElementalOfDestruction Feb 13, 2023
8384e16
Final fix for merge
TheElementalOfDestruction Feb 13, 2023
ca8a870
Merge pull request #342 from TeamMsgExtractor/next-release
TheElementalOfDestruction Feb 13, 2023
5ce8cb2
Add back CUSTOM to AttachmentType enum
TheElementalOfDestruction Feb 13, 2023
6f9845b
Merge pull request #346 from TeamMsgExtractor/next-release
TheElementalOfDestruction Feb 27, 2023
340168b
Revert and adjust some changes to sync with next-release
TheElementalOfDestruction Mar 18, 2023
9110cf6
Merge branch 'outlook-signature' of https://github.com/TeamMsgExtract…
TheElementalOfDestruction Mar 18, 2023
1d27fd3
Merge pull request #350 from TeamMsgExtractor/next-release
TheElementalOfDestruction Mar 18, 2023
1f30ab8
Add back change that was blocking merge
TheElementalOfDestruction Mar 18, 2023
d2fdda7
Remove unneded imports (also resolve merge)
TheElementalOfDestruction May 9, 2023
88fd211
Attempting to resolve merge
TheElementalOfDestruction May 9, 2023
3a20c74
More resolving merge
TheElementalOfDestruction May 9, 2023
2a1b0f4
Checking if that was one of the merge issues
TheElementalOfDestruction May 9, 2023
7c1a480
Is this the issue?
TheElementalOfDestruction May 9, 2023
f77a234
Merge pull request #361 from TeamMsgExtractor/next-release
TheElementalOfDestruction May 9, 2023
f0cfe3e
Add changed that prevented merge back
TheElementalOfDestruction May 9, 2023
ac8e1b2
Update organization to match current version
TheElementalOfDestruction May 10, 2023
68538ad
Changing the way custom attachments work
TheElementalOfDestruction Jun 10, 2023
197fc53
Remove odd newline
TheElementalOfDestruction Jun 10, 2023
68cfb25
Changed and finished the RTF code
TheElementalOfDestruction Jun 13, 2023
19b1e6c
Merge pull request #370 from TeamMsgExtractor/next-release
TheElementalOfDestruction Jun 13, 2023
beb9125
Finalize changes for transfer back to next-release
TheElementalOfDestruction Jun 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
7 changes: 7 additions & 0 deletions changelog_temp.md
Original file line number Diff line number Diff line change
@@ -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.
28 changes: 21 additions & 7 deletions extract_msg/attachment.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

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
Expand Down Expand Up @@ -47,6 +48,7 @@ def __init__(self, msg, dir_):
located.
"""
super().__init__(msg, dir_)
self.__customHandler = None

if '37050003' not in self.props:
from .prop import createProp
Expand Down Expand Up @@ -80,9 +82,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
Expand Down Expand Up @@ -118,8 +122,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!
Expand Down Expand Up @@ -213,7 +219,7 @@ def save(self, **kwargs) -> Optional[Union[str, MSGFile]]:

fullFilename = customPath / filename

if self.type is AttachmentType.DATA:
if isinstance(self.__data, bytes):
if _zip:
name, ext = os.path.splitext(filename)
nameList = _zip.namelist()
Expand Down Expand Up @@ -248,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)
Expand All @@ -268,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]]:
"""
Expand Down
35 changes: 31 additions & 4 deletions extract_msg/attachment_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -287,10 +287,37 @@ def cid(self) -> Optional[str]:

contendId = cid

@cached_property
def clsid(self) -> str:
"""
Returns the CLSID for the data stream/storage of the attachment.
"""
# 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.exists('__substg1.0_37010102'):
dataStream = [self.__dir, '__substg1.0_37010102']

# 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):
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
Expand Down
79 changes: 79 additions & 0 deletions extract_msg/custom_attachments/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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
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.
"""

__all__ = [
# Classes.
'CustomAttachmentHandler',
'OutlookImageDIB',

# Functions.
'getHandler',
'registerHandler',
]


from typing import List, Type, TYPE_CHECKING

from .custom_handler import CustomAttachmentHandler


# Create a way to register handlers.
_knownHandlers : List[CustomAttachmentHandler] = []

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_dib import OutlookImageDIB


if TYPE_CHECKING:
from ..attachment import Attachment


# 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.')
65 changes: 65 additions & 0 deletions extract_msg/custom_attachments/custom_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from __future__ import annotations


__all__ = [
'CustomAttachmentHandler',
]


import abc

from typing import List, Optional, Tuple, TYPE_CHECKING


if TYPE_CHECKING:
from ..attachment import Attachment


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 generateRtf(self) -> Optional[bytes]:
"""
Generates the RTF to inject in place of the \objattph tag.

If this function should do nothing, returns None.
"""

@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.

If an attachment should do nothing when saving, return None from this
property.
"""

@property
@abc.abstractmethod
def name(self) -> str:
"""
Returns the name to be used when saving the attachment.
"""
Loading