168 lines
5.3 KiB
Python
168 lines
5.3 KiB
Python
"""Domain-model objects.
|
|
|
|
This module should have no dependencies outside the domain model. In particular, any domain model
|
|
object should be importable anywhere in the package without risk of producing an import cycle.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime as dt
|
|
import uuid
|
|
from typing import Iterator, Protocol
|
|
|
|
from oxmsg.util import lazyproperty
|
|
|
|
# ================================================================================================
|
|
# EXCEPTIONS
|
|
# ================================================================================================
|
|
|
|
|
|
class UnrecognizedCodePageError(Exception):
|
|
"""The specified code-page is not a known Microsoft encoding."""
|
|
|
|
|
|
class UnsupportedEncodingError(Exception):
|
|
"""The encoding used for this MSG object has no built-in Python codec."""
|
|
|
|
|
|
# ================================================================================================
|
|
# INTERFACES
|
|
# ================================================================================================
|
|
|
|
|
|
class PropStorageT(Protocol):
|
|
"""The interface required of a storage object to extract the properties of that storage."""
|
|
|
|
@lazyproperty
|
|
def properties_stream_bytes(self) -> bytes:
|
|
"""Bytes of stream containing properties for the top-level object this storage represents.
|
|
|
|
Every storage must have exactly one such stream.
|
|
"""
|
|
...
|
|
|
|
def property_stream_bytes(self, pid: int, ptyp: int) -> bytes:
|
|
"""The bytes of the stream for variable-length property identified by `pid`."""
|
|
...
|
|
|
|
|
|
class PropertyT(Protocol):
|
|
"""The interface required of a property object, regardless of its type."""
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""The Microsft name for this property, like "PidTagMessageClass"."""
|
|
...
|
|
|
|
@property
|
|
def pid(self) -> int:
|
|
"""The property-id (PID) for this property, like 0x3701 for attachment bytes."""
|
|
...
|
|
|
|
@property
|
|
def ptyp(self) -> int:
|
|
"""The property-type (PTYP) for this property, like 0x0102 for PtypBinary."""
|
|
...
|
|
|
|
@property
|
|
def ptyp_name(self) -> str:
|
|
"""The Microsft name for the type of this property, like "PtypString"."""
|
|
...
|
|
|
|
@lazyproperty
|
|
def value(self) -> bool | bytes | dt.datetime | float | int | str | uuid.UUID:
|
|
"""The value of this property, its type depending on the property."""
|
|
...
|
|
|
|
|
|
class StorageT(Protocol):
|
|
"""Interface for a storage object."""
|
|
|
|
def iter_attachment_storages(self) -> Iterator[StorageT]:
|
|
"""Generate each storage object specific to an attachment to this message."""
|
|
...
|
|
|
|
def iter_recipient_storages(self) -> Iterator[StorageT]:
|
|
"""Generate each storage object specific to a recipient of this message."""
|
|
...
|
|
|
|
@lazyproperty
|
|
def name(self) -> str:
|
|
"""The last segment of the storage path.
|
|
|
|
This is the empty string for the root storage. Other storages are named is clearly specified
|
|
ways depending on the type of top-level object they contain, e.g. attachment, recipient,
|
|
etc.
|
|
"""
|
|
...
|
|
|
|
@property
|
|
def path(self) -> str:
|
|
"""String identifier for this storage; its location within the OXMSG CFB.
|
|
|
|
- Suitable as a unique storage identifier within the MSG file.
|
|
- The path of the root storage is the empty string ("").
|
|
"""
|
|
...
|
|
|
|
@lazyproperty
|
|
def properties_stream_bytes(self) -> bytes:
|
|
"""Bytes of stream containing properties for the top-level object this storage represents.
|
|
|
|
Every storage must have exactly one such stream.
|
|
"""
|
|
...
|
|
|
|
def property_stream_bytes(self, pid: int, ptyp: int) -> bytes:
|
|
"""The bytes of the stream for variable-length property identified by `pid`."""
|
|
...
|
|
|
|
|
|
class StreamT(Protocol):
|
|
"""Interface for a stream object."""
|
|
|
|
@lazyproperty
|
|
def name(self) -> str:
|
|
"""The last segment of the stream path.
|
|
|
|
Each stream contains the bytes for a property. The form of a stream name is clearly
|
|
specified in the OXMSG standard and includes both the property-id (PID) and property
|
|
data-type (PTYP) for the property data it contains.
|
|
"""
|
|
...
|
|
|
|
@property
|
|
def path(self) -> str:
|
|
"""String identifier for this storage; its location within the OXMSG CFB.
|
|
|
|
Suitable as a unique storage identifier within the MSG file.
|
|
"""
|
|
...
|
|
|
|
@property
|
|
def bytes_(self) -> bytes:
|
|
"""The bytes contained in this stream."""
|
|
...
|
|
|
|
|
|
# ================================================================================================
|
|
# MASKS
|
|
# ================================================================================================
|
|
|
|
# -- AF = attachment flag maybe? Means attachment is embedding, i.e. referenced "by value" --
|
|
AF_BY_VALUE = 0x00000001
|
|
|
|
STORE_UNICODE_OK = 0x00040000
|
|
|
|
|
|
# ================================================================================================
|
|
# PROPERTY STREAM HEADER OFFSETS
|
|
# ================================================================================================
|
|
|
|
# -- The starting location for the 16-byte property segments within the property stream varies
|
|
# -- depending on the storage type.
|
|
|
|
ATTACH_HDR_OFFSET = 8
|
|
MSG_HDR_OFFSET = 32
|
|
RECIP_HDR_OFFSET = 8
|