modo-email
Email sending for the modo framework. Markdown templates, responsive HTML output, pluggable transports, multi-tenant sender customization.
Features
- Markdown templates with YAML frontmatter and
{{var}}substitution - Button syntax --
[button|Label](url)renders as email-safe table-based buttons - Responsive HTML layout with dark mode support, or bring your own
- Plain text fallback auto-generated from Markdown
- Transports -- SMTP (default) and Resend HTTP API
- Multi-tenant -- per-email
SenderProfileoverride and brand context variables - Serializable payload (
SendEmailPayload) for async sending via modo-jobs - Custom template sources -- implement
TemplateProviderfor DB-backed templates, APIs, etc.
Quick Start
Add to Cargo.toml:
[]
= { = "../modo-email" }
# For Resend instead of (or alongside) SMTP:
# modo-email = { path = "../modo-email", features = ["resend"] }
Configuration
# config.yaml (or however your app loads config)
email:
transport: smtp # or "resend"
templates_path: "emails"
default_from_name: "My App"
default_from_email: "hello@myapp.com"
default_reply_to: "support@myapp.com"
smtp:
host: "smtp.example.com"
port: 587
username: "user"
password: "pass"
tls: true
All fields have defaults (templates_path defaults to "emails", smtp.port to 587, smtp.tls to true). Only specify what you need to override.
Setup and Send
use ;
// Build the mailer from config (uses FilesystemProvider + configured transport).
let config: EmailConfig = load_config; // your config loading
let m = mailer?;
// Send a templated email.
m.send.await?;
Render Without Sending
let message = m.render?;
println!;
println!;
println!;
Template Format
Templates are .md files with YAML frontmatter:
subject: "Welcome {{name}}!"
layout: default
Hi **{{name}}**,
Thanks for joining! Here's what you can do next.
[button|Get Started]({{dashboard_url}})
If you have questions, just reply to this email.
Frontmatter Fields
| Field | Required | Description |
|---|---|---|
subject |
Yes | Email subject line. Supports {{var}} placeholders. |
layout |
No | Layout name to wrap the body in. Falls back to "default" built-in. |
Variable Substitution
Use {{key}} or {{ key }} (whitespace is trimmed). Variables are passed via the builder:
new
.var
.var
.var
Unresolved placeholders are left as-is in the output.
Button Syntax
[button|Click Me](https://example.com)
Renders as an email-safe, table-based CTA button with a default indigo (#4F46E5) background. Customize the color per-email with the brand_color variable:
new
.var
Regular Markdown links render as styled inline links:
[View docs](https://docs.example.com)
Directory Structure
Single Language
emails/
welcome.md
reset_password.md
invoice.md
Multi-Language (Locale Subdirectories)
emails/
welcome.md # default / fallback
reset_password.md
de/
welcome.md # German override
reset_password.md
fr/
welcome.md # French override
layouts/
default.html # custom layout (overrides built-in)
minimal.html # additional named layout
Locale resolution: if de/welcome.md exists, it is used; otherwise falls back to welcome.md.
new
.locale
.var
Layouts
The built-in default layout provides a responsive, dark-mode-aware HTML email wrapper. It supports these context variables:
| Variable | Description |
|---|---|
content |
Rendered HTML body (injected automatically) |
subject |
Email subject (injected automatically) |
logo_url |
Optional logo image URL |
product_name |
Optional product name (logo alt text) |
footer_text |
Optional footer text |
To override the built-in layout or add custom ones, place .html files in {templates_path}/layouts/:
emails/layouts/default.html # overrides built-in default
emails/layouts/minimal.html # new named layout
Layouts use MiniJinja syntax. All email context variables are available. Auto-escaping is disabled since the content is pre-rendered HTML.
<!-- emails/layouts/minimal.html -->
{{content}}
Reference it in a template's frontmatter:
subject: "Alert"
layout: minimal
Multi-Tenant Usage
Per-Email Sender Override
use ;
let tenant_sender = SenderProfile ;
m.send.await?;
Without .sender(), the mailer uses the default sender from EmailConfig.
Brand Context Variables
Pass brand-specific variables to customize templates and layouts per tenant:
use HashMap;
let mut brand = new;
brand.insert;
brand.insert;
brand.insert;
brand.insert;
m.send.await?;
Async Sending via modo-jobs
SendEmailPayload is a serializable mirror of SendEmail, designed for job queue payloads. The job handler lives in your application, not in this crate:
use ;
use job;
// Enqueue from a handler:
let payload = from;
jobs.enqueue.await?;
// Job worker:
async
Custom TemplateProvider
Implement the TemplateProvider trait to load templates from a database, API, or any other source:
use ;
use Arc;
// Use it:
let provider = new;
let m = mailer_with?;
EmailTemplate fields if you want to build one without parsing:
EmailTemplate
Transports and Feature Flags
| Feature | Default | Transport | Dependency |
|---|---|---|---|
smtp |
Yes | SMTP via lettre | lettre |
resend |
No | Resend HTTP API | reqwest |
Both features can be enabled simultaneously. The active transport is selected by transport in config.
# SMTP only (default):
= { = "../modo-email" }
# Resend only:
= { = "../modo-email", = false, = ["resend"] }
# Both available:
= { = "../modo-email", = ["resend"] }
Resend Configuration
email:
transport: resend
templates_path: "emails"
default_from_name: "My App"
default_from_email: "hello@myapp.com"
resend:
api_key: "re_..."
Configuration Reference
EmailConfig
| Field | Type | Default | Description |
|---|---|---|---|
transport |
smtp / resend |
smtp |
Which transport backend to use |
templates_path |
String |
"emails" |
Directory containing .md templates |
default_from_name |
String |
"" |
Default sender display name |
default_from_email |
String |
"" |
Default sender email address |
default_reply_to |
Option<String> |
None |
Default reply-to address |
smtp |
SmtpConfig |
see below | SMTP settings (requires smtp feature) |
resend |
ResendConfig |
see below | Resend settings (requires resend feature) |
SmtpConfig
| Field | Type | Default | Description |
|---|---|---|---|
host |
String |
"localhost" |
SMTP server hostname |
port |
u16 |
587 |
SMTP server port |
username |
String |
"" |
SMTP auth username |
password |
String |
"" |
SMTP auth password |
tls |
bool |
true |
Enable STARTTLS |
ResendConfig
| Field | Type | Default | Description |
|---|---|---|---|
api_key |
String |
"" |
Resend API key |
Key Types
| Type / Trait | Description |
|---|---|
Mailer |
Central service: render and deliver emails |
SendEmail |
Builder for a single email send request |
SendEmailPayload |
Serializable mirror of SendEmail for job queues |
MailMessage |
Fully-rendered email ready for delivery (html, text, subject...) |
SenderProfile |
Sender identity (from_name, from_email, reply_to) |
EmailTemplate |
Parsed template (subject, body, optional layout name) |
TemplateProvider |
Trait for custom template sources |
MailTransport |
Async trait for custom delivery backends |
FilesystemProvider |
Built-in filesystem template provider |
LayoutEngine |
MiniJinja-based HTML layout renderer |
EmailConfig |
Top-level config struct (transport, paths, defaults) |