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