Missive integrates with Askama for type-safe email templates.
Enable the templates feature:
[dependencies]
missive = { version = "0.7.0", features = ["resend", "templates"] }
askama = "0.13"There are two ways to use templates with Missive:
EmailTemplatetrait - Define subject and recipient in the template struct- Manual rendering - Render the template yourself and pass to
Email
The EmailTemplate trait lets you encapsulate all email details in one struct:
<!-- templates/welcome.html -->
<!DOCTYPE html>
<html>
<head>
<title>Welcome to {{ app_name }}</title>
</head>
<body>
<h1>Welcome, {{ username }}!</h1>
<p>Thanks for signing up. Click below to verify your email:</p>
<a href="{{ verify_url }}">Verify Email</a>
</body>
</html>use askama::Template;
use missive::{Address, EmailTemplate};
#[derive(Template)]
#[template(path = "welcome.html")]
struct WelcomeEmail {
app_name: String,
username: String,
verify_url: String,
recipient: String,
}
impl EmailTemplate for WelcomeEmail {
fn subject(&self) -> String {
format!("Welcome to {}!", self.app_name)
}
fn to(&self) -> Address {
self.recipient.as_str().into()
}
}Convert to Email and send:
let template = WelcomeEmail {
app_name: "My App".to_string(),
username: "Alice".to_string(),
verify_url: "https://example.com/verify?token=abc123".to_string(),
recipient: "alice@example.com".to_string(),
};
let email = template.into_email()?;
missive::deliver(&email).await?;For more control, render templates manually:
use askama::Template;
use missive::Email;
#[derive(Template)]
#[template(path = "welcome.html")]
struct WelcomeTemplate {
app_name: String,
username: String,
verify_url: String,
}
let template = WelcomeTemplate {
app_name: "My App".to_string(),
username: "Alice".to_string(),
verify_url: "https://example.com/verify?token=abc123".to_string(),
};
let html = template.render()?;
let email = Email::new()
.to("alice@example.com")
.subject("Welcome to My App!")
.html_body(&html);
missive::deliver(&email).await?;The EmailTemplate trait has optional methods you can override:
impl EmailTemplate for WelcomeEmail {
fn subject(&self) -> String {
"Welcome!".to_string()
}
fn to(&self) -> Address {
self.recipient.as_str().into()
}
// Optional: Set the sender
fn from(&self) -> Option<Address> {
Some(("My App", "hello@myapp.com").into())
}
// Optional: Set reply-to
fn reply_to(&self) -> Option<Address> {
Some("support@myapp.com".into())
}
// Optional: Add CC recipients
fn cc(&self) -> Vec<Address> {
vec!["team@myapp.com".into()]
}
// Optional: Add BCC recipients
fn bcc(&self) -> Vec<Address> {
vec![]
}
}For better deliverability, provide both HTML and plain text versions.
Use into_email_with_text() from the EmailTemplateExt trait:
use askama::Template;
use missive::{Address, EmailTemplate, EmailTemplateExt};
#[derive(Template)]
#[template(path = "welcome.html")]
struct WelcomeEmail {
username: String,
verify_url: String,
recipient: String,
}
impl EmailTemplate for WelcomeEmail {
fn subject(&self) -> String {
"Welcome!".to_string()
}
fn to(&self) -> Address {
self.recipient.as_str().into()
}
}
// Create a separate text template
#[derive(Template)]
#[template(path = "welcome.txt")]
struct WelcomeText {
username: String,
verify_url: String,
}
let html_template = WelcomeEmail {
username: "Alice".to_string(),
verify_url: "https://example.com/verify".to_string(),
recipient: "alice@example.com".to_string(),
};
let text_template = WelcomeText {
username: "Alice".to_string(),
verify_url: "https://example.com/verify".to_string(),
};
let text_body = text_template.render()?;
let email = html_template.into_email_with_text(&text_body)?;#[derive(Template)]
#[template(path = "welcome.html")]
struct WelcomeHtml {
username: String,
verify_url: String,
}
#[derive(Template)]
#[template(path = "welcome.txt")]
struct WelcomeText {
username: String,
verify_url: String,
}
let html = WelcomeHtml { /* ... */ }.render()?;
let text = WelcomeText { /* ... */ }.render()?;
let email = Email::new()
.to("alice@example.com")
.subject("Welcome!")
.html_body(&html)
.text_body(&text);Example templates:
<!-- templates/welcome.html -->
<h1>Welcome, {{ username }}!</h1>
<a href="{{ verify_url }}">Verify</a><!-- templates/welcome.txt -->
Welcome, {{ username }}!
Verify: {{ verify_url }}
By default, Askama looks for templates in a templates/ directory at your crate root:
my-app/
├── Cargo.toml
├── src/
│ └── main.rs
└── templates/
├── welcome.html
├── welcome.txt
└── password_reset.html
Configure in askama.toml if needed:
[general]
dirs = ["templates", "emails"]Use Askama's template inheritance for consistent layouts:
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: sans-serif; }
.container { max-width: 600px; margin: 0 auto; }
.footer { color: #666; font-size: 12px; }
</style>
</head>
<body>
<div class="container">
{% block content %}{% endblock %}
<div class="footer">
<p>© 2024 {{ company_name }}</p>
<p><a href="{{ unsubscribe_url }}">Unsubscribe</a></p>
</div>
</div>
</body>
</html><!-- templates/welcome.html -->
{% extends "base.html" %}
{% block content %}
<h1>Welcome, {{ username }}!</h1>
<p>Thanks for joining us.</p>
{% endblock %}#[derive(Template)]
#[template(path = "welcome.html")]
struct WelcomeEmail {
username: String,
company_name: String,
unsubscribe_url: String,
recipient: String,
}
impl EmailTemplate for WelcomeEmail {
fn subject(&self) -> String {
format!("Welcome, {}!", self.username)
}
fn to(&self) -> Address {
self.recipient.as_str().into()
}
}Email clients have limited CSS support. Use inline styles or a CSS inliner:
// Option 1: Inline styles in template
// <h1 style="color: #333; font-size: 24px;">Welcome</h1>
// Option 2: Use css-inline crate
use css_inline::inline;
let html = template.render()?;
let inlined = inline(&html)?;
let email = Email::new()
.to("user@example.com")
.subject("Welcome!")
.html_body(inlined);Askama supports loops, conditionals, and filters:
<h1>Your Order #{{ order_id }}</h1>
<table>
{% for item in items %}
<tr>
<td>{{ item.name }}</td>
<td>{{ item.quantity }}</td>
<td>${{ item.price|fmt("{:.2}") }}</td>
</tr>
{% endfor %}
</table>
<p><strong>Total: ${{ total|fmt("{:.2}") }}</strong></p>
{% if has_discount %}
<p>Discount applied: {{ discount_code }}</p>
{% endif %}Template rendering can fail. Handle errors appropriately:
use missive::MailError;
let template = WelcomeEmail { /* ... */ };
match template.into_email() {
Ok(email) => {
missive::deliver(&email).await?;
}
Err(MailError::TemplateError(e)) => {
tracing::error!("Template rendering failed: {}", e);
// Maybe send a fallback plain text email
}
Err(e) => return Err(e),
}Test template rendering without sending:
#[test]
fn test_welcome_template() {
let template = WelcomeEmail {
username: "Alice".to_string(),
verify_url: "https://example.com/verify".to_string(),
recipient: "alice@example.com".to_string(),
};
// Test raw rendering
let html = template.render().unwrap();
assert!(html.contains("Welcome, Alice!"));
assert!(html.contains("https://example.com/verify"));
}
#[test]
fn test_email_conversion() {
let template = WelcomeEmail {
username: "Alice".to_string(),
verify_url: "https://example.com/verify".to_string(),
recipient: "alice@example.com".to_string(),
};
let email = template.into_email().unwrap();
assert_eq!(email.subject_line(), "Welcome!");
assert!(email.html_body_content().unwrap().contains("Alice"));
}Suggested structure for larger apps:
src/
├── emails/
│ ├── mod.rs
│ ├── welcome.rs
│ ├── password_reset.rs
│ └── order_confirmation.rs
└── main.rs
templates/
├── emails/
│ ├── base.html
│ ├── welcome.html
│ ├── welcome.txt
│ ├── password_reset.html
│ └── order_confirmation.html
// src/emails/mod.rs
mod welcome;
mod password_reset;
pub use welcome::send_welcome_email;
pub use password_reset::send_password_reset;// src/emails/welcome.rs
use askama::Template;
use missive::{Address, EmailTemplate, deliver};
#[derive(Template)]
#[template(path = "emails/welcome.html")]
struct WelcomeEmail {
username: String,
verify_url: String,
recipient: String,
}
impl EmailTemplate for WelcomeEmail {
fn subject(&self) -> String {
"Welcome!".to_string()
}
fn to(&self) -> Address {
self.recipient.as_str().into()
}
}
pub async fn send_welcome_email(
to: &str,
username: &str,
verify_url: &str,
) -> Result<(), missive::MailError> {
let template = WelcomeEmail {
username: username.to_string(),
verify_url: verify_url.to_string(),
recipient: to.to_string(),
};
let email = template.into_email()?;
deliver(&email).await?;
Ok(())
}