Skip to main content

email_clients/clients/
smtp.rs

1use crate::configuration::EmailConfiguration;
2use crate::email::{EmailAddress, EmailObject};
3use crate::traits::EmailTrait;
4use async_trait::async_trait;
5use lettre::message::MultiPart;
6use lettre::transport::smtp::authentication::Credentials;
7use lettre::transport::smtp::SMTP_PORT;
8use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
9use log::info;
10use secrecy::ExposeSecret;
11use secrecy::Secret;
12
13#[derive(
14    Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, Default, PartialOrd,
15)]
16pub enum TlsMode {
17    #[default]
18    Local,
19    Tls,      // Insecure connection only
20    StartTls, // Start with insecure connection and use STARTTLS when available
21}
22#[derive(Debug, Clone, serde::Deserialize)]
23pub struct SmtpConfig {
24    pub sender: EmailAddress,
25    pub relay: String,
26    pub username: String,
27    pub password: Secret<String>,
28    pub port: u16,
29    pub tls: TlsMode,
30}
31
32impl Default for SmtpConfig {
33    fn default() -> Self {
34        Self {
35            sender: "".into(),
36            relay: "localhost".to_owned(),
37            username: "".to_string(),
38            port: SMTP_PORT,
39            tls: TlsMode::Local,
40            password: Secret::from("".to_string()),
41        }
42    }
43}
44
45impl SmtpConfig {
46    /// Sets the sender of the SMTP config.
47    ///
48    /// ```
49    /// use email_clients::clients::smtp::SmtpConfig;
50    ///
51    /// let mut smtp_config = SmtpConfig::default().sender("Test Sender");
52    /// assert_eq!(smtp_config.sender.to_string(), "Test Sender");
53    /// ```
54    pub fn sender(mut self, value: impl Into<EmailAddress>) -> Self {
55        self.sender = value.into();
56        self
57    }
58
59    /// Sets the relay of the SMTP config.
60    ///
61    /// ```
62    /// use email_clients::clients::smtp::SmtpConfig;
63    ///
64    /// let mut smtp_config = SmtpConfig::default().relay("Test Relay");
65    /// assert_eq!(smtp_config.relay, "Test Relay");
66    /// ```
67    pub fn relay(mut self, value: impl AsRef<str>) -> Self {
68        self.relay = value.as_ref().to_string();
69        self
70    }
71
72    /// Sets the username of the SMTP config.
73    ///
74    /// ```
75    /// use email_clients::clients::smtp::SmtpConfig;
76    ///
77    /// let mut smtp_config = SmtpConfig::default().username("Test Username");
78    /// assert_eq!(smtp_config.username, "Test Username");
79    /// ```
80    pub fn username(mut self, value: impl AsRef<str>) -> Self {
81        self.username = value.as_ref().to_string();
82        self
83    }
84
85    /// Sets the password of the SMTP config.
86    ///
87    /// ```
88    /// use email_clients::clients::smtp::SmtpConfig;
89    /// use secrecy::{ExposeSecret, Secret};
90    ///
91    /// let mut smtp_config = SmtpConfig::default().password("Test Password");
92    /// assert_eq!(smtp_config.password.expose_secret(), "Test Password");
93    /// ```
94    pub fn password(mut self, value: impl AsRef<str>) -> Self {
95        self.password = Secret::new(value.as_ref().to_string());
96        self
97    }
98
99    /// Sets the port of the SMTP config.
100    ///
101    /// ```
102    /// use email_clients::clients::smtp::SmtpConfig;
103    ///
104    /// let mut smtp_config = SmtpConfig::default().port(1234);
105    /// assert_eq!(smtp_config.port, 1234);
106    /// ```
107    pub fn port(mut self, value: u16) -> Self {
108        self.port = value;
109        self
110    }
111
112    /// Sets the TLS mode of the SMTP config.
113    ///
114    /// ```
115    /// use email_clients::clients::smtp::{SmtpConfig, TlsMode};
116    ///
117    /// let mut smtp_config = SmtpConfig::default().tls(TlsMode::Tls);
118    /// assert_eq!(smtp_config.tls, TlsMode::Tls);
119    /// ```
120    pub fn tls(mut self, value: TlsMode) -> Self {
121        self.tls = value;
122        self
123    }
124}
125
126impl From<SmtpConfig> for EmailConfiguration {
127    /// Converts SmtpConfig to EmailConfiguration.
128    ///
129    /// ```
130    /// use email_clients::configuration::EmailConfiguration;
131    /// use email_clients::traits::EmailTrait;
132    /// use secrecy::Secret;
133    /// use email_clients::clients::smtp::{SmtpConfig, TlsMode};
134    ///
135    /// let smtp_config = SmtpConfig {
136    ///     sender: "Test Sender".into(),
137    ///     relay: "Test Relay".to_string(),
138    ///     username: "Test User".to_string(),
139    ///     password: Secret::new("Test Password".to_string()),
140    ///     port: 123,
141    ///     tls: TlsMode::Local,
142    /// };
143    ///
144    /// let email_config = EmailConfiguration::from(smtp_config);
145    /// assert!(matches!(email_config, EmailConfiguration::SMTP(_)));
146    /// ```
147    fn from(value: SmtpConfig) -> Self {
148        EmailConfiguration::SMTP(value)
149    }
150}
151
152#[derive(Clone, Debug, Default)]
153pub struct SmtpClient {
154    config: SmtpConfig,
155}
156
157impl SmtpClient {
158    fn get_transport(&self) -> AsyncSmtpTransport<Tokio1Executor> {
159        let settings = &self.config;
160        let creds = Credentials::new(
161            settings.username.to_owned(),
162            settings.password.expose_secret().to_owned(),
163        );
164
165        match settings.tls {
166            TlsMode::Local => {
167                AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(settings.relay.as_str())
168                    .port(settings.port)
169                    .timeout(Some(std::time::Duration::from_secs(10)))
170                    .build()
171            }
172            TlsMode::Tls => AsyncSmtpTransport::<Tokio1Executor>::relay(settings.relay.as_str())
173                .unwrap()
174                .credentials(creds)
175                .port(settings.port)
176                .build(),
177            TlsMode::StartTls => {
178                AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(settings.relay.as_str())
179                    .unwrap()
180                    .credentials(creds)
181                    .port(settings.port)
182                    .build()
183            }
184        }
185    }
186
187    pub fn new(config: SmtpConfig) -> Self {
188        info!("Starting smtp client");
189        Self { config }
190    }
191}
192
193#[async_trait]
194impl EmailTrait for SmtpClient {
195    fn get_sender(&self) -> EmailAddress {
196        self.config.sender.clone()
197    }
198
199    async fn send_emails(&self, email: EmailObject) -> crate::Result<()> {
200        let transport = self.get_transport();
201        let email_body = MultiPart::alternative_plain_html(email.plain, email.html);
202
203        let mut message_builder = Message::builder()
204            .from(self.get_sender().try_into()?)
205            .reply_to(self.get_sender().try_into()?);
206        for addr in email.to {
207            message_builder = message_builder.to(addr.try_into()?)
208        }
209        let message = message_builder
210            .subject(email.subject)
211            .multipart(email_body)?;
212        transport.send(message).await?;
213        Ok(())
214    }
215}