torii-mailer 0.5.2

Pluggable email system for the torii authentication ecosystem
Documentation
use crate::{MailerError, templates::TemplateData};
use askama::Template;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemplateContext {
    pub app_name: String,
    pub app_url: String,
    pub user_email: Option<String>,
    pub user_name: Option<String>,
}

impl Default for TemplateContext {
    fn default() -> Self {
        Self {
            app_name: "Your App".to_string(),
            app_url: "https://example.com".to_string(),
            user_email: None,
            user_name: None,
        }
    }
}

#[derive(Template)]
#[template(
    source = r#"
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Magic Link - {{ app_name }}</title>
    <style>
        body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 20px; background-color: #f4f4f4; }
        .container { max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        .header { text-align: center; margin-bottom: 30px; }
        .button { display: inline-block; padding: 12px 24px; background-color: #007bff; color: white; text-decoration: none; border-radius: 4px; margin: 20px 0; }
        .footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; font-size: 12px; color: #666; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>{{ app_name }}</h1>
        </div>
        
        <h2>Sign In to Your Account</h2>
        
        <p>{% if let Some(name) = user_name %}Hello {{ name }},{% else %}Hello,{% endif %}</p>
        
        <p>Click the button below to sign in to your account. This link will expire in 15 minutes for security.</p>
        
        <div style="text-align: center;">
            <a href="{{ magic_link }}" class="button">Sign In</a>
        </div>
        
        <p>Or copy and paste this URL into your browser:</p>
        <p style="word-break: break-all; background: #f8f9fa; padding: 10px; border-radius: 4px; font-family: monospace;">{{ magic_link }}</p>
        
        <p>If you didn't request this email, you can safely ignore it.</p>
        
        <div class="footer">
            <p>This email was sent by {{ app_name }}. If you have any questions, please contact our support team.</p>
        </div>
    </div>
</body>
</html>
"#,
    ext = "html"
)]
pub struct MagicLinkTemplate {
    pub app_name: String,
    pub app_url: String,
    pub user_name: Option<String>,
    pub magic_link: String,
}

impl MagicLinkTemplate {
    pub fn from_data(data: TemplateData) -> Result<Self, MailerError> {
        let context: TemplateContext = data
            .get("context")
            .and_then(|v| serde_json::from_value(v.clone()).ok())
            .unwrap_or_default();

        let magic_link = data
            .get("magic_link")
            .and_then(|v| v.as_str())
            .ok_or_else(|| MailerError::Builder("magic_link is required".to_string()))?
            .to_string();

        Ok(Self {
            app_name: context.app_name,
            app_url: context.app_url,
            user_name: context.user_name,
            magic_link,
        })
    }
}

#[derive(Template)]
#[template(
    source = r#"
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Welcome - {{ app_name }}</title>
    <style>
        body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 20px; background-color: #f4f4f4; }
        .container { max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        .header { text-align: center; margin-bottom: 30px; }
        .button { display: inline-block; padding: 12px 24px; background-color: #28a745; color: white; text-decoration: none; border-radius: 4px; margin: 20px 0; }
        .footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; font-size: 12px; color: #666; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>{{ app_name }}</h1>
        </div>
        
        <h2>Welcome to {{ app_name }}!</h2>
        
        <p>{% if let Some(name) = user_name %}Hello {{ name }},{% else %}Hello,{% endif %}</p>
        
        <p>We're excited to have you on board! Your account has been successfully created and you're ready to get started.</p>
        
        <div style="text-align: center;">
            <a href="{{ app_url }}" class="button">Get Started</a>
        </div>
        
        <p>Here are a few things you can do to get started:</p>
        <ul>
            <li>Complete your profile</li>
            <li>Explore the features</li>
            <li>Connect with other users</li>
        </ul>
        
        <p>If you have any questions, don't hesitate to reach out to our support team.</p>
        
        <div class="footer">
            <p>Thanks for joining {{ app_name }}!</p>
        </div>
    </div>
</body>
</html>
"#,
    ext = "html"
)]
pub struct WelcomeTemplate {
    pub app_name: String,
    pub app_url: String,
    pub user_name: Option<String>,
}

impl WelcomeTemplate {
    pub fn from_data(data: TemplateData) -> Result<Self, MailerError> {
        let context: TemplateContext = data
            .get("context")
            .and_then(|v| serde_json::from_value(v.clone()).ok())
            .unwrap_or_default();

        Ok(Self {
            app_name: context.app_name,
            app_url: context.app_url,
            user_name: context.user_name,
        })
    }
}

