Jesús Pérex 2f0f807331 feat: add dark mode functionality and improve navigation system
- Add complete dark mode system with theme context and toggle
- Implement dark mode toggle component in navigation menu
- Add client-side routing with SSR-safe signal handling
- Fix language selector styling for better dark mode compatibility
- Add documentation system with mdBook integration
- Improve navigation menu with proper external/internal link handling
- Add comprehensive project documentation and configuration
- Enhance theme system with localStorage persistence
- Fix arena panic issues during server-side rendering
- Add proper TypeScript configuration and build optimizations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-11 20:53:20 +01:00

18 KiB

Email System

RUSTELO

This guide covers RUSTELO's comprehensive email system, including setup, configuration, usage, and best practices for integrating email functionality into your application.

Overview

The RUSTELO email system provides a complete solution for sending emails from your web application with support for multiple providers, template-based emails, form submissions, and both server-side and client-side integration.

Architecture

  • Email Service: Core service that handles email sending
  • Providers: Pluggable email providers (SMTP, SendGrid, Console)
  • Templates: Handlebars-based email templates
  • Forms: Ready-to-use contact and support form components
  • API: REST endpoints for email operations

Features

Multiple Providers

  • SMTP (Gmail, Outlook, custom servers)
  • SendGrid API
  • Console output (development)

📧 Template System

  • Handlebars templates
  • HTML and text versions
  • Custom helpers
  • Variable substitution

🔧 Form Integration

  • Contact forms
  • Support forms with priorities
  • Custom form handling

🛡️ Security

  • Input validation
  • Rate limiting
  • CSRF protection
  • Secure configuration

🎨 Rich Components

  • React/Leptos form components
  • Real-time validation
  • Error handling
  • Success feedback

Quick Start

1. Enable Email Feature

Make sure the email feature is enabled in your Cargo.toml:

[features]
default = ["email"]
email = ["lettre", "handlebars", "urlencoding"]

2. Basic Configuration

Add email configuration to your config.toml:

[email]
enabled = true
provider = "console"  # Start with console for development
from_email = "noreply@yourapp.com"
from_name = "Your App"
template_dir = "templates/email"

3. Create Template Directory

mkdir -p templates/email/html
mkdir -p templates/email/text

4. Start Using

// Send a simple email
let result = email_service.send_simple_email(
    "user@example.com",
    "Welcome!",
    "Thank you for signing up!"
).await?;

// Send a contact form
let result = email_service.send_contact_form(
    "John Doe",
    "john@example.com",
    "Question about pricing",
    "I'd like to know more about your pricing plans.",
    "admin@yourapp.com"
).await?;

Configuration

Email Configuration Options

[email]
# Basic settings
enabled = true
provider = "smtp"  # "smtp", "sendgrid", "console"
from_email = "noreply@yourapp.com"
from_name = "Your App Name"
template_dir = "templates/email"

# SMTP settings (when provider = "smtp")
smtp_host = "smtp.gmail.com"
smtp_port = 587
smtp_username = "your-email@gmail.com"
smtp_password = "@encrypted_smtp_password"
smtp_use_tls = false
smtp_use_starttls = true

# SendGrid settings (when provider = "sendgrid")
sendgrid_api_key = "@encrypted_sendgrid_api_key"
sendgrid_endpoint = "https://api.sendgrid.com/v3/mail/send"

Environment-Specific Configuration

# Development
[environments.development]
email.provider = "console"
email.enabled = true

# Production
[environments.production]
email.provider = "sendgrid"
email.sendgrid_api_key = "@encrypted_sendgrid_api_key"
email.enabled = true

Email Providers

Console Provider

Perfect for development and testing. Prints emails to the console.

[email]
provider = "console"

Features:

  • No external dependencies
  • Immediate feedback
  • Safe for development

SMTP Provider

Use any SMTP server including Gmail, Outlook, or custom servers.

[email]
provider = "smtp"
smtp_host = "smtp.gmail.com"
smtp_port = 587
smtp_username = "your-email@gmail.com"
smtp_password = "@encrypted_smtp_password"
smtp_use_starttls = true

Common SMTP Configurations:

