async_smtp/
commands.rs

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