From 8121b062425059fccb38091ae466d917575d4165 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Fri, 4 Aug 2023 20:21:26 -0700 Subject: [PATCH 01/20] Bug fixes, cleanup, documentation updates --- CHANGELOG.md | 8 ++ extract_msg/msg_classes/message_base.py | 6 +- .../msg_classes/message_signed_base.py | 74 +++++++++---------- extract_msg/msg_classes/msg.py | 12 +-- extract_msg/properties/named.py | 46 ++++++------ extract_msg/utils.py | 17 +++-- 6 files changed, 86 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf48286b..b35ced36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +**v0.??.??** +* Updated typing information for some functions and classes. +* Fixed a bug with `MessageSignedBase.attachments` that would cause it to return None instead of an empty list if the number of normal attachments was 0 was the error behavior was set to ignore violations of the standard. +* Updated `MessageSignedBase.attachments` to use `functools.cached_property` instead of `property`. +* Fixed spelling errors in some exception strings. +* Made `NamedPropertyBase` a subclass of `abc.ABC`. +* Cleaned up some of the code for named properties to remove unused variables. + **v0.44.0** * Fixed a bug that caused `MessageBase.headerInit` to always return `False` after the 0.42.0 update. * Changed `MessageBase.headerInit` to a property. diff --git a/extract_msg/msg_classes/message_base.py b/extract_msg/msg_classes/message_base.py index 6664832d..f9dad190 100644 --- a/extract_msg/msg_classes/message_base.py +++ b/extract_msg/msg_classes/message_base.py @@ -24,7 +24,7 @@ from email import policy from email.message import EmailMessage from email.parser import HeaderParser -from typing import Callable, List, Optional, Union +from typing import Callable, Dict, List, Optional, Union from .. import constants from .._rtf.create_doc import createDocument @@ -119,7 +119,7 @@ def __init__(self, path, **kwargs): pass raise - def _genRecipient(self, recipientType, recipientInt : RecipientType) -> Optional[str]: + def _genRecipient(self, recipientType : str, recipientInt : RecipientType) -> Optional[str]: """ Returns the specified recipient field. """ @@ -1075,7 +1075,7 @@ def header(self) -> email.message.Message: return header @property - def headerDict(self) -> dict: + def headerDict(self) -> Dict: """ Returns a dictionary of the entries in the header """ diff --git a/extract_msg/msg_classes/message_signed_base.py b/extract_msg/msg_classes/message_signed_base.py index 08bcc297..8c1d32e2 100644 --- a/extract_msg/msg_classes/message_signed_base.py +++ b/extract_msg/msg_classes/message_signed_base.py @@ -8,25 +8,27 @@ import logging import re -from typing import List, Optional +from typing import Generic, List, Optional, Type, TypeVar +from ..attachments import AttachmentBase, SignedAttachment from ..enums import DeencapType, ErrorBehavior from ..exceptions import StandardViolationError from .message_base import MessageBase -from ..attachments import SignedAttachment from ..utils import inputToBytes, inputToString, unwrapMultipart logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) +_T = TypeVar('_T') -class MessageSignedBase(MessageBase): + +class MessageSignedBase(MessageBase, Generic[_T]): """ Base class for Message like msg files. """ - def __init__(self, path, **kwargs): + def __init__(self, path, signedAttachmentClass : Type[_T] = SignedAttachment, **kwargs): """ Supports all of the options from :method MessageBase.__init__: with some additional ones. @@ -34,45 +36,41 @@ def __init__(self, path, **kwargs): :param signedAttachmentClass: optional, the class the object will use for signed attachments. """ - self.__signedAttachmentClass = kwargs.get('signedAttachmentClass', SignedAttachment) + self.__sAttCls = signedAttachmentClass super().__init__(path, **kwargs) - @property - def attachments(self) -> List: + @functools.cached_property + def attachments(self) -> List[_T]: """ Returns a list of all attachments. :raises StandardViolationError: The standard for signed messages was blatantly violated. """ - try: - return self._sAttachments - except AttributeError: - atts = super().attachments - - if len(atts) != 1: - if ErrorBehavior.STANDARDS_VIOLATION in self.errorBehavior: - if len(atts) == 0: - logger.error('Signed message has no attachments, a violation of the standard.') - self._sAttachments = [] - self._signedBody = None - self._signedHtmlBody = None - return - # If there is at least one attachment, just try to use the - # first. - else: - raise StandardViolationError('Signed messages without exactly 1 (regular) attachment constitue a violation of the standard.') - - # We need to unwrap the multipart stream. - unwrapped = unwrapMultipart(atts[0].data) - - # Now store everything where it needs to be and make the - # attachments. - self._sAttachments = [self.__signedAttachmentClass(self, **att) for att in unwrapped['attachments']] - self._signedBody = unwrapped['plain_body'] - self._signedHtmlBody = inputToBytes(unwrapped['html_body'], 'utf-8') - - return self._sAttachments + atts = self._rawAttachments + + if len(atts) != 1: + if ErrorBehavior.STANDARDS_VIOLATION in self.errorBehavior: + if len(atts) == 0: + logger.error('Signed message has no attachments, a violation of the standard.') + self._sAttachments = [] + self._signedBody = None + self._signedHtmlBody = None + return [] + # If there is at least one attachment, just try to use the + # first. + else: + raise StandardViolationError('Signed messages without exactly 1 (regular) attachment constitute a violation of the standard.') + + # We need to unwrap the multipart stream. + unwrapped = unwrapMultipart(atts[0].data) + + # Now store everything where it needs to be and make the + # attachments. + self._signedBody = unwrapped['plain_body'] + self._signedHtmlBody = inputToBytes(unwrapped['html_body'], 'utf-8') + + return [self.__sAttCls(self, **att) for att in unwrapped['attachments']] @functools.cached_property def body(self) -> Optional[str]: @@ -119,18 +117,18 @@ def htmlBody(self) -> Optional[bytes]: return htmlBody @functools.cached_property - def _rawAttachments(self) -> List: + def _rawAttachments(self) -> List[AttachmentBase]: """ A property to allow access to the non-signed attachments. """ return super().attachments @property - def signedAttachmentClass(self): + def signedAttachmentClass(self) -> Type[_T]: """ The attachment class used for signed attachments. """ - return self.__signedAttachmentClass + return self.__sAttCls @functools.cached_property def signedBody(self) -> Optional[str]: diff --git a/extract_msg/msg_classes/msg.py b/extract_msg/msg_classes/msg.py index 620c4bf1..64b0a87e 100644 --- a/extract_msg/msg_classes/msg.py +++ b/extract_msg/msg_classes/msg.py @@ -19,7 +19,7 @@ import olefile -from typing import Any, Callable, cast, List, Optional, Set, Tuple, Union +from typing import Any, Callable, cast, Dict, List, Optional, Set, Tuple, Union from .. import constants from ..attachments import ( @@ -443,7 +443,7 @@ def _getTypedStream(self, filename, prefix : bool = True, _type = None): return True, parseType(int(_type, 16), contents, self.stringEncoding, extras) return False, None # We didn't find the stream. - def _oleListDir(self, streams : bool = True, storages : bool = False) -> List: + def _oleListDir(self, streams : bool = True, storages : bool = False) -> List[List[str]]: """ Calls :method OleFileIO.listdir: from the OleFileIO instance associated with this MSG file. Useful for if you need access to all the top level @@ -559,7 +559,7 @@ def fixPath(self, inp, prefix : bool = True) -> str: inp = self.__prefix + inp return inp - def listDir(self, streams : bool = True, storages : bool = False, includePrefix : bool = True) -> List[List]: + def listDir(self, streams : bool = True, storages : bool = False, includePrefix : bool = True) -> List[List[str]]: """ Replacement for OleFileIO.listdir that runs at the current prefix directory. @@ -584,7 +584,7 @@ def listDir(self, streams : bool = True, storages : bool = False, includePrefix entries = [x[prefixLength:] for x in entries] self.__listDirRes[(streams, storages, includePrefix)] = entries - return self.__listDirRes[(streams, storages, includePrefix)] + return entries def slistDir(self, streams : bool = True, storages : bool = False) -> List[str]: """ @@ -611,7 +611,7 @@ def saveAttachments(self, **kwargs) -> None: if not (skipHidden and attachment.hidden): attachment.save(**kwargs) - def saveRaw(self, path): + def saveRaw(self, path) -> None: # Create a 'raw' folder. path = pathlib.Path(path) # Make the location. @@ -777,7 +777,7 @@ def insecureFeatures(self) -> InsecureFeatures: return self.__inscFeat @property - def kwargs(self) -> dict: + def kwargs(self) -> Dict[str, object]: """ The kwargs used to initialize this message, excluding the prefix. This is used for initializing embedded msg files. diff --git a/extract_msg/properties/named.py b/extract_msg/properties/named.py index d7431ed4..75d47e54 100644 --- a/extract_msg/properties/named.py +++ b/extract_msg/properties/named.py @@ -10,11 +10,15 @@ ] +import abc import copy import logging import pprint -from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING, Union +from typing import ( + Any, Dict, Iterable, Iterator, List, Optional, Tuple, TYPE_CHECKING, + TypeVar, Union + ) from .. import constants from ..enums import NamedPropertyType @@ -30,6 +34,8 @@ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) +_T = TypeVar("_T") + class Named: """ @@ -46,14 +52,9 @@ def __init__(self, msg : MSGFile): self.guidStream = guidStream self.entryStream = entryStream self.namesStream = self._getStream('__substg1.0_00040102') or self._getStream('__substg1.0_00040102', False) - # The if else stuff is for protection against None. - guidStreamLength = len(guidStream) if guidStream else 0 - entryStreamLength = len(entryStream) if entryStream else 0 - self.__propertiesDict = {} - self.__properties = [] - self.__guids = tuple() - self.__names = {} + self.__propertiesDict : Dict[Tuple[str, str], NamedPropertyBase]= {} + self.__properties : List[NamedPropertyBase] = [] # Check that we even have any entries. If there are none, nothing to do. if entryStream: @@ -72,7 +73,6 @@ def __init__(self, msg : MSGFile): entries.append(entry) self.entries = entries - self.__guids = guids for entry in entries: self.__properties.append(StringNamedProperty(entry, self.__getName(entry['id'])) if entry['pkind'] == NamedPropertyType.STRING_NAMED else NumericalNamedProperty(entry)) @@ -84,7 +84,7 @@ def __init__(self, msg : MSGFile): def __contains__(self, key) -> bool: return key in self.__propertiesDict - def __getitem__(self, propertyName : Tuple[str, str]): + def __getitem__(self, propertyName : Tuple[str, str]) -> NamedPropertyBase: # Validate the key. if not hasattr(propertyName, '__len__') or len(propertyName) != 2: raise TypeError('Named property key must be a tuple of two strings.') @@ -97,7 +97,7 @@ def __getitem__(self, propertyName : Tuple[str, str]): raise KeyError(propertyName) - def __iter__(self): + def __iter__(self) -> Iterator[Tuple[str, str]]: return self.__propertiesDict.__iter__() def __len__(self) -> int: @@ -183,7 +183,7 @@ def sExists(self, filename) -> bool: raise ReferenceError('The msg file for this Named instance has been garbage collected.') return msg.sExists([self.__dir, filename]) - def get(self, propertyName, default = None): + def get(self, propertyName : Tuple[str, str], default : _T = None) -> Union[NamedPropertyBase, _T]: """ Tries to get a named property based on its key. Returns :param default: if not found. Key is a tuple of the name and the property set GUID. @@ -193,10 +193,10 @@ def get(self, propertyName, default = None): except KeyError: return default - def keys(self): + def keys(self) -> Iterable[Tuple[str, str]]: return self.__propertiesDict.keys() - def pprintKeys(self): + def pprintKeys(self) -> None: """ Uses the pprint function on a sorted list of keys. """ @@ -225,7 +225,7 @@ def msg(self) -> MSGFile: return msg @property - def namedProperties(self) -> Dict: + def namedProperties(self) -> Dict[Tuple[str, str], NamedPropertyBase]: """ Returns a copy of the dictionary containing all the named properties. """ @@ -238,9 +238,9 @@ class NamedProperties: An instance that uses a Named instance and an extract-msg class to read the data of named properties. """ - def __init__(self, named, streamSource : Union[MSGFile, AttachmentBase]): + def __init__(self, named : Named, streamSource : Union[MSGFile, AttachmentBase]): """ - :param named: The named instance to refer to for named properties + :param named: The Named instance to refer to for named properties entries. :param streamSource: The source to use for acquiring the data of a named property. @@ -278,8 +278,8 @@ def get(self, item, default = None): -class NamedPropertyBase: - def __init__(self, entry): +class NamedPropertyBase(abc.ABC): + def __init__(self, entry : Dict): self.__entry = entry self.__guidIndex = entry['guid_index'] self.__namedPropertyID = entry['pid'] @@ -315,7 +315,7 @@ def propertyStreamID(self) -> str: return self.__propertyStreamID @property - def rawEntry(self) -> dict: + def rawEntry(self) -> Dict: return copy.deepcopy(self.__entry) @property @@ -326,16 +326,16 @@ def rawEntryStream(self) -> bytes: return self.__entry['rawStream'] @property + @abc.abstractmethod def type(self) -> NamedPropertyType: """ The type of named property. """ - raise NotImplementedError('NamedPropertyBase cannot be used directly. Subclass it before using it.') class StringNamedProperty(NamedPropertyBase): - def __init__(self, entry, name): + def __init__(self, entry : Dict, name : str): super().__init__(entry) self.__name = name @@ -390,7 +390,7 @@ def type(self) -> NamedPropertyType: class NumericalNamedProperty(NamedPropertyBase): - def __init__(self, entry): + def __init__(self, entry : Dict): super().__init__(entry) self.__propertyID = properHex(entry['id'], 4).upper() self.__streamID = 0x1000 + (entry['id'] ^ (self.guidIndex << 1)) % 0x1F diff --git a/extract_msg/utils.py b/extract_msg/utils.py index 563a5ec9..bcffd3e5 100644 --- a/extract_msg/utils.py +++ b/extract_msg/utils.py @@ -73,7 +73,10 @@ import tzlocal from html import escape as htmlEscape -from typing import Any, Dict, List, Optional, TypeVar, TYPE_CHECKING, Union +from typing import ( + Any, Callable, Dict, List, Optional, Sequence, TypeVar, TYPE_CHECKING, + Union + ) from . import constants from .enums import AttachmentType @@ -184,7 +187,7 @@ def cloneOleFile(sourcePath, outputPath) -> None: writer.write(outputPath) -def createZipOpen(func): +def createZipOpen(func) -> Callable: """ Creates a wrapper for the open function of a ZipFile that will automatically set the current date as the modified time to the current time. @@ -246,7 +249,7 @@ def divide(string, length : int) -> List: >>>> print(a) ['Hello', ' Worl', 'd!'] """ - return [string[length * x:length * (x + 1)] for x in range(int(ceilDiv(len(string), length)))] + return [string[length * x:length * (x + 1)] for x in range(ceilDiv(len(string), length))] def filetimeToDatetime(rawTime : int) -> datetime.datetime: @@ -326,7 +329,7 @@ def fromTimeStamp(stamp : int) -> datetime.datetime: return datetime.datetime.fromtimestamp(stamp, tz) -def getCommandArgs(args) -> argparse.Namespace: +def getCommandArgs(args : Sequence[str]) -> argparse.Namespace: """ Parse command-line arguments. @@ -534,7 +537,7 @@ def htmlSanitize(inp : str) -> str: return inp -def inputToBytes(stringInputVar, encoding) -> bytes: +def inputToBytes(stringInputVar, encoding : str) -> bytes: """ Converts the input into bytes. @@ -550,7 +553,7 @@ def inputToBytes(stringInputVar, encoding) -> bytes: raise ConversionError('Cannot convert to bytes.') -def inputToMsgPath(inp) -> List: +def inputToMsgPath(inp) -> List[str]: """ Converts the input into an msg path. @@ -979,7 +982,7 @@ def unsignedToSignedInt(uInt : int) -> int: return constants.st.STI32.unpack(constants.st.STUI32.pack(uInt))[0] -def unwrapMsg(msg : MSGFile) -> Dict: +def unwrapMsg(msg : MSGFile) -> Dict[str, List]: """ Takes a recursive message-attachment structure and unwraps it into a linear dictionary for easy iteration. Dictionary contains 4 keys: "attachments" for From 8b907b1d038a1b113dbc380f6e63cbb7ae06b4cb Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Fri, 4 Aug 2023 20:37:12 -0700 Subject: [PATCH 02/20] More typing stuff --- CHANGELOG.md | 1 + extract_msg/properties/named.py | 2 +- extract_msg/properties/prop.py | 3 ++- extract_msg/properties/properties_store.py | 16 +++++++++------- extract_msg/structures/business_card.py | 2 +- extract_msg/structures/entry_id.py | 7 +++++-- extract_msg/structures/recurrence_pattern.py | 4 ++-- extract_msg/structures/system_time.py | 4 ++-- extract_msg/structures/time_zone_definition.py | 2 +- 9 files changed, 24 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b35ced36..8402bdf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Fixed spelling errors in some exception strings. * Made `NamedPropertyBase` a subclass of `abc.ABC`. * Cleaned up some of the code for named properties to remove unused variables. +* Changed `PropBase` to be a subclass of `abc.ABC`. **v0.44.0** * Fixed a bug that caused `MessageBase.headerInit` to always return `False` after the 0.42.0 update. diff --git a/extract_msg/properties/named.py b/extract_msg/properties/named.py index 75d47e54..34c32592 100644 --- a/extract_msg/properties/named.py +++ b/extract_msg/properties/named.py @@ -34,7 +34,7 @@ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) -_T = TypeVar("_T") +_T = TypeVar('_T') class Named: diff --git a/extract_msg/properties/prop.py b/extract_msg/properties/prop.py index c83fa7f4..bc139879 100644 --- a/extract_msg/properties/prop.py +++ b/extract_msg/properties/prop.py @@ -12,6 +12,7 @@ ] +import abc import datetime import logging @@ -37,7 +38,7 @@ def createProp(data : bytes) -> PropBase: return VariableLengthProp(data) -class PropBase: +class PropBase(abc.ABC): """ Base class for Prop instances. """ diff --git a/extract_msg/properties/properties_store.py b/extract_msg/properties/properties_store.py index 16244d35..a5f4623a 100644 --- a/extract_msg/properties/properties_store.py +++ b/extract_msg/properties/properties_store.py @@ -8,7 +8,7 @@ import logging import pprint -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Iterator, Optional, TypeVar, Union from warnings import warn from .. import constants @@ -20,6 +20,8 @@ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) +_T = TypeVar('_T') + class PropertiesStore: """ @@ -34,7 +36,7 @@ def __init__(self, data : Optional[bytes], _type : Optional[PropertiesType] = No raise TypeError(':param data: MUST be bytes.') self.__rawData = data self.__len = len(data) - self.__props : Dict[PropBase] = {} + self.__props : Dict[str, PropBase] = {} self.__naid = None self.__nrid = None self.__ac = None @@ -79,10 +81,10 @@ def __init__(self, data : Optional[bytes], _type : Optional[PropertiesType] = No def __contains__(self, key) -> bool: return self.__props.__contains__(key) - def __getitem__(self, key) -> PropBase: + def __getitem__(self, key : str) -> PropBase: return self.__props.__getitem__(key) - def __iter__(self): + def __iter__(self) -> Iterator[str]: return self.__props.__iter__() def __len__(self) -> int: @@ -94,7 +96,7 @@ def __len__(self) -> int: def __repr__(self) -> str: return self.__props.__repr__() - def get(self, name, default = None) -> Optional[Union[PropBase, Any]]: + def get(self, name, default : _T = None) -> Union[PropBase, _T]: """ Retrieve the property of :param name:. Returns the value of :param default: if the property could not be found. @@ -194,14 +196,14 @@ def nextRecipientId(self) -> int: return self.__nrid @property - def props(self) -> Dict: + def props(self) -> Dict[str, PropBase]: """ Returns a copy of the internal properties dict. """ return copy.deepcopy(self.__props) @property - def _propDict(self) -> Dict: + def _propDict(self) -> Dict[str, PropBase]: """ A direct reference to the underlying property dictionary. Used in one place in the code, and not recommended to be used if you are not a diff --git a/extract_msg/structures/business_card.py b/extract_msg/structures/business_card.py index 24edad88..d6f64294 100644 --- a/extract_msg/structures/business_card.py +++ b/extract_msg/structures/business_card.py @@ -75,7 +75,7 @@ def fieldInfoSize(self) -> int: return self.__fieldInfoSize @property - def fields(self) -> Tuple['FieldInfo']: + def fields(self) -> Tuple['FieldInfo', ...]: """ The field info structures """ diff --git a/extract_msg/structures/entry_id.py b/extract_msg/structures/entry_id.py index 0ad03fe3..777a7b27 100644 --- a/extract_msg/structures/entry_id.py +++ b/extract_msg/structures/entry_id.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + __all__ = [ 'AddressBookEntryID', 'ContactAddressEntryID', @@ -35,7 +38,7 @@ class EntryID: """ @classmethod - def autoCreate(cls, data) -> 'EntryID': + def autoCreate(cls, data) -> EntryID: """ Automatically determines the type of EntryID and returns an instance of the correct subclass. If the subclass cannot be determined, will return @@ -187,7 +190,7 @@ def __init__(self, data : bytes): self.__entryID = MessageEntryID(reader.read(self.__entryIdCount)) @property - def entryID(self) -> 'MessageEntryID': + def entryID(self) -> MessageEntryID: """ The EntryID contained in this object. """ diff --git a/extract_msg/structures/recurrence_pattern.py b/extract_msg/structures/recurrence_pattern.py index 5743f8d6..2a37b0bc 100644 --- a/extract_msg/structures/recurrence_pattern.py +++ b/extract_msg/structures/recurrence_pattern.py @@ -60,7 +60,7 @@ def calendarType(self) -> RecurCalendarType: return self.__calendarType @property - def deletedInstanceDates(self) -> Tuple[int]: + def deletedInstanceDates(self) -> Tuple[int, ...]: """ A tuple of the dates (stored as number of minutes between midnight, January 1, 1601, and midnight on the specified day in the timezone @@ -104,7 +104,7 @@ def firstDayOfWeek(self) -> RecurDOW: return self.__firstDOW @property - def modifiedInstanceDates(self) -> Tuple[int]: + def modifiedInstanceDates(self) -> Tuple[int, ...]: """ A tuple of the dates (stored as number of minutes between midnight, January 1, 1601, and midnight on the specified day in the timezone diff --git a/extract_msg/structures/system_time.py b/extract_msg/structures/system_time.py index e9f91067..fc9b82f5 100644 --- a/extract_msg/structures/system_time.py +++ b/extract_msg/structures/system_time.py @@ -23,10 +23,10 @@ class SystemTime: def __init__(self, data : bytes): self.unpack(data) - def __eq__(self, other): + def __eq__(self, other) -> bool: return self.pack() == other.pack() - def __ne__(self, other): + def __ne__(self, other) -> bool: return not self.__eq__(other) def pack(self) -> bytes: diff --git a/extract_msg/structures/time_zone_definition.py b/extract_msg/structures/time_zone_definition.py index b7ea0d4f..d4f3d77a 100644 --- a/extract_msg/structures/time_zone_definition.py +++ b/extract_msg/structures/time_zone_definition.py @@ -56,7 +56,7 @@ def rawData(self) -> bytes: return self.__rawData @property - def rules(self) -> Tuple[TZRule]: + def rules(self) -> Tuple[TZRule, ...]: """ A tuple of TZRule structures that specifies a time zone. """ From db5ccbbe2f34c5df3b975746e1a45c86d3ffe0e9 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sat, 5 Aug 2023 17:47:23 -0700 Subject: [PATCH 03/20] Work on new public api --- CHANGELOG.md | 9 +- README.rst | 18 +++ extract_msg/attachments/__init__.py | 2 +- extract_msg/attachments/attachment.py | 2 +- extract_msg/attachments/attachment_base.py | 65 ++++++--- .../custom_att_handler/outlook_image_dib.py | 6 +- extract_msg/enums.py | 14 -- extract_msg/msg_classes/calendar_base.py | 2 +- extract_msg/msg_classes/contact.py | 128 +++++++++--------- extract_msg/msg_classes/message_base.py | 36 ++--- .../msg_classes/message_signed_base.py | 4 +- extract_msg/msg_classes/msg.py | 78 +++++++---- extract_msg/msg_classes/post.py | 2 +- extract_msg/ole_writer.py | 4 +- extract_msg/properties/named.py | 55 ++++++-- extract_msg/properties/properties_store.py | 18 +-- extract_msg/recipient.py | 78 ++++++++--- 17 files changed, 316 insertions(+), 205 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8402bdf7..49dbe2bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,16 @@ -**v0.??.??** +**v0.45.0** * Updated typing information for some functions and classes. * Fixed a bug with `MessageSignedBase.attachments` that would cause it to return None instead of an empty list if the number of normal attachments was 0 was the error behavior was set to ignore violations of the standard. * Updated `MessageSignedBase.attachments` to use `functools.cached_property` instead of `property`. * Fixed spelling errors in some exception strings. * Made `NamedPropertyBase` a subclass of `abc.ABC`. -* Cleaned up some of the code for named properties to remove unused variables. +* Cleaned up some of the code for named properties to remove unused variables and remove inefficient code. * Changed `PropBase` to be a subclass of `abc.ABC`. +* Added detailed versioning info to the README. +* Deprecated many private functions, including methods on many of the classes. Of primary note are `_getStream` and `_getStringStream`, which have been moved to the public API as `getStream` and `getStringStream`. Any deprecated functions still exist and will forward to a public API function if they are not being removed. Additionally, all internal usage of them has been removed. This change is one of the big preparations that is needed for the `1.0.0` release. + * As mentioned, a number of these deprecated functions have been moved to the public api. It is recommended that you run tests with your code after enabling deprecation warnings to see what should be changed. +* Removed items deprecated in or before `0.42.0`. +* Changed the API for the private method `_genRecipient`. This is not intended for use outside of the module *except* for subclasses. The change removed the allowance of ints for the second argument, requiring that it be a valid enum type. **v0.44.0** * Fixed a bug that caused `MessageBase.headerInit` to always return `False` after the 0.42.0 update. diff --git a/README.rst b/README.rst index 4b14b9a3..f19f4c49 100644 --- a/README.rst +++ b/README.rst @@ -198,6 +198,23 @@ installed: * ``all``: Installs all of the extras. * ``mime``: Installs dependency used for mimetype generation when a mimetype is not specified. +Versioning +---------- + +This module uses Semantic Versioning, however it has not always done so. All versions greater than or equal to 0.40.* conform successfully. As the package is currently in major version zero (0.*.*), anything MAY change at any time, as per point 4 of the SemVer specification. However, I, Destiny, am aware of the module's usage in other packages and code, and so I have taken efforts to make the versioning more reliable. + +Any change to the minor version MUST be considered a potentially breaking change, and the changelog should be checked before assuming the API will function in the way it did in the previous minor version. I do, however, try to keep the API relatively stable between minor versions, so most typical usage is likely to remain entirely unaffected. + +Any change to a patch version before the 1.0.0 release SHOULD either add functionality or have no visible difference in usage, aside from changes to the typing infomation or from a bug fix correcting the data that a component created. + +In addition to the above conditions, it must be noted that any class, variable, function, etc., that is preceded by one or more underscores, excluding items preceded by two underscores and also proceeded by two underscores, MUST NOT be considered part of the public api. These methods may change at any time, in any way. + +I am aware of the F.A.Q. question that suggests that I should probably have pushed the module to a 1.0.0 release due to its usage in production, however there are a number of different items on the TODO list that I feel should be completed before that time. While some are simply important features I believe should exist, others are overhauls to sections of the public API that have needed careful fixing for quite a while, fixes that have slowly been happening throughout the versions. An important change was made in the 0.45.0 release which deprecates a large number of commonly used private functions and created more stable versions of them in the public API. + +Additionally, my focus on versioning info has revealed that some of the dependencies are still in major version 0 *or* do not necessarily conform to Semantic Versioning. As such, these packages are more tightly constrained on what versions are considered acceptable, and careful consideration should be taken before extending the accepted range of versions. + +Details on Semantic Versioning can be found at `semver.org`_. + Todo ---- @@ -268,3 +285,4 @@ your access to the newest major version of extract-msg. .. _wiki: https://github.com/TeamMsgExtractor/msg-extractor/wiki .. _Read the Docs: https://msg-extractor.rtfd.io/ .. _Changelog: https://github.com/TeamMsgExtractor/msg-extractor/blob/master/CHANGELOG.md +.. _`semver.org`: https://semver.org diff --git a/extract_msg/attachments/__init__.py b/extract_msg/attachments/__init__.py index 4edf59b6..d2a18ce8 100644 --- a/extract_msg/attachments/__init__.py +++ b/extract_msg/attachments/__init__.py @@ -63,7 +63,7 @@ def initStandardAttachment(msg : MSGFile, dir_) -> AttachmentBase: ) # First, create the properties store to check things like attachment type. - propertiesStream = msg._getStream([dir_, '__properties_version1.0']) + propertiesStream = msg.getStream([dir_, '__properties_version1.0']) propStore = PropertiesStore(propertiesStream, PropertiesType.ATTACHMENT) try: diff --git a/extract_msg/attachments/attachment.py b/extract_msg/attachments/attachment.py index 72849139..b3b6a38c 100644 --- a/extract_msg/attachments/attachment.py +++ b/extract_msg/attachments/attachment.py @@ -44,7 +44,7 @@ def __init__(self, msg : MSGFile, dir_, propStore : PropertiesStore): use. """ super().__init__(msg, dir_, propStore) - self.__data = self._getStream('__substg1.0_37010102') + self.__data = self.getStream('__substg1.0_37010102') def getFilename(self, **kwargs) -> str: """ diff --git a/extract_msg/attachments/attachment_base.py b/extract_msg/attachments/attachment_base.py index 17d6dd81..0a18d357 100644 --- a/extract_msg/attachments/attachment_base.py +++ b/extract_msg/attachments/attachment_base.py @@ -104,9 +104,9 @@ def _getStream(self, filename) -> Optional[bytes]: :raises ReferenceError: The associated MSGFile instance has been garbage collected. """ - if (msg := self.__msg()) is None: - raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') - return msg._getStream([self.__dir, filename]) + import warnings + warnings.warn(':method _getStream: has been deprecated and moved to the public api. Use :method getStream: instead (remove the underscore).', DeprecationWarning) + return self.getStream(filename) def _getStreamAs(self, streamID, stringStream : bool = True, overrideClass = None, preserveNone : bool = True): """ @@ -124,9 +124,9 @@ def _getStreamAs(self, streamID, stringStream : bool = True, overrideClass = Non If this is changed to False, then the value will be used regardless. """ if stringStream: - value = self._getStringStream(streamID) + value = self.getStringStream(streamID) else: - value = self._getStream(streamID) + value = self.getStream(streamID) # Check if we should be overriding the data type for this instance. if overrideClass is not None: @@ -146,9 +146,9 @@ def _getStringStream(self, filename) -> Optional[str]: :raises ReferenceError: The associated MSGFile instance has been garbage collected. """ - if (msg := self.__msg()) is None: - raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') - return msg._getStringStream([self.__dir, filename]) + import warnings + warnings.warn(':method _getStringStream: has been deprecated and moved to the public api. Use :method getStringStream: instead (remove the underscore).', DeprecationWarning) + return self.getStringStream(filename) def _getTypedAs(self, _id : str, overrideClass = None, preserveNone : bool = True): """ @@ -320,6 +320,35 @@ def existsTypedProperty(self, id, _type = None) -> bool: raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') return msg.existsTypedProperty(id, self.__dir, _type, True, self.__props) + def getStream(self, filename) -> Optional[bytes]: + """ + Gets a binary representation of the requested filename. + + This should ALWAYS return a bytes object if it was found, otherwise + returns None. + + :raises ReferenceError: The associated MSGFile instance has been garbage + collected. + """ + if (msg := self.__msg()) is None: + raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') + return msg.getStream([self.__dir, filename]) + + def getStringStream(self, filename) -> Optional[str]: + """ + Gets a string representation of the requested filename. + Checks for both ASCII and Unicode representations and returns + a value if possible. If there are both ASCII and Unicode + versions, then :param prefer: specifies which will be + returned. + + :raises ReferenceError: The associated MSGFile instance has been garbage + collected. + """ + if (msg := self.__msg()) is None: + raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') + return msg.getStringStream([self.__dir, filename]) + @abc.abstractmethod def getFilename(self, **kwargs) -> str: """ @@ -366,7 +395,7 @@ def attachmentEncoding(self) -> Optional[bytes]: b'*\\x86H\\x86\\xf7\\x14\\x03\\x0b\\x01' if encoded in MacBinary format, otherwise it is unset. """ - return self._getStream('__substg1.0_37020102') + return self.getStream('__substg1.0_37020102') @functools.cached_property def additionalInformation(self) -> Optional[str]: @@ -377,14 +406,14 @@ def additionalInformation(self) -> Optional[str]: four-letter Macintosh file creator code and ":TYPE" is a four-letter Macintosh type code. """ - return self._getStringStream('__substg1.0_370F') + return self.getStringStream('__substg1.0_370F') @functools.cached_property def cid(self) -> Optional[str]: """ Returns the Content ID of the attachment, if it exists. """ - return self._getStringStream('__substg1.0_3712') + return self.getStringStream('__substg1.0_3712') @cached_property def clsid(self) -> str: @@ -454,7 +483,7 @@ def displayName(self) -> Optional[str]: """ Returns the display name of the folder. """ - return self._getStringStream('__substg1.0_3001') + return self.getStringStream('__substg1.0_3001') @functools.cached_property def exceptionReplaceTime(self) -> Optional[datetime.datetime]: @@ -471,7 +500,7 @@ def extension(self) -> Optional[str]: """ The reported extension for the file. """ - return self._getStringStream('__substg1.0_3703') + return self.getStringStream('__substg1.0_3703') @functools.cached_property def hidden(self) -> bool: @@ -492,21 +521,21 @@ def longFilename(self) -> Optional[str]: """ Returns the long file name of the attachment, if it exists. """ - return self._getStringStream('__substg1.0_3707') + return self.getStringStream('__substg1.0_3707') @functools.cached_property def longPathname(self) -> Optional[str]: """ The fully qualified path and file name with extension. """ - return self._getStringStream('__substg1.0_370D') + return self.getStringStream('__substg1.0_370D') @functools.cached_property def mimetype(self) -> Optional[str]: """ The content-type mime header of the attachment, if specified. """ - return tryGetMimetype(self, self._getStringStream('__substg1.0_370E')) + return tryGetMimetype(self, self.getStringStream('__substg1.0_370E')) @property def msg(self) -> MSGFile: @@ -543,7 +572,7 @@ def payloadClass(self) -> Optional[str]: The class name of an object that can display the contents of the message. """ - return self._getStringStream('__substg1.0_371A') + return self.getStringStream('__substg1.0_371A') @property def props(self) -> PropertiesStore: @@ -566,7 +595,7 @@ def shortFilename(self) -> Optional[str]: """ Returns the short file name of the attachment, if it exists. """ - return self._getStringStream('__substg1.0_3704') + return self.getStringStream('__substg1.0_3704') @property def treePath(self) -> List[weakref.ReferenceType]: diff --git a/extract_msg/attachments/custom_att_handler/outlook_image_dib.py b/extract_msg/attachments/custom_att_handler/outlook_image_dib.py index b51c87c8..37c2c57f 100644 --- a/extract_msg/attachments/custom_att_handler/outlook_image_dib.py +++ b/extract_msg/attachments/custom_att_handler/outlook_image_dib.py @@ -32,17 +32,17 @@ class OutlookImageDIB(CustomAttachmentHandler): def __init__(self, attachment : AttachmentBase): super().__init__(attachment) # First we need to get the mailstream. - stream = attachment._getStream('__substg1.0_3701000D/\x03MailStream') + stream = attachment.getStream('__substg1.0_3701000D/\x03MailStream') if not stream: raise ValueError('MailStream could not be found.') if len(stream) != 12: raise ValueError('MailStream is the wrong length.') # Next get the bitmap data. - self.__data = attachment._getStream('__substg1.0_3701000D/CONTENTS') + self.__data = attachment.getStream('__substg1.0_3701000D/CONTENTS') if not self.__data: raise ValueError('Bitmap data could not be read for Outlook signature.') # Get the OLE data. - oleStream = attachment._getStream('__substg1.0_3701000D/\x01Ole') + oleStream = attachment.getStream('__substg1.0_3701000D/\x01Ole') if not oleStream: raise ValueError('OLE stream could not be found.') diff --git a/extract_msg/enums.py b/extract_msg/enums.py index 0d81454f..d117dad1 100644 --- a/extract_msg/enums.py +++ b/extract_msg/enums.py @@ -1811,17 +1811,3 @@ def __getitem__(self, name): return self.__new.__getitem__(self.__nameConv.get(name, name)) - - - -# Deprecated Enums. These are not exported but may be directly accessed. -AttachErrorBehavior = _EnumDeprecator( - 'AttachErrorBehavior', - ErrorBehavior, - { - 'BROKEN': 'ATTACH_SUPPRESS_ALL', - 'NOT_IMPLEMENTED': 'ATTACH_NOT_IMPLEMENTED', - }, - { - 2: 3 - }) diff --git a/extract_msg/msg_classes/calendar_base.py b/extract_msg/msg_classes/calendar_base.py index 08141c84..f9a9bd73 100644 --- a/extract_msg/msg_classes/calendar_base.py +++ b/extract_msg/msg_classes/calendar_base.py @@ -417,7 +417,7 @@ def organizer(self) -> Optional[str]: """ The meeting organizer. """ - return self._getStringStream('__substg1.0_0042') + return self.getStringStream('__substg1.0_0042') @functools.cached_property def ownerAppointmentID(self) -> Optional[int]: diff --git a/extract_msg/msg_classes/contact.py b/extract_msg/msg_classes/contact.py index d9cc40c1..e4429018 100644 --- a/extract_msg/msg_classes/contact.py +++ b/extract_msg/msg_classes/contact.py @@ -30,7 +30,7 @@ def account(self) -> Optional[str]: """ The account name of the contact. """ - return self._getStringStream('__substg1.0_3A00') + return self.getStringStream('__substg1.0_3A00') @functools.cached_property def addressBookProviderArrayType(self) -> Optional[ElectronicAddressProperties]: @@ -54,14 +54,14 @@ def assistant(self) -> Optional[str]: """ The name of the contact's assistant. """ - return self._getStringStream('__substg1.0_3A30') + return self.getStringStream('__substg1.0_3A30') @functools.cached_property def assistantTelephoneNumber(self) -> Optional[str]: """ Contains the telephone number of the contact's administrative assistant. """ - return self._getStringStream('__substg1.0_3A2E') + return self.getStringStream('__substg1.0_3A2E') @functools.cached_property def autoLog(self) -> bool: @@ -184,7 +184,7 @@ def businessFaxNumber(self) -> Optional[str]: """ Contains the number of the contact's business fax. """ - return self._getStringStream('__substg1.0_3A24') + return self.getStringStream('__substg1.0_3A24') @functools.cached_property def businessFaxOriginalDisplayName(self) -> Optional[str]: @@ -205,7 +205,7 @@ def businessTelephoneNumber(self) -> Optional[str]: """ Contains the number of the contact's business telephone. """ - return self._getStringStream('__substg1.0_3A08') + return self.getStringStream('__substg1.0_3A08') @functools.cached_property def businessTelephone2Number(self) -> Optional[Union[str, List[str]]]: @@ -219,21 +219,21 @@ def businessHomePage(self) -> Optional[str]: """ Contains the url of the homepage of the contact's business. """ - return self._getStringStream('__substg1.0_3A51') + return self.getStringStream('__substg1.0_3A51') @functools.cached_property def callbackTelephoneNumber(self) -> Optional[str]: """ Contains the contact's callback telephone number. """ - return self._getStringStream('__substg1.0_3A02') + return self.getStringStream('__substg1.0_3A02') @functools.cached_property def carTelephoneNumber(self) -> Optional[str]: """ Contains the number of the contact's car telephone. """ - return self._getStringStream('__substg1.0_3A1E') + return self.getStringStream('__substg1.0_3A1E') @functools.cached_property def childrensNames(self) -> Optional[List[str]]: @@ -247,21 +247,21 @@ def companyMainTelephoneNumber(self) -> Optional[str]: """ Contains the number of the main telephone of the contact's company. """ - return self._getStringStream('__substg1.0_3A57') + return self.getStringStream('__substg1.0_3A57') @functools.cached_property def companyName(self) -> Optional[str]: """ The name of the company the contact works at. """ - return self._getStringStream('__substg1.0_3A16') + return self.getStringStream('__substg1.0_3A16') @functools.cached_property def computerNetworkName(self) -> Optional[str]: """ The name of the network to wwhich the contact's computer is connected. """ - return self._getStringStream('__substg1.0_3A49') + return self.getStringStream('__substg1.0_3A49') @functools.cached_property def contactCharacterSet(self) -> Optional[int]: @@ -359,28 +359,28 @@ def customerID(self) -> Optional[str]: """ The contact's customer ID number. """ - return self._getStringStream('__substg1.0_3A4A') + return self.getStringStream('__substg1.0_3A4A') @functools.cached_property def departmentName(self) -> Optional[str]: """ The name of the department the contact works in. """ - return self._getStringStream('__substg1.0_3A18') + return self.getStringStream('__substg1.0_3A18') @functools.cached_property def displayName(self) -> Optional[str]: """ The full name of the contact. """ - return self._getStringStream('__substg1.0_3001') + return self.getStringStream('__substg1.0_3001') @functools.cached_property def displayNamePrefix(self) -> Optional[str]: """ The contact's honorific title. """ - return self._getStringStream('__substg1.0_3A45') + return self.getStringStream('__substg1.0_3A45') @functools.cached_property def email1(self) -> Optional[Dict]: @@ -590,7 +590,7 @@ def ftpSite(self) -> Optional[str]: """ The contact's File Transfer Protocol url. """ - return self._getStringStream('__substg1.0_3A4C') + return self.getStringStream('__substg1.0_3A4C') @functools.cached_property def gender(self) -> Optional[Gender]: @@ -605,21 +605,21 @@ def generation(self) -> Optional[str]: A generational abbreviation that follows the full name of the contact. """ - return self._getStringStream('__substg1.0_3A05') + return self.getStringStream('__substg1.0_3A05') @functools.cached_property def givenName(self) -> Optional[str]: """ The first name of the contact. """ - return self._getStringStream('__substg1.0_3A06') + return self.getStringStream('__substg1.0_3A06') @functools.cached_property def governmentIDNumber(self) -> Optional[str]: """ The contact's government ID number. """ - return self._getStringStream('__substg1.0_3A07') + return self.getStringStream('__substg1.0_3A07') @functools.cached_property def hasPicture(self) -> bool: @@ -725,7 +725,7 @@ def hobbies(self) -> Optional[str]: """ The hobies of the contact. """ - return self._getStringStream('__substg1.0_3A43') + return self.getStringStream('__substg1.0_3A43') @functools.cached_property def homeAddress(self) -> Optional[str]: @@ -739,7 +739,7 @@ def homeAddressCountry(self) -> Optional[str]: """ The country portion of the contact's home address. """ - return self._getStringStream('__substg1.0_3A5A') + return self.getStringStream('__substg1.0_3A5A') @functools.cached_property def homeAddressCountryCode(self) -> Optional[str]: @@ -753,35 +753,35 @@ def homeAddressLocality(self) -> Optional[str]: """ The locality or city portion of the contact's home address. """ - return self._getStringStream('__substg1.0_3A59') + return self.getStringStream('__substg1.0_3A59') @functools.cached_property def homeAddressPostalCode(self) -> Optional[str]: """ The postal code portion of the contact's home address. """ - return self._getStringStream('__substg1.0_3A5B') + return self.getStringStream('__substg1.0_3A5B') @functools.cached_property def homeAddressPostOfficeBox(self) -> Optional[str]: """ The number or identifier of the contact's home post office box. """ - return self._getStringStream('__substg1.0_3A5E') + return self.getStringStream('__substg1.0_3A5E') @functools.cached_property def homeAddressStateOrProvince(self) -> Optional[str]: """ The state or province portion of the contact's home address. """ - return self._getStringStream('__substg1.0_3A5C') + return self.getStringStream('__substg1.0_3A5C') @functools.cached_property def homeAddressStreet(self) -> Optional[str]: """ The street portion of the contact's home address. """ - return self._getStringStream('__substg1.0_3A5D') + return self.getStringStream('__substg1.0_3A5D') @functools.cached_property def homeFax(self) -> Optional[Dict]: @@ -821,7 +821,7 @@ def homeFaxNumber(self) -> Optional[str]: """ Contains the number of the contact's home fax. """ - return self._getStringStream('__substg1.0_3A25') + return self.getStringStream('__substg1.0_3A25') @functools.cached_property def homeFaxOriginalDisplayName(self) -> Optional[str]: @@ -842,7 +842,7 @@ def homeTelephoneNumber(self) -> Optional[str]: """ The number of the contact's home telephone. """ - return self._getStringStream('__substg1.0_3A09') + return self.getStringStream('__substg1.0_3A09') @functools.cached_property def homeTelephone2Number(self) -> Optional[Union[str, List[str]]]: @@ -856,7 +856,7 @@ def initials(self) -> Optional[str]: """ The initials of the contact. """ - return self._getStringStream('__substg1.0_3A0A') + return self.getStringStream('__substg1.0_3A0A') @functools.cached_property def instantMessagingAddress(self) -> Optional[str]: @@ -878,28 +878,28 @@ def isdnNumber(self) -> Optional[str]: The Integrated Services Digital Network (ISDN) telephone number of the contact. """ - return self._getStringStream('__substg1.0_3A2D') + return self.getStringStream('__substg1.0_3A2D') @functools.cached_property def jobTitle(self) -> Optional[str]: """ The job title of the contact. """ - return self._getStringStream('__substg1.0_3A17') + return self.getStringStream('__substg1.0_3A17') @functools.cached_property def language(self) -> Optional[str]: """ The language that the contact uses. """ - return self._getStringStream('__substg1.0_3A0C') + return self.getStringStream('__substg1.0_3A0C') @functools.cached_property def lastModifiedBy(self) -> Optional[str]: """ The name of the last user to modify the contact file. """ - return self._getStringStream('__substg1.0_3FFA') + return self.getStringStream('__substg1.0_3FFA') @functools.cached_property def location(self) -> Optional[str]: @@ -907,21 +907,21 @@ def location(self) -> Optional[str]: The location of the contact. For example, this could be the building or office number of the contact. """ - return self._getStringStream('__substg1.0_3A0D') + return self.getStringStream('__substg1.0_3A0D') @functools.cached_property def mailAddress(self) -> Optional[str]: """ The complete mail address of the contact. """ - return self._getStringStream('__substg1.0_3A15') + return self.getStringStream('__substg1.0_3A15') @functools.cached_property def mailAddressCountry(self) -> Optional[str]: """ The country portion of the contact's mail address. """ - return self._getStringStream('__substg1.0_3A26') + return self.getStringStream('__substg1.0_3A26') @functools.cached_property def mailAddressCountryCode(self) -> Optional[str]: @@ -935,70 +935,70 @@ def mailAddressLocality(self) -> Optional[str]: """ The locality or city portion of the contact's mail address. """ - return self._getStringStream('__substg1.0_3A27') + return self.getStringStream('__substg1.0_3A27') @functools.cached_property def mailAddressPostalCode(self) -> Optional[str]: """ The postal code portion of the contact's mail address. """ - return self._getStringStream('__substg1.0_3A2A') + return self.getStringStream('__substg1.0_3A2A') @functools.cached_property def mailAddressPostOfficeBox(self) -> Optional[str]: """ The number or identifier of the contact's mail post office box. """ - return self._getStringStream('__substg1.0_3A2B') + return self.getStringStream('__substg1.0_3A2B') @functools.cached_property def mailAddressStateOrProvince(self) -> Optional[str]: """ The state or province portion of the contact's mail address. """ - return self._getStringStream('__substg1.0_3A28') + return self.getStringStream('__substg1.0_3A28') @functools.cached_property def mailAddressStreet(self) -> Optional[str]: """ The street portion of the contact's mail address. """ - return self._getStringStream('__substg1.0_3A29') + return self.getStringStream('__substg1.0_3A29') @functools.cached_property def managerName(self) -> Optional[str]: """ The name of the contact's manager. """ - return self._getStringStream('__substg1.0_3A4E') + return self.getStringStream('__substg1.0_3A4E') @functools.cached_property def middleName(self) -> Optional[str]: """ The middle name(s) of the contact. """ - return self._getStringStream('__substg1.0_3A44') + return self.getStringStream('__substg1.0_3A44') @functools.cached_property def mobileTelephoneNumber(self) -> Optional[str]: """ The mobile telephone number of the contact. """ - return self._getStringStream('__substg1.0_3A1C') + return self.getStringStream('__substg1.0_3A1C') @functools.cached_property def nickname(self) -> Optional[str]: """ The nickname of the contanct. """ - return self._getStringStream('__substg1.0_3A4F') + return self.getStringStream('__substg1.0_3A4F') @functools.cached_property def officeLocation(self) -> Optional[str]: """ The location of the office that the contact works in. """ - return self._getStringStream('__substg1.0_3A19') + return self.getStringStream('__substg1.0_3A19') @functools.cached_property def organizationalIDNumber(self) -> Optional[str]: @@ -1006,7 +1006,7 @@ def organizationalIDNumber(self) -> Optional[str]: The organizational ID number for the contact, such as an employee ID number. """ - return self._getStringStream('__substg1.0_3A10') + return self.getStringStream('__substg1.0_3A10') @functools.cached_property def oscSyncEnabled(self) -> bool: @@ -1028,7 +1028,7 @@ def otherAddressCountry(self) -> Optional[str]: """ The country portion of the contact's other address. """ - return self._getStringStream('__substg1.0_3A60') + return self.getStringStream('__substg1.0_3A60') @functools.cached_property def otherAddressCountryCode(self) -> Optional[str]: @@ -1042,56 +1042,56 @@ def otherAddressLocality(self) -> Optional[str]: """ The locality or city portion of the contact's other address. """ - return self._getStringStream('__substg1.0_3A5F') + return self.getStringStream('__substg1.0_3A5F') @functools.cached_property def otherAddressPostalCode(self) -> Optional[str]: """ The postal code portion of the contact's other address. """ - return self._getStringStream('__substg1.0_3A61') + return self.getStringStream('__substg1.0_3A61') @functools.cached_property def otherAddressPostOfficeBox(self) -> Optional[str]: """ The number or identifier of the contact's other post office box. """ - return self._getStringStream('__substg1.0_3A64') + return self.getStringStream('__substg1.0_3A64') @functools.cached_property def otherAddressStateOrProvince(self) -> Optional[str]: """ The state or province portion of the contact's other address. """ - return self._getStringStream('__substg1.0_3A62') + return self.getStringStream('__substg1.0_3A62') @functools.cached_property def otherAddressStreet(self) -> Optional[str]: """ The street portion of the contact's other address. """ - return self._getStringStream('__substg1.0_3A63') + return self.getStringStream('__substg1.0_3A63') @functools.cached_property def otherTelephoneNumber(self) -> Optional[str]: """ Contains the number of the contact's other telephone. """ - return self._getStringStream('__substg1.0_3A1F') + return self.getStringStream('__substg1.0_3A1F') @functools.cached_property def pagerTelephoneNumber(self) -> Optional[str]: """ The contact's pager telephone number. """ - return self._getStringStream('__substg1.0_3A21') + return self.getStringStream('__substg1.0_3A21') @functools.cached_property def personalHomePage(self) -> Optional[str]: """ The contact's personal web page UL. """ - return self._getStringStream('__substg1.0_3A50') + return self.getStringStream('__substg1.0_3A50') @functools.cached_property def phoneticCompanyName(self) -> Optional[str]: @@ -1160,7 +1160,7 @@ def primaryFaxNumber(self) -> Optional[str]: """ Contains the number of the contact's primary fax. """ - return self._getStringStream('__substg1.0_3A23') + return self.getStringStream('__substg1.0_3A23') @functools.cached_property def primaryFaxOriginalDisplayName(self) -> Optional[str]: @@ -1181,21 +1181,21 @@ def primaryTelephoneNumber(self) -> Optional[str]: """ Contains the number of the contact's primary telephone. """ - return self._getStringStream('__substg1.0_3A1A') + return self.getStringStream('__substg1.0_3A1A') @functools.cached_property def profession(self) -> Optional[str]: """ The profession of the contact. """ - return self._getStringStream('__substg1.0_3A46') + return self.getStringStream('__substg1.0_3A46') @functools.cached_property def radioTelephoneNumber(self) -> Optional[str]: """ Contains the number of the contact's radio telephone. """ - return self._getStringStream('__substg1.0_3A1D') + return self.getStringStream('__substg1.0_3A1D') @functools.cached_property def referenceEntryID(self) -> Optional[EntryID]: @@ -1211,21 +1211,21 @@ def referredByName(self) -> Optional[str]: """ The name of the person who referred this contact to the user. """ - return self._getStringStream('__substg1.0_3A47') + return self.getStringStream('__substg1.0_3A47') @functools.cached_property def spouseName(self) -> Optional[str]: """ The name of the contact's spouse. """ - return self._getStringStream('__substg1.0_3A48') + return self.getStringStream('__substg1.0_3A48') @functools.cached_property def surname(self) -> Optional[str]: """ The surname of the contact. """ - return self._getStringStream('__substg1.0_3A11') + return self.getStringStream('__substg1.0_3A11') @functools.cached_property def tddTelephoneNumber(self) -> Optional[str]: @@ -1233,7 +1233,7 @@ def tddTelephoneNumber(self) -> Optional[str]: The telephone number for the contact's text telephone (TTY) or telecommunication device for the deaf (TDD). """ - return self._getStringStream('__substg1.0_3A4B') + return self.getStringStream('__substg1.0_3A4B') @functools.cached_property def telexNumber(self) -> Optional[Union[str, List[str]]]: diff --git a/extract_msg/msg_classes/message_base.py b/extract_msg/msg_classes/message_base.py index f9dad190..ab4f305e 100644 --- a/extract_msg/msg_classes/message_base.py +++ b/extract_msg/msg_classes/message_base.py @@ -76,15 +76,6 @@ def __init__(self, path, **kwargs): internally or they will not be caught. The original deencapsulation method will not run if this is set. """ - if 'ignoreRtfDeErrors' in kwargs: - import warnings - warnings.warn(':param ignoreRtfDeErrors: is deprecated. Use :param ErrorBehavior: instead.', DeprecationWarning) - - if kwargs.get('ignoreRtfDeErrors', False): - errorBehavior = kwargs.get('errorBehavior', ErrorBehavior.THROW) - errorBehavior |= ErrorBehavior.RTFDE - kwargs['errorBehavior'] = errorBehavior - super().__init__(path, **kwargs) # The rest needs to be in a try-except block to ensure the file closes # if an error occurs. @@ -119,15 +110,14 @@ def __init__(self, path, **kwargs): pass raise - def _genRecipient(self, recipientType : str, recipientInt : RecipientType) -> Optional[str]: + def _genRecipient(self, recipientStr : str, recipientType : RecipientType) -> Optional[str]: """ Returns the specified recipient field. """ - recipientInt = RecipientType(recipientInt) value = None # Check header first. if self.headerInit: - value = self.header[recipientType] + value = self.header[recipientStr] if value: value = decodeRfc2047(value) value = value.replace(',', self.__recipientSeparator) @@ -137,10 +127,10 @@ def _genRecipient(self, recipientType : str, recipientInt : RecipientType) -> Op if not value: # Check if the header has initialized. if self.headerInit: - logger.info(f'Header found, but "{recipientType}" is not included. Will be generated from other streams.') + logger.info(f'Header found, but "{recipientStr}" is not included. Will be generated from other streams.') # Get a list of the recipients of the specified type. - foundRecipients = tuple(recipient.formatted for recipient in self.recipients if recipient.type == recipientInt) + foundRecipients = tuple(recipient.formatted for recipient in self.recipients if recipient.type is recipientType) # If we found recipients, join them with the recipient separator # and a space. @@ -932,7 +922,7 @@ def body(self) -> Optional[str]: Returns the message body, if it exists. """ # If the body exists but is empty, that means it should be returned. - if (body := self._getStringStream('__substg1.0_1000')) is not None: + if (body := self.getStringStream('__substg1.0_1000')) is not None: pass elif self.rtfBody: # If the body doesn't exist, see if we can get it from the RTF @@ -959,7 +949,7 @@ def compressedRtf(self) -> Optional[bytes]: """ Returns the compressed RTF stream, if it exists. """ - return self._getStream('__substg1.0_10090102') + return self.getStream('__substg1.0_10090102') @property def crlf(self) -> str: @@ -1143,14 +1133,14 @@ def headerText(self) -> Optional[str]: """ The raw text of the header stream, if it exists. """ - return self._getStringStream('__substg1.0_007D') + return self.getStringStream('__substg1.0_007D') @functools.cached_property def htmlBody(self) -> Optional[bytes]: """ Returns the html body, if it exists. """ - if (htmlBody := self._getStream('__substg1.0_10130102')) is not None: + if (htmlBody := self.getStream('__substg1.0_10130102')) is not None: pass elif self.rtfBody: logger.info('HTML body was not found, attempting to generate from RTF.') @@ -1213,7 +1203,7 @@ def inReplyTo(self) -> Optional[str]: """ Returns the message id that this message is in reply to. """ - return self._getStringStream('__substg1.0_1042') + return self.getStringStream('__substg1.0_1042') @functools.cached_property def isRead(self) -> bool: @@ -1246,7 +1236,7 @@ def messageId(self) -> Optional[str]: if self.headerInit: logger.info('Header found, but "Message-Id" is not included. Will be generated from other streams.') - return self._getStringStream('__substg1.0_1035') + return self.getStringStream('__substg1.0_1035') @functools.cached_property def parsedDate(self): @@ -1328,8 +1318,8 @@ def sender(self) -> Optional[str]: return decodeRfc2047(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') + 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 @@ -1347,7 +1337,7 @@ def subject(self) -> Optional[str]: """ Returns the message subject, if it exists. """ - return self._getStringStream('__substg1.0_0037') + return self.getStringStream('__substg1.0_0037') @functools.cached_property def to(self) -> Optional[str]: diff --git a/extract_msg/msg_classes/message_signed_base.py b/extract_msg/msg_classes/message_signed_base.py index 8c1d32e2..0a2b01f4 100644 --- a/extract_msg/msg_classes/message_signed_base.py +++ b/extract_msg/msg_classes/message_signed_base.py @@ -77,7 +77,7 @@ def body(self) -> Optional[str]: """ Returns the message body, if it exists. """ - if (body := self._getStringStream('__substg1.0_1000')) is not None: + if (body := self.getStringStream('__substg1.0_1000')) is not None: pass elif self.signedBody: body = self.signedBody @@ -99,7 +99,7 @@ def htmlBody(self) -> Optional[bytes]: """ Returns the html body, if it exists. """ - if (htmlBody := self._getStream('__substg1.0_10130102')) is not None: + if (htmlBody := self.getStream('__substg1.0_10130102')) is not None: pass elif self.signedHtmlBody: htmlBody = self.signedHtmlBody diff --git a/extract_msg/msg_classes/msg.py b/extract_msg/msg_classes/msg.py index 64b0a87e..4f497a18 100644 --- a/extract_msg/msg_classes/msg.py +++ b/extract_msg/msg_classes/msg.py @@ -112,14 +112,6 @@ def __init__(self, path, **kwargs): self.__attachmentsDelayed = kwargs.get('delayAttachments', False) self.__attachmentsReady = False self.__errorBehavior = ErrorBehavior(kwargs.get('errorBehavior', ErrorBehavior.THROW)) - if self.__errorBehavior is None: - if 'attachmentErrorBehavior' in kwargs: - import warnings - warnings.warn(':param attachmentErrorsBehavior: is deprecated. Use :param ErrorBehavior: instead.', DeprecationWarning) - - # Get the error behavior and call the old class to convert it if - # necessary. - self.__errorBehavior = AttachErrorBehavior(kwargs['attachmentErrorBehavior']) if overrideEncoding is not None: codecs.lookup(overrideEncoding) @@ -179,7 +171,7 @@ def __init__(self, path, **kwargs): self.__prefixList = prefixl self.__prefixLen = len(prefixl) if prefix and not filename: - filename = self._getStringStream(prefixl[:-1] + ['__substg1.0_3001'], prefix = False) + filename = self.getStringStream(prefixl[:-1] + ['__substg1.0_3001'], prefix = False) if filename: self.filename = filename elif hasLen(path): @@ -279,13 +271,9 @@ def _getStream(self, filename, prefix : bool = True) -> Optional[bytes]: This should ALWAYS return a bytes object if it was found, otherwise returns None. """ - filename = self.fixPath(filename, prefix) - if self.exists(filename, False): - with self.__ole.openstream(filename) as stream: - return stream.read() or b'' - else: - logger.info(f'Stream "{filename}" was requested but could not be found. Returning `None`.') - return None + import warnings + warnings.warn(':method _getStream: has been deprecated and moved to the public api. Use :method getStream: instead (remove the underscore).', DeprecationWarning) + return self.getStream(filename, prefix) def _getStreamAs(self, streamID, stringStream : bool = True, overrideClass = None, preserveNone : bool = True): """ @@ -303,9 +291,9 @@ def _getStreamAs(self, streamID, stringStream : bool = True, overrideClass = Non If this is changed to False, then the value will be used regardless. """ if stringStream: - value = self._getStringStream(streamID) + value = self.getStringStream(streamID) else: - value = self._getStream(streamID) + value = self.getStream(streamID) # Check if we should be overriding the data type for this instance. if overrideClass is not None: @@ -325,12 +313,9 @@ def _getStringStream(self, filename, prefix : bool = True) -> Optional[str]: This should ALWAYS return a string if it was found, otherwise returns None. """ - filename = self.fixPath(filename, prefix) - if self.areStringsUnicode: - return windowsUnicode(self._getStream(filename + '001F', prefix = False)) - else: - tmp = self._getStream(filename + '001E', prefix = False) - return None if tmp is None else tmp.decode(self.stringEncoding) + import warnings + warnings.warn(':method _getStringStream: has been deprecated and moved to the public api. Use :method getStringStream: instead (remove the underscore).', DeprecationWarning) + return self.getStringStream(filename, prefix) def _getTypedAs(self, _id : str, overrideClass = None, preserveNone : bool = True): """ @@ -412,7 +397,7 @@ def _getTypedStream(self, filename, prefix : bool = True, _type = None): filename = self.fixPath(filename, prefix) for x in (filename + _type,) if _type is not None else self.slistDir(): if x.startswith(filename) and x.find('-') == -1: - contents = self._getStream(x, False) + contents = self.getStream(x, False) if contents is None: continue if len(contents) == 0: @@ -436,7 +421,7 @@ def _getTypedStream(self, filename, prefix : bool = True, _type = None): if self.exists(x + '-00000000', False): for y in range(streams): if self.exists(x + '-' + properHex(y, 8), False): - extras.append(self._getStream(x + '-' + properHex(y, 8), False)) + extras.append(self.getStream(x + '-' + properHex(y, 8), False)) elif _type in ('1002', '1003', '1004', '1005', '1007', '1014', '1040', '1048'): extras = divide(contents, (2 if _type in constants.MULTIPLE_2_BYTES else 4 if _type in constants.MULTIPLE_4_BYTES else 8 if _type in constants.MULTIPLE_8_BYTES else 16)) contents = streams @@ -470,7 +455,7 @@ def debug(self) -> None: for dir_ in self.listDir(): if dir_[-1].endswith('001E') or dir_[-1].endswith('001F'): print('Directory: ' + str(dir_[:-1])) - print(f'Contents: {self._getStream(dir_)}') + print(f'Contents: {self.getStream(dir_)}') def exists(self, inp, prefix : bool = True) -> bool: """ @@ -559,6 +544,39 @@ def fixPath(self, inp, prefix : bool = True) -> str: inp = self.__prefix + inp return inp + def getStream(self, filename, prefix : bool = True) -> Optional[bytes]: + """ + Gets a binary representation of the requested filename. + + This should ALWAYS return a bytes object if it was found, otherwise + returns None. + """ + filename = self.fixPath(filename, prefix) + if self.exists(filename, False): + with self.__ole.openstream(filename) as stream: + return stream.read() or b'' + else: + logger.info(f'Stream "{filename}" was requested but could not be found. Returning `None`.') + return None + + def getStringStream(self, filename, prefix : bool = True) -> Optional[str]: + """ + Gets a string representation of the requested filename. + + Rather than the full filename, you should only feed this function the + filename sans the type. So if the full name is "__substg1.0_001A001F", + the filename this function should receive should be "__substg1.0_001A". + + This should ALWAYS return a string if it was found, otherwise returns + None. + """ + filename = self.fixPath(filename, prefix) + if self.areStringsUnicode: + return windowsUnicode(self.getStream(filename + '001F', prefix = False)) + else: + tmp = self.getStream(filename + '001E', prefix = False) + return None if tmp is None else tmp.decode(self.stringEncoding) + def listDir(self, streams : bool = True, storages : bool = False, includePrefix : bool = True) -> List[List[str]]: """ Replacement for OleFileIO.listdir that runs at the current prefix @@ -636,7 +654,7 @@ def saveRaw(self, path) -> None: # Save contents of directory. with zfile.open(sysdir + '/' + filename, 'w') as f: - data = self._getStream(dir_) + data = self.getStream(dir_) # Specifically check for None. If this is bytes we still want to do this line. # There was actually this weird issue where for some reason data would be bytes # but then also simultaneously register as None? @@ -701,7 +719,7 @@ def classType(self) -> Optional[str]: """ The class type of the MSG file. """ - return self._getStringStream('__substg1.0_001A') + return self.getStringStream('__substg1.0_001A') @functools.cached_property def commonEnd(self) -> Optional[datetime.datetime]: @@ -861,7 +879,7 @@ def props(self) -> PropertiesStore: """ Returns the Properties instance used by the MSGFile instance. """ - if not (stream := self._getStream('__properties_version1.0')): + if not (stream := self.getStream('__properties_version1.0')): if ErrorBehavior.STANDARDS_VIOLATION in self.__errorBehavior: logger.error('File does not contain a property stream.') else: diff --git a/extract_msg/msg_classes/post.py b/extract_msg/msg_classes/post.py index 8167c260..aaf673f8 100644 --- a/extract_msg/msg_classes/post.py +++ b/extract_msg/msg_classes/post.py @@ -37,7 +37,7 @@ def conversation(self) -> Optional[str]: """ The name of the conversation being posted to. """ - return self._getStringStream('__substg1.0_0070') + return self.getStringStream('__substg1.0_0070') @property def headerFormatProperties(self) -> constants.HEADER_FORMAT_TYPE: diff --git a/extract_msg/ole_writer.py b/extract_msg/ole_writer.py index 7158d865..579d0826 100644 --- a/extract_msg/ole_writer.py +++ b/extract_msg/ole_writer.py @@ -744,7 +744,7 @@ def fromMsg(self, msg : MSGFile) -> None: for x in entries: entry = msg._getOleEntry(x) - data = msg._getStream(x) if entry.entry_type == DirectoryEntryType.STREAM else None + data = msg.getStream(x) if entry.entry_type == DirectoryEntryType.STREAM else None # THe properties stream on embedded messages actualy needs to be # transformed a little (*why* it is like that is a mystery to me). # Basically we just need to add a "reserved" section to it in a @@ -767,7 +767,7 @@ def fromMsg(self, msg : MSGFile) -> None: # Create our generator. gen = (x for x in msg._oleListDir() if len(x) > 1 and x[0] == '__nameid_version1.0') for x in gen: - self.addOleEntry(x, msg._getOleEntry(x, prefix = False), msg._getStream(x, prefix = False)) + self.addOleEntry(x, msg._getOleEntry(x, prefix = False), msg.getStream(x, prefix = False)) def fromOleFile(self, ole : OleFileIO, rootPath = []) -> None: """ diff --git a/extract_msg/properties/named.py b/extract_msg/properties/named.py index 34c32592..302b4114 100644 --- a/extract_msg/properties/named.py +++ b/extract_msg/properties/named.py @@ -47,11 +47,11 @@ class Named: def __init__(self, msg : MSGFile): self.__msg = makeWeakRef(msg) # Get the basic streams. If all are emtpy, then nothing to do. - guidStream = self._getStream('__substg1.0_00020102') or self._getStream('__substg1.0_00020102', False) - entryStream = self._getStream('__substg1.0_00030102') or self._getStream('__substg1.0_00030102', False) + guidStream = self.getStream('__substg1.0_00020102', False) + entryStream = self.getStream('__substg1.0_00030102', False) self.guidStream = guidStream self.entryStream = entryStream - self.namesStream = self._getStream('__substg1.0_00040102') or self._getStream('__substg1.0_00040102', False) + self.namesStream = self.getStream('__substg1.0_00040102', False) self.__propertiesDict : Dict[Tuple[str, str], NamedPropertyBase]= {} self.__properties : List[NamedPropertyBase] = [] @@ -139,9 +139,9 @@ def _getStream(self, filename, prefix = True) -> Optional[bytes]: :raises ReferenceError: The associated MSGFile instance has been garbage collected. """ - if (msg := self.__msg()) is None: - raise ReferenceError('The msg file for this Named instance has been garbage collected.') - return msg._getStream([self.__dir, filename], prefix = prefix) + import warnings + warnings.warn(':method _getStream: has been deprecated and moved to the public api. Use :method getStream: instead (remove the underscore).', DeprecationWarning) + return self.getStream(filename, prefix) def _getStringStream(self, filename, prefix = True) -> Optional[str]: """ @@ -157,9 +157,9 @@ def _getStringStream(self, filename, prefix = True) -> Optional[str]: :raises ReferenceError: The associated MSGFile instance has been garbage collected. """ - if (msg := self.__msg()) is None: - raise ReferenceError('The msg file for this Named instance has been garbage collected.') - return msg._getStringStream([self.__dir, filename], prefix = prefix) + import warnings + warnings.warn(':method _getStringStream: has been deprecated and moved to the public api. Use :method getStringStream: instead (remove the underscore).', DeprecationWarning) + return self.getStringStream(filename, prefix) def exists(self, filename) -> bool: """ @@ -193,6 +193,41 @@ def get(self, propertyName : Tuple[str, str], default : _T = None) -> Union[Name except KeyError: return default + def getStream(self, filename, prefix = True) -> Optional[bytes]: + """ + Gets a binary representation of the requested filename. + + This should ALWAYS return a bytes object if it was found, otherwise + returns None. + + :raises ReferenceError: The associated MSGFile instance has been garbage + collected. + """ + if (msg := self.__msg()) is None: + raise ReferenceError('The msg file for this Named instance has been garbage collected.') + return msg.getStream([self.__dir, filename], prefix = prefix) + + def getStringStream(self, filename, prefix = True) -> Optional[str]: + """ + Gets a string representation of the requested filename. + + Rather than the full filename, you should only feed this function the + filename sans the type. So if the full name is "__substg1.0_001A001F", + the filename this function should receive should be "__substg1.0_001A". + + This should ALWAYS return a string if it was found, otherwise returns + None. + + :raises ReferenceError: The associated MSGFile instance has been garbage + collected. + """ + if (msg := self.__msg()) is None: + raise ReferenceError('The msg file for this Named instance has been garbage collected.') + return msg.getStringStream([self.__dir, filename], prefix = prefix) + + def items(self) -> Iterable[NamedPropertyBase]: + return self.__propertiesDict.items() + def keys(self) -> Iterable[Tuple[str, str]]: return self.__propertiesDict.keys() @@ -202,7 +237,7 @@ def pprintKeys(self) -> None: """ pprint.pprint(sorted(self.__propertiesDict.keys())) - def values(self): + def values(self) -> Iterable[Tuple[Tuple[str, str], NamedPropertyBase]]: return self.__propertiesDict.values() @property diff --git a/extract_msg/properties/properties_store.py b/extract_msg/properties/properties_store.py index a5f4623a..9909f2de 100644 --- a/extract_msg/properties/properties_store.py +++ b/extract_msg/properties/properties_store.py @@ -8,8 +8,7 @@ import logging import pprint -from typing import Any, Dict, Iterator, Optional, TypeVar, Union -from warnings import warn +from typing import Dict, Iterable, Iterator, Optional, Tuple, TypeVar, Union from .. import constants from ..enums import Intelligence, PropertiesType @@ -96,7 +95,7 @@ def __len__(self) -> int: def __repr__(self) -> str: return self.__props.__repr__() - def get(self, name, default : _T = None) -> Union[PropBase, _T]: + def get(self, name : str, default : _T = None) -> Union[PropBase, _T]: """ Retrieve the property of :param name:. Returns the value of :param default: if the property could not be found. @@ -110,17 +109,10 @@ def get(self, name, default : _T = None) -> Union[PropBase, _T]: logger.debug(self.__props) return default - def has_key(self, key) -> bool: - """ - Checks if :param key: is a key in the properties dictionary. - """ - warn('`Properties.has_key` is deprecated. Use the `in` keyword instead.', DeprecationWarning) - return key in self.__props - - def items(self): + def items(self) -> Iterable[PropBase]: return self.__props.items() - def keys(self): + def keys(self) -> Iterable[str]: return self.__props.keys() def pprintKeys(self) -> None: @@ -129,7 +121,7 @@ def pprintKeys(self) -> None: """ pprint.pprint(sorted(tuple(self.__props.keys()))) - def values(self): + def values(self) -> Iterable[Tuple[str, PropBase]]: return self.__props.values() items.__doc__ = dict.items.__doc__ diff --git a/extract_msg/recipient.py b/extract_msg/recipient.py index 66e54ce9..5420b21f 100644 --- a/extract_msg/recipient.py +++ b/extract_msg/recipient.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + __all__ = [ 'Recipient', ] @@ -6,7 +9,7 @@ import functools import logging -from typing import Optional, Tuple, Union +from typing import Optional, Tuple, TYPE_CHECKING, Union from .enums import ErrorBehavior, MeetingRecipientType, PropertiesType, RecipientType from .exceptions import StandardViolationError @@ -16,6 +19,9 @@ from .utils import makeWeakRef, verifyPropertyId, verifyType +if TYPE_CHECKING: + from .msg_classes.msg import MSGFile + logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) @@ -25,7 +31,7 @@ class Recipient: Contains the data of one of the recipients in an MSG file. """ - def __init__(self, _dir, msg): + def __init__(self, _dir, msg : MSGFile): self.__msg = makeWeakRef(msg) # Allows calls to original msg file. self.__dir = _dir if not self.exists('__properties_version1.0'): @@ -33,11 +39,11 @@ def __init__(self, _dir, msg): logger.error('Recipients MUST have a property stream.') else: raise StandardViolationError('Recipients MUST have a property stream.') from None - self.__props = PropertiesStore(self._getStream('__properties_version1.0'), PropertiesType.RECIPIENT) - self.__email = self._getStringStream('__substg1.0_39FE') + self.__props = PropertiesStore(self.getStream('__properties_version1.0'), PropertiesType.RECIPIENT) + self.__email = self.getStringStream('__substg1.0_39FE') if not self.__email: - self.__email = self._getStringStream('__substg1.0_3003') - self.__name = self._getStringStream('__substg1.0_3001') + self.__email = self.getStringStream('__substg1.0_3003') + self.__name = self.getStringStream('__substg1.0_3001') self.__typeFlags = self.__props.get('0C150003').value or 0 from .msg_classes.calendar_base import CalendarBase if isinstance(msg, CalendarBase): @@ -79,9 +85,9 @@ def _getStream(self, filename) -> Optional[bytes]: :raises ReferenceError: The associated MSGFile instance has been garbage collected. """ - if (msg := self.__msg()) is None: - raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') - return msg._getStream([self.__dir, filename]) + import warnings + warnings.warn(':method _getStream: has been deprecated and moved to the public api. Use :method getStream: instead (remove the underscore).', DeprecationWarning) + return self.getStream(filename) def _getStreamAs(self, streamID, stringStream : bool = True, overrideClass = None, preserveNone : bool = True): """ @@ -99,9 +105,9 @@ def _getStreamAs(self, streamID, stringStream : bool = True, overrideClass = Non If this is changed to False, then the value will be used regardless. """ if stringStream: - value = self._getStringStream(streamID) + value = self.getStringStream(streamID) else: - value = self._getStream(streamID) + value = self.getStream(streamID) # Check if we should be overriding the data type for this instance. if overrideClass is not None: @@ -124,9 +130,9 @@ def _getStringStream(self, filename) -> Optional[str]: :raises ReferenceError: The associated MSGFile instance has been garbage collected. """ - if (msg := self.__msg()) is None: - raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') - return msg._getStringStream([self.__dir, filename]) + import warnings + warnings.warn(':method _getStringStream: has been deprecated and moved to the public api. Use :method getStringStream: instead (remove the underscore).', DeprecationWarning) + return self.getStringStream(filename) def _getTypedAs(self, _id : str, overrideClass = None, preserveNone : bool = True): """ @@ -249,12 +255,44 @@ def existsTypedProperty(self, id, _type = None) -> bool: raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') return msg.existsTypedProperty(id, self.__dir, _type, True, self.__props) + def getStream(self, filename) -> Optional[bytes]: + """ + Gets a binary representation of the requested filename. + + This should ALWAYS return a bytes object if it was found, otherwise + returns None. + + :raises ReferenceError: The associated MSGFile instance has been garbage + collected. + """ + if (msg := self.__msg()) is None: + raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') + return msg.getStream([self.__dir, filename]) + + def getStringStream(self, filename) -> Optional[str]: + """ + Gets a string representation of the requested filename. + + Rather than the full filename, you should only feed this function the + filename sans the type. So if the full name is "__substg1.0_001A001F", + the filename this function should receive should be "__substg1.0_001A". + + This should ALWAYS return a string if it was found, otherwise returns + None. + + :raises ReferenceError: The associated MSGFile instance has been garbage + collected. + """ + if (msg := self.__msg()) is None: + raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') + return msg.getStringStream([self.__dir, filename]) + @functools.cached_property def account(self) -> Optional[str]: """ Returns the account of this recipient. """ - return self._getStringStream('__substg1.0_3A00') + return self.getStringStream('__substg1.0_3A00') @property def email(self) -> Optional[str]: @@ -282,7 +320,7 @@ def instanceKey(self) -> Optional[bytes]: """ Returns the instance key of this recipient. """ - return self._getStream('__substg1.0_0FF60102') + return self.getStream('__substg1.0_0FF60102') @property def name(self) -> Optional[str]: @@ -303,28 +341,28 @@ def recordKey(self) -> Optional[bytes]: """ Returns the instance key of this recipient. """ - return self._getStream('__substg1.0_0FF90102') + return self.getStream('__substg1.0_0FF90102') @functools.cached_property def searchKey(self) -> Optional[bytes]: """ Returns the search key of this recipient. """ - return self._getStream('__substg1.0_300B0102') + return self.getStream('__substg1.0_300B0102') @functools.cached_property def smtpAddress(self) -> Optional[str]: """ Returns the SMTP address of this recipient. """ - return self._getStringStream('__substg1.0_39FE') + return self.getStringStream('__substg1.0_39FE') @functools.cached_property def transmittableDisplayName(self) -> Optional[str]: """ Returns the transmittable display name of this recipient. """ - return self._getStringStream('__substg1.0_3A20') + return self.getStringStream('__substg1.0_3A20') @property def type(self) -> Union[RecipientType, MeetingRecipientType]: From eba0155f88d5417ebaad65387bd0cf03cba04ee3 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sat, 5 Aug 2023 17:59:49 -0700 Subject: [PATCH 04/20] Using typing types for enum deprecator --- extract_msg/enums.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extract_msg/enums.py b/extract_msg/enums.py index d117dad1..6bba275f 100644 --- a/extract_msg/enums.py +++ b/extract_msg/enums.py @@ -73,7 +73,7 @@ import enum -from typing import Union +from typing import Dict, Union class AddressBookType(enum.Enum): @@ -1776,7 +1776,7 @@ class _EnumDeprecator: Special class for handling deprecated enums in a way that shouldn't break existing code, including code for checking `is` on a member of the enum. """ - def __init__(self, oldClassName : str, newClass : enum.Enum, nameConversion : dict = {}, valueConversion : dict = {}): + def __init__(self, oldClassName : str, newClass : enum.Enum, nameConversion : Dict = {}, valueConversion : Dict = {}): """ :param oldClassName: The name to use in the deprecation message. :param newClass: The new enum class to look for the value in. From 4b40f914ab17a15a01658929d56928fcf2ffbfb8 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sat, 5 Aug 2023 18:46:43 -0700 Subject: [PATCH 05/20] Convert many enums to IntEnum --- extract_msg/enums.py | 102 +++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/extract_msg/enums.py b/extract_msg/enums.py index 6bba275f..589714b1 100644 --- a/extract_msg/enums.py +++ b/extract_msg/enums.py @@ -76,7 +76,7 @@ from typing import Dict, Union -class AddressBookType(enum.Enum): +class AddressBookType(enum.IntEnum): """ The type of object that an address book entry ID represents. MUST be one of these or it is invalid. @@ -113,7 +113,7 @@ class AppointmentAuxilaryFlag(enum.IntFlag): -class AppointmentColor(enum.Enum): +class AppointmentColor(enum.IntEnum): NONE = 0x00000000 RED = 0x00000001 BLUE = 0x00000002 @@ -140,7 +140,7 @@ class AppointmentStateFlag(enum.IntFlag): -class AttachmentPermissionType(enum.Enum): +class AttachmentPermissionType(enum.IntEnum): """ The permission type data associated with a web reference attachment. """ @@ -150,7 +150,7 @@ class AttachmentPermissionType(enum.Enum): -class AttachmentType(enum.Enum): +class AttachmentType(enum.IntEnum): """ The type represented by the attachment. @@ -177,7 +177,7 @@ class AttachmentType(enum.Enum): -class BCImageAlignment(enum.Enum): +class BCImageAlignment(enum.IntEnum): STRETCH = 0x00 TOP_LEFT = 0x01 TOP_CENTER = 0x02 @@ -191,13 +191,13 @@ class BCImageAlignment(enum.Enum): -class BCImageSource(enum.Enum): +class BCImageSource(enum.IntEnum): CONTACT_PHOTO = 0 CARD_PHOTO = 1 -class BCLabelFormat(enum.Enum): +class BCLabelFormat(enum.IntEnum): """ The format for a label of a business card. Left of the underscore represents the alignment, right indicates reading order. @@ -212,7 +212,7 @@ class BCLabelFormat(enum.Enum): -class BCTemplateID(enum.Enum): +class BCTemplateID(enum.IntEnum): """ The template ID for a business card. @@ -243,7 +243,7 @@ class BCTemplateID(enum.Enum): -class BCTextFormat(enum.Enum): +class BCTextFormat(enum.IntEnum): """ Converts the bits of the text format to an understandable enum value. @@ -325,7 +325,7 @@ class BodyTypes(enum.IntFlag): -class BusyStatus(enum.Enum): +class BusyStatus(enum.IntEnum): """ The availability of a use for the event described by the object. @@ -389,7 +389,7 @@ class Color(enum.IntEnum): -class ContactAddressIndex(enum.Enum): +class ContactAddressIndex(enum.IntEnum): EMAIL_1 = 0 EMAIL_2 = 1 EMAIL_3 = 2 @@ -399,7 +399,7 @@ class ContactAddressIndex(enum.Enum): -class ContactLinkState(enum.Enum): +class ContactLinkState(enum.IntEnum): """ Values for PidLidContactLinkGlobalAddressListLinkState. @@ -415,7 +415,7 @@ class ContactLinkState(enum.Enum): -class DeencapType(enum.Enum): +class DeencapType(enum.IntEnum): """ Enum to specify to custom deencapsulation functions the type of data being requested. @@ -434,7 +434,7 @@ class DirectoryEntryType(enum.IntEnum): -class DisplayType(enum.Enum): +class DisplayType(enum.IntEnum): MAILUSER = 0x0000 DISTLIST = 0x0001 FORUM = 0x0002 @@ -459,7 +459,7 @@ class DVAspect(enum.IntEnum): ICON = 4 -class ElectronicAddressProperties(enum.Enum): +class ElectronicAddressProperties(enum.IntEnum): EMAIL_1 = 0x00000000 EMAIL_2 = 0x00000001 EMAIL_3 = 0x00000002 @@ -553,7 +553,7 @@ class ErrorBehavior(enum.IntFlag): -class ErrorCode(enum.Enum): +class ErrorCode(enum.IntEnum): SUCCESS = 0x00000000 GENERAL_FAILURE = 0x80004005 OUT_OF_MEMORY = 0x8007000E @@ -626,7 +626,7 @@ class ErrorCode(enum.Enum): -class ErrorCodeType(enum.Enum): +class ErrorCodeType(enum.IntEnum): """ Enum representing values for PtypErrorCode. @@ -1153,7 +1153,7 @@ class ErrorCodeType(enum.Enum): -class Gender(enum.Enum): +class Gender(enum.IntEnum): # Seems rather binary, which is less than ideal. We are directly using the # terms used by the documentation. UNSPECIFIED = 0x0000 @@ -1162,7 +1162,7 @@ class Gender(enum.Enum): -class IconIndex(enum.Enum): +class IconIndex(enum.IntEnum): @classmethod def tryMake(cls, value : int) -> Union['IconIndex', int]: """ @@ -1188,7 +1188,7 @@ def tryMake(cls, value : int) -> Union['IconIndex', int]: -class Importance(enum.Enum): +class Importance(enum.IntEnum): LOW = 0 MEDIUM = 1 HIGH = 2 @@ -1218,14 +1218,14 @@ class InsecureFeatures(enum.IntFlag): -class Intelligence(enum.Enum): +class Intelligence(enum.IntEnum): ERROR = -1 DUMB = 0 SMART = 1 -class MacintoshEncoding(enum.Enum): +class MacintoshEncoding(enum.IntEnum): """ The encoding to use for Macintosh-specific data attachments. """ @@ -1265,7 +1265,7 @@ class MeetingObjectChange(enum.IntFlag): -class MeetingRecipientType(enum.Enum): +class MeetingRecipientType(enum.IntEnum): ORGANIZER = 0x01 SENDABLE_REQUIRED_ATTENDEE = 0x01 SENDABLE_OPTIONAL_ATTENDEE = 0x02 @@ -1273,7 +1273,7 @@ class MeetingRecipientType(enum.Enum): -class MeetingType(enum.Enum): +class MeetingType(enum.IntEnum): """ The type of Meeting Request object of Meeting Update object. @@ -1298,13 +1298,13 @@ class MeetingType(enum.Enum): -class MessageFormat(enum.Enum): +class MessageFormat(enum.IntEnum): TNEF = 0 MIME = 1 -class MessageType(enum.Enum): +class MessageType(enum.IntEnum): PRIVATE_FOLDER = 0x0001 PUBLIC_FOLDER = 0x0003 MAPPED_PUBLIC_FOLDER = 0x0005 @@ -1315,13 +1315,13 @@ class MessageType(enum.Enum): -class NamedPropertyType(enum.Enum): +class NamedPropertyType(enum.IntEnum): NUMERICAL_NAMED = 0 STRING_NAMED = 1 -class NoteColor(enum.Enum): +class NoteColor(enum.IntEnum): BLUE = 0 GREEN = 1 PINK = 2 @@ -1330,7 +1330,7 @@ class NoteColor(enum.Enum): -class OORBodyFormat(enum.Enum): +class OORBodyFormat(enum.IntEnum): """ The body format for One Off Recipients. """ @@ -1344,7 +1344,7 @@ class OORBodyFormat(enum.Enum): -class PostalAddressID(enum.Enum): +class PostalAddressID(enum.IntEnum): UNSPECIFIED = 0x00000000 HOME = 0x00000001 WORK = 0x00000002 @@ -1352,14 +1352,14 @@ class PostalAddressID(enum.Enum): -class Priority(enum.Enum): +class Priority(enum.IntEnum): URGENT = 0x00000001 NORMAL = 0x00000000 NOT_URGENT = 0xFFFFFFFF -class PropertiesType(enum.Enum): +class PropertiesType(enum.IntEnum): """ The type of the properties instance. """ @@ -1370,7 +1370,7 @@ class PropertiesType(enum.Enum): -class RecipientRowFlagType(enum.Enum): +class RecipientRowFlagType(enum.IntEnum): NOTYPE = 0x0 X500DN = 0x1 MSMAIL = 0x2 @@ -1382,7 +1382,7 @@ class RecipientRowFlagType(enum.Enum): -class RecipientType(enum.Enum): +class RecipientType(enum.IntEnum): """ The type of recipient. """ @@ -1393,7 +1393,7 @@ class RecipientType(enum.Enum): -class RecurCalendarType(enum.Enum): +class RecurCalendarType(enum.IntEnum): DEFAULT = 0x0000 CAL_GREGORIAN = 0x0001 CAL_GREGORIAN_US = 0x0002 @@ -1418,7 +1418,7 @@ class RecurCalendarType(enum.Enum): -class RecurDOW(enum.Enum): +class RecurDOW(enum.IntEnum): SUNDAY = 0x00000000 MONDAY = 0x00000001 TUESDAY = 0x00000002 @@ -1429,7 +1429,7 @@ class RecurDOW(enum.Enum): -class RecurEndType(enum.Enum): +class RecurEndType(enum.IntEnum): @classmethod def fromInt(cls, value) -> 'RecurEndType': """ @@ -1442,7 +1442,7 @@ def fromInt(cls, value) -> 'RecurEndType': NEVER_END = 0x00002023 -class RecurFrequency(enum.Enum): +class RecurFrequency(enum.IntEnum): """ See [MS-OXOCAL] for details. """ @@ -1453,7 +1453,7 @@ class RecurFrequency(enum.Enum): -class RecurMonthNthWeek(enum.Enum): +class RecurMonthNthWeek(enum.IntEnum): FIRST = 0x00000001 SECOND = 0x00000002 THIRD = 0x00000003 @@ -1476,7 +1476,7 @@ class RecurPatternTypeSpecificWeekday(enum.IntFlag): -class RecurPatternType(enum.Enum): +class RecurPatternType(enum.IntEnum): """ See [MS-OXOCAL] for details. """ @@ -1491,7 +1491,7 @@ class RecurPatternType(enum.Enum): -class ResponseStatus(enum.Enum): +class ResponseStatus(enum.IntEnum): """ The response status of an attendee. @@ -1521,7 +1521,7 @@ class ResponseType(enum.Enum): -class RuleActionType(enum.Enum): +class RuleActionType(enum.IntEnum): OP_MOVE = 0x01 OP_COPY = 0x02 OP_REPLY = 0x03 @@ -1536,7 +1536,7 @@ class RuleActionType(enum.Enum): -class SaveType(enum.Enum): +class SaveType(enum.IntEnum): """ Specifies the way that a function saved the data. Used to determine how the return value from a save function should be read. @@ -1561,7 +1561,7 @@ class SaveType(enum.Enum): -class Sensitivity(enum.Enum): +class Sensitivity(enum.IntEnum): NORMAL = 0 PERSONAL = 1 PRIVATE = 2 @@ -1623,7 +1623,7 @@ class SideEffect(enum.IntFlag): -class TaskAcceptance(enum.Enum): +class TaskAcceptance(enum.IntEnum): """ The acceptance state of the task. """ @@ -1634,7 +1634,7 @@ class TaskAcceptance(enum.Enum): -class TaskHistory(enum.Enum): +class TaskHistory(enum.IntEnum): """ The type of the last change to the Task object. """ @@ -1647,7 +1647,7 @@ class TaskHistory(enum.Enum): -class TaskMode(enum.Enum): +class TaskMode(enum.IntEnum): """ The mode of the Task object used in task communication (PidLidTaskMode). @@ -1674,7 +1674,7 @@ class TaskMultipleRecipients(enum.IntFlag): -class TaskOwnership(enum.Enum): +class TaskOwnership(enum.IntEnum): """ The role of the current user relative to the Task object. @@ -1690,7 +1690,7 @@ class TaskOwnership(enum.Enum): -class TaskRequestType(enum.Enum): +class TaskRequestType(enum.IntEnum): """ The type of task request. @@ -1720,7 +1720,7 @@ def fromClassType(cls, classType : str) -> 'TaskRequestType': -class TaskState(enum.Enum): +class TaskState(enum.IntEnum): """ NOT_ASSIGNED: The Task object is not assigned. ASSIGNEES_COPY_ACCEPTED: The Task object is the task assignee's copy of an @@ -1741,7 +1741,7 @@ class TaskState(enum.Enum): -class TaskStatus(enum.Enum): +class TaskStatus(enum.IntEnum): """ The status of a task object (PidLidTaskStatus). From bea0c8f2f29e8e64ab1a612b711f993f48c89ec0 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sat, 5 Aug 2023 18:47:10 -0700 Subject: [PATCH 06/20] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49dbe2bd..d863bb50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * As mentioned, a number of these deprecated functions have been moved to the public api. It is recommended that you run tests with your code after enabling deprecation warnings to see what should be changed. * Removed items deprecated in or before `0.42.0`. * Changed the API for the private method `_genRecipient`. This is not intended for use outside of the module *except* for subclasses. The change removed the allowance of ints for the second argument, requiring that it be a valid enum type. +* Convert many enum types to `IntEnum`. **v0.44.0** * Fixed a bug that caused `MessageBase.headerInit` to always return `False` after the 0.42.0 update. From c6faec340e1f526f0248a4728003cebbaaf90b49 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sat, 5 Aug 2023 18:56:20 -0700 Subject: [PATCH 07/20] Typing updates --- extract_msg/_rtf/tokenize_rtf.py | 4 ++-- extract_msg/enums.py | 9 ++++++--- extract_msg/ole_writer.py | 6 +++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/extract_msg/_rtf/tokenize_rtf.py b/extract_msg/_rtf/tokenize_rtf.py index 469beda4..e8fcc62f 100644 --- a/extract_msg/_rtf/tokenize_rtf.py +++ b/extract_msg/_rtf/tokenize_rtf.py @@ -99,7 +99,7 @@ def _finishTag(startText : bytes, reader : io.BytesIO) -> Tuple[bytes, Optional[ return startText, name, param, nextChar -def _readControl(startChar : bytes, reader : io.BytesIO) -> Tuple[Tuple[Token], bytes]: +def _readControl(startChar : bytes, reader : io.BytesIO) -> Tuple[Tuple[Token, ...], bytes]: """ Attempts to read the next data as a control, returning as many tokens as necessary. @@ -163,7 +163,7 @@ def _readControl(startChar : bytes, reader : io.BytesIO) -> Tuple[Tuple[Token], return (Token(startChar, TokenType.SYMBOL),), reader.read(1) -def _readText(startChar : bytes, reader : io.BytesIO) -> Tuple[Tuple[Token], bytes]: +def _readText(startChar : bytes, reader : io.BytesIO) -> Tuple[Tuple[Token, ...], bytes]: """ Attempts to read the next data as text. """ diff --git a/extract_msg/enums.py b/extract_msg/enums.py index 589714b1..0e4580be 100644 --- a/extract_msg/enums.py +++ b/extract_msg/enums.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + __all__ = [ 'AddressBookType', 'AppointmentAuxilaryFlag', @@ -1164,7 +1167,7 @@ class Gender(enum.IntEnum): class IconIndex(enum.IntEnum): @classmethod - def tryMake(cls, value : int) -> Union['IconIndex', int]: + def tryMake(cls, value : int) -> Union[IconIndex, int]: """ Try to make an instance, returning the value on failure. """ @@ -1431,7 +1434,7 @@ class RecurDOW(enum.IntEnum): class RecurEndType(enum.IntEnum): @classmethod - def fromInt(cls, value) -> 'RecurEndType': + def fromInt(cls, value) -> RecurEndType: """ Some enum values CAN be created from more than one int, so handle that. """ @@ -1700,7 +1703,7 @@ class TaskRequestType(enum.IntEnum): UPDATE: Task has been updated. """ @classmethod - def fromClassType(cls, classType : str) -> 'TaskRequestType': + def fromClassType(cls, classType : str) -> TaskRequestType: """ Convert a class type string into a TaskRequestType. """ diff --git a/extract_msg/ole_writer.py b/extract_msg/ole_writer.py index 579d0826..489c106b 100644 --- a/extract_msg/ole_writer.py +++ b/extract_msg/ole_writer.py @@ -30,9 +30,9 @@ class DirectoryEntry: Originals should be inaccessible outside of the class. """ name : str = '' - rightChild : 'DirectoryEntry' = None - leftChild : 'DirectoryEntry' = None - childTreeRoot : 'DirectoryEntry' = None + rightChild : DirectoryEntry = None + leftChild : DirectoryEntry = None + childTreeRoot : DirectoryEntry = None stateBits : int = 0 creationTime : int = 0 modifiedTime : int = 0 From e023d1aec531ea81d531d50c8bc8ac1cf8cb19d8 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sat, 5 Aug 2023 22:41:21 -0700 Subject: [PATCH 08/20] Improvemented to properties and typing --- CHANGELOG.md | 2 + extract_msg/_rtf/tokenize_rtf.py | 2 - extract_msg/attachments/attachment_base.py | 19 +++-- extract_msg/msg_classes/msg.py | 28 ++++--- extract_msg/properties/named.py | 4 +- extract_msg/properties/properties_store.py | 91 ++++++++++++++++++---- extract_msg/recipient.py | 19 +++-- extract_msg/utils.py | 2 +- 8 files changed, 126 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d863bb50..2ee56d85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ * Removed items deprecated in or before `0.42.0`. * Changed the API for the private method `_genRecipient`. This is not intended for use outside of the module *except* for subclasses. The change removed the allowance of ints for the second argument, requiring that it be a valid enum type. * Convert many enum types to `IntEnum`. +* Extended functionality of `PropertiesStore` to allow for integer property names and getting a property based on just the ID. You can also get a list of all properties that use a given ID. +* Improved internal code related to getting a property with a potentially unknown type. **v0.44.0** * Fixed a bug that caused `MessageBase.headerInit` to always return `False` after the 0.42.0 update. diff --git a/extract_msg/_rtf/tokenize_rtf.py b/extract_msg/_rtf/tokenize_rtf.py index e8fcc62f..0df620ab 100644 --- a/extract_msg/_rtf/tokenize_rtf.py +++ b/extract_msg/_rtf/tokenize_rtf.py @@ -221,8 +221,6 @@ def tokenizeRTF(data : bytes, validateStart : bool = True) -> None: tokens = [] nextChar = reader.read(1) - newToken = None - # At every iteration, so long as there is more data, nextChar should be # set. As such, use it to determine what kind of data to try to read, # using the delimeter of that type of data to know what to do next. diff --git a/extract_msg/attachments/attachment_base.py b/extract_msg/attachments/attachment_base.py index 0a18d357..1ca7a01d 100644 --- a/extract_msg/attachments/attachment_base.py +++ b/extract_msg/attachments/attachment_base.py @@ -207,12 +207,19 @@ def _getTypedProperty(self, propertyID, _type = None) -> Tuple[bool, Optional[ob VARIABLE_LENGTH_PROPS_STRING. """ verifyPropertyId(propertyID) - verifyType(_type) - propertyID = propertyID.upper() - for x in (propertyID + _type,) if _type is not None else self.props: - if x.startswith(propertyID): - prop = self.props[x] - return True, (prop.value if isinstance(prop, FixedLengthProp) else prop) + if _type: + verifyType(_type) + prop = self.props.get(propertyID + _type) + if isinstance(prop, FixedLengthProp): + return True, prop.value + else: + return False, None + else: + props = self.props.getProperties(propertyID) + for prop in props: + if isinstance(prop, FixedLengthProp): + return True, prop.value + return False, None def _getTypedStream(self, filename, _type = None): diff --git a/extract_msg/msg_classes/msg.py b/extract_msg/msg_classes/msg.py index 4f497a18..4a48a32b 100644 --- a/extract_msg/msg_classes/msg.py +++ b/extract_msg/msg_classes/msg.py @@ -357,7 +357,7 @@ def _getTypedData(self, _id : str, _type = None, prefix : bool = True): found, result = self._getTypedProperty(_id, _type) return result if found else None - def _getTypedProperty(self, propertyID : str, _type = None) -> Tuple[bool, Optional[object]]: + def _getTypedProperty(self, propertyID : str, _type = None) -> Tuple[bool, Optional[Any]]: """ Gets the property with the specified id as the type that it is supposed to be. :param id: MUST be a 4 digit hexadecimal string. @@ -367,15 +367,22 @@ def _getTypedProperty(self, propertyID : str, _type = None) -> Tuple[bool, Optio FIXED_LENGTH_PROPS_STRING or VARIABLE_LENGTH_PROPS_STRING. """ verifyPropertyId(propertyID) - verifyType(_type) - propertyID = propertyID.upper() - for x in (propertyID + _type,) if _type is not None else self.props: - if x.startswith(propertyID): - prop = self.props[x] - return True, (prop.value if isinstance(prop, FixedLengthProp) else prop) + if _type: + verifyType(_type) + prop = self.props.get(propertyID + _type) + if isinstance(prop, FixedLengthProp): + return True, prop.value + else: + return False, None + else: + props = self.props.getProperties(propertyID) + for prop in props: + if isinstance(prop, FixedLengthProp): + return True, prop.value + return False, None - def _getTypedStream(self, filename, prefix : bool = True, _type = None): + def _getTypedStream(self, filename, prefix : bool = True, _type = None) -> Tuple[bool, Optional[Any]]: """ Gets the contents of the specified stream as the type that it is supposed to be. @@ -396,9 +403,8 @@ def _getTypedStream(self, filename, prefix : bool = True, _type = None): verifyType(_type) filename = self.fixPath(filename, prefix) for x in (filename + _type,) if _type is not None else self.slistDir(): - if x.startswith(filename) and x.find('-') == -1: - contents = self.getStream(x, False) - if contents is None: + if x.startswith(filename) and '-' not in x: + if (contents := self.getStream(x, False)) is None: continue if len(contents) == 0: return True, None # We found the file, but it was empty. diff --git a/extract_msg/properties/named.py b/extract_msg/properties/named.py index 302b4114..9423f67d 100644 --- a/extract_msg/properties/named.py +++ b/extract_msg/properties/named.py @@ -225,7 +225,7 @@ def getStringStream(self, filename, prefix = True) -> Optional[str]: raise ReferenceError('The msg file for this Named instance has been garbage collected.') return msg.getStringStream([self.__dir, filename], prefix = prefix) - def items(self) -> Iterable[NamedPropertyBase]: + def items(self) -> Iterable[Tuple[Tuple[str, str], NamedPropertyBase]]: return self.__propertiesDict.items() def keys(self) -> Iterable[Tuple[str, str]]: @@ -237,7 +237,7 @@ def pprintKeys(self) -> None: """ pprint.pprint(sorted(self.__propertiesDict.keys())) - def values(self) -> Iterable[Tuple[Tuple[str, str], NamedPropertyBase]]: + def values(self) -> Iterable[NamedPropertyBase]: return self.__propertiesDict.values() @property diff --git a/extract_msg/properties/properties_store.py b/extract_msg/properties/properties_store.py index 9909f2de..51a5a429 100644 --- a/extract_msg/properties/properties_store.py +++ b/extract_msg/properties/properties_store.py @@ -8,7 +8,9 @@ import logging import pprint -from typing import Dict, Iterable, Iterator, Optional, Tuple, TypeVar, Union +from typing import ( + Dict, Iterable, Iterator, List, Optional, Tuple, TypeVar, Union + ) from .. import constants from ..enums import Intelligence, PropertiesType @@ -36,6 +38,9 @@ def __init__(self, data : Optional[bytes], _type : Optional[PropertiesType] = No self.__rawData = data self.__len = len(data) self.__props : Dict[str, PropBase] = {} + # This maps short IDs to all properties that use that ID. More than one + # property with the same ID but a different type may exist. + self.__idMapping : Dict[str, List[str]] = {} self.__naid = None self.__nrid = None self.__ac = None @@ -73,6 +78,12 @@ def __init__(self, data : Optional[bytes], _type : Optional[PropertiesType] = No if len(st) == 16: prop = createProp(st) self.__props[prop.name] = prop + + # Add the ID to our mapping list. + id_ = prop.name[:4] + if id_ not in self.__idMapping: + self.__idMapping[id_] = [] + self.__idMapping[id_].append(prop.name) else: logger.warning(f'Found stream from divide that was not 16 bytes: {st}. Ignoring.') self.__pl = len(self.__props) @@ -80,8 +91,10 @@ def __init__(self, data : Optional[bytes], _type : Optional[PropertiesType] = No def __contains__(self, key) -> bool: return self.__props.__contains__(key) - def __getitem__(self, key : str) -> PropBase: - return self.__props.__getitem__(key) + def __getitem__(self, key : Union[str, int]) -> PropBase: + if (found := self._mapId(key)): + return self.__props.__getitem__(found) + raise KeyError(key) def __iter__(self) -> Iterator[str]: return self.__props.__iter__() @@ -95,21 +108,73 @@ def __len__(self) -> int: def __repr__(self) -> str: return self.__props.__repr__() - def get(self, name : str, default : _T = None) -> Union[PropBase, _T]: + def _mapId(self, id_ : Union[int, str]) -> str: + """ + Converts an input into an appropriate property ID. + + This is a complex function, allowing the user to specify an int or + string. If the input is a string that is not 4 characters, it is + returned. Otherwise, a series of checks will be + performed. If the input is an int that is less than 0x10000, it is + considered a property ID without a type and converted to a 4 character + hexadecimal string. Otherwise, it is converted to an 8 character + hexadecimal string and returned. + + Once the input is a 4 character string from the other paths, it will + then be checked against the list of found property IDs, and the first + full ID will be returned. + + If a valid conversion could not be done, returns an empty string. + + All strings returned will be uppercase. + """ + # See if we need to convert to 4 character string and map or if we just + # need to return quickly. + if isinstance(id_, str): + id_ = id_.upper() + if len(id_) != 4: + return id_ + elif isinstance(id_, int): + if id_ >= 0x10000: + return f'{id_:08X}' + else: + id_ = f'{id_:04X}' + else: + return '' + + return self.__idMapping.get(id_, ('',))[0] + + def get(self, name : Union[str, int], default : _T = None) -> Union[PropBase, _T]: """ Retrieve the property of :param name:. Returns the value of :param default: if the property could not be found. """ - try: - return self.__props[name] - except KeyError: - # DEBUG - logger.debug('KeyError exception.') - logger.debug(properHex(self.__rawData)) - logger.debug(self.__props) + if (name := self._mapId(name)): + return self.__props.get(name, default) + else: return default - def items(self) -> Iterable[PropBase]: + def getProperties(self, id_ : Union[str, int]) -> List[PropBase]: + """ + Gets all properties with the specified ID. + + :param ID: An 4 digit hexadecimal string or an int that is less than + 0x10000. + """ + if isinstance(id_, int): + if id_ >= 0x10000: + return [] + else: + id_ = f'{id_:04X}' + elif isinstance(id_, str): + if len(id_) == 4: + id_ = id_.upper() + else: + return [] + + return [self[x] for x in self.__idMapping.get(id_, [])] + + def items(self) -> Iterable[Tuple[str, PropBase]]: return self.__props.items() def keys(self) -> Iterable[str]: @@ -121,7 +186,7 @@ def pprintKeys(self) -> None: """ pprint.pprint(sorted(tuple(self.__props.keys()))) - def values(self) -> Iterable[Tuple[str, PropBase]]: + def values(self) -> Iterable[PropBase]: return self.__props.values() items.__doc__ = dict.items.__doc__ diff --git a/extract_msg/recipient.py b/extract_msg/recipient.py index 5420b21f..d8f5700b 100644 --- a/extract_msg/recipient.py +++ b/extract_msg/recipient.py @@ -187,12 +187,19 @@ def _getTypedProperty(self, propertyID : str, _type = None) -> Tuple[bool, Optio FIXED_LENGTH_PROPS_STRING or VARIABLE_LENGTH_PROPS_STRING. """ verifyPropertyId(propertyID) - verifyType(_type) - propertyID = propertyID.upper() - for x in (propertyID + _type,) if _type is not None else self.props: - if x.startswith(propertyID): - prop = self.props[x] - return True, (prop.value if isinstance(prop, FixedLengthProp) else prop) + if _type: + verifyType(_type) + prop = self.props.get(propertyID + _type) + if isinstance(prop, FixedLengthProp): + return True, prop.value + else: + return False, None + else: + props = self.props.getProperties(propertyID) + for prop in props: + if isinstance(prop, FixedLengthProp): + return True, prop.value + return False, None def _getTypedStream(self, filename, _type = None): diff --git a/extract_msg/utils.py b/extract_msg/utils.py index bcffd3e5..042fcbad 100644 --- a/extract_msg/utils.py +++ b/extract_msg/utils.py @@ -1199,7 +1199,7 @@ def verifyPropertyId(id : str) -> None: raise InvaildPropertyIdError('ID was not a 4 digit hexadecimal string') -def verifyType(_type) -> str: +def verifyType(_type) -> None: """ Verifies that the type is valid. Raises an exception if it is not. From 742c5518bfb6f13ea9386c3a67f306fdb0e1a01e Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sun, 6 Aug 2023 21:48:34 -0700 Subject: [PATCH 09/20] Fix imports and minimize properHex --- extract_msg/msg_classes/msg.py | 6 +++--- extract_msg/properties/prop.py | 2 +- extract_msg/properties/properties_store.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extract_msg/msg_classes/msg.py b/extract_msg/msg_classes/msg.py index 4a48a32b..b6439794 100644 --- a/extract_msg/msg_classes/msg.py +++ b/extract_msg/msg_classes/msg.py @@ -39,7 +39,7 @@ from ..properties.properties_store import PropertiesStore from ..utils import ( divide, hasLen, inputToMsgPath, makeWeakRef, msgPathToString, - parseType, properHex, verifyPropertyId, verifyType, windowsUnicode + parseType, verifyPropertyId, verifyType, windowsUnicode ) @@ -426,8 +426,8 @@ def _getTypedStream(self, filename, prefix : bool = True, _type = None) -> Tuple if _type in ('101F', '101E', '1102'): if self.exists(x + '-00000000', False): for y in range(streams): - if self.exists(x + '-' + properHex(y, 8), False): - extras.append(self.getStream(x + '-' + properHex(y, 8), False)) + if self.exists((name := f'{x}-{y:08X}'), False): + extras.append(self.getStream(name, False)) elif _type in ('1002', '1003', '1004', '1005', '1007', '1014', '1040', '1048'): extras = divide(contents, (2 if _type in constants.MULTIPLE_2_BYTES else 4 if _type in constants.MULTIPLE_4_BYTES else 8 if _type in constants.MULTIPLE_8_BYTES else 16)) contents = streams diff --git a/extract_msg/properties/prop.py b/extract_msg/properties/prop.py index bc139879..d71b060b 100644 --- a/extract_msg/properties/prop.py +++ b/extract_msg/properties/prop.py @@ -45,7 +45,7 @@ class PropBase(abc.ABC): def __init__(self, data : bytes): self.__rawData = data - self.__name = properHex(data[3::-1]).upper() + self.__name = data[3::-1].hex().upper() self.__type, self.__flags = constants.st.ST2.unpack(data) self.__fm = self.__flags & 1 == 1 self.__fr = self.__flags & 2 == 2 diff --git a/extract_msg/properties/properties_store.py b/extract_msg/properties/properties_store.py index 51a5a429..e0b5813a 100644 --- a/extract_msg/properties/properties_store.py +++ b/extract_msg/properties/properties_store.py @@ -15,7 +15,7 @@ from .. import constants from ..enums import Intelligence, PropertiesType from .prop import createProp, PropBase -from ..utils import divide, properHex +from ..utils import divide logger = logging.getLogger(__name__) From 4caaca0f688eee093cea8ff9f0cd6060a8596625 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Mon, 7 Aug 2023 22:18:50 -0700 Subject: [PATCH 10/20] Work on additional public api --- CHANGELOG.md | 8 ++ extract_msg/msg_classes/calendar_base.py | 39 -------- extract_msg/msg_classes/contact.py | 16 ++-- extract_msg/msg_classes/msg.py | 115 ++++++++++++++++++++++- extract_msg/properties/named.py | 63 ++----------- extract_msg/utils.py | 4 +- 6 files changed, 136 insertions(+), 109 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ee56d85..351aa0c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ **v0.45.0** +* BREAKING: Changed parsing of string multiple properties to remove the trailing null byte. This *will* cause the output of parsing them to differ. * Updated typing information for some functions and classes. * Fixed a bug with `MessageSignedBase.attachments` that would cause it to return None instead of an empty list if the number of normal attachments was 0 was the error behavior was set to ignore violations of the standard. * Updated `MessageSignedBase.attachments` to use `functools.cached_property` instead of `property`. @@ -14,6 +15,13 @@ * Convert many enum types to `IntEnum`. * Extended functionality of `PropertiesStore` to allow for integer property names and getting a property based on just the ID. You can also get a list of all properties that use a given ID. * Improved internal code related to getting a property with a potentially unknown type. +* Added a number of entirely new functions to the public API on `MSGFile`, `AttachmentBase`, `PropertiesStore`, and `Recipient` objects: + * `getMultipleBinary`: Gets a multiple binary property as a list of `bytes` objects. + * `getSingleOrMultipleBinary`: A combination of `getStream` and `getMultipleBinary` which prefers a single binary stream. Returns a single `bytes` object or a list of `bytes` objects. + * `getMultipleString`: Gets a multiple string property as a list of `str` objects. + * `getSingleOrMultipleString`: A combination of `getStringStream` and `getMultipleString` which prefers a single string stream. Returns a single bytes objecct or a list of bytes objects. +* Removed `Named._getStringStream` and `Named.sExists`. The named properties storage will *always* +* Changed all `Named` methods to no longer have a prefix argument. The prefix should *always* be false sense the named property mapping will only exist in the top level directory. **v0.44.0** * Fixed a bug that caused `MessageBase.headerInit` to always return `False` after the 0.42.0 update. diff --git a/extract_msg/msg_classes/calendar_base.py b/extract_msg/msg_classes/calendar_base.py index f9a9bd73..bcdf439b 100644 --- a/extract_msg/msg_classes/calendar_base.py +++ b/extract_msg/msg_classes/calendar_base.py @@ -28,45 +28,6 @@ class CalendarBase(MessageBase): Common base for all Appointment and Meeting objects. """ - def _genRecipient(self, recipientType, recipientInt : MeetingRecipientType) -> Optional[str]: - """ - Returns the specified recipient field. - """ - recipientInt = MeetingRecipientType(recipientInt) - value = None - # Check header first. - if self.headerInit: - value = self.header[recipientType] - if value: - value = value.replace(',', self.recipientSeparator) - - # If the header had a blank field or didn't have the field, generate - # it manually. - if not value: - # Check if the header has initialized. - if self.headerInit: - logger.info(f'Header found, but "{recipientType}" is not included. Will be generated from other streams.') - - # Get a list of the recipients of the specified type. - foundRecipients = tuple(recipient.formatted for recipient in self.recipients if recipient.type == recipientInt) - - # If we found recipients, join them with the recipient separator - # and a space. - if len(foundRecipients) > 0: - value = (self.recipientSeparator + ' ').join(foundRecipients) - - # Code to fix the formatting so it's all a single line. This allows - # the user to format it themself if they want. This should probably - # be redone to use re or something, but I can do that later. This - # shouldn't be a huge problem for now. - if value: - value = value.replace(' \r\n\t', ' ').replace('\r\n\t ', ' ').replace('\r\n\t', ' ') - value = value.replace('\r\n', ' ').replace('\r', ' ').replace('\n', ' ') - while value.find(' ') != -1: - value = value.replace(' ', ' ') - - return value - @functools.cached_property def allAttendeesString(self) -> Optional[str]: """ diff --git a/extract_msg/msg_classes/contact.py b/extract_msg/msg_classes/contact.py index e4429018..a185025c 100644 --- a/extract_msg/msg_classes/contact.py +++ b/extract_msg/msg_classes/contact.py @@ -212,7 +212,7 @@ def businessTelephone2Number(self) -> Optional[Union[str, List[str]]]: """ Contains the second number or numbers of the contact's business. """ - return self._getTypedAs('3A1B') + return self.getSingleOrMultipleString('__substg1.0_3A1B') @functools.cached_property def businessHomePage(self) -> Optional[str]: @@ -240,7 +240,7 @@ def childrensNames(self) -> Optional[List[str]]: """ A list of the named of the contact's children. """ - return self._getTypedAs('3A58') + return self.getMultipleString('__substg1.0_3A58') @functools.cached_property def companyMainTelephoneNumber(self) -> Optional[str]: @@ -437,7 +437,7 @@ def email1OriginalEntryId(self) -> Optional[EntryID]: return self._getNamedAs('8085', ps.PSETID_ADDRESS, EntryID.autoCreate) @functools.cached_property - def email2(self) -> Optional[dict]: + def email2(self) -> Optional[Dict]: """ Returns a dict of the data for email 2. Returns None if no fields are set. @@ -488,7 +488,7 @@ def email2OriginalEntryId(self) -> Optional[EntryID]: return self._getNamedAs('8095', ps.PSETID_ADDRESS, EntryID.autoCreate) @functools.cached_property - def email3(self) -> Optional[dict]: + def email3(self) -> Optional[Dict]: """ Returns a dict of the data for email 3. Returns None if no fields are set. @@ -849,7 +849,7 @@ def homeTelephone2Number(self) -> Optional[Union[str, List[str]]]: """ The number(s) of the contact's second home telephone. """ - return self._getTypedAs('3A2F') + return self.getSingleOrMultipleString('__substg1.0_3A2F') @functools.cached_property def initials(self) -> Optional[str]: @@ -1123,7 +1123,7 @@ def postalAddressID(self) -> PostalAddressID: return self._getNamedAs('8022', ps.PSETID_ADDRESS, lambda x : PostalAddressID(x or 0), False) @functools.cached_property - def primaryFax(self) -> Optional[dict]: + def primaryFax(self) -> Optional[Dict]: """ Returns a dict of the data for the primary fax. Returns None if no fields are set. @@ -1240,14 +1240,14 @@ def telexNumber(self) -> Optional[Union[str, List[str]]]: """ The contact's telex number(s). """ - return self._getTypedAs('3A2C') + return self.getSingleOrMultipleString('__substg1.0_3A2C') @functools.cached_property def userX509Certificate(self) -> Optional[List[bytes]]: """ A list of certificates for the contact. """ - return self._getTypedAs('3A70') + return self.getMultipleBinary('3A70') @functools.cached_property def weddingAnniversary(self) -> Optional[datetime.datetime]: diff --git a/extract_msg/msg_classes/msg.py b/extract_msg/msg_classes/msg.py index b6439794..b62bacd4 100644 --- a/extract_msg/msg_classes/msg.py +++ b/extract_msg/msg_classes/msg.py @@ -19,7 +19,7 @@ import olefile -from typing import Any, Callable, cast, Dict, List, Optional, Set, Tuple, Union +from typing import Any, Callable, cast, Dict, List, Optional, Tuple, Union from .. import constants from ..attachments import ( @@ -27,8 +27,8 @@ ) from ..encoding import lookupCodePage from ..enums import ( - AttachErrorBehavior, ErrorBehavior, InsecureFeatures, Importance, - Priority, PropertiesType, SaveType, Sensitivity, SideEffect + ErrorBehavior, InsecureFeatures, Importance, Priority, PropertiesType, + SaveType, Sensitivity, SideEffect ) from ..exceptions import ( ConversionError, InvalidFileFormatError, PrefixError, @@ -550,12 +550,114 @@ def fixPath(self, inp, prefix : bool = True) -> str: inp = self.__prefix + inp return inp + def getMultipleBinary(self, filename, prefix : bool = True) -> Optional[List[bytes]]: + """ + Gets a multiple binary property as a list of bytes objects. + + Like :method getStringStream:, the 4 character type suffix should be + omitted. So if you want the stream "__substg1.0_00011102" then the + filename would simply be "__substg1.0_0001". + + :param prefix: Bool, whether to search for the entry at the root of the + MSG file (False) or look in the current child MSG file (True). + """ + filename = self.fixPath(filename, prefix) + '1102' + multStream = self.getStream(filename) + if multStream is None: + return None + + if len(multStream) == 0: + return [] + elif len(multStream) & 7 != 0: + raise StandardViolationError(f'Length stream for multiple binary was not a multiple of 8.') + else: + ret = [self.getStream(filename + f'-{x:08X}') for x in range(len(multStream) // 8)] + # We could do more checking here, but we'll just check for None. + if (index := next((x for x in ret if x is None), -1)) != -1: + logger.error('Unable to get the desired number of binary streams for multiple, not all streams were found.') + return ret[:index] + return ret + + def getMultipleString(self, filename, prefix : bool = True) -> Optional[List[str]]: + """ + Gets a multiple string property as a list of str objects. + + Like :method getStringStream:, the 4 character type suffix should be + omitted. So if you want the stream "__substg1.0_00011102" then the + filename would simply be "__substg1.0_0001". + + :param prefix: Bool, whether to search for the entry at the root of the + MSG file (False) or look in the current child MSG file (True). + """ + filename = self.fixPath(filename, prefix) + '101F' if self.areStringsUnicode else '101E' + multStream = self.getStream(filename) + if multStream is None: + return [] + + if len(multStream) == 0: + return [] + elif len(multStream) & 3 != 0: + raise StandardViolationError(f'Length stream for multiple string was not a multiple of 4.') + else: + ret = [self.getStream(filename + f'-{x:08X}') for x in range(len(multStream) // 4)] + # We could do more checking here, but we'll just check for None. + for index, item in enumerate(ret): + if item is None: + logger.error('Unable to get the desired number of string streams for multiple, not all streams were found.') + return ret[:index] + # Decode the bytes and remove the null byte. + ret[index] = item.decode(self.stringEncoding)[:-1] + return ret + + def getSingleOrMultipleBinary(self, filename, prefix : bool = True) -> Optional[Union[List[bytes], bytes]]: + """ + A combination of :method getStringStream: and + :method getMultipleString:. + + Checks to see if a single binary stream exists to return, otherwise + tries to return the multiple binary stream of the same ID. + + Like :method getStringStream:, the 4 character type suffix should be + omitted. So if you want the stream "__substg1.0_00010102" then the + filename would simply be "__substg1.0_0001". + """ + filename = self.fixPath(filename, prefix) + # Check for a single binary stream first. + if (ret := self.getStream(filename + '0102', False)) is not None: + return ret + # Otherwise, we just let the return from `getMultipleBinary` do the + # work. + return self.getMultipleBinary(filename, False) + + def getSingleOrMultipleString(self, filename, prefix : bool = True) -> Optional[Union[List[str], str]]: + """ + A combination of :method getStringStream: and + :method getMultipleString:. + + Checks to see if a single string stream exists to return, otherwise + tries to return the multiple string stream of the same ID. + + Like :method getStringStream:, the 4 character type suffix should be + omitted. So if you want the stream "__substg1.0_0001001F" then the + filename would simply be "__substg1.0_0001". + """ + filename = self.fixPath(filename, prefix) + # Check for a single stribng stream first. + if (ret := self.getStringStream(filename, False)) is not None: + return ret + # Otherwise, we just let the return from `getMultipleString` do the + # work. + return self.getMultipleString(filename, False) + def getStream(self, filename, prefix : bool = True) -> Optional[bytes]: """ Gets a binary representation of the requested filename. This should ALWAYS return a bytes object if it was found, otherwise returns None. + + :param prefix: Bool, whether to search for the entry at the root of the + MSG file (False) or look in the current child MSG file (True). """ filename = self.fixPath(filename, prefix) if self.exists(filename, False): @@ -575,6 +677,9 @@ def getStringStream(self, filename, prefix : bool = True) -> Optional[str]: This should ALWAYS return a string if it was found, otherwise returns None. + + :param prefix: Bool, whether to search for the entry at the root of the + MSG file (False) or look in the current child MSG file (True). """ filename = self.fixPath(filename, prefix) if self.areStringsUnicode: @@ -610,12 +715,12 @@ def listDir(self, streams : bool = True, storages : bool = False, includePrefix return entries - def slistDir(self, streams : bool = True, storages : bool = False) -> List[str]: + def slistDir(self, streams : bool = True, storages : bool = False, includePrefix : bool = True) -> List[str]: """ Replacement for OleFileIO.listdir that runs at the current prefix directory. Returns a list of strings instead of lists. """ - return [msgPathToString(x) for x in self.listDir(streams, storages)] + return [msgPathToString(x) for x in self.listDir(streams, storages, includePrefix)] def save(self, **kwargs) -> constants.SAVE_TYPE: if kwargs.get('skipNotImplemented', False): diff --git a/extract_msg/properties/named.py b/extract_msg/properties/named.py index 9423f67d..f2777f3a 100644 --- a/extract_msg/properties/named.py +++ b/extract_msg/properties/named.py @@ -47,11 +47,11 @@ class Named: def __init__(self, msg : MSGFile): self.__msg = makeWeakRef(msg) # Get the basic streams. If all are emtpy, then nothing to do. - guidStream = self.getStream('__substg1.0_00020102', False) - entryStream = self.getStream('__substg1.0_00030102', False) + guidStream = self.getStream('__substg1.0_00020102') + entryStream = self.getStream('__substg1.0_00030102') self.guidStream = guidStream self.entryStream = entryStream - self.namesStream = self.getStream('__substg1.0_00040102', False) + self.namesStream = self.getStream('__substg1.0_00040102') self.__propertiesDict : Dict[Tuple[str, str], NamedPropertyBase]= {} self.__properties : List[NamedPropertyBase] = [] @@ -129,7 +129,7 @@ def __getName(self, offset : int) -> str: return self.namesStream[offset:offset + length].decode('utf-16-le') - def _getStream(self, filename, prefix = True) -> Optional[bytes]: + def _getStream(self, filename) -> Optional[bytes]: """ Gets a binary representation of the requested filename. @@ -141,25 +141,7 @@ def _getStream(self, filename, prefix = True) -> Optional[bytes]: """ import warnings warnings.warn(':method _getStream: has been deprecated and moved to the public api. Use :method getStream: instead (remove the underscore).', DeprecationWarning) - return self.getStream(filename, prefix) - - def _getStringStream(self, filename, prefix = True) -> Optional[str]: - """ - Gets a string representation of the requested filename. - - Rather than the full filename, you should only feed this function the - filename sans the type. So if the full name is "__substg1.0_001A001F", - the filename this function should receive should be "__substg1.0_001A". - - This should ALWAYS return a string if it was found, otherwise returns - None. - - :raises ReferenceError: The associated MSGFile instance has been garbage - collected. - """ - import warnings - warnings.warn(':method _getStringStream: has been deprecated and moved to the public api. Use :method getStringStream: instead (remove the underscore).', DeprecationWarning) - return self.getStringStream(filename, prefix) + return self.getStream(filename) def exists(self, filename) -> bool: """ @@ -170,18 +152,7 @@ def exists(self, filename) -> bool: """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Named instance has been garbage collected.') - return msg.exists([self.__dir, filename]) - - def sExists(self, filename) -> bool: - """ - Checks if the string stream exists inside the named properties folder. - - :raises ReferenceError: The associated MSGFile instance has been garbage - collected. - """ - if (msg := self.__msg()) is None: - raise ReferenceError('The msg file for this Named instance has been garbage collected.') - return msg.sExists([self.__dir, filename]) + return msg.exists([self.__dir, filename], False) def get(self, propertyName : Tuple[str, str], default : _T = None) -> Union[NamedPropertyBase, _T]: """ @@ -193,7 +164,7 @@ def get(self, propertyName : Tuple[str, str], default : _T = None) -> Union[Name except KeyError: return default - def getStream(self, filename, prefix = True) -> Optional[bytes]: + def getStream(self, filename) -> Optional[bytes]: """ Gets a binary representation of the requested filename. @@ -205,25 +176,7 @@ def getStream(self, filename, prefix = True) -> Optional[bytes]: """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Named instance has been garbage collected.') - return msg.getStream([self.__dir, filename], prefix = prefix) - - def getStringStream(self, filename, prefix = True) -> Optional[str]: - """ - Gets a string representation of the requested filename. - - Rather than the full filename, you should only feed this function the - filename sans the type. So if the full name is "__substg1.0_001A001F", - the filename this function should receive should be "__substg1.0_001A". - - This should ALWAYS return a string if it was found, otherwise returns - None. - - :raises ReferenceError: The associated MSGFile instance has been garbage - collected. - """ - if (msg := self.__msg()) is None: - raise ReferenceError('The msg file for this Named instance has been garbage collected.') - return msg.getStringStream([self.__dir, filename], prefix = prefix) + return msg.getStream([self.__dir, filename], False) def items(self) -> Iterable[Tuple[Tuple[str, str], NamedPropertyBase]]: return self.__propertiesDict.items() diff --git a/extract_msg/utils.py b/extract_msg/utils.py index 042fcbad..3999177d 100644 --- a/extract_msg/utils.py +++ b/extract_msg/utils.py @@ -723,9 +723,9 @@ def parseType(_type : int, stream, encoding, extras): elif _type == 0x0102: # PtypBinary return value elif _type & 0x1000 == 0x1000: # PtypMultiple - # TODO parsing for `multiple` types. + # TODO parsing for remaining "multiple" types. if _type in (0x101F, 0x101E): # PtypMultipleString/PtypMultipleString8 - ret = [x.decode(encoding) for x in extras] + ret = [x.decode(encoding)[:-1] for x in extras] lengths = struct.unpack(f'<{len(ret)}i', stream) lengthLengths = len(lengths) if lengthLengths > lengthExtras: From 64e7c5df6ce2853bb7c2ba4c79ffccf5410d394e Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 9 Aug 2023 15:13:13 -0700 Subject: [PATCH 11/20] More public api development and transition --- CHANGELOG.md | 5 +- extract_msg/attachments/attachment_base.py | 74 +++++++++++++++++++++- extract_msg/msg_classes/calendar_base.py | 12 ++-- extract_msg/msg_classes/contact.py | 12 ++-- extract_msg/msg_classes/meeting_related.py | 2 +- extract_msg/msg_classes/message_base.py | 2 +- extract_msg/msg_classes/msg.py | 13 +--- extract_msg/msg_classes/task_request.py | 2 +- extract_msg/properties/named.py | 17 +++-- extract_msg/properties/prop.py | 7 +- extract_msg/properties/properties_store.py | 27 +++++++- extract_msg/recipient.py | 72 ++++++++++++++++++++- extract_msg/utils.py | 19 +++--- 13 files changed, 209 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 351aa0c2..f25861b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,13 @@ * Changed `PropBase` to be a subclass of `abc.ABC`. * Added detailed versioning info to the README. * Deprecated many private functions, including methods on many of the classes. Of primary note are `_getStream` and `_getStringStream`, which have been moved to the public API as `getStream` and `getStringStream`. Any deprecated functions still exist and will forward to a public API function if they are not being removed. Additionally, all internal usage of them has been removed. This change is one of the big preparations that is needed for the `1.0.0` release. - * As mentioned, a number of these deprecated functions have been moved to the public api. It is recommended that you run tests with your code after enabling deprecation warnings to see what should be changed. + * As mentioned, a number of these deprecated functions have been moved to the public API. It is recommended that you run tests with your code after enabling deprecation warnings to see what should be changed. * Removed items deprecated in or before `0.42.0`. * Changed the API for the private method `_genRecipient`. This is not intended for use outside of the module *except* for subclasses. The change removed the allowance of ints for the second argument, requiring that it be a valid enum type. * Convert many enum types to `IntEnum`. * Extended functionality of `PropertiesStore` to allow for integer property names and getting a property based on just the ID. You can also get a list of all properties that use a given ID. +* Added new function `PropertiesStore.getProperties` which gets a list of all properties matching the property ID. Return type is a list of `PropBase` instances. +* Added new function `PropertiesStore.getValue` which looks for the first matching `FixedLengthProp` and returns the value from it. * Improved internal code related to getting a property with a potentially unknown type. * Added a number of entirely new functions to the public API on `MSGFile`, `AttachmentBase`, `PropertiesStore`, and `Recipient` objects: * `getMultipleBinary`: Gets a multiple binary property as a list of `bytes` objects. @@ -22,6 +24,7 @@ * `getSingleOrMultipleString`: A combination of `getStringStream` and `getMultipleString` which prefers a single string stream. Returns a single bytes objecct or a list of bytes objects. * Removed `Named._getStringStream` and `Named.sExists`. The named properties storage will *always* * Changed all `Named` methods to no longer have a prefix argument. The prefix should *always* be false sense the named property mapping will only exist in the top level directory. +* Adjusted `tryGetMimeType` to allows any attachments whose `data` property would return a `bytes` instance. **v0.44.0** * Fixed a bug that caused `MessageBase.headerInit` to always return `False` after the 0.42.0 update. diff --git a/extract_msg/attachments/attachment_base.py b/extract_msg/attachments/attachment_base.py index 1ca7a01d..ab092bd1 100644 --- a/extract_msg/attachments/attachment_base.py +++ b/extract_msg/attachments/attachment_base.py @@ -14,8 +14,8 @@ import pathlib import weakref -from functools import cached_property, partial -from typing import List, Optional, Tuple, Type, TYPE_CHECKING +from functools import cached_property +from typing import List, Optional, Tuple, Type, TYPE_CHECKING, Union from .. import constants from ..enums import AttachmentType @@ -327,6 +327,74 @@ def existsTypedProperty(self, id, _type = None) -> bool: raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') return msg.existsTypedProperty(id, self.__dir, _type, True, self.__props) + def getMultipleBinary(self, filename) -> Optional[List[bytes]]: + """ + Gets a multiple binary property as a list of bytes objects. + + Like :method getStringStream:, the 4 character type suffix should be + omitted. So if you want the stream "__substg1.0_00011102" then the + filename would simply be "__substg1.0_0001". + + :raises ReferenceError: The associated MSGFile instance has been garbage + collected. + """ + if (msg := self.__msg()) is None: + raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') + return msg.getMultipleBinary([self.__dir, filename]) + + def getMultipleString(self, filename) -> Optional[List[str]]: + """ + Gets a multiple string property as a list of str objects. + + Like :method getStringStream:, the 4 character type suffix should be + omitted. So if you want the stream "__substg1.0_00011102" then the + filename would simply be "__substg1.0_0001". + + :raises ReferenceError: The associated MSGFile instance has been garbage + collected. + """ + if (msg := self.__msg()) is None: + raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') + return msg.getMultipleString([self.__dir, filename]) + + def getSingleOrMultipleBinary(self, filename) -> Optional[Union[List[bytes], bytes]]: + """ + A combination of :method getStringStream: and + :method getMultipleString:. + + Checks to see if a single binary stream exists to return, otherwise + tries to return the multiple binary stream of the same ID. + + Like :method getStringStream:, the 4 character type suffix should be + omitted. So if you want the stream "__substg1.0_00010102" then the + filename would simply be "__substg1.0_0001". + + :raises ReferenceError: The associated MSGFile instance has been garbage + collected. + """ + if (msg := self.__msg()) is None: + raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') + return msg.getSingleOrMultipleBinary([self.__dir, filename]) + + def getSingleOrMultipleString(self, filename) -> Optional[Union[List[str], str]]: + """ + A combination of :method getStringStream: and + :method getMultipleString:. + + Checks to see if a single string stream exists to return, otherwise + tries to return the multiple string stream of the same ID. + + Like :method getStringStream:, the 4 character type suffix should be + omitted. So if you want the stream "__substg1.0_0001001F" then the + filename would simply be "__substg1.0_0001". + + :raises ReferenceError: The associated MSGFile instance has been garbage + collected. + """ + if (msg := self.__msg()) is None: + raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') + return msg.getSingleOrMultipleString([self.__dir, filename]) + def getStream(self, filename) -> Optional[bytes]: """ Gets a binary representation of the requested filename. @@ -463,7 +531,7 @@ def data(self) -> Optional[object]: """ @functools.cached_property - def dataType(self) -> Optional[Type[type]]: + def dataType(self) -> Optional[Type[object]]: """ The class that the data type will use, if it can be retrieved. diff --git a/extract_msg/msg_classes/calendar_base.py b/extract_msg/msg_classes/calendar_base.py index bcdf439b..4b35f261 100644 --- a/extract_msg/msg_classes/calendar_base.py +++ b/extract_msg/msg_classes/calendar_base.py @@ -248,7 +248,7 @@ def endDate(self) -> Optional[datetime.datetime]: """ The end date of the appointment. """ - return self._getPropertyAs('00610040') + return self.props.getValue('00610040') @functools.cached_property def globalObjectID(self) -> Optional[GlobalObjectID]: @@ -387,7 +387,7 @@ def ownerAppointmentID(self) -> Optional[int]: Assists a client or server in finding a Calendar object but is not guarenteed to be unique amoung all objects. """ - return self._getPropertyAs('00620003') + return self.props.getValue('00620003') @functools.cached_property def ownerCriticalChange(self) -> Optional[datetime.datetime]: @@ -417,7 +417,7 @@ def replyRequested(self) -> bool: """ Whether the organizer requests a reply from attendees. """ - return self._getPropertyAs('0C17000B', bool, False) + return bool(self.props.getValue('0C17000B')) @functools.cached_property def requiredAttendees(self) -> Optional[str]: @@ -438,21 +438,21 @@ def responseRequested(self) -> bool: """ Whether to send Meeting Response objects to the organizer. """ - return self._getPropertyAs('0063000B', bool, False) + return bool(self.props.getValue('0063000B')) @functools.cached_property def responseStatus(self) -> ResponseStatus: """ The response status of an attendee. """ - return self._getNamedAs('8218', ps.PSETID_APPOINTMENT, lambda x: ResponseStatus(x or 0), False) + return ResponseStatus(self.namedProperties.get(('8218', ps.PSETID_APPOINTMENT), 0)) @functools.cached_property def startDate(self) -> Optional[datetime.datetime]: """ The start date of the appointment. """ - return self._getPropertyAs('00600040') + return self.props.getValue('00600040') @functools.cached_property def timeZoneDescription(self) -> Optional[str]: diff --git a/extract_msg/msg_classes/contact.py b/extract_msg/msg_classes/contact.py index a185025c..e0fe0a8a 100644 --- a/extract_msg/msg_classes/contact.py +++ b/extract_msg/msg_classes/contact.py @@ -83,7 +83,7 @@ def birthday(self) -> Optional[datetime.datetime]: """ The birthday of the contact at 11:59 UTC. """ - return self._getPropertyAs('3A420040') + return self.props.getValue('3A420040') @functools.cached_property def birthdayEventEntryID(self) -> Optional[EntryID]: @@ -593,11 +593,11 @@ def ftpSite(self) -> Optional[str]: return self.getStringStream('__substg1.0_3A4C') @functools.cached_property - def gender(self) -> Optional[Gender]: + def gender(self) -> Gender: """ The gender of the contact. """ - return self._getPropertyAs('3A4D0002', lambda x : Gender(x or 0), False) + return Gender(self.props.getValue('3A4D0002', 0)) @functools.cached_property def generation(self) -> Optional[str]: @@ -1014,7 +1014,7 @@ def oscSyncEnabled(self) -> bool: Whether contact synchronization with an external source (such as a social networking site) is handled by the server. """ - return self._getPropertyAs('7C24000B', bool, False) + return bool(self.props.getValue('7C24000B')) @functools.cached_property def otherAddress(self) -> Optional[str]: @@ -1120,7 +1120,7 @@ def postalAddressID(self) -> PostalAddressID: Indicates which physical address is the Mailing Address for this contact. """ - return self._getNamedAs('8022', ps.PSETID_ADDRESS, lambda x : PostalAddressID(x or 0), False) + return PostalAddressID(self.namedProperties.get(('8022', ps.PSETID_ADDRESS), 0)) @functools.cached_property def primaryFax(self) -> Optional[Dict]: @@ -1254,7 +1254,7 @@ def weddingAnniversary(self) -> Optional[datetime.datetime]: """ The wedding anniversary of the contact at 11:59 UTC. """ - return self._getPropertyAs('3A410040') + return self.props.getValue('3A410040') @functools.cached_property def weddingAnniversaryEventEntryID(self) -> Optional[EntryID]: diff --git a/extract_msg/msg_classes/meeting_related.py b/extract_msg/msg_classes/meeting_related.py index 6d2f8bef..e976a6e7 100644 --- a/extract_msg/msg_classes/meeting_related.py +++ b/extract_msg/msg_classes/meeting_related.py @@ -30,7 +30,7 @@ def processed(self) -> bool: """ Indicates whether a client has processed a meeting-related object. """ - return self._getPropertyAs('7D01000B', bool, False) + return bool(self.props.getValue('7D01000B')) @functools.cached_property def serverProcessed(self) -> bool: diff --git a/extract_msg/msg_classes/message_base.py b/extract_msg/msg_classes/message_base.py index ab4f305e..c982775c 100644 --- a/extract_msg/msg_classes/message_base.py +++ b/extract_msg/msg_classes/message_base.py @@ -1247,7 +1247,7 @@ def receivedTime(self) -> Optional[datetime.datetime]: """ The date and time the message was received by the server. """ - return self._getPropertyAs('0E060040') + return self.props.getValue('0E060040') @property def recipientSeparator(self) -> str: diff --git a/extract_msg/msg_classes/msg.py b/extract_msg/msg_classes/msg.py index b62bacd4..8c2616e5 100644 --- a/extract_msg/msg_classes/msg.py +++ b/extract_msg/msg_classes/msg.py @@ -253,10 +253,7 @@ def _getPropertyAs(self, propertyName, overrideClass = None, preserveNone : bool :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: - value = self.props[propertyName].value - except (KeyError, AttributeError): - value = None + value = self.props.getValue(propertyName) # Check if we should be overriding the data type for this instance. if overrideClass is not None: if (value is not None or not preserveNone): @@ -777,11 +774,7 @@ def areStringsUnicode(self) -> bool: """ Returns a boolean telling if the strings are unicode encoded. """ - if '340D0003' in self.props: - if (self.props['340D0003'].value & 0x40000) != 0: - return True - - return False + return (self.props.getValue('340D0003', 0) & 0x40000) != 0 @functools.cached_property def attachments(self) -> Union[List[AttachmentBase], List[SignedAttachment]]: @@ -1033,7 +1026,7 @@ def stringEncoding(self): logger.warning('Encoding property not found. Defaulting to ISO-8859-15.') self.__stringEncoding = 'iso-8859-15' else: - enc = self.props['3FFD0003'].value + enc = cast(int, self.props['3FFD0003'].value) # Now we just need to translate that value. self.__stringEncoding = lookupCodePage(enc) return self.__stringEncoding diff --git a/extract_msg/msg_classes/task_request.py b/extract_msg/msg_classes/task_request.py index 0a19c9c1..b08394a4 100644 --- a/extract_msg/msg_classes/task_request.py +++ b/extract_msg/msg_classes/task_request.py @@ -60,7 +60,7 @@ def processed(self) -> bool: Indicates whether a client has already processed a received task communication. """ - return self._getPropertyAs('7D01000B', bool, False) + return bool(self.props.getValue('7D01000B')) @functools.cached_property def taskMode(self) -> Optional[TaskMode]: diff --git a/extract_msg/properties/named.py b/extract_msg/properties/named.py index f2777f3a..a0889f43 100644 --- a/extract_msg/properties/named.py +++ b/extract_msg/properties/named.py @@ -53,7 +53,7 @@ def __init__(self, msg : MSGFile): self.entryStream = entryStream self.namesStream = self.getStream('__substg1.0_00040102') - self.__propertiesDict : Dict[Tuple[str, str], NamedPropertyBase]= {} + self.__propertiesDict : Dict[Tuple[str, str], NamedPropertyBase] = {} self.__properties : List[NamedPropertyBase] = [] # Check that we even have any entries. If there are none, nothing to do. @@ -251,7 +251,7 @@ def __getitem__(self, item): else: return source._getTypedData(self.__named[item].propertyStreamID) - def get(self, item, default = None): + def get(self, item, default : _T = None) -> Union[Any, _T]: """ Get a named property, returning the value of :param default: if not found. Item must be a tuple with 2 items: the name and the GUID string. @@ -317,7 +317,8 @@ def rawEntryStream(self) -> bytes: @abc.abstractmethod def type(self) -> NamedPropertyType: """ - The type of named property. + Returns the type of the named property. This will be a member of the + NamedPropertyType enum. """ @@ -371,7 +372,8 @@ def streamID(self) -> int: @property def type(self) -> NamedPropertyType: """ - Returns the type of the named property. This will either be NUMERICAL_NAMED or STRING_NAMED. + Returns the type of the named property. This will be a member of the + NamedPropertyType enum. """ return NamedPropertyType.STRING_NAMED @@ -380,7 +382,7 @@ def type(self) -> NamedPropertyType: class NumericalNamedProperty(NamedPropertyBase): def __init__(self, entry : Dict): super().__init__(entry) - self.__propertyID = properHex(entry['id'], 4).upper() + self.__propertyID = f'{entry["id"]:04X}' self.__streamID = 0x1000 + (entry['id'] ^ (self.guidIndex << 1)) % 0x1F @property @@ -393,13 +395,14 @@ def propertyID(self) -> str: @property def streamID(self) -> int: """ - Returns the streamID of the named property. This may not be accurate + Returns the streamID of the named property. This may not be accurate. """ return self.__streamID @property def type(self) -> NamedPropertyType: """ - Returns the type of the named property. This will either be NUMERICAL_NAMED or STRING_NAMED. + Returns the type of the named property. This will be a member of the + NamedPropertyType enum. """ return NamedPropertyType.NUMERICAL_NAMED diff --git a/extract_msg/properties/prop.py b/extract_msg/properties/prop.py index d71b060b..663a1ed3 100644 --- a/extract_msg/properties/prop.py +++ b/extract_msg/properties/prop.py @@ -20,7 +20,7 @@ from .. import constants from ..enums import ErrorCode, ErrorCodeType -from ..utils import filetimeToDatetime, properHex +from ..utils import filetimeToDatetime logger = logging.getLogger(__name__) @@ -34,7 +34,7 @@ def createProp(data : bytes) -> PropBase: else: if temp not in constants.VARIABLE_LENGTH_PROPS: # DEBUG. - logger.warning(f'Unknown property type: {properHex(temp)}') + logger.warning(f'Unknown property type: {temp:04X}') return VariableLengthProp(data) @@ -165,8 +165,7 @@ def parseType(self, _type : int, stream : bytes) -> Any: try: value = filetimeToDatetime(rawTime) except ValueError as e: - logger.exception(e) - logger.error(self.rawData) + logger.exception(self.rawData) elif _type == 0x0048: # PtypGuid # TODO parsing for this pass diff --git a/extract_msg/properties/properties_store.py b/extract_msg/properties/properties_store.py index e0b5813a..24b4b6db 100644 --- a/extract_msg/properties/properties_store.py +++ b/extract_msg/properties/properties_store.py @@ -9,12 +9,12 @@ import pprint from typing import ( - Dict, Iterable, Iterator, List, Optional, Tuple, TypeVar, Union + Any, Dict, Iterable, Iterator, List, Optional, Tuple, TypeVar, Union ) from .. import constants from ..enums import Intelligence, PropertiesType -from .prop import createProp, PropBase +from .prop import createProp, FixedLengthProp, PropBase from ..utils import divide @@ -174,6 +174,29 @@ def getProperties(self, id_ : Union[str, int]) -> List[PropBase]: return [self[x] for x in self.__idMapping.get(id_, [])] + def getValue(self, name : Union[str, int], default : _T = None) -> Union[Any, _T]: + """ + Attempts to get the first property + """ + if isinstance(name, int): + if name >= 0x10000: + name = f'{name:08X}' + else: + name = f'{name:04X}' + if len(name) == 4: + for prop in self.getProperties(name): + if isinstance(prop, FixedLengthProp): + return prop.value + return default + elif len(name) == 8: + if (prop := self.get()): + if isinstance(prop, FixedLengthProp): + return prop.value + else: + return default + else: + raise ValueError('Property name must be an int less than 0x100000000, a 4 character hex string, or an 8 character hex string.') + def items(self) -> Iterable[Tuple[str, PropBase]]: return self.__props.items() diff --git a/extract_msg/recipient.py b/extract_msg/recipient.py index d8f5700b..946c8269 100644 --- a/extract_msg/recipient.py +++ b/extract_msg/recipient.py @@ -9,7 +9,7 @@ import functools import logging -from typing import Optional, Tuple, TYPE_CHECKING, Union +from typing import List, Optional, Tuple, TYPE_CHECKING, Union from .enums import ErrorBehavior, MeetingRecipientType, PropertiesType, RecipientType from .exceptions import StandardViolationError @@ -44,7 +44,7 @@ def __init__(self, _dir, msg : MSGFile): if not self.__email: self.__email = self.getStringStream('__substg1.0_3003') self.__name = self.getStringStream('__substg1.0_3001') - self.__typeFlags = self.__props.get('0C150003').value or 0 + self.__typeFlags = self.__props.getValue('0C150003') or 0 from .msg_classes.calendar_base import CalendarBase if isinstance(msg, CalendarBase): self.__type = MeetingRecipientType(0xF & self.__typeFlags) @@ -262,6 +262,74 @@ def existsTypedProperty(self, id, _type = None) -> bool: raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') return msg.existsTypedProperty(id, self.__dir, _type, True, self.__props) + def getMultipleBinary(self, filename) -> Optional[List[bytes]]: + """ + Gets a multiple binary property as a list of bytes objects. + + Like :method getStringStream:, the 4 character type suffix should be + omitted. So if you want the stream "__substg1.0_00011102" then the + filename would simply be "__substg1.0_0001". + + :raises ReferenceError: The associated MSGFile instance has been garbage + collected. + """ + if (msg := self.__msg()) is None: + raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') + return msg.getMultipleBinary([self.__dir, filename]) + + def getMultipleString(self, filename) -> Optional[List[str]]: + """ + Gets a multiple string property as a list of str objects. + + Like :method getStringStream:, the 4 character type suffix should be + omitted. So if you want the stream "__substg1.0_00011102" then the + filename would simply be "__substg1.0_0001". + + :raises ReferenceError: The associated MSGFile instance has been garbage + collected. + """ + if (msg := self.__msg()) is None: + raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') + return msg.getMultipleString([self.__dir, filename]) + + def getSingleOrMultipleBinary(self, filename) -> Optional[Union[List[bytes], bytes]]: + """ + A combination of :method getStringStream: and + :method getMultipleString:. + + Checks to see if a single binary stream exists to return, otherwise + tries to return the multiple binary stream of the same ID. + + Like :method getStringStream:, the 4 character type suffix should be + omitted. So if you want the stream "__substg1.0_00010102" then the + filename would simply be "__substg1.0_0001". + + :raises ReferenceError: The associated MSGFile instance has been garbage + collected. + """ + if (msg := self.__msg()) is None: + raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') + return msg.getSingleOrMultipleBinary([self.__dir, filename]) + + def getSingleOrMultipleString(self, filename) -> Optional[Union[List[str], str]]: + """ + A combination of :method getStringStream: and + :method getMultipleString:. + + Checks to see if a single string stream exists to return, otherwise + tries to return the multiple string stream of the same ID. + + Like :method getStringStream:, the 4 character type suffix should be + omitted. So if you want the stream "__substg1.0_0001001F" then the + filename would simply be "__substg1.0_0001". + + :raises ReferenceError: The associated MSGFile instance has been garbage + collected. + """ + if (msg := self.__msg()) is None: + raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') + return msg.getSingleOrMultipleString([self.__dir, filename]) + def getStream(self, filename) -> Optional[bytes]: """ Gets a binary representation of the requested filename. diff --git a/extract_msg/utils.py b/extract_msg/utils.py index 3999177d..857f94dd 100644 --- a/extract_msg/utils.py +++ b/extract_msg/utils.py @@ -74,8 +74,8 @@ from html import escape as htmlEscape from typing import ( - Any, Callable, Dict, List, Optional, Sequence, TypeVar, TYPE_CHECKING, - Union + Any, Callable, Dict, Iterable, List, Optional, Sequence, TypeVar, + TYPE_CHECKING, Union ) from . import constants @@ -622,14 +622,12 @@ def makeWeakRef(obj : Optional[_T]) -> Optional[weakref.ReferenceType[_T]]: return None -def msgPathToString(inp) -> str: +def msgPathToString(inp : Union[str, Iterable[str]]) -> str: """ Converts an MSG path (one of the internal paths inside an MSG file) into a string. """ - if inp is None: - return None - if isinstance(inp, (list, tuple)): + if not isinstance(inp, str): inp = '/'.join(inp) inp.replace('\\', '/') return inp @@ -825,7 +823,7 @@ def rtfSanitizeHtml(inp : str) -> str: output += char elif ord(char) < 32 or 128 <= ord(char) <= 255: # Otherwise, see if it is just a small escape. - output += "\\'" + properHex(char, 2) + output += f"\\'{ord(char):02X}" else: # Handle Unicode characters. enc = char.encode('utf-16-le') @@ -849,7 +847,7 @@ def rtfSanitizePlain(inp : str) -> str: output += char elif ord(char) < 32 or 128 <= ord(char) <= 255: # Otherwise, see if it is just a small escape. - output += "\\'" + properHex(char, 2) + output += f"\\'{ord(char):02X}" else: # Handle Unicode characters. # Handle Unicode characters. @@ -952,9 +950,8 @@ def tryGetMimetype(att, mimetype : Union[str, None]) -> Union[str, None]: if mimetype: return mimetype - # We only try anything if it is a plain attachment or signed attachment. - # Web attachments and embedded MSG files are completely ignored. - if att.type in (AttachmentType.DATA, AttachmentType.SIGNED): + # We only try anything if the data is bytes. + if att.dataType: # Try to import our dependency module to use it. try: import magic From b1be06d094d268f9d6db57e540607d9c6ba5f0a6 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 9 Aug 2023 15:14:00 -0700 Subject: [PATCH 12/20] Bump version --- README.rst | 4 ++-- extract_msg/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index f19f4c49..0e15f44f 100644 --- a/README.rst +++ b/README.rst @@ -259,8 +259,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.44.0-blue.svg - :target: https://pypi.org/project/extract-msg/0.44.0/ +.. |PyPI3| image:: https://img.shields.io/badge/pypi-0.45.0-blue.svg + :target: https://pypi.org/project/extract-msg/0.45.0/ .. |PyPI2| image:: https://img.shields.io/badge/python-3.8+-brightgreen.svg :target: https://www.python.org/downloads/release/python-3816/ diff --git a/extract_msg/__init__.py b/extract_msg/__init__.py index 06618a9e..cbf2b3e3 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__ = '2023-08-03' -__version__ = '0.44.0' +__date__ = '2023-08-09' +__version__ = '0.45.0' __all__ = [ # Modules: From d625ae04c8c02c75d32b2c1bd089913e9c075528 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 9 Aug 2023 15:19:35 -0700 Subject: [PATCH 13/20] Reduce reliance on private api for many functions --- extract_msg/attachments/attachment_base.py | 8 ++++---- extract_msg/msg_classes/calendar_base.py | 14 +++++++------- extract_msg/recipient.py | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/extract_msg/attachments/attachment_base.py b/extract_msg/attachments/attachment_base.py index ab092bd1..473d13fb 100644 --- a/extract_msg/attachments/attachment_base.py +++ b/extract_msg/attachments/attachment_base.py @@ -568,7 +568,7 @@ def exceptionReplaceTime(self) -> Optional[datetime.datetime]: Only applicable if the attachment is an Exception object. """ - return self._getPropertyAs('7FF90040') + return self.props.getValue('7FF90040') @functools.cached_property def extension(self) -> Optional[str]: @@ -582,14 +582,14 @@ def hidden(self) -> bool: """ Indicates whether an Attachment object is hidden from the end user. """ - return self._getPropertyAs('7FFE000B', bool, False) + return bool(self.props.getValue('7FFE000B')) @functools.cached_property def isAttachmentContactPhoto(self) -> bool: """ Whether the attachment is a contact photo for a Contact object. """ - return self._getPropertyAs('7FFF000B', bool, False) + return bool(self.props.getValue('7FFF000B')) @functools.cached_property def longFilename(self) -> Optional[str]: @@ -663,7 +663,7 @@ def renderingPosition(self) -> Optional[int]: within the main message text. A value of 0xFFFFFFFF indicates a hidden attachment that is not to be rendered. """ - return self._getPropertyAs('370B0003') + return self.props.getValue('370B0003') @property def shortFilename(self) -> Optional[str]: diff --git a/extract_msg/msg_classes/calendar_base.py b/extract_msg/msg_classes/calendar_base.py index 4b35f261..5c231466 100644 --- a/extract_msg/msg_classes/calendar_base.py +++ b/extract_msg/msg_classes/calendar_base.py @@ -395,7 +395,7 @@ def ownerCriticalChange(self) -> Optional[datetime.datetime]: The date and time at which a Meeting Request object was sent by the organizer, in UTC. """ - return self._getNamedAs('001A', ps.PSETID_MEETING) + return self.namedProperties.get(('001A', ps.PSETID_MEETING)) @functools.cached_property def recurrencePattern(self) -> Optional[str]: @@ -403,14 +403,14 @@ def recurrencePattern(self) -> Optional[str]: A description of the recurrence specified by the appointmentRecur property. """ - return self._getNamedAs('8232', ps.PSETID_APPOINTMENT) + return self.namedProperties.get(('8232', ps.PSETID_APPOINTMENT)) @functools.cached_property def recurring(self) -> bool: """ Specifies whether the object represents a recurring series. """ - return self._getNamedAs('8223', ps.PSETID_APPOINTMENT, bool, True) + return bool(self.namedProperties.get(('8223', ps.PSETID_APPOINTMENT), False)) @functools.cached_property def replyRequested(self) -> bool: @@ -424,14 +424,14 @@ def requiredAttendees(self) -> Optional[str]: """ Returns the required attendees of the meeting. """ - return self._getNamedAs('0006', ps.PSETID_MEETING) + return self.namedProperties.get(('0006', ps.PSETID_MEETING)) @functools.cached_property def resourceAttendees(self) -> Optional[str]: """ Returns the resource attendees of the meeting. """ - return self._getNamedAs('0008', ps.PSETID_MEETING) + return self.namedProperties.get(('0008', ps.PSETID_MEETING)) @functools.cached_property def responseRequested(self) -> bool: @@ -460,7 +460,7 @@ def timeZoneDescription(self) -> Optional[str]: A human-readable description of the time zone that is represented by the data in the timeZoneStruct property. """ - return self._getNamedAs('8234', ps.PSETID_APPOINTMENT) + return self.namedProperties.get(('8234', ps.PSETID_APPOINTMENT)) @functools.cached_property def timeZoneStruct(self) -> Optional[TimeZoneStruct]: @@ -482,4 +482,4 @@ def toAttendeesString(self) -> Optional[str]: """ A list of all the sendable attendees, who are also required attendees. """ - return self._getNamedAs('823B', ps.PSETID_APPOINTMENT) + return self.namedProperties.get(('823B', ps.PSETID_APPOINTMENT)) diff --git a/extract_msg/recipient.py b/extract_msg/recipient.py index 946c8269..d15fafb3 100644 --- a/extract_msg/recipient.py +++ b/extract_msg/recipient.py @@ -44,7 +44,7 @@ def __init__(self, _dir, msg : MSGFile): if not self.__email: self.__email = self.getStringStream('__substg1.0_3003') self.__name = self.getStringStream('__substg1.0_3001') - self.__typeFlags = self.__props.getValue('0C150003') or 0 + self.__typeFlags = self.__props.getValue('0C150003', 0) from .msg_classes.calendar_base import CalendarBase if isinstance(msg, CalendarBase): self.__type = MeetingRecipientType(0xF & self.__typeFlags) From 2c715516e9e031af5ab9b61d164d56dbcf503f25 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 9 Aug 2023 15:31:01 -0700 Subject: [PATCH 14/20] More progress --- extract_msg/attachments/__init__.py | 2 +- extract_msg/attachments/attachment.py | 2 +- extract_msg/attachments/attachment_base.py | 23 ++++++++++++---------- extract_msg/attachments/custom_att.py | 2 +- extract_msg/attachments/emb_msg_att.py | 4 ++-- extract_msg/msg_classes/msg.py | 2 +- extract_msg/properties/named.py | 6 +++--- extract_msg/recipient.py | 20 +++++++++---------- 8 files changed, 32 insertions(+), 29 deletions(-) diff --git a/extract_msg/attachments/__init__.py b/extract_msg/attachments/__init__.py index d2a18ce8..29cceaf0 100644 --- a/extract_msg/attachments/__init__.py +++ b/extract_msg/attachments/__init__.py @@ -48,7 +48,7 @@ _logger.addHandler(_logging.NullHandler()) -def initStandardAttachment(msg : MSGFile, dir_) -> AttachmentBase: +def initStandardAttachment(msg : MSGFile, dir_ : str) -> AttachmentBase: """ Returns an instance of AttachmentBase for the attachment in the MSG file at the specified internal directory. diff --git a/extract_msg/attachments/attachment.py b/extract_msg/attachments/attachment.py index b3b6a38c..2740b708 100644 --- a/extract_msg/attachments/attachment.py +++ b/extract_msg/attachments/attachment.py @@ -35,7 +35,7 @@ class Attachment(AttachmentBase): A standard data attachment of an MSG file. """ - def __init__(self, msg : MSGFile, dir_, propStore : PropertiesStore): + def __init__(self, msg : MSGFile, dir_ : str, propStore : PropertiesStore): """ :param msg: The MSGFile instance that the attachment belongs to. :param dir_: The directory inside the MSG file where the attachment is diff --git a/extract_msg/attachments/attachment_base.py b/extract_msg/attachments/attachment_base.py index 473d13fb..3047fc93 100644 --- a/extract_msg/attachments/attachment_base.py +++ b/extract_msg/attachments/attachment_base.py @@ -22,7 +22,10 @@ from ..properties.named import NamedProperties from ..properties.prop import FixedLengthProp from ..properties.properties_store import PropertiesStore -from ..utils import makeWeakRef, tryGetMimetype, verifyPropertyId, verifyType +from ..utils import ( + makeWeakRef, msgPathToString, tryGetMimetype, verifyPropertyId, + verifyType + ) # Allow for nice type checking. @@ -38,7 +41,7 @@ class AttachmentBase(abc.ABC): The base class for all Attachments used by the module, if not overriden. """ - def __init__(self, msg : MSGFile, dir_, propStore : PropertiesStore): + def __init__(self, msg : MSGFile, dir_ : str, propStore : PropertiesStore): """ :param msg: the Message instance that the attachment belongs to. :param dir_: the directory inside the msg file where the attachment is located. @@ -248,7 +251,7 @@ def _getTypedStream(self, filename, _type = None): """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') - return msg._getTypedStream([self.__dir, filename], True, _type) + return msg._getTypedStream([self.__dir, msgPathToString(filename)], True, _type) def _handleFnc(self, _zip, filename, customPath, kwargs) -> pathlib.Path: """ @@ -301,7 +304,7 @@ def exists(self, filename) -> bool: """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') - return msg.exists([self.__dir, filename]) + return msg.exists([self.__dir, msgPathToString(filename)]) def sExists(self, filename) -> bool: """ @@ -340,7 +343,7 @@ def getMultipleBinary(self, filename) -> Optional[List[bytes]]: """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') - return msg.getMultipleBinary([self.__dir, filename]) + return msg.getMultipleBinary([self.__dir, msgPathToString(filename)]) def getMultipleString(self, filename) -> Optional[List[str]]: """ @@ -355,7 +358,7 @@ def getMultipleString(self, filename) -> Optional[List[str]]: """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') - return msg.getMultipleString([self.__dir, filename]) + return msg.getMultipleString([self.__dir, msgPathToString(filename)]) def getSingleOrMultipleBinary(self, filename) -> Optional[Union[List[bytes], bytes]]: """ @@ -374,7 +377,7 @@ def getSingleOrMultipleBinary(self, filename) -> Optional[Union[List[bytes], byt """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') - return msg.getSingleOrMultipleBinary([self.__dir, filename]) + return msg.getSingleOrMultipleBinary([self.__dir, msgPathToString(filename)]) def getSingleOrMultipleString(self, filename) -> Optional[Union[List[str], str]]: """ @@ -393,7 +396,7 @@ def getSingleOrMultipleString(self, filename) -> Optional[Union[List[str], str]] """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') - return msg.getSingleOrMultipleString([self.__dir, filename]) + return msg.getSingleOrMultipleString([self.__dir, msgPathToString(filename)]) def getStream(self, filename) -> Optional[bytes]: """ @@ -407,7 +410,7 @@ def getStream(self, filename) -> Optional[bytes]: """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') - return msg.getStream([self.__dir, filename]) + return msg.getStream([self.__dir, msgPathToString(filename)]) def getStringStream(self, filename) -> Optional[str]: """ @@ -422,7 +425,7 @@ def getStringStream(self, filename) -> Optional[str]: """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') - return msg.getStringStream([self.__dir, filename]) + return msg.getStringStream([self.__dir, msgPathToString(filename)]) @abc.abstractmethod def getFilename(self, **kwargs) -> str: diff --git a/extract_msg/attachments/custom_att.py b/extract_msg/attachments/custom_att.py index 9391d5c2..603ff9ce 100644 --- a/extract_msg/attachments/custom_att.py +++ b/extract_msg/attachments/custom_att.py @@ -34,7 +34,7 @@ class CustomAttachment(AttachmentBase): The attachment entry for custom attachments. """ - def __init__(self, msg : MSGFile, dir_, propStore : PropertiesStore): + def __init__(self, msg : MSGFile, dir_ : str, propStore : PropertiesStore): super().__init__(msg, dir_, propStore) self.__customHandler = getHandler(self) diff --git a/extract_msg/attachments/emb_msg_att.py b/extract_msg/attachments/emb_msg_att.py index d5b1728f..b93c09c4 100644 --- a/extract_msg/attachments/emb_msg_att.py +++ b/extract_msg/attachments/emb_msg_att.py @@ -32,8 +32,8 @@ class EmbeddedMsgAttachment(AttachmentBase): The attachment entry for an Embedded MSG file. """ - def __init__(self, msg : MSGFile, dir_, propertiesStore : PropertiesStore): - super().__init__(msg, dir_, propertiesStore) + def __init__(self, msg : MSGFile, dir_ : str, propStore : PropertiesStore): + super().__init__(msg, dir_, propStore) self.__prefix = msg.prefixList + [dir_, '__substg1.0_3701000D'] self.__data = openMsg(self.msg.path, prefix = self.__prefix, parentMsg = self.msg, treePath = self.treePath, **self.msg.kwargs) diff --git a/extract_msg/msg_classes/msg.py b/extract_msg/msg_classes/msg.py index 8c2616e5..36de4132 100644 --- a/extract_msg/msg_classes/msg.py +++ b/extract_msg/msg_classes/msg.py @@ -883,7 +883,7 @@ def importanceString(self) -> Union[str, None]: }[self.importance] @property - def initAttachmentFunc(self) -> Callable[[MSGFile, Any], AttachmentBase]: + def initAttachmentFunc(self) -> Callable[[MSGFile, str], AttachmentBase]: """ Returns the method for initializing attachments being used, should you need to use it externally for whatever reason. diff --git a/extract_msg/properties/named.py b/extract_msg/properties/named.py index a0889f43..490e52ff 100644 --- a/extract_msg/properties/named.py +++ b/extract_msg/properties/named.py @@ -22,7 +22,7 @@ from .. import constants from ..enums import NamedPropertyType -from ..utils import bytesToGuid, divide, makeWeakRef, properHex +from ..utils import bytesToGuid, divide, makeWeakRef, msgPathToString from compressed_rtf.crc32 import crc32 @@ -152,7 +152,7 @@ def exists(self, filename) -> bool: """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Named instance has been garbage collected.') - return msg.exists([self.__dir, filename], False) + return msg.exists([self.__dir, msgPathToString(filename)], False) def get(self, propertyName : Tuple[str, str], default : _T = None) -> Union[NamedPropertyBase, _T]: """ @@ -176,7 +176,7 @@ def getStream(self, filename) -> Optional[bytes]: """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Named instance has been garbage collected.') - return msg.getStream([self.__dir, filename], False) + return msg.getStream([self.__dir, msgPathToString(filename)], False) def items(self) -> Iterable[Tuple[Tuple[str, str], NamedPropertyBase]]: return self.__propertiesDict.items() diff --git a/extract_msg/recipient.py b/extract_msg/recipient.py index d15fafb3..a27e0e98 100644 --- a/extract_msg/recipient.py +++ b/extract_msg/recipient.py @@ -16,7 +16,7 @@ from .properties.prop import FixedLengthProp from .properties.properties_store import PropertiesStore from .structures.entry_id import PermanentEntryID -from .utils import makeWeakRef, verifyPropertyId, verifyType +from .utils import makeWeakRef, msgPathToString, verifyPropertyId, verifyType if TYPE_CHECKING: @@ -225,7 +225,7 @@ def _getTypedStream(self, filename, _type = None): """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') - return msg._getTypedStream(self, [self.__dir, filename], True, _type) + return msg._getTypedStream(self, [self.__dir, msgPathToString(filename)], True, _type) def exists(self, filename) -> bool: """ @@ -236,7 +236,7 @@ def exists(self, filename) -> bool: """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') - return msg.exists([self.__dir, filename]) + return msg.exists([self.__dir, msgPathToString(filename)]) def sExists(self, filename) -> bool: """ @@ -247,7 +247,7 @@ def sExists(self, filename) -> bool: """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') - return msg.sExists([self.__dir, filename]) + return msg.sExists([self.__dir, msgPathToString(filename)]) def existsTypedProperty(self, id, _type = None) -> bool: """ @@ -275,7 +275,7 @@ def getMultipleBinary(self, filename) -> Optional[List[bytes]]: """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') - return msg.getMultipleBinary([self.__dir, filename]) + return msg.getMultipleBinary([self.__dir, msgPathToString(filename)]) def getMultipleString(self, filename) -> Optional[List[str]]: """ @@ -290,7 +290,7 @@ def getMultipleString(self, filename) -> Optional[List[str]]: """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') - return msg.getMultipleString([self.__dir, filename]) + return msg.getMultipleString([self.__dir, msgPathToString(filename)]) def getSingleOrMultipleBinary(self, filename) -> Optional[Union[List[bytes], bytes]]: """ @@ -309,7 +309,7 @@ def getSingleOrMultipleBinary(self, filename) -> Optional[Union[List[bytes], byt """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') - return msg.getSingleOrMultipleBinary([self.__dir, filename]) + return msg.getSingleOrMultipleBinary([self.__dir, msgPathToString(filename)]) def getSingleOrMultipleString(self, filename) -> Optional[Union[List[str], str]]: """ @@ -328,7 +328,7 @@ def getSingleOrMultipleString(self, filename) -> Optional[Union[List[str], str]] """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') - return msg.getSingleOrMultipleString([self.__dir, filename]) + return msg.getSingleOrMultipleString([self.__dir, msgPathToString(filename)]) def getStream(self, filename) -> Optional[bytes]: """ @@ -342,7 +342,7 @@ def getStream(self, filename) -> Optional[bytes]: """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') - return msg.getStream([self.__dir, filename]) + return msg.getStream([self.__dir, msgPathToString(filename)]) def getStringStream(self, filename) -> Optional[str]: """ @@ -360,7 +360,7 @@ def getStringStream(self, filename) -> Optional[str]: """ if (msg := self.__msg()) is None: raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') - return msg.getStringStream([self.__dir, filename]) + return msg.getStringStream([self.__dir, msgPathToString(filename)]) @functools.cached_property def account(self) -> Optional[str]: From b74282ba68ff6e2ba1c084145996bffe20d0c58b Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 9 Aug 2023 17:28:11 -0700 Subject: [PATCH 15/20] More adjustments to public and private API --- CHANGELOG.md | 3 + extract_msg/msg_classes/calendar_base.py | 79 +++++++------ extract_msg/msg_classes/contact.py | 124 ++++++++++----------- extract_msg/msg_classes/meeting_related.py | 10 +- extract_msg/msg_classes/msg.py | 42 +++++-- extract_msg/msg_classes/task_request.py | 10 +- 6 files changed, 144 insertions(+), 124 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f25861b3..a26107db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,9 +22,12 @@ * `getSingleOrMultipleBinary`: A combination of `getStream` and `getMultipleBinary` which prefers a single binary stream. Returns a single `bytes` object or a list of `bytes` objects. * `getMultipleString`: Gets a multiple string property as a list of `str` objects. * `getSingleOrMultipleString`: A combination of `getStringStream` and `getMultipleString` which prefers a single string stream. Returns a single bytes objecct or a list of bytes objects. + * `getPropertyVal`: Shortcut for `instance.props.getValue` that allows new behavior to be added by overriding it. + * `getNamedProp`: Shortcut for `instance.namedProperties.get((propertyName, guid), default)` that allows new behavior to be added by overriding it. * Removed `Named._getStringStream` and `Named.sExists`. The named properties storage will *always* * Changed all `Named` methods to no longer have a prefix argument. The prefix should *always* be false sense the named property mapping will only exist in the top level directory. * Adjusted `tryGetMimeType` to allows any attachments whose `data` property would return a `bytes` instance. +* Changed internal code to use public SPI functions wherever possible. This includes making many private API functions use calls to the public API for getting bits of data. **v0.44.0** * Fixed a bug that caused `MessageBase.headerInit` to always return `False` after the 0.42.0 update. diff --git a/extract_msg/msg_classes/calendar_base.py b/extract_msg/msg_classes/calendar_base.py index 5c231466..65c78162 100644 --- a/extract_msg/msg_classes/calendar_base.py +++ b/extract_msg/msg_classes/calendar_base.py @@ -33,7 +33,7 @@ def allAttendeesString(self) -> Optional[str]: """ A list of all attendees, excluding the organizer. """ - return self._getNamedAs('8238', ps.PSETID_APPOINTMENT) + return self.getNamedProp('8238', ps.PSETID_APPOINTMENT) @functools.cached_property def appointmentAuxilaryFlags(self) -> Optional[AppointmentAuxilaryFlag]: @@ -54,14 +54,14 @@ def appointmentDuration(self) -> Optional[int]: """ The length of the event, in minutes. """ - return self._getNamedAs('8213', ps.PSETID_APPOINTMENT) + return self.getNamedProp('8213', ps.PSETID_APPOINTMENT) @functools.cached_property def appointmentEndWhole(self) -> Optional[datetime.datetime]: """ The end date and time of the event in UTC. """ - return self._getNamedAs('820E', ps.PSETID_APPOINTMENT) + return self.getNamedProp('820E', ps.PSETID_APPOINTMENT) @functools.cached_property def appointmentNotAllowPropose(self) -> bool: @@ -69,7 +69,7 @@ def appointmentNotAllowPropose(self) -> bool: Indicates that attendees are not allowed to propose a new date and/or time for the meeting if True. """ - return self._getNamedAs('8259', ps.PSETID_APPOINTMENT, bool, False) + return bool(self.getNamedProp('8259', ps.PSETID_APPOINTMENT)) @functools.cached_property def appointmentRecur(self) -> Optional[RecurrencePattern]: @@ -86,14 +86,14 @@ def appointmentSequence(self) -> Optional[int]: begins with the sequence number set to 0 and is incremented each time the organizer sends out a Meeting Update object. """ - return self._getNamedAs('8201', ps.PSETID_APPOINTMENT) + return self.getNamedProp('8201', ps.PSETID_APPOINTMENT) @functools.cached_property def appointmentStartWhole(self) -> Optional[datetime.datetime]: """ The start date and time of the event in UTC. """ - return self._getNamedAs('820D', ps.PSETID_APPOINTMENT) + return self.getNamedProp('820D', ps.PSETID_APPOINTMENT) @functools.cached_property def appointmentStateFlags(self) -> Optional[AppointmentStateFlag]: @@ -107,7 +107,7 @@ def appointmentSubType(self) -> bool: """ Whether the event is an all-day event or not. """ - return self._getNamedAs('8215', ps.PSETID_APPOINTMENT, bool, False) + return bool(self.getNamedProp('8215', ps.PSETID_APPOINTMENT)) @functools.cached_property def appointmentTimeZoneDefinitionEndDisplay(self) -> Optional[TimeZoneDefinition]: @@ -142,7 +142,7 @@ def appointmentUnsendableRecipients(self) -> Optional[bytes]: the specifications. If you have examples, let me know and I can ask you to run a verification on it. """ - return self._getNamedAs('825D', ps.PSETID_APPOINTMENT) + return self.getNamedProp('825D', ps.PSETID_APPOINTMENT) @functools.cached_property def bcc(self) -> Optional[str]: @@ -156,7 +156,7 @@ def birthdayContactAttributionDisplayName(self) -> Optional[str]: """ Indicated the name of the contact associated with the birthday event. """ - return self._getNamedAs('BirthdayContactAttributionDisplayName', ps.PSETID_ADDRESS) + return self.getNamedProp('BirthdayContactAttributionDisplayName', ps.PSETID_ADDRESS) @functools.cached_property def birthdayContactEntryID(self) -> Optional[EntryID]: @@ -171,7 +171,7 @@ def birthdayContactPersonGuid(self) -> Optional[bytes]: Indicates the person ID's GUID of the contact associated with the birthday event. """ - return self._getNamedAs('BirthdayContactPersonGuid', ps.PSETID_ADDRESS) + return self.getNamedProp('BirthdayContactPersonGuid', ps.PSETID_ADDRESS) @functools.cached_property def busyStatus(self) -> Optional[BusyStatus]: @@ -193,7 +193,7 @@ def ccAttendeesString(self) -> Optional[str]: """ A list of all the sendable attendees, who are also optional attendees. """ - return self._getNamedAs('823C', ps.PSETID_APPOINTMENT) + return self.getNamedProp('823C', ps.PSETID_APPOINTMENT) @functools.cached_property def cleanGlobalObjectID(self) -> Optional[GlobalObjectID]: @@ -215,7 +215,7 @@ def clipEnd(self) -> Optional[datetime.datetime]: Honestly, not sure what this is. [MS-OXOCAL]: PidLidClipEnd. """ - return self._getNamedAs('8236', ps.PSETID_APPOINTMENT) + return self.getNamedProp('8236', ps.PSETID_APPOINTMENT) @functools.cached_property def clipStart(self) -> Optional[datetime.datetime]: @@ -226,14 +226,14 @@ def clipStart(self) -> Optional[datetime.datetime]: Honestly, not sure what this is. [MS-OXOCAL]: PidLidClipStart. """ - return self._getNamedAs('8235', ps.PSETID_APPOINTMENT) + return self.getNamedProp('8235', ps.PSETID_APPOINTMENT) @functools.cached_property def commonEnd(self) -> Optional[datetime.datetime]: """ The end date and time of an event. MUST be equal to appointmentEndWhole. """ - return self._getNamedAs('8517', ps.PSETID_COMMON) + return self.getNamedProp('8517', ps.PSETID_COMMON) @functools.cached_property def commonStart(self) -> Optional[datetime.datetime]: @@ -241,14 +241,14 @@ def commonStart(self) -> Optional[datetime.datetime]: The start date and time of an event. MUST be equal to appointmentStartWhole. """ - return self._getNamedAs('8516', ps.PSETID_COMMON) + return self.getNamedProp('8516', ps.PSETID_COMMON) @functools.cached_property def endDate(self) -> Optional[datetime.datetime]: """ The end date of the appointment. """ - return self.props.getValue('00610040') + return self.getPropertyVal('00610040') @functools.cached_property def globalObjectID(self) -> Optional[GlobalObjectID]: @@ -270,29 +270,28 @@ def isBirthdayContactWritable(self) -> bool: Indicates whether the contact associated with the birthday event is writable. """ - return self._getNamedAs('IsBirthdayContactWritable', ps.PSETID_ADDRESS, bool, False) - + return bool(self.getNamedProp('IsBirthdayContactWritable', ps.PSETID_ADDRESS)) @functools.cached_property def isException(self) -> bool: """ Whether the object represents an exception. False indicates that the object represents a recurring series or a single-instance object. """ - return self._getNamedAs('000A', ps.PSETID_MEETING, bool, False) + return bool(self.getNamedProp('000A', ps.PSETID_MEETING)) @functools.cached_property def isRecurring(self) -> bool: """ Whether the object is associated with a recurring series. """ - return self._getNamedAs('0005', ps.PSETID_MEETING, bool, False) + return bool(self.getNamedProp('0005', ps.PSETID_MEETING)) @functools.cached_property def keywords(self) -> Optional[List[str]]: """ The color to be used when displaying a Calendar object. """ - return self._getNamedAs('Keywords', ps.PS_PUBLIC_STRINGS) + return self.getNamedProp('Keywords', ps.PS_PUBLIC_STRINGS) @functools.cached_property def linkedTaskItems(self) -> Optional[List[EntryID]]: @@ -307,14 +306,14 @@ def location(self) -> Optional[str]: """ Returns the location of the meeting. """ - return self._getNamedAs('8208', ps.PSETID_APPOINTMENT) + return self.getNamedProp('8208', ps.PSETID_APPOINTMENT) @functools.cached_property def meetingDoNotForward(self) -> bool: """ Whether to allow the meeting to be forwarded. True disallows forwarding. """ - return self._getNamedAs('DoNotForward', ps.PS_PUBLIC_STRINGS, bool, False) + return bool(self.getNamedProp('DoNotForward', ps.PS_PUBLIC_STRINGS)) @functools.cached_property def meetingWorkspaceUrl(self) -> Optional[str]: @@ -322,28 +321,28 @@ def meetingWorkspaceUrl(self) -> Optional[str]: The URL of the Meeting Workspace, as specified in [MS-MEETS], that is associated with a Calendar object. """ - return self._getNamedAs('8209', ps.PSETID_APPOINTMENT) + return self.getNamedProp('8209', ps.PSETID_APPOINTMENT) @functools.cached_property def nonSendableBcc(self) -> Optional[str]: """ A list of all unsendable attendees who are also resource objects. """ - return self._getNamedAs('8538', ps.PSETID_COMMON) + return self.getNamedProp('8538', ps.PSETID_COMMON) @functools.cached_property def nonSendableCc(self) -> Optional[str]: """ A list of all unsendable attendees who are also optional attendees. """ - return self._getNamedAs('8537', ps.PSETID_COMMON) + return self.getNamedProp('8537', ps.PSETID_COMMON) @functools.cached_property def nonSendableTo(self) -> Optional[str]: """ A list of all unsendable attendees who are also required attendees. """ - return self._getNamedAs('8536', ps.PSETID_COMMON) + return self.getNamedProp('8536', ps.PSETID_COMMON) @functools.cached_property def nonSendBccTrackStatus(self) -> Optional[List[ResponseStatus]]: @@ -371,7 +370,7 @@ def optionalAttendees(self) -> Optional[str]: """ Returns the optional attendees of the meeting. """ - return self._getNamedAs('0007', ps.PSETID_MEETING) + return self.getNamedProp('0007', ps.PSETID_MEETING) @property def organizer(self) -> Optional[str]: @@ -387,7 +386,7 @@ def ownerAppointmentID(self) -> Optional[int]: Assists a client or server in finding a Calendar object but is not guarenteed to be unique amoung all objects. """ - return self.props.getValue('00620003') + return self.getPropertyVal('00620003') @functools.cached_property def ownerCriticalChange(self) -> Optional[datetime.datetime]: @@ -395,7 +394,7 @@ def ownerCriticalChange(self) -> Optional[datetime.datetime]: The date and time at which a Meeting Request object was sent by the organizer, in UTC. """ - return self.namedProperties.get(('001A', ps.PSETID_MEETING)) + return self.getNamedProp('001A', ps.PSETID_MEETING) @functools.cached_property def recurrencePattern(self) -> Optional[str]: @@ -403,56 +402,56 @@ def recurrencePattern(self) -> Optional[str]: A description of the recurrence specified by the appointmentRecur property. """ - return self.namedProperties.get(('8232', ps.PSETID_APPOINTMENT)) + return self.getNamedProp('8232', ps.PSETID_APPOINTMENT) @functools.cached_property def recurring(self) -> bool: """ Specifies whether the object represents a recurring series. """ - return bool(self.namedProperties.get(('8223', ps.PSETID_APPOINTMENT), False)) + return bool(self.getNamedProp('8223', ps.PSETID_APPOINTMENT)) @functools.cached_property def replyRequested(self) -> bool: """ Whether the organizer requests a reply from attendees. """ - return bool(self.props.getValue('0C17000B')) + return bool(self.getPropertyVal('0C17000B')) @functools.cached_property def requiredAttendees(self) -> Optional[str]: """ Returns the required attendees of the meeting. """ - return self.namedProperties.get(('0006', ps.PSETID_MEETING)) + return self.getNamedProp('0006', ps.PSETID_MEETING) @functools.cached_property def resourceAttendees(self) -> Optional[str]: """ Returns the resource attendees of the meeting. """ - return self.namedProperties.get(('0008', ps.PSETID_MEETING)) + return self.getNamedProp('0008', ps.PSETID_MEETING) @functools.cached_property def responseRequested(self) -> bool: """ Whether to send Meeting Response objects to the organizer. """ - return bool(self.props.getValue('0063000B')) + return bool(self.getPropertyVal('0063000B')) @functools.cached_property def responseStatus(self) -> ResponseStatus: """ The response status of an attendee. """ - return ResponseStatus(self.namedProperties.get(('8218', ps.PSETID_APPOINTMENT), 0)) + return ResponseStatus(self.getNamedProp('8218', ps.PSETID_APPOINTMENT, 0)) @functools.cached_property def startDate(self) -> Optional[datetime.datetime]: """ The start date of the appointment. """ - return self.props.getValue('00600040') + return self.getPropertyVal('00600040') @functools.cached_property def timeZoneDescription(self) -> Optional[str]: @@ -460,7 +459,7 @@ def timeZoneDescription(self) -> Optional[str]: A human-readable description of the time zone that is represented by the data in the timeZoneStruct property. """ - return self.namedProperties.get(('8234', ps.PSETID_APPOINTMENT)) + return self.getNamedProp('8234', ps.PSETID_APPOINTMENT) @functools.cached_property def timeZoneStruct(self) -> Optional[TimeZoneStruct]: @@ -482,4 +481,4 @@ def toAttendeesString(self) -> Optional[str]: """ A list of all the sendable attendees, who are also required attendees. """ - return self.namedProperties.get(('823B', ps.PSETID_APPOINTMENT)) + return self.getNamedProp('823B', ps.PSETID_APPOINTMENT) diff --git a/extract_msg/msg_classes/contact.py b/extract_msg/msg_classes/contact.py index e0fe0a8a..95eebaa7 100644 --- a/extract_msg/msg_classes/contact.py +++ b/extract_msg/msg_classes/contact.py @@ -69,7 +69,7 @@ def autoLog(self) -> bool: Whether the client should create a Journal object for each action associated with the Contact object. """ - return self._getNamedAs('8025', ps.PSETID_ADDRESS, bool, False) + return bool(self.getNamedProp('8025', ps.PSETID_ADDRESS)) @functools.cached_property def billing(self) -> Optional[str]: @@ -83,7 +83,7 @@ def birthday(self) -> Optional[datetime.datetime]: """ The birthday of the contact at 11:59 UTC. """ - return self.props.getValue('3A420040') + return self.getPropertyVal('3A420040') @functools.cached_property def birthdayEventEntryID(self) -> Optional[EntryID]: @@ -98,7 +98,7 @@ def birthdayLocal(self) -> Optional[datetime.datetime]: """ The birthday of the contact at 0:00 in the client's local time zone. """ - return self._getNamedAs('80DE', ps.PSETID_ADDRESS) + return self.getNamedProp('80DE', ps.PSETID_ADDRESS) @functools.cached_property def businessCard(self) -> bytes: @@ -136,7 +136,7 @@ def businessCardCardPicture(self) -> Optional[bytes]: The image to be used on a business card. Must be either a PNG file or a JPEG file. """ - return self._getNamedAs('8041', ps.PSETID_ADDRESS) + return self.getNamedProp('8041', ps.PSETID_ADDRESS) @functools.cached_property def businessCardDisplayDefinition(self) -> Optional[BusinessCardDisplayDefinition]: @@ -169,7 +169,7 @@ def businessFaxAddressType(self) -> Optional[str]: """ The type of address for the fax. MUST be set to "FAX" if present. """ - return self._getNamedAs('80C2', ps.PSETID_ADDRESS) + return self.getNamedProp('80C2', ps.PSETID_ADDRESS) @functools.cached_property def businessFaxEmailAddress(self) -> Optional[str]: @@ -177,7 +177,7 @@ def businessFaxEmailAddress(self) -> Optional[str]: Contains a user-readable display name, followed by the "@" character, followed by a fax number. """ - return self._getNamedAs('80C3', ps.PSETID_ADDRESS) + return self.getNamedProp('80C3', ps.PSETID_ADDRESS) @functools.cached_property def businessFaxNumber(self) -> Optional[str]: @@ -191,7 +191,7 @@ def businessFaxOriginalDisplayName(self) -> Optional[str]: """ The normalized subject for the contact. """ - return self._getNamedAs('80C4', ps.PSETID_ADDRESS) + return self.getNamedProp('80C4', ps.PSETID_ADDRESS) @functools.cached_property def businessFaxOriginalEntryId(self) -> Optional[EntryID]: @@ -268,14 +268,14 @@ def contactCharacterSet(self) -> Optional[int]: """ The character set that is used for this Contact object. """ - return self._getNamedAs('8023', ps.PSETID_ADDRESS) + return self.getNamedProp('8023', ps.PSETID_ADDRESS) @functools.cached_property def contactItemData(self) -> Optional[List[int]]: """ Used to help display the contact information. """ - return self._getNamedAs('8007', ps.PSETID_ADDRESS) + return self.getNamedProp('8007', ps.PSETID_ADDRESS) @functools.cached_property def contactLinkedGlobalAddressListEntryID(self) -> Optional[EntryID]: @@ -289,7 +289,7 @@ def contactLinkGlobalAddressListLinkID(self) -> Optional[str]: """ The GUID of the GAL contact to which the duplicate contact is linked. """ - return self._getNamedAs('80E8', ps.PSETID_ADDRESS) + return self.getNamedProp('80E8', ps.PSETID_ADDRESS) @functools.cached_property def contactLinkGlobalAddressListLinkState(self) -> Optional[ContactLinkState]: @@ -304,7 +304,7 @@ def contactLinkLinkRejectHistory(self) -> Optional[List[bytes]]: A list of any contacts that were previously rejected for linking with the duplicate contact. """ - return self._getNamedAs('80E5', ps.PSETID_ADDRESS) + return self.getNamedProp('80E5', ps.PSETID_ADDRESS) @functools.cached_property def contactLinkSMTPAddressCache(self) -> Optional[List[str]]: @@ -312,7 +312,7 @@ def contactLinkSMTPAddressCache(self) -> Optional[List[str]]: A list of the SMTP addresses that are used by the GAL contact that are linked to the duplicate contact. """ - return self._getNamedAs('80E3', ps.PSETID_ADDRESS) + return self.getNamedProp('80E3', ps.PSETID_ADDRESS) @functools.cached_property def contactPhoto(self) -> Optional[bytes]: @@ -331,28 +331,28 @@ def contactUserField1(self) -> Optional[str]: """ Used to store custom text for a business card. """ - return self._getNamedAs('804F', ps.PSETID_ADDRESS) + return self.getNamedProp('804F', ps.PSETID_ADDRESS) @functools.cached_property def contactUserField2(self) -> Optional[str]: """ Used to store custom text for a business card. """ - return self._getNamedAs('8050', ps.PSETID_ADDRESS) + return self.getNamedProp('8050', ps.PSETID_ADDRESS) @functools.cached_property def contactUserField3(self) -> Optional[str]: """ Used to store custom text for a business card. """ - return self._getNamedAs('8051', ps.PSETID_ADDRESS) + return self.getNamedProp('8051', ps.PSETID_ADDRESS) @functools.cached_property def contactUserField4(self) -> Optional[str]: """ Used to store custom text for a business card. """ - return self._getNamedAs('8052', ps.PSETID_ADDRESS) + return self.getNamedProp('8052', ps.PSETID_ADDRESS) @functools.cached_property def customerID(self) -> Optional[str]: @@ -405,21 +405,21 @@ def email1AddressType(self) -> Optional[str]: """ The address type of the first email address. """ - return self._getNamedAs('8082', ps.PSETID_ADDRESS) + return self.getNamedProp('8082', ps.PSETID_ADDRESS) @functools.cached_property def email1DisplayName(self) -> Optional[str]: """ The user-readable display name of the first email address. """ - return self._getNamedAs('8080', ps.PSETID_ADDRESS) + return self.getNamedProp('8080', ps.PSETID_ADDRESS) @functools.cached_property def email1EmailAddress(self) -> Optional[str]: """ The first email address. """ - return self._getNamedAs('8083', ps.PSETID_ADDRESS) + return self.getNamedProp('8083', ps.PSETID_ADDRESS) @functools.cached_property def email1OriginalDisplayName(self) -> Optional[str]: @@ -427,7 +427,7 @@ def email1OriginalDisplayName(self) -> Optional[str]: The first SMTP email address that corresponds to the first email address for the contact. """ - return self._getNamedAs('8084', ps.PSETID_ADDRESS) + return self.getNamedProp('8084', ps.PSETID_ADDRESS) @functools.cached_property def email1OriginalEntryId(self) -> Optional[EntryID]: @@ -456,21 +456,21 @@ def email2AddressType(self) -> Optional[str]: """ The address type of the second email address. """ - return self._getNamedAs('8092', ps.PSETID_ADDRESS) + return self.getNamedProp('8092', ps.PSETID_ADDRESS) @functools.cached_property def email2DisplayName(self) -> Optional[str]: """ The user-readable display name of the second email address. """ - return self._getNamedAs('8090', ps.PSETID_ADDRESS) + return self.getNamedProp('8090', ps.PSETID_ADDRESS) @functools.cached_property def email2EmailAddress(self) -> Optional[str]: """ The second email address. """ - return self._getNamedAs('8093', ps.PSETID_ADDRESS) + return self.getNamedProp('8093', ps.PSETID_ADDRESS) @functools.cached_property def email2OriginalDisplayName(self) -> Optional[str]: @@ -478,7 +478,7 @@ def email2OriginalDisplayName(self) -> Optional[str]: The second SMTP email address that corresponds to the second email address for the contact. """ - return self._getNamedAs('8094', ps.PSETID_ADDRESS) + return self.getNamedProp('8094', ps.PSETID_ADDRESS) @functools.cached_property def email2OriginalEntryId(self) -> Optional[EntryID]: @@ -507,21 +507,21 @@ def email3AddressType(self) -> Optional[str]: """ The address type of the third email address. """ - return self._getNamedAs('80A2', ps.PSETID_ADDRESS) + return self.getNamedProp('80A2', ps.PSETID_ADDRESS) @functools.cached_property def email3DisplayName(self) -> Optional[str]: """ The user-readable display name of the third email address. """ - return self._getNamedAs('80A0', ps.PSETID_ADDRESS) + return self.getNamedProp('80A0', ps.PSETID_ADDRESS) @functools.cached_property def email3EmailAddress(self) -> Optional[str]: """ The third email address. """ - return self._getNamedAs('80A3', ps.PSETID_ADDRESS) + return self.getNamedProp('80A3', ps.PSETID_ADDRESS) @functools.cached_property def email3OriginalDisplayName(self) -> Optional[str]: @@ -529,7 +529,7 @@ def email3OriginalDisplayName(self) -> Optional[str]: The third SMTP email address that corresponds to the third email address for the contact. """ - return self._getNamedAs('80A4', ps.PSETID_ADDRESS) + return self.getNamedProp('80A4', ps.PSETID_ADDRESS) @functools.cached_property def email3OriginalEntryId(self) -> Optional[EntryID]: @@ -567,7 +567,7 @@ def fileUnder(self) -> Optional[str]: The name under which to file a contact when displaying a list of contacts. """ - return self._getNamedAs('8005', ps.PSETID_ADDRESS) + return self.getNamedProp('8005', ps.PSETID_ADDRESS) @functools.cached_property def fileUnderID(self) -> Optional[int]: @@ -575,7 +575,7 @@ def fileUnderID(self) -> Optional[int]: The format to use for fileUnder. See PidLidFileUnderId in [MS-OXOCNTC] for details. """ - return self._getNamedAs('8006', ps.PSETID_ADDRESS) + return self.getNamedProp('8006', ps.PSETID_ADDRESS) @functools.cached_property def freeBusyLocation(self) -> Optional[str]: @@ -583,7 +583,7 @@ def freeBusyLocation(self) -> Optional[str]: A URL path from which a client can retrieve free/busy status information for the contact as an iCalendat file. """ - return self._getNamedAs('80D8', ps.PSETID_ADDRESS) + return self.getNamedProp('80D8', ps.PSETID_ADDRESS) @functools.cached_property def ftpSite(self) -> Optional[str]: @@ -597,7 +597,7 @@ def gender(self) -> Gender: """ The gender of the contact. """ - return Gender(self.props.getValue('3A4D0002', 0)) + return Gender(self.getPropertyVal('3A4D0002', 0)) @functools.cached_property def generation(self) -> Optional[str]: @@ -626,7 +626,7 @@ def hasPicture(self) -> bool: """ Whether the contact has a contact photo. """ - return self._getNamedAs('8015', ps.PSETID_ADDRESS, bool, False) + return bool(self.getNamedProp('8015', ps.PSETID_ADDRESS)) @property def headerFormatProperties(self) -> HEADER_FORMAT_TYPE: @@ -732,7 +732,7 @@ def homeAddress(self) -> Optional[str]: """ The complete home address of the contact. """ - return self._getNamedAs('801A', ps.PSETID_ADDRESS) + return self.getNamedProp('801A', ps.PSETID_ADDRESS) @functools.cached_property def homeAddressCountry(self) -> Optional[str]: @@ -746,7 +746,7 @@ def homeAddressCountryCode(self) -> Optional[str]: """ The country code portion of the contact's home address. """ - return self._getNamedAs('80DA', ps.PSETID_ADDRESS) + return self.getNamedProp('80DA', ps.PSETID_ADDRESS) @functools.cached_property def homeAddressLocality(self) -> Optional[str]: @@ -806,7 +806,7 @@ def homeFaxAddressType(self) -> Optional[str]: """ The type of address for the fax. MUST be set to "FAX" if present. """ - return self._getNamedAs('80D2', ps.PSETID_ADDRESS) + return self.getNamedProp('80D2', ps.PSETID_ADDRESS) @functools.cached_property def homeFaxEmailAddress(self) -> Optional[str]: @@ -814,7 +814,7 @@ def homeFaxEmailAddress(self) -> Optional[str]: Contains a user-readable display name, followed by the "@" character, followed by a fax number. """ - return self._getNamedAs('80D3', ps.PSETID_ADDRESS) + return self.getNamedProp('80D3', ps.PSETID_ADDRESS) @functools.cached_property def homeFaxNumber(self) -> Optional[str]: @@ -828,7 +828,7 @@ def homeFaxOriginalDisplayName(self) -> Optional[str]: """ The normalized subject for the contact. """ - return self._getNamedAs('80D4', ps.PSETID_ADDRESS) + return self.getNamedProp('80D4', ps.PSETID_ADDRESS) @functools.cached_property def homeFaxOriginalEntryId(self) -> Optional[EntryID]: @@ -863,14 +863,14 @@ def instantMessagingAddress(self) -> Optional[str]: """ The instant messaging address of the contact. """ - return self._getNamedAs('8062', ps.PSETID_ADDRESS) + return self.getNamedProp('8062', ps.PSETID_ADDRESS) @functools.cached_property def isContactLinked(self) -> bool: """ Whether the contact is linked to other contacts. """ - return self._getNamedAs('80E0', ps.PSETID_ADDRESS, bool, False) + return bool(self.getNamedProp('80E0', ps.PSETID_ADDRESS)) @functools.cached_property def isdnNumber(self) -> Optional[str]: @@ -928,7 +928,7 @@ def mailAddressCountryCode(self) -> Optional[str]: """ The country code portion of the contact's mail address. """ - return self._getNamedAs('80DD', ps.PSETID_ADDRESS) + return self.getNamedProp('80DD', ps.PSETID_ADDRESS) @functools.cached_property def mailAddressLocality(self) -> Optional[str]: @@ -1014,14 +1014,14 @@ def oscSyncEnabled(self) -> bool: Whether contact synchronization with an external source (such as a social networking site) is handled by the server. """ - return bool(self.props.getValue('7C24000B')) + return bool(self.getPropertyVal('7C24000B')) @functools.cached_property def otherAddress(self) -> Optional[str]: """ The complete other address of the contact. """ - return self._getNamedAs('801C', ps.PSETID_ADDRESS) + return self.getNamedProp('801C', ps.PSETID_ADDRESS) @functools.cached_property def otherAddressCountry(self) -> Optional[str]: @@ -1035,7 +1035,7 @@ def otherAddressCountryCode(self) -> Optional[str]: """ The country code portion of the contact's other address. """ - return self._getNamedAs('80DC', ps.PSETID_ADDRESS) + return self.getNamedProp('80DC', ps.PSETID_ADDRESS) @functools.cached_property def otherAddressLocality(self) -> Optional[str]: @@ -1098,21 +1098,21 @@ def phoneticCompanyName(self) -> Optional[str]: """ The phonetic pronunciation of the contact's company name. """ - return self._getNamedAs('802E', ps.PSETID_ADDRESS) + return self.getNamedProp('802E', ps.PSETID_ADDRESS) @functools.cached_property def phoneticGivenName(self) -> Optional[str]: """ The phonetic pronunciation of the contact's given name. """ - return self._getNamedAs('802C', ps.PSETID_ADDRESS) + return self.getNamedProp('802C', ps.PSETID_ADDRESS) @functools.cached_property def phoneticSurname(self) -> Optional[str]: """ The phonetic pronunciation of the given name of the contact. """ - return self._getNamedAs('802D', ps.PSETID_ADDRESS) + return self.getNamedProp('802D', ps.PSETID_ADDRESS) @functools.cached_property def postalAddressID(self) -> PostalAddressID: @@ -1120,7 +1120,7 @@ def postalAddressID(self) -> PostalAddressID: Indicates which physical address is the Mailing Address for this contact. """ - return PostalAddressID(self.namedProperties.get(('8022', ps.PSETID_ADDRESS), 0)) + return PostalAddressID(self.getNamedProp('8022', ps.PSETID_ADDRESS, 0)) @functools.cached_property def primaryFax(self) -> Optional[Dict]: @@ -1145,7 +1145,7 @@ def primaryFaxAddressType(self) -> Optional[str]: """ The type of address for the fax. MUST be set to "FAX" if present. """ - return self._getNamedAs('80B2', ps.PSETID_ADDRESS) + return self.getNamedProp('80B2', ps.PSETID_ADDRESS) @functools.cached_property def primaryFaxEmailAddress(self) -> Optional[str]: @@ -1153,7 +1153,7 @@ def primaryFaxEmailAddress(self) -> Optional[str]: Contains a user-readable display name, followed by the "@" character, followed by a fax number. """ - return self._getNamedAs('80B3', ps.PSETID_ADDRESS) + return self.getNamedProp('80B3', ps.PSETID_ADDRESS) @functools.cached_property def primaryFaxNumber(self) -> Optional[str]: @@ -1167,7 +1167,7 @@ def primaryFaxOriginalDisplayName(self) -> Optional[str]: """ The normalized subject for the contact. """ - return self._getNamedAs('80B4', ps.PSETID_ADDRESS) + return self.getNamedProp('80B4', ps.PSETID_ADDRESS) @functools.cached_property def primaryFaxOriginalEntryId(self) -> Optional[EntryID]: @@ -1254,7 +1254,7 @@ def weddingAnniversary(self) -> Optional[datetime.datetime]: """ The wedding anniversary of the contact at 11:59 UTC. """ - return self.props.getValue('3A410040') + return self.getPropertyVal('3A410040') @functools.cached_property def weddingAnniversaryEventEntryID(self) -> Optional[EntryID]: @@ -1270,67 +1270,67 @@ def weddingAnniversaryLocal(self) -> Optional[datetime.datetime]: The wedding anniversary of the contact at 0:00 in the client's local time zone. """ - return self._getNamedAs('80DF', ps.PSETID_ADDRESS) + return self.getNamedProp('80DF', ps.PSETID_ADDRESS) @functools.cached_property def webpageUrl(self) -> Optional[str]: """ The contact's business web page url. SHOULD be the same as businessUrl. """ - return self._getNamedAs('802B', ps.PSETID_ADDRESS) + return self.getNamedProp('802B', ps.PSETID_ADDRESS) @functools.cached_property def workAddress(self) -> Optional[str]: """ The complete work address of the contact. """ - return self._getNamedAs('801B', ps.PSETID_ADDRESS) + return self.getNamedProp('801B', ps.PSETID_ADDRESS) @functools.cached_property def workAddressCountry(self) -> Optional[str]: """ The country portion of the contact's work address. """ - return self._getNamedAs('8049', ps.PSETID_ADDRESS) + return self.getNamedProp('8049', ps.PSETID_ADDRESS) @functools.cached_property def workAddressCountryCode(self) -> Optional[str]: """ The country code portion of the contact's work address. """ - return self._getNamedAs('80DB', ps.PSETID_ADDRESS) + return self.getNamedProp('80DB', ps.PSETID_ADDRESS) @functools.cached_property def workAddressLocality(self) -> Optional[str]: """ The locality or city portion of the contact's work address. """ - return self._getNamedAs('8046', ps.PSETID_ADDRESS) + return self.getNamedProp('8046', ps.PSETID_ADDRESS) @functools.cached_property def workAddressPostalCode(self) -> Optional[str]: """ The postal code portion of the contact's work address. """ - return self._getNamedAs('8048', ps.PSETID_ADDRESS) + return self.getNamedProp('8048', ps.PSETID_ADDRESS) @functools.cached_property def workAddressPostOfficeBox(self) -> Optional[str]: """ The number or identifier of the contact's work post office box. """ - return self._getNamedAs('804A', ps.PSETID_ADDRESS) + return self.getNamedProp('804A', ps.PSETID_ADDRESS) @functools.cached_property def workAddressStateOrProvince(self) -> Optional[str]: """ The state or province portion of the contact's work address. """ - return self._getNamedAs('8047', ps.PSETID_ADDRESS) + return self.getNamedProp('8047', ps.PSETID_ADDRESS) @functools.cached_property def workAddressStreet(self) -> Optional[str]: """ The street portion of the contact's work address. """ - return self._getNamedAs('8045', ps.PSETID_ADDRESS) + return self.getNamedProp('8045', ps.PSETID_ADDRESS) diff --git a/extract_msg/msg_classes/meeting_related.py b/extract_msg/msg_classes/meeting_related.py index e976a6e7..c963f998 100644 --- a/extract_msg/msg_classes/meeting_related.py +++ b/extract_msg/msg_classes/meeting_related.py @@ -23,14 +23,14 @@ def attendeeCriticalChange(self) -> Optional[datetime.datetime]: """ The date and time at which the meeting-related object was sent. """ - return self._getNamedAs('0001', ps.PSETID_MEETING) + return self.getNamedProp('0001', ps.PSETID_MEETING) @functools.cached_property def processed(self) -> bool: """ Indicates whether a client has processed a meeting-related object. """ - return bool(self.props.getValue('7D01000B')) + return bool(self.getPropertyVal('7D01000B')) @functools.cached_property def serverProcessed(self) -> bool: @@ -38,7 +38,7 @@ def serverProcessed(self) -> bool: Indicates that the Meeting Request object or Meeting Update object has been processed. """ - return self._getNamedAs('85CC', ps.PSETID_CALENDAR_ASSISTANT, bool, False) + return bool(self.getNamedProp('85CC', ps.PSETID_CALENDAR_ASSISTANT)) @functools.cached_property def serverProcessingActions(self) -> Optional[ServerProcessingAction]: @@ -55,11 +55,11 @@ def timeZone(self) -> Optional[int]: See PidLidTimeZone in [MS-OXOCAL] for details. """ - return self._getNamedAs('000C', ps.PSETID_MEETING) + return self.getNamedProp('000C', ps.PSETID_MEETING) @functools.cached_property def where(self) -> Optional[str]: """ PidLidWhere. Should be the same as location. """ - return self._getNamedAs('0002', ps.PSETID_MEETING) + return self.getNamedProp('0002', ps.PSETID_MEETING) diff --git a/extract_msg/msg_classes/msg.py b/extract_msg/msg_classes/msg.py index 36de4132..379fd8c0 100644 --- a/extract_msg/msg_classes/msg.py +++ b/extract_msg/msg_classes/msg.py @@ -19,7 +19,9 @@ import olefile -from typing import Any, Callable, cast, Dict, List, Optional, Tuple, Union +from typing import ( + Any, Callable, cast, Dict, List, Optional, Tuple, TypeVar, Union + ) from .. import constants from ..attachments import ( @@ -37,7 +39,7 @@ from ..properties.named import Named, NamedProperties from ..properties.prop import FixedLengthProp from ..properties.properties_store import PropertiesStore -from ..utils import ( +from ..utils import ( divide, hasLen, inputToMsgPath, makeWeakRef, msgPathToString, parseType, verifyPropertyId, verifyType, windowsUnicode ) @@ -46,6 +48,8 @@ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) +_T = TypeVar('_T') + class MSGFile: """ @@ -217,7 +221,7 @@ def _getNamedAs(self, propertyName : str, guid : str, overrideClass = None, pres :param overrideClass: when the value could not be found (is None). If this is changed to False, then the value will be used regardless. """ - value = self.namedProperties.get((propertyName, guid)) + value = self.getNamedProp(propertyName, guid) # Check if we should be overriding the data type for this instance. if overrideClass is not None: if value is not None or not preserveNone: @@ -253,7 +257,7 @@ def _getPropertyAs(self, propertyName, overrideClass = None, preserveNone : bool :param overrideClass: when the value could not be found (is None). If this is changed to False, then the value will be used regardless. """ - value = self.props.getValue(propertyName) + value = self.getPropertyVal(propertyName) # Check if we should be overriding the data type for this instance. if overrideClass is not None: if (value is not None or not preserveNone): @@ -606,6 +610,22 @@ def getMultipleString(self, filename, prefix : bool = True) -> Optional[List[str ret[index] = item.decode(self.stringEncoding)[:-1] return ret + def getNamedProp(self, propertyName : str, guid : str, default : _T = None) -> Union[Any, _T]: + """ + instance.namedProperties.get((propertyName, guid), default) + + Can be override to create new behavior. + """ + return self.namedProperties.get((propertyName, guid), default) + + def getPropertyVal(self, name, default : _T = None) -> Union[Any, _T]: + """ + instance.props.getValue(name, default) + + Can be overriden to create new behavior. + """ + return self.props.getValue(name, default) + def getSingleOrMultipleBinary(self, filename, prefix : bool = True) -> Optional[Union[List[bytes], bytes]]: """ A combination of :method getStringStream: and @@ -774,7 +794,7 @@ def areStringsUnicode(self) -> bool: """ Returns a boolean telling if the strings are unicode encoded. """ - return (self.props.getValue('340D0003', 0) & 0x40000) != 0 + return (self.getPropertyVal('340D0003', 0) & 0x40000) != 0 @functools.cached_property def attachments(self) -> Union[List[AttachmentBase], List[SignedAttachment]]: @@ -816,7 +836,7 @@ def classified(self) -> bool: Indicates whether the contents of this message are regarded as classified information. """ - return self._getNamedAs('85B5', constants.ps.PSETID_COMMON, overrideClass = bool, preserveNone = False) + return bool(self.getNamedProp('85B5', constants.ps.PSETID_COMMON)) @functools.cached_property def classType(self) -> Optional[str]: @@ -830,14 +850,14 @@ def commonEnd(self) -> Optional[datetime.datetime]: """ The end time for the object. """ - return self._getNamedAs('8517', constants.ps.PSETID_COMMON) + return self.getNamedProp('8517', constants.ps.PSETID_COMMON) @functools.cached_property def commonStart(self) -> Optional[datetime.datetime]: """ The start time for the object. """ - return self._getNamedAs('8516', constants.ps.PSETID_COMMON) + return self.getNamedProp('8516', constants.ps.PSETID_COMMON) @functools.cached_property def currentVersion(self) -> Optional[int]: @@ -845,14 +865,14 @@ def currentVersion(self) -> Optional[int]: Specifies the build number of the client application that sent the message. """ - return self._getNamedAs('8552', constants.ps.PSETID_COMMON) + return self.getNamedProp('8552', constants.ps.PSETID_COMMON) @functools.cached_property def currentVersionName(self) -> Optional[str]: """ Specifies the name of the client application that sent the message. """ - return self._getNamedAs('8554', constants.ps.PSETID_COMMON) + return self.getNamedProp('8554', constants.ps.PSETID_COMMON) @property def errorBehavior(self) -> ErrorBehavior: @@ -1026,7 +1046,7 @@ def stringEncoding(self): logger.warning('Encoding property not found. Defaulting to ISO-8859-15.') self.__stringEncoding = 'iso-8859-15' else: - enc = cast(int, self.props['3FFD0003'].value) + enc = cast(int, self.getPropertyVal('3FFD0003')) # Now we just need to translate that value. self.__stringEncoding = lookupCodePage(enc) return self.__stringEncoding diff --git a/extract_msg/msg_classes/task_request.py b/extract_msg/msg_classes/task_request.py index b08394a4..3fa17035 100644 --- a/extract_msg/msg_classes/task_request.py +++ b/extract_msg/msg_classes/task_request.py @@ -6,7 +6,7 @@ import functools import logging -from typing import Optional +from typing import cast, Optional from .. import constants from ..enums import ErrorBehavior, TaskMode, TaskRequestType @@ -60,7 +60,7 @@ def processed(self) -> bool: Indicates whether a client has already processed a received task communication. """ - return bool(self.props.getValue('7D01000B')) + return bool(self.getPropertyVal('7D01000B')) @functools.cached_property def taskMode(self) -> Optional[TaskMode]: @@ -90,7 +90,7 @@ def taskObject(self) -> Optional[Task]: if task is None: if ErrorBehavior.STANDARDS_VIOLATION in self.errorBehavior: logger.error('Task object not found on TaskRequest object.') - return + return None raise StandardViolationError('Task object not found on TaskRequest object.') # We know we have the task, let's make sure it's at index 0. If not, @@ -98,9 +98,7 @@ def taskObject(self) -> Optional[Task]: if task[0] != 0: logger.warning('Embedded task object was not located at index 0.') - self._taskObject = task[1] - - return self._taskObject + return cast(Task, task[1]) @functools.cached_property def taskRequestType(self) -> TaskRequestType: From b2ce27bb06e6a5484759984702d677cd09670483 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Wed, 9 Aug 2023 17:45:07 -0700 Subject: [PATCH 16/20] Potentially done transferring api usage --- CHANGELOG.md | 1 + extract_msg/attachments/attachment_base.py | 67 ++++++++++++---------- extract_msg/attachments/web_att.py | 2 +- extract_msg/msg_classes/message_base.py | 12 +--- extract_msg/msg_classes/msg.py | 18 +++--- extract_msg/recipient.py | 12 +++- 6 files changed, 60 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a26107db..e80e9714 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ * Changed all `Named` methods to no longer have a prefix argument. The prefix should *always* be false sense the named property mapping will only exist in the top level directory. * Adjusted `tryGetMimeType` to allows any attachments whose `data` property would return a `bytes` instance. * Changed internal code to use public SPI functions wherever possible. This includes making many private API functions use calls to the public API for getting bits of data. +* Fixed potential issue with `AttachmentBase.clsid` which had the potential to cause some attachments to fail to generate a CLSID. **v0.44.0** * Fixed a bug that caused `MessageBase.headerInit` to always return `False` after the 0.42.0 update. diff --git a/extract_msg/attachments/attachment_base.py b/extract_msg/attachments/attachment_base.py index 3047fc93..05e08031 100644 --- a/extract_msg/attachments/attachment_base.py +++ b/extract_msg/attachments/attachment_base.py @@ -15,7 +15,9 @@ import weakref from functools import cached_property -from typing import List, Optional, Tuple, Type, TYPE_CHECKING, Union +from typing import ( + Any, List, Optional, Tuple, Type, TYPE_CHECKING, TypeVar, Union + ) from .. import constants from ..enums import AttachmentType @@ -35,6 +37,8 @@ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) +_T = TypeVar('_T') + class AttachmentBase(abc.ABC): """ @@ -66,7 +70,7 @@ def _getNamedAs(self, propertyName : str, guid : str, overrideClass = None, pres :param overrideClass: when the value could not be found (is None). If this is changed to False, then the value will be used regardless. """ - value = self.namedProperties.get((propertyName, guid)) + value = self.getNamedProp(propertyName, guid) # Check if we should be overriding the data type for this instance. if overrideClass is not None: if value is not None or not preserveNone: @@ -86,10 +90,8 @@ def _getPropertyAs(self, propertyName, overrideClass = None, preserveNone : bool :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: - value = self.props[propertyName].value - except (KeyError, AttributeError): - value = None + value = self.getPropertyVal(propertyName) + # Check if we should be overriding the data type for this instance. if overrideClass is not None: if (value is not None or not preserveNone): @@ -212,18 +214,14 @@ def _getTypedProperty(self, propertyID, _type = None) -> Tuple[bool, Optional[ob verifyPropertyId(propertyID) if _type: verifyType(_type) - prop = self.props.get(propertyID + _type) - if isinstance(prop, FixedLengthProp): - return True, prop.value - else: - return False, None - else: - props = self.props.getProperties(propertyID) - for prop in props: - if isinstance(prop, FixedLengthProp): - return True, prop.value + propertyID += _type + + notFound = object() + ret = self.getPropertyVal(propertyID, notFound) + if ret is notFound: + return False, None - return False, None + return True, ret def _getTypedStream(self, filename, _type = None): """ @@ -360,6 +358,22 @@ def getMultipleString(self, filename) -> Optional[List[str]]: raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') return msg.getMultipleString([self.__dir, msgPathToString(filename)]) + def getNamedProp(self, propertyName : str, guid : str, default : _T = None) -> Union[Any, _T]: + """ + instance.namedProperties.get((propertyName, guid), default) + + Can be override to create new behavior. + """ + return self.namedProperties.get((propertyName, guid), default) + + def getPropertyVal(self, name, default : _T = None) -> Union[Any, _T]: + """ + instance.props.getValue(name, default) + + Can be overriden to create new behavior. + """ + return self.props.getValue(name, default) + def getSingleOrMultipleBinary(self, filename) -> Optional[Union[List[bytes], bytes]]: """ A combination of :method getStringStream: and @@ -502,17 +516,10 @@ def clsid(self) -> str: 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): + 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: @@ -571,7 +578,7 @@ def exceptionReplaceTime(self) -> Optional[datetime.datetime]: Only applicable if the attachment is an Exception object. """ - return self.props.getValue('7FF90040') + return self.getPropertyVal('7FF90040') @functools.cached_property def extension(self) -> Optional[str]: @@ -585,14 +592,14 @@ def hidden(self) -> bool: """ Indicates whether an Attachment object is hidden from the end user. """ - return bool(self.props.getValue('7FFE000B')) + return bool(self.getPropertyVal('7FFE000B')) @functools.cached_property def isAttachmentContactPhoto(self) -> bool: """ Whether the attachment is a contact photo for a Contact object. """ - return bool(self.props.getValue('7FFF000B')) + return bool(self.getPropertyVal('7FFF000B')) @functools.cached_property def longFilename(self) -> Optional[str]: @@ -666,7 +673,7 @@ def renderingPosition(self) -> Optional[int]: within the main message text. A value of 0xFFFFFFFF indicates a hidden attachment that is not to be rendered. """ - return self.props.getValue('370B0003') + return self.getPropertyVal('370B0003') @property def shortFilename(self) -> Optional[str]: diff --git a/extract_msg/attachments/web_att.py b/extract_msg/attachments/web_att.py index 669fe0fe..035def80 100644 --- a/extract_msg/attachments/web_att.py +++ b/extract_msg/attachments/web_att.py @@ -57,7 +57,7 @@ def providerName(self) -> Optional[str]: """ The type of web service manipulating the attachment. """ - return self._getNamedAs('AttachmentProviderType', constants.ps.PSETID_ATTACHMENT) + return self.getNamedProp('AttachmentProviderType', constants.ps.PSETID_ATTACHMENT) @property def type(self) -> AttachmentType: diff --git a/extract_msg/msg_classes/message_base.py b/extract_msg/msg_classes/message_base.py index c982775c..18179800 100644 --- a/extract_msg/msg_classes/message_base.py +++ b/extract_msg/msg_classes/message_base.py @@ -1210,10 +1210,7 @@ def isRead(self) -> bool: """ Returns if this email has been marked as read. """ - try: - return bool(self.props['0E070003'].value & 1) - except (AttributeError, KeyError): - return False + return bool(self.getPropertyVal('0E070003', 0) & 1) @functools.cached_property def isSent(self) -> bool: @@ -1221,10 +1218,7 @@ def isSent(self) -> bool: Returns if this email has been marked as sent. Assumes True if no flags are found. """ - if not self.props.get('0E070003'): - return True - else: - return not bool(self.props['0E070003'].value & 8) + return not bool(self.getPropertyVal('0E070003', 0) & 8) @functools.cached_property def messageId(self) -> Optional[str]: @@ -1247,7 +1241,7 @@ def receivedTime(self) -> Optional[datetime.datetime]: """ The date and time the message was received by the server. """ - return self.props.getValue('0E060040') + return self.getPropertyVal('0E060040') @property def recipientSeparator(self) -> str: diff --git a/extract_msg/msg_classes/msg.py b/extract_msg/msg_classes/msg.py index 379fd8c0..046c5413 100644 --- a/extract_msg/msg_classes/msg.py +++ b/extract_msg/msg_classes/msg.py @@ -370,18 +370,14 @@ def _getTypedProperty(self, propertyID : str, _type = None) -> Tuple[bool, Optio verifyPropertyId(propertyID) if _type: verifyType(_type) - prop = self.props.get(propertyID + _type) - if isinstance(prop, FixedLengthProp): - return True, prop.value - else: - return False, None - else: - props = self.props.getProperties(propertyID) - for prop in props: - if isinstance(prop, FixedLengthProp): - return True, prop.value + propertyID += _type + + notFound = object() + ret = self.getPropertyVal(propertyID, notFound) + if ret is notFound: + return False, None - return False, None + return True, ret def _getTypedStream(self, filename, prefix : bool = True, _type = None) -> Tuple[bool, Optional[Any]]: """ diff --git a/extract_msg/recipient.py b/extract_msg/recipient.py index a27e0e98..ddeaa8fe 100644 --- a/extract_msg/recipient.py +++ b/extract_msg/recipient.py @@ -9,7 +9,7 @@ import functools import logging -from typing import List, Optional, Tuple, TYPE_CHECKING, Union +from typing import Any, List, Optional, Tuple, TYPE_CHECKING, TypeVar, Union from .enums import ErrorBehavior, MeetingRecipientType, PropertiesType, RecipientType from .exceptions import StandardViolationError @@ -25,6 +25,8 @@ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) +_T = TypeVar('_T') + class Recipient: """ @@ -292,6 +294,14 @@ def getMultipleString(self, filename) -> Optional[List[str]]: raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') return msg.getMultipleString([self.__dir, msgPathToString(filename)]) + def getPropertyVal(self, name, default : _T = None) -> Union[Any, _T]: + """ + instance.props.getValue(name, default) + + Can be overriden to create new behavior. + """ + return self.props.getValue(name, default) + def getSingleOrMultipleBinary(self, filename) -> Optional[Union[List[bytes], bytes]]: """ A combination of :method getStringStream: and From 7c14152851ba37003e40d7b2eb5cdb03e4fd7955 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sat, 12 Aug 2023 15:47:48 -0700 Subject: [PATCH 17/20] Significant progress converting from old api --- extract_msg/attachments/attachment_base.py | 20 +++----- extract_msg/enums.py | 17 +++++- extract_msg/msg_classes/appointment.py | 16 +++--- extract_msg/msg_classes/calendar.py | 16 +++--- extract_msg/msg_classes/calendar_base.py | 6 +-- extract_msg/msg_classes/contact.py | 6 +-- extract_msg/msg_classes/meeting_exception.py | 6 +-- extract_msg/msg_classes/meeting_forward.py | 4 +- extract_msg/msg_classes/meeting_request.py | 10 ++-- extract_msg/msg_classes/meeting_response.py | 12 ++--- extract_msg/msg_classes/msg.py | 17 +++--- extract_msg/msg_classes/sticky_note.py | 8 +-- extract_msg/msg_classes/task.py | 54 ++++++++++---------- 13 files changed, 99 insertions(+), 93 deletions(-) diff --git a/extract_msg/attachments/attachment_base.py b/extract_msg/attachments/attachment_base.py index 05e08031..dad49c1f 100644 --- a/extract_msg/attachments/attachment_base.py +++ b/extract_msg/attachments/attachment_base.py @@ -16,7 +16,8 @@ from functools import cached_property from typing import ( - Any, List, Optional, Tuple, Type, TYPE_CHECKING, TypeVar, Union + Any, Callable, List, Optional, Tuple, Type, TYPE_CHECKING, TypeVar, + Union ) from .. import constants @@ -58,24 +59,19 @@ def __init__(self, msg : MSGFile, dir_ : str, propStore : PropertiesStore): self.__namedProperties = NamedProperties(msg.named, self) self.__treePath = msg.treePath + [makeWeakRef(self)] - def _getNamedAs(self, propertyName : str, guid : str, overrideClass = None, preserveNone : bool = True): + def _getNamedAs(self, propertyName : str, guid : str, overrideClass : Callable[..., _T]) -> Optional[_T]: """ Returns the named property, setting the class if specified. :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. + function or the function itself, if that is what is provided. If + the value is None, this function is not called. If you want it to + be called regardless, you should handle the data directly. """ value = self.getNamedProp(propertyName, guid) - # Check if we should be overriding the data type for this instance. - if overrideClass is not None: - if value is not None or not preserveNone: - value = overrideClass(value) - + if value is not None: + value = overrideClass(value) return value def _getPropertyAs(self, propertyName, overrideClass = None, preserveNone : bool = True): diff --git a/extract_msg/enums.py b/extract_msg/enums.py index 0e4580be..2c237b01 100644 --- a/extract_msg/enums.py +++ b/extract_msg/enums.py @@ -76,7 +76,7 @@ import enum -from typing import Dict, Union +from typing import Dict, Iterable, List, Set, Union class AddressBookType(enum.IntEnum): @@ -463,6 +463,13 @@ class DVAspect(enum.IntEnum): class ElectronicAddressProperties(enum.IntEnum): + @classmethod + def fromIter(cls, items : Iterable[int]) -> Set[ElectronicAddressProperties]: + """ + Uses the iterable of ints to create a set of this enum. + """ + return {cls(x) for x in items} + EMAIL_1 = 0x00000000 EMAIL_2 = 0x00000001 EMAIL_3 = 0x00000002 @@ -1505,6 +1512,14 @@ class ResponseStatus(enum.IntEnum): DECLINED: The attendee has declined. NOT_RESPONDED: The attendee has not yet responded. """ + + @classmethod + def fromIter(cls, items : Iterable[int]) -> List[ResponseStatus]: + """ + Uses the iterable of ints to create a list of this enum. + """ + return {cls(x) for x in items} + NONE = 0x00000000 ORGANIZED = 0x00000001 TENTATIVE = 0x00000002 diff --git a/extract_msg/msg_classes/appointment.py b/extract_msg/msg_classes/appointment.py index e205c3fd..33bdf2bb 100644 --- a/extract_msg/msg_classes/appointment.py +++ b/extract_msg/msg_classes/appointment.py @@ -29,14 +29,14 @@ def appointmentCounterProposal(self) -> bool: Indicates to the organizer that there are counter proposals that have not been accepted or rejected by the organizer. """ - return self._getNamedAs('8257', ps.PSETID_APPOINTMENT, bool, False) + return bool(self.getNamedProp('8257', ps.PSETID_APPOINTMENT)) @functools.cached_property def appointmentLastSequence(self) -> Optional[int]: """ The last sequence number that was sent to any attendee. """ - return self._getNamedAs('8203', ps.PSETID_APPOINTMENT) + return self.getNamedProp('8203', ps.PSETID_APPOINTMENT) @functools.cached_property def appointmentProposalNumber(self) -> Optional[int]: @@ -44,14 +44,14 @@ def appointmentProposalNumber(self) -> Optional[int]: The number of attendees who have sent counter propostals that have not been accepted or rejected by the organizer. """ - return self._getNamedAs('8259', ps.PSETID_APPOINTMENT) + return self.getNamedProp('8259', ps.PSETID_APPOINTMENT) @functools.cached_property def appointmentReplyName(self) -> Optional[datetime.datetime]: """ The user who last replied to the meeting request or meeting update. """ - return self._getNamedAs('8230', ps.PSETID_APPOINTMENT) + return self.getNamedProp('8230', ps.PSETID_APPOINTMENT) @functools.cached_property def appointmentReplyTime(self) -> Optional[datetime.datetime]: @@ -59,7 +59,7 @@ def appointmentReplyTime(self) -> Optional[datetime.datetime]: The date and time at which the attendee responded to a received Meeting Request object of Meeting Update object in UTC. """ - return self._getNamedAs('8220', ps.PSETID_APPOINTMENT) + return self.getNamedProp('8220', ps.PSETID_APPOINTMENT) @functools.cached_property def appointmentSequenceTime(self) -> Optional[datetime.datetime]: @@ -67,7 +67,7 @@ def appointmentSequenceTime(self) -> Optional[datetime.datetime]: The date and time at which the appointmentSequence property was last modified. """ - return self._getNamedAs('8202', ps.PSETID_APPOINTMENT) + return self.getNamedProp('8202', ps.PSETID_APPOINTMENT) @functools.cached_property def autoFillLocation(self) -> bool: @@ -79,14 +79,14 @@ def autoFillLocation(self) -> bool: A value of False indicates that the value of the location property is not automatically set. """ - return self._getNamedAs('823A', ps.PSETID_APPOINTMENT, bool, False) + return bool(self.getNamedProp('823A', ps.PSETID_APPOINTMENT)) @functools.cached_property def fInvited(self) -> bool: """ Whether a Meeting Request object has been sent out. """ - return self._getNamedAs('8229', ps.PSETID_APPOINTMENT, bool, False) + return bool(self.getNamedProp('8229', ps.PSETID_APPOINTMENT)) @property def headerFormatProperties(self) -> HEADER_FORMAT_TYPE: diff --git a/extract_msg/msg_classes/calendar.py b/extract_msg/msg_classes/calendar.py index c97144e0..584a8ec5 100644 --- a/extract_msg/msg_classes/calendar.py +++ b/extract_msg/msg_classes/calendar.py @@ -35,7 +35,7 @@ def fExceptionalAttendees(self) -> Optional[bool]: SHOULD NOT be set for any Calendar object other than that of the organizer's. """ - return self._getNamedAs('822B', ps.PSETID_APPOINTMENT) + return self.getNamedProp('822B', ps.PSETID_APPOINTMENT) @functools.cached_property def reminderDelta(self) -> Optional[int]: @@ -43,7 +43,7 @@ def reminderDelta(self) -> Optional[int]: The interval, in minutes, between the time at which the reminder first becomes overdue and the start time of the Calendar object. """ - return self._getNamedAs('8501', ps.PSETID_COMMON) + return self.getNamedProp('8501', ps.PSETID_COMMON) @functools.cached_property def reminderFileParameter(self) -> Optional[str]: @@ -52,7 +52,7 @@ def reminderFileParameter(self) -> Optional[str]: client SHOULD play when the reminder for the Message Object becomes overdue. """ - return self._getNamedAs('851F', ps.PSETID_COMMON) + return self.getNamedProp('851F', ps.PSETID_COMMON) @functools.cached_property def reminderOverride(self) -> bool: @@ -60,7 +60,7 @@ def reminderOverride(self) -> bool: Specifies if clients SHOULD respect the value of the reminderPlaySound property and the reminderFileParameter property. """ - return self._getNamedAs('851C', ps.PSETID_COMMON, bool, False) + return bool(self.getNamedProp('851C', ps.PSETID_COMMON)) @functools.cached_property def reminderPlaySound(self) -> bool: @@ -68,25 +68,25 @@ def reminderPlaySound(self) -> bool: Specified that the cliebnt should play a sound when the reminder becomes overdue. """ - return self._getNamedAs('851E', ps.PSETID_COMMON, bool, False) + return bool(self.getNamedProp('851E', ps.PSETID_COMMON)) @functools.cached_property def reminderSet(self) -> bool: """ Specifies whether a reminder is set on the object. """ - return self._getNamedAs('8503', ps.PSETID_COMMON, bool, False) + return bool(self.getNamedProp('8503', ps.PSETID_COMMON)) @functools.cached_property def reminderSignalTime(self) -> Optional[datetime.datetime]: """ The point in time when a reminder transitions from pending to overdue. """ - return self._getNamedAs('8560', ps.PSETID_COMMON) + return self.getNamedProp('8560', ps.PSETID_COMMON) @functools.cached_property def reminderTime(self) -> Optional[datetime.datetime]: """ The time after which the user would be late. """ - return self._getNamedAs('8502', ps.PSETID_COMMON) + return self.getNamedProp('8502', ps.PSETID_COMMON) diff --git a/extract_msg/msg_classes/calendar_base.py b/extract_msg/msg_classes/calendar_base.py index 65c78162..007853fb 100644 --- a/extract_msg/msg_classes/calendar_base.py +++ b/extract_msg/msg_classes/calendar_base.py @@ -349,21 +349,21 @@ def nonSendBccTrackStatus(self) -> Optional[List[ResponseStatus]]: """ A ResponseStatus for each of the attendees in nonSendableBcc. """ - return self._getNamedAs('8545', ps.PSETID_COMMON, lambda x : list(ResponseStatus(y) for y in x)) + return self._getNamedAs('8545', ps.PSETID_COMMON, ResponseStatus.fromIter) @functools.cached_property def nonSendCcTrackStatus(self) -> Optional[List[ResponseStatus]]: """ A ResponseStatus for each of the attendees in nonSendableCc. """ - return self._getNamedAs('8544', ps.PSETID_COMMON, lambda x : list(ResponseStatus(y) for y in x)) + return self._getNamedAs('8544', ps.PSETID_COMMON, ResponseStatus.fromIter) @functools.cached_property def nonSendToTrackStatus(self) -> Optional[List[ResponseStatus]]: """ A ResponseStatus for each of the attendees in nonSendableTo. """ - return self._getNamedAs('8543', ps.PSETID_COMMON, lambda x : list(ResponseStatus(y) for y in x)) + return self._getNamedAs('8543', ps.PSETID_COMMON, ResponseStatus.fromIter) @functools.cached_property def optionalAttendees(self) -> Optional[str]: diff --git a/extract_msg/msg_classes/contact.py b/extract_msg/msg_classes/contact.py index 95eebaa7..adbc21b9 100644 --- a/extract_msg/msg_classes/contact.py +++ b/extract_msg/msg_classes/contact.py @@ -38,7 +38,7 @@ def addressBookProviderArrayType(self) -> Optional[ElectronicAddressProperties]: A union of which Electronic Address properties are set on the contact. Property is stored in the MSG file as a sinlge int. The result should be - identical to addressBookProviderEmailList. + a union of the flags specified by addressBookProviderEmailList. """ return self._getNamedAs('8029', ps.PSETID_ADDRESS, ElectronicAddressProperties) @@ -47,7 +47,7 @@ def addressBookProviderEmailList(self) -> Optional[Set[ElectronicAddressProperti """ A set of which Electronic Address properties are set on the contact. """ - return self._getNamedAs('8028', ps.PSETID_ADDRESS, lambda x : {ElectronicAddressProperties(y) for y in x}) + return self._getNamedAs('8028', ps.PSETID_ADDRESS, ElectronicAddressProperties.fromIter) @functools.cached_property def assistant(self) -> Optional[str]: @@ -76,7 +76,7 @@ def billing(self) -> Optional[str]: """ Billing information for the contact. """ - return self._getNamedAs('8535', ps.PSETID_COMMON) + return self.getNamedProp('8535', ps.PSETID_COMMON) @functools.cached_property def birthday(self) -> Optional[datetime.datetime]: diff --git a/extract_msg/msg_classes/meeting_exception.py b/extract_msg/msg_classes/meeting_exception.py index 3748be0c..c704ccf6 100644 --- a/extract_msg/msg_classes/meeting_exception.py +++ b/extract_msg/msg_classes/meeting_exception.py @@ -35,7 +35,7 @@ def exceptionReplaceTime(self) -> Optional[datetime.datetime]: The date and time within the recurrence pattern that the exception will replace. The value is specified in UTC. """ - return self._getNamedAs('8228', constants.ps.PSETID_APPOINTMENT) + return self.getNamedProp('8228', constants.ps.PSETID_APPOINTMENT) @functools.cached_property def fExceptionalBody(self) -> bool: @@ -44,11 +44,11 @@ def fExceptionalBody(self) -> bool: differs from the Recurring Calendar object. If True, the Exception MUST have a body. """ - return self._getNamedAs('8206', constants.ps.PSETID_APPOINTMENT, bool, False) + return bool(self.getNamedProp('8206', constants.ps.PSETID_APPOINTMENT)) @functools.cached_property def fInvited(self) -> bool: """ Indicates if invitations have been sent for this exception. """ - return self._getNamedAs('8229', constants.ps.PSETID_APPOINTMENT, bool, False) + return bool(self.getNamedProp('8229', constants.ps.PSETID_APPOINTMENT)) diff --git a/extract_msg/msg_classes/meeting_forward.py b/extract_msg/msg_classes/meeting_forward.py index a4d0625f..3ab66caf 100644 --- a/extract_msg/msg_classes/meeting_forward.py +++ b/extract_msg/msg_classes/meeting_forward.py @@ -26,7 +26,7 @@ def forwardNotificationRecipients(self) -> Optional[bytes]: Incomplete, looks to be the same structure as appointmentUnsendableRecipients, so we need more examples of this. """ - return self._getNamedAs('8261', constants.ps.PSETID_APPOINTMENT) + return self.getNamedProp('8261', constants.ps.PSETID_APPOINTMENT) @property def headerFormatProperties(self) -> constants.HEADER_FORMAT_TYPE: @@ -96,4 +96,4 @@ def promptSendUpdate(self) -> bool: Indicates that the Meeting Forward Notification object was out-of-date when it was received. """ - return self._getNamedAs('8045', constants.ps.PSETID_COMMON, bool, False) + return bool(self.getNamedProp('8045', constants.ps.PSETID_COMMON)) diff --git a/extract_msg/msg_classes/meeting_request.py b/extract_msg/msg_classes/meeting_request.py index 5893ea75..cc6e2d6d 100644 --- a/extract_msg/msg_classes/meeting_request.py +++ b/extract_msg/msg_classes/meeting_request.py @@ -25,7 +25,7 @@ def appointmentMessageClass(self) -> Optional[str]: object that is to be generated from the Meeting Request object. MUST start with "IPM.Appointment". """ - return self._getNamedAs('0024', ps.PSETID_MEETING) + return self.getNamedProp('0024', ps.PSETID_MEETING) @functools.cached_property def calendarType(self) -> Optional[RecurCalendarType]: @@ -53,7 +53,7 @@ def forwardInstance(self) -> bool: recurring series, and it was forwarded (even when forwarded by the organizer) rather than being an invitation sent by the organizer. """ - return self._getNamedAs('820A', ps.PSETID_APPOINTMENT, bool, False) + return bool(self.getNamedProp('820A', ps.PSETID_APPOINTMENT)) @property def headerFormatProperties(self) -> HEADER_FORMAT_TYPE: @@ -154,7 +154,7 @@ def oldLocation(self) -> Optional[str]: """ The original value of the location property before a meeting update. """ - return self._getNamedAs('0028', ps.PSETID_MEETING) + return self.getNamedProp('0028', ps.PSETID_MEETING) @functools.cached_property def oldWhenEndWhole(self) -> Optional[datetime.datetime]: @@ -162,7 +162,7 @@ def oldWhenEndWhole(self) -> Optional[datetime.datetime]: The original value of the appointmentEndWhole property before a meeting update. """ - return self._getNamedAs('002A', ps.PSETID_MEETING) + return self.getNamedProp('002A', ps.PSETID_MEETING) @functools.cached_property def oldWhenStartWhole(self) -> Optional[datetime.datetime]: @@ -170,4 +170,4 @@ def oldWhenStartWhole(self) -> Optional[datetime.datetime]: The original value of the appointmentStartWhole property before a meeting update. """ - return self._getNamedAs('0029', ps.PSETID_MEETING) + return self.getNamedProp('0029', ps.PSETID_MEETING) diff --git a/extract_msg/msg_classes/meeting_response.py b/extract_msg/msg_classes/meeting_response.py index 67bd1d93..aa2e166c 100644 --- a/extract_msg/msg_classes/meeting_response.py +++ b/extract_msg/msg_classes/meeting_response.py @@ -23,7 +23,7 @@ def appointmentCounterProposal(self) -> bool: """ Indicates if the response is a counter proposal. """ - return self._getNamedAs('8257', ps.PSETID_APPOINTMENT, bool, False) + return bool(self.getNamedProp('8257', ps.PSETID_APPOINTMENT)) @functools.cached_property def appointmentProposedDuration(self) -> Optional[int]: @@ -31,7 +31,7 @@ def appointmentProposedDuration(self) -> Optional[int]: The proposed value for the appointmentDuration property for a counter proposal. """ - return self._getNamedAs('8256', ps.PSETID_APPOINTMENT) + return self.getNamedProp('8256', ps.PSETID_APPOINTMENT) @functools.cached_property def appointmentProposedEndWhole(self) -> Optional[datetime.datetime]: @@ -39,7 +39,7 @@ def appointmentProposedEndWhole(self) -> Optional[datetime.datetime]: The proposal value for the appointmentEndWhole property for a counter proposal. """ - return self._getNamedAs('8251', ps.PSETID_APPOINTMENT) + return self.getNamedProp('8251', ps.PSETID_APPOINTMENT) @functools.cached_property def appointmentProposedStartWhole(self) -> Optional[datetime.datetime]: @@ -47,7 +47,7 @@ def appointmentProposedStartWhole(self) -> Optional[datetime.datetime]: The proposal value for the appointmentStartWhole property for a counter proposal. """ - return self._getNamedAs('8250', ps.PSETID_APPOINTMENT) + return self.getNamedProp('8250', ps.PSETID_APPOINTMENT) @functools.cached_property def isSilent(self) -> bool: @@ -55,7 +55,7 @@ def isSilent(self) -> bool: Indicates if the user did not include any text in the body of the Meeting Response object. """ - return self._getNamedAs('0004', ps.PSETID_MEETING, bool, False) + return bool(self.getNamedProp('0004', ps.PSETID_MEETING)) @functools.cached_property def promptSendUpdate(self) -> bool: @@ -63,7 +63,7 @@ def promptSendUpdate(self) -> bool: Indicates that the Meeting Response object was out-of-date when it was received. """ - return self._getNamedAs('8045', ps.PSETID_COMMON, bool, False) + return bool(self.getNamedProp('8045', ps.PSETID_COMMON)) @functools.cached_property def responseType(self) -> ResponseType: diff --git a/extract_msg/msg_classes/msg.py b/extract_msg/msg_classes/msg.py index 046c5413..113acd11 100644 --- a/extract_msg/msg_classes/msg.py +++ b/extract_msg/msg_classes/msg.py @@ -209,24 +209,19 @@ def __enter__(self) -> MSGFile: def __exit__(self, *_) -> None: self.close() - def _getNamedAs(self, propertyName : str, guid : str, overrideClass = None, preserveNone : bool = True): + def _getNamedAs(self, propertyName : str, guid : str, overrideClass : Callable[..., _T]) -> Optional[_T]: """ Returns the named property, setting the class if specified. :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. + function or the function itself, if that is what is provided. If + the value is None, this function is not called. If you want it to + be called regardless, you should handle the data directly. """ value = self.getNamedProp(propertyName, guid) - # Check if we should be overriding the data type for this instance. - if overrideClass is not None: - if value is not None or not preserveNone: - value = overrideClass(value) - + if value is not None: + value = overrideClass(value) return value def _getOleEntry(self, filename, prefix : bool = True) -> olefile.olefile.OleDirectoryEntry: diff --git a/extract_msg/msg_classes/sticky_note.py b/extract_msg/msg_classes/sticky_note.py index ca9eaa00..843adb23 100644 --- a/extract_msg/msg_classes/sticky_note.py +++ b/extract_msg/msg_classes/sticky_note.py @@ -30,14 +30,14 @@ def noteHeight(self) -> Optional[int]: """ The height of the note window, in pixels. """ - return self._getNamedAs('8B03', constants.ps.PSETID_NOTE) + return self.getNamedProp('8B03', constants.ps.PSETID_NOTE) @functools.cached_property def noteWidth(self) -> Optional[int]: """ The width of the note window, in pixels. """ - return self._getNamedAs('8B02', constants.ps.PSETID_NOTE) + return self.getNamedProp('8B02', constants.ps.PSETID_NOTE) @functools.cached_property def noteX(self) -> Optional[int]: @@ -45,7 +45,7 @@ def noteX(self) -> Optional[int]: The distance, in pixels, from the left edge of the screen that a user interface displays the note. """ - return self._getNamedAs('8B02', constants.ps.PSETID_NOTE) + return self.getNamedProp('8B02', constants.ps.PSETID_NOTE) @functools.cached_property def noteY(self) -> Optional[int]: @@ -53,4 +53,4 @@ def noteY(self) -> Optional[int]: The distance, in pixels, from the top edge of the screen that a user interafce displays the note. """ - return self._getNamedAs('8B02', constants.ps.PSETID_NOTE) \ No newline at end of file + return self.getNamedProp('8B02', constants.ps.PSETID_NOTE) \ No newline at end of file diff --git a/extract_msg/msg_classes/task.py b/extract_msg/msg_classes/task.py index 71533955..2a8d8bd8 100644 --- a/extract_msg/msg_classes/task.py +++ b/extract_msg/msg_classes/task.py @@ -88,7 +88,7 @@ def percentComplete(self) -> Optional[float]: Indicates whether a time-flagged Message object is complete. Returns a percentage in decimal form. 1.0 indicates it is complete. """ - return self._getNamedAs('8102', constants.ps.PSETID_TASK) + return self.getNamedProp('8102', constants.ps.PSETID_TASK) @functools.cached_property def taskAcceptanceState(self) -> Optional[TaskAcceptance]: @@ -103,7 +103,7 @@ def taskAccepted(self) -> bool: Indicates whether a task assignee has replied to a tesk request for this task object. Does not indicate if it was accepted or rejected. """ - return self._getNamedAs('8108', constants.ps.PSETID_TASK, bool, False) + return bool(self.getNamedProp('8108', constants.ps.PSETID_TASK)) @functools.cached_property def taskActualEffort(self) -> Optional[int]: @@ -111,14 +111,14 @@ def taskActualEffort(self) -> Optional[int]: Indicates the number of minutes that the user actually spent working on a task. """ - return self._getNamedAs('8110', constants.ps.PSETID_TASK) + return self.getNamedProp('8110', constants.ps.PSETID_TASK) @functools.cached_property def taskAssigner(self) -> Optional[str]: """ Specifies the name of the user that last assigned the task. """ - return self._getNamedAs('8121', constants.ps.PSETID_TASK) + return self.getNamedProp('8121', constants.ps.PSETID_TASK) @functools.cached_property def taskAssigners(self) -> Optional[bytes]: @@ -128,28 +128,28 @@ def taskAssigners(self) -> Optional[bytes]: The documentation on this is weird, so I don't know how to parse it. """ - return self._getNamedAs('8117', constants.ps.PSETID_TASK) + return self.getNamedProp('8117', constants.ps.PSETID_TASK) @functools.cached_property def taskComplete(self) -> bool: """ Indicates if the task is complete. """ - return self._getNamedAs('811C', constants.ps.PSETID_TASK, bool, False) + return bool(self.getNamedProp('811C', constants.ps.PSETID_TASK)) @functools.cached_property def taskCustomFlags(self) -> Optional[int]: """ Custom flags set on the task. """ - return self._getNamedAs('8139', constants.ps.PSETID_TASK) + return self.getNamedProp('8139', constants.ps.PSETID_TASK) @functools.cached_property def taskDateCompleted(self) -> Optional[datetime.datetime]: """ The date when the user completed work on the task. """ - return self._getNamedAs('810F', constants.ps.PSETID_TASK) + return self.getNamedProp('810F', constants.ps.PSETID_TASK) @functools.cached_property def taskDeadOccurrence(self) -> bool: @@ -158,7 +158,7 @@ def taskDeadOccurrence(self) -> bool: False on a new Task object and True when the client generates the last recurring task. """ - return self._getNamedAs('8109', constants.ps.PSETID_TASK, bool, False) + return bool(self.getNamedProp('8109', constants.ps.PSETID_TASK)) @functools.cached_property def taskDueDate(self) -> Optional[datetime.datetime]: @@ -166,14 +166,14 @@ def taskDueDate(self) -> Optional[datetime.datetime]: Specifies the date by which the user expects work on the task to be complete. """ - return self._getNamedAs('8105', constants.ps.PSETID_TASK) + return self.getNamedProp('8105', constants.ps.PSETID_TASK) @functools.cached_property def taskEstimatedEffort(self) -> Optional[int]: """ Indicates the number of minutes that the user expects to work on a task. """ - return self._getNamedAs('8111', constants.ps.PSETID_TASK) + return self.getNamedProp('8111', constants.ps.PSETID_TASK) @functools.cached_property def taskFCreator(self) -> bool: @@ -182,21 +182,21 @@ def taskFCreator(self) -> bool: the current user or user agent instead of by the processing of a task request. """ - return self._getNamedAs('811E', constants.ps.PSETID_TASK, bool, False) + return bool(self.getNamedProp('811E', constants.ps.PSETID_TASK)) @functools.cached_property def taskFFixOffline(self) -> bool: """ Indicates whether the value of the taskOwner property is correct. """ - return self._getNamedAs('812C', constants.ps.PSETID_TASK, bool, False) + return bool(self.getNamedProp('812C', constants.ps.PSETID_TASK)) @functools.cached_property def taskFRecurring(self) -> bool: """ Indicates whether the task includes a recurrence pattern. """ - return self._getNamedAs('8126', constants.ps.PSETID_TASK, bool, False) + return bool(self.getNamedProp('8126', constants.ps.PSETID_TASK)) @functools.cached_property def taskGlobalID(self) -> Optional[bytes]: @@ -204,7 +204,7 @@ def taskGlobalID(self) -> Optional[bytes]: Specifies a unique GUID for this task, used to locate an existing task upon receipt of a task response or task update. """ - return self._getNamedAs('8519', constants.ps.PSETID_COMMON) + return self.getNamedProp('8519', constants.ps.PSETID_COMMON) @functools.cached_property def taskHistory(self) -> Optional[TaskHistory]: @@ -219,14 +219,14 @@ def taskLastDelegate(self) -> Optional[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._getNamedAs('8125', constants.ps.PSETID_TASK) + return self.getNamedProp('8125', constants.ps.PSETID_TASK) @functools.cached_property def taskLastUpdate(self) -> Optional[datetime.datetime]: """ The date and time of the most recent change made to the task object. """ - return self._getNamedAs('8115', constants.ps.PSETID_TASK) + return self.getNamedProp('8115', constants.ps.PSETID_TASK) @functools.cached_property def taskLastUser(self) -> Optional[str]: @@ -234,7 +234,7 @@ def taskLastUser(self) -> Optional[str]: Contains the name of the most recent user to have been the owner of the task. """ - return self._getNamedAs('8122', constants.ps.PSETID_TASK) + return self.getNamedProp('8122', constants.ps.PSETID_TASK) @functools.cached_property def taskMode(self) -> Optional[TaskMode]: @@ -257,7 +257,7 @@ def taskNoCompute(self) -> Optional[bool]: This value is not used and has no impact on a Task, but is provided for completeness. """ - return self._getNamedAs('8124', constants.ps.PSETID_TASK) + return self.getNamedProp('8124', constants.ps.PSETID_TASK) @functools.cached_property def taskOrdinal(self) -> Optional[int]: @@ -271,7 +271,7 @@ def taskOwner(self) -> Optional[str]: """ Contains the name of the owner of the task. """ - return self._getNamedAs('811F', constants.ps.PSETID_TASK) + return self.getNamedProp('811F', constants.ps.PSETID_TASK) @functools.cached_property def taskOwnership(self) -> Optional[TaskOwnership]: @@ -293,7 +293,7 @@ def taskResetReminder(self) -> bool: """ Indicates whether future recurring tasks need reminders. """ - return self._getNamedAs('8107', constants.ps.PSETID_TASK, bool, False) + return bool(self.getNamedProp('8107', constants.ps.PSETID_TASK)) @functools.cached_property def taskRole(self) -> Optional[str]: @@ -301,14 +301,14 @@ def taskRole(self) -> Optional[str]: This value is not used and has no impact on a Task, but is provided for completeness. """ - return self._getNamedAs('8127', constants.ps.PSETID_TASK) + return self.getNamedProp('8127', constants.ps.PSETID_TASK) @functools.cached_property def taskStartDate(self) -> Optional[datetime.datetime]: """ Specifies the date on which the user expects work on the task to begin. """ - return self._getNamedAs('8104', constants.ps.PSETID_TASK) + return self.getNamedProp('8104', constants.ps.PSETID_TASK) @functools.cached_property def taskState(self) -> Optional[TaskState]: @@ -330,7 +330,7 @@ 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._getNamedAs('8119', constants.ps.PSETID_TASK, bool, False) + return bool(self.getNamedProp('8119', constants.ps.PSETID_TASK)) @functools.cached_property def taskUpdates(self) -> bool: @@ -338,14 +338,14 @@ 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._getNamedAs('811B', constants.ps.PSETID_TASK, bool, False) + return bool(self.getNamedProp('811B', constants.ps.PSETID_TASK)) @functools.cached_property def taskVersion(self) -> Optional[int]: """ Indicates which copy is the latest update of a Task object. """ - return self._getNamedAs('8112', constants.ps.PSETID_TASK) + return self.getNamedProp('8112', constants.ps.PSETID_TASK) @functools.cached_property def teamTask(self) -> Optional[bool]: @@ -353,4 +353,4 @@ def teamTask(self) -> Optional[bool]: This value is not used and has no impact on a Task, but is provided for completeness. """ - return self._getNamedAs('8103', constants.ps.PSETID_TASK) + return self.getNamedProp('8103', constants.ps.PSETID_TASK) From c14966691431abae2882de33cd53a6e28451c178 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sat, 12 Aug 2023 16:23:34 -0700 Subject: [PATCH 18/20] Finish reworking significant parts of private api --- CHANGELOG.md | 8 +- docs/conf.py | 2 +- extract_msg/__init__.py | 2 +- extract_msg/attachments/attachment_base.py | 131 +++++++++---------- extract_msg/attachments/web_att.py | 4 +- extract_msg/msg_classes/appointment.py | 2 +- extract_msg/msg_classes/calendar.py | 2 +- extract_msg/msg_classes/calendar_base.py | 34 ++--- extract_msg/msg_classes/contact.py | 28 ++--- extract_msg/msg_classes/meeting_related.py | 2 +- extract_msg/msg_classes/meeting_request.py | 8 +- extract_msg/msg_classes/message_base.py | 2 +- extract_msg/msg_classes/msg.py | 138 +++++++++++---------- extract_msg/msg_classes/sticky_note.py | 2 +- extract_msg/msg_classes/task.py | 18 +-- extract_msg/msg_classes/task_request.py | 4 +- extract_msg/recipient.py | 109 ++++++++-------- 17 files changed, 258 insertions(+), 238 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e80e9714..3cd45afe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,11 +24,15 @@ * `getSingleOrMultipleString`: A combination of `getStringStream` and `getMultipleString` which prefers a single string stream. Returns a single bytes objecct or a list of bytes objects. * `getPropertyVal`: Shortcut for `instance.props.getValue` that allows new behavior to be added by overriding it. * `getNamedProp`: Shortcut for `instance.namedProperties.get((propertyName, guid), default)` that allows new behavior to be added by overriding it. -* Removed `Named._getStringStream` and `Named.sExists`. The named properties storage will *always* +* Removed `Named._getStringStream` and `Named.sExists`. The named properties storage will *always* use regular streams and not string streams. * Changed all `Named` methods to no longer have a prefix argument. The prefix should *always* be false sense the named property mapping will only exist in the top level directory. * Adjusted `tryGetMimeType` to allows any attachments whose `data` property would return a `bytes` instance. -* Changed internal code to use public SPI functions wherever possible. This includes making many private API functions use calls to the public API for getting bits of data. +* Changed internal code to use public API functions wherever possible. This includes making many private API functions use calls to the public API for getting bits of data. * Fixed potential issue with `AttachmentBase.clsid` which had the potential to cause some attachments to fail to generate a CLSID. +* Outright removed or changed a significant portion of the private API. I have rarely, if ever, seen references to these parts, so this should cause you no issues. Some of these have also been moved to the public API, either identically or with changes, and the mapping is as such: + * `_getNamedAs` -> `getNamedAs`: Changed to *always* require a conversion argument. If you were previously using it to plainly get a named property or to handle the properly being None or a real value, you should use the return value of `getNamedProp` instead. + * `_getPropertyAs` -> `getPropertyAs`: Same as above, use `getPropertyVal` instead for None or plain access. + * `_getStreamAs` -> `getStreamAs`, `getStringStreamAs`: Once again, see above. Use `getStream` and `getStringStream`, respectively. **v0.44.0** * Fixed a bug that caused `MessageBase.headerInit` to always return `False` after the 0.42.0 update. diff --git a/docs/conf.py b/docs/conf.py index ae8a0e11..0c112369 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,7 +12,7 @@ sys.path.insert(0, os.path.abspath("..")) __author__ = 'Destiny Peterson & Matthew Walker' -__version__ = '0.44.0' +__version__ = '0.45.0' __year__ = '2023' diff --git a/extract_msg/__init__.py b/extract_msg/__init__.py index cbf2b3e3..83251e96 100644 --- a/extract_msg/__init__.py +++ b/extract_msg/__init__.py @@ -27,7 +27,7 @@ # along with this program. If not, see . __author__ = 'Destiny Peterson & Matthew Walker' -__date__ = '2023-08-09' +__date__ = '2023-08-12' __version__ = '0.45.0' __all__ = [ diff --git a/extract_msg/attachments/attachment_base.py b/extract_msg/attachments/attachment_base.py index dad49c1f..90457cfd 100644 --- a/extract_msg/attachments/attachment_base.py +++ b/extract_msg/attachments/attachment_base.py @@ -59,42 +59,6 @@ def __init__(self, msg : MSGFile, dir_ : str, propStore : PropertiesStore): self.__namedProperties = NamedProperties(msg.named, self) self.__treePath = msg.treePath + [makeWeakRef(self)] - def _getNamedAs(self, propertyName : str, guid : str, overrideClass : Callable[..., _T]) -> Optional[_T]: - """ - Returns the named property, setting the class if specified. - - :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. If - the value is None, this function is not called. If you want it to - be called regardless, you should handle the data directly. - """ - value = self.getNamedProp(propertyName, guid) - if value is not None: - value = overrideClass(value) - return value - - def _getPropertyAs(self, propertyName, overrideClass = None, preserveNone : bool = True): - """ - Returns the property, setting the class if specified. - - :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. - """ - value = self.getPropertyVal(propertyName) - - # Check if we should be overriding the data type for this instance. - if overrideClass is not None: - if (value is not None or not preserveNone): - value = overrideClass(value) - - return value - def _getStream(self, filename) -> Optional[bytes]: """ Gets a binary representation of the requested filename. @@ -109,33 +73,6 @@ def _getStream(self, filename) -> Optional[bytes]: warnings.warn(':method _getStream: has been deprecated and moved to the public api. Use :method getStream: instead (remove the underscore).', DeprecationWarning) return self.getStream(filename) - def _getStreamAs(self, streamID, stringStream : bool = True, overrideClass = None, preserveNone : bool = True): - """ - Returns the specified stream, modifying it to the class if specified. - - 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. - """ - if stringStream: - value = self.getStringStream(streamID) - else: - value = self.getStream(streamID) - - # Check if we should be overriding the data type for this instance. - if overrideClass is not None: - if value is not None or not preserveNone: - value = overrideClass(value) - - return value - def _getStringStream(self, filename) -> Optional[str]: """ Gets a string representation of the requested filename. @@ -354,6 +291,21 @@ def getMultipleString(self, filename) -> Optional[List[str]]: raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') return msg.getMultipleString([self.__dir, msgPathToString(filename)]) + def getNamedAs(self, propertyName : str, guid : str, overrideClass : Callable[..., _T]) -> Optional[_T]: + """ + Returns the named property, setting the class if specified. + + :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. If + the value is None, this function is not called. If you want it to + be called regardless, you should handle the data directly. + """ + value = self.getNamedProp(propertyName, guid) + if value is not None: + value = overrideClass(value) + return value + def getNamedProp(self, propertyName : str, guid : str, default : _T = None) -> Union[Any, _T]: """ instance.namedProperties.get((propertyName, guid), default) @@ -362,6 +314,23 @@ def getNamedProp(self, propertyName : str, guid : str, default : _T = None) -> U """ return self.namedProperties.get((propertyName, guid), default) + def getPropertyAs(self, propertyName, overrideClass : Callable[..., _T]) -> Optional[_T]: + """ + Returns the property, setting the class if found. + + :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. If + the value is None, this function is not called. If you want it to + be called regardless, you should handle the data directly. + """ + value = self.getPropertyVal(propertyName) + + if value is not None: + value = overrideClass(value) + + return value + def getPropertyVal(self, name, default : _T = None) -> Union[Any, _T]: """ instance.props.getValue(name, default) @@ -422,6 +391,24 @@ def getStream(self, filename) -> Optional[bytes]: raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') return msg.getStream([self.__dir, msgPathToString(filename)]) + def getStreamAs(self, streamID, overrideClass : Callable[..., _T]) -> Optional[_T]: + """ + Returns the specified stream, modifying it to the specified class if it + is found. + + :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. If + the value is None, this function is not called. If you want it to + be called regardless, you should handle the data directly. + """ + value = self.getStream(streamID) + + if value is not None: + value = overrideClass(value) + + return value + def getStringStream(self, filename) -> Optional[str]: """ Gets a string representation of the requested filename. @@ -437,6 +424,24 @@ def getStringStream(self, filename) -> Optional[str]: raise ReferenceError('The msg file for this Attachment instance has been garbage collected.') return msg.getStringStream([self.__dir, msgPathToString(filename)]) + def getStringStreamAs(self, streamID, overrideClass : Callable[..., _T]) -> Optional[_T]: + """ + Returns the specified string stream, modifying it to the specified + class if it is found. + + :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. If + the value is None, this function is not called. If you want it to + be called regardless, you should handle the data directly. + """ + value = self.getStream(streamID) + + if value is not None: + value = overrideClass(value) + + return value + @abc.abstractmethod def getFilename(self, **kwargs) -> str: """ diff --git a/extract_msg/attachments/web_att.py b/extract_msg/attachments/web_att.py index 035def80..d2c4fb4e 100644 --- a/extract_msg/attachments/web_att.py +++ b/extract_msg/attachments/web_att.py @@ -43,14 +43,14 @@ def originalPermissionType(self) -> Optional[AttachmentPermissionType]: """ The permission type data associated with a web reference attachment. """ - return self._getNamedAs('AttachmentOriginalPermissionType', constants.ps.PSETID_ATTACHMENT, AttachmentPermissionType) + return self.getNamedAs('AttachmentOriginalPermissionType', constants.ps.PSETID_ATTACHMENT, AttachmentPermissionType) @functools.cached_property def permissionType(self) -> Optional[AttachmentPermissionType]: """ The permission type data associated with a web reference attachment. """ - return self._getNamedAs('AttachmentPermissionType', constants.ps.PSETID_ATTACHMENT, AttachmentPermissionType) + return self.getNamedAs('AttachmentPermissionType', constants.ps.PSETID_ATTACHMENT, AttachmentPermissionType) @functools.cached_property def providerName(self) -> Optional[str]: diff --git a/extract_msg/msg_classes/appointment.py b/extract_msg/msg_classes/appointment.py index 33bdf2bb..a770928c 100644 --- a/extract_msg/msg_classes/appointment.py +++ b/extract_msg/msg_classes/appointment.py @@ -175,4 +175,4 @@ def originalStoreEntryID(self) -> Optional[EntryID]: """ The EntryID of the delegator's message store. """ - return self._getNamedAs('8237', ps.PSETID_APPOINTMENT, EntryID.autoCreate) + return self.getNamedAs('8237', ps.PSETID_APPOINTMENT, EntryID.autoCreate) diff --git a/extract_msg/msg_classes/calendar.py b/extract_msg/msg_classes/calendar.py index 584a8ec5..0a4384d5 100644 --- a/extract_msg/msg_classes/calendar.py +++ b/extract_msg/msg_classes/calendar.py @@ -23,7 +23,7 @@ def clientIntent(self) -> Optional[ClientIntentFlag]: """ A set of the actions a user has taken on a Meeting object. """ - return self._getNamedAs('0015', ps.PSETID_CALENDAR_ASSISTANT, ClientIntentFlag) + return self.getNamedAs('0015', ps.PSETID_CALENDAR_ASSISTANT, ClientIntentFlag) @functools.cached_property def fExceptionalAttendees(self) -> Optional[bool]: diff --git a/extract_msg/msg_classes/calendar_base.py b/extract_msg/msg_classes/calendar_base.py index 007853fb..d95bd380 100644 --- a/extract_msg/msg_classes/calendar_base.py +++ b/extract_msg/msg_classes/calendar_base.py @@ -40,14 +40,14 @@ def appointmentAuxilaryFlags(self) -> Optional[AppointmentAuxilaryFlag]: """ The auxiliary state of the object. """ - return self._getNamedAs('8207', ps.PSETID_APPOINTMENT, AppointmentAuxilaryFlag) + return self.getNamedAs('8207', ps.PSETID_APPOINTMENT, AppointmentAuxilaryFlag) @functools.cached_property def appointmentColor(self) -> Optional[AppointmentColor]: """ The color to be used when displaying a Calendar object. """ - return self._getNamedAs('8214', ps.PSETID_APPOINTMENT, AppointmentColor) + return self.getNamedAs('8214', ps.PSETID_APPOINTMENT, AppointmentColor) @functools.cached_property def appointmentDuration(self) -> Optional[int]: @@ -77,7 +77,7 @@ def appointmentRecur(self) -> Optional[RecurrencePattern]: Specifies the dates and times when a recurring series occurs by using one of the recurrence patterns and ranges specified in this section. """ - return self._getNamedAs('8216', ps.PSETID_APPOINTMENT, RecurrencePattern) + return self.getNamedAs('8216', ps.PSETID_APPOINTMENT, RecurrencePattern) @functools.cached_property def appointmentSequence(self) -> Optional[int]: @@ -100,7 +100,7 @@ def appointmentStateFlags(self) -> Optional[AppointmentStateFlag]: """ The appointment state of the object. """ - return self._getNamedAs('8217', ps.PSETID_APPOINTMENT, AppointmentStateFlag) + return self.getNamedAs('8217', ps.PSETID_APPOINTMENT, AppointmentStateFlag) @functools.cached_property def appointmentSubType(self) -> bool: @@ -115,7 +115,7 @@ def appointmentTimeZoneDefinitionEndDisplay(self) -> Optional[TimeZoneDefinition Specifies the time zone information for the appointmentEndWhole property Used to convert the end date and time to and from UTC. """ - return self._getNamedAs('825F', ps.PSETID_APPOINTMENT, TimeZoneDefinition) + return self.getNamedAs('825F', ps.PSETID_APPOINTMENT, TimeZoneDefinition) @functools.cached_property def appointmentTimeZoneDefinitionRecur(self) -> Optional[TimeZoneDefinition]: @@ -123,7 +123,7 @@ def appointmentTimeZoneDefinitionRecur(self) -> Optional[TimeZoneDefinition]: Specified the time zone information that specifies how to convert the meeting date and time on a recurring series to and from UTC. """ - return self._getNamedAs('8260', ps.PSETID_APPOINTMENT, TimeZoneDefinition) + return self.getNamedAs('8260', ps.PSETID_APPOINTMENT, TimeZoneDefinition) @functools.cached_property def appointmentTimeZoneDefinitionStartDisplay(self) -> Optional[TimeZoneDefinition]: @@ -131,7 +131,7 @@ def appointmentTimeZoneDefinitionStartDisplay(self) -> Optional[TimeZoneDefiniti Specifies the time zone information for the appointmentStartWhole property. Used to convert the start date and time to and from UTC. """ - return self._getNamedAs('825E', ps.PSETID_APPOINTMENT, TimeZoneDefinition) + return self.getNamedAs('825E', ps.PSETID_APPOINTMENT, TimeZoneDefinition) @functools.cached_property def appointmentUnsendableRecipients(self) -> Optional[bytes]: @@ -163,7 +163,7 @@ def birthdayContactEntryID(self) -> Optional[EntryID]: """ Indicates the EntryID of the contact associated with the birthday event. """ - return self._getNamedAs('BirthdayContactEntryId', ps.PSETID_ADDRESS, EntryID.autoCreate) + return self.getNamedAs('BirthdayContactEntryId', ps.PSETID_ADDRESS, EntryID.autoCreate) @functools.cached_property def birthdayContactPersonGuid(self) -> Optional[bytes]: @@ -179,7 +179,7 @@ def busyStatus(self) -> Optional[BusyStatus]: Specified the availability of a user for the event described by the object. """ - return self._getNamedAs('8205', ps.PSETID_APPOINTMENT, BusyStatus) + return self.getNamedAs('8205', ps.PSETID_APPOINTMENT, BusyStatus) @functools.cached_property def cc(self) -> Optional[str]: @@ -202,7 +202,7 @@ def cleanGlobalObjectID(self) -> Optional[GlobalObjectID]: an Exception object to a recurring series, where the year, month, and day fields are all 0. """ - return self._getNamedAs('0023', ps.PSETID_MEETING, GlobalObjectID) + return self.getNamedAs('0023', ps.PSETID_MEETING, GlobalObjectID) @functools.cached_property def clipEnd(self) -> Optional[datetime.datetime]: @@ -255,14 +255,14 @@ def globalObjectID(self) -> Optional[GlobalObjectID]: """ The unique identifier or the Calendar object. """ - return self._getNamedAs('0003', ps.PSETID_MEETING, GlobalObjectID) + return self.getNamedAs('0003', ps.PSETID_MEETING, GlobalObjectID) @functools.cached_property def iconIndex(self) -> Optional[Union[IconIndex, int]]: """ The icon to use for the object. """ - return self._getPropertyAs('10800003', IconIndex.tryMake) + return self.getPropertyAs('10800003', IconIndex.tryMake) @functools.cached_property def isBirthdayContactWritable(self) -> bool: @@ -299,7 +299,7 @@ def linkedTaskItems(self) -> Optional[List[EntryID]]: A list of PidTagEntryId properties of Task objects related to the Calendar object that are set by a client. """ - return self._getNamedAs('820C', ps.PSETID_APPOINTMENT, lambda x : list(EntryID.autoCreate(y) for y in x)) + return self.getNamedAs('820C', ps.PSETID_APPOINTMENT, lambda x : list(EntryID.autoCreate(y) for y in x)) @functools.cached_property def location(self) -> Optional[str]: @@ -349,21 +349,21 @@ def nonSendBccTrackStatus(self) -> Optional[List[ResponseStatus]]: """ A ResponseStatus for each of the attendees in nonSendableBcc. """ - return self._getNamedAs('8545', ps.PSETID_COMMON, ResponseStatus.fromIter) + return self.getNamedAs('8545', ps.PSETID_COMMON, ResponseStatus.fromIter) @functools.cached_property def nonSendCcTrackStatus(self) -> Optional[List[ResponseStatus]]: """ A ResponseStatus for each of the attendees in nonSendableCc. """ - return self._getNamedAs('8544', ps.PSETID_COMMON, ResponseStatus.fromIter) + return self.getNamedAs('8544', ps.PSETID_COMMON, ResponseStatus.fromIter) @functools.cached_property def nonSendToTrackStatus(self) -> Optional[List[ResponseStatus]]: """ A ResponseStatus for each of the attendees in nonSendableTo. """ - return self._getNamedAs('8543', ps.PSETID_COMMON, ResponseStatus.fromIter) + return self.getNamedAs('8543', ps.PSETID_COMMON, ResponseStatus.fromIter) @functools.cached_property def optionalAttendees(self) -> Optional[str]: @@ -467,7 +467,7 @@ def timeZoneStruct(self) -> Optional[TimeZoneStruct]: Set on a recurring series to specify time zone information. Specifies how to convert time fields between local time and UTC. """ - return self._getNamedAs('8233', ps.PSETID_APPOINTMENT, TimeZoneStruct) + return self.getNamedAs('8233', ps.PSETID_APPOINTMENT, TimeZoneStruct) @functools.cached_property def to(self) -> Optional[str]: diff --git a/extract_msg/msg_classes/contact.py b/extract_msg/msg_classes/contact.py index adbc21b9..36fa727a 100644 --- a/extract_msg/msg_classes/contact.py +++ b/extract_msg/msg_classes/contact.py @@ -40,14 +40,14 @@ def addressBookProviderArrayType(self) -> Optional[ElectronicAddressProperties]: Property is stored in the MSG file as a sinlge int. The result should be a union of the flags specified by addressBookProviderEmailList. """ - return self._getNamedAs('8029', ps.PSETID_ADDRESS, ElectronicAddressProperties) + return self.getNamedAs('8029', ps.PSETID_ADDRESS, ElectronicAddressProperties) @functools.cached_property def addressBookProviderEmailList(self) -> Optional[Set[ElectronicAddressProperties]]: """ A set of which Electronic Address properties are set on the contact. """ - return self._getNamedAs('8028', ps.PSETID_ADDRESS, ElectronicAddressProperties.fromIter) + return self.getNamedAs('8028', ps.PSETID_ADDRESS, ElectronicAddressProperties.fromIter) @functools.cached_property def assistant(self) -> Optional[str]: @@ -91,7 +91,7 @@ def birthdayEventEntryID(self) -> Optional[EntryID]: The EntryID of an optional Appointement object that represents the contact's birtday. """ - return self._getNamedAs('804D', ps.PSETID_ADDRESS, EntryID.autoCreate) + return self.getNamedAs('804D', ps.PSETID_ADDRESS, EntryID.autoCreate) @functools.cached_property def birthdayLocal(self) -> Optional[datetime.datetime]: @@ -144,7 +144,7 @@ def businessCardDisplayDefinition(self) -> Optional[BusinessCardDisplayDefinitio Specifies the customization details for displaying a contact as a business card. """ - return self._getNamedAs('8040', ps.PSETID_ADDRESS, BusinessCardDisplayDefinition) + return self.getNamedAs('8040', ps.PSETID_ADDRESS, BusinessCardDisplayDefinition) @functools.cached_property def businessFax(self) -> Optional[Dict]: @@ -198,7 +198,7 @@ def businessFaxOriginalEntryId(self) -> Optional[EntryID]: """ The one-off EntryID corresponding to this fax address. """ - return self._getNamedAs('80C5', ps.PSETID_ADDRESS, EntryID.autoCreate) + return self.getNamedAs('80C5', ps.PSETID_ADDRESS, EntryID.autoCreate) @functools.cached_property def businessTelephoneNumber(self) -> Optional[str]: @@ -282,7 +282,7 @@ def contactLinkedGlobalAddressListEntryID(self) -> Optional[EntryID]: """ The EntryID of the GAL object to which the duplicate contact is linked. """ - return self._getNamedAs('80E2', ps.PSETID_ADDRESS, EntryID.autoCreate) + return self.getNamedAs('80E2', ps.PSETID_ADDRESS, EntryID.autoCreate) @functools.cached_property def contactLinkGlobalAddressListLinkID(self) -> Optional[str]: @@ -296,7 +296,7 @@ def contactLinkGlobalAddressListLinkState(self) -> Optional[ContactLinkState]: """ The state of linking between the GAL contact and the duplicate contact. """ - return self._getNamedAs('80E6', ps.PSETID_ADDRESS, ContactLinkState) + return self.getNamedAs('80E6', ps.PSETID_ADDRESS, ContactLinkState) @functools.cached_property def contactLinkLinkRejectHistory(self) -> Optional[List[bytes]]: @@ -434,7 +434,7 @@ def email1OriginalEntryId(self) -> Optional[EntryID]: """ The EntryID of the object correspinding to this electronic address. """ - return self._getNamedAs('8085', ps.PSETID_ADDRESS, EntryID.autoCreate) + return self.getNamedAs('8085', ps.PSETID_ADDRESS, EntryID.autoCreate) @functools.cached_property def email2(self) -> Optional[Dict]: @@ -485,7 +485,7 @@ def email2OriginalEntryId(self) -> Optional[EntryID]: """ The EntryID of the object correspinding to this electronic address. """ - return self._getNamedAs('8095', ps.PSETID_ADDRESS, EntryID.autoCreate) + return self.getNamedAs('8095', ps.PSETID_ADDRESS, EntryID.autoCreate) @functools.cached_property def email3(self) -> Optional[Dict]: @@ -536,7 +536,7 @@ def email3OriginalEntryId(self) -> Optional[EntryID]: """ The EntryID of the object correspinding to this electronic address. """ - return self._getNamedAs('80A5', ps.PSETID_ADDRESS, EntryID.autoCreate) + return self.getNamedAs('80A5', ps.PSETID_ADDRESS, EntryID.autoCreate) @functools.cached_property def emails(self) -> Tuple[Union[Dict, None], Union[Dict, None], Union[Dict, None]]: @@ -835,7 +835,7 @@ def homeFaxOriginalEntryId(self) -> Optional[EntryID]: """ The one-off EntryID corresponding to this fax address. """ - return self._getNamedAs('80D5', ps.PSETID_ADDRESS, EntryID.autoCreate) + return self.getNamedAs('80D5', ps.PSETID_ADDRESS, EntryID.autoCreate) @functools.cached_property def homeTelephoneNumber(self) -> Optional[str]: @@ -1174,7 +1174,7 @@ def primaryFaxOriginalEntryId(self) -> Optional[EntryID]: """ The one-off EntryID corresponding to this fax address. """ - return self._getNamedAs('80B5', ps.PSETID_ADDRESS, EntryID.autoCreate) + return self.getNamedAs('80B5', ps.PSETID_ADDRESS, EntryID.autoCreate) @functools.cached_property def primaryTelephoneNumber(self) -> Optional[str]: @@ -1204,7 +1204,7 @@ def referenceEntryID(self) -> Optional[EntryID]: Contact object unless the Contact object is a copy of an earlier original. """ - return self._getNamedAs('85BD', ps.PSETID_COMMON, EntryID.autoCreate) + return self.getNamedAs('85BD', ps.PSETID_COMMON, EntryID.autoCreate) @functools.cached_property def referredByName(self) -> Optional[str]: @@ -1262,7 +1262,7 @@ def weddingAnniversaryEventEntryID(self) -> Optional[EntryID]: The EntryID of an optional Appointement object that represents the contact's wedding anniversary. """ - return self._getNamedAs('804E', ps.PSETID_ADDRESS, EntryID.autoCreate) + return self.getNamedAs('804E', ps.PSETID_ADDRESS, EntryID.autoCreate) @functools.cached_property def weddingAnniversaryLocal(self) -> Optional[datetime.datetime]: diff --git a/extract_msg/msg_classes/meeting_related.py b/extract_msg/msg_classes/meeting_related.py index c963f998..509581a7 100644 --- a/extract_msg/msg_classes/meeting_related.py +++ b/extract_msg/msg_classes/meeting_related.py @@ -46,7 +46,7 @@ def serverProcessingActions(self) -> Optional[ServerProcessingAction]: A union of which actions have been taken on the Meeting Request object or Meeting Update object. """ - return self._getNamedAs('85CD', ps.PSETID_CALENDAR_ASSISTANT, ServerProcessingAction) + return self.getNamedAs('85CD', ps.PSETID_CALENDAR_ASSISTANT, ServerProcessingAction) @functools.cached_property def timeZone(self) -> Optional[int]: diff --git a/extract_msg/msg_classes/meeting_request.py b/extract_msg/msg_classes/meeting_request.py index cc6e2d6d..3c56a868 100644 --- a/extract_msg/msg_classes/meeting_request.py +++ b/extract_msg/msg_classes/meeting_request.py @@ -34,7 +34,7 @@ def calendarType(self) -> Optional[RecurCalendarType]: property if the Meeting Request object represents a recurring series or an exception. """ - return self._getNamedAs('001C', ps.PSETID_MEETING, RecurCalendarType) + return self.getNamedAs('001C', ps.PSETID_MEETING, RecurCalendarType) @functools.cached_property def changeHighlight(self) -> Optional[MeetingObjectChange]: @@ -44,7 +44,7 @@ def changeHighlight(self) -> Optional[MeetingObjectChange]: Returns a union of the set flags. """ - return self._getNamedAs('8204', ps.PSETID_APPOINTMENT, MeetingObjectChange) + return self.getNamedAs('8204', ps.PSETID_APPOINTMENT, MeetingObjectChange) @functools.cached_property def forwardInstance(self) -> bool: @@ -140,14 +140,14 @@ def intendedBusyStatus(self) -> Optional[BusyStatus]: calendar at the time the Meeting Request object or Meeting Update object was sent. """ - return self._getNamedAs('8224', ps.PSETID_APPOINTMENT, BusyStatus) + return self.getNamedAs('8224', ps.PSETID_APPOINTMENT, BusyStatus) @functools.cached_property def meetingType(self) -> Optional[MeetingType]: """ The type of Meeting Request object or Meeting Update object. """ - return self._getNamedAs('0026', ps.PSETID_MEETING, MeetingType) + return self.getNamedAs('0026', ps.PSETID_MEETING, MeetingType) @functools.cached_property def oldLocation(self) -> Optional[str]: diff --git a/extract_msg/msg_classes/message_base.py b/extract_msg/msg_classes/message_base.py index 18179800..1db80fca 100644 --- a/extract_msg/msg_classes/message_base.py +++ b/extract_msg/msg_classes/message_base.py @@ -1266,7 +1266,7 @@ def reportTag(self) -> Optional[ReportTag]: """ Data that is used to correlate the report and the original message. """ - return self._getStreamAs('__substg1.0_00310102', False, ReportTag) + return self.getStreamAs('__substg1.0_00310102', ReportTag) @functools.cached_property def rtfBody(self) -> Optional[bytes]: diff --git a/extract_msg/msg_classes/msg.py b/extract_msg/msg_classes/msg.py index 113acd11..2dcf5383 100644 --- a/extract_msg/msg_classes/msg.py +++ b/extract_msg/msg_classes/msg.py @@ -209,21 +209,6 @@ def __enter__(self) -> MSGFile: def __exit__(self, *_) -> None: self.close() - def _getNamedAs(self, propertyName : str, guid : str, overrideClass : Callable[..., _T]) -> Optional[_T]: - """ - Returns the named property, setting the class if specified. - - :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. If - the value is None, this function is not called. If you want it to - be called regardless, you should handle the data directly. - """ - value = self.getNamedProp(propertyName, guid) - if value is not None: - value = overrideClass(value) - return value - def _getOleEntry(self, filename, prefix : bool = True) -> olefile.olefile.OleDirectoryEntry: """ Finds the directory entry from the olefile for the stream or storage @@ -240,26 +225,6 @@ def _getOleEntry(self, filename, prefix : bool = True) -> olefile.olefile.OleDir return self.__ole.direntries[sid] - def _getPropertyAs(self, propertyName, overrideClass = None, preserveNone : bool = True): - """ - Returns the property, setting the class if specified. - - :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. - """ - value = self.getPropertyVal(propertyName) - # Check if we should be overriding the data type for this instance. - if overrideClass is not None: - if (value is not None or not preserveNone): - value = overrideClass(value) - - return value - def _getStream(self, filename, prefix : bool = True) -> Optional[bytes]: """ Gets a binary representation of the requested filename. @@ -271,33 +236,6 @@ def _getStream(self, filename, prefix : bool = True) -> Optional[bytes]: warnings.warn(':method _getStream: has been deprecated and moved to the public api. Use :method getStream: instead (remove the underscore).', DeprecationWarning) return self.getStream(filename, prefix) - def _getStreamAs(self, streamID, stringStream : bool = True, overrideClass = None, preserveNone : bool = True): - """ - Returns the specified stream, modifying it to the class if specified. - - 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. - """ - if stringStream: - value = self.getStringStream(streamID) - else: - value = self.getStream(streamID) - - # Check if we should be overriding the data type for this instance. - if overrideClass is not None: - if value is not None or not preserveNone: - value = overrideClass(value) - - return value - def _getStringStream(self, filename, prefix : bool = True) -> Optional[str]: """ Gets a string representation of the requested filename. @@ -601,6 +539,21 @@ def getMultipleString(self, filename, prefix : bool = True) -> Optional[List[str ret[index] = item.decode(self.stringEncoding)[:-1] return ret + def getNamedAs(self, propertyName : str, guid : str, overrideClass : Callable[..., _T]) -> Optional[_T]: + """ + Returns the named property, setting the class if specified. + + :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. If + the value is None, this function is not called. If you want it to + be called regardless, you should handle the data directly. + """ + value = self.getNamedProp(propertyName, guid) + if value is not None: + value = overrideClass(value) + return value + def getNamedProp(self, propertyName : str, guid : str, default : _T = None) -> Union[Any, _T]: """ instance.namedProperties.get((propertyName, guid), default) @@ -609,6 +562,23 @@ def getNamedProp(self, propertyName : str, guid : str, default : _T = None) -> U """ return self.namedProperties.get((propertyName, guid), default) + def getPropertyAs(self, propertyName, overrideClass : Callable[..., _T]) -> Optional[_T]: + """ + Returns the property, setting the class if found. + + :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. If + the value is None, this function is not called. If you want it to + be called regardless, you should handle the data directly. + """ + value = self.getPropertyVal(propertyName) + + if value is not None: + value = overrideClass(value) + + return value + def getPropertyVal(self, name, default : _T = None) -> Union[Any, _T]: """ instance.props.getValue(name, default) @@ -675,6 +645,24 @@ def getStream(self, filename, prefix : bool = True) -> Optional[bytes]: logger.info(f'Stream "{filename}" was requested but could not be found. Returning `None`.') return None + def getStreamAs(self, streamID, overrideClass : Callable[..., _T]) -> Optional[_T]: + """ + Returns the specified stream, modifying it to the specified class if it + is found. + + :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. If + the value is None, this function is not called. If you want it to + be called regardless, you should handle the data directly. + """ + value = self.getStream(streamID) + + if value is not None: + value = overrideClass(value) + + return value + def getStringStream(self, filename, prefix : bool = True) -> Optional[str]: """ Gets a string representation of the requested filename. @@ -696,6 +684,24 @@ def getStringStream(self, filename, prefix : bool = True) -> Optional[str]: tmp = self.getStream(filename + '001E', prefix = False) return None if tmp is None else tmp.decode(self.stringEncoding) + def getStringStreamAs(self, streamID, overrideClass : Callable[..., _T]) -> Optional[_T]: + """ + Returns the specified string stream, modifying it to the specified + class if it is found. + + :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. If + the value is None, this function is not called. If you want it to + be called regardless, you should handle the data directly. + """ + value = self.getStream(streamID) + + if value is not None: + value = overrideClass(value) + + return value + def listDir(self, streams : bool = True, storages : bool = False, includePrefix : bool = True) -> List[List[str]]: """ Replacement for OleFileIO.listdir that runs at the current prefix @@ -878,7 +884,7 @@ def importance(self) -> Optional[Importance]: """ The specified importance of the msg file. """ - return self._getPropertyAs('00170003', Importance) + return self.getPropertyAs('00170003', Importance) @property def importanceString(self) -> Union[str, None]: @@ -987,7 +993,7 @@ def priority(self) -> Optional[Priority]: """ The specified priority of the msg file. """ - return self._getPropertyAs('00260003', Priority) + return self.getPropertyAs('00260003', Priority) @functools.cached_property def props(self) -> PropertiesStore: @@ -1009,7 +1015,7 @@ def sensitivity(self) -> Optional[Sensitivity]: """ The specified sensitivity of the msg file. """ - return self._getPropertyAs('00360003', Sensitivity) + return self.getPropertyAs('00360003', Sensitivity) @functools.cached_property def sideEffects(self) -> Optional[SideEffect]: @@ -1017,7 +1023,7 @@ def sideEffects(self) -> Optional[SideEffect]: Controls how a Message object is handled by the client in relation to certain user interface actions by the user, such as deleting a message. """ - return self._getNamedAs('8510', constants.ps.PSETID_COMMON, SideEffect) + return self.getNamedAs('8510', constants.ps.PSETID_COMMON, SideEffect) @property def stringEncoding(self): diff --git a/extract_msg/msg_classes/sticky_note.py b/extract_msg/msg_classes/sticky_note.py index 843adb23..b382228b 100644 --- a/extract_msg/msg_classes/sticky_note.py +++ b/extract_msg/msg_classes/sticky_note.py @@ -23,7 +23,7 @@ def noteColor(self) -> Optional[NoteColor]: """ The color of the sticky note. """ - return self._getNamedAs('8B00', constants.ps.PSETID_NOTE, NoteColor) + return self.getNamedAs('8B00', constants.ps.PSETID_NOTE, NoteColor) @functools.cached_property def noteHeight(self) -> Optional[int]: diff --git a/extract_msg/msg_classes/task.py b/extract_msg/msg_classes/task.py index 2a8d8bd8..23b1c2cd 100644 --- a/extract_msg/msg_classes/task.py +++ b/extract_msg/msg_classes/task.py @@ -95,7 +95,7 @@ def taskAcceptanceState(self) -> Optional[TaskAcceptance]: """ Indicates the acceptance state of the task. """ - return self._getNamedAs('812A', constants.ps.PSETID_TASK, TaskAcceptance) + return self.getNamedAs('812A', constants.ps.PSETID_TASK, TaskAcceptance) @functools.cached_property def taskAccepted(self) -> bool: @@ -211,7 +211,7 @@ def taskHistory(self) -> Optional[TaskHistory]: """ Indicates the type of change that was last made to the Task object. """ - return self._getNamedAs('811A', constants.ps.PSETID_TASK, TaskHistory) + return self.getNamedAs('811A', constants.ps.PSETID_TASK, TaskHistory) @functools.cached_property def taskLastDelegate(self) -> Optional[str]: @@ -241,7 +241,7 @@ def taskMode(self) -> Optional[TaskMode]: """ Used in a task communication. Should be 0 (UNASSIGNED) on task objects. """ - return self._getNamedAs('8518', constants.ps.PSETID_COMMON, TaskMode) + return self.getNamedAs('8518', constants.ps.PSETID_COMMON, TaskMode) @functools.cached_property def taskMultipleRecipients(self) -> Optional[TaskMultipleRecipients]: @@ -249,7 +249,7 @@ def taskMultipleRecipients(self) -> Optional[TaskMultipleRecipients]: Returns a union of flags that specify optimization hints about the recipients of a Task object. """ - return self._getNamedAs('8120', constants.ps.PSETID_TASK, TaskMultipleRecipients) + return self.getNamedAs('8120', constants.ps.PSETID_TASK, TaskMultipleRecipients) @functools.cached_property def taskNoCompute(self) -> Optional[bool]: @@ -264,7 +264,7 @@ def taskOrdinal(self) -> Optional[int]: """ Specifies a number that aids custom sorting of Task objects. """ - return self._getNamedAs('8123', constants.ps.PSETID_TASK, unsignedToSignedInt) + return self.getNamedAs('8123', constants.ps.PSETID_TASK, unsignedToSignedInt) @functools.cached_property def taskOwner(self) -> Optional[str]: @@ -278,7 +278,7 @@ def taskOwnership(self) -> Optional[TaskOwnership]: """ Contains the name of the owner of the task. """ - return self._getNamedAs('8129', constants.ps.PSETID_TASK, TaskOwnership) + return self.getNamedAs('8129', constants.ps.PSETID_TASK, TaskOwnership) @functools.cached_property def taskRecurrence(self) -> Optional[RecurrencePattern]: @@ -286,7 +286,7 @@ def taskRecurrence(self) -> Optional[RecurrencePattern]: Contains a RecurrencePattern structure that provides information about recurring tasks. """ - return self._getNamedAs('8116', constants.ps.PSETID_TASK, RecurrencePattern) + return self.getNamedAs('8116', constants.ps.PSETID_TASK, RecurrencePattern) @functools.cached_property def taskResetReminder(self) -> bool: @@ -315,14 +315,14 @@ def taskState(self) -> Optional[TaskState]: """ Indicates the current assignment state of the Task object. """ - return self._getNamedAs('8113', constants.ps.PSETID_TASK, TaskState) + return self.getNamedAs('8113', constants.ps.PSETID_TASK, TaskState) @functools.cached_property def taskStatus(self) -> Optional[TaskStatus]: """ The completion status of a task. """ - return self._getNamedAs('8101', constants.ps.PSETID_TASK, TaskStatus) + return self.getNamedAs('8101', constants.ps.PSETID_TASK, TaskStatus) @functools.cached_property def taskStatusOnComplete(self) -> bool: diff --git a/extract_msg/msg_classes/task_request.py b/extract_msg/msg_classes/task_request.py index 3fa17035..34a83d7a 100644 --- a/extract_msg/msg_classes/task_request.py +++ b/extract_msg/msg_classes/task_request.py @@ -67,7 +67,7 @@ def taskMode(self) -> Optional[TaskMode]: """ The assignment status of the embedded Task object. """ - return self._getNamedAs('8518', constants.ps.PSETID_COMMON, TaskMode) + return self.getNamedAs('8518', constants.ps.PSETID_COMMON, TaskMode) @functools.cached_property def taskObject(self) -> Optional[Task]: @@ -105,4 +105,4 @@ def taskRequestType(self) -> TaskRequestType: """ The type of task request. """ - return self._getStreamAs('__substg1.0_001A', TaskRequestType.fromClassType) + return self.getStringStreamAs('__substg1.0_001A', TaskRequestType.fromClassType) diff --git a/extract_msg/recipient.py b/extract_msg/recipient.py index ddeaa8fe..72ee3a97 100644 --- a/extract_msg/recipient.py +++ b/extract_msg/recipient.py @@ -9,7 +9,9 @@ import functools import logging -from typing import Any, List, Optional, Tuple, TYPE_CHECKING, TypeVar, Union +from typing import ( + Any, Callable, List, Optional, Tuple, TYPE_CHECKING, TypeVar, Union + ) from .enums import ErrorBehavior, MeetingRecipientType, PropertiesType, RecipientType from .exceptions import StandardViolationError @@ -54,29 +56,6 @@ def __init__(self, _dir, msg : MSGFile): self.__type = RecipientType(0xF & self.__typeFlags) self.__formatted = f'{self.__name} <{self.__email}>' - def _getPropertyAs(self, propertyName, overrideClass = None, preserveNone : bool = True): - """ - Returns the property, setting the class if specified. - - :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: - value = self.props[propertyName].value - except (KeyError, AttributeError): - value = None - # Check if we should be overriding the data type for this instance. - if overrideClass is not None: - if (value is not None or not preserveNone): - value = overrideClass(value) - - return value - def _getStream(self, filename) -> Optional[bytes]: """ Gets a binary representation of the requested filename. @@ -91,33 +70,6 @@ def _getStream(self, filename) -> Optional[bytes]: warnings.warn(':method _getStream: has been deprecated and moved to the public api. Use :method getStream: instead (remove the underscore).', DeprecationWarning) return self.getStream(filename) - def _getStreamAs(self, streamID, stringStream : bool = True, overrideClass = None, preserveNone : bool = True): - """ - Returns the specified stream, modifying it to the class if specified. - - 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. - """ - if stringStream: - value = self.getStringStream(streamID) - else: - value = self.getStream(streamID) - - # Check if we should be overriding the data type for this instance. - if overrideClass is not None: - if value is not None or not preserveNone: - value = overrideClass(value) - - return value - def _getStringStream(self, filename) -> Optional[str]: """ Gets a string representation of the requested filename. @@ -294,6 +246,23 @@ def getMultipleString(self, filename) -> Optional[List[str]]: raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') return msg.getMultipleString([self.__dir, msgPathToString(filename)]) + def getPropertyAs(self, propertyName, overrideClass : Callable[..., _T]) -> Optional[_T]: + """ + Returns the property, setting the class if found. + + :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. If + the value is None, this function is not called. If you want it to + be called regardless, you should handle the data directly. + """ + value = self.getPropertyVal(propertyName) + + if value is not None: + value = overrideClass(value) + + return value + def getPropertyVal(self, name, default : _T = None) -> Union[Any, _T]: """ instance.props.getValue(name, default) @@ -354,6 +323,24 @@ def getStream(self, filename) -> Optional[bytes]: raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') return msg.getStream([self.__dir, msgPathToString(filename)]) + def getStreamAs(self, streamID, overrideClass : Callable[..., _T]) -> Optional[_T]: + """ + Returns the specified stream, modifying it to the specified class if it + is found. + + :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. If + the value is None, this function is not called. If you want it to + be called regardless, you should handle the data directly. + """ + value = self.getStream(streamID) + + if value is not None: + value = overrideClass(value) + + return value + def getStringStream(self, filename) -> Optional[str]: """ Gets a string representation of the requested filename. @@ -372,6 +359,24 @@ def getStringStream(self, filename) -> Optional[str]: raise ReferenceError('The msg file for this Recipient instance has been garbage collected.') return msg.getStringStream([self.__dir, msgPathToString(filename)]) + def getStringStreamAs(self, streamID, overrideClass : Callable[..., _T]) -> Optional[_T]: + """ + Returns the specified string stream, modifying it to the specified + class if it is found. + + :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. If + the value is None, this function is not called. If you want it to + be called regardless, you should handle the data directly. + """ + value = self.getStream(streamID) + + if value is not None: + value = overrideClass(value) + + return value + @functools.cached_property def account(self) -> Optional[str]: """ @@ -391,7 +396,7 @@ def entryID(self) -> Optional[PermanentEntryID]: """ Returns the recipient's Entry ID. """ - return self._getStreamAs('__substg1.0_0FFF0102', False, PermanentEntryID) + return self.getStreamAs('__substg1.0_0FFF0102', PermanentEntryID) @property def formatted(self) -> str: From 448513c7abbacd85d6597fe5ae978b5b3b1703ca Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sat, 12 Aug 2023 16:25:42 -0700 Subject: [PATCH 19/20] Replace mixed quotes in documentation file --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 0c112369..29af9ff5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ import os import sys -sys.path.insert(0, os.path.abspath("..")) +sys.path.insert(0, os.path.abspath('..')) __author__ = 'Destiny Peterson & Matthew Walker' __version__ = '0.45.0' @@ -24,7 +24,7 @@ # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = ["sphinx.ext.todo", "sphinx.ext.viewcode", "sphinx.ext.autodoc"] +extensions = ['sphinx.ext.todo', 'sphinx.ext.viewcode', 'sphinx.ext.autodoc'] templates_path = ['_templates'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '_autogen.txt'] From 442f2cd549cd8cc9076bc42dba27162a0483c164 Mon Sep 17 00:00:00 2001 From: TheElementalOfDestruction Date: Sat, 12 Aug 2023 16:39:49 -0700 Subject: [PATCH 20/20] Fix bug in PropertiesStore.getValue --- extract_msg/properties/properties_store.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extract_msg/properties/properties_store.py b/extract_msg/properties/properties_store.py index 24b4b6db..5884c897 100644 --- a/extract_msg/properties/properties_store.py +++ b/extract_msg/properties/properties_store.py @@ -189,11 +189,12 @@ def getValue(self, name : Union[str, int], default : _T = None) -> Union[Any, _T return prop.value return default elif len(name) == 8: - if (prop := self.get()): + if (prop := self.get(name)): if isinstance(prop, FixedLengthProp): return prop.value else: return default + return default else: raise ValueError('Property name must be an int less than 0x100000000, a 4 character hex string, or an 8 character hex string.')