Gmail:

smtp_host = "smtp.gmail.com"
smtp_port = 587
smtp_use_starttls = true
# Requires App Password (not regular password)

Outlook:

smtp_host = "smtp-mail.outlook.com"
smtp_port = 587
smtp_use_starttls = true

Custom SMTP:

smtp_host = "mail.yourserver.com"
smtp_port = 587
smtp_use_starttls = true

SendGrid Provider

Use SendGrid's API for reliable email delivery.

[email]
provider = "sendgrid"
sendgrid_api_key = "@encrypted_sendgrid_api_key"

Features:

  • High deliverability
  • Built-in analytics
  • Bounce handling
  • Reliable service

Templates

Template Structure

templates/email/
├── html/
│   ├── contact.hbs
│   ├── support.hbs
│   ├── welcome.hbs
│   └── notification.hbs
└── text/
    ├── contact.hbs
    ├── support.hbs
    ├── welcome.hbs
    └── notification.hbs

Template Naming

  • contact.hbs - Contact form emails
  • support.hbs - Support form emails
  • welcome.hbs - Welcome/registration emails
  • notification.hbs - General notifications

Handlebars Helpers

Built-in helpers for common email tasks:

  • {{format_date}} - Format dates
  • {{format_currency}} - Format money
  • {{upper}} - Uppercase text
  • {{lower}} - Lowercase text
  • {{capitalize}} - Capitalize words

Example Template

