lettre/transport/smtp/
commands.rs

1//! SMTP commands
2
3use std::fmt::{self, Display, Formatter};
4
5use crate::{
6    address::Address,
7    transport::smtp::{
8        authentication::{Credentials, Mechanism},
9        error::{self, Error},
10        extension::{ClientId, MailParameter, RcptParameter},
11        response::Response,
12    },
13};
14
15/// EHLO command
16#[derive(PartialEq, Eq, Clone, Debug)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub struct Ehlo {
19    client_id: ClientId,
20}
21
22impl Display for Ehlo {
23    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
24        write!(f, "EHLO {}\r\n", self.client_id)
25    }
26}
27
28impl Ehlo {
29    /// Creates a EHLO command
30    pub fn new(client_id: ClientId) -> Ehlo {
31        Ehlo { client_id }
32    }
33}
34
35/// STARTTLS command
36#[derive(PartialEq, Eq, Clone, Debug, Copy)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38pub struct Starttls;
39
40impl Display for Starttls {
41    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
42        f.write_str("STARTTLS\r\n")
43    }
44}
45
46/// MAIL command
47#[derive(PartialEq, Eq, Clone, Debug)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49pub struct Mail {
50    sender: Option<Address>,
51    parameters: Vec<MailParameter>,
52}
53
54impl Display for Mail {
55    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
56        write!(
57            f,
58            "MAIL FROM:<{}>",
59            self.sender.as_ref().map_or("", |s| s.as_ref())
60        )?;
61        for parameter in &self.parameters {
62            write!(f, " {parameter}")?;
63        }
64        f.write_str("\r\n")
65    }
66}
67
68impl Mail {
69    /// Creates a MAIL command
70    pub fn new(sender: Option<Address>, parameters: Vec<MailParameter>) -> Mail {
71        Mail { sender, parameters }
72    }
73}
74
75/// RCPT command
76#[derive(PartialEq, Eq, Clone, Debug)]
77#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
78pub struct Rcpt {
79    recipient: Address,
80    parameters: Vec<RcptParameter>,
81}
82
83impl Display for Rcpt {
84    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
85        write!(f, "RCPT TO:<{}>", self.recipient)?;
86        for parameter in &self.parameters {
87            write!(f, " {parameter}")?;
88        }
89        f.write_str("\r\n")
90    }
91}
92
93impl Rcpt {
94    /// Creates an RCPT command
95    pub fn new(recipient: Address, parameters: Vec<RcptParameter>) -> Rcpt {
96        Rcpt {
97            recipient,
98            parameters,
99        }
100    }
101}
102
103/// DATA command
104#[derive(PartialEq, Eq, Clone, Debug, Copy)]
105#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
106pub struct Data;
107
108impl Display for Data {
109    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
110        f.write_str("DATA\r\n")
111    }
112}
113
114/// QUIT command
115#[derive(PartialEq, Eq, Clone, Debug, Copy)]
116#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
117pub struct Quit;
118
119impl Display for Quit {
120    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
121        f.write_str("QUIT\r\n")
122    }
123}
124
125/// NOOP command
126#[derive(PartialEq, Eq, Clone, Debug, Copy)]
127#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
128pub struct Noop;
129
130impl Display for Noop {
131    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
132        f.write_str("NOOP\r\n")
133    }
134}
135
136/// HELP command
137#[derive(PartialEq, Eq, Clone, Debug)]
138#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
139pub struct Help {
140    argument: Option<String>,
141}
142
143impl Display for Help {
144    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
145        f.write_str("HELP")?;
146        if let Some(argument) = &self.argument {
147            write!(f, " {argument}")?;
148        }
149        f.write_str("\r\n")
150    }
151}
152
153impl Help {
154    /// Creates an HELP command
155    pub fn new(argument: Option<String>) -> Help {
156        Help { argument }
157    }
158}
159
160/// VRFY command
161#[derive(PartialEq, Eq, Clone, Debug)]
162#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
163pub struct Vrfy {
164    argument: String,
165}
166
167impl Display for Vrfy {
168    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
169        write!(f, "VRFY {}\r\n", self.argument)
170    }
171}
172
173impl Vrfy {
174    /// Creates a VRFY command
175    pub fn new(argument: String) -> Vrfy {
176        Vrfy { argument }
177    }
178}
179
180/// EXPN command
181#[derive(PartialEq, Eq, Clone, Debug)]
182#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
183pub struct Expn {
184    argument: String,
185}
186
187impl Display for Expn {
188    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
189        write!(f, "EXPN {}\r\n", self.argument)
190    }
191}
192
193impl Expn {
194    /// Creates an EXPN command
195    pub fn new(argument: String) -> Expn {
196        Expn { argument }
197    }
198}
199
200/// RSET command
201#[derive(PartialEq, Eq, Clone, Debug, Copy)]
202#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
203pub struct Rset;
204
205impl Display for Rset {
206    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
207        f.write_str("RSET\r\n")
208    }
209}
210
211/// AUTH command
212#[derive(PartialEq, Eq, Clone, Debug)]
213#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
214pub struct Auth {
215    mechanism: Mechanism,
216    credentials: Credentials,
217    challenge: Option<String>,
218    response: Option<String>,
219}
220
221impl Display for Auth {
222    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
223        let encoded_response = self.response.as_ref().map(crate::base64::encode);
224
225        if self.mechanism.supports_initial_response() {
226            write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?;
227        } else {
228            match encoded_response {
229                Some(response) => f.write_str(&response)?,
230                None => write!(f, "AUTH {}", self.mechanism)?,
231            }
232        }
233        f.write_str("\r\n")
234    }
235}
236
237impl Auth {
238    /// Creates an AUTH command (from a challenge if provided)
239    pub fn new(
240        mechanism: Mechanism,
241        credentials: Credentials,
242        challenge: Option<String>,
243    ) -> Result<Auth, Error> {
244        let response = if mechanism.supports_initial_response() || challenge.is_some() {
245            Some(mechanism.response(&credentials, challenge.as_deref())?)
246        } else {
247            None
248        };
249        Ok(Auth {
250            mechanism,
251            credentials,
252            challenge,
253            response,
254        })
255    }
256
257    /// Creates an AUTH command from a response that needs to be a
258    /// valid challenge (with 334 response code)
259    pub fn new_from_response(
260        mechanism: Mechanism,
261        credentials: Credentials,
262        response: &Response,
263    ) -> Result<Auth, Error> {
264        if !response.has_code(334) {
265            return Err(error::response("Expecting a challenge"));
266        }
267
268        let encoded_challenge = response
269            .first_word()
270            .ok_or_else(|| error::response("Could not read auth challenge"))?;
271        #[cfg(feature = "tracing")]
272        tracing::debug!("auth encoded challenge: {}", encoded_challenge);
273
274        let decoded_base64 = crate::base64::decode(encoded_challenge).map_err(error::response)?;
275        let decoded_challenge = String::from_utf8(decoded_base64).map_err(error::response)?;
276        #[cfg(feature = "tracing")]
277        tracing::debug!("auth decoded challenge: {}", decoded_challenge);
278
279        let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
280
281        Ok(Auth {
282            mechanism,
283            credentials,
284            challenge: Some(decoded_challenge),
285            response,
286        })
287    }
288}
289
290#[cfg(test)]
291mod test {
292    use std::str::FromStr;
293
294    use super::*;
295    use crate::transport::smtp::extension::MailBodyParameter;
296
297    #[test]
298    fn test_display() {
299        let id = ClientId::Domain("localhost".to_owned());
300        let email = Address::from_str("test@example.com").unwrap();
301        let mail_parameter = MailParameter::Other {
302            keyword: "TEST".to_owned(),
303            value: Some("value".to_owned()),
304        };
305        let rcpt_parameter = RcptParameter::Other {
306            keyword: "TEST".to_owned(),
307            value: Some("value".to_owned()),
308        };
309        assert_eq!(format!("{}", Ehlo::new(id)), "EHLO localhost\r\n");
310        assert_eq!(
311            format!("{}", Mail::new(Some(email.clone()), vec![])),
312            "MAIL FROM:<test@example.com>\r\n"
313        );
314        assert_eq!(format!("{}", Mail::new(None, vec![])), "MAIL FROM:<>\r\n");
315        assert_eq!(
316            format!(
317                "{}",
318                Mail::new(Some(email.clone()), vec![MailParameter::Size(42)])
319            ),
320            "MAIL FROM:<test@example.com> SIZE=42\r\n"
321        );
322        assert_eq!(
323            format!(
324                "{}",
325                Mail::new(
326                    Some(email.clone()),
327                    vec![
328                        MailParameter::Size(42),
329                        MailParameter::Body(MailBodyParameter::EightBitMime),
330                        mail_parameter,
331                    ],
332                )
333            ),
334            "MAIL FROM:<test@example.com> SIZE=42 BODY=8BITMIME TEST=value\r\n"
335        );
336        assert_eq!(
337            format!("{}", Rcpt::new(email.clone(), vec![])),
338            "RCPT TO:<test@example.com>\r\n"
339        );
340        assert_eq!(
341            format!("{}", Rcpt::new(email, vec![rcpt_parameter])),
342            "RCPT TO:<test@example.com> TEST=value\r\n"
343        );
344        assert_eq!(format!("{Quit}"), "QUIT\r\n");
345        assert_eq!(format!("{Data}"), "DATA\r\n");
346        assert_eq!(format!("{Noop}"), "NOOP\r\n");
347        assert_eq!(format!("{}", Help::new(None)), "HELP\r\n");
348        assert_eq!(
349            format!("{}", Help::new(Some("test".to_owned()))),
350            "HELP test\r\n"
351        );
352        assert_eq!(format!("{}", Vrfy::new("test".to_owned())), "VRFY test\r\n");
353        assert_eq!(format!("{}", Expn::new("test".to_owned())), "EXPN test\r\n");
354        assert_eq!(format!("{Rset}"), "RSET\r\n");
355        let credentials = Credentials::new("user".to_owned(), "password".to_owned());
356        assert_eq!(
357            format!(
358                "{}",
359                Auth::new(Mechanism::Plain, credentials.clone(), None).unwrap()
360            ),
361            "AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"
362        );
363        assert_eq!(
364            format!(
365                "{}",
366                Auth::new(Mechanism::Login, credentials, None).unwrap()
367            ),
368            "AUTH LOGIN\r\n"
369        );
370    }
371}