1#![doc(html_root_url = "https://docs.rs/nestrs-mail/1.7.1")]
25
26mod module;
27mod options;
28mod transport;
29
30pub use module::MailModule;
31pub use options::{MailMessage, MailOptions};
32#[cfg(feature = "http")]
33pub use transport::HttpMailTransport;
34pub use transport::{InMemoryMailTransport, MailTransport, SharedMailTransport};
35
36pub use nestrs_core::{DynamicModule, Injectable, ProviderRegistry};
37use std::collections::HashMap;
38use std::sync::Arc;
39
40#[derive(Debug, Clone)]
41pub enum MailError {
42 Transport(String),
43 Permanent(String),
44}
45
46impl std::fmt::Display for MailError {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Self::Transport(m) | Self::Permanent(m) => write!(f, "{m}"),
50 }
51 }
52}
53
54impl std::error::Error for MailError {}
55
56impl MailError {
57 pub fn permanent(&self) -> bool {
58 matches!(self, Self::Permanent(_))
59 }
60}
61
62pub fn render_template(template: &str, data: &HashMap<String, String>) -> String {
64 let mut out = template.to_string();
65 for (k, v) in data {
66 let needle = format!("{{{{{k}}}}}");
67 out = out.replace(&needle, &html_escape::encode_text(v));
68 }
69 out
70}
71
72pub struct MailService {
73 transport: SharedMailTransport,
74}
75
76impl MailService {
77 pub fn from_options(opts: &MailOptions) -> Self {
78 Self {
79 transport: opts.transport.clone(),
80 }
81 }
82
83 pub async fn send(&self, message: MailMessage) -> Result<(), MailError> {
84 self.transport.send(message).await
85 }
86
87 pub fn transport(&self) -> SharedMailTransport {
89 self.transport.clone()
90 }
91}
92
93impl Injectable for MailService {
94 fn construct(_registry: &nestrs_core::ProviderRegistry) -> Arc<Self> {
95 panic!("MailService must be provided by MailModule::register(...)");
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use crate::transport::InMemoryMailTransport;
103
104 #[tokio::test]
105 async fn renders_and_sends_to_memory() {
106 let mem = InMemoryMailTransport::new();
107 let svc = MailService {
108 transport: mem.clone(),
109 };
110 let mut data = HashMap::new();
111 data.insert("name".into(), "<script>".into());
112 let body = render_template("Hello {{name}}", &data);
113 assert!(body.contains("<script>"));
114 svc.send(MailMessage {
115 to: "a@b.com".into(),
116 subject: "Hi".into(),
117 html: body,
118 idempotency_key: Some("k1".into()),
119 })
120 .await
121 .unwrap();
122 let inbox = mem.inbox().await;
123 assert_eq!(inbox.len(), 1);
124 }
125}