> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/trailbaseio/trailbase/llms.txt
> Use this file to discover all available pages before exploring further.

# Email Configuration

> Send transactional emails with SMTP or sendmail

## 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`:

```protobuf theme={null}
email {
  # Sender information
  sender_address: "noreply@example.com"
  sender_name: "My Application"
  
  # SMTP server settings
  smtp_host: "smtp.gmail.com"
  smtp_port: 587
  smtp_username: "your-email@gmail.com"
  smtp_password: "your-app-password"
  smtp_encryption: STARTTLS
}
```

### Encryption Options

```protobuf theme={null}
email {
  # No encryption (not recommended for production)
  smtp_encryption: NONE
  
  # STARTTLS (recommended for most providers)
  smtp_encryption: STARTTLS
  
  # Direct TLS connection
  smtp_encryption: TLS
}
```

<Warning>
  When using `NONE` encryption, passwords are sent in plain text. Only use this for development or trusted networks.
</Warning>

### Local Sendmail

If SMTP is not configured, TrailBase falls back to local sendmail:

```rust theme={null}
// 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:

```rust theme={null}
// 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:

```rust theme={null}
// 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:

```rust theme={null}
// 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:

```protobuf theme={null}
email {
  sender_address: "noreply@example.com"
  
  # 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

```rust theme={null}
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`:

```rust theme={null}
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

```protobuf theme={null}
email {
  smtp_host: "smtp.gmail.com"
  smtp_port: 587
  smtp_username: "your-email@gmail.com"
  smtp_password: "your-app-password"  # Use App Password, not account password
  smtp_encryption: STARTTLS
}
```

<Info>
  **Gmail App Passwords**: Generate an app-specific password at [myaccount.google.com/apppasswords](https://myaccount.google.com/apppasswords)
</Info>

### SendGrid

```protobuf theme={null}
email {
  smtp_host: "smtp.sendgrid.net"
  smtp_port: 587
  smtp_username: "apikey"
  smtp_password: "your-sendgrid-api-key"
  smtp_encryption: STARTTLS
}
```

### Mailgun

```protobuf theme={null}
email {
  smtp_host: "smtp.mailgun.org"
  smtp_port: 587
  smtp_username: "postmaster@yourdomain.com"
  smtp_password: "your-mailgun-smtp-password"
  smtp_encryption: STARTTLS
}
```

### AWS SES

```protobuf theme={null}
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

```protobuf theme={null}
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

<Note>
  Direct email sending is not currently available in WASM components. Instead, queue email jobs in the database and process them with a scheduled job.
</Note>

```rust theme={null}
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

```sql theme={null}
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

```rust theme={null}
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(2u64.pow(attempt))).await;
            }
        }
    }
}
```

## Rate Limiting

```rust theme={null}
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

```sql theme={null}
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

```rust theme={null}
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

<Steps>
  <Step title="Use app-specific passwords">
    Never use your main account password for SMTP. Generate app-specific passwords.
  </Step>

  <Step title="Set proper sender info">
    Configure `sender_address` and `sender_name` to avoid spam filters:

    ```protobuf theme={null}
    email {
      sender_address: "noreply@yourdomain.com"
      sender_name: "Your App Name"
    }
    ```
  </Step>

  <Step title="Configure SPF and DKIM">
    Set up email authentication records for your domain to improve deliverability.
  </Step>

  <Step title="Handle bounces">
    Monitor bounce rates and remove invalid addresses from your lists.
  </Step>

  <Step title="Rate limit sends">
    Respect SMTP provider limits. Use queues for bulk emails.
  </Step>

  <Step title="Test in development">
    Use services like [Mailhog](https://github.com/mailhog/MailHog) or [MailCatcher](https://mailcatcher.me/) for testing.
  </Step>
</Steps>

## Testing Emails

### Development Setup

Use Mailhog for local testing:

```bash theme={null}
# Run Mailhog
docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog
```

```protobuf theme={null}
email {
  smtp_host: "localhost"
  smtp_port: 1025
  smtp_encryption: NONE
  sender_address: "dev@localhost"
}
```

View emails at [http://localhost:8025](http://localhost:8025)

## Next Steps

<CardGroup cols={2}>
  <Card title="Jobs Scheduler" href="/advanced/jobs-scheduler" icon="clock">
    Send scheduled emails
  </Card>

  <Card title="Custom Endpoints" href="/advanced/custom-endpoints" icon="route">
    Create email triggers
  </Card>

  <Card title="OAuth Providers" href="/advanced/oauth-providers" icon="shield">
    Configure authentication
  </Card>

  <Card title="Server-Side Rendering" href="/advanced/server-side-rendering" icon="server">
    Generate HTML emails
  </Card>
</CardGroup>
