tuwunel_service/sendmail/
mod.rs1use std::sync::Arc;
2
3use lettre::{
4 Address, AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor,
5 message::{Mailbox, header::ContentType},
6};
7use tuwunel_core::{Err, Result, err, implement};
8
9pub struct Service {
12 transport: Option<Transport>,
13}
14
15struct Transport {
16 smtp: AsyncSmtpTransport<Tokio1Executor>,
17 sender: Mailbox,
18}
19
20impl crate::Service for Service {
21 fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
22 let smtp = &args.server.config.smtp;
23 let transport = smtp
24 .connection_uri
25 .is_some()
26 .then(|| build_transport(smtp))
27 .transpose()?;
28
29 Ok(Arc::new(Self { transport }))
30 }
31
32 fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
33}
34
35#[implement(Service)]
37#[inline]
38#[must_use]
39pub fn is_enabled(&self) -> bool { self.transport.is_some() }
40
41#[implement(Service)]
44#[tracing::instrument(
45 level = "debug",
46 skip(self, subject, body_html),
47 fields(
48 %to,
49 ),
50)]
51pub async fn send(&self, to: &Address, subject: &str, body_html: String) -> Result<()> {
52 let Some(transport) = self.transport.as_ref() else {
53 return Err!(Config("smtp", "The email subsystem is not configured"));
54 };
55
56 let message = Message::builder()
57 .from(transport.sender.clone())
58 .to(Mailbox::new(None, to.clone()))
59 .subject(subject)
60 .header(ContentType::TEXT_HTML)
61 .body(body_html)
62 .map_err(|e| err!(Request(Unknown("Failed to build email message: {e}"))))?;
63
64 transport
65 .smtp
66 .send(message)
67 .await
68 .map_err(|e| err!(Request(Unknown("Failed to send email: {e}"))))?;
69
70 Ok(())
71}
72
73#[implement(Service)]
75pub async fn send_to(&self, to: &str, subject: &str, body_html: String) -> Result<()> {
76 let to: Address = to
77 .parse()
78 .map_err(|_| err!(Request(InvalidParam("Email address is malformed"))))?;
79
80 self.send(&to, subject, body_html).await
81}
82
83#[implement(Service)]
86pub fn check_address(&self, to: &str) -> Result<()> {
87 to.parse::<Address>()
88 .map(|_| ())
89 .map_err(|_| err!(Request(InvalidParam("Email address is malformed"))))
90}
91
92fn build_transport(config: &tuwunel_core::config::SmtpConfig) -> Result<Transport> {
93 let uri = config.connection_uri.as_deref().ok_or_else(|| {
94 err!(Config(
95 "smtp.connection_uri",
96 "An SMTP connection_uri is required to send email"
97 ))
98 })?;
99
100 let sender = config
101 .sender
102 .as_deref()
103 .ok_or_else(|| err!(Config("smtp.sender", "An SMTP sender mailbox is required")))?
104 .parse()
105 .map_err(|e| err!(Config("smtp.sender", "Invalid sender mailbox: {e}")))?;
106
107 let smtp = AsyncSmtpTransport::<Tokio1Executor>::from_url(uri)
108 .map_err(|e| err!(Config("smtp.connection_uri", "Invalid SMTP connection_uri: {e}")))?
109 .build();
110
111 Ok(Transport { smtp, sender })
112}