Skip to main content

Pure Python Outlook .msg file builder — no COM, no Outlook required

Project description

msgforge

Pure Python Outlook .msg file builder. No COM, no Outlook installation, no native dependencies. Works on Linux.

Implements the MS-CFB and MS-OXMSG specs from scratch. Output is validated against strict OLE parsers and round-tripped through extract-msg in CI, and has been tested with Outlook on Windows — but if you hit an edge case, please open an issue.

Install

pip install msgforge

Usage

from msgforge import Message

# Simple text email
msg = Message(
    subject="Hello",
    text_body="Hi there!",
    to=[("alice@example.com", "Alice")],
)
msg.save("hello.msg")

# HTML email with table, attachments, multiple recipients
msg = Message(
    subject="Weekly Report",
    html_body="""
    <p>Hi team,</p>
    <p>Please find the <b>weekly report</b> below:</p>
    <table border="1" style="border-collapse: collapse;">
        <tr><th style="padding: 4px 8px; background: #f0f0f0;">Region</th>
            <th style="padding: 4px 8px; background: #f0f0f0;">Revenue</th></tr>
        <tr><td style="padding: 4px 8px;">EMEA</td>
            <td style="padding: 4px 8px;">$1.2M</td></tr>
        <tr><td style="padding: 4px 8px;">APAC</td>
            <td style="padding: 4px 8px;">$800K</td></tr>
    </table>
    <p>Best regards</p>
    """,
    to=[("alice@example.com", "Alice"), ("bob@example.com", "Bob")],
    cc=[("carol@example.com", "Carol")],
)
msg.attach("report.xlsx")
msg.save("weekly_report.msg")

# Inline images — reference with cid: in HTML, attach with content_id
msg = Message(
    subject="Product Update",
    html_body="""
    <p>Hi team,</p>
    <p>Here's the new logo and sales chart:</p>
    <img src="cid:logo" width="200">
    <br>
    <img src="cid:chart" width="400">
    <p>Best regards</p>
    """,
    to=[("alice@example.com", "Alice")],
)
msg.attach("logo.png", content_id="logo")
msg.attach("chart.png", content_id="chart")
msg.attach("report.xlsx")  # regular attachment — shows in attachment bar
msg.save("product_update.msg")

# High importance
msg = Message(
    subject="Action Required",
    text_body="Please review ASAP.",
    to=[("bob@example.com", "Bob")],
    importance="high",
)
msg.save("urgent.msg")

# Sent/received message with a From address (e.g. for archival or export)
from datetime import datetime

msg = Message(
    subject="Meeting notes",
    text_body="Notes attached.",
    sender=("boss@corp.example", "The Boss"),
    to=[("alice@example.com", "Alice")],
    sent=datetime(2026, 7, 1, 12, 0),  # or sent=True for "now"
)
msg.save("archived.msg")

