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