189 lines
6.8 KiB
Markdown
189 lines
6.8 KiB
Markdown
---
|
|
title: Email Formatting Is Harder Than It Looks
|
|
date: 2025-07-13
|
|
draft: true
|
|
---
|
|
|
|
*[UTF-8]: Unicode Transformation Format - 8 bit.
|
|
|
|
[Kakoune]: https://kakoune.org
|
|
|
|
As I've [mentioned before](./email-in-kakoune.md), I like using [Kakoune] for
|
|
reading & writing emails. Of course, Kakoune is a text editor, not a _rich text_
|
|
editor. It operates on UTF-8 _plaintext_ --- which means that the emails I write
|
|
need to be in plaintext, too.
|
|
|
|
As I went down this path, I quickly discovered that I needed an **email
|
|
formatter**. I eventually wrote [`mailfmt`](https://git.sr.ht/~ficd/mailfmt) to
|
|
fill this niche. It provides consistent paragraph spacing, hard-wrapping and
|
|
paragraph reflow, while preserving Markdown syntax, email headers, quotes,
|
|
sign-offs, and signature blocks. Additionally, the wrapped output can be made
|
|
safe for passing to a Markdown parser. This is useful if you want to build an
|
|
HTML email from plain-text.
|
|
|
|
`mailfmt` open-source under the ISC license, and is available on
|
|
[PyPI](https://pypi.org/project/mailfmt/) for installation with tools like
|
|
`pipx` and `uv`. The source code is available on sourcehut at
|
|
[git.ficd.sh/ficd/mailfmt](https://git.ficd.sh/ficd/mailfmt).
|
|
|
|
## Target Audience
|
|
|
|
I wrote this tool primarily for myself. It's served me very well over the past
|
|
few months. `mailfmt` could be helpful for anyone that prefers writing email in
|
|
plain-text using text editors like Kakoune, Helix, and Vim. It can format via
|
|
`stdin`/`stdout` and read/write files, making `mailfmt` easy to configure as a
|
|
formatter for the `mail` filetype in your editor.
|
|
|
|
I'm including a very lengthy explanation of exactly why I built this tool. You
|
|
may think it's overkill for such a small program — but I like to be crystal
|
|
clear about justifying my work. It reads like blog post rather than the
|
|
emoji-filled `README`/marketing style we're accustomed to seeing on this
|
|
platform. I've put a lot of thought into this, and I want to share my work. I
|
|
hope you enjoy reading about my thought process.
|
|
|
|
## Why I Built It (Comparison)
|
|
|
|
Unsurprisingly, it all started with a specific problem I was having composing
|
|
emails in plain-text format in my preferred text editor. As I searched for a
|
|
solution, I couldn't find anything that met all my needs, so I wrote it myself.
|
|
|
|
Here's what I wanted:
|
|
|
|
- A way to consistently format my outgoing emails in my text editor.
|
|
- Paragraph reflow and automatic line wrapping.
|
|
- Not all plain-text clients are capable of line-wrap. In some contexts, such
|
|
as mailing lists, the author is expected to wrap the text themselves.
|
|
- Inline Markdown syntax `can _still_ look great, **even** in plain-text!` Thus,
|
|
I wanted to use it:
|
|
- Without it being broken by reflow & wrap.
|
|
- While looking good and retaining the same semantics in _both_ rendered
|
|
**and** plain-text form — ideal for `multipart` emails.
|
|
- Ensure signature block is formatted properly.
|
|
- The single space after `--` and before the newline **must** be included.
|
|
|
|
### `fmt` and Markdown Formatters Don't Work For Email
|
|
|
|
The `fmt` utility provides great wrapping and reflow capabilities — I use it all
|
|
the time while writing LaTeX. However, it's syntax agnostic, and breaks
|
|
Markdown. For example, it completely mangles fenced code blocks. I figured: hey,
|
|
why not just use a Markdown formatter? It supports Markdown (obviously), _and_
|
|
can reflow & wrap text! Here's the problem: it turns out treating your
|
|
**entire** email as a Markdown document isn't ideal.
|
|
|
|
`mailfmt`'s approach is simple: detect when a line matches a known pattern of
|
|
Markdown block element syntax, such as leading `#` for headings, `-` for lists,
|
|
etc. If so, **leave the line untouched**. Similarly, **don't format anything
|
|
inside fenced code blocks**.
|
|
|
|
#### Sign-Offs
|
|
|
|
Consider the following sign-off:
|
|
|
|
```
|
|
Best wishes,
|
|
Daniel
|
|
```
|
|
|
|
A Markdown formatter considers this to be one paragraph, and reflows it
|
|
accordingly, causing it to lost semantic meaning:
|
|
|
|
```
|
|
Best wishes, Daniel
|
|
```
|
|
|
|
Within the confines of Markdown, I counted three ways of dealing with the
|
|
problem:
|
|
|
|
1. Put an empty line between the two parts:
|
|
|
|
```
|
|
Best wishes,
|
|
|
|
Daniel
|
|
```
|
|
|
|
> However, this empty line looks _awkward_ when viewed in plain-text.
|
|
|
|
2. Put a backslash after the intentional line break:
|
|
|
|
```
|
|
Best wishes, \
|
|
Daniel
|
|
```
|
|
|
|
> Again, this looks bad when the Markdown isn't rendered.
|
|
|
|
3. Put two spaces after the intentional line break (• = space):
|
|
|
|
```
|
|
Best•wishes,••
|
|
Daniel
|
|
```
|
|
|
|
> This syntax is **ambiguous, easy to forget**, and **not supported by editors
|
|
> that trim trailing whitespace.**
|
|
|
|
`mailfmt` detects sign-offs using a very simple heuristic. First, we check if a
|
|
line has _5 or less_ words, and **ends with a comma**. If we find such a line,
|
|
we check the _next_ line. If it has 5 or less words **that all begin with an
|
|
uppercase letter**, then we assume these two lines are a _sign-off_, and we
|
|
don't reflow or wrap them. The heuristic matches a very simple pattern:
|
|
|
|
```
|
|
A courteous greeting,
|
|
First Middle Last Name
|
|
```
|
|
|
|
#### Signature Block
|
|
|
|
The convention for signature blocks is as follows:
|
|
|
|
1. Begins with two `-` characters followed by a single space, then a newline.
|
|
2. Everything that follows until the EOF is part of the signature.
|
|
|
|
Here's an example (note the • = space):
|
|
|
|
```
|
|
--•
|
|
Daniel
|
|
|
|
Software•Developer,•Company
|
|
email@website.com
|
|
```
|
|
|
|
As with sign-offs, such a signature block gets mangled by Markdown formatters.
|
|
Furthermore, the single space after the `--` token is important: if it's
|
|
missing, some clients won't recognize it is a valid signature — our formatter
|
|
should address this too.
|
|
|
|
`mailfmt` detects when a line's _only_ content is `--`. It adds the required
|
|
trailing space if it's missing, and it treats the rest of the input as part of
|
|
the signature, leaving it completely untouched.
|
|
|
|
### Consistent Multipart Emails
|
|
|
|
Something you may want to do is generate a `multipart` email. This means that
|
|
_both_ an HTML **and** plain-text representation of the _same_ email are
|
|
included in the file — leaving it up to the reader's client to pick which one to
|
|
display.
|
|
|
|
The plain-text email **must** be able to stand on its own, and _also_ render to
|
|
decent-looking HTML. Essentially, you want to write your email in plain-text
|
|
once, ensuring it has proper formatting, and then use a command to generate an
|
|
HTML email from it. For this, `mailfmt` provides the `--markdown-safe` flag,
|
|
which appends backslashes to the formatted output, making it safe for Markdown
|
|
parsing without messing up the line breaks after sign-offs and signature blocks.
|
|
|
|
For example, I use the following in [aerc](https://aerc-mail.org/) to generate
|
|
an HTML multipart email whenever I want:
|
|
|
|
```ini
|
|
[multipart-converters]
|
|
text/html=mailfmt --markdown-safe | pandoc -f markdown -t html --standalone
|
|
```
|
|
|
|
## Conclusion
|
|
|
|
If you've made it this far, thanks for sticking with me and reading to the end!
|
|
Even if you don't plan to write plain-text email or use `mailfmt` at all, I hope
|
|
you learned something interesting.
|