Skip to main content

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.

Metadata

Release files for msgforge 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for msgforge 1.0.0
File Size Uploaded
msgforge-1.0.0.tar.gz 20.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for msgforge 1.0.0
File Interpreter ABI Platform
msgforge-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 36.5 kB

Release files / msgforge-1.0.0.tar.gz

Download URL msgforge-1.0.0.tar.gz
Size 20.7 kB
Tags Source
SHA-256 checksum
How to use checksums
bfb6058d4de1d70c58940adf90216f5c587a98118e2be1bcf7341dcab29bb5ac
BLAKE2b-256 checksum
How to use checksums
e1e6f56029ffacebb7dbf953cb7af8b6b4025967511d01ab704b9ea8d8fedd09
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.2

Release files / msgforge-1.0.0-py3-none-any.whl

Download URL msgforge-1.0.0-py3-none-any.whl
Size 15.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8b97d5bf8ea9109909aa54bfc92420656c875991b2a1912534d27535696ed3ff
BLAKE2b-256 checksum
How to use checksums
c0bdc186cb760a3ecf1194f76fcfcd17fb5c75b83cfb667839cbd08e75e0de29
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.2

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page