Overview
TrailBase provides built-in email functionality for sending transactional emails like verification emails, password resets, and custom notifications. Configure SMTP or use the local sendmail transport.
Configuration
SMTP Configuration
Configure SMTP in your config.textproto:
email {
# Sender information
sender_address: "[email protected] "
sender_name: "My Application"
# SMTP server settings
smtp_host: "smtp.gmail.com"
smtp_port: 587
smtp_username: "[email protected] "
smtp_password: "your-app-password"
smtp_encryption: STARTTLS
}
Encryption Options
email {
# No encryption (not recommended for production)
smtp_encryption: NONE
# STARTTLS (recommended for most providers)
smtp_encryption: STARTTLS
# Direct TLS connection
smtp_encryption: TLS
}
When using NONE encryption, passwords are sent in plain text. Only use this for development or trusted networks.
Local Sendmail
If SMTP is not configured, TrailBase falls back to local sendmail:
// From crates/core/src/email.rs
pub ( crate ) fn new_from_config ( config : & Config ) -> Mailer {
return match smtp_from_config ( & config . email) {
Ok ( mailer ) => mailer ,
Err ( err ) => {
info! ( "Falling back to local sendmail: {err}" );
Self :: new_local ()
}
};
}
Built-in Email Templates
TrailBase includes default templates for common emails:
Email Verification
Sent when users register with email/password:
// Default subject
"Verify your Email Address for {APP_NAME}"
// Default body (HTML)
"""
<p>Welcome {EMAIL}!</p>
<p>Please verify your email address by clicking the link below:</p>
<p><a href=" { VERIFICATION_URL } ">Verify Email</a></p>
<p>Or copy and paste this code: {CODE}</p>
"""
Password Reset
Sent when users request password reset:
// Default subject
"Reset your Password for {APP_NAME}"
// Default body (HTML)
"""
<p>Hello {EMAIL},</p>
<p>You requested to reset your password. Click the link below to set a new password:</p>
<p><a href=" { VERIFICATION_URL } ">Reset Password</a></p>
<p>Or use this code: {CODE}</p>
<p>If you didn't request this, please ignore this email.</p>
"""
Change Email Address
Sent when users change their email:
// Default subject
"Change your Email Address for {APP_NAME}"
// Default body (HTML)
"""
<p>Hello {EMAIL},</p>
<p>Please confirm your new email address by clicking the link below:</p>
<p><a href=" { VERIFICATION_URL } ">Confirm Email Change</a></p>
<p>Or use this code: {CODE}</p>
"""
Custom Templates
Override default templates in your configuration:
email {
sender_address: "[email protected] "
# Custom verification email template
user_verification_template {
subject: "Welcome to {APP_NAME}! Verify your email"
body: """
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
.button {
background: #0066cc;
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 4px;
display: inline-block;
}
</style>
</head>
<body>
<h1>Welcome to {APP_NAME}!</h1>
<p>Hi there,</p>
<p>Thanks for signing up. Please verify your email address:</p>
<p><a href="{VERIFICATION_URL}" class="button">Verify Email</a></p>
<p>Or enter this code: <strong>{CODE}</strong></p>
<hr>
<p><small>If you didn't sign up, you can ignore this email.</small></p>
</body>
</html>
"""
}
# Custom password reset template
password_reset_template {
subject: "Reset your {APP_NAME} password"
body: """
<html>
<body>
<h2>Password Reset Request</h2>
<p>Click the link below to reset your password:</p>
<p><a href="{VERIFICATION_URL}">Reset Password</a></p>
<p>This link expires in 1 hour.</p>
</body>
</html>
"""
}
# Custom email change template
change_email_template {
subject: "Confirm your new email address"
body: """
<html>
<body>
<p>Please confirm your new email address: {EMAIL}</p>
<p><a href="{VERIFICATION_URL}">Confirm Change</a></p>
</body>
</html>
"""
}
}
Template Variables
Available variables in templates:
{APP_NAME} - Application name from config
{EMAIL} - User’s email address
{VERIFICATION_URL} - Complete verification URL
{SITE_URL} - Site URL from config
{CODE} - Verification code
Sending Custom Emails
From Rust
use trailbase_core :: api :: { Email , EmailError };
use trailbase_core :: AppState ;
async fn send_welcome_email (
state : & AppState ,
user_email : & str ,
user_name : & str ,
) -> Result <(), EmailError > {
let subject = format! ( "Welcome {}" , user_name );
let body = format! ( r#"
<html>
<body>
<h1>Welcome, {}!</h1>
<p>Thanks for joining our platform.</p>
<p>Get started by exploring our features.</p>
</body>
</html>
"# , user_name );
let email = Email :: new ( state , user_email , subject , body ) ? ;
email . send () . await ? ;
Ok (())
}
Email Implementation
From crates/core/src/email.rs:
pub struct Email {
mailer : Mailer ,
from : Mailbox ,
to : Mailbox ,
subject : String ,
body : String ,
}
impl Email {
pub fn new (
state : & AppState ,
to : & str ,
subject : String ,
body : String ,
) -> Result < Self , EmailError > {
// Constructs email with sender from config
}
pub async fn send ( & self ) -> Result <(), EmailError > {
let email = Message :: builder ()
. to ( self . to . clone ())
. from ( self . from . clone ())
. subject ( self . subject . clone ())
. header ( ContentType :: TEXT_HTML )
. body ( Body :: new ( self . body . clone ())) ? ;
match self . mailer {
Mailer :: Smtp ( ref mailer ) => {
mailer . send ( email ) . await ? ;
}
Mailer :: Local ( ref mailer ) => {
mailer . send ( email ) . await ? ;
}
};
Ok (())
}
}
Common SMTP Providers
Gmail
email {
smtp_host: "smtp.gmail.com"
smtp_port: 587
smtp_username: "[email protected] "
smtp_password: "your-app-password" # Use App Password, not account password
smtp_encryption: STARTTLS
}
SendGrid
email {
smtp_host: "smtp.sendgrid.net"
smtp_port: 587
smtp_username: "apikey"
smtp_password: "your-sendgrid-api-key"
smtp_encryption: STARTTLS
}
Mailgun
email {
smtp_host: "smtp.mailgun.org"
smtp_port: 587
smtp_username: "[email protected] "
smtp_password: "your-mailgun-smtp-password"
smtp_encryption: STARTTLS
}
AWS SES
email {
smtp_host: "email-smtp.us-east-1.amazonaws.com"
smtp_port: 587
smtp_username: "your-ses-smtp-username"
smtp_password: "your-ses-smtp-password"
smtp_encryption: STARTTLS
}
Postmark
email {
smtp_host: "smtp.postmarkapp.com"
smtp_port: 587
smtp_username: "your-server-api-token"
smtp_password: "your-server-api-token"
smtp_encryption: STARTTLS
}
Email in WASM Components
Direct email sending is not currently available in WASM components. Instead, queue email jobs in the database and process them with a scheduled job.
use trailbase_wasm :: http :: { HttpRoute , Request , routing};
use trailbase_wasm :: db :: {execute, Value };
use trailbase_wasm :: job :: Job ;
use trailbase_wasm :: { Guest , export};
struct EmailJobs ;
impl Guest for EmailJobs {
fn http_handlers () -> Vec < HttpRoute > {
vec! [
routing :: post ( "/send-notification" , queue_notification ),
]
}
fn job_handlers () -> Vec < Job > {
vec! [
Job :: minutely ( "process_email_queue" , process_email_queue ),
]
}
}
// Endpoint to queue emails
async fn queue_notification ( mut req : Request ) -> Result < String , HttpError > {
#[derive( Deserialize )]
struct NotificationRequest {
email : String ,
subject : String ,
message : String ,
}
let notification : NotificationRequest = req . body () . json () . await ? ;
// Queue email for processing
execute (
r#"
INSERT INTO email_queue (recipient, subject, body, status, created_at)
VALUES ($1, $2, $3, 'pending', CURRENT_TIMESTAMP)
"# ,
[
Value :: Text ( notification . email),
Value :: Text ( notification . subject),
Value :: Text ( notification . message),
]
) . await ? ;
Ok ( "Email queued" . to_string ())
}
// Job to process queued emails
async fn process_email_queue () {
use trailbase_wasm :: fetch :: {fetch, Request };
let pending = query (
"SELECT id, recipient, subject, body FROM email_queue WHERE status = 'pending' LIMIT 10" ,
[]
) . await . unwrap ();
for row in pending {
let id = row [ 0 ] . as_integer () . unwrap ();
let recipient = row [ 1 ] . as_text () . unwrap ();
let subject = row [ 2 ] . as_text () . unwrap ();
let body = row [ 3 ] . as_text () . unwrap ();
// Call admin API to send email (requires internal auth)
let result = send_email_via_api ( recipient , subject , body ) . await ;
match result {
Ok ( _ ) => {
execute (
"UPDATE email_queue SET status = 'sent', sent_at = CURRENT_TIMESTAMP WHERE id = $1" ,
[ Value :: Integer ( id )]
) . await . unwrap ();
}
Err ( e ) => {
execute (
"UPDATE email_queue SET status = 'failed', error = $1 WHERE id = $2" ,
[ Value :: Text ( e . to_string ()), Value :: Integer ( id )]
) . await . unwrap ();
}
}
}
}
export! ( EmailJobs );
Email Queue Schema
CREATE TABLE email_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
recipient TEXT NOT NULL ,
subject TEXT NOT NULL ,
body TEXT NOT NULL ,
status TEXT NOT NULL DEFAULT 'pending' CHECK ( status IN ( 'pending' , 'sent' , 'failed' )),
error TEXT ,
created_at INTEGER NOT NULL ,
sent_at INTEGER
);
CREATE INDEX idx_email_queue_status ON email_queue( status , created_at);
Error Handling
use trailbase_core :: api :: { Email , EmailError };
async fn send_with_retry (
state : & AppState ,
recipient : & str ,
subject : String ,
body : String ,
) -> Result <(), EmailError > {
let max_retries = 3 ;
let mut attempt = 0 ;
loop {
attempt += 1 ;
let email = Email :: new ( state , recipient , subject . clone (), body . clone ()) ? ;
match email . send () . await {
Ok ( _ ) => {
log :: info! ( "Email sent to {} on attempt {}" , recipient , attempt );
return Ok (());
}
Err ( e ) => {
if attempt >= max_retries {
log :: error! ( "Failed to send email after {} attempts: {}" , max_retries , e );
return Err ( e );
}
log :: warn! ( "Email send attempt {} failed, retrying: {}" , attempt , e );
tokio :: time :: sleep ( tokio :: time :: Duration :: from_secs ( 2 u64 . pow ( attempt ))) . await ;
}
}
}
}
Rate Limiting
use std :: sync :: Arc ;
use tokio :: sync :: Semaphore ;
// Limit concurrent email sends
static EMAIL_SEMAPHORE : LazyLock < Arc < Semaphore >> =
LazyLock :: new ( || Arc :: new ( Semaphore :: new ( 10 )));
async fn send_batch_emails (
state : & AppState ,
recipients : Vec <( String , String , String )>, // (email, subject, body)
) -> Result <(), EmailError > {
let mut tasks = vec! [];
for ( email , subject , body ) in recipients {
let state = state . clone ();
let permit = EMAIL_SEMAPHORE . clone () . acquire_owned () . await . unwrap ();
let task = tokio :: spawn ( async move {
let result = Email :: new ( & state , & email , subject , body ) ?
. send ()
. await ;
drop ( permit ); // Release permit
result
});
tasks . push ( task );
}
// Wait for all emails
for task in tasks {
task . await . unwrap () ? ;
}
Ok (())
}
Monitoring
Log Email Activity
CREATE TABLE email_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
recipient TEXT NOT NULL ,
subject TEXT NOT NULL ,
status TEXT NOT NULL CHECK ( status IN ( 'sent' , 'failed' , 'bounced' )),
error TEXT ,
sent_at INTEGER NOT NULL ,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
);
CREATE INDEX idx_email_log_recipient ON email_log(recipient);
CREATE INDEX idx_email_log_sent_at ON email_log(sent_at);
Track Delivery
async fn log_email_send (
conn : & Connection ,
recipient : & str ,
subject : & str ,
result : & Result <(), EmailError >,
) {
let ( status , error ) = match result {
Ok ( _ ) => ( "sent" , None ),
Err ( e ) => ( "failed" , Some ( e . to_string ())),
};
let _ = conn . execute (
"INSERT INTO email_log (recipient, subject, status, error, sent_at) VALUES (?, ?, ?, ?, ?)" ,
params! [ recipient , subject , status , error , chrono :: Utc :: now () . timestamp ()]
) . await ;
}
Best Practices
Use app-specific passwords
Never use your main account password for SMTP. Generate app-specific passwords.
Set proper sender info
Configure sender_address and sender_name to avoid spam filters:
Configure SPF and DKIM
Set up email authentication records for your domain to improve deliverability.
Handle bounces
Monitor bounce rates and remove invalid addresses from your lists.
Rate limit sends
Respect SMTP provider limits. Use queues for bulk emails.
Testing Emails
Development Setup
Use Mailhog for local testing:
# Run Mailhog
docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog
email {
smtp_host: "localhost"
smtp_port: 1025
smtp_encryption: NONE
sender_address: "dev@localhost"
}
View emails at http://localhost:8025
Next Steps
Jobs Scheduler Send scheduled emails
Custom Endpoints Create email triggers
OAuth Providers Configure authentication
Server-Side Rendering Generate HTML emails