acton_htmx/email/
builder.rs1use serde::{Deserialize, Serialize};
6
7use super::{EmailError, EmailTemplate};
8
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
24pub struct Email {
25 pub to: Vec<String>,
27
28 pub from: Option<String>,
30
31 pub reply_to: Option<String>,
33
34 pub cc: Vec<String>,
36
37 pub bcc: Vec<String>,
39
40 pub subject: Option<String>,
42
43 pub text: Option<String>,
45
46 pub html: Option<String>,
48
49 pub headers: Vec<(String, String)>,
51}
52
53impl Email {
54 #[must_use]
64 pub fn new() -> Self {
65 Self::default()
66 }
67
68 pub fn from_template<T: EmailTemplate>(template: &T) -> Result<Self, EmailError> {
96 let (html, text) = template.render_email()?;
97
98 let mut email = Self::new();
99 if let Some(html_content) = html {
100 email = email.html(&html_content);
101 }
102 if let Some(text_content) = text {
103 email = email.text(&text_content);
104 }
105
106 Ok(email)
107 }
108
109 #[must_use]
120 pub fn to(mut self, address: &str) -> Self {
121 self.to.push(address.to_string());
122 self
123 }
124
125 #[must_use]
136 pub fn to_multiple(mut self, addresses: &[&str]) -> Self {
137 for address in addresses {
138 self.to.push((*address).to_string());
139 }
140 self
141 }
142
143 #[must_use]
154 pub fn from(mut self, address: &str) -> Self {
155 self.from = Some(address.to_string());
156 self
157 }
158
159 #[must_use]
170 pub fn reply_to(mut self, address: &str) -> Self {
171 self.reply_to = Some(address.to_string());
172 self
173 }
174
175 #[must_use]
186 pub fn cc(mut self, address: &str) -> Self {
187 self.cc.push(address.to_string());
188 self
189 }
190
191 #[must_use]
202 pub fn bcc(mut self, address: &str) -> Self {
203 self.bcc.push(address.to_string());
204 self
205 }
206
207 #[must_use]
218 pub fn subject(mut self, subject: &str) -> Self {
219 self.subject = Some(subject.to_string());
220 self
221 }
222
223 #[must_use]
234 pub fn text(mut self, body: &str) -> Self {
235 self.text = Some(body.to_string());
236 self
237 }
238
239 #[must_use]
250 pub fn html(mut self, body: &str) -> Self {
251 self.html = Some(body.to_string());
252 self
253 }
254
255 #[must_use]
266 pub fn header(mut self, name: &str, value: &str) -> Self {
267 self.headers.push((name.to_string(), value.to_string()));
268 self
269 }
270
271 pub fn validate(&self) -> Result<(), EmailError> {
283 if self.to.is_empty() && self.cc.is_empty() && self.bcc.is_empty() {
284 return Err(EmailError::NoRecipients);
285 }
286
287 if self.from.is_none() {
288 return Err(EmailError::NoSender);
289 }
290
291 if self.subject.is_none() {
292 return Err(EmailError::NoSubject);
293 }
294
295 if self.text.is_none() && self.html.is_none() {
296 return Err(EmailError::NoContent);
297 }
298
299 Ok(())
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
308 fn test_email_builder() {
309 let email = Email::new()
310 .to("user@example.com")
311 .from("noreply@myapp.com")
312 .subject("Test")
313 .text("Hello, World!");
314
315 assert_eq!(email.to, vec!["user@example.com"]);
316 assert_eq!(email.from, Some("noreply@myapp.com".to_string()));
317 assert_eq!(email.subject, Some("Test".to_string()));
318 assert_eq!(email.text, Some("Hello, World!".to_string()));
319 }
320
321 #[test]
322 fn test_email_validation_no_recipients() {
323 let email = Email::new()
324 .from("noreply@myapp.com")
325 .subject("Test")
326 .text("Hello");
327
328 assert!(matches!(email.validate(), Err(EmailError::NoRecipients)));
329 }
330
331 #[test]
332 fn test_email_validation_no_sender() {
333 let email = Email::new()
334 .to("user@example.com")
335 .subject("Test")
336 .text("Hello");
337
338 assert!(matches!(email.validate(), Err(EmailError::NoSender)));
339 }
340
341 #[test]
342 fn test_email_validation_no_subject() {
343 let email = Email::new()
344 .to("user@example.com")
345 .from("noreply@myapp.com")
346 .text("Hello");
347
348 assert!(matches!(email.validate(), Err(EmailError::NoSubject)));
349 }
350
351 #[test]
352 fn test_email_validation_no_content() {
353 let email = Email::new()
354 .to("user@example.com")
355 .from("noreply@myapp.com")
356 .subject("Test");
357
358 assert!(matches!(email.validate(), Err(EmailError::NoContent)));
359 }
360
361 #[test]
362 fn test_email_validation_success() {
363 let email = Email::new()
364 .to("user@example.com")
365 .from("noreply@myapp.com")
366 .subject("Test")
367 .text("Hello, World!");
368
369 assert!(email.validate().is_ok());
370 }
371
372 #[test]
373 fn test_multiple_recipients() {
374 let email = Email::new()
375 .to_multiple(&["user1@example.com", "user2@example.com"])
376 .from("noreply@myapp.com")
377 .subject("Test")
378 .text("Hello");
379
380 assert_eq!(email.to.len(), 2);
381 assert!(email.to.contains(&"user1@example.com".to_string()));
382 assert!(email.to.contains(&"user2@example.com".to_string()));
383 }
384
385 #[test]
386 fn test_cc_and_bcc() {
387 let email = Email::new()
388 .to("user@example.com")
389 .cc("manager@example.com")
390 .bcc("admin@example.com")
391 .from("noreply@myapp.com")
392 .subject("Test")
393 .text("Hello");
394
395 assert_eq!(email.cc, vec!["manager@example.com"]);
396 assert_eq!(email.bcc, vec!["admin@example.com"]);
397 }
398
399 #[test]
400 fn test_custom_headers() {
401 let email = Email::new()
402 .to("user@example.com")
403 .from("noreply@myapp.com")
404 .subject("Test")
405 .text("Hello")
406 .header("X-Priority", "1")
407 .header("X-Custom", "value");
408
409 assert_eq!(email.headers.len(), 2);
410 assert!(email.headers.contains(&("X-Priority".to_string(), "1".to_string())));
411 assert!(email.headers.contains(&("X-Custom".to_string(), "value".to_string())));
412 }
413
414 #[test]
415 fn test_html_and_text() {
416 let email = Email::new()
417 .to("user@example.com")
418 .from("noreply@myapp.com")
419 .subject("Test")
420 .text("Plain text content")
421 .html("<h1>HTML content</h1>");
422
423 assert_eq!(email.text, Some("Plain text content".to_string()));
424 assert_eq!(email.html, Some("<h1>HTML content</h1>".to_string()));
425 }
426}