Inkletter
Write your emails like prose, send them like a pro.
Inkletter turns plain Markdown files into beautiful, responsive MJML and HTML email layouts, ready to be previewed, shared or sent to the world.
Why Inkletter?
Because writing HTML emails by hand is like ironing socks: pointless and painful.
With Inkletter, you write your content in Markdown (like a decent human being), and it becomes a gorgeous, mobile-friendly HTML email powered by MJML.
Features
- Markdown to MJML or to final responsive HTML, in one command
- Seven built-in themes, or your own theme in a small TOML file
- Live preview in your browser, with a device simulator (iPhone, iPad, desktop)
- Clean Python API if you'd rather script it
- Runs entirely on your machine, no account, no vendor lock-in
Installation
Python 3.10+ required.
pip install inkletter
Or for development:
git clone https://github.com/lpauloin/Inkletter.git
cd Inkletter
pip install -e .
Usage
Preview a Markdown file as a responsive email
inkletter preview newsletter.md
Opens a split view in your browser: your Markdown, the generated MJML, and the rendered email in a device simulator.
Convert to HTML
inkletter md2html newsletter.md -o newsletter.html --view
Writes the final email HTML, and opens it in your browser with --view.
Export the raw MJML
inkletter md2mjml newsletter.md -o newsletter.mjml
Without -o, the MJML is printed to stdout, ready to be piped anywhere.
Image layout
Layout is driven by plain CommonMark structure — no custom syntax, the same file stays clean in any Markdown editor (and reusable for other channels):
-
A paragraph made only of images becomes a row of side-by-side columns (up to 4 on one row, more wrap into rows of 3):
 
-
A paragraph starting (or ending) with a single image beside text becomes a media object — image next to its text, 30/70 by default. Put the image last to place it on the right:
 Jean joined the team this week. He will own the rendering platform.
On mobile both patterns stack, image on top. Ratios, spacing and behaviour
are tuned in the [images] theme section below — including
text_layout = "stacked" to disable media-object columns entirely.
Theming
Every command accepts --theme with a preset name or a theme file,
and --no-theme for bare, unstyled MJML:
inkletter md2html newsletter.md --theme dark
inkletter md2html newsletter.md --theme mytheme.toml
Built-in presets
| Preset | Mood |
|---|---|
default |
Clean and neutral — Helvetica, gray text, blue links |
dark |
Slate night mode — light text, Trebuchet MS headings |
crystal |
Airy and elegant — Palatino headings, cold blue accents |
blue |
Corporate and trustworthy — Tahoma text, Trebuchet MS headings |
green |
Organic and editorial — Georgia throughout |
red |
Bold and editorial — Georgia headings over Helvetica text |
yellow |
Warm and friendly — Verdana text, Trebuchet MS headings |
Here is each preset rendered on desktop and on a 375px mobile screen:
Write your own
A theme file is partial — set only what you want to change, everything else keeps the default look:
[layout]
width = "640px"
[text]
font_family = "Georgia, serif"
[links]
color = "#c0392b"
underline = false
| Section | Keys |
|---|---|
[layout] |
width, background_color, content_background_color, section_padding |
[text] |
font_family, font_size, line_height, color |
[headings] |
font_family, color, font_weight, h1_size, h2_size, h3_size |
[links] |
color, underline |
[code] |
font_family, background_color, color |
[quote] |
color, border_color, font_style |
[divider] |
color, width |
[table] |
border_color, cell_padding, header_color, header_background_color |
[images] |
align, row_gap, border_radius, text_layout, media_ratio |
Any unknown section or key fails loudly, with the list of valid ones.
From Python
Same defaults, same presets, plus optional named color palettes:
from inkletter.colors import Blue
from inkletter.md_to_html import parse_markdown_to_html
from inkletter.theme import Links, Text, Theme
theme = Theme(text=Text(font_family="Georgia, serif"), links=Links(color=Blue.DARK))
html = parse_markdown_to_html(markdown, theme=theme)
Samples
- sample.md — the Markdown source
- sample.html — the generated responsive email
- sample/themes/ — the same source rendered with every preset
Contributing
French or not, you are welcome to contribute. Fork it, branch it, test it, PR it — with love.
pip install -r requirements-test.txt
pytest
Every push and pull request runs through the GitHub Actions CI on Python 3.10 to 3.13.
License
MIT — but don't forget to say "merci" 😉
Made with ❤️ and markdown in France.
Metadata
Release files for inkletter 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| inkletter-1.1.0.tar.gz | 26.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| inkletter-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 50.1 kB
Release files / inkletter-1.1.0.tar.gz
| Download URL | inkletter-1.1.0.tar.gz |
|---|---|
| Size | 26.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d2087bf9fddd41ae1c9e185b7ebf7a084d4a865a8ed4ce21110e813cdb6d4f45
|
|
BLAKE2b-256 checksum How to use checksums |
9139602e6bd6c2cd2d91ee29da36cbb062a66061cf6737d3e58bd39d20c97641
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Release files / inkletter-1.1.0-py3-none-any.whl
| Download URL | inkletter-1.1.0-py3-none-any.whl |
|---|---|
| Size | 24.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
aeb4cc9bbd1d6ddd0d52af1779722f985cf699dd727b302117190915c6148864
|
|
BLAKE2b-256 checksum How to use checksums |
714272b2eb43c0f1e878bb2868d7bf6216e14bb1a8b598e4f2a5bc8813080bd4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|