async_smtp/
smtp_client.rs

1use std::fmt::Debug;
2
3use log::{debug, info};
4
5use crate::authentication::{Credentials, Mechanism};
6use crate::commands::*;
7use crate::error::{Error, SmtpResult};
8use crate::extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo};
9use crate::stream::SmtpStream;
10use crate::SendableEmail;
11
12#[cfg(feature = "runtime-async-std")]
13use async_std::io::{BufRead, Write};
14#[cfg(feature = "runtime-tokio")]
15use tokio::io::{AsyncBufRead as BufRead, AsyncWrite as Write};
16
17/// Contains client configuration
18#[derive(Debug)]
19pub struct SmtpClient {
20    /// Name sent during EHLO
21    hello_name: ClientId,
22    /// Enable UTF8 mailboxes in envelope or headers
23    smtp_utf8: bool,
24    /// Whether to expect greeting.
25    /// Normally the server sends a greeting after connection,
26    /// but not after STARTTLS.
27    expect_greeting: bool,
28    /// Use pipelining if the server supports it
29    pipelining: bool,
30}
31
32impl Default for SmtpClient {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38/// Builder for the SMTP `SmtpTransport`
39impl SmtpClient {
40    /// Creates a new SMTP client.
41    ///
42    /// It does not connect to the server, but only creates the `SmtpTransport`.
43    ///
44    /// Defaults are:
45    ///
46    /// * No authentication
47    /// * No SMTPUTF8 support
48    pub fn new() -> Self {
49        SmtpClient {
50            smtp_utf8: false,
51            hello_name: Default::default(),
52            expect_greeting: true,
53            pipelining: true,
54        }
55    }
56
57    /// Enable SMTPUTF8 if the server supports it
58    pub fn smtp_utf8(self, enabled: bool) -> SmtpClient {
59        Self {
60            smtp_utf8: enabled,
61            ..self
62        }
63    }
64
65    /// Enable PIPELINING if the server supports it
66    pub fn pipelining(self, enabled: bool) -> SmtpClient {
67        Self {
68            pipelining: enabled,
69            ..self
70        }
71    }
72
73    /// Set the name used during EHLO
74    pub fn hello_name(self, name: ClientId) -> SmtpClient {
75        Self {
76            hello_name: name,
77            ..self
78        }
79    }
80
81    /// Do not expect greeting.
82    ///
83    /// Could be used for STARTTLS connections.
84    pub fn without_greeting(self) -> SmtpClient {
85        Self {
86            expect_greeting: false,
87            ..self
88        }
89    }
90}
91
92/// Structure that implements the high level SMTP client
93#[derive(Debug)]
94pub struct SmtpTransport<S: BufRead + Write + Unpin> {
95    /// Information about the server
96    server_info: ServerInfo,
97    /// Information about the client
98    client_info: SmtpClient,
99    /// Low level client
100    stream: SmtpStream<S>,
101}
102
103impl<S: BufRead + Write + Unpin> SmtpTransport<S> {
104    /// Creates a new SMTP transport and connects.
105    pub async fn new(builder: SmtpClient, stream: S) -> Result<Self, Error> {
106        let mut stream = SmtpStream::new(stream);
107        if builder.expect_greeting {
108            let _greeting = stream.read_response().await?;
109        }
110        let ehlo_response = stream
111            .ehlo(ClientId::new(builder.hello_name.to_string()))
112            .await?;
113        let server_info = ServerInfo::from_response(&ehlo_response)?;
114
115        // Print server information
116        debug!("server {}", server_info);
117
118        let transport = SmtpTransport {
119            server_info,
120            client_info: builder,
121            stream,
122        };
123        Ok(transport)
124    }
125
126    /// Gets a mutable reference to the underlying `SmtpStream`.
127    pub fn get_mut(&mut self) -> &mut SmtpStream<S> {
128        &mut self.stream
129    }
130
131    /// Gets a reference to the underlying `SmtpStream`.
132    pub fn get_ref(&mut self) -> &SmtpStream<S> {
133        &self.stream
134    }
135
136    /// Consumes the SMTP transport and returns the inner `SmtpStream`.
137    pub fn into_inner(self) -> SmtpStream<S> {
138        self.stream
139    }
140
141    /// Try to login with the given accepted mechanisms.
142    pub async fn try_login(
143        &mut self,
144        credentials: &Credentials,
145        accepted_mechanisms: &[Mechanism],
146    ) -> Result<(), Error> {
147        if let Some(mechanism) = accepted_mechanisms
148            .iter()
149            .find(|mechanism| self.server_info.supports_auth_mechanism(**mechanism))
150        {
151            self.auth(*mechanism, credentials).await?;
152        } else {
153            info!("No supported authentication mechanisms available");
154        }
155
156        Ok(())
157    }
158
159    /// Sends STARTTLS command if the server supports it.
160    ///
161    /// Returns inner stream which should be upgraded to TLS.
162    pub async fn starttls(mut self) -> Result<S, Error> {
163        if !self.supports_feature(Extension::StartTls) {
164            return Err(From::from("server does not support STARTTLS"));
165        }
166
167        self.stream.command(StarttlsCommand).await?;
168
169        // Return the stream, so the caller can upgrade it to TLS.
170        Ok(self.stream.into_inner())
171    }
172
173    fn supports_feature(&self, keyword: Extension) -> bool {
174        self.server_info.supports_feature(keyword)
175    }
176
177    /// Closes the SMTP transaction if possible.
178    pub async fn quit(&mut self) -> Result<(), Error> {
179        self.stream.command(QuitCommand).await?;
180
181        Ok(())
182    }
183
184    /// Sends an AUTH command with the given mechanism, and handles challenge if needed
185    pub async fn auth(&mut self, mechanism: Mechanism, credentials: &Credentials) -> SmtpResult {
186        // TODO
187        let mut challenges = 10;
188        let mut response = self
189            .stream
190            .command(AuthCommand::new(mechanism, credentials.clone(), None)?)
191            .await?;
192
193        while challenges > 0 && response.has_code(334) {
194            challenges -= 1;
195            response = self
196                .stream
197                .command(AuthCommand::new_from_response(
198                    mechanism,
199                    credentials.clone(),
200                    &response,
201                )?)
202                .await?;
203        }
204
205        if challenges == 0 {
206            Err(Error::ResponseParsing("Unexpected number of challenges"))
207        } else {
208            Ok(response)
209        }
210    }
211
212    /// Sends an email.
213    pub async fn send(&mut self, email: SendableEmail) -> SmtpResult {
214        // Mail
215        let mut mail_options = vec![];
216
217        if self.supports_feature(Extension::EightBitMime) {
218            mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
219        }
220
221        if self.supports_feature(Extension::SmtpUtfEight) && self.client_info.smtp_utf8 {
222            mail_options.push(MailParameter::SmtpUtfEight);
223        }
224
225        let pipelining =
226            self.supports_feature(Extension::Pipelining) && self.client_info.pipelining;
227
228        if pipelining {
229            self.stream
230                .send_command(MailCommand::new(
231                    email.envelope().from().cloned(),
232                    mail_options,
233                ))
234                .await?;
235            let mut sent_commands = 1;
236
237            // Recipient
238            for to_address in email.envelope().to() {
239                self.stream
240                    .send_command(RcptCommand::new(to_address.clone(), vec![]))
241                    .await?;
242                sent_commands += 1;
243            }
244
245            // Data
246            self.stream.send_command(DataCommand).await?;
247            sent_commands += 1;
248
249            for _ in 0..sent_commands {
250                self.stream.read_response().await?;
251            }
252        } else {
253            self.stream
254                .command(MailCommand::new(
255                    email.envelope().from().cloned(),
256                    mail_options,
257                ))
258                .await?;
259
260            // Recipient
261            for to_address in email.envelope().to() {
262                self.stream
263                    .command(RcptCommand::new(to_address.clone(), vec![]))
264                    .await?;
265                // Log the rcpt command
266                debug!("to=<{}>", to_address);
267            }
268
269            // Data
270            self.stream.command(DataCommand).await?;
271        }
272
273        let res = self.stream.message(email.message()).await;
274
275        // Message content
276        if let Ok(result) = &res {
277            // Log the message
278            debug!(
279                "status=sent ({})",
280                result.message.first().unwrap_or(&"no response".to_string())
281            );
282        }
283
284        res
285    }
286}