Metadata-Version: 2.4
Name: markitdown-email-plugin
Version: 0.1.0
Summary: MarkItDown plugin that converts .eml and .msg email files to Markdown, with configurable attachment handling.
Project-URL: Homepage, https://github.com/ReeceM/markitdown-email-plugin
Project-URL: Issues, https://github.com/ReeceM/markitdown-email-plugin/issues
Author: ReeceM
License: MIT License
        
        Copyright (c) 2026 ReeceM
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: email,eml,llm,markdown,markitdown,msg,outlook
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Text Processing :: Markup :: Markdown
Requires-Python: >=3.10
Requires-Dist: markitdown>=0.1.2
Requires-Dist: olefile>=0.47
Description-Content-Type: text/markdown

# markitdown-email-plugin

[![CI](https://github.com/reecemiao/markitdown-email-plugin/actions/workflows/ci.yml/badge.svg)](https://github.com/reecemiao/markitdown-email-plugin/actions/workflows/ci.yml)

A [MarkItDown](https://github.com/microsoft/markitdown) plugin that converts
**`.eml`** (RFC 5322) and **`.msg`** (Outlook) emails to Markdown — with
attachments ignored, inlined, or written out as **separate linked `.md` files**.

MarkItDown ships a built-in `.msg` reader that only emits From/To/Subject/body
and silently drops attachments. This plugin replaces it and adds `.eml`.

```markdown
# Q1 numbers + logo

| Field | Value |
| --- | --- |
| From | Alice Smith <alice@example.com> |
| To | Bob Jones <bob@example.com> |
| Date | 2025-01-06 09:30:00+02:00 |
| Attachments | 3 |

## Body

### Summary

Revenue is **up 12%**.

![](sample_attachments/02-logo.png)

## Attachments

| # | Name | Type | Size | Status | Markdown | Original |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | q1.csv | text/csv | 28 B | converted | [01-q1.md](…) | [01-q1.csv](…) |
| 2 | logo.png | image/png | 48 B | saved (inline) |  | [02-logo.png](…) |
| 3 | forwarded.eml | message/rfc822 |  | converted | [03-forwarded.md](…) |  |
```

## Install

```bash
uv pip install markitdown-email-plugin
```

MarkItDown finds the plugin through the `markitdown.plugin` entry point — no
registration code needed, just pass `enable_plugins=True`.

## Use

```python
from markitdown import MarkItDown

md = MarkItDown(enable_plugins=True)

print(md.convert("message.eml").markdown)  # attachments listed only
md.convert("message.msg", email_attachment_mode="inline")  # in one document
md.convert("message.msg", email_attachment_mode="separate")  # linked .md files
```

Or from the shell:

```bash
markitdown-email message.msg -o message.md --attachments separate
```

## Attachment modes

| Mode | What happens | Use it when |
| --- | --- | --- |
| `ignore` *(default)* | Attachments appear in the manifest table (name, type, size). Nothing is read or written. | You only care about the message. |
| `inline` | Each attachment is converted by MarkItDown and embedded under its own heading in the same document. | You want a single self-contained file. |
| `separate` | Each attachment is converted to its **own `.md` file** and linked from the manifest; the original file is copied alongside it. | **Recommended.** Keeps the email readable and gives every attachment its own document boundary. |

Attachment conversion is delegated to MarkItDown itself, so PDF, DOCX, XLSX,
PPTX, images, HTML, CSV — anything it supports — is supported here.

In `separate` mode files land in `<email-stem>_attachments/` next to the source
file (or next to the `-o` output), named `01-report.md`, `01-report.pdf`, …
The index prefix keeps same-named attachments apart, and links are relative and
URL-encoded so the document stays portable.

## Options

Every option is an `email_*` keyword and a `MARKITDOWN_EMAIL_*` environment
variable. Precedence, highest first:

1. `md.convert(path, email_attachment_mode="separate")` — per call
2. `MarkItDown(enable_plugins=True, email_attachment_mode="separate")` — per instance
3. `MARKITDOWN_EMAIL_ATTACHMENT_MODE=separate` — environment (this is how you
   configure the plugin through the stock `markitdown --use-plugins` CLI, which
   cannot forward custom flags)
4. the defaults below

| Option | Default | Meaning |
| --- | --- | --- |
| `email_attachment_mode` | `ignore` | `ignore` / `inline` / `separate` |
| `email_attachment_dir` | `<stem>_attachments/` | Where `separate` mode writes |
| `email_save_original_attachments` | `True` | Also copy the untouched original |
| `email_max_attachment_bytes` | `26214400` (25 MB) | Larger ones are listed, never converted |
| `email_max_depth` | `3` | How deep to follow emails attached to emails |
| `email_body_format` | `auto` | `auto` (HTML, else text) / `html` / `text` |
| `email_include_all_headers` | `False` | Append a table of every raw header |
| `email_frontmatter` | `False` | Prepend YAML frontmatter with the metadata |

## Output structure

The layout is fixed, so downstream tooling and LLMs can rely on it:

```
[optional YAML frontmatter]
# <subject>                  (or "# (no subject)")
| Field | Value |            metadata table — always present
## Body                      always present ("*(no body content)*" if empty)
## Attachments               only when the email has attachments
| # | Name | Type | ... |    one row per attachment, with its status
### Attachment 1: <name>     inline mode only — the converted content
## Headers                   only with email_include_all_headers
```

Two details that matter when feeding this to a model:

- **Headings from converted content are demoted** so they never outrank the
  section holding them — the document keeps one coherent outline.
- **`cid:` references are rewritten.** Inline images in an HTML body point at
  the saved file, so `![](mail_attachments/02-logo.png)` actually resolves.

Attachment statuses are explicit rather than silent: `listed`, `converted`,
`saved`, `unsupported format`, `too large`, `no content`, `depth limit reached`,
`conversion failed`.

## How it fits together

```
src/markitdown_email/
├── __init__.py       plugin entry point + public API
├── _options.py       EmailOptions — every knob and how it is resolved
├── _models.py        ParsedEmail / Attachment — the format-neutral model
├── _parsers/
│   ├── _eml.py       bytes -> ParsedEmail   (stdlib `email`)
│   └── _msg.py       bytes -> ParsedEmail   (OLE compound file via `olefile`)
├── _renderer.py      ParsedEmail -> Markdown
├── _attachments.py   the ignore / inline / separate modes
├── _context.py       RenderContext shared by renderer and attachments
├── _converters.py    MarkItDown DocumentConverter adapters
└── _cli.py           the `markitdown-email` command
```

The pipeline runs one direction, with a single junction:

```
.eml ─┐
      ├─> ParsedEmail ─> render ─> Markdown
.msg ─┘
```

Both formats meet at `ParsedEmail` and share everything after it, so supporting
another mail container means writing one parser and nothing else.

## Development

```bash
uv sync --all-groups
uv run pytest
uv run ruff check .
uv run mypy
uv run pre-commit install
```

CI runs the same checks on Python 3.10–3.13 across Linux and Windows, and
installs the built wheel into a clean environment to confirm the plugin entry
point still resolves. Publishing is documented in [RELEASING.md](RELEASING.md).

## Notes and limits

- **`.msg` test coverage.** `.msg` is an OLE2 compound document and no
  pure-Python package can *write* one, so the parser tests drive it through an
  in-memory stand-in for `olefile`'s reader API (see `tests/conftest.py`) rather
  than a binary fixture. The property, attachment and recipient decoding is
  covered; `olefile`'s own container reading is not re-tested here.
- **Opaque binaries are not converted.** MarkItDown's plain-text fallback will
  decode arbitrary bytes into mojibake, so attachments that nothing identifies
  (no usable mimetype *and* no known extension) are reported as
  `unsupported format` and, in `separate` mode, simply saved.
- **RTF-only `.msg` bodies** are not decoded; such messages fall back to their
  plain-text body.
- `.eml` detection is by extension and mimetype only — the plugin does not sniff
  arbitrary text files for mail headers.

## License

MIT
