mail2md: turn .eml files into notes with YAML frontmatter (macOS CLI)

Disclaimer

Is this project open source? Yes (MIT)
Is this project completely free? Yes
Does this project use AI beyond the author’s ability to comprehend how it works? No. It contains no model and makes no network calls.


I keep a fair amount of correspondence in my vault: job applications, client threads, anything with a decision in it. Getting a mail in there used to be copy, paste, and retype the metadata by hand.

So I wrote a small command line tool for it. One .eml in, one .md out:

mail2md Mail.eml
---
created: 2026-06-15T09:41
from: "Jane Doe <[email protected]>"
to: "Anton Fillmann <[email protected]>"
via:
subject: "Project inquiry"
attachments:
---

Hallo Anton,

...

The point of the frontmatter is that the note becomes queryable. You can sort by created, filter by from, or list every mail that carried an attachment. Most converters I found write that same information into the prose instead, as a heading plus a bullet list, which reads fine and answers no questions.

A few details that took the longest to get right:

  • created is rendered in your local time, not the sender’s timezone, so it sorts correctly next to the rest of your notes.
  • Quoted reply chains stay in one file. Earlier versions split a thread into one note per message, and I removed that again: interleaved replies have no clean boundary, quote separators are client- and language-specific, and no RFC defines them. A converter that guesses there produces confident garbage. Splitting is a judgment call, so it belongs to whoever knows the context.
  • Nothing is overwritten silently. An existing target with identical content is skipped, one that differs aborts unless you pass --force.
  • Attachments are listed in the frontmatter and can be extracted with --extract-attachments. Inline images are deliberately not treated as attachments, since a named inline image is a signature logo or a tracking pixel far more often than a file someone meant to send.

How it compares. Postbox ( GitHub - istefox/Postbox: Obsidian plugin to import and manage .eml email files in your vault · GitHub ) is the closest thing I know of, and parts of it are better than mine: it renders inline cid: images, keeps HTML tables, reads .msg, and de-duplicates by Message-ID. It is a plugin, so it is desktop-only and Obsidian has to be running. mail2md is the better fit when the conversion should happen without Obsidian in the loop, in a shell pipeline, a cron job, or a Makefile. Several EML viewer plugins exist too, but they render the mail rather than convert it, and the file stays an .eml.

Install (macOS 13+):

brew install antonfill/tap/mail2md

The formula builds from source against Swift 6.0, so Xcode 16 or newer has to be installed. There is no prebuilt binary yet, and that is the honest catch: if there is no Xcode on your Mac at all, this is a large download for a small tool.

MIT licensed, source at GitHub - AntonFill/mail2md: Convert .eml files to Markdown with YAML frontmatter. A local-first CLI for macOS. · GitHub . JSON output and stdin/stdout piping are on the roadmap.

There is a companion tool in the same family, realdate ( GitHub - AntonFill/RealDate: A CLI tool that extracts dates from filenames, sets macOS file timestamps, and cleans up filenames for better file system sorting. · GitHub ), which takes the date out of a filename like 2026.06.07 Invoice.pdf and writes it into the file’s timestamps. Both are local-first: no account, no cloud, no network access.

I would like to know how other people handle this. What do you do today with mail you want to keep in your vault, and which part of it annoys you enough that you have thought about it more than once?

Small update, since this thread is the release channel for the tool: v1.1.0 is out, and it is the release that matters most if you keep mail in a vault.

Until now, a mail that consisted of its attachments turned into a note that looked empty. Fourteen screenshots and a signature line came out as the signature line. The frontmatter named the files, the body said nothing about them, and it looked like the tool had given up halfway.

Since 1.1.0 the attachments close the body: images embedded two per row, documents linked, no invented heading and no rule above them. And --attachment-name takes a naming pattern from outside, so extracted files land under your own convention instead of their MIME names:

mail2md Mail.eml \
  --attachments-dir ./attachments \
  --attachment-name "{date:yyyy.MM.dd} {time:HH.mm} ENCL {name}"
---
created: 2026-08-18T07:52
from: "Jane Doe <[email protected]>"
subject: "Kitchen photos and the quote"
attachments:
  - "[[2026.08.18 07.52 ENCL wall-left.png]]"
  - "[[2026.08.18 07.52 ENCL wall-right.png]]"
  - "[[2026.08.18 07.52 ENCL Quote 2026-08.pdf]]"
---

Hi Anton,

photos of the two walls and the quote as PDF.

|  |  |
|---|---|
| ![[2026.08.18 07.52 ENCL wall-left.png]] | ![[2026.08.18 07.52 ENCL wall-right.png]] |

[[2026.08.18 07.52 ENCL Quote 2026-08.pdf|Quote 2026-08.pdf]]

The placeholders are {name}, {ext}, {date} and {time}. The last two render the mail’s own timestamp, which is what makes an attachment sort directly behind the mail it came from, and they take a format string after a colon.

Two decisions in there could have gone the other way, so they are worth stating:

  • The attachment block only appears when the files are actually written. An embed pointing at a file nobody extracted is a dead link, and a default output full of dead links would have been worse than the previous silence. Without the extraction flags, 1.1.0 produces byte-identical output to 1.0.2.
  • The tool does not learn a vault schema, it accepts one. Whether a screenshot should be called 01 Kitchen wall or IMG_0001 is a judgment about content, and a converter has no business guessing it.

Upgrading is brew upgrade mail2md. One caveat worth knowing: if you re-run the tool with extraction over a note that 1.0.x wrote, you get a conflict and exit 1 rather than a silent overwrite. That is the safety check working as intended, and --force is the answer.

Still on the roadmap: JSON output and stdin/stdout piping.

One question, since naming is the part I had to decide alone: what does an attachment have to be called for you to find it again a year later, without opening the note it came from?