修改为东南天坐标系
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
__version__ = "0.0.2"
|
||||
|
||||
from oxmsg.message import Message
|
||||
|
||||
__all__ = [
|
||||
"Message",
|
||||
"__version__",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,80 @@
|
||||
"""Provides access to the properties of an attachment in an Outlook MSG file.
|
||||
|
||||
These properies include the attached file's original file-name, its last-modified date, size, and
|
||||
content-type (MIME-type).
|
||||
|
||||
```python
|
||||
>>> from oxmsg import Message
|
||||
|
||||
>>> msg = Message.load("message.msg")
|
||||
>>> msg.attachment_count
|
||||
1
|
||||
>>> attachment = msg.attachments[0]
|
||||
>>> attachment.file_name
|
||||
'financial-forecast.xlsx'
|
||||
>>> with open(attachment.file_name, "wb") as f:
|
||||
... f.write()
|
||||
```
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
|
||||
from oxmsg.domain import constants as c
|
||||
from oxmsg.domain import model as m
|
||||
from oxmsg.properties import Properties
|
||||
from oxmsg.util import lazyproperty
|
||||
|
||||
|
||||
class Attachment:
|
||||
"""A file attached to an Outlook email message."""
|
||||
|
||||
def __init__(self, storage: m.StorageT):
|
||||
self._storage = storage
|
||||
|
||||
@lazyproperty
|
||||
def attached_by_value(self) -> bool:
|
||||
"""True when the `PidTagAttachDataBinary` property contains the attachment data.
|
||||
|
||||
This is as opposed to "by-reference" where only a path or URL is stored.
|
||||
"""
|
||||
attach_method = self.properties.int_prop_value(c.PID_ATTACH_METHOD)
|
||||
assert attach_method is not None
|
||||
return bool(attach_method & m.AF_BY_VALUE)
|
||||
|
||||
@lazyproperty
|
||||
def file_bytes(self) -> bytes | None:
|
||||
"""The attachment binary, suitable for saving to a file when detaching."""
|
||||
return self.properties.binary_prop_value(c.PID_ATTACH_DATA_BINARY)
|
||||
|
||||
@lazyproperty
|
||||
def file_name(self) -> str | None:
|
||||
"""The full name of this file as it was originally attached.
|
||||
|
||||
Like "FY24-quarterly-projections.xlsx". Does not include a path.
|
||||
"""
|
||||
return self.properties.str_prop_value(c.PID_ATTACH_LONG_FILENAME)
|
||||
|
||||
@lazyproperty
|
||||
def last_modified(self) -> dt.datetime | None:
|
||||
"""Timezone-aware UTC datetime when this attachment was last modified.
|
||||
|
||||
`None` if this property is not present on the attachment.
|
||||
"""
|
||||
return self.properties.date_prop_value(c.PID_LAST_MODIFICATION_TIME)
|
||||
|
||||
@lazyproperty
|
||||
def mime_type(self) -> str | None:
|
||||
"""ISO 8601 str representation of time this attachment was last modified."""
|
||||
return self.properties.str_prop_value(c.PID_ATTACH_MIME_TAG) or "application/octet-stream"
|
||||
|
||||
@lazyproperty
|
||||
def properties(self) -> Properties:
|
||||
"""Provides access to the properties of this OXMSG object."""
|
||||
return Properties(self._storage, properties_header_offset=m.ATTACH_HDR_OFFSET)
|
||||
|
||||
@lazyproperty
|
||||
def size(self) -> int:
|
||||
"""Length in bytes of this attachment."""
|
||||
return len(self.file_bytes) if self.file_bytes else 0
|
||||
181
backend_service/venv/lib/python3.13/site-packages/oxmsg/cli.py
Normal file
181
backend_service/venv/lib/python3.13/site-packages/oxmsg/cli.py
Normal file
@@ -0,0 +1,181 @@
|
||||
# pyright: reportPrivateUsage=false
|
||||
|
||||
"""The command-line interface (CLI) for `python-oxmsg`.
|
||||
|
||||
The CLI provides the command `oxmsg`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Iterator, cast
|
||||
|
||||
import click
|
||||
from olefile import OleFileIO
|
||||
|
||||
from oxmsg.attachment import Attachment
|
||||
from oxmsg.domain import constants as c
|
||||
from oxmsg.domain.encodings import encoding_from_codepage
|
||||
from oxmsg.message import Message
|
||||
from oxmsg.properties import Properties
|
||||
from oxmsg.recipient import Recipient
|
||||
from oxmsg.storage import Storage
|
||||
|
||||
# TODO: add `body` sub-command
|
||||
# TODO: add `detach` sub-command
|
||||
|
||||
|
||||
@click.group()
|
||||
def oxmsg():
|
||||
"""Utility CLI for `python-oxmsg`.
|
||||
|
||||
Provides the subcommands listed below, useful for exploratory or diagnostic purposes.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@oxmsg.command()
|
||||
@click.argument("msg_file_path", type=str)
|
||||
def dump(msg_file_path: str):
|
||||
"""Write a summary of the MSG file's properties to stdout."""
|
||||
msg = Message.load(msg_file_path)
|
||||
print(f"{dump_message_properties(msg)}")
|
||||
|
||||
for r in msg.recipients:
|
||||
print(f"{dump_recipient_properties(r)}")
|
||||
|
||||
for a in msg.attachments:
|
||||
if not a.attached_by_value:
|
||||
print(f"attachment {a.file_name} is not embedded in message, file unavailable")
|
||||
print(f"{dump_attachment_properties(a)}")
|
||||
|
||||
|
||||
@oxmsg.command()
|
||||
@click.argument("msg_file_path", type=str)
|
||||
def storage(msg_file_path: str):
|
||||
"""Summarize low-level "directories and files" structure of MSG."""
|
||||
|
||||
def iter_storage_dump_lines(storage: Storage, prefix: str = "") -> Iterator[str]:
|
||||
yield f"{prefix}{storage.name or 'root'}"
|
||||
for stream in storage.streams:
|
||||
yield f"{prefix} {stream.name}"
|
||||
for s in storage.storages:
|
||||
yield from iter_storage_dump_lines(s, prefix + " ")
|
||||
|
||||
with OleFileIO(msg_file_path) as ole:
|
||||
root_storage = Storage.from_ole(ole)
|
||||
|
||||
print("\n".join(iter_storage_dump_lines(root_storage)))
|
||||
|
||||
|
||||
def dump_message_properties(msg: Message) -> str:
|
||||
"""A summary of this MS-OXMSG object's top-level properties."""
|
||||
string_props_are_unicode = msg.properties.string_props_are_unicode
|
||||
str_prop_encoding = msg.properties._str_prop_encoding
|
||||
internet_code_page = msg.properties.int_prop_value(0x3FDE)
|
||||
internet_encoding = (
|
||||
None if internet_code_page is None else encoding_from_codepage(internet_code_page)
|
||||
)
|
||||
|
||||
def iter_lines() -> Iterator[str]:
|
||||
yield ""
|
||||
yield "------------------"
|
||||
yield "Message Properties"
|
||||
yield "------------------"
|
||||
yield ""
|
||||
yield "header-properties"
|
||||
yield "-----------------"
|
||||
yield f"recipient_count: {msg._header_prop_values[2]}"
|
||||
yield ""
|
||||
yield "distinguished-properties"
|
||||
yield "------------------------"
|
||||
yield f"attachment_count: {msg.attachment_count}"
|
||||
yield f"internet_code_page: {internet_encoding}"
|
||||
yield f"message_class: {msg.message_class}"
|
||||
yield f"sender: {msg.sender}"
|
||||
yield f"sent_date: {msg.sent_date}"
|
||||
yield f"string_props_are_unicode: {string_props_are_unicode}"
|
||||
if not string_props_are_unicode:
|
||||
yield f"string_props_encoding: {str_prop_encoding}"
|
||||
yield f"subject: {repr(msg.subject)}"
|
||||
yield f"message_headers:\n{json.dumps(msg.message_headers, indent=4, sort_keys=True)}"
|
||||
yield ""
|
||||
yield "other properties"
|
||||
yield dump_properties(msg.properties)
|
||||
|
||||
return "\n".join(iter_lines())
|
||||
|
||||
|
||||
def dump_attachment_properties(attachment: Attachment) -> str:
|
||||
"""Report of message properies suitable for writing to the console."""
|
||||
|
||||
def iter_lines() -> Iterator[str]:
|
||||
yield ""
|
||||
yield "---------------------"
|
||||
yield "Attachment Properties"
|
||||
yield "---------------------"
|
||||
yield ""
|
||||
yield "distinguished-properties"
|
||||
yield "------------------------"
|
||||
yield f"attached_by_value: {attachment.attached_by_value}"
|
||||
yield f"file_name: {attachment.file_name}"
|
||||
yield f"last_modified: {attachment.last_modified}"
|
||||
yield f"mime_type: {attachment.mime_type}"
|
||||
yield f"size: {attachment.size:,}"
|
||||
yield ""
|
||||
yield "other properties"
|
||||
yield dump_properties(attachment.properties)
|
||||
|
||||
return "\n".join(iter_lines())
|
||||
|
||||
|
||||
def dump_recipient_properties(recipient: Recipient) -> str:
|
||||
"""Report of message properies suitable for writing to the console."""
|
||||
|
||||
def iter_lines() -> Iterator[str]:
|
||||
yield ""
|
||||
yield "---------------------"
|
||||
yield "Recipient Properties"
|
||||
yield "---------------------"
|
||||
yield ""
|
||||
yield "distinguished-properties"
|
||||
yield "------------------------"
|
||||
yield f"name: {repr(recipient.name)}"
|
||||
yield f"email_address: {recipient.email_address}"
|
||||
yield ""
|
||||
yield "other properties"
|
||||
yield dump_properties(recipient.properties)
|
||||
|
||||
return "\n".join(iter_lines())
|
||||
|
||||
|
||||
def dump_properties(self: Properties) -> str:
|
||||
"""A summary of these properties suitable for printing to the console."""
|
||||
|
||||
def iter_lines() -> Iterator[str]:
|
||||
head_rule = f"{'-'*53}+{'-'*23}+{'-'*70}"
|
||||
yield head_rule
|
||||
yield "property-id" + " " * 42 + "| type" + " " * 18 + "| value"
|
||||
yield head_rule
|
||||
|
||||
for p in self:
|
||||
value = p.value
|
||||
if p.ptyp in (c.PTYP_STRING, c.PTYP_STRING8):
|
||||
value = cast(str, self.str_prop_value(p.pid))
|
||||
value = repr(value)[:64] + "..." if len(value) > 64 else repr(value)
|
||||
elif p.ptyp == c.PTYP_BINARY and p.pid == c.PID_HTML:
|
||||
assert isinstance(value, bytes)
|
||||
value = value[:64]
|
||||
elif isinstance(value, bytes):
|
||||
value = f"{len(value):,} bytes"
|
||||
elif p.ptyp == c.PTYP_INTEGER_32:
|
||||
assert isinstance(value, int)
|
||||
b0 = value & 0xFF
|
||||
b1 = (value & 0xFF00) >> 8
|
||||
b2 = (value & 0xFF0000) >> 16
|
||||
b3 = (value & 0xFF000000) >> 24
|
||||
value = f"{b3:02X} {b2:02X} {b1:02X} {b0:02X}"
|
||||
|
||||
yield f"0x{p.pid:04X} - {p.name:<43} | {p.ptyp_name:<21} | {value}"
|
||||
|
||||
return "\n".join(iter_lines())
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,216 @@
|
||||
"""Constant definitions, mostly property ids (PIDs) and property types (PTYPs)."""
|
||||
|
||||
# -- Property IDs - These names are formed by upper-snake-casing the Microsoft name for each
|
||||
# -- property-id, dropping the "Tag" segment. E.g. "PidTagAttachSize" -> "PID_ATTACH_SIZE"
|
||||
|
||||
PID_ACCESS = 0x0FF4
|
||||
PID_ACCESS_LEVEL = 0x0FF7
|
||||
PID_ACKNOWLEDGEMENT_MODE = 0x0001
|
||||
PID_ADDRESS_BOOK_FOLDER_PATHNAME = 0x8004
|
||||
PID_ADDRESS_BOOK_HOME_MESSAGE_DATABASE = 0x8006
|
||||
PID_ADDRESS_BOOK_IS_MEMBER_OF_DISTRIBUTION_LIST = 0x8008
|
||||
PID_ADDRESS_BOOK_MANAGER_DISTINGUISHED_NAME = 0x8005
|
||||
PID_ADDRESS_BOOK_MEMBER = 0x8009
|
||||
PID_ADDRESS_TYPE = 0x3002
|
||||
PID_ALTERNATE_RECIPIENT_ALLOWED = 0x0002
|
||||
PID_ATTACHMENT_FLAGS = 0x7FFD
|
||||
PID_ATTACHMENT_HIDDEN = 0x7FFE
|
||||
PID_ATTACHMENT_LINK_ID = 0x7FFA
|
||||
PID_ATTACH_CONTENT_ID = 0x3712
|
||||
PID_ATTACH_DATA_BINARY = 0x3701
|
||||
PID_ATTACH_ENCODING = 0x3702
|
||||
PID_ATTACH_EXTENSION = 0x3703
|
||||
PID_ATTACH_FILENAME = 0x3704
|
||||
PID_ATTACH_FLAGS = 0x3714
|
||||
PID_ATTACH_LONG_FILENAME = 0x3707
|
||||
PID_ATTACH_METHOD = 0x3705
|
||||
PID_ATTACH_MIME_SEQUENCE = 0x3710
|
||||
PID_ATTACH_MIME_TAG = 0x370E
|
||||
PID_ATTACH_NUMBER = 0x0E21
|
||||
PID_ATTACH_RENDERING = 0x3709
|
||||
PID_ATTACH_RENDERING_POSITION = 0x370B
|
||||
PID_ATTACH_SIZE = 0x0E20
|
||||
PID_ATTACH_TAG = 0x370A
|
||||
PID_AUTHORIZING_USERS = 0x0003
|
||||
PID_AUTO_FORWARDED = 0x0005
|
||||
PID_AUTO_FORWARD_COMMENT = 0x0004
|
||||
PID_BODY = 0x1000
|
||||
PID_BODY_HTML = 0x1013
|
||||
PID_CHANGE_KEY = 0x65E2
|
||||
PID_CLIENT_SUBMIT_TIME = 0x0039
|
||||
PID_CONTENT_CONFIDENTIALITY_ALGORITHM_ID = 0x0006
|
||||
PID_CONTENT_CORRELATOR = 0x0007
|
||||
PID_CONTENT_IDENTIFIER = 0x0008
|
||||
PID_CONTENT_LENGTH = 0x0009
|
||||
PID_CONTENT_RETURN_REQUESTED = 0x000A
|
||||
PID_CONVERSATION_INDEX = 0x0071
|
||||
PID_CONVERSATION_INDEX_TRACKING = 0x3016
|
||||
PID_CONVERSATION_KEY = 0x000B
|
||||
PID_CONVERSATION_TOPIC = 0x0070
|
||||
PID_CONVERSION_EITS = 0x000C
|
||||
PID_CONVERSION_WITH_LOSS_PROHIBITED = 0x000D
|
||||
PID_CONVERTED_EITS = 0x000E
|
||||
PID_CREATION_TIME = 0x3007
|
||||
PID_CREATOR_ADDRESS_TYPE = 0x4022
|
||||
PID_CREATOR_EMAIL_ADDRESS = 0x4023
|
||||
PID_CREATOR_SIMPLE_DISPLAY_NAME = 0x4038
|
||||
PID_DEFERRED_DELIVERY_TIME = 0x000F
|
||||
PID_DELEGATION = 0x007E
|
||||
PID_DELETE_AFTER_SUBMIT = 0x0E01
|
||||
PID_DELIVER_TIME = 0x0010
|
||||
PID_DISCARD_REASON = 0x0011
|
||||
PID_DISCLOSURE_OF_RECIPIENTS = 0x0012
|
||||
PID_DISPLAY_BCC = 0x0E02
|
||||
PID_DISPLAY_CC = 0x0E03
|
||||
PID_DISPLAY_NAME = 0x3001
|
||||
PID_DISPLAY_TO = 0x0E04
|
||||
PID_DISTRIBUTION_LIST_EXPANSION_HISTORY = 0x0013
|
||||
PID_DISTRIBUTION_LIST_EXPANSION_PROHIBITED = 0x0014
|
||||
PID_EMAIL_ADDRESS = 0x3003
|
||||
PID_END_DATE = 0x0061
|
||||
PID_ENTRY_ID = 0x0FFF
|
||||
PID_EXCEPTION_END_TIME = 0x7FFC
|
||||
PID_EXCEPTION_START_TIME = 0x7FFB
|
||||
PID_EXPIRY_TIME = 0x0015
|
||||
PID_FLAG_STATUS = 0x1090
|
||||
PID_HAS_ATTACHMENTS = 0x0E1B
|
||||
PID_HTML = 0x1013
|
||||
PID_ICON_INDEX = 0x1080
|
||||
PID_IMPLICIT_CONVERSION_PROHIBITED = 0x0016
|
||||
PID_IMPORTANCE = 0x0017
|
||||
PID_INITIAL_DETAILS_PANE = 0x3F08
|
||||
PID_INSTANCE_KEY = 0x0FF6
|
||||
PID_INTERNET_CODEPAGE = 0x3FDE
|
||||
PID_INTERNET_MESSAGE_ID = 0x1035
|
||||
PID_INTERNET_REFERENCES = 0x1039
|
||||
PID_IN_REPLY_TO_ID = 0x1042
|
||||
PID_LAST_MODIFICATION_TIME = 0x3008
|
||||
PID_LAST_MODIFIER_NAME = 0x3FFA
|
||||
PID_LATEST_DELIVERY_TIME = 0x0019
|
||||
PID_LID_CONTACT_ITEM_DATA = 0x8007
|
||||
PID_MESSAGE_CC_ME = 0x0058
|
||||
PID_MESSAGE_CLASS = 0x001A
|
||||
PID_MESSAGE_CODEPAGE = 0x3FFD
|
||||
PID_MESSAGE_DELIVERY_ID = 0x001B
|
||||
PID_MESSAGE_DELIVERY_TIME = 0x0E06
|
||||
PID_MESSAGE_FLAGS = 0x0E07
|
||||
PID_MESSAGE_LOCALE_ID = 0x3FF1
|
||||
PID_MESSAGE_RECIPIENT_ME = 0x0059
|
||||
PID_MESSAGE_SECURITY_LABEL = 0x001E
|
||||
PID_MESSAGE_SIZE_EXTENDED = 0x0E08
|
||||
PID_MESSAGE_SUBMISSION_ID = 0x0047
|
||||
PID_MESSAGE_TO_ME = 0x0057
|
||||
PID_NATIVE_BODY = 0x1016
|
||||
PID_NON_RECEIPT_NOTIFICATION_REQUESTED = 0x0C06
|
||||
PID_NORMALIZED_SUBJECT = 0x0E1D
|
||||
PID_OBJECT_TYPE = 0x0FFE
|
||||
PID_OBSOLETED_MESSAGE_IDS = 0x001F
|
||||
PID_ORIGINALLY_INTENDED_RECIPIENT_NAME = 0x0020
|
||||
PID_ORIGINALLY_INTENDED_RECIP_ADDRTYPE = 0x007B
|
||||
PID_ORIGINALLY_INTENDED_RECIP_EMAIL_ADDRESS = 0x007C
|
||||
PID_ORIGINAL_AUTHOR_ADDRESS_TYPE = 0x0079
|
||||
PID_ORIGINAL_AUTHOR_EMAIL_ADDRESS = 0x007A
|
||||
PID_ORIGINAL_AUTHOR_ENTRY_ID = 0x004C
|
||||
PID_ORIGINAL_AUTHOR_NAME = 0x004D
|
||||
PID_ORIGINAL_DELIVERY_TIME = 0x0055
|
||||
PID_ORIGINAL_DISPLAY_BCC = 0x0072
|
||||
PID_ORIGINAL_DISPLAY_CC = 0x0073
|
||||
PID_ORIGINAL_DISPLAY_TO = 0x0074
|
||||
PID_ORIGINAL_EITS = 0x0021
|
||||
PID_ORIGINAL_MESSAGE_CLASS = 0x004B
|
||||
PID_ORIGINAL_MESSAGE_ID = 0x1046
|
||||
PID_ORIGINAL_SENDER_ADDRESS_TYPE = 0x0066
|
||||
PID_ORIGINAL_SENDER_EMAIL_ADDRESS = 0x0067
|
||||
PID_ORIGINAL_SENDER_ENTRY_ID = 0x005B
|
||||
PID_ORIGINAL_SENDER_NAME = 0x005A
|
||||
PID_ORIGINAL_SENDER_SEARCH_KEY = 0x005C
|
||||
PID_ORIGINAL_SENSITIVITY = 0x002E
|
||||
PID_ORIGINAL_SENT_REPRESENTING_ADDRESS_TYPE = 0x0068
|
||||
PID_ORIGINAL_SENT_REPRESENTING_EMAIL_ADDRESS = 0x0069
|
||||
PID_ORIGINAL_SENT_REPRESENTING_ENTRY_ID = 0x005E
|
||||
PID_ORIGINAL_SENT_REPRESENTING_NAME = 0x005D
|
||||
PID_ORIGINAL_SENT_REPRESENTING_SEARCH_KEY = 0x005F
|
||||
PID_ORIGINAL_SUBJECT = 0x0049
|
||||
PID_ORIGINAL_SUBMIT_TIME = 0x004E
|
||||
PID_ORIGINATOR_CERTIFICATE = 0x0022
|
||||
PID_ORIGINATOR_DELIVERY_REPORT_REQUESTED = 0x0023
|
||||
PID_ORIGINATOR_RETURN_ADDRESS = 0x0024
|
||||
PID_ORIGIN_CHECK = 0x0027
|
||||
PID_OWNER_APPOINTMENT_ID = 0x0062
|
||||
PID_PARENT_DISPLAY = 0x0E05
|
||||
PID_PARENT_KEY = 0x0025
|
||||
PID_PREDECESSOR_CHANGE_LIST = 0x65E3
|
||||
PID_PRIORITY = 0x0026
|
||||
PID_PROOF_OF_SUBMISSION_REQUESTED = 0x0028
|
||||
PID_READ_RECEIPT_ENTRY_ID = 0x0046
|
||||
PID_READ_RECEIPT_REQUESTED = 0x0029
|
||||
PID_READ_RECEIPT_SEARCH_KEY = 0x0053
|
||||
PID_RECEIPT_TIME = 0x002A
|
||||
PID_RECEIVED_BY_ADDRESS_TYPE = 0x0075
|
||||
PID_RECEIVED_BY_EMAIL_ADDRESS = 0x0076
|
||||
PID_RECEIVED_BY_ENTRY_ID = 0x003F
|
||||
PID_RECEIVED_BY_NAME = 0x0040
|
||||
PID_RECEIVED_BY_SEARCH_KEY = 0x0051
|
||||
PID_RECEIVED_REPRESENTING_ADDRESS_TYPE = 0x0077
|
||||
PID_RECEIVED_REPRESENTING_EMAIL_ADDRESS = 0x0078
|
||||
PID_RECEIVED_REPRESENTING_ENTRY_ID = 0x0043
|
||||
PID_RECEIVED_REPRESENTING_NAME = 0x0044
|
||||
PID_RECEIVED_REPRESENTING_SEARCH_KEY = 0x0052
|
||||
PID_RECIPIENT_REASSIGNMENT_PROHIBITED = 0x002B
|
||||
PID_RECIPIENT_TYPE = 0x0C15
|
||||
PID_RECORD_KEY = 0x0FF9
|
||||
PID_REDIRECTION_HISTORY = 0x002C
|
||||
PID_REPLY_RECIPIENT_ENTRIES = 0x004F
|
||||
PID_REPLY_RECIPIENT_NAMES = 0x0050
|
||||
PID_REPLY_TIME = 0x0030
|
||||
PID_REPORT = 0x0031
|
||||
PID_REPORT_DISPOSITION = 0x0080
|
||||
PID_REPORT_DISPOSITION_MODE = 0x0081
|
||||
PID_REPORT_ENTRY_ID = 0x0045
|
||||
PID_REPORT_NAME = 0x003A
|
||||
PID_REPORT_SEARCH_KEY = 0x0054
|
||||
PID_REPORT_TIME = 0x0032
|
||||
PID_RESPONSE_REQUESTED = 0x0063
|
||||
PID_ROW_ID = 0x3000
|
||||
PID_RTF_COMPRESSED = 0x1009
|
||||
PID_RTF_IN_SYNC = 0x0E1F
|
||||
PID_SEARCH_KEY = 0x300B
|
||||
PID_SECURITY = 0x0034
|
||||
PID_SENDER_ADDRESS_TYPE = 0x0C1E
|
||||
PID_SENDER_EMAIL_ADDRESS = 0x0C1F
|
||||
PID_SENDER_ENTRY_ID = 0x0C19
|
||||
PID_SENDER_NAME = 0x0C1A
|
||||
PID_SENDER_SEARCH_KEY = 0x0C1D
|
||||
PID_SENDER_SMTP_ADDRESS = 0x5D01
|
||||
PID_SENSITIVITY = 0x0036
|
||||
PID_SENT_REPRESENTING_ADDRESS_TYPE = 0x0064
|
||||
PID_SENT_REPRESENTING_EMAIL_ADDRESS = 0x0065
|
||||
PID_SENT_REPRESENTING_ENTRY_ID = 0x0041
|
||||
PID_SENT_REPRESENTING_NAME = 0x0042
|
||||
PID_SENT_REPRESENTING_SEARCH_KEY = 0x003B
|
||||
PID_SMTP_ADDRESS = 0x39FE
|
||||
PID_START_DATE = 0x0060
|
||||
PID_STORE_SUPPORT_MASK = 0x340D
|
||||
PID_STORE_UNICODE_MASK = 0x340F
|
||||
PID_SUBJECT = 0x0037
|
||||
PID_SUBJECT_MESSAGE_ID = 0x0038
|
||||
PID_SUBJECT_PREFIX = 0x003D
|
||||
PID_TNEF_CORRELATION_KEY = 0x007F
|
||||
PID_TRANSPORT_MESSAGE_HEADERS = 0x007D
|
||||
|
||||
|
||||
# -- Property Data Types - These are detailed in the MS-OXCDATA spec --
|
||||
|
||||
PTYP_BINARY = 0x0102
|
||||
PTYP_BOOLEAN = 0x000B
|
||||
PTYP_FLOATING_32 = 0x0004
|
||||
PTYP_FLOATING_64 = 0x0005
|
||||
PTYP_GUID = 0x0048
|
||||
PTYP_INTEGER_16 = 0x0002
|
||||
PTYP_INTEGER_32 = 0x0003
|
||||
PTYP_MULTIPLE_INTEGER_32 = 0x1003
|
||||
PTYP_MULTIPLE_STRING = 0x101F
|
||||
PTYP_OBJECT = 0x000D
|
||||
PTYP_STRING = 0x001F
|
||||
PTYP_STRING8 = 0x001E
|
||||
PTYP_TIME = 0x0040
|
||||
@@ -0,0 +1,192 @@
|
||||
"""Mapping of Microsoft code-page-identifiers (ints) to Python character-encoding names.
|
||||
|
||||
Derived from the table at:
|
||||
https://learn.microsoft.com/en-us/windows/win32/intl/code-page-identifiers
|
||||
|
||||
Microsoft-supported code pages that do not have a built-in Python codec are indicated by the empty
|
||||
string. This allows a `.get()` on this mapping to distinguish between a codec that is unknown and
|
||||
one that is recognized but not supported.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from oxmsg.domain import model as m
|
||||
|
||||
|
||||
def encoding_from_codepage(codepage: int) -> str:
|
||||
"""Map `codepage` to Python character encoding like "iso-2022-jp".
|
||||
|
||||
- Raises `UnrecognizedCodePageError` when `codepage` is not a known Microsoft character
|
||||
codepage.
|
||||
- Raises `UnsupportedEncodingError` when `codepage` is recognized but Python has no builtin
|
||||
codec for that encoding.
|
||||
"""
|
||||
encoding = _CODE_PAGE_ENCODINGS.get(codepage)
|
||||
|
||||
if encoding is None:
|
||||
raise m.UnrecognizedCodePageError( # pragma: no cover
|
||||
"the code-page specified in this message is not a known Microsoft character encoding"
|
||||
)
|
||||
|
||||
if encoding == "":
|
||||
raise m.UnsupportedEncodingError( # pragma: no cover
|
||||
"the character-encoding used in this message is not supported by Python"
|
||||
)
|
||||
|
||||
return encoding
|
||||
|
||||
|
||||
_CODE_PAGE_ENCODINGS = {
|
||||
37: "IBM037", # -- IBM EBCDIC US-Canada --
|
||||
437: "IBM437", # -- OEM United States --
|
||||
500: "IBM500", # -- IBM EBCDIC International --
|
||||
708: "ASMO-708", # -- Arabic (ASMO 708) --
|
||||
709: "", # -- Arabic (ASMO-449+, BCON V4) --
|
||||
710: "", # -- Arabic - Transparent Arabic --
|
||||
720: "DOS-720", # -- Arabic (Transparent ASMO) --
|
||||
737: "cp737", # -- OEM Greek (formerly 437G) --
|
||||
775: "ibm775", # -- OEM Baltic --
|
||||
850: "ibm850", # -- OEM Multilingual Latin 1 --
|
||||
852: "ibm852", # -- OEM Latin 2 --
|
||||
855: "IBM855", # -- OEM Cyrillic (primarily Russian) --
|
||||
857: "ibm857", # -- OEM Turkish --
|
||||
858: "cp858", # -- OEM Multilingual Latin 1 + Euro symbol --
|
||||
860: "IBM860", # -- OEM Portuguese --
|
||||
861: "ibm861", # -- OEM Icelandic --
|
||||
862: "cp862", # -- OEM Hebrew --
|
||||
863: "IBM863", # -- OEM French Canadian --
|
||||
864: "IBM864", # -- OEM Arabic --
|
||||
865: "IBM865", # -- OEM Nordic --
|
||||
866: "cp866", # -- OEM Russian --
|
||||
869: "ibm869", # -- OEM Modern Greek --
|
||||
870: "cp870", # -- IBM EBCDIC Multilingual/ROECE (Latin 2) --
|
||||
874: "windows-874", # -- ANSI/OEM Thai (ISO 8859-11) --
|
||||
875: "cp875", # -- IBM EBCDIC Greek Modern --
|
||||
932: "shift_jis", # -- ANSI/OEM Japanese --
|
||||
936: "gb2312", # -- ANSI/OEM Simplified Chinese (PRC, Singapore) --
|
||||
949: "ks_c_5601-1987", # -- ANSI/OEM Korean (Unified Hangul Code) --
|
||||
950: "windows-950", # -- ANSI/OEM Traditional Chinese (Taiwan, Hong Kong SAR, PRC) --
|
||||
1026: "IBM1026", # -- IBM EBCDIC Turkish (Latin 5) --
|
||||
1047: "cp1047", # -- IBM EBCDIC Latin 1/Open System --
|
||||
1140: "cp1140", # -- IBM EBCDIC US-Canada (037 + Euro symbol) --
|
||||
1141: "cp1141", # -- IBM EBCDIC Germany (20273 + Euro symbol) --
|
||||
1142: "cp1142", # -- IBM EBCDIC Denmark-Norway (20277 + Euro symbol) --
|
||||
1143: "cp1143", # -- IBM EBCDIC Finland-Sweden (20278 + Euro symbol) --
|
||||
1144: "cp1144", # -- IBM EBCDIC Italy (20280 + Euro symbol) --
|
||||
1145: "cp1145", # -- IBM EBCDIC Latin America-Spain (20284 + Euro symbol) --
|
||||
1146: "cp1146", # -- IBM EBCDIC United Kingdom (20285 + Euro symbol) --
|
||||
1147: "cp1147", # -- IBM EBCDIC France (20297 + Euro symbol) --
|
||||
1148: "cp1148ms", # -- IBM EBCDIC International (500 + Euro symbol) --
|
||||
1149: "cp1149", # -- IBM EBCDIC Icelandic (20871 + Euro symbol) --
|
||||
1200: "utf-16-le", # -- Unicode UTF-16, little endian byte order (BMP of ISO 10646); --
|
||||
1201: "utf-16-be", # -- Unicode UTF-16, big endian byte order; --
|
||||
1250: "windows-1250", # -- ANSI Central European --
|
||||
1251: "windows-1251", # -- ANSI Cyrillic --
|
||||
1252: "windows-1252", # -- ANSI Latin 1 --
|
||||
1253: "windows-1253", # -- ANSI Greek --
|
||||
1254: "windows-1254", # -- ANSI Turkish --
|
||||
1255: "windows-1255", # -- ANSI Hebrew --
|
||||
1256: "windows-1256", # -- ANSI Arabic --
|
||||
1257: "windows-1257", # -- ANSI Baltic --
|
||||
1258: "windows-1258", # -- ANSI/OEM Vietnamese --
|
||||
1361: "Johab", # -- Korean (Johab) --
|
||||
10000: "macintosh", # -- MAC Roman; Western European (Mac) --
|
||||
10001: "x-mac-japanese", # -- Japanese (Mac) --
|
||||
10002: "", # -- (x-mac-chinesetrad) MAC Traditional Chinese (Big5) --
|
||||
10003: "x-mac-korean", # -- Korean (Mac) --
|
||||
10004: "", # -- (x-mac-arabic) Arabic (Mac) --
|
||||
10005: "", # -- (x-mac-hebrew) Hebrew (Mac) --
|
||||
10006: "x-mac-greek", # -- Greek (Mac) --
|
||||
10007: "x-mac-cyrillic", # -- Cyrillic (Mac) --
|
||||
10008: "", # -- (x-mac-chinesesimp) MAC Simplified Chinese (GB 2312) --
|
||||
10010: "", # -- (x-mac-romanian) Romanian (Mac) --
|
||||
10017: "", # -- (x-mac-ukrainian) Ukrainian (Mac) --
|
||||
10021: "", # -- (x-mac-thai) Thai (Mac) --
|
||||
10029: "x-mac-ce", # -- MAC Latin 2; Central European (Mac) --
|
||||
10079: "x-mac-icelandic", # -- Icelandic (Mac) --
|
||||
10081: "x-mac-turkish", # -- Turkish (Mac) --
|
||||
10082: "", # -- (x-mac-croatian) Croatian (Mac) --
|
||||
12000: "utf-32", # -- Unicode UTF-32, little endian byte order --
|
||||
12001: "utf-32BE", # -- Unicode UTF-32, big endian byte order --
|
||||
20000: "", # -- (x-Chinese_CNS) CNS Taiwan --
|
||||
20001: "", # -- (x-cp20001) TCA Taiwan --
|
||||
20002: "", # -- (x_Chinese-Eten) Eten Taiwan --
|
||||
20003: "", # -- (x-cp20003) IBM5550 Taiwan --
|
||||
20004: "", # -- (x-cp20004) TeleText Taiwan --
|
||||
20005: "", # -- (x-cp20005) Wang Taiwan --
|
||||
20105: "", # -- (x-IA5) IA5 (IRV International Alphabet No. 5, 7-bit) --
|
||||
20106: "", # -- (x-IA5-German) IA5 German (7-bit) --
|
||||
20107: "", # -- (x-IA5-Swedish) IA5 Swedish (7-bit) --
|
||||
20108: "", # -- (x-IA5-Norwegian) IA5 Norwegian (7-bit) --
|
||||
20127: "us-ascii", # -- US-ASCII (7-bit) --
|
||||
20261: "", # -- (x-cp20261) T.61 --
|
||||
20269: "", # -- (x-cp20269) ISO 6937 Non-Spacing Accent --
|
||||
20273: "IBM273", # -- IBM EBCDIC Germany --
|
||||
20277: "cp277", # -- IBM EBCDIC Denmark-Norway --
|
||||
20278: "cp278", # -- IBM EBCDIC Finland-Sweden --
|
||||
20280: "cp280", # -- IBM EBCDIC Italy --
|
||||
20284: "cp284", # -- IBM EBCDIC Latin America-Spain --
|
||||
20285: "cp285", # -- IBM EBCDIC United Kingdom --
|
||||
20290: "cp290", # -- IBM EBCDIC Japanese Katakana Extended --
|
||||
20297: "cp297", # -- IBM EBCDIC France --
|
||||
20420: "cp420", # -- IBM EBCDIC Arabic --
|
||||
20423: "", # -- (IBM423) IBM EBCDIC Greek --
|
||||
20424: "IBM424", # -- IBM EBCDIC Hebrew --
|
||||
20833: "cp833", # -- IBM EBCDIC Korean Extended --
|
||||
20838: "cp838", # -- IBM EBCDIC Thai --
|
||||
20866: "koi8-r", # -- Russian (KOI8-R); Cyrillic (KOI8-R) --
|
||||
20871: "cp871", # -- IBM EBCDIC Icelandic --
|
||||
20880: "", # -- (IBM880) IBM EBCDIC Cyrillic Russian --
|
||||
20905: "IBM905", # -- IBM EBCDIC Turkish --
|
||||
20924: "", # -- (IBM00924) IBM EBCDIC Latin 1/Open System (1047 + Euro symbol) --
|
||||
20932: "EUC-JP", # -- Japanese (JIS 0208-1990 and 0212-1990) --
|
||||
20936: "", # -- (x-cp20936) Simplified Chinese (GB2312) --
|
||||
20949: "", # -- (x-cp20949) Korean Wansung --
|
||||
21025: "cp1025", # -- IBM EBCDIC Cyrillic Serbian-Bulgarian --
|
||||
21027: "", # -- (deprecated) --
|
||||
21866: "koi8-u", # -- Ukrainian (KOI8-U); Cyrillic (KOI8-U) --
|
||||
28591: "iso-8859-1", # -- ISO 8859-1 Latin 1; Western European (ISO) --
|
||||
28592: "iso-8859-2", # -- ISO 8859-2 Central European; Central European (ISO) --
|
||||
28593: "iso-8859-3", # -- ISO 8859-3 Latin 3 --
|
||||
28594: "iso-8859-4", # -- ISO 8859-4 Baltic --
|
||||
28595: "iso-8859-5", # -- ISO 8859-5 Cyrillic --
|
||||
28596: "iso-8859-6", # -- ISO 8859-6 Arabic --
|
||||
28597: "iso-8859-7", # -- ISO 8859-7 Greek --
|
||||
28598: "iso-8859-8", # -- ISO 8859-8 Hebrew --
|
||||
28599: "iso-8859-9", # -- ISO 8859-9 Turkish --
|
||||
28603: "iso-8859-13", # -- ISO 8859-13 Estonian --
|
||||
28605: "iso-8859-15", # -- ISO 8859-15 Latin 9 --
|
||||
29001: "", # -- (x-Europa) Europa 3 --
|
||||
38598: "", # -- (iso-8859-8-i) ISO 8859-8 Hebrew --
|
||||
50220: "iso-2022-jp", # -- ISO 2022 Japanese with no halfwidth Katakana --
|
||||
50221: "csISO2022JP", # -- ISO 2022 Japanese with halfwidth Katakana --
|
||||
50222: "iso-2022-jp", # -- ISO 2022 Japanese JIS X 0201-1989 --
|
||||
50225: "iso-2022-kr", # -- ISO 2022 Korean --
|
||||
50227: "", # -- (x-cp50227) ISO 2022 Simplified Chinese --
|
||||
50229: "", # -- ISO 2022 Traditional Chinese --
|
||||
50930: "", # -- EBCDIC Japanese (Katakana) Extended --
|
||||
50931: "", # -- EBCDIC US-Canada and Japanese --
|
||||
50933: "", # -- EBCDIC Korean Extended and Korean --
|
||||
50935: "", # -- EBCDIC Simplified Chinese Extended and Simplified Chinese --
|
||||
50936: "", # -- EBCDIC Simplified Chinese --
|
||||
50937: "", # -- EBCDIC US-Canada and Traditional Chinese --
|
||||
50939: "", # -- EBCDIC Japanese (Latin) Extended and Japanese --
|
||||
51932: "euc-jp", # -- EUC Japanese --
|
||||
51936: "EUC-CN", # -- EUC Simplified Chinese --
|
||||
51949: "euc-kr", # -- EUC Korean --
|
||||
51950: "", # -- EUC Traditional Chinese --
|
||||
52936: "hz-gb-2312", # -- HZ-GB2312 Simplified Chinese --
|
||||
54936: "GB18030", # -- Windows XP and later: GB18030 Simplified Chinese (4 byte) --
|
||||
57002: "", # -- (x-iscii-de) ISCII Devanagari --
|
||||
57003: "", # -- (x-iscii-be) ISCII Bangla --
|
||||
57004: "", # -- (x-iscii-ta) ISCII Tamil --
|
||||
57005: "", # -- (x-iscii-te) ISCII Telugu --
|
||||
57006: "", # -- (x-iscii-as) ISCII Assamese --
|
||||
57007: "", # -- (x-iscii-or) ISCII Odia --
|
||||
57008: "", # -- (x-iscii-ka) ISCII Kannada --
|
||||
57009: "", # -- (x-iscii-ma) ISCII Malayalam --
|
||||
57010: "", # -- (x-iscii-gu) ISCII Gujarati --
|
||||
57011: "", # -- (x-iscii-pa) ISCII Punjabi --
|
||||
65000: "utf-7", # -- Unicode (UTF-7) --
|
||||
65001: "utf-8", # -- Unicode (UTF-8) --
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
"""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
|
||||
@@ -0,0 +1,612 @@
|
||||
"""Provides a catalog of OXMSG properties and property types.
|
||||
|
||||
This module provides mappings from integer PID and PTYP keys to meaningful names that can also be
|
||||
used to find more detailed information on web-search.
|
||||
|
||||
In addition it is a source for property names the user can select from to reference the message
|
||||
attibutes they are interested in.
|
||||
|
||||
Also used for discovery utilities to characterize a MSG file and its properties.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses as dc
|
||||
import datetime as dt
|
||||
|
||||
from oxmsg.domain import constants as c
|
||||
|
||||
|
||||
@dc.dataclass(frozen=True)
|
||||
class PropertyDescriptor:
|
||||
"""Describes an OXMSG property.
|
||||
|
||||
All attributes of an OXMSG object are properties, even though the body of variable-length
|
||||
properties like PID_ATTACH_FILENAME are stored in a stream.
|
||||
"""
|
||||
|
||||
pid: int
|
||||
"""The property-id. This is a 16-bit integer value specified by the MS-OXMSG standard."""
|
||||
|
||||
ms_name: str
|
||||
"""The name like "PidTagLastModificationTime" used by Microsoft for this property.
|
||||
|
||||
This is the value to search on for the specification of the property.
|
||||
"""
|
||||
|
||||
ptyp: int
|
||||
"""The property type, one of the OXMSG property types."""
|
||||
|
||||
|
||||
@dc.dataclass(frozen=True)
|
||||
class PropertyTypeDescriptor:
|
||||
"""Describes an OXMSG property data type."""
|
||||
|
||||
ptyp: int
|
||||
"""The property-type id, a 16-bit integer value specified by the MS-OXMSG standard."""
|
||||
|
||||
ms_name: str
|
||||
"""The name like "PtypInteger32" used by Microsoft for this type."""
|
||||
|
||||
python_type: type
|
||||
"""The Python type a property of this OXMSG type is converted to."""
|
||||
|
||||
|
||||
"""Mapping of property-id codes to property descriptor objects."""
|
||||
property_descriptors: dict[int, PropertyDescriptor] = {
|
||||
c.PID_ACCESS: PropertyDescriptor(c.PID_ACCESS, "PidTagAccess", c.PTYP_INTEGER_32),
|
||||
c.PID_ACCESS_LEVEL: PropertyDescriptor(
|
||||
c.PID_ACCESS_LEVEL, "PidTagAccessLevel", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_ACKNOWLEDGEMENT_MODE: PropertyDescriptor(
|
||||
c.PID_ACKNOWLEDGEMENT_MODE, "PidTagAcknowledgementMode", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_ADDRESS_BOOK_FOLDER_PATHNAME: PropertyDescriptor(
|
||||
c.PID_ADDRESS_BOOK_FOLDER_PATHNAME, "PidTagAddressBookFolderPathname", c.PTYP_STRING
|
||||
),
|
||||
c.PID_ADDRESS_BOOK_HOME_MESSAGE_DATABASE: PropertyDescriptor(
|
||||
c.PID_ADDRESS_BOOK_HOME_MESSAGE_DATABASE,
|
||||
"PidTagAddressBookHomeMessageDatabase",
|
||||
c.PTYP_OBJECT,
|
||||
),
|
||||
c.PID_ADDRESS_BOOK_IS_MEMBER_OF_DISTRIBUTION_LIST: PropertyDescriptor(
|
||||
c.PID_ADDRESS_BOOK_IS_MEMBER_OF_DISTRIBUTION_LIST,
|
||||
"PidTagAddressBookIsMemberOfDistributionList",
|
||||
c.PTYP_OBJECT,
|
||||
),
|
||||
c.PID_ADDRESS_BOOK_MANAGER_DISTINGUISHED_NAME: PropertyDescriptor(
|
||||
c.PID_ADDRESS_BOOK_MANAGER_DISTINGUISHED_NAME,
|
||||
"PidTagAddressBookManagerDistinguishedName",
|
||||
c.PTYP_OBJECT,
|
||||
),
|
||||
c.PID_ADDRESS_BOOK_MEMBER: PropertyDescriptor(
|
||||
c.PID_ADDRESS_BOOK_MEMBER, "PidTagAddressBookMember", c.PTYP_OBJECT
|
||||
),
|
||||
c.PID_ADDRESS_TYPE: PropertyDescriptor(c.PID_ADDRESS_TYPE, "PidTagAddressType", c.PTYP_STRING),
|
||||
c.PID_ALTERNATE_RECIPIENT_ALLOWED: PropertyDescriptor(
|
||||
c.PID_ALTERNATE_RECIPIENT_ALLOWED, "PidTagAlternateRecipientAllowed", c.PTYP_BOOLEAN
|
||||
),
|
||||
c.PID_ATTACHMENT_FLAGS: PropertyDescriptor(
|
||||
c.PID_ATTACHMENT_FLAGS, "PidTagAttachmentFlags", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_ATTACHMENT_HIDDEN: PropertyDescriptor(
|
||||
c.PID_ATTACHMENT_HIDDEN, "PidTagAttachmentHidden", c.PTYP_BOOLEAN
|
||||
),
|
||||
c.PID_ATTACHMENT_LINK_ID: PropertyDescriptor(
|
||||
c.PID_ATTACHMENT_LINK_ID, "PidTagAttachmentLinkId", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_ATTACH_CONTENT_ID: PropertyDescriptor(
|
||||
c.PID_ATTACH_CONTENT_ID, "PidTagAttachContentId", c.PTYP_STRING
|
||||
),
|
||||
c.PID_ATTACH_DATA_BINARY: PropertyDescriptor(
|
||||
c.PID_ATTACH_DATA_BINARY, "PidTagAttachDataBinary", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_ATTACH_ENCODING: PropertyDescriptor(
|
||||
c.PID_ATTACH_ENCODING, "PidTagAttachEncoding", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_ATTACH_EXTENSION: PropertyDescriptor(
|
||||
c.PID_ATTACH_EXTENSION, "PidTagAttachExtension", c.PTYP_STRING
|
||||
),
|
||||
c.PID_ATTACH_FILENAME: PropertyDescriptor(
|
||||
c.PID_ATTACH_FILENAME, "PidTagAttachFilename", c.PTYP_STRING
|
||||
),
|
||||
c.PID_ATTACH_FLAGS: PropertyDescriptor(
|
||||
c.PID_ATTACH_FLAGS, "PidTagAttachFlags", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_ATTACH_LONG_FILENAME: PropertyDescriptor(
|
||||
c.PID_ATTACH_LONG_FILENAME, "PidTagAttachLongFilename", c.PTYP_STRING
|
||||
),
|
||||
c.PID_ATTACH_METHOD: PropertyDescriptor(
|
||||
c.PID_ATTACH_METHOD, "PidTagAttachMethod", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_ATTACH_MIME_SEQUENCE: PropertyDescriptor(
|
||||
c.PID_ATTACH_MIME_SEQUENCE, "PidTagAttachMimeSequence", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_ATTACH_MIME_TAG: PropertyDescriptor(
|
||||
c.PID_ATTACH_MIME_TAG, "PidTagAttachMimeTag", c.PTYP_STRING
|
||||
),
|
||||
c.PID_ATTACH_NUMBER: PropertyDescriptor(
|
||||
c.PID_ATTACH_NUMBER, "PidTagAttachNumber", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_ATTACH_RENDERING: PropertyDescriptor(
|
||||
c.PID_ATTACH_RENDERING, "PidTagAttachRendering", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_ATTACH_RENDERING_POSITION: PropertyDescriptor(
|
||||
c.PID_ATTACH_RENDERING_POSITION, "PidTagAttachRenderingPosition", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_ATTACH_SIZE: PropertyDescriptor(c.PID_ATTACH_SIZE, "PidTagAttachSize", c.PTYP_INTEGER_32),
|
||||
c.PID_ATTACH_TAG: PropertyDescriptor(c.PID_ATTACH_TAG, "PidTagAttachTag", c.PTYP_BINARY),
|
||||
c.PID_AUTHORIZING_USERS: PropertyDescriptor(
|
||||
c.PID_AUTHORIZING_USERS, "PidTagAuthorizingUsers", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_AUTO_FORWARDED: PropertyDescriptor(
|
||||
c.PID_AUTO_FORWARDED, "PidTagAutoForwarded", c.PTYP_BOOLEAN
|
||||
),
|
||||
c.PID_AUTO_FORWARD_COMMENT: PropertyDescriptor(
|
||||
c.PID_AUTO_FORWARD_COMMENT, "PidTagAutoForwardComment", c.PTYP_STRING
|
||||
),
|
||||
c.PID_BODY: PropertyDescriptor(c.PID_BODY, "PidTagBody", c.PTYP_STRING),
|
||||
c.PID_CHANGE_KEY: PropertyDescriptor(c.PID_CHANGE_KEY, "PidTagChangeKey", c.PTYP_BINARY),
|
||||
c.PID_CLIENT_SUBMIT_TIME: PropertyDescriptor(
|
||||
c.PID_CLIENT_SUBMIT_TIME, "PidTagClientSubmitTime", c.PTYP_TIME
|
||||
),
|
||||
c.PID_CONTENT_CONFIDENTIALITY_ALGORITHM_ID: PropertyDescriptor(
|
||||
c.PID_CONTENT_CONFIDENTIALITY_ALGORITHM_ID,
|
||||
"PidTagContentConfidentialityAlgorithmId",
|
||||
c.PTYP_BINARY,
|
||||
),
|
||||
c.PID_CONTENT_CORRELATOR: PropertyDescriptor(
|
||||
c.PID_CONTENT_CORRELATOR, "PidTagContentCorrelator", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_CONTENT_IDENTIFIER: PropertyDescriptor(
|
||||
c.PID_CONTENT_IDENTIFIER, "PidTagContentIdentifier", c.PTYP_STRING
|
||||
),
|
||||
c.PID_CONTENT_LENGTH: PropertyDescriptor(
|
||||
c.PID_CONTENT_LENGTH, "PidTagContentLength", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_CONTENT_RETURN_REQUESTED: PropertyDescriptor(
|
||||
c.PID_CONTENT_RETURN_REQUESTED, "PidTagContentReturnRequested", c.PTYP_BOOLEAN
|
||||
),
|
||||
c.PID_CONVERSATION_INDEX: PropertyDescriptor(
|
||||
c.PID_CONVERSATION_INDEX, "PidTagConversationIndex", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_CONVERSATION_INDEX_TRACKING: PropertyDescriptor(
|
||||
c.PID_CONVERSATION_INDEX_TRACKING, "PidTagConversationIndexTracking", c.PTYP_BOOLEAN
|
||||
),
|
||||
c.PID_CONVERSATION_KEY: PropertyDescriptor(
|
||||
c.PID_CONVERSATION_KEY, "PidTagConversationKey", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_CONVERSATION_TOPIC: PropertyDescriptor(
|
||||
c.PID_CONVERSATION_TOPIC, "PidTagConversationTopic", c.PTYP_STRING
|
||||
),
|
||||
c.PID_CONVERSION_EITS: PropertyDescriptor(
|
||||
c.PID_CONVERSION_EITS, "PidTagConversionEits", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_CONVERSION_WITH_LOSS_PROHIBITED: PropertyDescriptor(
|
||||
c.PID_CONVERSION_WITH_LOSS_PROHIBITED, "PidTagConversionWithLossProhibited", c.PTYP_BOOLEAN
|
||||
),
|
||||
c.PID_CONVERTED_EITS: PropertyDescriptor(
|
||||
c.PID_CONVERTED_EITS, "PidTagConvertedEits", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_CREATION_TIME: PropertyDescriptor(c.PID_CREATION_TIME, "PidTagCreationTime", c.PTYP_TIME),
|
||||
c.PID_CREATOR_ADDRESS_TYPE: PropertyDescriptor(
|
||||
c.PID_CREATOR_ADDRESS_TYPE, "PidTagCreatorAddressType", c.PTYP_STRING
|
||||
),
|
||||
c.PID_CREATOR_EMAIL_ADDRESS: PropertyDescriptor(
|
||||
c.PID_CREATOR_EMAIL_ADDRESS, "PidTagCreatorEmailAddress", c.PTYP_STRING
|
||||
),
|
||||
c.PID_CREATOR_SIMPLE_DISPLAY_NAME: PropertyDescriptor(
|
||||
c.PID_CREATOR_SIMPLE_DISPLAY_NAME, "PidTagCreatorSimpleDisplayName", c.PTYP_STRING
|
||||
),
|
||||
c.PID_DEFERRED_DELIVERY_TIME: PropertyDescriptor(
|
||||
c.PID_DEFERRED_DELIVERY_TIME, "PidTagDeferredDeliveryTime", c.PTYP_TIME
|
||||
),
|
||||
c.PID_DELEGATION: PropertyDescriptor(c.PID_DELEGATION, "PidTagDelegation", c.PTYP_BINARY),
|
||||
c.PID_DELETE_AFTER_SUBMIT: PropertyDescriptor(
|
||||
c.PID_DELETE_AFTER_SUBMIT, "PidTagDeleteAfterSubmit", c.PTYP_BOOLEAN
|
||||
),
|
||||
c.PID_DELIVER_TIME: PropertyDescriptor(c.PID_DELIVER_TIME, "PidTagDeliverTime", c.PTYP_TIME),
|
||||
c.PID_DISCARD_REASON: PropertyDescriptor(
|
||||
c.PID_DISCARD_REASON, "PidTagDiscardReason", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_DISCLOSURE_OF_RECIPIENTS: PropertyDescriptor(
|
||||
c.PID_DISCLOSURE_OF_RECIPIENTS, "PidTagDisclosureOfRecipients", c.PTYP_BOOLEAN
|
||||
),
|
||||
c.PID_DISPLAY_BCC: PropertyDescriptor(c.PID_DISPLAY_BCC, "PidTagDisplayBcc", c.PTYP_STRING),
|
||||
c.PID_DISPLAY_CC: PropertyDescriptor(c.PID_DISPLAY_CC, "PidTagDisplayCc", c.PTYP_STRING),
|
||||
c.PID_DISPLAY_NAME: PropertyDescriptor(c.PID_DISPLAY_NAME, "PidTagDisplayName", c.PTYP_STRING),
|
||||
c.PID_DISPLAY_TO: PropertyDescriptor(c.PID_DISPLAY_TO, "PidTagDisplayTo", c.PTYP_STRING),
|
||||
c.PID_DISTRIBUTION_LIST_EXPANSION_HISTORY: PropertyDescriptor(
|
||||
c.PID_DISTRIBUTION_LIST_EXPANSION_HISTORY,
|
||||
"PidTagDistributionListExpansionHistory",
|
||||
c.PTYP_BINARY,
|
||||
),
|
||||
c.PID_DISTRIBUTION_LIST_EXPANSION_PROHIBITED: PropertyDescriptor(
|
||||
c.PID_DISTRIBUTION_LIST_EXPANSION_PROHIBITED,
|
||||
"PidTagDistributionListExpansionProhibited",
|
||||
c.PTYP_BOOLEAN,
|
||||
),
|
||||
c.PID_EMAIL_ADDRESS: PropertyDescriptor(
|
||||
c.PID_EMAIL_ADDRESS, "PidTagEmailAddress", c.PTYP_STRING
|
||||
),
|
||||
c.PID_END_DATE: PropertyDescriptor(c.PID_END_DATE, "PidTagEndDate", c.PTYP_TIME),
|
||||
c.PID_ENTRY_ID: PropertyDescriptor(c.PID_ENTRY_ID, "PidTagEntryId", c.PTYP_BINARY),
|
||||
c.PID_EXCEPTION_END_TIME: PropertyDescriptor(
|
||||
c.PID_EXCEPTION_END_TIME, "PidTagExceptionEndTime", c.PTYP_TIME
|
||||
),
|
||||
c.PID_EXCEPTION_START_TIME: PropertyDescriptor(
|
||||
c.PID_EXCEPTION_START_TIME, "PidTagExceptionStartTime", c.PTYP_TIME
|
||||
),
|
||||
c.PID_EXPIRY_TIME: PropertyDescriptor(c.PID_EXPIRY_TIME, "PidTagExpiryTime", c.PTYP_TIME),
|
||||
c.PID_FLAG_STATUS: PropertyDescriptor(c.PID_FLAG_STATUS, "PidTagFlagStatus", c.PTYP_INTEGER_32),
|
||||
c.PID_HAS_ATTACHMENTS: PropertyDescriptor(
|
||||
c.PID_HAS_ATTACHMENTS, "PidTagHasAttachments", c.PTYP_BOOLEAN
|
||||
),
|
||||
c.PID_HTML: PropertyDescriptor(c.PID_HTML, "PidTagHtml", c.PTYP_BINARY),
|
||||
c.PID_ICON_INDEX: PropertyDescriptor(c.PID_ICON_INDEX, "PidTagIconIndex", c.PTYP_INTEGER_32),
|
||||
c.PID_IMPLICIT_CONVERSION_PROHIBITED: PropertyDescriptor(
|
||||
c.PID_IMPLICIT_CONVERSION_PROHIBITED, "PidTagImplicitConversionProhibited", c.PTYP_BOOLEAN
|
||||
),
|
||||
c.PID_IMPORTANCE: PropertyDescriptor(c.PID_IMPORTANCE, "PidTagImportance", c.PTYP_INTEGER_32),
|
||||
c.PID_INITIAL_DETAILS_PANE: PropertyDescriptor(
|
||||
c.PID_INITIAL_DETAILS_PANE, "PidTagInitialDetailsPane", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_INSTANCE_KEY: PropertyDescriptor(c.PID_INSTANCE_KEY, "PidTagInstanceKey", c.PTYP_BINARY),
|
||||
c.PID_INTERNET_CODEPAGE: PropertyDescriptor(
|
||||
c.PID_INTERNET_CODEPAGE, "PidTagInternetCodepage", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_INTERNET_MESSAGE_ID: PropertyDescriptor(
|
||||
c.PID_INTERNET_MESSAGE_ID, "PidTagInternetMessageId", c.PTYP_STRING
|
||||
),
|
||||
c.PID_INTERNET_REFERENCES: PropertyDescriptor(
|
||||
c.PID_INTERNET_REFERENCES, "PidTagInternetReferences", c.PTYP_STRING
|
||||
),
|
||||
c.PID_IN_REPLY_TO_ID: PropertyDescriptor(
|
||||
c.PID_IN_REPLY_TO_ID, "PidTagInReplyToId", c.PTYP_STRING
|
||||
),
|
||||
c.PID_LAST_MODIFICATION_TIME: PropertyDescriptor(
|
||||
c.PID_LAST_MODIFICATION_TIME, "PidTagLastModificationTime", c.PTYP_TIME
|
||||
),
|
||||
c.PID_LAST_MODIFIER_NAME: PropertyDescriptor(
|
||||
c.PID_LAST_MODIFIER_NAME, "PidTagLastModifierName", c.PTYP_STRING
|
||||
),
|
||||
c.PID_LATEST_DELIVERY_TIME: PropertyDescriptor(
|
||||
c.PID_LATEST_DELIVERY_TIME, "PidTagLatestDeliveryTime", c.PTYP_TIME
|
||||
),
|
||||
c.PID_LID_CONTACT_ITEM_DATA: PropertyDescriptor(
|
||||
c.PID_LID_CONTACT_ITEM_DATA, "PidLidContactItemData", c.PTYP_STRING
|
||||
),
|
||||
c.PID_MESSAGE_CC_ME: PropertyDescriptor(
|
||||
c.PID_MESSAGE_CC_ME, "PidTagMessageCcMe", c.PTYP_BOOLEAN
|
||||
),
|
||||
c.PID_MESSAGE_CLASS: PropertyDescriptor(
|
||||
c.PID_MESSAGE_CLASS, "PidTagMessageClass", c.PTYP_STRING
|
||||
),
|
||||
c.PID_MESSAGE_DELIVERY_ID: PropertyDescriptor(
|
||||
c.PID_MESSAGE_DELIVERY_ID, "PidTagMessageDeliveryId", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_MESSAGE_DELIVERY_TIME: PropertyDescriptor(
|
||||
c.PID_MESSAGE_DELIVERY_TIME, "PidTagMessageDeliveryTime", c.PTYP_TIME
|
||||
),
|
||||
c.PID_MESSAGE_FLAGS: PropertyDescriptor(
|
||||
c.PID_MESSAGE_FLAGS, "PidTagMessageFlags", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_MESSAGE_LOCALE_ID: PropertyDescriptor(
|
||||
c.PID_MESSAGE_LOCALE_ID, "PidTagMessageLocaleId", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_MESSAGE_RECIPIENT_ME: PropertyDescriptor(
|
||||
c.PID_MESSAGE_RECIPIENT_ME, "PidTagMessageRecipientMe", c.PTYP_STRING
|
||||
),
|
||||
c.PID_MESSAGE_SECURITY_LABEL: PropertyDescriptor(
|
||||
c.PID_MESSAGE_SECURITY_LABEL, "PidTagMessageSecurityLabel", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_MESSAGE_SIZE_EXTENDED: PropertyDescriptor(
|
||||
c.PID_MESSAGE_SIZE_EXTENDED, "PidTagMessageSizeExtended", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_MESSAGE_SUBMISSION_ID: PropertyDescriptor(
|
||||
c.PID_MESSAGE_SUBMISSION_ID, "PidTagMessageSubmissionId", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_MESSAGE_TO_ME: PropertyDescriptor(
|
||||
c.PID_MESSAGE_TO_ME, "PidTagMessageToMe", c.PTYP_BOOLEAN
|
||||
),
|
||||
c.PID_NATIVE_BODY: PropertyDescriptor(c.PID_NATIVE_BODY, "PidTagNativeBody", c.PTYP_INTEGER_32),
|
||||
c.PID_NORMALIZED_SUBJECT: PropertyDescriptor(
|
||||
c.PID_NORMALIZED_SUBJECT, "PidTagNormalizedSubject", c.PTYP_STRING
|
||||
),
|
||||
c.PID_NON_RECEIPT_NOTIFICATION_REQUESTED: PropertyDescriptor(
|
||||
c.PID_NON_RECEIPT_NOTIFICATION_REQUESTED,
|
||||
"PidTagNonReceiptNotificationRequested",
|
||||
c.PTYP_BOOLEAN,
|
||||
),
|
||||
c.PID_OBJECT_TYPE: PropertyDescriptor(c.PID_OBJECT_TYPE, "PidTagObjectType", c.PTYP_INTEGER_32),
|
||||
c.PID_OBSOLETED_MESSAGE_IDS: PropertyDescriptor(
|
||||
c.PID_OBSOLETED_MESSAGE_IDS, "PidTagObsoletedMessageIds", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_ORIGINALLY_INTENDED_RECIPIENT_NAME: PropertyDescriptor(
|
||||
c.PID_ORIGINALLY_INTENDED_RECIPIENT_NAME,
|
||||
"PidTagOriginallyIntendedRecipientName",
|
||||
c.PTYP_BINARY,
|
||||
),
|
||||
c.PID_ORIGINALLY_INTENDED_RECIP_ADDRTYPE: PropertyDescriptor(
|
||||
c.PID_ORIGINALLY_INTENDED_RECIP_ADDRTYPE,
|
||||
"PidTagOriginallyIntendedRecipAddrtype",
|
||||
c.PTYP_STRING,
|
||||
),
|
||||
c.PID_ORIGINALLY_INTENDED_RECIP_EMAIL_ADDRESS: PropertyDescriptor(
|
||||
c.PID_ORIGINALLY_INTENDED_RECIP_EMAIL_ADDRESS,
|
||||
"PidTagOriginallyIntendedRecipEmailAddress",
|
||||
c.PTYP_STRING,
|
||||
),
|
||||
c.PID_ORIGINAL_AUTHOR_ADDRESS_TYPE: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_AUTHOR_ADDRESS_TYPE, "PidTagOriginalAuthorAddressType", c.PTYP_STRING
|
||||
),
|
||||
c.PID_ORIGINAL_AUTHOR_EMAIL_ADDRESS: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_AUTHOR_EMAIL_ADDRESS, "PidTagOriginalAuthorEmailAddress", c.PTYP_STRING
|
||||
),
|
||||
c.PID_ORIGINAL_AUTHOR_ENTRY_ID: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_AUTHOR_ENTRY_ID, "PidTagOriginalAuthorEntryId", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_ORIGINAL_AUTHOR_NAME: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_AUTHOR_NAME, "PidTagOriginalAuthorName", c.PTYP_STRING
|
||||
),
|
||||
c.PID_ORIGINAL_DELIVERY_TIME: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_DELIVERY_TIME, "PidTagOriginalDeliveryTime", c.PTYP_TIME
|
||||
),
|
||||
c.PID_ORIGINAL_DISPLAY_BCC: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_DISPLAY_BCC, "PidTagOriginalDisplayBcc", c.PTYP_STRING
|
||||
),
|
||||
c.PID_ORIGINAL_DISPLAY_CC: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_DISPLAY_CC, "PidTagOriginalDisplayCc", c.PTYP_STRING
|
||||
),
|
||||
c.PID_ORIGINAL_DISPLAY_TO: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_DISPLAY_TO, "PidTagOriginalDisplayTo", c.PTYP_STRING
|
||||
),
|
||||
c.PID_ORIGINAL_EITS: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_EITS, "PidTagOriginalEits", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_ORIGINAL_MESSAGE_CLASS: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_MESSAGE_CLASS, "PidTagOriginalMessageClass", c.PTYP_STRING
|
||||
),
|
||||
c.PID_ORIGINAL_MESSAGE_ID: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_MESSAGE_ID, "PidTagOriginalMessageId", c.PTYP_STRING
|
||||
),
|
||||
c.PID_ORIGINAL_SENDER_ADDRESS_TYPE: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_SENDER_ADDRESS_TYPE, "PidTagOriginalSenderAddressType", c.PTYP_STRING
|
||||
),
|
||||
c.PID_ORIGINAL_SENDER_EMAIL_ADDRESS: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_SENDER_EMAIL_ADDRESS, "PidTagOriginalSenderEmailAddress", c.PTYP_STRING
|
||||
),
|
||||
c.PID_ORIGINAL_SENDER_ENTRY_ID: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_SENDER_ENTRY_ID, "PidTagOriginalSenderEntryId", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_ORIGINAL_SENDER_NAME: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_SENDER_NAME, "PidTagOriginalSenderName", c.PTYP_STRING
|
||||
),
|
||||
c.PID_ORIGINAL_SENDER_SEARCH_KEY: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_SENDER_SEARCH_KEY, "PidTagOriginalSenderSearchKey", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_ORIGINAL_SENSITIVITY: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_SENSITIVITY, "PidTagOriginalSensitivity", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_ORIGINAL_SENT_REPRESENTING_ADDRESS_TYPE: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_SENT_REPRESENTING_ADDRESS_TYPE,
|
||||
"PidTagOriginalSentRepresentingAddressType",
|
||||
c.PTYP_STRING,
|
||||
),
|
||||
c.PID_ORIGINAL_SENT_REPRESENTING_EMAIL_ADDRESS: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_SENT_REPRESENTING_EMAIL_ADDRESS,
|
||||
"PidTagOriginalSentRepresentingEmailAddress",
|
||||
c.PTYP_STRING,
|
||||
),
|
||||
c.PID_ORIGINAL_SENT_REPRESENTING_ENTRY_ID: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_SENT_REPRESENTING_ENTRY_ID,
|
||||
"PidTagOriginalSentRepresentingEntryId",
|
||||
c.PTYP_BINARY,
|
||||
),
|
||||
c.PID_ORIGINAL_SENT_REPRESENTING_NAME: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_SENT_REPRESENTING_NAME, "PidTagOriginalSentRepresentingName", c.PTYP_STRING
|
||||
),
|
||||
c.PID_ORIGINAL_SENT_REPRESENTING_SEARCH_KEY: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_SENT_REPRESENTING_SEARCH_KEY,
|
||||
"PidTagOriginalSentRepresentingSearchKey",
|
||||
c.PTYP_BINARY,
|
||||
),
|
||||
c.PID_ORIGINAL_SUBJECT: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_SUBJECT, "PidTagOriginalSubject", c.PTYP_STRING
|
||||
),
|
||||
c.PID_ORIGINAL_SUBMIT_TIME: PropertyDescriptor(
|
||||
c.PID_ORIGINAL_SUBMIT_TIME, "PidTagOriginalSubmitTime", c.PTYP_TIME
|
||||
),
|
||||
c.PID_ORIGINATOR_CERTIFICATE: PropertyDescriptor(
|
||||
c.PID_ORIGINATOR_CERTIFICATE, "PidTagOriginatorCertificate", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_ORIGINATOR_DELIVERY_REPORT_REQUESTED: PropertyDescriptor(
|
||||
c.PID_ORIGINATOR_DELIVERY_REPORT_REQUESTED,
|
||||
"PidTagOriginatorDeliveryReportRequested",
|
||||
c.PTYP_BOOLEAN,
|
||||
),
|
||||
c.PID_ORIGINATOR_RETURN_ADDRESS: PropertyDescriptor(
|
||||
c.PID_ORIGINATOR_RETURN_ADDRESS, "PidTagOriginatorReturnAddress", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_ORIGIN_CHECK: PropertyDescriptor(c.PID_ORIGIN_CHECK, "PidTagOriginCheck", c.PTYP_BINARY),
|
||||
c.PID_OWNER_APPOINTMENT_ID: PropertyDescriptor(
|
||||
c.PID_OWNER_APPOINTMENT_ID, "PidTagOwnerAppointmentId", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_PARENT_DISPLAY: PropertyDescriptor(
|
||||
c.PID_PARENT_DISPLAY, "PidTagParentDisplay", c.PTYP_STRING
|
||||
),
|
||||
c.PID_PARENT_KEY: PropertyDescriptor(c.PID_PARENT_KEY, "PidTagParentKey", c.PTYP_BINARY),
|
||||
c.PID_PREDECESSOR_CHANGE_LIST: PropertyDescriptor(
|
||||
c.PID_PREDECESSOR_CHANGE_LIST, "PidTagPredecessorChangeList", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_PRIORITY: PropertyDescriptor(c.PID_PRIORITY, "PidTagPriority", c.PTYP_INTEGER_32),
|
||||
c.PID_PROOF_OF_SUBMISSION_REQUESTED: PropertyDescriptor(
|
||||
c.PID_PROOF_OF_SUBMISSION_REQUESTED, "PidTagProofOfSubmissionRequested", c.PTYP_BOOLEAN
|
||||
),
|
||||
c.PID_READ_RECEIPT_ENTRY_ID: PropertyDescriptor(
|
||||
c.PID_READ_RECEIPT_ENTRY_ID, "PidTagReadReceiptEntryId", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_READ_RECEIPT_REQUESTED: PropertyDescriptor(
|
||||
c.PID_READ_RECEIPT_REQUESTED, "PidTagReadReceiptRequested", c.PTYP_BOOLEAN
|
||||
),
|
||||
c.PID_READ_RECEIPT_SEARCH_KEY: PropertyDescriptor(
|
||||
c.PID_READ_RECEIPT_SEARCH_KEY, "PidTagReadReceiptSearchKey", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_RECEIPT_TIME: PropertyDescriptor(c.PID_RECEIPT_TIME, "PidTagReceiptTime", c.PTYP_TIME),
|
||||
c.PID_RECEIVED_BY_ADDRESS_TYPE: PropertyDescriptor(
|
||||
c.PID_RECEIVED_BY_ADDRESS_TYPE, "PidTagReceivedByAddressType", c.PTYP_STRING
|
||||
),
|
||||
c.PID_RECEIVED_BY_EMAIL_ADDRESS: PropertyDescriptor(
|
||||
c.PID_RECEIVED_BY_EMAIL_ADDRESS, "PidTagReceivedByEmailAddress", c.PTYP_STRING
|
||||
),
|
||||
c.PID_RECEIVED_BY_ENTRY_ID: PropertyDescriptor(
|
||||
c.PID_RECEIVED_BY_ENTRY_ID, "PidTagReceivedByEntryId", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_RECEIVED_BY_NAME: PropertyDescriptor(
|
||||
c.PID_RECEIVED_BY_NAME, "PidTagReceivedByName", c.PTYP_STRING
|
||||
),
|
||||
c.PID_RECEIVED_BY_SEARCH_KEY: PropertyDescriptor(
|
||||
c.PID_RECEIVED_BY_SEARCH_KEY, "PidTagReceivedBySearchKey", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_RECEIVED_REPRESENTING_ADDRESS_TYPE: PropertyDescriptor(
|
||||
c.PID_RECEIVED_REPRESENTING_ADDRESS_TYPE,
|
||||
"PidTagReceivedRepresentingAddressType",
|
||||
c.PTYP_STRING,
|
||||
),
|
||||
c.PID_RECEIVED_REPRESENTING_EMAIL_ADDRESS: PropertyDescriptor(
|
||||
c.PID_RECEIVED_REPRESENTING_EMAIL_ADDRESS,
|
||||
"PidTagReceivedRepresentingEmailAddress",
|
||||
c.PTYP_STRING,
|
||||
),
|
||||
c.PID_RECEIVED_REPRESENTING_ENTRY_ID: PropertyDescriptor(
|
||||
c.PID_RECEIVED_REPRESENTING_ENTRY_ID, "PidTagReceivedRepresentingEntryId", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_RECEIVED_REPRESENTING_NAME: PropertyDescriptor(
|
||||
c.PID_RECEIVED_REPRESENTING_NAME, "PidTagReceivedRepresentingName", c.PTYP_STRING
|
||||
),
|
||||
c.PID_RECEIVED_REPRESENTING_SEARCH_KEY: PropertyDescriptor(
|
||||
c.PID_RECEIVED_REPRESENTING_SEARCH_KEY, "PidTagReceivedRepresentingSearchKey", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_RECIPIENT_REASSIGNMENT_PROHIBITED: PropertyDescriptor(
|
||||
c.PID_RECIPIENT_REASSIGNMENT_PROHIBITED,
|
||||
"PidTagRecipientReassignmentProhibited",
|
||||
c.PTYP_BOOLEAN,
|
||||
),
|
||||
c.PID_RECIPIENT_TYPE: PropertyDescriptor(
|
||||
c.PID_RECIPIENT_TYPE,
|
||||
"PidTagRecipientType",
|
||||
c.PTYP_INTEGER_32,
|
||||
),
|
||||
c.PID_RECORD_KEY: PropertyDescriptor(c.PID_RECORD_KEY, "PidTagRecordKey", c.PTYP_BINARY),
|
||||
c.PID_REDIRECTION_HISTORY: PropertyDescriptor(
|
||||
c.PID_REDIRECTION_HISTORY, "PidTagRedirectionHistory", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_REPLY_RECIPIENT_ENTRIES: PropertyDescriptor(
|
||||
c.PID_REPLY_RECIPIENT_ENTRIES, "PidTagReplyRecipientEntries", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_REPLY_RECIPIENT_NAMES: PropertyDescriptor(
|
||||
c.PID_REPLY_RECIPIENT_NAMES, "PidTagReplyRecipientNames", c.PTYP_STRING
|
||||
),
|
||||
c.PID_REPLY_TIME: PropertyDescriptor(c.PID_REPLY_TIME, "PidTagReplyTime", c.PTYP_TIME),
|
||||
c.PID_REPORT: PropertyDescriptor(c.PID_REPORT, "PidTagReportTag", c.PTYP_BINARY),
|
||||
c.PID_REPORT_DISPOSITION: PropertyDescriptor(
|
||||
c.PID_REPORT_DISPOSITION, "PidTagReportDisposition", c.PTYP_STRING
|
||||
),
|
||||
c.PID_REPORT_DISPOSITION_MODE: PropertyDescriptor(
|
||||
c.PID_REPORT_DISPOSITION_MODE, "PidTagReportDispositionMode", c.PTYP_STRING
|
||||
),
|
||||
c.PID_REPORT_ENTRY_ID: PropertyDescriptor(
|
||||
c.PID_REPORT_ENTRY_ID, "PidTagReportEntryId", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_REPORT_NAME: PropertyDescriptor(c.PID_REPORT_NAME, "PidTagReportName", c.PTYP_STRING),
|
||||
c.PID_REPORT_SEARCH_KEY: PropertyDescriptor(
|
||||
c.PID_REPORT_SEARCH_KEY, "PidTagReportSearchKey", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_REPORT_TIME: PropertyDescriptor(c.PID_REPORT_TIME, "PidTagReportTime", c.PTYP_TIME),
|
||||
c.PID_RESPONSE_REQUESTED: PropertyDescriptor(
|
||||
c.PID_RESPONSE_REQUESTED, "PidTagResponseRequested", c.PTYP_BOOLEAN
|
||||
),
|
||||
c.PID_ROW_ID: PropertyDescriptor(c.PID_ROW_ID, "PidTagRowId", c.PTYP_INTEGER_32),
|
||||
c.PID_RTF_COMPRESSED: PropertyDescriptor(
|
||||
c.PID_RTF_COMPRESSED, "PidTagRtfCompressed", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_RTF_IN_SYNC: PropertyDescriptor(c.PID_RTF_IN_SYNC, "PidTagRtfInSync", c.PTYP_BOOLEAN),
|
||||
c.PID_SEARCH_KEY: PropertyDescriptor(c.PID_SEARCH_KEY, "PidTagSearchKey", c.PTYP_BINARY),
|
||||
c.PID_SECURITY: PropertyDescriptor(c.PID_SECURITY, "PidTagSecurity", c.PTYP_INTEGER_32),
|
||||
c.PID_SENDER_ADDRESS_TYPE: PropertyDescriptor(
|
||||
c.PID_SENDER_ADDRESS_TYPE, "PidTagSenderAddressType", c.PTYP_STRING
|
||||
),
|
||||
c.PID_SENDER_EMAIL_ADDRESS: PropertyDescriptor(
|
||||
c.PID_SENDER_EMAIL_ADDRESS, "PidTagSenderEmailAddress", c.PTYP_STRING
|
||||
),
|
||||
c.PID_SENDER_ENTRY_ID: PropertyDescriptor(
|
||||
c.PID_SENDER_ENTRY_ID, "PidTagSenderEntryId", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_SENDER_NAME: PropertyDescriptor(c.PID_SENDER_NAME, "PidTagSenderName", c.PTYP_STRING),
|
||||
c.PID_SENDER_SEARCH_KEY: PropertyDescriptor(
|
||||
c.PID_SENDER_SEARCH_KEY, "PidTagSenderSearchKey", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_SENDER_SMTP_ADDRESS: PropertyDescriptor(
|
||||
c.PID_SENDER_SMTP_ADDRESS, "PidTagSenderSmtpAddress", c.PTYP_STRING
|
||||
),
|
||||
c.PID_SENSITIVITY: PropertyDescriptor(
|
||||
c.PID_SENSITIVITY, "PidTagSensitivity", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_SENT_REPRESENTING_ADDRESS_TYPE: PropertyDescriptor(
|
||||
c.PID_SENT_REPRESENTING_ADDRESS_TYPE, "PidTagSentRepresentingAddressType", c.PTYP_STRING
|
||||
),
|
||||
c.PID_SENT_REPRESENTING_EMAIL_ADDRESS: PropertyDescriptor(
|
||||
c.PID_SENT_REPRESENTING_EMAIL_ADDRESS, "PidTagSentRepresentingEmailAddress", c.PTYP_STRING
|
||||
),
|
||||
c.PID_SENT_REPRESENTING_ENTRY_ID: PropertyDescriptor(
|
||||
c.PID_SENT_REPRESENTING_ENTRY_ID, "PidTagSentRepresentingEntryId", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_SENT_REPRESENTING_NAME: PropertyDescriptor(
|
||||
c.PID_SENT_REPRESENTING_NAME, "PidTagSentRepresentingName", c.PTYP_STRING
|
||||
),
|
||||
c.PID_SENT_REPRESENTING_SEARCH_KEY: PropertyDescriptor(
|
||||
c.PID_SENT_REPRESENTING_SEARCH_KEY, "PidTagSentRepresentingSearchKey", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_SMTP_ADDRESS: PropertyDescriptor(c.PID_SMTP_ADDRESS, "PidTagSmtpAddress", c.PTYP_STRING),
|
||||
c.PID_START_DATE: PropertyDescriptor(c.PID_START_DATE, "PidTagStartDate", c.PTYP_TIME),
|
||||
c.PID_STORE_SUPPORT_MASK: PropertyDescriptor(
|
||||
c.PID_STORE_SUPPORT_MASK, "PidTagStoreSupportMask", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_STORE_UNICODE_MASK: PropertyDescriptor(
|
||||
c.PID_STORE_UNICODE_MASK, "PidTagStoreUnicodeMask", c.PTYP_INTEGER_32
|
||||
),
|
||||
c.PID_SUBJECT: PropertyDescriptor(c.PID_SUBJECT, "PidTagSubject", c.PTYP_STRING),
|
||||
c.PID_SUBJECT_MESSAGE_ID: PropertyDescriptor(
|
||||
c.PID_SUBJECT_MESSAGE_ID, "PidTagSubjectMessageId", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_SUBJECT_PREFIX: PropertyDescriptor(
|
||||
c.PID_SUBJECT_PREFIX, "PidTagSubjectPrefix", c.PTYP_STRING
|
||||
),
|
||||
c.PID_TNEF_CORRELATION_KEY: PropertyDescriptor(
|
||||
c.PID_TNEF_CORRELATION_KEY, "PidTagTnefCorrelationKey", c.PTYP_BINARY
|
||||
),
|
||||
c.PID_TRANSPORT_MESSAGE_HEADERS: PropertyDescriptor(
|
||||
c.PID_TRANSPORT_MESSAGE_HEADERS, "PidTagTransportMessageHeaders", c.PTYP_STRING
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
"""Mapping of property-type codes to descriptor objects."""
|
||||
property_type_descriptors: dict[int, PropertyTypeDescriptor] = {
|
||||
c.PTYP_BINARY: PropertyTypeDescriptor(c.PTYP_BINARY, "PtypBinary", bytes),
|
||||
c.PTYP_BOOLEAN: PropertyTypeDescriptor(c.PTYP_BOOLEAN, "PtypBoolean", bool),
|
||||
c.PTYP_FLOATING_64: PropertyTypeDescriptor(c.PTYP_FLOATING_64, "PtypFloating64", float),
|
||||
c.PTYP_GUID: PropertyTypeDescriptor(c.PTYP_GUID, "PtypGuid", bytes),
|
||||
c.PTYP_INTEGER_16: PropertyTypeDescriptor(c.PTYP_INTEGER_16, "PtypInteger16", int),
|
||||
c.PTYP_INTEGER_32: PropertyTypeDescriptor(c.PTYP_INTEGER_32, "PtypInteger32", int),
|
||||
c.PTYP_MULTIPLE_INTEGER_32: PropertyTypeDescriptor(
|
||||
c.PTYP_MULTIPLE_INTEGER_32, "PtypMultipleInteger32", list[int]
|
||||
),
|
||||
c.PTYP_MULTIPLE_STRING: PropertyTypeDescriptor(
|
||||
c.PTYP_MULTIPLE_STRING, "PtypMultipleString", list[str]
|
||||
),
|
||||
c.PTYP_OBJECT: PropertyTypeDescriptor(c.PTYP_OBJECT, "PtypObject", bytes),
|
||||
c.PTYP_STRING: PropertyTypeDescriptor(c.PTYP_STRING, "PtypString", str),
|
||||
c.PTYP_STRING8: PropertyTypeDescriptor(c.PTYP_STRING8, "PtypString8", str),
|
||||
c.PTYP_TIME: PropertyTypeDescriptor(c.PTYP_TIME, "PtypTime", dt.datetime),
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
"""The `Message` object is the primary interface object in `python-oxmsg`.
|
||||
|
||||
This object is returned by the `Message.load()` classmethod:
|
||||
|
||||
```python
|
||||
>>> from oxmsg import Message
|
||||
|
||||
>>> msg = Message.load("message.msg")
|
||||
>>> msg.subject
|
||||
'Agenda for the Acme meeting'
|
||||
```
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
import email.message
|
||||
import email.parser
|
||||
import os
|
||||
import struct
|
||||
from typing import IO, Iterator
|
||||
|
||||
from olefile import OleFileIO, isOleFile
|
||||
|
||||
from oxmsg.attachment import Attachment
|
||||
from oxmsg.domain import constants as c
|
||||
from oxmsg.domain import model as m
|
||||
from oxmsg.properties import Properties
|
||||
from oxmsg.recipient import Recipient
|
||||
from oxmsg.storage import Storage
|
||||
from oxmsg.util import lazyproperty
|
||||
|
||||
|
||||
class Message:
|
||||
"""An Outlook email message loaded from an OXMSG format (.msg) file."""
|
||||
|
||||
def __init__(self, storage: m.StorageT):
|
||||
self._storage = storage
|
||||
|
||||
@classmethod
|
||||
def load(cls, msg_file: str | IO[bytes] | bytes) -> Message:
|
||||
"""Load an instance from `msg_file`.
|
||||
|
||||
`msg_file` can be a file-path, a file-like object, or the bytes of the MSG file.
|
||||
"""
|
||||
# -- ensure magic bytes on file are as expected --
|
||||
is_olefile = (
|
||||
isOleFile(data=msg_file) if isinstance(msg_file, bytes) else isOleFile(msg_file)
|
||||
)
|
||||
if not is_olefile:
|
||||
filename = os.path.basename(msg_file) if isinstance(msg_file, str) else "msg_file"
|
||||
raise ValueError(f"{filename} is not an Outlook MSG file")
|
||||
|
||||
with OleFileIO(msg_file) as ole:
|
||||
root_storage = Storage.from_ole(ole)
|
||||
|
||||
self = cls(root_storage)
|
||||
self._validate()
|
||||
return self
|
||||
|
||||
@property
|
||||
def attachment_count(self) -> int:
|
||||
"""Number of attachments on this message."""
|
||||
return self._header_prop_values[3]
|
||||
|
||||
@property
|
||||
def attachments(self) -> tuple[Attachment, ...]:
|
||||
"""Attachment object for each attachment in this message."""
|
||||
return tuple(self._iter_attachments())
|
||||
|
||||
@property
|
||||
def body(self) -> str | None:
|
||||
"""The plain-text body of this message; `None` if not present."""
|
||||
return self.properties.str_prop_value(c.PID_BODY)
|
||||
|
||||
@property
|
||||
def html_body(self) -> str | None:
|
||||
"""The HTML body of this message if present, `None` otherwise."""
|
||||
# -- body HTML most commonly (these days) is bytes encoded with internet-codepage --
|
||||
if html_bytes := self.properties.binary_prop_value(c.PID_HTML):
|
||||
return html_bytes.decode(self.properties.body_encoding)
|
||||
# -- however older emails may encode the HTML as Unicode or an 8-bit string --
|
||||
if html_str := self.properties.str_prop_value(c.PID_BODY_HTML):
|
||||
return html_str
|
||||
return None
|
||||
|
||||
@property
|
||||
def message_headers(self) -> dict[str, str]:
|
||||
"""From, To, Content-Type, etc. headers for this message as {name: value} mapping."""
|
||||
return dict(self._message_headers)
|
||||
|
||||
@property
|
||||
def message_class(self) -> str:
|
||||
"""Outlook Message Class identifier like "IPM.Note" for this message."""
|
||||
return self.properties.str_prop_value(c.PID_MESSAGE_CLASS) or ""
|
||||
|
||||
@property
|
||||
def properties(self) -> Properties:
|
||||
"""Provides access to the properties of this OXMSG object."""
|
||||
return self._properties
|
||||
|
||||
@property
|
||||
def recipients(self) -> tuple[Recipient, ...]:
|
||||
"""`Recipient` object for each recipient of this message."""
|
||||
return tuple(self._iter_recipients())
|
||||
|
||||
@property
|
||||
def sender(self) -> str | None:
|
||||
"""Name and email address of the message sender.
|
||||
|
||||
Like '"John Doe" <john@bigisp.com>'.
|
||||
|
||||
None if it is not recorded in the message. This may occur when the message is a draft or
|
||||
system-generated or perhaps in other cases.
|
||||
|
||||
Note that the value of the From: message header is returned when present (usually I expect)
|
||||
and may contain multiple addresses separated by commas.
|
||||
"""
|
||||
# -- start by looking in the message "From:" header --
|
||||
if from_header_value := self._message_headers["from"]:
|
||||
return from_header_value
|
||||
# -- assemble from parts --
|
||||
props = self.properties
|
||||
raw_name = (props.str_prop_value(c.PID_SENDER_NAME) or "").strip()
|
||||
name = f'"{raw_name}" ' if raw_name else ""
|
||||
email = props.str_prop_value(c.PID_SENDER_EMAIL_ADDRESS) or props.str_prop_value(
|
||||
c.PID_SENDER_SMTP_ADDRESS
|
||||
)
|
||||
return f"{name}<{email}>" if email else props.str_prop_value(c.PID_SENT_REPRESENTING_NAME)
|
||||
|
||||
@property
|
||||
def sent_date(self) -> dt.datetime | None:
|
||||
"""When this message was submitted by the sender.
|
||||
|
||||
This value will be `None` if the message has not been sent and possibly in other cases.
|
||||
"""
|
||||
return self.properties.date_prop_value(c.PID_CLIENT_SUBMIT_TIME)
|
||||
|
||||
@property
|
||||
def subject(self) -> str:
|
||||
"""Subject line of this message."""
|
||||
return self.properties.str_prop_value(c.PID_SUBJECT) or ""
|
||||
|
||||
@lazyproperty
|
||||
def _header_prop_values(self) -> tuple[int, int, int, int]:
|
||||
"""The property values in the MSG-root properties header.
|
||||
|
||||
It is a tuple of the four int values:
|
||||
- next_recipient_id
|
||||
- next_attachment_id
|
||||
- recipient_count
|
||||
- attachment_count
|
||||
"""
|
||||
return struct.unpack("<8x4I", self._storage.properties_stream_bytes[:24])
|
||||
|
||||
def _iter_attachments(self) -> Iterator[Attachment]:
|
||||
"""Generate `Attachment` object for each attachment in this message.
|
||||
|
||||
Should need to be called at most once.
|
||||
"""
|
||||
return (Attachment(storage) for storage in self._storage.iter_attachment_storages())
|
||||
|
||||
def _iter_recipients(self) -> Iterator[Recipient]:
|
||||
"""Generate `Recipient` object for each recipient in this message.
|
||||
|
||||
Should need to be called at most once.
|
||||
"""
|
||||
return (Recipient(storage) for storage in self._storage.iter_recipient_storages())
|
||||
|
||||
@lazyproperty
|
||||
def _message_headers(self) -> email.message.Message:
|
||||
"""From, To, Content-Type, etc. headers for this message as Message object.
|
||||
|
||||
This provides case-insensitive access along with some other convenient behaviors not
|
||||
available from a `dict`.
|
||||
"""
|
||||
headers = self.properties.str_prop_value(c.PID_TRANSPORT_MESSAGE_HEADERS) or ""
|
||||
return email.parser.HeaderParser().parsestr(headers)
|
||||
|
||||
@lazyproperty
|
||||
def _properties(self) -> Properties:
|
||||
return Properties(self._storage, properties_header_offset=m.MSG_HDR_OFFSET)
|
||||
|
||||
def _validate(self) -> None:
|
||||
"""Raise if this message is invalid for one of a variety of possible reasons."""
|
||||
# -- for now, we only process email messages --
|
||||
# if not self.message_class.startswith("IPM.Note"):
|
||||
# raise ValueError(f"{self.message_class} files not supported")
|
||||
@@ -0,0 +1,439 @@
|
||||
"""Provides access to all properties of a top-level object (message, recipient, attachment).
|
||||
|
||||
This object is accessed using the `.properties` property of a top-level object. The properties for
|
||||
each top-level object are segregated into its own properties object.
|
||||
|
||||
Many property-ids (PIDs) and property-types (PTYPs) are available as constants in
|
||||
`oxml.domain.contants`.
|
||||
|
||||
```python
|
||||
>>> from oxmsg import Message
|
||||
>>> from oxmsg.domain import constants as c
|
||||
|
||||
>>> msg = Message.load("message.msg")
|
||||
>>> properties = msg.properties
|
||||
>>> properties.str_prop_value(c.PID_MESSAGE_CLASS).value
|
||||
'IPM.Note'
|
||||
```
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
import itertools
|
||||
import struct
|
||||
import types
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from typing import Final, Iterator, cast
|
||||
|
||||
from oxmsg.domain import constants as c
|
||||
from oxmsg.domain import encodings
|
||||
from oxmsg.domain import model as m
|
||||
from oxmsg.domain import reference as ref
|
||||
from oxmsg.util import lazyproperty
|
||||
|
||||
|
||||
class Properties:
|
||||
"""Provides access to properties from an OXMSG storage."""
|
||||
|
||||
def __init__(self, storage: m.PropStorageT, properties_header_offset: int):
|
||||
self._storage = storage
|
||||
# -- Offset within the properties stream at which 16-byte property segments start. This
|
||||
# -- varies between storage types, e.g. root properties and attachment properties, etc.
|
||||
self._properties_header_offset = properties_header_offset
|
||||
|
||||
def __iter__(self) -> Iterator[m.PropertyT]:
|
||||
return iter(self._property_sequence)
|
||||
|
||||
def binary_prop_value(self, pid: int) -> bytes | None:
|
||||
"""Retrieve bytes of PtypBinary property identified by `pid`.
|
||||
|
||||
Returns `None` if property is not present in this collection.
|
||||
"""
|
||||
property = self._get_property(pid, c.PTYP_BINARY)
|
||||
|
||||
if property is None:
|
||||
return None
|
||||
|
||||
return cast(BinaryProperty, property).value
|
||||
|
||||
@lazyproperty
|
||||
def body_encoding(self) -> str:
|
||||
"""The encoding used for a PidTagBody or PidTagHtml property of PtypString8/Binary.
|
||||
|
||||
Must be cherry-picked because it is required before constructing the properties collection.
|
||||
|
||||
Note when these are PtypString they unconditionally use UTF-16LE.
|
||||
"""
|
||||
# -- Case 1: Use `PID_INTERNET_CODEPAGE` (0x3FDE) when present --
|
||||
internet_codepage = self._cherry_pick_int_prop(c.PID_INTERNET_CODEPAGE)
|
||||
if internet_codepage is not None:
|
||||
return encodings.encoding_from_codepage(internet_codepage.value)
|
||||
|
||||
# -- the fallbacks are the same encoding sources as string properties --
|
||||
return self._str_prop_encoding
|
||||
|
||||
def date_prop_value(self, pid: int) -> dt.datetime | None:
|
||||
"""Read datetime property value from the properties stream.
|
||||
|
||||
- Microseconds are truncated.
|
||||
- Returns `None` when no `pid` property is present in properties stream.
|
||||
"""
|
||||
property = self._properties_mapping.get(pid)
|
||||
|
||||
if property is None:
|
||||
return None
|
||||
|
||||
return cast(TimeProperty, property).value
|
||||
|
||||
def int_prop_value(self, pid: int) -> int | None:
|
||||
"""Retrieve int value of PtypInteger32 property identified by `pid`.
|
||||
|
||||
Returns `None` if no `pid` property is present in this collection.
|
||||
"""
|
||||
property = self._properties_mapping.get(pid)
|
||||
|
||||
if property is None:
|
||||
return None
|
||||
|
||||
return cast(Int32Property, property).value
|
||||
|
||||
def str_prop_value(self, pid: int) -> str | None:
|
||||
"""Retrieve str value of PtypString or PtypString8 property identified by `pid`.
|
||||
|
||||
Returns the empty str if property is not present in this collection.
|
||||
"""
|
||||
property = self._get_property(pid, (c.PTYP_STRING, c.PTYP_STRING8))
|
||||
|
||||
if property is None:
|
||||
return None
|
||||
|
||||
return cast(StringProperty, property).value
|
||||
|
||||
@lazyproperty
|
||||
def string_props_are_unicode(self) -> bool: # pragma: no cover
|
||||
"""True indicates PtypString properties in this message are encoded "utf-16-le"."""
|
||||
store_support_mask = self.int_prop_value(c.PID_STORE_SUPPORT_MASK)
|
||||
|
||||
if store_support_mask is None:
|
||||
return False
|
||||
|
||||
return bool(store_support_mask & m.STORE_UNICODE_OK)
|
||||
|
||||
def _cherry_pick_int_prop(self, pid: int) -> Int32Property | None:
|
||||
"""Get an Int32 property without triggering broader property load.
|
||||
|
||||
Used to solve chicken-and-egg problem of determining encoding required by string
|
||||
properties before atomically loading all properties.
|
||||
"""
|
||||
PID = struct.Struct("<2xH")
|
||||
for segment in self._prop_segment_sequence:
|
||||
pid_ = PID.unpack(segment[:4])[0]
|
||||
if pid_ == pid:
|
||||
return Int32Property(segment)
|
||||
return None
|
||||
|
||||
def _get_property(self, pid: int, ptyps: int | tuple[int, ...]) -> m.PropertyT | None:
|
||||
"""Retrieve the first property with `pid` and one of `ptyps`.
|
||||
|
||||
The general expectation is that at most one property with `pid` and one of `ptyps` will be
|
||||
present in the collection. In the unusual case there could be more than one this method
|
||||
may need to be called once for each possible type to get them all or in a particular order
|
||||
of preference.
|
||||
"""
|
||||
acceptable_ptyps = (ptyps,) if isinstance(ptyps, int) else ptyps
|
||||
candidate_props = self._properties_by_pid[pid]
|
||||
for p in candidate_props:
|
||||
if p.ptyp in acceptable_ptyps:
|
||||
return p
|
||||
return None
|
||||
|
||||
@lazyproperty
|
||||
def _str_prop_encoding(self) -> str:
|
||||
"""The encoding used for non-body properties of PtypString8.
|
||||
|
||||
Must be cherry-picked because it is required before constructing the properties collection.
|
||||
|
||||
Note when PtypString properties are unconditionally encoded with UTF-16LE.
|
||||
"""
|
||||
# -- Case 1: `PID_TAG_MESSAGE_CODEPAGE` (0x3FFD) is present and specifies the int
|
||||
# -- code-page used to encode the non-Unicode string properties on the Message object.
|
||||
message_codepage = self._cherry_pick_int_prop(c.PID_MESSAGE_CODEPAGE)
|
||||
if message_codepage is not None:
|
||||
return encodings.encoding_from_codepage(message_codepage.value)
|
||||
|
||||
# - Case 2: not specified one way or another, default to "iso-8859-15" (Latin 9) --
|
||||
return "iso-8859-15"
|
||||
|
||||
@lazyproperty
|
||||
def _prop_segment_sequence(self) -> tuple[bytes, ...]:
|
||||
"""16-byte segments comprising property blocks from the attachment properties stream."""
|
||||
return tuple(
|
||||
segment
|
||||
for segment in _batched_bytes(
|
||||
self._storage.properties_stream_bytes[self._properties_header_offset :], 16
|
||||
)
|
||||
# -- drop any trailing short segment, happens sometimes --
|
||||
if len(segment) == 16
|
||||
)
|
||||
|
||||
@lazyproperty
|
||||
def _properties_by_pid(self) -> defaultdict[int, list[m.PropertyT]]:
|
||||
"""Properties in this collection grouped by property-id (PID).
|
||||
|
||||
Not sure if this solves an actual problem in practice, but it's at least
|
||||
theoretically possible that the same PID could appear twice in a property collection
|
||||
with different PTYPs.
|
||||
"""
|
||||
properties_by_pid: defaultdict[int, list[m.PropertyT]] = defaultdict(list)
|
||||
for p in self._property_sequence:
|
||||
properties_by_pid[p.pid].append(p)
|
||||
return properties_by_pid
|
||||
|
||||
@lazyproperty
|
||||
def _properties_mapping(self) -> types.MappingProxyType[int, m.PropertyT]:
|
||||
"""The property objects in this collection keyed by pid."""
|
||||
return types.MappingProxyType({p.pid: p for p in self._property_sequence})
|
||||
|
||||
@lazyproperty
|
||||
def _property_sequence(self) -> tuple[m.PropertyT, ...]:
|
||||
"""Property object for each property in this collection.
|
||||
|
||||
Properties are in property-id (PID) order.
|
||||
"""
|
||||
PID = struct.Struct("<2xH")
|
||||
segments = sorted(self._prop_segment_sequence, key=lambda x: PID.unpack(x[:4])[0])
|
||||
return tuple(
|
||||
BaseProperty.factory(
|
||||
segment=segment,
|
||||
storage=self._storage,
|
||||
str_prop_encoding=self._str_prop_encoding,
|
||||
body_encoding=self.body_encoding,
|
||||
)
|
||||
for segment in segments
|
||||
)
|
||||
|
||||
|
||||
class BaseProperty:
|
||||
"""Base class for properties, providing common behaviors."""
|
||||
|
||||
PID: Final[struct.Struct] = struct.Struct("<2xH")
|
||||
PTYP: Final[struct.Struct] = struct.Struct("<H")
|
||||
|
||||
def __init__(self, segment: bytes):
|
||||
self._segment = segment
|
||||
|
||||
@classmethod
|
||||
def factory(
|
||||
cls, segment: bytes, storage: m.PropStorageT, str_prop_encoding: str, body_encoding: str
|
||||
) -> m.PropertyT:
|
||||
"""Construct a property object of the appropriate sub-type for `segment`."""
|
||||
ptyp = cls.PTYP.unpack(segment[:2])[0]
|
||||
|
||||
if ptyp == c.PTYP_BINARY:
|
||||
return BinaryProperty(segment, storage)
|
||||
|
||||
if ptyp == c.PTYP_BOOLEAN:
|
||||
return BooleanProperty(segment)
|
||||
|
||||
if ptyp == c.PTYP_FLOATING_64:
|
||||
return Float64Property(segment)
|
||||
|
||||
if ptyp == c.PTYP_GUID:
|
||||
return GuidProperty(segment, storage)
|
||||
|
||||
if ptyp == c.PTYP_INTEGER_16:
|
||||
return Int16Property(segment)
|
||||
|
||||
if ptyp == c.PTYP_INTEGER_32:
|
||||
return Int32Property(segment)
|
||||
|
||||
if ptyp == c.PTYP_STRING:
|
||||
return StringProperty(segment, storage)
|
||||
|
||||
if ptyp == c.PTYP_STRING8:
|
||||
return String8Property(
|
||||
segment=segment,
|
||||
storage=storage,
|
||||
str_prop_encoding=str_prop_encoding,
|
||||
body_encoding=body_encoding,
|
||||
)
|
||||
|
||||
if ptyp == c.PTYP_TIME:
|
||||
return TimeProperty(segment)
|
||||
|
||||
# -- default to Int32 --
|
||||
return Int32Property(segment)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""The Microsft name for this property, like "PidTagMessageClass"."""
|
||||
prop_desc = ref.property_descriptors.get(self.pid)
|
||||
return prop_desc.ms_name if prop_desc is not None else "not recorded in model"
|
||||
|
||||
@property
|
||||
def pid(self) -> int:
|
||||
"""The property-id (PID) for this property, like 0x3701 for attachment bytes."""
|
||||
return self.PID.unpack(self._segment[:4])[0]
|
||||
|
||||
@property
|
||||
def ptyp(self) -> int:
|
||||
"""The property-type (PTYP) for this property, like 0x0102 for PtypBinary."""
|
||||
return self.PTYP.unpack(self._segment[:2])[0]
|
||||
|
||||
@property
|
||||
def ptyp_name(self) -> str:
|
||||
"""The Microsft name for the type of this property, like "PtypString"."""
|
||||
prop_type_desc = ref.property_type_descriptors.get(self.ptyp)
|
||||
return (
|
||||
prop_type_desc.ms_name if prop_type_desc else f"{self.ptyp:04X} not recorded in model"
|
||||
)
|
||||
|
||||
@lazyproperty
|
||||
def _payload(self) -> bytes:
|
||||
"""The latter 8 bytes of the property segment, where the property value is stored."""
|
||||
return self._segment[8:]
|
||||
|
||||
|
||||
class BinaryProperty(BaseProperty):
|
||||
"""Property for PtypBinary OLE properties."""
|
||||
|
||||
def __init__(self, segment: bytes, storage: m.PropStorageT):
|
||||
super().__init__(segment)
|
||||
self._storage = storage
|
||||
|
||||
@lazyproperty
|
||||
def value(self) -> bytes:
|
||||
"""The bytes of this binary property."""
|
||||
return self._storage.property_stream_bytes(self.pid, self.ptyp)
|
||||
|
||||
|
||||
class BooleanProperty(BaseProperty):
|
||||
"""Property for PtypBoolean OLE properties."""
|
||||
|
||||
SIGNED_CHAR: Final[struct.Struct] = struct.Struct("<b")
|
||||
|
||||
@lazyproperty
|
||||
def value(self) -> bool:
|
||||
"""The boolean value of this property."""
|
||||
return self.SIGNED_CHAR.unpack(self._payload[:1])[0] != 0
|
||||
|
||||
|
||||
class Float64Property(BaseProperty):
|
||||
"""Property for PtypFloating64 OLE properties."""
|
||||
|
||||
FLOAT64: Final[struct.Struct] = struct.Struct("<d")
|
||||
|
||||
@lazyproperty
|
||||
def value(self) -> float:
|
||||
"""The 64-bit floating-point value of this property."""
|
||||
return self.FLOAT64.unpack(self._payload)[0]
|
||||
|
||||
|
||||
class GuidProperty(BaseProperty):
|
||||
"""Property for PtypGuid OLE properties."""
|
||||
|
||||
GUID: Final[struct.Struct] = struct.Struct("<IHH8s")
|
||||
|
||||
def __init__(self, segment: bytes, storage: m.PropStorageT):
|
||||
super().__init__(segment)
|
||||
self._storage = storage
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Hex str representation of this UUID like '9d947746-9662-40a8-a526-abd4faec9737'."""
|
||||
return str(self.value)
|
||||
|
||||
@lazyproperty
|
||||
def value(self) -> uuid.UUID:
|
||||
"""The value of this property as a uuid.UUID object.
|
||||
|
||||
The `str` value of this object is the standard-form string for the UUID, like:
|
||||
'9d947746-9662-40a8-a526-abd4faec9737'.
|
||||
"""
|
||||
# -- In the OXMSG format, a GUID (aka. UUID) is stored as four distinct fields, each in
|
||||
# -- little-endian form. Luckily Python's uuid built-in can parse this directly.
|
||||
return uuid.UUID(bytes_le=self._storage.property_stream_bytes(self.pid, self.ptyp)[:16])
|
||||
|
||||
|
||||
class Int16Property(BaseProperty):
|
||||
"""Property for PtypInteger16 OLE properties."""
|
||||
|
||||
INT16: Final[struct.Struct] = struct.Struct("<H")
|
||||
|
||||
@lazyproperty
|
||||
def value(self) -> int:
|
||||
"""The integer value of this property."""
|
||||
return self.INT16.unpack(self._payload[:2])[0]
|
||||
|
||||
|
||||
class Int32Property(BaseProperty):
|
||||
"""Property for PtypInteger32 OLE properties."""
|
||||
|
||||
INT32: Final[struct.Struct] = struct.Struct("<I")
|
||||
|
||||
@lazyproperty
|
||||
def value(self) -> int:
|
||||
"""The integer value of this property."""
|
||||
return self.INT32.unpack(self._payload[:4])[0]
|
||||
|
||||
|
||||
class StringProperty(BaseProperty):
|
||||
"""Property for PtypString OLE properties."""
|
||||
|
||||
def __init__(self, segment: bytes, storage: m.PropStorageT):
|
||||
super().__init__(segment)
|
||||
self._storage = storage
|
||||
|
||||
@lazyproperty
|
||||
def value(self) -> str:
|
||||
"""The decoded str from this string property."""
|
||||
return self._storage.property_stream_bytes(self.pid, self.ptyp).decode("utf-16-le")
|
||||
|
||||
|
||||
class String8Property(BaseProperty):
|
||||
"""Property for PtypString8 (8-bit characters, not Unicode) OLE properties."""
|
||||
|
||||
def __init__(
|
||||
self, segment: bytes, storage: m.PropStorageT, str_prop_encoding: str, body_encoding: str
|
||||
):
|
||||
super().__init__(segment)
|
||||
self._storage = storage
|
||||
self._str_prop_encoding = str_prop_encoding
|
||||
self._body_encoding = body_encoding
|
||||
|
||||
@lazyproperty
|
||||
def value(self) -> str:
|
||||
"""The encoded bytes of this string property.
|
||||
|
||||
The caller is responsible for determining the encoding and applying it to get a str value.
|
||||
"""
|
||||
return self._storage.property_stream_bytes(self.pid, self.ptyp).decode(
|
||||
self._body_encoding if self.pid == c.PID_BODY else self._str_prop_encoding
|
||||
)
|
||||
|
||||
|
||||
class TimeProperty(BaseProperty):
|
||||
"""Property for PtypTime OLE properties."""
|
||||
|
||||
TIME: Final[struct.Struct] = struct.Struct("<Q")
|
||||
|
||||
@lazyproperty
|
||||
def value(self) -> dt.datetime:
|
||||
"""The value of this property as a timezone-aware `datetime`."""
|
||||
hundred_nanosecond_intervals_since_epoch = self.TIME.unpack(self._payload)[0]
|
||||
epoch = dt.datetime(1601, 1, 1, tzinfo=dt.timezone.utc)
|
||||
seconds_since_epoch = hundred_nanosecond_intervals_since_epoch // 1e7
|
||||
return epoch + dt.timedelta(seconds=seconds_since_epoch)
|
||||
|
||||
|
||||
def _batched_bytes(block: bytes, n: int) -> Iterator[bytes]:
|
||||
"""Batch bytes from `block` into segments of `n` bytes each.
|
||||
|
||||
Last batch is shorter than `n` when `block` is not evenly divisible by `n`.
|
||||
"""
|
||||
if n < 1: # pragma: no cover
|
||||
raise ValueError("n must be at least one")
|
||||
iter_bytes = iter(block)
|
||||
while batch := bytes(itertools.islice(iter_bytes, n)):
|
||||
yield batch
|
||||
@@ -0,0 +1,48 @@
|
||||
"""The `Recipient` object provides access to properties for a recipient of the message.
|
||||
|
||||
This object is accessed using the `Message.recipients` property:
|
||||
```python
|
||||
>>> from oxmsg import Message
|
||||
|
||||
>>> msg = Message.load("message.msg")
|
||||
>>> recipient = msg.recipients[0]
|
||||
>>> recipient.name
|
||||
'Jane Doe'
|
||||
```
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from oxmsg.domain import constants as c
|
||||
from oxmsg.domain import model as m
|
||||
from oxmsg.properties import Properties
|
||||
from oxmsg.util import lazyproperty
|
||||
|
||||
|
||||
class Recipient:
|
||||
"""A recipient of an Outlook email message."""
|
||||
|
||||
def __init__(self, storage: m.StorageT):
|
||||
self._storage = storage
|
||||
|
||||
@lazyproperty
|
||||
def email_address(self) -> str:
|
||||
"""The email address of this recipient."""
|
||||
props = self.properties
|
||||
# -- Preferred source is the SMTP address property, fall back to PidTagEmailAddress which
|
||||
# -- can theoretically be X.400 or Exchange email format.
|
||||
return (
|
||||
props.str_prop_value(c.PID_SMTP_ADDRESS)
|
||||
or props.str_prop_value(c.PID_EMAIL_ADDRESS)
|
||||
or ""
|
||||
)
|
||||
|
||||
@lazyproperty
|
||||
def name(self) -> str:
|
||||
"""The name of this recipient."""
|
||||
return self.properties.str_prop_value(c.PID_DISPLAY_NAME) or ""
|
||||
|
||||
@lazyproperty
|
||||
def properties(self) -> Properties:
|
||||
"""Provides access to the properties of this OXMSG object."""
|
||||
return Properties(self._storage, properties_header_offset=m.RECIP_HDR_OFFSET)
|
||||
@@ -0,0 +1,111 @@
|
||||
"""A "folder" in Microsoft Compound File Binary (CFB) format.
|
||||
|
||||
The CFB/OLE file format encloses a filesystem, to a first appoximation, much like a Zip
|
||||
archive does. In this format, a "storage" corresponds to a directory and a "stream"
|
||||
corresponds to a file. A storage can contain both streams and other storages.
|
||||
|
||||
Each MSG file has a "root" storage, represented in this package by `MessageStorage`.
|
||||
Each attachment and recipient has their own storage in the root, with a pre-defined
|
||||
name, and there are other top-level objects than can appear in a MSG file that get their
|
||||
own storage.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses as dc
|
||||
from typing import Iterator, Mapping
|
||||
|
||||
from olefile import OleFileIO
|
||||
from olefile.olefile import STGTY_STORAGE, STGTY_STREAM, OleDirectoryEntry
|
||||
|
||||
from oxmsg.util import lazyproperty
|
||||
|
||||
|
||||
@dc.dataclass
|
||||
class Storage:
|
||||
"""Container for streams and sub-storages."""
|
||||
|
||||
path: str
|
||||
streams: tuple[Stream, ...]
|
||||
storages: tuple[Storage, ...]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"Storage(path={repr(self.path)}, {len(self.streams)} streams,"
|
||||
f" {len(self.storages)} storages)"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_ole(
|
||||
cls, ole: OleFileIO, node: OleDirectoryEntry | None = None, prefix: str = ""
|
||||
) -> Storage:
|
||||
"""Return a Storage loaded from `node` and containing its streams and sub-storages."""
|
||||
# -- initial call is `.from_ole(ole)`; other args are only specified on recursion --
|
||||
node = node if node else ole.root
|
||||
|
||||
def _iter_streams(ole: OleFileIO, node: OleDirectoryEntry, prefix: str) -> Iterator[Stream]:
|
||||
"""Generate `Stream` object for each stream in `nodes`."""
|
||||
for stream_node in (k for k in node.kids if k.entry_type == STGTY_STREAM):
|
||||
path = f"{prefix}/{stream_node.name}" if prefix else stream_node.name
|
||||
with ole.openstream(path) as f:
|
||||
bytes_ = f.read()
|
||||
yield Stream(path, bytes_)
|
||||
|
||||
streams = tuple(_iter_streams(ole, node, prefix))
|
||||
sub_storages = tuple(
|
||||
cls.from_ole(ole, k, f"{prefix}/{k.name}" if prefix else k.name)
|
||||
for k in node.kids
|
||||
if k.entry_type == STGTY_STORAGE
|
||||
)
|
||||
return cls(path=prefix, streams=streams, storages=sub_storages)
|
||||
|
||||
def iter_attachment_storages(self) -> Iterator[Storage]:
|
||||
"""Generate storage object specific to each attachment in this message."""
|
||||
for s in self.storages:
|
||||
if s.name.startswith("__attach_version1.0_#"):
|
||||
yield s
|
||||
|
||||
def iter_recipient_storages(self) -> Iterator[Storage]:
|
||||
"""Generate storage object specific to each recipent in this message."""
|
||||
for s in self.storages:
|
||||
if s.name.startswith("__recip_version1.0_#"):
|
||||
yield s
|
||||
|
||||
@lazyproperty
|
||||
def name(self) -> str:
|
||||
"""The "directory-name" of this storage, with no path-prefix."""
|
||||
return self.path.split("/")[-1]
|
||||
|
||||
@lazyproperty
|
||||
def properties_stream_bytes(self) -> bytes:
|
||||
"""The bytes of the one-and-only-one properties stream in this storage."""
|
||||
# -- every storage mush have a properties stream --
|
||||
return self._streams_by_name["__properties_version1.0"].bytes_
|
||||
|
||||
def property_stream_bytes(self, pid: int, ptyp: int) -> bytes:
|
||||
"""Read variable-length property bytes from the stream it's stored in."""
|
||||
# -- This method should not be called unless there is an entry for this property in the
|
||||
# -- properties stream. If the property exists but its stream does not, that's an
|
||||
# -- exception, not an expected occurence.
|
||||
return self._streams_by_name[f"__substg1.0_{pid:04X}{ptyp:04X}"].bytes_
|
||||
|
||||
@lazyproperty
|
||||
def _streams_by_name(self) -> Mapping[str, Stream]:
|
||||
"""dict semantics on streams of this storage."""
|
||||
return {s.name: s for s in self.streams}
|
||||
|
||||
|
||||
@dc.dataclass
|
||||
class Stream:
|
||||
"""Bytes of a property of a top-level object in an OXMSG file."""
|
||||
|
||||
path: str
|
||||
bytes_: bytes
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Stream(path={repr(self.path)}, {len(self.bytes_):,} bytes)"
|
||||
|
||||
@lazyproperty
|
||||
def name(self) -> str:
|
||||
"""The "filename" of this stream, with no path-prefix."""
|
||||
return self.path.split("/")[-1]
|
||||
117
backend_service/venv/lib/python3.13/site-packages/oxmsg/util.py
Normal file
117
backend_service/venv/lib/python3.13/site-packages/oxmsg/util.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""Stand-alone utility functions independent of OXMSG domain."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
from typing import Any, Callable, Generic, TypeVar, cast
|
||||
|
||||
_T = TypeVar("_T", covariant=True)
|
||||
|
||||
|
||||
class lazyproperty(Generic[_T]):
|
||||
"""Decorator like @property, but evaluated only on first access.
|
||||
|
||||
Like @property, this can only be used to decorate methods having only a `self` parameter, and
|
||||
is accessed like an attribute on an instance, i.e. trailing parentheses are not used. Unlike
|
||||
@property, the decorated method is only evaluated on first access; the resulting value is
|
||||
cached and that same value returned on second and later access without re-evaluation of the
|
||||
method.
|
||||
|
||||
Like @property, this class produces a *data descriptor* object, which is stored in the __dict__
|
||||
of the *class* under the name of the decorated method ('fget' nominally). The cached value is
|
||||
stored in the __dict__ of the *instance* under that same name.
|
||||
|
||||
Because it is a data descriptor (as opposed to a *non-data descriptor*), its `__get__()` method
|
||||
is executed on each access of the decorated attribute; the __dict__ item of the same name is
|
||||
"shadowed" by the descriptor.
|
||||
|
||||
While this may represent a performance improvement over a property, its greater benefit may be
|
||||
its other characteristics. One common use is to construct collaborator objects, removing that
|
||||
"real work" from the constructor, while still only executing once. It also de-couples client
|
||||
code from any sequencing considerations; if it's accessed from more than one location, it's
|
||||
assured it will be ready whenever needed.
|
||||
|
||||
Loosely based on: https://stackoverflow.com/a/6849299/1902513.
|
||||
|
||||
A lazyproperty is read-only. There is no counterpart to the optional "setter" (or deleter)
|
||||
behavior of an @property. This is critically important to maintaining its immutability and
|
||||
idempotence guarantees. Attempting to assign to a lazyproperty raises AttributeError
|
||||
unconditionally.
|
||||
|
||||
The parameter names in the methods below correspond to this usage example::
|
||||
|
||||
class Obj(object)
|
||||
|
||||
@lazyproperty
|
||||
def fget(self):
|
||||
return 'some result'
|
||||
|
||||
obj = Obj()
|
||||
|
||||
Not suitable for wrapping a function (as opposed to a method) because it is not callable.
|
||||
"""
|
||||
|
||||
def __init__(self, fget: Callable[..., _T]) -> None:
|
||||
"""*fget* is the decorated method (a "getter" function).
|
||||
|
||||
A lazyproperty is read-only, so there is only an *fget* function (a regular
|
||||
@property can also have an fset and fdel function). This name was chosen for
|
||||
consistency with Python's `property` class which uses this name for the
|
||||
corresponding parameter.
|
||||
"""
|
||||
# --- maintain a reference to the wrapped getter method
|
||||
self._fget = fget
|
||||
# --- and store the name of that decorated method
|
||||
self._name = fget.__name__
|
||||
# --- adopt fget's __name__, __doc__, and other attributes
|
||||
functools.update_wrapper(self, fget) # pyright: ignore
|
||||
|
||||
def __get__(self, obj: Any, type: Any = None) -> _T:
|
||||
"""Called on each access of 'fget' attribute on class or instance.
|
||||
|
||||
*self* is this instance of a lazyproperty descriptor "wrapping" the property
|
||||
method it decorates (`fget`, nominally).
|
||||
|
||||
*obj* is the "host" object instance when the attribute is accessed from an
|
||||
object instance, e.g. `obj = Obj(); obj.fget`. *obj* is None when accessed on
|
||||
the class, e.g. `Obj.fget`.
|
||||
|
||||
*type* is the class hosting the decorated getter method (`fget`) on both class
|
||||
and instance attribute access.
|
||||
"""
|
||||
# --- when accessed on class, e.g. Obj.fget, just return this descriptor
|
||||
# --- instance (patched above to look like fget).
|
||||
if obj is None: # pragma: no cover
|
||||
return self # type: ignore
|
||||
|
||||
# --- when accessed on instance, start by checking instance __dict__ for
|
||||
# --- item with key matching the wrapped function's name
|
||||
value = obj.__dict__.get(self._name)
|
||||
if value is None:
|
||||
# --- on first access, the __dict__ item will be absent. Evaluate fget()
|
||||
# --- and store that value in the (otherwise unused) host-object
|
||||
# --- __dict__ value of same name ('fget' nominally)
|
||||
value = self._fget(obj)
|
||||
obj.__dict__[self._name] = value
|
||||
return cast(_T, value)
|
||||
|
||||
def __set__(self, obj: Any, value: Any) -> None: # pragma: no cover
|
||||
"""Raises unconditionally, to preserve read-only behavior.
|
||||
|
||||
This decorator is intended to implement immutable (and idempotent) object
|
||||
attributes. For that reason, assignment to this property must be explicitly
|
||||
prevented.
|
||||
|
||||
If this __set__ method was not present, this descriptor would become a
|
||||
*non-data descriptor*. That would be nice because the cached value would be
|
||||
accessed directly once set (__dict__ attrs have precedence over non-data
|
||||
descriptors on instance attribute lookup). The problem is, there would be
|
||||
nothing to stop assignment to the cached value, which would overwrite the result
|
||||
of `fget()` and break both the immutability and idempotence guarantees of this
|
||||
decorator.
|
||||
|
||||
The performance with this __set__() method in place was roughly 0.4 usec per
|
||||
access when measured on a 2.8GHz development machine; so quite snappy and
|
||||
probably not a rich target for optimization efforts.
|
||||
"""
|
||||
raise AttributeError("can't set attribute")
|
||||
Reference in New Issue
Block a user