templates/email/html/contact.hbs:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Contact Form Submission</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .header { background-color: #f8f9fa; padding: 20px; border-radius: 8px; }
        .content { margin: 20px 0; }
        .footer { color: #666; font-size: 12px; }
    </style>
</head>
<body>
    <div class="header">
        <h1>New Contact Form Submission</h1>
    </div>
    
    <div class="content">
        <p><strong>Name:</strong> {{name}}</p>
        <p><strong>Email:</strong> {{email}}</p>
        <p><strong>Subject:</strong> {{subject}}</p>
        <p><strong>Message:</strong></p>
        <p>{{message}}</p>
    </div>
    
    <div class="footer">
        <p>Sent at {{format_date timestamp}}</p>
    </div>
</body>
</html>

templates/email/text/contact.hbs:

New Contact Form Submission

Name: {{name}}
Email: {{email}}
Subject: {{subject}}

Message:
{{message}}

Sent at {{format_date timestamp}}

API Endpoints

GET /api/email/status

Check email system status.

Response:

{
  "enabled": true,
  "provider": "smtp",
  "configured": true,
  "templates": ["contact", "support", "welcome", "notification"]
}

POST /api/email/contact

Send a contact form email.

Request:

{
  "name": "John Doe",
  "email": "john@example.com",
  "subject": "Question about pricing",
  "message": "I'd like to know more about your pricing plans.",
  "recipient": "admin@yourapp.com"
}

Response:

{
  "message": "Email sent successfully",
  "message_id": "abc123def456",
  "status": "sent"
}

POST /api/email/support

Send a support form email with priority.

Request:

{
  "name": "Jane Smith",
  "email": "jane@example.com",
  "subject": "Technical Issue",
  "message": "Having trouble with login functionality.",
  "priority": "high",
  "category": "technical",
  "recipient": "support@yourapp.com"
}

POST /api/email/send

Send a template-based email.

Request:

{
  "to": "user@example.com",
  "subject": "Welcome to Our Platform",
  "template": "welcome",
  "template_data": {
    "user_name": "John Doe",
    "activation_link": "https://yourapp.com/activate/token123"
  }
}

POST /api/email/notification

Send a notification email.

Request:

{
  "to": "user@example.com",
  "title": "Important Update",
  "message": "Your account has been updated successfully.",
  "content": "Additional details about the update..."
}

Client Components

ContactForm Component

#[component]
pub fn ContactForm() -> impl IntoView {
    let (form_data, set_form_data) = create_signal(ContactFormData::default());
    let (is_submitting, set_is_submitting) = create_signal(false);
    let (message, set_message) = create_signal(String::new());

    let submit_form = create_action(move |data: &ContactFormData| {
        let data = data.clone();
        async move {
            set_is_submitting(true);
            let result = send_contact_form(data).await;
            set_is_submitting(false);
            match result {
                Ok(_) => set_message("Thank you for your message! We'll get back to you soon.".to_string()),
                Err(e) => set_message(format!("Error sending message: {}", e)),
            }
        }
    });

    view! {
        <div class="contact-form">
            <h2>"Contact Us"</h2>
            <form on:submit=move |ev| {
                ev.prevent_default();
                submit_form.dispatch(form_data.get());
            }>
                <div class="form-group">
                    <label for="name">"Name"</label>
                    <input
                        type="text"
                        id="name"
                        required
                        on:input=move |ev| {
                            set_form_data.update(|data| data.name = event_target_value(&ev));
                        }
                    />
                </div>
                
                <div class="form-group">
                    <label for="email">"Email"</label>
                    <input
                        type="email"
                        id="email"
                        required
                        on:input=move |ev| {
                            set_form_data.update(|data| data.email = event_target_value(&ev));
                        }
                    />
                </div>
                
                <div class="form-group">
                    <label for="subject">"Subject"</label>
                    <input
                        type="text"
                        id="subject"
                        required
                        on:input=move |ev| {
                            set_form_data.update(|data| data.subject = event_target_value(&ev));
                        }
                    />
                </div>
                
                <div class="form-group">
                    <label for="message">"Message"</label>
                    <textarea
                        id="message"
                        required
                        on:input=move |ev| {
                            set_form_data.update(|data| data.message = event_target_value(&ev));
                        }
                    ></textarea>
                </div>
                
                <button type="submit" disabled=move || is_submitting.get()>
                    {move || if is_submitting.get() { "Sending..." } else { "Send Message" }}
                </button>
            </form>
            
            <Show when=move || !message.get().is_empty()>
                <div class="message">{move || message.get()}</div>
            </Show>
        </div>
    }
}

SupportForm Component

#[component]
pub fn SupportForm() -> impl IntoView {
    let (form_data, set_form_data) = create_signal(SupportFormData::default());
    let (is_submitting, set_is_submitting) = create_signal(false);
    let (message, set_message) = create_signal(String::new());

    let submit_form = create_action(move |data: &SupportFormData| {
        let data = data.clone();
        async move {
            set_is_submitting(true);
            let result = send_support_form(data).await;
            set_is_submitting(false);
            match result {
                Ok(_) => set_message("Support ticket submitted successfully!".to_string()),
                Err(e) => set_message(format!("Error submitting ticket: {}", e)),
            }
        }
    });

    view! {
        <div class="support-form">
            <h2>"Support Request"</h2>
            <form on:submit=move |ev| {
                ev.prevent_default();
                submit_form.dispatch(form_data.get());
            }>
                // Similar form fields with priority selector
                <div class="form-group">
                    <label for="priority">"Priority"</label>
                    <select
                        id="priority"
                        on:change=move |ev| {
                            set_form_data.update(|data| data.priority = event_target_value(&ev));
                        }
                    >
                        <option value="low">"Low"</option>
                        <option value="medium">"Medium"</option>
                        <option value="high">"High"</option>
                        <option value="urgent">"Urgent"</option>
                    </select>
                </div>
                
                <button type="submit" disabled=move || is_submitting.get()>
                    {move || if is_submitting.get() { "Submitting..." } else { "Submit Ticket" }}
                </button>
            </form>
        </div>
    }
}

Server Usage

Basic Email Sending

use server::email::{EmailService, EmailConfig};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = EmailConfig::from_file("config.toml")?;
    let email_service = EmailService::new(config).await?;
    
    // Send simple email
    email_service.send_simple_email(
        "user@example.com",
        "Welcome!",
        "Thank you for signing up!"
    ).await?;
    
    Ok(())
}

Template-Based Emails

use std::collections::HashMap;

#[server(SendWelcomeEmail, "/api/email/welcome")]
pub async fn send_welcome_email(
    email: String,
    name: String,
    activation_token: String,
) -> Result<String, ServerFnError> {
    let email_service = get_email_service().await?;
    
    let mut template_data = HashMap::new();
    template_data.insert("user_name".to_string(), name);
    template_data.insert("activation_link".to_string(), 
        format!("https://yourapp.com/activate/{}", activation_token));
    
    email_service.send_template_email(
        &email,
        "Welcome to Our Platform",
        "welcome",
        template_data
    ).await?;
    
    Ok("Welcome email sent successfully".to_string())
}

Form Handling

#[server(HandleContactForm, "/api/email/contact")]
pub async fn handle_contact_form(
    name: String,
    email: String,
    subject: String,
    message: String,
) -> Result<String, ServerFnError> {
    let email_service = get_email_service().await?;
    
    // Validate input
    if name.is_empty() || email.is_empty() || message.is_empty() {
        return Err(ServerFnError::ServerError("All fields are required".to_string()));
    }
    
    // Send email
    email_service.send_contact_form(
        &name,
        &email,
        &subject,
        &message,
        "admin@yourapp.com"
    ).await?;
    
    Ok("Contact form submitted successfully".to_string())
}

Environment Variables

Common Variables

# SMTP Configuration
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=your-email@gmail.com
SMTP_PASSWORD=your-app-password
SMTP_USE_STARTTLS=true

# SendGrid Configuration
SENDGRID_API_KEY=your-sendgrid-api-key

# Email Settings
EMAIL_FROM=noreply@yourapp.com
EMAIL_FROM_NAME="Your App"

Using in Configuration

[email]
smtp_username = "${SMTP_USERNAME}"
smtp_password = "@encrypted_smtp_password"
sendgrid_api_key = "@encrypted_sendgrid_api_key"
from_email = "${EMAIL_FROM}"

Security Considerations

1. Credential Management

Always encrypt sensitive credentials:

# Encrypt SMTP password
cargo run --bin config_crypto_tool encrypt "your-smtp-password"

# Encrypt SendGrid API key
cargo run --bin config_crypto_tool encrypt "your-sendgrid-api-key"

2. Input Validation

Always validate email inputs:

use validator::{Validate, ValidationError};

#[derive(Validate)]
struct ContactFormData {
    #[validate(length(min = 1, message = "Name is required"))]
    name: String,
    
    #[validate(email(message = "Invalid email address"))]
    email: String,
    
    #[validate(length(min = 1, max = 1000, message = "Message must be between 1 and 1000 characters"))]
    message: String,
}

3. Rate Limiting

Implement rate limiting for email endpoints:

use tower_governor::{governor::GovernorLayer, GovernorConfigBuilder};

// Limit to 5 emails per minute per IP
let governor_config = GovernorConfigBuilder::default()
    .per_minute(5)
    .burst_size(2)
    .finish()
    .unwrap();

let governor_layer = GovernorLayer {
    config: Arc::new(governor_config),
};

4. CSRF Protection

Enable CSRF protection for email forms:

use axum_csrf::{CsrfConfig, CsrfLayer};

let csrf_config = CsrfConfig::default();
let csrf_layer = CsrfLayer::new(csrf_config);

Troubleshooting

Common Issues

Email not sending:

  • Check provider configuration
  • Verify credentials
  • Check network connectivity
  • Review email service logs

Template not found:

  • Verify template directory path
  • Check template file naming
  • Ensure HTML and text versions exist

Authentication failed:

  • For Gmail: Use App Password, not regular password
  • For other providers: Check username/password
  • Verify server and port settings

Rate limiting:

  • Check provider limits
  • Implement proper rate limiting
  • Consider using queues for bulk emails

Debug Mode

Enable debug logging to troubleshoot issues:

[logging]
level = "debug"

[email]
enabled = true
debug = true

Best Practices

  1. Use encrypted configuration for sensitive credentials
  2. Implement proper validation for all email inputs
  3. Use rate limiting to prevent abuse
  4. Provide both HTML and text versions of templates
  5. Test with console provider during development
  6. Monitor email delivery in production
  7. Handle errors gracefully with user-friendly messages
  8. Use meaningful subject lines and sender names

Next Steps

The email system provides a robust foundation for all your application's communication needs while maintaining security and reliability.