Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]

steps:
- uses: actions/checkout@v4
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
**v0.58.0**
* Changed module to primarily use `MSGPath` for pathing. All functions still except string paths or list paths, they now just also accept `MSGPath` and use it under the hood for path conversions. With this, some functions have been changed to specifically return `MSGPath` rather than list and string versions of paths. Removed: `MSGBase.prefixList`, `utils.inputToMsgPath()`, and `utils.msgPathToString()`. Modified: `*.fixPath()`, `*.listDir()`, and `MSGBase.prefix`.
* Changed setup to use `find_packages()` to try and get ahead of deprecation and ensure all files are distributed correctly.
* Fixed `MSGFile.existsTypedProperty()` to actually do what it says it will do. Previous code had it not checking if the right directory was being searched when `location` was specified.
* Changed `MSGFile.sExists()` to correctly detect *only* the valid string stream type for the MSG file instead of unconditionally checking both types.
* Re-added `extract_msg.constants.PROPERTIES` which was removed for unknown reasons in an earlier version (0.42.0). `MSGBase.saveRaw()` appears to have been non-functional since then.

**v0.57.1**
* [[TeamMsgExtractor #489](https://github.com/TeamMsgExtractor/msg-extractor/issues/489)] Fix an issue where `MessageSignedBase.body` would raise an error when the plain text body was not utf-8 encoded bytes. This error was due to `extract_msg.utils.unwrapMultipart()` accidentally returning the plain text body as bytes instead of string.
* Change some of the command line documentation.
Expand Down
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,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.57.1-blue.svg
:target: https://pypi.org/project/extract-msg/0.57.1/
.. |PyPI3| image:: https://img.shields.io/badge/pypi-0.58.0-blue.svg
:target: https://pypi.org/project/extract-msg/0.58.0/

.. |PyPI2| image:: https://img.shields.io/badge/python-3.8+-brightgreen.svg
:target: https://www.python.org/downloads/release/python-3810/
Expand Down
5 changes: 4 additions & 1 deletion extract_msg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

__author__ = 'Destiny Peterson & Matthew Walker'
__date__ = '2026-10-28'
__version__ = '0.57.1'
__version__ = '0.58.0'

__all__ = [
# Modules:
Expand All @@ -37,6 +37,7 @@
'enums',
'exceptions',
'msg_classes',
'msg_path',
'open_msg',
'null_date',
'properties',
Expand All @@ -48,6 +49,7 @@
'AttachmentBase',
'Message',
'MSGFile',
'MSGPath',
'Named',
'NamedProperties',
'OleWriter',
Expand All @@ -68,6 +70,7 @@
from . import attachments, msg_classes, null_date, properties, structures
from .attachments import Attachment, AttachmentBase, SignedAttachment
from .msg_classes import Message, MSGFile
from .msg_path import MSGPath
from .ole_writer import OleWriter
from .open_msg import openMsg, openMsgBulk
from .properties import Named, NamedProperties, PropertiesStore
Expand Down
40 changes: 20 additions & 20 deletions extract_msg/attachments/attachment_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@

from ..constants import MSG_PATH, OVERRIDE_CLASS, SAVE_TYPE
from ..enums import AttachmentType
from ..msg_path import MSGPath
from ..properties.named import NamedProperties
from ..properties.properties_store import PropertiesStore
from ..utils import (
msgPathToString, tryGetMimetype, verifyPropertyId, verifyType
)
from ..utils import tryGetMimetype, verifyPropertyId, verifyType


# Allow for nice type checking.
Expand All @@ -51,7 +50,7 @@ def __init__(self, msg: MSGFile, dir_: str, propStore: PropertiesStore):
:param propStore: The PropertiesStore instance for the attachment.
"""
self.__msg = weakref.ref(msg)
self.__dir = dir_
self.__dir = MSGPath(dir_)
self.__props = propStore
self.__namedProperties = NamedProperties(msg.named, self)
self.__treePath = msg.treePath + [weakref.ref(self)]
Expand Down Expand Up @@ -150,7 +149,7 @@ def _getTypedStream(self, filename: MSG_PATH, _type = None):
"""
if (msg := self.__msg()) is None:
raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
return msg._getTypedStream([self.__dir, msgPathToString(filename)], True, _type)
return msg._getTypedStream(MSGPath(self.__dir, filename), True, _type)

def _handleFnc(self, _zip, filename, customPath: pathlib.Path, kwargs) -> pathlib.Path:
"""
Expand Down Expand Up @@ -203,7 +202,7 @@ def exists(self, filename: MSG_PATH) -> bool:
"""
if (msg := self.__msg()) is None:
raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
return msg.exists([self.__dir, msgPathToString(filename)])
return msg.exists(MSGPath(self.__dir, filename))

def sExists(self, filename: MSG_PATH) -> bool:
"""
Expand All @@ -214,7 +213,7 @@ def sExists(self, filename: MSG_PATH) -> bool:
"""
if (msg := self.__msg()) is None:
raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
return msg.sExists([self.__dir, msgPathToString(filename)])
return msg.sExists(MSGPath(self.__dir, filename))

def existsTypedProperty(self, id, _type = None) -> Tuple[bool, int]:
"""
Expand Down Expand Up @@ -255,7 +254,7 @@ def getMultipleBinary(self, filename: MSG_PATH) -> Optional[List[bytes]]:
"""
if (msg := self.__msg()) is None:
raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
return msg.getMultipleBinary([self.__dir, msgPathToString(filename)])
return msg.getMultipleBinary(MSGPath(self.__dir, filename))

def getMultipleString(self, filename: MSG_PATH) -> Optional[List[str]]:
"""
Expand All @@ -270,7 +269,7 @@ def getMultipleString(self, filename: MSG_PATH) -> Optional[List[str]]:
"""
if (msg := self.__msg()) is None:
raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
return msg.getMultipleString([self.__dir, msgPathToString(filename)])
return msg.getMultipleString(MSGPath(self.__dir, filename))

def getNamedAs(self, propertyName: str, guid: str, overrideClass: OVERRIDE_CLASS[_T]) -> Optional[_T]:
"""
Expand Down Expand Up @@ -338,7 +337,7 @@ def getSingleOrMultipleBinary(self, filename: MSG_PATH) -> Optional[Union[List[b
"""
if (msg := self.__msg()) is None:
raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
return msg.getSingleOrMultipleBinary([self.__dir, msgPathToString(filename)])
return msg.getSingleOrMultipleBinary(MSGPath(self.__dir, filename))

def getSingleOrMultipleString(self, filename: MSG_PATH) -> Optional[Union[List[str], str]]:
"""
Expand All @@ -356,7 +355,7 @@ def getSingleOrMultipleString(self, filename: MSG_PATH) -> Optional[Union[List[s
"""
if (msg := self.__msg()) is None:
raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
return msg.getSingleOrMultipleString([self.__dir, msgPathToString(filename)])
return msg.getSingleOrMultipleString(MSGPath(self.__dir, filename))

def getStream(self, filename: MSG_PATH) -> Optional[bytes]:
"""
Expand All @@ -370,7 +369,7 @@ def getStream(self, filename: MSG_PATH) -> Optional[bytes]:
"""
if (msg := self.__msg()) is None:
raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
return msg.getStream([self.__dir, msgPathToString(filename)])
return msg.getStream(MSGPath(self.__dir, filename))

def getStreamAs(self, streamID: MSG_PATH, overrideClass: OVERRIDE_CLASS[_T]) -> Optional[_T]:
"""
Expand Down Expand Up @@ -407,7 +406,7 @@ def getStringStream(self, filename: MSG_PATH) -> Optional[str]:
"""
if (msg := self.__msg()) is None:
raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
return msg.getStringStream([self.__dir, msgPathToString(filename)])
return msg.getStringStream(MSGPath(self.__dir, filename))

def getStringStreamAs(self, streamID: MSG_PATH, overrideClass: OVERRIDE_CLASS[_T]) -> Optional[_T]:
"""
Expand All @@ -428,7 +427,7 @@ class if it is found.

return value

def listDir(self, streams: bool = True, storages: bool = False) -> List[List[str]]:
def listDir(self, streams: bool = True, storages: bool = False) -> Tuple[MSGPath, ...]:
"""
Lists the streams and/or storages that exist in the attachment
directory.
Expand All @@ -438,14 +437,15 @@ def listDir(self, streams: bool = True, storages: bool = False) -> List[List[str
"""
if (msg := self.__msg()) is None:
raise ReferenceError('The MSGFile for this Attachment instance has been garbage collected.')
return [path[1:] for path in msg.listDir(streams, storages, False)
if len(path) > 1 and path[0] == self.__dir]
return tuple(path.relative_to(self.__dir)
for path in msg.listDir(streams, storages, False)
if path.is_relative_to(self.__dir))

def slistDir(self, streams: bool = True, storages: bool = False) -> List[str]:
def slistDir(self, streams: bool = True, storages: bool = False) -> Tuple[str, ...]:
"""
Like listDir, except it returns the paths as strings.
"""
return ['/'.join(path) for path in self.listDir(streams, storages)]
return Tuple(str(path) for path in self.listDir(streams, storages))

@abc.abstractmethod
def save(self, **kwargs) -> SAVE_TYPE:
Expand Down Expand Up @@ -512,9 +512,9 @@ def clsid(self) -> str:
dataStream = None

if self.exists('__substg1.0_3701000D'):
dataStream = [self.__dir, '__substg1.0_3701000D']
dataStream = self.__dir / '__substg1.0_3701000D'
elif self.exists('__substg1.0_37010102'):
dataStream = [self.__dir, '__substg1.0_37010102']
dataStream = self.__dir / '__substg1.0_37010102'

# If we found the right item, get the CLSID.
if dataStream:
Expand Down
5 changes: 2 additions & 3 deletions extract_msg/attachments/custom_att_handler/custom_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@
from typing import Dict, Optional, TYPE_CHECKING, TypeVar

from ...constants import MSG_PATH, OVERRIDE_CLASS
from ...msg_path import MSGPath
from ...structures.odt import ODTStruct
from ...structures.ole_pres import OLEPresentationStream
from ...structures.ole_stream_struct import OleStreamStruct
from ...utils import msgPathToString


if TYPE_CHECKING:
from ..attachment_base import AttachmentBase
Expand All @@ -38,7 +37,7 @@ def getStream(self, filename: MSG_PATH) -> Optional[bytes]:
"""
Gets a stream from the custom data directory.
"""
return self.attachment.getStream('__substg1.0_3701000D/' + msgPathToString(filename))
return self.attachment.getStream(MSGPath('__substg1.0_3701000D/', filename))

def getStreamAs(self, streamID: MSG_PATH, overrideClass: OVERRIDE_CLASS[_T]) -> Optional[_T]:
"""
Expand Down
Loading
Loading