1use crate::prelude::*;
8use crate::EmailMessage;
9use async_trait::async_trait;
10use cloudillo_core::scheduler::Task;
11use serde::{Deserialize, Serialize};
12use std::fmt::Debug;
13use std::sync::Arc;
14
15pub type TaskId = u64;
16
17#[derive(Clone, Debug, Serialize, Deserialize)]
23pub struct EmailSenderTask {
24 pub tn_id: TnId,
25 pub to: String,
26 #[serde(default)]
28 pub subject: Option<String>,
29 pub template_name: String,
30 pub template_vars: serde_json::Value,
31 #[serde(default)]
33 pub lang: Option<String>,
34 #[serde(default)]
36 pub from_name_override: Option<String>,
37}
38
39impl EmailSenderTask {
40 pub fn new(
42 tn_id: TnId,
43 to: String,
44 subject: Option<String>,
45 template_name: String,
46 template_vars: serde_json::Value,
47 lang: Option<String>,
48 from_name_override: Option<String>,
49 ) -> Self {
50 Self { tn_id, to, subject, template_name, template_vars, lang, from_name_override }
51 }
52}
53
54#[async_trait]
55impl Task<App> for EmailSenderTask {
56 fn kind() -> &'static str {
57 "email.send"
58 }
59
60 fn kind_of(&self) -> &'static str {
61 Self::kind()
62 }
63
64 fn build(_id: TaskId, context: &str) -> ClResult<Arc<dyn Task<App>>> {
65 let task: EmailSenderTask = serde_json::from_str(context).map_err(|e| {
67 Error::ValidationError(format!("Failed to deserialize email task: {}", e))
68 })?;
69 Ok(Arc::new(task))
70 }
71
72 fn serialize(&self) -> String {
73 serde_json::to_string(self)
74 .unwrap_or_else(|_| format!("email.send:{}:{}", self.tn_id.0, self.to))
75 }
76
77 async fn run(&self, app: &App) -> ClResult<()> {
78 info!(
79 "Executing email task for {} (template: {}, lang: {:?})",
80 self.to, self.template_name, self.lang
81 );
82
83 let email_module = app.ext::<Arc<crate::EmailModule>>()?;
84
85 let render_result = email_module
87 .template_engine
88 .render(self.tn_id, &self.template_name, &self.template_vars, self.lang.as_deref())
89 .await?;
90
91 let subject = self.subject.clone().or(render_result.subject).ok_or_else(|| {
93 Error::ConfigError(format!(
94 "No subject provided and template '{}' has no subject in frontmatter",
95 self.template_name
96 ))
97 })?;
98
99 let message = EmailMessage {
101 to: self.to.clone(),
102 subject,
103 text_body: render_result.text_body,
104 html_body: Some(render_result.html_body),
105 from_name_override: self.from_name_override.clone(),
106 };
107
108 email_module.send_now(self.tn_id, message).await?;
110
111 info!("Email task completed for {}", self.to);
112 Ok(())
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn test_email_task_creation() {
122 let vars = serde_json::json!({
123 "user_name": "Alice",
124 "instance_name": "Cloudillo",
125 });
126
127 let task = EmailSenderTask::new(
128 TnId(1),
129 "user@example.com".to_string(),
130 Some("Test Email".to_string()),
131 "welcome".to_string(),
132 vars.clone(),
133 None,
134 None,
135 );
136
137 assert_eq!(task.tn_id.0, 1);
138 assert_eq!(task.to, "user@example.com");
139 assert_eq!(task.subject, Some("Test Email".to_string()));
140 assert_eq!(task.template_name, "welcome");
141 assert_eq!(task.template_vars, vars);
142 assert_eq!(task.lang, None);
143 }
144
145 #[test]
146 fn test_email_task_with_lang() {
147 let vars = serde_json::json!({
148 "user_name": "Béla",
149 });
150
151 let task = EmailSenderTask::new(
152 TnId(1),
153 "user@example.com".to_string(),
154 None, "welcome".to_string(),
156 vars.clone(),
157 Some("hu".to_string()),
158 None,
159 );
160
161 assert_eq!(task.lang, Some("hu".to_string()));
162 assert!(task.subject.is_none());
163 }
164
165 #[test]
166 fn test_email_task_serialization() {
167 let vars = serde_json::json!({
168 "user_name": "Bob",
169 });
170
171 let task = EmailSenderTask::new(
172 TnId(1),
173 "user@example.com".to_string(),
174 Some("Test".to_string()),
175 "notification".to_string(),
176 vars,
177 Some("de".to_string()),
178 None,
179 );
180
181 let serialized = cloudillo_core::scheduler::Task::serialize(&task);
183
184 assert!(serialized.contains("user@example.com"));
186 assert!(serialized.contains("notification"));
187 assert!(serialized.contains("de"));
188 let deserialized: Result<EmailSenderTask, _> = serde_json::from_str(&serialized);
189 assert!(deserialized.is_ok());
190 }
191
192 #[test]
193 fn test_email_task_kind() {
194 assert_eq!(EmailSenderTask::kind(), "email.send");
195
196 let vars = serde_json::json!({});
197 let task = EmailSenderTask::new(
198 TnId(1),
199 "test@example.com".to_string(),
200 Some("Test".to_string()),
201 "test".to_string(),
202 vars,
203 None,
204 None,
205 );
206 assert_eq!(task.kind_of(), "email.send");
207 }
208
209 #[test]
210 fn test_email_task_template_vars() {
211 let vars = serde_json::json!({
212 "user_name": "Charlie",
213 "instance_name": "Cloudillo",
214 "welcome_link": "https://example.com/welcome",
215 });
216
217 let task = EmailSenderTask::new(
218 TnId(1),
219 "user@example.com".to_string(),
220 None, "welcome".to_string(),
222 vars.clone(),
223 None,
224 None,
225 );
226
227 assert_eq!(task.template_name, "welcome");
228 assert_eq!(task.template_vars["user_name"], "Charlie");
229 assert_eq!(task.template_vars["instance_name"], "Cloudillo");
230 assert_eq!(task.template_vars["welcome_link"], "https://example.com/welcome");
231 }
232
233 #[test]
234 fn test_email_task_backward_compat() {
235 let json = r#"{
237 "tn_id": 1,
238 "to": "user@example.com",
239 "subject": "Test",
240 "template_name": "test",
241 "template_vars": {}
242 }"#;
243
244 let task: Result<EmailSenderTask, _> = serde_json::from_str(json);
245 assert!(task.is_ok());
246 let task = task.unwrap();
247 assert!(task.lang.is_none());
248 assert_eq!(task.subject, Some("Test".to_string()));
250 }
251}
252
253