From 758329c098ea367063516a1b9a0de6d9ae9610e5 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 8 Jun 2022 12:46:37 -0700 Subject: [PATCH 1/8] Update readme --- README.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.rst b/README.rst index 226130f4..75561394 100644 --- a/README.rst +++ b/README.rst @@ -209,6 +209,15 @@ Credits And thank you to everyone who has opened an issue and helped us track down those pesky bugs. +Extra +----- + +Check out the new project `msg-explorer`_ that allows you to open MSG files and +explore their contents in a GUI. It is usually updated within a few days of a +major release to ensure continued support. Because of this, it is recommended to +install it to a separate environment (like a vitural env) to not interfere with +your access to the newest major version of extract-msg. + .. |License: GPL v3| image:: https://img.shields.io/badge/License-GPLv3-blue.svg :target: LICENSE.txt @@ -227,3 +236,4 @@ And thank you to everyone who has opened an issue and helped us track down those .. _Seamus Tuohy: https://github.com/seamustuohy .. _Discord: https://discord.com/invite/B77McRmzdc .. _Buy Me a Coffee: https://www.buymeacoffee.com/DestructionE +.. _msg-explorer: https://pypi.org/project/msg-explorer/ From a2b8ad1009bfab0e59e91c1fd468af6f0e80dcec Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 8 Jun 2022 15:13:53 -0700 Subject: [PATCH 2/8] Started on release for #212 --- CHANGELOG.md | 8 ++++ README.rst | 4 +- extract_msg/__init__.py | 4 +- extract_msg/attachment.py | 2 +- extract_msg/attachment_base.py | 60 +++++++++++++++++++++++++++-- extract_msg/enums.py | 37 ++++++++++++++++++ extract_msg/msg.py | 64 ++++++++++++++++++++++++++++--- extract_msg/named.py | 23 +++++++++--- extract_msg/properties.py | 1 - extract_msg/recipient.py | 53 ++++++++++++++++++++++++-- extract_msg/task.py | 69 ++++++++++++++++++++++++++++++++++ extract_msg/utils.py | 4 ++ 12 files changed, 303 insertions(+), 26 deletions(-) create mode 100644 extract_msg/task.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e9e125ab..d5bdb6b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +**v0.33.0** +* [[TeamMsgExtractor #212](https://github.com/TeamMsgExtractor/msg-extractor/issues/212)] Added support for Task objects. +* Added additional parameter `overrideClass` to all `_ensureSetX` type functions. This parameter is a class to initialize using the data, if provided. The data will be provided as the first argument to the `__init__` function of the class *if* the data is not `None`. If the data is done, the value set and returned from `_ensureSetX` will be `None`. This can be overriden using the second added parameter, which defaults to this behavior. Simply set `preserveNone` to `False` to force the data to be passed to the class. Keep in mind that this means the class will receive `None` as it's first parameter. +* Updated `Named` class to make it more like the dictionary it is build on top of. Specifically, added `keys`, `values`, and `items` as functions. +* Fixed issue in `Named` where old code wasn't deleted, causing some named properties to be completely omitted. +* Improved efficiency of `Named` by making it so `get` no longer copied the entire dictionary repeatedly while looking for the property. +* Fixed typo in `Attachment` that caused embedded msg files to still regenerate the `Named` instance even though they should have been using the parent's. + **v0.32.0** * [[TeamMsgExtractor #217](https://github.com/TeamMsgExtractor/msg-extractor/issues/217)] Redesigned named properties to be much more efficient. All in all, load times for MSG files are significantly improved all around. * Named properties load dynamically. diff --git a/README.rst b/README.rst index 75561394..d53c2b0a 100644 --- a/README.rst +++ b/README.rst @@ -221,8 +221,8 @@ your access to the newest major version of extract-msg. .. |License: GPL v3| image:: https://img.shields.io/badge/License-GPLv3-blue.svg :target: LICENSE.txt -.. |PyPI3| image:: https://img.shields.io/badge/pypi-0.32.0-blue.svg - :target: https://pypi.org/project/extract-msg/0.32.0/ +.. |PyPI3| image:: https://img.shields.io/badge/pypi-0.33.0-blue.svg + :target: https://pypi.org/project/extract-msg/0.33.0/ .. |PyPI2| image:: https://img.shields.io/badge/python-3.6+-brightgreen.svg :target: https://www.python.org/downloads/release/python-367/ diff --git a/extract_msg/__init__.py b/extract_msg/__init__.py index bde7ff89..95fb9300 100644 --- a/extract_msg/__init__.py +++ b/extract_msg/__init__.py @@ -27,8 +27,8 @@ # along with this program. If not, see . __author__ = 'Destiny Peterson & Matthew Walker' -__date__ = '2022-06-07' -__version__ = '0.32.0' +__date__ = '2022-06-08' +__version__ = '0.33.0' import logging diff --git a/extract_msg/attachment.py b/extract_msg/attachment.py index e470114c..df674521 100644 --- a/extract_msg/attachment.py +++ b/extract_msg/attachment.py @@ -44,7 +44,7 @@ def __init__(self, msg, dir_): else: self.__prefix = msg.prefixList + [dir_, '__substg1.0_3701000D'] self.__type = AttachmentType.MSG - self.__data = openMsg(self.msg.path, prefix = self.__prefix, parent = self.msg, **self.msg.kwargs) + self.__data = openMsg(self.msg.path, prefix = self.__prefix, parentMsg = self.msg, **self.msg.kwargs) elif (self.props['37050003'].value & 0x7) == 0x7: # TODO Handling for special attacment type 0x7 self.__type = AttachmentType.WEB diff --git a/extract_msg/attachment_base.py b/extract_msg/attachment_base.py index 1c90772c..443bd306 100644 --- a/extract_msg/attachment_base.py +++ b/extract_msg/attachment_base.py @@ -30,13 +30,21 @@ def __init__(self, msg, dir_): self.__namedProperties = NamedProperties(msg.named, self) - def _ensureSet(self, variable, streamID, stringStream = True): + def _ensureSet(self, variable, streamID, stringStream = True, **kwargs): """ Ensures that the variable exists, otherwise will set it using the specified stream. After that, return said variable. If the specified stream is not a string stream, make sure to set :param stringStream: to False. + + :param overrideClass: Class/function to use to morph the data that was + read. The data will be the first argument to the class's __init__ + function or the function itself, if that is what is provided. By + default, this will be completely ignored if the value was not found. + :param preserveNone: If true (default), causes the function to ignore + :param overrideClass: when the value could not be found (is None). + If this is changed to False, then the value will be used regardless. """ try: return getattr(self, variable) @@ -45,25 +53,51 @@ def _ensureSet(self, variable, streamID, stringStream = True): value = self._getStringStream(streamID) else: value = self._getStream(streamID) + # Check if we should be overriding the data type for this instance. + if kwargs: + overrideClass = kwargs.get('overrideClass') + if overrideClass is not None and (value is not None or not kwargs.get('preserveNone', True)): + value = overrideClass(value) setattr(self, variable, value) return value - def _ensureSetNamed(self, variable, propertyName): + def _ensureSetNamed(self, variable, propertyName, **kwargs): """ Ensures that the variable exists, otherwise will set it using the named property. After that, return said variable. + + :param overrideClass: Class/function to use to morph the data that was + read. The data will be the first argument to the class's __init__ + function or the function itself, if that is what is provided. By + default, this will be completely ignored if the value was not found. + :param preserveNone: If true (default), causes the function to ignore + :param overrideClass: when the value could not be found (is None). + If this is changed to False, then the value will be used regardless. """ try: return getattr(self, variable) except AttributeError: value = self.namedProperties.get(propertyName) + # Check if we should be overriding the data type for this instance. + if kwargs: + overrideClass = kwargs.get('overrideClass') + if overrideClass is not None and (value is not None or not kwargs.get('preserveNone', True)): + value = overrideClass(value) setattr(self, variable, value) return value - def _ensureSetProperty(self, variable, propertyName): + def _ensureSetProperty(self, variable, propertyName, **kwargs): """ Ensures that the variable exists, otherwise will set it using the property. After that, return said variable. + + :param overrideClass: Class/function to use to morph the data that was + read. The data will be the first argument to the class's __init__ + function or the function itself, if that is what is provided. By + default, this will be completely ignored if the value was not found. + :param preserveNone: If true (default), causes the function to ignore + :param overrideClass: when the value could not be found (is None). + If this is changed to False, then the value will be used regardless. """ try: return getattr(self, variable) @@ -72,19 +106,37 @@ def _ensureSetProperty(self, variable, propertyName): value = self.props[propertyName].value except (KeyError, AttributeError): value = None + # Check if we should be overriding the data type for this instance. + if kwargs: + overrideClass = kwargs.get('overrideClass') + if overrideClass is not None and (value is not None or not kwargs.get('preserveNone', True)): + value = overrideClass(value) setattr(self, variable, value) return value - def _ensureSetTyped(self, variable, _id): + def _ensureSetTyped(self, variable, _id, **kwargs): """ Like the other ensure set functions, but designed for when something could be multiple types (where only one will be present). This way you have no need to set the type, it will be handled for you. + + :param overrideClass: Class/function to use to morph the data that was + read. The data will be the first argument to the class's __init__ + function or the function itself, if that is what is provided. By + default, this will be completely ignored if the value was not found. + :param preserveNone: If true (default), causes the function to ignore + :param overrideClass: when the value could not be found (is None). + If this is changed to False, then the value will be used regardless. """ try: return getattr(self, variable) except AttributeError: value = self._getTypedData(_id) + # Check if we should be overriding the data type for this instance. + if kwargs: + overrideClass = kwargs.get('overrideClass') + if overrideClass is not None and (value is not None or not kwargs.get('preserveNone', True)): + value = overrideClass(value) setattr(self, variable, value) return value diff --git a/extract_msg/enums.py b/extract_msg/enums.py index 8dcd0f95..107e082f 100644 --- a/extract_msg/enums.py +++ b/extract_msg/enums.py @@ -118,3 +118,40 @@ class Sensitivity(enum.Enum): PERSONAL = 1 PRIVATE = 2 CONFIDENTIAL = 3 + +class TaskMode(enum.Enum): + """ + The mode of the Task object used in task communication (PidLidTaskMode). + + UNASSIGNED: The Task object is not assigned. + EMBEDDED_REQUEST: The Task object is embedded in a task request. + ACCEPTED: The Task object has been accepted by the task assignee. + REJECTED: The Task object was rejected by the task assignee. + EMBEDDED_UPDATE: The Task object is embedded in a task update. + SELF_ASSIGNED: The Task object was assigned to the task assigner + (self-delegation). + """ + UNASSIGNED = 0 + EMBEDDED_REQUEST = 1 + ACCEPTED = 2 + REJECTED = 3 + EMBEDDED_UPDATE = 4 + SELF_ASSIGNED = 5 + + + +class TaskStatus(enum.Enum): + """ + The status of a task object (PidLidTaskStatus). + + NOT_STARTED: The user has not started the task. + IN_PROGRESS: The users's work on the Task object is in progress. + COMPLETE: The user's work on the Task object is complete. + WAITING_ON_OTHER: The user is waiting on somebody else. + DEFERRED: The user has deffered work on the Task object. + """ + NOT_STARTED = 0x00000000 + IN_PROGRESS = 0x00000001 + COMPLETE = 0x00000002 + WAITING_ON_OTHER = 0x00000003 + DEFERRED = 0x00000004 diff --git a/extract_msg/msg.py b/extract_msg/msg.py index 838ebba6..16bc449c 100644 --- a/extract_msg/msg.py +++ b/extract_msg/msg.py @@ -103,8 +103,8 @@ def __init__(self, path, **kwargs): kwargsCopy = copy.copy(kwargs) if 'prefix' in kwargsCopy: del kwargsCopy['prefix'] - if 'parent' in kwargsCopy: - del kwargsCopy['parent'] + if 'parentMsg' in kwargsCopy: + del kwargsCopy['parentMsg'] self.__kwargs = kwargsCopy prefixl = [] @@ -141,13 +141,21 @@ def __init__(self, path, **kwargs): else: self.filename = None - def _ensureSet(self, variable : str, streamID, stringStream : bool = True): + def _ensureSet(self, variable : str, streamID, stringStream : bool = True, **kwargs): """ Ensures that the variable exists, otherwise will set it using the specified stream. After that, return said variable. If the specified stream is not a string stream, make sure to set :param stringStream: to False. + + :param overrideClass: Class/function to use to morph the data that was + read. The data will be the first argument to the class's __init__ + function or the function itself, if that is what is provided. By + default, this will be completely ignored if the value was not found. + :param preserveNone: If true (default), causes the function to ignore + :param overrideClass: when the value could not be found (is None). + If this is changed to False, then the value will be used regardless. """ try: return getattr(self, variable) @@ -156,25 +164,51 @@ def _ensureSet(self, variable : str, streamID, stringStream : bool = True): value = self._getStringStream(streamID) else: value = self._getStream(streamID) + # Check if we should be overriding the data type for this instance. + if kwargs: + overrideClass = kwargs.get('overrideClass') + if overrideClass is not None and (value is not None or not kwargs.get('preserveNone', True)): + value = overrideClass(value) setattr(self, variable, value) return value - def _ensureSetNamed(self, variable : str, propertyName): + def _ensureSetNamed(self, variable : str, propertyName, **kwargs): """ Ensures that the variable exists, otherwise will set it using the named property. After that, return said variable. + + :param overrideClass: Class/function to use to morph the data that was + read. The data will be the first argument to the class's __init__ + function or the function itself, if that is what is provided. By + default, this will be completely ignored if the value was not found. + :param preserveNone: If true (default), causes the function to ignore + :param overrideClass: when the value could not be found (is None). + If this is changed to False, then the value will be used regardless. """ try: return getattr(self, variable) except AttributeError: value = self.namedProperties.get(propertyName) + # Check if we should be overriding the data type for this instance. + if kwargs: + overrideClass = kwargs.get('overrideClass') + if overrideClass is not None and (value is not None or not kwargs.get('preserveNone', True)): + value = overrideClass(value) setattr(self, variable, value) return value - def _ensureSetProperty(self, variable : str, propertyName): + def _ensureSetProperty(self, variable : str, propertyName, **kwargs): """ Ensures that the variable exists, otherwise will set it using the property. After that, return said variable. + + :param overrideClass: Class/function to use to morph the data that was + read. The data will be the first argument to the class's __init__ + function or the function itself, if that is what is provided. By + default, this will be completely ignored if the value was not found. + :param preserveNone: If true (default), causes the function to ignore + :param overrideClass: when the value could not be found (is None). + If this is changed to False, then the value will be used regardless. """ try: return getattr(self, variable) @@ -183,19 +217,37 @@ def _ensureSetProperty(self, variable : str, propertyName): value = self.mainProperties[propertyName].value except (KeyError, AttributeError): value = None + # Check if we should be overriding the data type for this instance. + if kwargs: + overrideClass = kwargs.get('overrideClass') + if overrideClass is not None and (value is not None or not kwargs.get('preserveNone', True)): + value = overrideClass(value) setattr(self, variable, value) return value - def _ensureSetTyped(self, variable : str, _id): + def _ensureSetTyped(self, variable : str, _id, **kwargs): """ Like the other ensure set functions, but designed for when something could be multiple types (where only one will be present). This way you have no need to set the type, it will be handled for you. + + :param overrideClass: Class/function to use to morph the data that was + read. The data will be the first argument to the class's __init__ + function or the function itself, if that is what is provided. By + default, this will be completely ignored if the value was not found. + :param preserveNone: If true (default), causes the function to ignore + :param overrideClass: when the value could not be found (is None). + If this is changed to False, then the value will be used regardless. """ try: return getattr(self, variable) except AttributeError: value = self._getTypedData(_id) + # Check if we should be overriding the data type for this instance. + if kwargs: + overrideClass = kwargs.get('overrideClass') + if overrideClass is not None and (value is not None or not kwargs.get('preserveNone', True)): + value = overrideClass(value) setattr(self, variable, value) return value diff --git a/extract_msg/named.py b/extract_msg/named.py index ee3a12de..bb5e094a 100644 --- a/extract_msg/named.py +++ b/extract_msg/named.py @@ -61,9 +61,7 @@ def __init__(self, msg): for entry in entries: streamID = properHex(0x8000 + entry['pid']) - #msg._registerNamedProperty(entry, entry['pkind'], names[entry['id']] if entry['pkind'] == NamedPropertyType.STRING_NAMED else None) - if msg.existsTypedProperty(streamID): - self.__properties.append(StringNamedProperty(entry, names[entry['id']]) if entry['pkind'] == NamedPropertyType.STRING_NAMED else NumericalNamedProperty(entry)) + self.__properties.append(StringNamedProperty(entry, names[entry['id']]) if entry['pkind'] == NamedPropertyType.STRING_NAMED else NumericalNamedProperty(entry)) for property in self.__properties: self.__propertiesDict[property.name if isinstance(property, StringNamedProperty) else property.propertyID] = property @@ -71,6 +69,12 @@ def __init__(self, msg): def __getitem__(self, key): return self.__propertiesDict[key] + def __iter__(self): + return self.__propertiesDict.__iter__() + + def __len__(self): + return self.__propertiesDict.__len__() + def _getStream(self, filename, prefix = True): return self.__msg._getStream([self.__dir, filename], prefix = prefix) @@ -102,20 +106,26 @@ def get(self, propertyName, default = None): if not found. """ try: - return self.namedProperties[propertyName] + return self.__propertiesDict[propertyName] except KeyError: propertyName = propertyName.upper() - for key in self.namedProperties.keys(): + for key in self.__propertiesDict.keys(): if propertyName == key.upper(): - return self.namedProperties[key] + return self.__propertiesDict[key] return default + def keys(self): + return self.__propertiesDict.keys() + def pprintKeys(self): """ Uses the pprint function on a sorted list of keys. """ pprint.pprint(sorted(tuple(self.__propertiesDict.keys()))) + def values(self): + return self.__propertiesDict.values() + @property def dir(self): """ @@ -138,6 +148,7 @@ def namedProperties(self): return copy.deepcopy(self.__propertiesDict) + class NamedProperties: """ An instance that uses a Named instance and an extract-msg class to read the diff --git a/extract_msg/properties.py b/extract_msg/properties.py index 486494c8..e7ae65d7 100644 --- a/extract_msg/properties.py +++ b/extract_msg/properties.py @@ -106,7 +106,6 @@ def pprintKeys(self): """ pprint.pprint(sorted(tuple(self.__props.keys()))) - def values(self): return self.__props.values() diff --git a/extract_msg/recipient.py b/extract_msg/recipient.py index 42ac5a5f..ad83493a 100644 --- a/extract_msg/recipient.py +++ b/extract_msg/recipient.py @@ -16,7 +16,7 @@ class Recipient: Contains the data of one of the recipients in an msg file. """ def __init__(self, _dir, msg): - self.__msg = msg # Allows calls to original msg file. + self.__msg = msg # Allows calls to original msg file. self.__dir = _dir self.__props = Properties(self._getStream('__properties_version1.0'), PropertiesType.RECIPIENT) self.__email = self._getStringStream('__substg1.0_39FE') @@ -27,13 +27,21 @@ def __init__(self, _dir, msg): self.__type = RecipientType(0xF & self.__typeFlags) self.__formatted = f'{self.__name} <{self.__email}>' - def _ensureSet(self, variable, streamID, stringStream : bool = True): + def _ensureSet(self, variable, streamID, stringStream : bool = True, **kwargs): """ Ensures that the variable exists, otherwise will set it using the specified stream. After that, return said variable. If the specified stream is not a string stream, make sure to set :param string stream: to False. + + :param overrideClass: Class/function to use to morph the data that was + read. The data will be the first argument to the class's __init__ + function or the function itself, if that is what is provided. By + default, this will be completely ignored if the value was not found. + :param preserveNone: If true (default), causes the function to ignore + :param overrideClass: when the value could not be found (is None). + If this is changed to False, then the value will be used regardless. """ try: return getattr(self, variable) @@ -42,13 +50,26 @@ def _ensureSet(self, variable, streamID, stringStream : bool = True): value = self._getStringStream(streamID) else: value = self._getStream(streamID) + # Check if we should be overriding the data type for this instance. + if kwargs: + overrideClass = kwargs.get('overrideClass') + if overrideClass is not None and (value is not None or not kwargs.get('preserveNone', True)): + value = overrideClass(value) setattr(self, variable, value) return value - def _ensureSetProperty(self, variable : str, propertyName : str): + def _ensureSetProperty(self, variable : str, propertyName : str, **kwargs): """ Ensures that the variable exists, otherwise will set it using the property. After that, return said variable. + + :param overrideClass: Class/function to use to morph the data that was + read. The data will be the first argument to the class's __init__ + function or the function itself, if that is what is provided. By + default, this will be completely ignored if the value was not found. + :param preserveNone: If true (default), causes the function to ignore + :param overrideClass: when the value could not be found (is None). + If this is changed to False, then the value will be used regardless. """ try: return getattr(self, variable) @@ -57,23 +78,47 @@ def _ensureSetProperty(self, variable : str, propertyName : str): value = self.props[propertyName].value except (KeyError, AttributeError): value = None + # Check if we should be overriding the data type for this instance. + if kwargs: + overrideClass = kwargs.get('overrideClass') + if overrideClass is not None and (value is not None or not kwargs.get('preserveNone', True)): + value = overrideClass(value) setattr(self, variable, value) return value - def _ensureSetTyped(self, variable : str, _id): + def _ensureSetTyped(self, variable : str, _id, **kwargs): """ Like the other ensure set functions, but designed for when something could be multiple types (where only one will be present). This way you have no need to set the type, it will be handled for you. + + :param overrideClass: Class/function to use to morph the data that was + read. The data will be the first argument to the class's __init__ + function or the function itself, if that is what is provided. By + default, this will be completely ignored if the value was not found. + :param preserveNone: If true (default), causes the function to ignore + :param overrideClass: when the value could not be found (is None). + If this is changed to False, then the value will be used regardless. """ try: return getattr(self, variable) except AttributeError: value = self._getTypedData(_id) + # Check if we should be overriding the data type for this instance. + if kwargs: + overrideClass = kwargs.get('overrideClass') + if overrideClass is not None and (value is not None or not kwargs.get('preserveNone', True)): + value = overrideClass(value) setattr(self, variable, value) return value def _getStream(self, filename): + """ + Gets a binary representation of the requested filename. + + This should ALWAYS return a bytes object if it was found, otherwise + returns None. + """ return self.__msg._getStream([self.__dir, filename]) def _getStringStream(self, filename): diff --git a/extract_msg/task.py b/extract_msg/task.py new file mode 100644 index 00000000..f764c5e5 --- /dev/null +++ b/extract_msg/task.py @@ -0,0 +1,69 @@ +import datetime +import logging + +from .enums import TaskMode, TaskStatus +from .msg import MSGFile + +logger = logging.getLogger(__name__) +logger.addHandler(logging.NullHandler()) + +class Task(MSGFile): + """ + Class used for parsing task files. + """ + + def __init__(self, path, **kwargs): + super().__init__(path, **kwargs) + + @property + def percentComplete(self) -> float: + """ + Indicates whether a time-flagged Message object is complete. Returns a + percentage in decimal form. 1.0 indicates it is complete. + """ + return self._ensureSetNamed('_percentComplete', '8102') + + @property + def taskDueDate(self) -> datetime.datetime: + """ + Specifies the date by which the user expects work on the task to be + complete. + """ + return self._ensureSetNamed('_taskStartDate', '8105') + + @property + def taskMode(self) -> TaskMode: + """ + Used in a task communication. Should be 0 (UNASSIGNED) on task objects. + """ + return self._ensureSetNamed('_taskMode', '8518', overrideClass = TaskMode) + + @property + def taskStartDate(self) -> datetime.datetime: + """ + Specifies the date on which the user expects work on the task to begin. + """ + return self._ensureSetNamed('_taskStartDate', '8104') + + @property + def taskStatus(self) -> TaskStatus: + """ + The completion status of a task. + """ + return self._ensureSetNamed('_taskStatus', '8101', overrideClass = TaskStatus) + + @property + def taskStatusOnComplete(self) -> bool: + """ + Indicates whether the task assignee has been requested to send an email + message upon completion of the assigned task. + """ + return self._ensureSetNamed('_taskStatusOnComplete', '8119') + + @property + def taskUpdates(self) -> bool: + """ + Indicates whether the task assignee has been requested to send a task + update when the assigned Task object changes. + """ + return self._ensureSetNamed('_taskUpdates', '811B') diff --git a/extract_msg/utils.py b/extract_msg/utils.py index 49af8cbf..7ec6d2d8 100644 --- a/extract_msg/utils.py +++ b/extract_msg/utils.py @@ -598,6 +598,7 @@ def openMsg(path, **kwargs): from .message import Message from .msg import MSGFile from .message_signed import MessageSigned + from .task import Task msg = MSGFile(path, **kwargs) # After rechecking the docs, all comparisons should be case-insensitive, not case-sensitive. My reading ability is great. @@ -614,6 +615,9 @@ def openMsg(path, **kwargs): elif classType.startswith('ipm.appointment') or classType.startswith('ipm.schedule'): msg.close() return Appointment(path, **kwargs) + elif classType.startswith('ipm.task'): + msg.close() + return Task(path, **kwargs) elif classType == 'ipm': # Unspecified format. It should be equal to this and not just start with it. return msg elif kwargs.get('strict', True): From bf674f9f568b972fbea784de3b31c28d6a2227ad Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 8 Jun 2022 15:26:51 -0700 Subject: [PATCH 3/8] Added more properties --- extract_msg/enums.py | 13 ++++++++++-- extract_msg/task.py | 50 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/extract_msg/enums.py b/extract_msg/enums.py index 107e082f..0ecd7e1d 100644 --- a/extract_msg/enums.py +++ b/extract_msg/enums.py @@ -119,6 +119,17 @@ class Sensitivity(enum.Enum): PRIVATE = 2 CONFIDENTIAL = 3 +class TaskHistory(enum.Enum): + """ + The type of the last change to the Task object. + """ + NONE = 0x00000000 + ACCEPTED = 0x00000001 + REJECTED = 0x00000002 + OTHER = 0x00000003 + DUE_DATE_CHANGED = 0x00000004 + ASSIGNED = 0x00000005 + class TaskMode(enum.Enum): """ The mode of the Task object used in task communication (PidLidTaskMode). @@ -138,8 +149,6 @@ class TaskMode(enum.Enum): EMBEDDED_UPDATE = 4 SELF_ASSIGNED = 5 - - class TaskStatus(enum.Enum): """ The status of a task object (PidLidTaskStatus). diff --git a/extract_msg/task.py b/extract_msg/task.py index f764c5e5..d3f53140 100644 --- a/extract_msg/task.py +++ b/extract_msg/task.py @@ -23,6 +23,28 @@ def percentComplete(self) -> float: """ return self._ensureSetNamed('_percentComplete', '8102') + @property + def taskActualEffort(self) -> int: + """ + Indicates the number of minutes that the user actually spent working on + a task. + """ + return self._ensureSetNamed('_taskActualEffort', '8110') + + @property + def taskAssigner(self) -> str: + """ + Specifies the name of the user that last assigned the task. + """ + return self._ensureSetNamed('_taskAssigner', '811F') + + @property + def taskComplete(self) -> bool: + """ + Indicates if the task is complete. + """ + return self._ensureSetNamed('_taskComplete', '811C') + @property def taskDueDate(self) -> datetime.datetime: """ @@ -31,6 +53,20 @@ def taskDueDate(self) -> datetime.datetime: """ return self._ensureSetNamed('_taskStartDate', '8105') + @property + def taskEstimatedEffort(self) -> int: + """ + Indicates the number of minutes that the user expects to work on a task. + """ + return self._ensureSetNamed('_taskEstimatedEffort', '8111') + + @property + def taskHistory(self) -> int: + """ + Indicates the type of change that was last made to the Task object. + """ + return self._ensureSetNamed('_taskHistory', '8110', overrideClass = TaskHistory) + @property def taskMode(self) -> TaskMode: """ @@ -38,6 +74,13 @@ def taskMode(self) -> TaskMode: """ return self._ensureSetNamed('_taskMode', '8518', overrideClass = TaskMode) + @property + def taskOwner(self) -> str: + """ + Contains the name of the owner of the task. + """ + return self._ensureSetNamed('_taskOwner', '811F') + @property def taskStartDate(self) -> datetime.datetime: """ @@ -67,3 +110,10 @@ def taskUpdates(self) -> bool: update when the assigned Task object changes. """ return self._ensureSetNamed('_taskUpdates', '811B') + + @property + def taskVersion(self) -> int: + """ + Indicates which copy is the latest update of a Task object. + """ + return self._ensureSetNamed('_taskVersion', '8113') From aa93ada7d7c6c0597a5e0f4ef162e3240371dcfd Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 8 Jun 2022 16:08:15 -0700 Subject: [PATCH 4/8] More properties --- CHANGELOG.md | 2 ++ extract_msg/__init__.py | 1 + extract_msg/enums.py | 23 +++++++++++++++++++ extract_msg/msg.py | 47 +++++++++++++++++++++++++++++++++----- extract_msg/task.py | 50 ++++++++++++++++++++++++++++++++++++++--- 5 files changed, 115 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5bdb6b0..f084fe84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ * Fixed issue in `Named` where old code wasn't deleted, causing some named properties to be completely omitted. * Improved efficiency of `Named` by making it so `get` no longer copied the entire dictionary repeatedly while looking for the property. * Fixed typo in `Attachment` that caused embedded msg files to still regenerate the `Named` instance even though they should have been using the parent's. +* Updated fields in `MSGFile` to use enums. +* Added additional properties to `MSGFile`. **v0.32.0** * [[TeamMsgExtractor #217](https://github.com/TeamMsgExtractor/msg-extractor/issues/217)] Redesigned named properties to be much more efficient. All in all, load times for MSG files are significantly improved all around. diff --git a/extract_msg/__init__.py b/extract_msg/__init__.py index 95fb9300..23129c6c 100644 --- a/extract_msg/__init__.py +++ b/extract_msg/__init__.py @@ -45,4 +45,5 @@ from .prop import createProp from .properties import Properties from .recipient import Recipient +from .task import Task from .utils import openMsg, properHex diff --git a/extract_msg/enums.py b/extract_msg/enums.py index 0ecd7e1d..222d20de 100644 --- a/extract_msg/enums.py +++ b/extract_msg/enums.py @@ -119,6 +119,15 @@ class Sensitivity(enum.Enum): PRIVATE = 2 CONFIDENTIAL = 3 +class TaskAcceptance(enum.Enum): + """ + The acceptance state of the task. + """ + NOT_ASSIGNED = 0x00000000 + UNKNOWN = 0x00000001 + ACCEPTED = 0x00000002 + REJECTED = 0x00000003 + class TaskHistory(enum.Enum): """ The type of the last change to the Task object. @@ -149,6 +158,20 @@ class TaskMode(enum.Enum): EMBEDDED_UPDATE = 4 SELF_ASSIGNED = 5 +class TaskOwnership(enum.Enum): + """ + The role of the current user relative to the Task object. + + NOT_ASSIGNED: The Task object is not assigned. + ASSIGNERS_COPY: The Task object is the task assigner's copy of the Task + object. + ASSIGNEES_COPY: The Task object is the task assignee's copy of the Task + object. + """ + NOT_ASSIGNED = 0x00000000 + ASSIGNERS_COPY = 0x00000001 + ASSIGNEES_COPY = 0x00000002 + class TaskStatus(enum.Enum): """ The status of a task object (PidLidTaskStatus). diff --git a/extract_msg/msg.py b/extract_msg/msg.py index 16bc449c..34272906 100644 --- a/extract_msg/msg.py +++ b/extract_msg/msg.py @@ -9,7 +9,7 @@ from . import constants from .attachment import Attachment, BrokenAttachment, UnsupportedAttachment -from .enums import AttachErrorBehavior, PropertiesType +from .enums import AttachErrorBehavior, Priority, PropertiesType, Sensitivity from .exceptions import InvalidFileFormatError, UnrecognizedMSGTypeError from .named import Named, NamedProperties from .prop import FixedLengthProp, VariableLengthProp @@ -609,6 +609,14 @@ def attachmentsReady(self) -> bool: """ return self.__attachmentsReady + @property + def classified(self) -> bool: + """ + Indicates whether the contents of this message are regarded as + classified information. + """ + return self._ensureSetNamed('_classified', '85B5') + @property def classType(self) -> str: """ @@ -616,6 +624,35 @@ def classType(self) -> str: """ return self._ensureSet('_classType', '__substg1.0_001A') + @property + def commonEnd(self) -> datetime.datetime: + """ + The end time for the object. + """ + return self._ensureSetNamed('_commonEnd', '8517') + + @property + def commonStart(self) -> datetime.datetime: + """ + The start time for the object. + """ + return self._ensureSetNamed('_commonStart', '8516') + + @property + def currentVersion(self) -> int: + """ + Specifies the build number of the client application that sent the + message. + """ + return self._ensureSetNamed('_currentVersion', '8552') + + @property + def currentVersionName(self) -> str: + """ + Specifies the name of the client application that sent the message. + """ + return self._ensureSetNamed('_currentVersionName', '8554') + @property def importance(self) -> int: """ @@ -706,18 +743,18 @@ def prefixList(self): return copy.deepcopy(self.__prefixList) @property - def priority(self) -> int: + def priority(self) -> Priority: """ The specified priority of the msg file. """ - return self._ensureSetProperty('_priority', '00260003') + return self._ensureSetProperty('_priority', '00260003', overrideClass = Priority) @property - def sensitivity(self) -> int: + def sensitivity(self) -> Sensitivity: """ The specified sensitivity of the msg file. """ - return self._ensureSetProperty('_sensitivity', '00360003') + return self._ensureSetProperty('_sensitivity', '00360003', overrideClass = Sensitivity) @property def stringEncoding(self): diff --git a/extract_msg/task.py b/extract_msg/task.py index d3f53140..470dffb0 100644 --- a/extract_msg/task.py +++ b/extract_msg/task.py @@ -1,7 +1,7 @@ import datetime import logging -from .enums import TaskMode, TaskStatus +from .enums import TaskAcceptance, TaskHistory, TaskMode, TaskOwnership, TaskStatus from .msg import MSGFile logger = logging.getLogger(__name__) @@ -23,6 +23,13 @@ def percentComplete(self) -> float: """ return self._ensureSetNamed('_percentComplete', '8102') + @property + def taskAcceptanceState(self) -> TaskAcceptance: + """ + Indicates the acceptance state of the task. + """ + return self._ensureSetNamed('_percentComplete', '812A', overrideClass = TaskAcceptance) + @property def taskActualEffort(self) -> int: """ @@ -45,6 +52,13 @@ def taskComplete(self) -> bool: """ return self._ensureSetNamed('_taskComplete', '811C') + @property + def taskCustomFlags(self) -> int: + """ + Custom flags set on the task. + """ + return self._ensureSetNamed('_taskCustomFlags', '8139') + @property def taskDueDate(self) -> datetime.datetime: """ @@ -61,11 +75,34 @@ def taskEstimatedEffort(self) -> int: return self._ensureSetNamed('_taskEstimatedEffort', '8111') @property - def taskHistory(self) -> int: + def taskFRecurring(self) -> bool: + """ + Indicates whether the task includes a recurrence pattern. + """ + return self._ensureSetNamed('_taskFRecurring', '8126') + + @property + def taskHistory(self) -> TaskHistory: """ Indicates the type of change that was last made to the Task object. """ - return self._ensureSetNamed('_taskHistory', '8110', overrideClass = TaskHistory) + return self._ensureSetNamed('_taskHistory', '811A', overrideClass = TaskHistory) + + @property + def taskLastDelegate(self) -> str: + """ + Contains the name of the user who most recently assigned the task, or + the user to whom it was most recently assigned. + """ + return self._ensureSetNamed('_taskLastDelegate', '8125') + + @property + def taskLastUser(self) -> str: + """ + Contains the name of the most recent user to have been the owner of the + task. + """ + return self._ensureSetNamed('_taskLastUser', '8122') @property def taskMode(self) -> TaskMode: @@ -81,6 +118,13 @@ def taskOwner(self) -> str: """ return self._ensureSetNamed('_taskOwner', '811F') + @property + def taskOwnership(self) -> TaskOwnership: + """ + Contains the name of the owner of the task. + """ + return self._ensureSetNamed('_taskOwnership', '8129', overrideClass = TaskOwnership) + @property def taskStartDate(self) -> datetime.datetime: """ From 6426606ac549996caccae0bf1c6967deca59e8bf Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 8 Jun 2022 16:08:52 -0700 Subject: [PATCH 5/8] Fix missing import --- extract_msg/msg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/extract_msg/msg.py b/extract_msg/msg.py index 34272906..58ddc2b3 100644 --- a/extract_msg/msg.py +++ b/extract_msg/msg.py @@ -1,5 +1,6 @@ import codecs import copy +import datetime import logging import os import pathlib From a12d248149dbc7335726d9a120fbed976ee7b0b0 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 8 Jun 2022 16:16:56 -0700 Subject: [PATCH 6/8] Changed parent for Task. --- extract_msg/msg.py | 1 + extract_msg/recipient.py | 1 + extract_msg/task.py | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/extract_msg/msg.py b/extract_msg/msg.py index 58ddc2b3..f74c0ca0 100644 --- a/extract_msg/msg.py +++ b/extract_msg/msg.py @@ -25,6 +25,7 @@ class MSGFile(olefile.OleFileIO): """ Parser for .msg files. """ + def __init__(self, path, **kwargs): """ :param path: path to the msg file in the system or is the raw msg file. diff --git a/extract_msg/recipient.py b/extract_msg/recipient.py index ad83493a..bee44649 100644 --- a/extract_msg/recipient.py +++ b/extract_msg/recipient.py @@ -15,6 +15,7 @@ class Recipient: """ Contains the data of one of the recipients in an msg file. """ + def __init__(self, _dir, msg): self.__msg = msg # Allows calls to original msg file. self.__dir = _dir diff --git a/extract_msg/task.py b/extract_msg/task.py index 470dffb0..176ace63 100644 --- a/extract_msg/task.py +++ b/extract_msg/task.py @@ -2,12 +2,12 @@ import logging from .enums import TaskAcceptance, TaskHistory, TaskMode, TaskOwnership, TaskStatus -from .msg import MSGFile +from .message_base import MessageBase logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) -class Task(MSGFile): +class Task(MessageBase): """ Class used for parsing task files. """ From b9467e6053e25841aaeca15b87f236c3e3eddd4d Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 8 Jun 2022 17:11:51 -0700 Subject: [PATCH 7/8] Signigicant cleanup --- CHANGELOG.md | 3 +- extract_msg/__main__.py | 3 +- extract_msg/appointment.py | 5 +- extract_msg/attachment.py | 5 +- extract_msg/attachment_base.py | 2 +- extract_msg/contact.py | 4 +- extract_msg/data.py | 4 +- extract_msg/dev.py | 4 +- extract_msg/dev_classes/attachment.py | 1 - extract_msg/dev_classes/message.py | 33 +++-- extract_msg/message.py | 21 +-- extract_msg/message_base.py | 14 +- extract_msg/message_signed.py | 19 +-- extract_msg/message_signed_base.py | 177 ++------------------------ extract_msg/msg.py | 7 +- extract_msg/named.py | 2 + extract_msg/properties.py | 3 +- extract_msg/recipient.py | 2 +- extract_msg/signed_attachment.py | 9 +- extract_msg/task.py | 2 + 20 files changed, 76 insertions(+), 244 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f084fe84..c5dc17a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ * Improved efficiency of `Named` by making it so `get` no longer copied the entire dictionary repeatedly while looking for the property. * Fixed typo in `Attachment` that caused embedded msg files to still regenerate the `Named` instance even though they should have been using the parent's. * Updated fields in `MSGFile` to use enums. -* Added additional properties to `MSGFile`. +* Added additional properties to `MSGFile`. +* Significant cleanup. **v0.32.0** * [[TeamMsgExtractor #217](https://github.com/TeamMsgExtractor/msg-extractor/issues/217)] Redesigned named properties to be much more efficient. All in all, load times for MSG files are significantly improved all around. diff --git a/extract_msg/__main__.py b/extract_msg/__main__.py index 15687e06..d9632638 100644 --- a/extract_msg/__main__.py +++ b/extract_msg/__main__.py @@ -4,7 +4,6 @@ import traceback from extract_msg import __doc__, utils -from extract_msg.message import Message def main() -> None: @@ -14,7 +13,7 @@ def main() -> None: level = logging.INFO if args.verbose else logging.WARNING # Determine where to save the files to. - currentDir = os.getcwd() # Store this incase the path changes. + currentDir = os.getcwd() # Store this in case the path changes. if not args.zip: if args.out_path: if not os.path.exists(args.out_path): diff --git a/extract_msg/appointment.py b/extract_msg/appointment.py index f5d4e207..1adac53b 100644 --- a/extract_msg/appointment.py +++ b/extract_msg/appointment.py @@ -1,5 +1,3 @@ -from . import constants -from .attachment import Attachment from .message_base import MessageBase @@ -33,8 +31,7 @@ def location(self): try: return self.__location except AttributeError: - self.__location = self.named.getNamedValue('8208') - self.__location = self.named.getNamedValue('0002') if self.__location is None else self.__location + self.__location = self.named.getNamedValue('8208') or self.named.getNamedValue('0002') return self.__location @property diff --git a/extract_msg/attachment.py b/extract_msg/attachment.py index df674521..25787d61 100644 --- a/extract_msg/attachment.py +++ b/extract_msg/attachment.py @@ -8,9 +8,8 @@ from . import constants from .attachment_base import AttachmentBase from .enums import AttachmentType -from .prop import FixedLengthProp, VariableLengthProp -from .properties import Properties -from .utils import createZipOpen, inputToString, openMsg, prepareFilename, verifyPropertyId, verifyType +from .utils import createZipOpen, inputToString, openMsg, prepareFilename + logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) diff --git a/extract_msg/attachment_base.py b/extract_msg/attachment_base.py index 443bd306..f15ecb68 100644 --- a/extract_msg/attachment_base.py +++ b/extract_msg/attachment_base.py @@ -1,12 +1,12 @@ import logging -from . import constants from .enums import PropertiesType from .named import NamedProperties from .prop import FixedLengthProp from .properties import Properties from .utils import verifyPropertyId, verifyType + logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) diff --git a/extract_msg/contact.py b/extract_msg/contact.py index 474697e7..d7e5d582 100644 --- a/extract_msg/contact.py +++ b/extract_msg/contact.py @@ -1,5 +1,3 @@ -from . import constants -from .attachment import Attachment from .msg import MSGFile @@ -113,7 +111,7 @@ def country(self): @property def departmentName(self): """ - The name of the dapartment the contact works in. + The name of the department the contact works in. """ return self._ensureSet('_departmentName', '__substg1.0_3A18') diff --git a/extract_msg/data.py b/extract_msg/data.py index e4f87b6d..b26a7c4b 100644 --- a/extract_msg/data.py +++ b/extract_msg/data.py @@ -17,7 +17,7 @@ def __init__(self, data : bytes): @property def globalCounter(self) -> int: """ - An unsigned ineger identifying the folder within its Store object. + An unsigned integer identifying the folder within its Store object. """ return self.__globalCounter @@ -42,7 +42,7 @@ def __init__(self, data): @property def globalCounter(self) -> int: """ - An unsigned ineger identifying the folder within its Store object. + An unsigned integer identifying the folder within its Store object. """ return self.__globalCounter diff --git a/extract_msg/dev.py b/extract_msg/dev.py index 448c35fd..7f137098 100644 --- a/extract_msg/dev.py +++ b/extract_msg/dev.py @@ -21,7 +21,7 @@ logger.addHandler(logging.NullHandler()) -def setupDevLogger(defaultPath=None, logfile = None, envKey='EXTRACT_MSG_LOG_CFG'): +def setupDevLogger(defaultPath = None, logfile = None, envKey = 'EXTRACT_MSG_LOG_CFG'): utils.setupLogging(defaultPath, 5, logfile, True, envKey) @@ -64,4 +64,4 @@ def main(args, argv): logpath = x.baseFilename except AttributeError: pass; - print(g'Logging complete. Log has been saved to {logpath}') + print(f'Logging complete. Log has been saved to {logpath}') diff --git a/extract_msg/dev_classes/attachment.py b/extract_msg/dev_classes/attachment.py index b5619773..e0f1157f 100644 --- a/extract_msg/dev_classes/attachment.py +++ b/extract_msg/dev_classes/attachment.py @@ -1,6 +1,5 @@ import logging -from .. import constants from ..enums import PropertiesType from ..properties import Properties from ..utils import properHex diff --git a/extract_msg/dev_classes/message.py b/extract_msg/dev_classes/message.py index ae71ef71..ea003d23 100644 --- a/extract_msg/dev_classes/message.py +++ b/extract_msg/dev_classes/message.py @@ -2,7 +2,6 @@ import logging import olefile -from .. import constants from ..enums import PropertiesType from ..dev_classes.attachment import Attachment from ..properties import Properties @@ -19,7 +18,7 @@ class Message(olefile.OleFileIO): Useful for malformed msg files. """ - def __init__(self, path, prefix='', filename=None): + def __init__(self, path, prefix = '', filename = None): """ :param path: path to the msg file in the system or is the raw msg file. :param prefix: used for extracting embedded msg files @@ -49,12 +48,12 @@ def __init__(self, path, prefix='', filename=None): self.__prefixList = prefixl if tmp_condition: - filename = self._getStringStream(prefixl[:-1] + ['__substg1.0_3001'], prefix=False) + filename = self._getStringStream(prefixl[:-1] + ['__substg1.0_3001'], prefix = False) if filename is not None: self.filename = filename else: - logger.log(5, f':param path: has __len__ attribute?: {has_len(path)}') - if has_len(path): + logger.log(5, f':param path: has __len__ attribute?: {hasLen(path)}') + if hasLen(path): if len(path) < 1536: self.filename = path logger.log(5, f':param path: length is {len(path)}; Using :param path: as file path') @@ -68,7 +67,7 @@ def __init__(self, path, prefix='', filename=None): recipientDirs = [] for dir_ in self.listDir(): - if dir_[len(self.__prefixList)].startswith('__recip') and\ + if dir_[len(self.__prefixList)].startswith('__recip') and \ dir_[len(self.__prefixList)] not in recipientDirs: recipientDirs.append(dir_[len(self.__prefixList)]) @@ -76,7 +75,7 @@ def __init__(self, path, prefix='', filename=None): self.attachments self.date - def _getStream(self, filename, prefix=True): + def _getStream(self, filename, prefix = True): filename = self.fix_path(filename, prefix) if self.exists(filename): stream = self.openstream(filename) @@ -85,13 +84,13 @@ def _getStream(self, filename, prefix=True): logger.info(f'Stream "{filename}" was requested but could not be found. Returning `None`.') return None - def _getStringStream(self, filename, prefer='unicode', prefix=True): + def _getStringStream(self, filename, prefix = True): """ Gets a string representation of the requested filename. This should ALWAYS return a string. """ - filename = self.fix_path(filename, prefix) + filename = self.fixPath(filename, prefix) if self.areStringsUnicode: return windowsUnicode(self._getStream(filename + '001F', prefix = False)) else: @@ -102,14 +101,14 @@ def exists(self, filename): """ Checks if :param filename: exists in the msg file. """ - filename = self.fix_path(filename) + filename = self.fixPath(filename) return self.exists(filename) def sExists(self, filename): """ Checks if string stream :param filename: exists in the msg file. """ - filename = self.fix_path(filename) + filename = self.fixPath(filename) return self.exists(filename + '001F') or self.exists(filename + '001E') def fixPath(self, filename, prefix=True): @@ -124,7 +123,7 @@ def fixPath(self, filename, prefix=True): filename = self.__prefix + filename return filename - def listDir(self, streams=True, storages=False): + def listDir(self, streams = True, storages = False): """ Replacement for OleFileIO.listdir that runs at the current prefix directory. """ @@ -142,15 +141,15 @@ def listDir(self, streams=True, storages=False): for pathEntry in entries: good = True # If the entry we are looking at is not longer then the prefix, it's not good. - if len(x) <= len(prefix): + if len(pathEntry) <= len(prefix): continue for index, entry in enumerate(prefix): - if x[y] != entry: + if pathEntry[index] != entry: good = False if good: - out.append(x) + out.append(pathEntry) return out @@ -181,7 +180,7 @@ def attachments(self): attachmentDirs = [] for dir_ in self.listDir(): - if dir_[len(self.__prefixList)].startswith('__attach') and\ + if dir_[len(self.__prefixList)].startswith('__attach') and \ dir_[len(self.__prefixList)] not in attachmentDirs: attachmentDirs.append(dir_[len(self.__prefixList)]) @@ -252,7 +251,7 @@ def recipients(self): recipientDirs = [] for dir_ in self.listDir(): - if dir_[len(self.__prefixList)].startswith('__recip') and\ + if dir_[len(self.__prefixList)].startswith('__recip') and \ dir_[len(self.__prefixList)] not in recipientDirs: recipientDirs.append(dir_[len(self.__prefixList)]) diff --git a/extract_msg/message.py b/extract_msg/message.py index 825035f2..57d2b5f0 100644 --- a/extract_msg/message.py +++ b/extract_msg/message.py @@ -9,7 +9,6 @@ from imapclient.imapclient import decode_utf7 from . import constants -from .attachment import Attachment from .exceptions import DataNotFoundError, IncompatibleOptionsError from .message_base import MessageBase from .utils import addNumToDir, addNumToZipDir, createZipOpen, injectHtmlHeader, injectRtfHeader, inputToBytes, inputToString, prepareFilename @@ -18,10 +17,12 @@ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) + class Message(MessageBase): """ Parser for Microsoft Outlook message files. """ + def __init__(self, path, **kwargs): super().__init__(path, **kwargs) @@ -103,7 +104,7 @@ def save(self, **kwargs): something parsing the html can properly determine the encoding (as not having this tag can cause errors in some programs). Set this to `None` or an empty string to not insert the tag (Default: 'utf-8'). - :param kwargs: Used to allow kwags expansion in the save function. + :param kwargs: Used to allow kwargs expansion in the save function. :param preparedHtml: When set, prepares the HTML body for standalone usage, doing things like adding tags, injecting attachments, etc. This is useful for things like trying to convert the HTML body @@ -232,17 +233,17 @@ def save(self, **kwargs): useRtf = False if html: if self.htmlBody: - useHtml = True - fext = 'html' + useHtml = True + fext = 'html' elif not allowFallback: - raise DataNotFoundError('Could not find the htmlBody') + raise DataNotFoundError('Could not find the htmlBody') if rtf or (html and not useHtml): if self.rtfBody: - useRtf = True - fext = 'rtf' + useRtf = True + fext = 'rtf' elif not allowFallback: - raise DataNotFoundError('Could not find the rtfBody') + raise DataNotFoundError('Could not find the rtfBody') if not skipAttachments: # Save the attachments. @@ -312,7 +313,7 @@ def getSaveHtmlBody(self, preparedHtml : bool = False, charset : str = 'utf-8', something parsing the html can properly determine the encoding (as not having this tag can cause errors in some programs). Set this to `None` or an empty string to not insert the tag (Default: 'utf-8'). - :param **kwargs: Used to allow kwargs expansion in the save function. + :param kwargs: Used to allow kwargs expansion in the save function. Arguments absorbed by this are simply ignored. :raises BadHtmlError: if :param preparedHtml: is False and the HTML @@ -355,7 +356,7 @@ def getSaveRtfBody(self, **kwargs) -> bytes: """ Returns the RTF body that will be used in saving based on the arguments. - :param **kwargs: Used to allow kwargs expansion in the save function. + :param kwargs: Used to allow kwargs expansion in the save function. Arguments absorbed by this are simply ignored. """ # Inject the header into the data. diff --git a/extract_msg/message_base.py b/extract_msg/message_base.py index be509e87..59509909 100644 --- a/extract_msg/message_base.py +++ b/extract_msg/message_base.py @@ -1,22 +1,19 @@ import base64 import email.utils +import html import logging -import os import re import bs4 import compressed_rtf import RTFDE -from . import constants -from .attachment import Attachment, BrokenAttachment, UnsupportedAttachment from .enums import RecipientType -from .exceptions import UnrecognizedMSGTypeError from .msg import MSGFile from .recipient import Recipient -from .utils import addNumToDir, inputToBytes, inputToString, prepareFilename +from .utils import inputToString, prepareFilename from email.parser import Parser as EmailParser -from imapclient.imapclient import decode_utf7 + logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) @@ -26,6 +23,7 @@ class MessageBase(MSGFile): """ Base class for Message like msg files. """ + def __init__(self, path, **kwargs): """ :param path: path to the msg file in the system or is the raw msg file. @@ -295,8 +293,8 @@ def htmlBody(self) -> bytes: elif self.body: # Convert the plain text body to html. logger.info('HTML body was not found, attempting to generate from plain text body.') - correctedBody = self.body.encode('utf-8').replace('\r', '').replace('\n', '
') - self._htmlBody = f'{correctedBody}' + correctedBody = html.escpae(self.body).replace('\r', '').replace('\n', '
') + self._htmlBody = f'{correctedBody}'.encode('utf-8') else: logger.info('HTML body could not be found nor generated.') diff --git a/extract_msg/message_signed.py b/extract_msg/message_signed.py index fc91e55c..141d0879 100644 --- a/extract_msg/message_signed.py +++ b/extract_msg/message_signed.py @@ -9,7 +9,6 @@ from imapclient.imapclient import decode_utf7 from . import constants -from .attachment import Attachment from .exceptions import DataNotFoundError, IncompatibleOptionsError from .message_signed_base import MessageSignedBase from .utils import addNumToDir, addNumToZipDir, createZipOpen, injectHtmlHeader, injectRtfHeader, inputToBytes, inputToString, prepareFilename @@ -18,10 +17,12 @@ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) + class MessageSigned(MessageSignedBase): """ Parser for Signed Microsoft Outlook message files. """ + def __init__(self, path, **kwargs): super().__init__(path, **kwargs) @@ -230,17 +231,17 @@ def save(self, **kwargs): useRtf = False if html: if self.htmlBody: - useHtml = True - fext = 'html' + useHtml = True + fext = 'html' elif not allowFallback: - raise DataNotFoundError('Could not find the htmlBody') + raise DataNotFoundError('Could not find the htmlBody') if rtf or (html and not useHtml): if self.rtfBody: - useRtf = True - fext = 'rtf' + useRtf = True + fext = 'rtf' elif not allowFallback: - raise DataNotFoundError('Could not find the rtfBody') + raise DataNotFoundError('Could not find the rtfBody') # Save the attachments. attachmentNames = [attachment.save(**kwargs) for attachment in self.attachments] @@ -308,7 +309,7 @@ def getSaveHtmlBody(self, preparedHtml : bool = False, charset : str = 'utf-8', something parsing the html can properly determine the encoding (as not having this tag can cause errors in some programs). Set this to `None` or an empty string to not insert the tag (Default: 'utf-8'). - :param **kwargs: Used to allow kwargs expansion in the save function. + :param kwargs: Used to allow kwargs expansion in the save function. Arguments absorbed by this are simply ignored. :raises BadHtmlError: if :param preparedHtml: is False and the HTML @@ -351,7 +352,7 @@ def getSaveRtfBody(self, **kwargs) -> bytes: """ Returns the RTF body that will be used in saving based on the arguments. - :param **kwargs: Used to allow kwargs expansion in the save function. + :param kwargs: Used to allow kwargs expansion in the save function. Arguments absorbed by this are simply ignored. """ # Inject the header into the data. diff --git a/extract_msg/message_signed_base.py b/extract_msg/message_signed_base.py index 7a63ba26..11950890 100644 --- a/extract_msg/message_signed_base.py +++ b/extract_msg/message_signed_base.py @@ -1,24 +1,14 @@ -import base64 import email.utils +import html import logging -import os import re -import bs4 -import compressed_rtf import mailbits -import RTFDE -from . import constants -from .exceptions import UnrecognizedMSGTypeError from .message_base import MessageBase -from .recipient import Recipient from .signed_attachment import SignedAttachment -from .utils import addNumToDir, inputToBytes, inputToString, prepareFilename +from .utils import inputToString -from email.parser import Parser as EmailParser - -from imapclient.imapclient import decode_utf7 logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) @@ -28,6 +18,7 @@ class MessageSignedBase(MessageBase): """ Base class for Message like msg files. """ + def __init__(self, path, **kwargs): """ :param path: path to the msg file in the system or is the raw msg file. @@ -86,15 +77,15 @@ def attachments(self) -> list: self._signedBody = None self._signedHtmlBody = None if len(atts) > 1: - logger.warn('') + logger.warning('Found more than one regular attachment when parsing signed message.') elif len(atts) == 0: - logger.warn('Failed to access any attachments from the signed message.') + logger.warning('Failed to access any attachments from the signed message.') return self._sAttachments try: mainAttachment = next(att for att in atts if hasattr(att, 'getFilename') and att.getFilename() == 'smime.p7m') except StopIteration: - logger.warn('Failed to find signed attachment.') + logger.warning('Failed to find signed attachment.') return self._sAttachments # If we are here, we should have the attachment. So now we need to @@ -133,14 +124,6 @@ def attachments(self) -> list: return self._sAttachments - - @property - def bcc(self): - """ - Returns the bcc field, if it exists. - """ - return self._genRecipient('bcc', 3) - @property def body(self): """ @@ -189,75 +172,13 @@ def htmlBody(self) -> bytes: elif self.body: # Convert the plain text body to html. logger.info('HTML body was not found, attempting to generate from plain text body.') - correctedBody = self.body.encode('utf-8').replace('\r', '').replace('\n', '
') - self._htmlBody = f'{correctedBody}' + correctedBody = html.escpae(self.body).replace('\r', '').replace('\n', '
') + self._htmlBody = f'{correctedBody}'.encode('utf-8') else: logger.info('HTML body could not be found nor generated.') return self._htmlBody - @property - def htmlBodyPrepared(self) -> bytes: - """ - Returns the HTML body that has (where possible) the embedded attachments - inserted into the body. - """ - # If we can't get an HTML body then we have nothing to do. - if not self.htmlBody: - return self.htmlBody - - # Create the BeautifulSoup instance to use. - 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 - # and is using the content id of that attachment. - tags = (tag for tag in soup.findAll('img') if tag.get('src') and tag.get('src').startswith('cid:')) - - for tag in tags: - # Iterate through the attachments until we get the right one. - cid = tag['src'][4:] - data = next((attachment.data for attachment in self.attachments if attachment.cid == cid), None) - # If we found anything, inject it. - if data: - tag['src'] = (b'data:image;base64,' + base64.b64encode(data)).decode('utf-8') - - return soup.prettify('utf-8') - - @property - def inReplyTo(self) -> str: - """ - Returns the message id that this message is in reply to. - """ - return self._ensureSet('_in_reply_to', '__substg1.0_1042') - - @property - def isRead(self) -> bool: - """ - Returns if this email has been marked as read. - """ - return bool(self.mainProperties['0E070003'].value & 1) - - @property - def messageId(self): - try: - return self._messageId - except AttributeError: - headerResult = None - if self.headerInit(): - headerResult = self._header['message-id'] - if headerResult is not None: - self._messageId = headerResult - else: - if self.headerInit(): - logger.info('Header found, but "Message-Id" is not included. Will be generated from other streams.') - self._messageId = self._getStringStream('__substg1.0_1035') - return self._messageId - - @property - def parsedDate(self): - return email.utils.parsedate(self.date) - @property def _rawAttachments(self): """ @@ -265,74 +186,6 @@ def _rawAttachments(self): """ return super().attachments - @property - def recipientSeparator(self) -> str: - return self.__recipientSeparator - - @property - def recipients(self) -> list: - """ - Returns a list of all recipients. - """ - try: - return self._recipients - except AttributeError: - # Get the recipients - recipientDirs = [] - prefixLen = self.prefixLen - for dir_ in self.listDir(): - if dir_[prefixLen].startswith('__recip') and\ - dir_[prefixLen] not in recipientDirs: - recipientDirs.append(dir_[prefixLen]) - - self._recipients = [] - - for recipientDir in recipientDirs: - self._recipients.append(Recipient(recipientDir, self)) - - return self._recipients - - @property - def rtfBody(self) -> bytes: - """ - Returns the decompressed Rtf body from the message. - """ - try: - return self._rtfBody - except AttributeError: - self._rtfBody = compressed_rtf.decompress(self.compressedRtf) if self.compressedRtf else None - return self._rtfBody - - @property - def sender(self) -> str: - """ - Returns the message sender, if it exists. - """ - try: - return self._sender - except AttributeError: - # Check header first - if self.headerInit(): - headerResult = self.header['from'] - if headerResult is not None: - self._sender = headerResult - return headerResult - logger.info('Header found, but "sender" is not included. Will be generated from other streams.') - # Extract from other fields - text = self._getStringStream('__substg1.0_0C1A') - email = self._getStringStream('__substg1.0_5D01') - # Will not give an email address sometimes. Seems to exclude the email address if YOU are the sender. - result = None - if text is None: - result = email - else: - result = text - if email is not None: - result += ' <' + email + '>' - - self._sender = result - return result - @property def signedAttachmentClass(self): """ @@ -361,17 +214,3 @@ def signedHtmlBody(self): except AttributeError: self.attachments return self._signedHtmlBody - - @property - def subject(self): - """ - Returns the message subject, if it exists. - """ - return self._ensureSet('_subject', '__substg1.0_0037') - - @property - def to(self): - """ - Returns the to field, if it exists. - """ - return self._genRecipient('to', 1) diff --git a/extract_msg/msg.py b/extract_msg/msg.py index f74c0ca0..142027d0 100644 --- a/extract_msg/msg.py +++ b/extract_msg/msg.py @@ -13,7 +13,7 @@ from .enums import AttachErrorBehavior, Priority, PropertiesType, Sensitivity from .exceptions import InvalidFileFormatError, UnrecognizedMSGTypeError from .named import Named, NamedProperties -from .prop import FixedLengthProp, VariableLengthProp +from .prop import FixedLengthProp from .properties import Properties from .utils import divide, getEncodingName, hasLen, inputToMsgpath, inputToString, msgpathToString, parseType, properHex, verifyPropertyId, verifyType, windowsUnicode @@ -21,6 +21,7 @@ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) + class MSGFile(olefile.OleFileIO): """ Parser for .msg files. @@ -29,9 +30,9 @@ class MSGFile(olefile.OleFileIO): def __init__(self, path, **kwargs): """ :param path: path to the msg file in the system or is the raw msg file. - :param prefix: Used for extracting embeded msg files inside the main + :param prefix: Used for extracting embedded msg files inside the main one. Do not set manually unless you know what you are doing. - :param parentMsg: Used for syncronizing named properties instances. Do + :param parentMsg: Used for synchronizing named properties instances. Do not set this unless you know what you are doing. :param attachmentClass: Optional, the class the MSGFile object will use for attachments. You probably should diff --git a/extract_msg/named.py b/extract_msg/named.py index bb5e094a..91fc3be2 100644 --- a/extract_msg/named.py +++ b/extract_msg/named.py @@ -7,9 +7,11 @@ from .utils import bytesToGuid, divide, properHex, roundUp from compressed_rtf.crc32 import crc32 + logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) + class Named: __dir = '__nameid_version1.0' def __init__(self, msg): diff --git a/extract_msg/properties.py b/extract_msg/properties.py index e7ae65d7..ad8bf7c1 100644 --- a/extract_msg/properties.py +++ b/extract_msg/properties.py @@ -5,7 +5,8 @@ from . import constants from .enums import Intelligence, PropertiesType from .prop import createProp -from .utils import divide, fromTimeStamp, filetimeToUtc, properHex +from .utils import divide, properHex + logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) diff --git a/extract_msg/recipient.py b/extract_msg/recipient.py index bee44649..98b5b5f1 100644 --- a/extract_msg/recipient.py +++ b/extract_msg/recipient.py @@ -1,8 +1,8 @@ import logging -from . import constants from .data import PermanentEntryID from .enums import PropertiesType, RecipientType +from .prop import FixedLengthProp from .properties import Properties from .utils import verifyPropertyId, verifyType diff --git a/extract_msg/signed_attachment.py b/extract_msg/signed_attachment.py index 2e509dd1..caa09328 100644 --- a/extract_msg/signed_attachment.py +++ b/extract_msg/signed_attachment.py @@ -1,16 +1,11 @@ import logging import os import pathlib -import random -import string import zipfile -from . import constants -from .attachment_base import AttachmentBase from .enums import AttachmentType -from .prop import FixedLengthProp, VariableLengthProp -from .properties import Properties -from .utils import createZipOpen, inputToString, openMsg, prepareFilename, verifyPropertyId, verifyType +from .utils import createZipOpen, inputToString, prepareFilename + logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) diff --git a/extract_msg/task.py b/extract_msg/task.py index 176ace63..70958a9b 100644 --- a/extract_msg/task.py +++ b/extract_msg/task.py @@ -4,9 +4,11 @@ from .enums import TaskAcceptance, TaskHistory, TaskMode, TaskOwnership, TaskStatus from .message_base import MessageBase + logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) + class Task(MessageBase): """ Class used for parsing task files. From ce45949e111607bcad6a947c0e866aaabd26dc65 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 8 Jun 2022 17:15:11 -0700 Subject: [PATCH 8/8] Final cleanup before testing --- extract_msg/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extract_msg/__init__.py b/extract_msg/__init__.py index 23129c6c..1df63cc8 100644 --- a/extract_msg/__init__.py +++ b/extract_msg/__init__.py @@ -9,7 +9,7 @@ https://github.com/TeamMsgExtractor/msg-extractor """ -# --- LICENSE.txt ----------------------------------------------------------------- +# --- LICENSE.txt -------------------------------------------------------------- # # Copyright 2013-2022 Matthew Walker and Destiny Peterson #