71b8e14
commit 71b8e14
2 files changed
+89 -0
Showing 2 changed files+89-0
▼crates/justapi-core/src/mail/queue.rs
+42-0
| @@ -0,0 +1,42 @@ | ||
1 | +use tokio::sync::mpsc; | |
2 | + | |
3 | +use super::{EmailMessage, smtp::SmtpSender}; | |
4 | + | |
5 | +/// Background email delivery via a tokio mpsc channel. | |
6 | +pub struct EmailQueue { | |
7 | + sender: mpsc::UnboundedSender<QueuedEmail>, | |
8 | +} | |
9 | + | |
10 | +struct QueuedEmail { | |
11 | + msg: EmailMessage, | |
12 | +} | |
13 | + | |
14 | +impl EmailQueue { | |
15 | + /// Spawn a background worker that processes emails from the queue. | |
16 | + /// The worker receives an SmtpSender. | |
17 | + pub fn spawn(smtp: SmtpSender) -> Self { | |
18 | + let (tx, mut rx) = mpsc::unbounded_channel::<QueuedEmail>(); | |
19 | + | |
20 | + tokio::spawn(async move { | |
21 | + while let Some(queued) = rx.recv().await { | |
22 | + if let Err(e) = smtp.send(&queued.msg).await { | |
23 | + tracing::error!("Failed to send queued email: {}", e); | |
24 | + } | |
25 | + } | |
26 | + }); | |
27 | + | |
28 | + Self { sender: tx } | |
29 | + } | |
30 | + | |
31 | + /// Enqueue an email for background delivery. | |
32 | + pub fn enqueue(&self, msg: EmailMessage) -> Result<(), anyhow::Error> { | |
33 | + self.sender | |
34 | + .send(QueuedEmail { msg }) | |
35 | + .map_err(|_| anyhow::anyhow!("email queue is closed")) | |
36 | + } | |
37 | + | |
38 | + /// Enqueue and return immediately. Errors are logged by the worker. | |
39 | + pub fn enqueue_lossy(&self, msg: EmailMessage) { | |
40 | + let _ = self.enqueue(msg); | |
41 | + } | |
42 | +} | |
0 | 43 | |
▼crates/justapi-core/src/mail/template.rs
+47-0
| @@ -0,0 +1,47 @@ | ||
1 | +use std::collections::HashMap; | |
2 | +use std::sync::Arc; | |
3 | + | |
4 | +use minijinja::Environment; | |
5 | +use tokio::sync::RwLock; | |
6 | + | |
7 | +/// Renders email templates using MiniJinja. | |
8 | +pub struct EmailTemplates { | |
9 | + env: Arc<RwLock<Environment<'static>>>, | |
10 | +} | |
11 | + | |
12 | +impl std::fmt::Debug for EmailTemplates { | |
13 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
14 | + f.debug_struct("EmailTemplates").finish() | |
15 | + } | |
16 | +} | |
17 | + | |
18 | +impl EmailTemplates { | |
19 | + pub fn new() -> Self { | |
20 | + Self { env: Arc::new(RwLock::new(Environment::new())) } | |
21 | + } | |
22 | + | |
23 | + /// Add a template from a string. | |
24 | + pub async fn add_template(&self, name: &str, template: &str) -> Result<(), anyhow::Error> { | |
25 | + let mut env = self.env.write().await; | |
26 | + env.add_template_owned(name.to_string(), template.to_string())?; | |
27 | + Ok(()) | |
28 | + } | |
29 | + | |
30 | + /// Render a named template with the given context. | |
31 | + pub async fn render( | |
32 | + &self, | |
33 | + name: &str, | |
34 | + ctx: &HashMap<String, serde_json::Value>, | |
35 | + ) -> Result<String, anyhow::Error> { | |
36 | + let env = self.env.read().await; | |
37 | + let tmpl = env.get_template(name)?; | |
38 | + let result = tmpl.render(ctx)?; | |
39 | + Ok(result) | |
40 | + } | |
41 | +} | |
42 | + | |
43 | +impl Default for EmailTemplates { | |
44 | + fn default() -> Self { | |
45 | + Self::new() | |
46 | + } | |
47 | +} | |
0 | 48 | |