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