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.
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):
= "0.2"
# Resend only:
= { = "0.2", = false, = ["resend"] }
# Both available:
= { = "0.2", = ["resend"] }
Usage
Configuration
# config.yaml
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.
Send a Templated Email
use ;
// Build the mailer from config (uses FilesystemProvider + configured transport).
let config: EmailConfig = /* load from YAML */;
let m = mailer?;
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). Pass variables 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
brand_color must be a valid CSS hex color (#RGB or #RRGGBB); invalid values fall back to the default.
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
The mailer is registered as a service on the jobs builder, not on the app. Job handlers
access it via the Service<T> parameter, which the #[job] macro resolves from the
jobs service registry.
use ;
use Service;
use JobQueue;
// In main: build and register the mailer with the jobs runner.
// let mailer = modo_email::mailer(&config.email)?;
// let jobs = modo_jobs::new(&db, &config.jobs)
// .service(mailer)
// .run()
// .await?;
// Job worker — Service<Mailer> resolves from the jobs service registry:
async
// Enqueue from an HTTP handler using the generated struct method:
async
SendEmailJob is the struct generated by #[job] for the send_email function (PascalCase + Job suffix). Import Service from modo::extractor::Service.
Custom TemplateProvider
Implement TemplateProvider to load templates from a database, API, or any other source:
use ;
use Arc;
;
let provider = new;
let m = mailer_with?;
Build an EmailTemplate directly (without parsing) when you control the source:
EmailTemplate
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 (port 587) |
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 queue payloads |
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) |