mas_email/
transport.rs

1// Copyright 2024, 2025 New Vector Ltd.
2// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5// Please see LICENSE files in the repository root for full details.
6
7//! Email transport backends
8
9use std::{ffi::OsString, num::NonZeroU16, sync::Arc};
10
11use async_trait::async_trait;
12use lettre::{
13    AsyncTransport, Tokio1Executor,
14    address::Envelope,
15    transport::{
16        sendmail::AsyncSendmailTransport,
17        smtp::{AsyncSmtpTransport, authentication::Credentials},
18    },
19};
20use thiserror::Error;
21
22/// Encryption mode to use
23#[derive(Debug, Clone, Copy)]
24pub enum SmtpMode {
25    /// Plain text
26    Plain,
27    /// `StartTLS` (starts as plain text then upgrade to TLS)
28    StartTls,
29    /// TLS
30    Tls,
31}
32
33/// A wrapper around many [`AsyncTransport`]s
34#[derive(Default, Clone)]
35pub struct Transport {
36    inner: Arc<TransportInner>,
37}
38
39#[derive(Default)]
40enum TransportInner {
41    #[default]
42    Blackhole,
43    Smtp(AsyncSmtpTransport<Tokio1Executor>),
44    Sendmail(AsyncSendmailTransport<Tokio1Executor>),
45}
46
47impl Transport {
48    fn new(inner: TransportInner) -> Self {
49        let inner = Arc::new(inner);
50        Self { inner }
51    }
52
53    /// Construct a blackhole transport
54    #[must_use]
55    pub fn blackhole() -> Self {
56        Self::new(TransportInner::Blackhole)
57    }
58
59    /// Construct a SMTP transport
60    ///
61    /// # Errors
62    ///
63    /// Returns an error if the underlying SMTP transport could not be built
64    pub fn smtp(
65        mode: SmtpMode,
66        hostname: &str,
67        port: Option<NonZeroU16>,
68        credentials: Option<Credentials>,
69    ) -> Result<Self, lettre::transport::smtp::Error> {
70        let mut t = match mode {
71            SmtpMode::Plain => AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(hostname),
72            SmtpMode::StartTls => AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(hostname)?,
73            SmtpMode::Tls => AsyncSmtpTransport::<Tokio1Executor>::relay(hostname)?,
74        };
75
76        if let Some(credentials) = credentials {
77            t = t.credentials(credentials);
78        }
79
80        if let Some(port) = port {
81            t = t.port(port.into());
82        }
83
84        Ok(Self::new(TransportInner::Smtp(t.build())))
85    }
86
87    /// Construct a Sendmail transport
88    #[must_use]
89    pub fn sendmail(command: Option<impl Into<OsString>>) -> Self {
90        let transport = if let Some(command) = command {
91            AsyncSendmailTransport::new_with_command(command)
92        } else {
93            AsyncSendmailTransport::new()
94        };
95        Self::new(TransportInner::Sendmail(transport))
96    }
97}
98
99impl Transport {
100    /// Test the connection to the underlying transport. Only works with the
101    /// SMTP backend for now
102    ///
103    /// # Errors
104    ///
105    /// Will return `Err` if the connection test failed
106    pub async fn test_connection(&self) -> Result<(), Error> {
107        match self.inner.as_ref() {
108            TransportInner::Smtp(t) => {
109                t.test_connection().await?;
110            }
111            TransportInner::Blackhole | TransportInner::Sendmail(_) => {}
112        }
113
114        Ok(())
115    }
116}
117
118#[derive(Debug, Error)]
119#[error(transparent)]
120pub enum Error {
121    Smtp(#[from] lettre::transport::smtp::Error),
122    Sendmail(#[from] lettre::transport::sendmail::Error),
123}
124
125#[async_trait]
126impl AsyncTransport for Transport {
127    type Ok = ();
128    type Error = Error;
129
130    async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
131        match self.inner.as_ref() {
132            TransportInner::Blackhole => {
133                tracing::warn!(
134                    "An email was supposed to be sent but no email backend is configured"
135                );
136            }
137            TransportInner::Smtp(t) => {
138                t.send_raw(envelope, email).await?;
139            }
140            TransportInner::Sendmail(t) => {
141                t.send_raw(envelope, email).await?;
142            }
143        }
144
145        Ok(())
146    }
147}