修改为东南天坐标系

This commit is contained in:
2026-01-20 09:49:52 +08:00
parent 9538757047
commit 333fad40ac
7201 changed files with 1030888 additions and 85410 deletions

View File

@@ -0,0 +1,8 @@
__version__ = "0.0.2"
from oxmsg.message import Message
__all__ = [
"Message",
"__version__",
]

View File

@@ -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

View 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())

View File

@@ -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

View File

@@ -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) --
}

View File

@@ -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

View File

@@ -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),
}

View File

@@ -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")

View File

@@ -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

View File

@@ -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)

View File

@@ -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]

View 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")