mas_email/
transport.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE 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
39enum TransportInner {
40    Blackhole,
41    Smtp(AsyncSmtpTransport<Tokio1Executor>),
42    Sendmail(AsyncSendmailTransport<Tokio1Executor>),
43}
44
45impl Transport {
46    fn new(inner: TransportInner) -> Self {
47        let inner = Arc::new(inner);
48        Self { inner }
49    }
50
51    /// Construct a blackhole transport
52    #[must_use]
53    pub fn blackhole() -> Self {
54        Self::new(TransportInner::Blackhole)
55    }
56
57    /// Construct a SMTP transport
58    ///
59    /// # Errors
60    ///
61    /// Returns an error if the underlying SMTP transport could not be built
62    pub fn smtp(
63        mode: SmtpMode,
64        hostname: &str,
65        port: Option<NonZeroU16>,
66        credentials: Option<Credentials>,
67    ) -> Result<Self, lettre::transport::smtp::Error> {
68        let mut t = match mode {
69            SmtpMode::Plain => AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(hostname),
70            SmtpMode::StartTls => AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(hostname)?,
71            SmtpMode::Tls => AsyncSmtpTransport::<Tokio1Executor>::relay(hostname)?,
72        };
73
74        if let Some(credentials) = credentials {
75            t = t.credentials(credentials);
76        }
77
78        if let Some(port) = port {
79            t = t.port(port.into());
80        }
81
82        Ok(Self::new(TransportInner::Smtp(t.build())))
83    }
84
85    /// Construct a Sendmail transport
86    #[must_use]
87    pub fn sendmail(command: Option<impl Into<OsString>>) -> Self {
88        let transport = if let Some(command) = command {
89            AsyncSendmailTransport::new_with_command(command)
90        } else {
91            AsyncSendmailTransport::new()
92        };
93        Self::new(TransportInner::Sendmail(transport))
94    }
95}
96
97impl Transport {
98    /// Test the connection to the underlying transport. Only works with the
99    /// SMTP backend for now
100    ///
101    /// # Errors
102    ///
103    /// Will return `Err` if the connection test failed
104    pub async fn test_connection(&self) -> Result<(), Error> {
105        match self.inner.as_ref() {
106            TransportInner::Smtp(t) => {
107                t.test_connection().await?;
108            }
109            TransportInner::Blackhole | TransportInner::Sendmail(_) => {}
110        }
111
112        Ok(())
113    }
114}
115
116impl Default for TransportInner {
117    fn default() -> Self {
118        Self::Blackhole
119    }
120}
121
122#[derive(Debug, Error)]
123#[error(transparent)]
124pub enum Error {
125    Smtp(#[from] lettre::transport::smtp::Error),
126    Sendmail(#[from] lettre::transport::sendmail::Error),
127}
128
129#[async_trait]
130impl AsyncTransport for Transport {
131    type Ok = ();
132    type Error = Error;
133
134    async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
135        match self.inner.as_ref() {
136            TransportInner::Blackhole => {
137                tracing::warn!(
138                    "An email was supposed to be sent but no email backend is configured"
139                );
140            }
141            TransportInner::Smtp(t) => {
142                t.send_raw(envelope, email).await?;
143            }
144            TransportInner::Sendmail(t) => {
145                t.send_raw(envelope, email).await?;
146            }
147        }
148
149        Ok(())
150    }
151}