1use 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#[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 pub fn new(client_id: ClientId) -> EhloCommand {
28 EhloCommand { client_id }
29 }
30}
31
32#[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#[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 pub fn new(sender: Option<EmailAddress>, parameters: Vec<MailParameter>) -> MailCommand {
66 MailCommand { sender, parameters }
67 }
68}
69
70#[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 pub fn new(recipient: EmailAddress, parameters: Vec<RcptParameter>) -> RcptCommand {
90 RcptCommand {
91 recipient,
92 parameters,
93 }
94 }
95}
96
97#[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#[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#[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#[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 pub fn new(argument: Option<String>) -> HelpCommand {
146 HelpCommand { argument }
147 }
148}
149
150#[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 pub fn new(argument: String) -> VrfyCommand {
165 VrfyCommand { argument }
166 }
167}
168
169#[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 pub fn new(argument: String) -> ExpnCommand {
184 ExpnCommand { argument }
185 }
186}
187
188#[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#[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 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 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}