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