fix: resolve Outlook 365 sender address and convert HTML-only message bodies - #2245
fix: resolve Outlook 365 sender address and convert HTML-only message bodies#2245gaurav0107 wants to merge 2 commits into
Conversation
OutlookMsgConverter read the sender only from PR_SENDER_EMAIL_ADDRESS (0x0C1F). That property holds an email address only when PR_SENDER_ADDRTYPE (0x0C1E) is "SMTP"; messages sent through Exchange set it to "EX" and store a Distinguished Name there instead, so From rendered as "/O=EXCHANGELABS/OU=.../CN=RECIPIENTS/CN=...". Resolve the sender from PR_SENDER_SMTP_ADDRESS / PR_SENT_REPRESENTING_SMTP_ADDRESS first, use the legacy properties only when their address type is not the DN form, and keep the DN as a last resort so the header is never dropped. The body was likewise read only from PR_BODY (0x1000), which HTML-only messages leave empty, so the whole message silently vanished from "## Content". Fall back to PR_HTML (0x1013) and convert it with the existing HtmlConverter, handling both the binary and string variants of the property and detecting the byte stream's charset rather than assuming UTF-8. Binary streams are read through a new _read_stream helper because _get_stream_data decodes UTF-16 first, which rarely fails on arbitrary bytes and would return mojibake instead of raising. Fixes microsoft#1188
|
@gaurav0107 please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.
Contributor License AgreementContribution License AgreementThis Contribution License Agreement (“Agreement”) is agreed to by the party signing below (“You”),
|
… class docstring An empty 0x1013 binary property must not mask a populated string variant of the same property.
ericsondea-collab
left a comment
There was a problem hiding this comment.
Summary
.msgfiles saved from Outlook 365 / Exchange lose two things on conversion. Both are reproduced againstmainbelow.1.
Fromis an Exchange DN, not an address. The converter read the sender only fromPR_SENDER_EMAIL_ADDRESS(0x0C1F). That property holds an email address only whenPR_SENDER_ADDRTYPE(0x0C1E) isSMTP; anything routed through Exchange sets the address type toEXand stores a Distinguished Name there instead, so the header rendered as:**From:** /O=EXCHANGELABS/OU=EXCHANGE ADMINISTRATIVE GROUP (FYDIBOHF23SPDLT)/CN=RECIPIENTS/CN=ab12cd34The sender is now resolved in MAPI-correct order —
PR_SENDER_SMTP_ADDRESS(0x5D01), thenPR_SENT_REPRESENTING_SMTP_ADDRESS(0x5D02), then the legacy properties but only when their address type is not the DN form. If a file genuinely carries nothing else, the DN is still emitted, so no message loses itsFromheader.2. HTML-only messages render an empty
## Content. The body was read only fromPR_BODY(0x1000), which Outlook on the web leaves empty for HTML-only mail — the entire message body was silently dropped. It now falls back toPR_HTML(0x1013) and converts it with theHtmlConverterthis repo already ships, the same wayPptxConverter/XlsxConverter/EpubConverterdelegate. Both the binary (0102) and string (001F) variants of the property are handled, and the byte stream's charset is detected withcharset_normalizer— already a core dependency, used the same way in_csv_converter.pyand_plain_text_converter.py— rather than assuming UTF-8.Binary streams are read through a new
_read_streamhelper rather than_get_stream_data, because that function decodes UTF-16-LE first and arbitrary even-length bytes almost never fail that decode:PR_HTMLwould come back as mojibake instead of raising._get_stream_datakeeps its exact previous signature and decoding behavior.No new dependency, and plain-text messages take the identical path they took before — the HTML fallback is reached only when
PR_BODYis absent or empty.Before / after on the message shape described in the issue:
**From:** /O=EXCHANGELABS/OU=.../CN=RECIPIENTS/CN=ab12cd34 -> **From:** sender@example.com ## Content -> ## Content (empty) Hello **world**Testing
New
packages/markitdown/tests/test_outlook_msg.py, 11 cases: SMTP-over-DN preference, thesent representingvariants, the plain-SMTPcase (unchanged behavior), the last-resort DN fallback, HTML-only body conversion, the string-variant property, non-UTF-8 (cp1252) HTML bytes, absent HTML, plain-text-wins-over-HTML, and an end-to-endconvert()of the reported message shape.A
.msgfile is an OLE2 compound file and nothing in the dependency set can write that container, so a synthetic binary fixture is not possible here the waytest_svg_no_fallback.pptxwas. The tests instead serve the MAPI streams through a stand-in forolefile.OleFileIO, matching the stub-the-collaborator style already used intests/test_docintel_html.pyandtests/test_cu_converter.py. Happy to swap in a real fixture if a maintainer can share a sanitised Exchange.msg.pytest tests/test_outlook_msg.py 11 passed pytest tests/ (full suite) 347 passed, 3 skipped black --check . 73 files unchanged mypy --ignore-missing-imports no issuesThe existing
test_outlook_msg.msgvector is unaffected — that fixture hasPR_SENDER_ADDRTYPE == "SMTP", so**From:** test.sender@example.comstill holds.The only full-suite failure,
test_module_misc.py::test_speech_transcription, fails identically on a clean checkout ofmain: the online recogniser returns1 2 3 4for a clip the test expects to end in5. Unrelated to this change.Fixes #1188
Summary
.msgfiles saved from Outlook 365 / Exchange lose two things on conversion. Both are reproduced againstmainbelow.1.
Fromis an Exchange DN, not an address. The converter read the sender only fromPR_SENDER_EMAIL_ADDRESS(0x0C1F). That property holds an email address only whenPR_SENDER_ADDRTYPE(0x0C1E) isSMTP; anything routed through Exchange sets the address type toEXand stores a Distinguished Name there instead, so the header rendered as:The sender is now resolved in MAPI-correct order —
PR_SENDER_SMTP_ADDRESS(0x5D01), thenPR_SENT_REPRESENTING_SMTP_ADDRESS(0x5D02), then the legacy properties but only when their address type is not the DN form. If a file genuinely carries nothing else, the DN is still emitted, so no message loses itsFromheader.2. HTML-only messages render an empty
## Content. The body was read only fromPR_BODY(0x1000), which Outlook on the web leaves empty for HTML-only mail — the entire message body was silently dropped. It now falls back toPR_HTML(0x1013) and converts it with theHtmlConverterthis repo already ships, the same wayPptxConverter/XlsxConverter/EpubConverterdelegate. Both the binary (0102) and string (001F) variants of the property are handled, and the byte stream's charset is detected withcharset_normalizer— already a core dependency, used the same way in_csv_converter.pyand_plain_text_converter.py— rather than assuming UTF-8.Binary streams are read through a new
_read_streamhelper rather than_get_stream_data, because that function decodes UTF-16-LE first and arbitrary even-length bytes almost never fail that decode:PR_HTMLwould come back as mojibake instead of raising._get_stream_datakeeps its exact previous signature and decoding behavior.No new dependency, and plain-text messages take the identical path they took before — the HTML fallback is reached only when
PR_BODYis absent or empty.Before / after on the message shape described in the issue:
Testing
New
packages/markitdown/tests/test_outlook_msg.py, 11 cases: SMTP-over-DN preference, thesent representingvariants, the plain-SMTPcase (unchanged behavior), the last-resort DN fallback, HTML-only body conversion, the string-variant property, non-UTF-8 (cp1252) HTML bytes, absent HTML, plain-text-wins-over-HTML, and an end-to-endconvert()of the reported message shape.A
.msgfile is an OLE2 compound file and nothing in the dependency set can write that container, so a synthetic binary fixture is not possible here the waytest_svg_no_fallback.pptxwas. The tests instead serve the MAPI streams through a stand-in forolefile.OleFileIO, matching the stub-the-collaborator style already used intests/test_docintel_html.pyandtests/test_cu_converter.py. Happy to swap in a real fixture if a maintainer can share a sanitised Exchange.msg.The existing
test_outlook_msg.msgvector is unaffected — that fixture hasPR_SENDER_ADDRTYPE == "SMTP", so**From:** test.sender@example.comstill holds.The only full-suite failure,
test_module_misc.py::test_speech_transcription, fails identically on a clean checkout ofmain: the online recogniser returns1 2 3 4for a clip the test expects to end in5. Unrelated to this change.Fixes #1188