Features

  • HTML body — full CSS, tables, bold/italic rendering in Outlook (via encapsulated HTML in RTF); the raw HTML is also stored in PidTagHtml for non-Outlook readers
  • Plain text body — fallback when no HTML provided
  • Inline images — embed images in HTML via cid: references (hidden from attachment bar)
  • Sender — set the From address for archival/export use cases
  • Sent or draft — messages are unsent drafts by default (open in compose mode); pass sent= to mark them sent/received with a timestamp
  • Recipients — TO, CC, BCC with display names
  • File attachments — appear in Outlook's attachment bar; large attachments supported (DIFAT)
  • Importance"low", "normal", or "high" (shown in Outlook's priority column)
  • Unicode support — full Unicode in HTML bodies (smart quotes, CJK, emoji) via proper RTF Unicode escapes
  • Deterministic output — the same message always produces the same bytes
  • Typed — accurate type hints, ships py.typed
  • Pure Python — only dependency is compressed-rtf
  • Cross-platform — works on Linux, macOS, Windows

API

Message(...)

Message(
    subject="...",           # Email subject
    html_body="<p>...</p>",  # HTML body (rendered in Outlook with full formatting)
    text_body="...",         # Plain text body (auto-generated from HTML if omitted)
    to=[("email", "Name")],  # TO recipients
    cc=[...],                # CC recipients
    bcc=[...],               # BCC recipients
    importance="normal",     # "low", "normal", or "high"
    sender=("email", "Name"),# From address (optional)
    sent=False,              # False = unsent draft; True or a datetime = sent/received
)

Recipients (and sender) can be ("email", "Name") tuples or just "email" strings.

.attach(path, filename=None, content_id=None)

Attach a file from disk. Returns self for chaining. Pass content_id to embed as an inline image (<img src="cid:content_id">).

.attach_bytes(filename, data, mime_type=None, content_id=None)

Attach a file from bytes. MIME type auto-detected from extension. Pass content_id for inline images.

.save(path)

Write the .msg file to disk.

.as_bytes()

Return the .msg file content as bytes (for web responses, etc).

Limitations

  • No read support — this package only creates .msg files, it cannot read or parse them (use extract-msg for that)
  • No RTF body authoring — rich text is generated from HTML only; direct RTF input is not supported
  • No digital signatures or encryption — messages are unsigned and unencrypted
  • No calendar/contact/task items — only email messages (IPM.Note) are supported
  • Recipient resolution — recipients show display names but may not resolve to Exchange contacts until Outlook processes them
  • No streaming — the entire message is built in memory (large attachments work, but need RAM proportional to their size)
  • <pre> blocks — whitespace inside HTML is collapsed per normal HTML semantics, so preformatted text is not preserved exactly

How it works

Implements two Microsoft specs from scratch:

  1. MS-CFB (Compound File Binary Format) — the OLE structured storage container
  2. MS-OXMSG (Outlook Item File Format) — MAPI properties, recipients, attachments

HTML bodies use the \fromhtml1 encapsulated HTML format, the same format Outlook uses internally. This gives full HTML/CSS rendering including tables, styling, etc.

Key implementation details discovered through testing:

  • Directory entry names must be sorted by length first, then case-insensitive (MS-CFB spec)
  • PT_UNICODE property sizes must include +2 bytes for the null terminator
  • HTML bodies require encapsulated HTML in RTF (\fromhtml1) — raw PR_HTML is not rendered by Outlook (it is still written for non-Outlook consumers)
  • Inline images use base64 data URIs embedded directly in the HTML before RTF encapsulation — Outlook's \fromhtml1 renderer cannot resolve cid: references against the attachment table
  • Files larger than ~7 MB need DIFAT sectors: the CFB header only holds 109 FAT sector pointers
  • RTF readers ignore raw newlines, so whitespace in HTML must be normalized to explicit spaces before encapsulation

Running tests

pip install .[dev]
pytest tests/

Built with AI

This package was built collaboratively with Claude Code (Claude Opus 4.6). The OLE compound file writer, MAPI property encoding, and encapsulated HTML format were implemented from the MS-CFB and MS-OXMSG specifications through iterative development and testing.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

msgforge-1.0.0.tar.gz (20.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

msgforge-1.0.0-py3-none-any.whl (15.9 kB view details)

Uploaded Python 3

File details

Details for the file msgforge-1.0.0.tar.gz.

File metadata

  • Download URL: msgforge-1.0.0.tar.gz
  • Upload date:
  • Size: 20.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.2

File hashes

Hashes for msgforge-1.0.0.tar.gz
Algorithm Hash digest
SHA256 bfb6058d4de1d70c58940adf90216f5c587a98118e2be1bcf7341dcab29bb5ac
MD5 2534fc22fa5cbd8e2d1e1d6ac813273a
BLAKE2b-256 e1e6f56029ffacebb7dbf953cb7af8b6b4025967511d01ab704b9ea8d8fedd09

See more details on using hashes here.

File details

Details for the file msgforge-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: msgforge-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 15.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.2

File hashes

Hashes for msgforge-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8b97d5bf8ea9109909aa54bfc92420656c875991b2a1912534d27535696ed3ff
MD5 aaf8817b87f4a7ee5f5a719e7fce756a
BLAKE2b-256 c0bdc186cb760a3ecf1194f76fcfcd17fb5c75b83cfb667839cbd08e75e0de29

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page