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