#[derive(Template)]
#[template(
    source = r#"
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Reset Your Password - {{ app_name }}</title>
    <style>
        body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 20px; background-color: #f4f4f4; }
        .container { max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        .header { text-align: center; margin-bottom: 30px; }
        .button { display: inline-block; padding: 12px 24px; background-color: #dc3545; color: white; text-decoration: none; border-radius: 4px; margin: 20px 0; }
        .footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; font-size: 12px; color: #666; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>{{ app_name }}</h1>
        </div>
        
        <h2>Reset Your Password</h2>
        
        <p>{% if let Some(name) = user_name %}Hello {{ name }},{% else %}Hello,{% endif %}</p>
        
        <p>We received a request to reset your password. Click the button below to create a new password:</p>
        
        <div style="text-align: center;">
            <a href="{{ reset_link }}" class="button">Reset Password</a>
        </div>
        
        <p>Or copy and paste this URL into your browser:</p>
        <p style="word-break: break-all; background: #f8f9fa; padding: 10px; border-radius: 4px; font-family: monospace;">{{ reset_link }}</p>
        
        <p>This link will expire in 1 hour for security reasons.</p>
        
        <p>If you didn't request a password reset, you can safely ignore this email. Your password will not be changed.</p>
        
        <div class="footer">
            <p>For security, this request was made from {{ app_name }}. If you have any concerns, please contact our support team.</p>
        </div>
    </div>
</body>
</html>
"#,
    ext = "html"
)]
pub struct PasswordResetTemplate {
    pub app_name: String,
    pub app_url: String,
    pub user_name: Option<String>,
    pub reset_link: String,
}

impl PasswordResetTemplate {
    pub fn from_data(data: TemplateData) -> Result<Self, MailerError> {
        let context: TemplateContext = data
            .get("context")
            .and_then(|v| serde_json::from_value(v.clone()).ok())
            .unwrap_or_default();

        let reset_link = data
            .get("reset_link")
            .and_then(|v| v.as_str())
            .ok_or_else(|| MailerError::Builder("reset_link is required".to_string()))?
            .to_string();

        Ok(Self {
            app_name: context.app_name,
            app_url: context.app_url,
            user_name: context.user_name,
            reset_link,
        })
    }
}

#[derive(Template)]
#[template(
    source = r#"
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Password Changed - {{ app_name }}</title>
    <style>
        body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 20px; background-color: #f4f4f4; }
        .container { max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        .header { text-align: center; margin-bottom: 30px; }
        .alert { background-color: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 15px; border-radius: 4px; margin: 20px 0; }
        .footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; font-size: 12px; color: #666; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>{{ app_name }}</h1>
        </div>
        
        <h2>Password Successfully Changed</h2>
        
        <p>{% if let Some(name) = user_name %}Hello {{ name }},{% else %}Hello,{% endif %}</p>
        
        <div class="alert">
            <strong>Your password has been successfully changed.</strong>
        </div>
        
        <p>This is a confirmation that your account password was recently changed. If you made this change, no further action is required.</p>
        
        <p><strong>If you did not make this change:</strong></p>
        <ul>
            <li>Your account may have been compromised</li>
            <li>Please contact our support team immediately</li>
            <li>Consider reviewing your recent account activity</li>
        </ul>
        
        <p>For your security, we recommend:</p>
        <ul>
            <li>Using a strong, unique password</li>
            <li>Enabling two-factor authentication if available</li>
            <li>Regularly reviewing your account activity</li>
        </ul>
        
        <div class="footer">
            <p>This notification was sent for your security by {{ app_name }}. If you have any concerns, please contact our support team immediately.</p>
        </div>
    </div>
</body>
</html>
"#,
    ext = "html"
)]
pub struct PasswordChangedTemplate {
    pub app_name: String,
    pub app_url: String,
    pub user_name: Option<String>,
}

impl PasswordChangedTemplate {
    pub fn from_data(data: TemplateData) -> Result<Self, MailerError> {
        let context: TemplateContext = data
            .get("context")
            .and_then(|v| serde_json::from_value(v.clone()).ok())
            .unwrap_or_default();

        Ok(Self {
            app_name: context.app_name,
            app_url: context.app_url,
            user_name: context.user_name,
        })
    }
}