Skip to main content

cloudillo_email/
lib.rs

1//! Email notification system with templates and SMTP integration
2//!
3//! This module provides:
4//! - Template rendering with variable substitution (Handlebars)
5//! - SMTP email sending with lettre
6//! - Email sender task for async/persistent sending via scheduler
7//! - Configuration via global settings module
8
9#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
10#![forbid(unsafe_code)]
11
12pub mod sender;
13pub mod settings;
14pub mod task;
15pub mod template;
16
17pub use sender::EmailSender;
18pub use task::EmailSenderTask;
19pub use template::TemplateEngine;
20
21mod prelude;
22
23use crate::prelude::*;
24use cloudillo_core::settings::service::SettingsService;
25use serde::{Deserialize, Serialize};
26use std::sync::Arc;
27
28/// Email message to be sent
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct EmailMessage {
31	pub to: String,
32	pub subject: String,
33	pub text_body: String,
34	pub html_body: Option<String>,
35	/// Optional sender name override (e.g., "Cloudillo (myinstance)")
36	#[serde(default)]
37	pub from_name_override: Option<String>,
38}
39
40/// Email task parameters
41#[derive(Debug, Clone)]
42pub struct EmailTaskParams {
43	pub to: String,
44	/// Optional subject - if None, will be extracted from template frontmatter
45	pub subject: Option<String>,
46	pub template_name: String,
47	pub template_vars: serde_json::Value,
48	/// Optional language code for localized templates (e.g., "hu", "de")
49	pub lang: Option<String>,
50	pub custom_key: Option<String>,
51	/// Optional sender name override (e.g., "Cloudillo (myinstance)" or identity_provider)
52	pub from_name_override: Option<String>,
53}
54
55/// Email module - main orchestrator for email operations
56pub struct EmailModule {
57	pub settings_service: Arc<SettingsService>,
58	pub template_engine: Arc<TemplateEngine>,
59	pub sender: Arc<EmailSender>,
60}
61
62impl EmailModule {
63	pub fn new(settings_service: Arc<SettingsService>) -> ClResult<Self> {
64		let template_engine = Arc::new(TemplateEngine::new(settings_service.clone())?);
65		let sender = Arc::new(EmailSender::new(settings_service.clone())?);
66
67		Ok(Self { settings_service, template_engine, sender })
68	}
69
70	/// Schedule email for async sending via task system with automatic retries
71	///
72	/// Uses the scheduler's built-in RetryPolicy with exponential backoff.
73	/// Retry configuration is loaded from settings (email.retry_attempts).
74	///
75	/// Template is rendered at execution time, not scheduling time.
76	pub async fn schedule_email_task(
77		scheduler: &cloudillo_core::scheduler::Scheduler<App>,
78		settings_service: &cloudillo_core::settings::service::SettingsService,
79		tn_id: TnId,
80		params: EmailTaskParams,
81	) -> ClResult<()> {
82		Self::schedule_email_task_with_key(scheduler, settings_service, tn_id, params).await
83	}
84
85	/// Schedule email task with optional custom key for deduplication
86	pub async fn schedule_email_task_with_key(
87		scheduler: &cloudillo_core::scheduler::Scheduler<App>,
88		settings_service: &cloudillo_core::settings::service::SettingsService,
89		tn_id: TnId,
90		params: EmailTaskParams,
91	) -> ClResult<()> {
92		// Get max retry attempts from settings (default: 3)
93		let max_retries = match settings_service.get(tn_id, "email.retry_attempts").await {
94			Ok(cloudillo_core::settings::SettingValue::Int(n)) => n as u16,
95			_ => 3,
96		};
97
98		// Create RetryPolicy with exponential backoff
99		// - Backoff: min=60s, max=3600s (1 hour)
100		// - Formula: 60 * 2^attempt, capped at 3600s
101		// - Attempts: 60s, 120s, 240s, 480s, 960s, 1800s, 3600s...
102		let retry_policy = cloudillo_core::scheduler::RetryPolicy::new((60, 3600), max_retries);
103
104		let task = EmailSenderTask::new(
105			tn_id,
106			params.to.clone(),
107			params.subject,
108			params.template_name,
109			params.template_vars,
110			params.lang,
111			params.from_name_override,
112		);
113		let task_key =
114			params.custom_key.unwrap_or_else(|| format!("email:{}:{}", tn_id.0, params.to));
115
116		scheduler
117			.task(std::sync::Arc::new(task))
118			.key(task_key)
119			.with_retry(retry_policy)
120			.schedule()
121			.await?;
122		info!("Email task scheduled for {} with {} retry attempts", params.to, max_retries);
123		Ok(())
124	}
125
126	/// Send email immediately (bypass scheduler)
127	pub async fn send_now(&self, tn_id: TnId, message: EmailMessage) -> ClResult<()> {
128		self.sender.send(tn_id, message).await
129	}
130}
131
132pub fn register_settings(
133	registry: &mut cloudillo_core::settings::SettingsRegistry,
134) -> ClResult<()> {
135	settings::register_settings(registry)
136}
137
138/// Initialize email module (register tasks with scheduler)
139pub fn init(app: &App) -> ClResult<()> {
140	app.scheduler.register::<EmailSenderTask>()?;
141	Ok(())
142}
143
144/// Get tenant's preferred language from settings
145///
146/// Returns None if no language preference is set.
147pub async fn get_tenant_lang(settings: &SettingsService, tn_id: TnId) -> Option<String> {
148	match settings.get(tn_id, "profile.lang").await {
149		Ok(cloudillo_core::settings::SettingValue::String(lang)) => Some(lang),
150		_ => None,
151	}
152}
153
154// vim: ts=4