//! SMTP commands use std::fmt::{self, Display, Formatter}; use crate::{ address::Address, transport::smtp::{ authentication::{Credentials, Mechanism}, error::{self, Error}, extension::{ClientId, MailParameter, RcptParameter}, response::Response, }, }; /// EHLO command #[derive(PartialEq, Clone, Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Ehlo { client_id: ClientId, } impl Display for Ehlo { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "EHLO {}\r\n", self.client_id) } } impl Ehlo { /// Creates a EHLO command pub fn new(client_id: ClientId) -> Ehlo { Ehlo { client_id } } } /// STARTTLS command #[derive(PartialEq, Clone, Debug, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Starttls; impl Display for Starttls { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str("STARTTLS\r\n") } } /// MAIL command #[derive(PartialEq, Clone, Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Mail { sender: Option
, parameters: Vec, } impl Display for Mail { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!( f, "MAIL FROM:<{}>", self.sender.as_ref().map_or("", |s| s.as_ref()) )?; for parameter in &self.parameters { write!(f, " {}", parameter)?; } f.write_str("\r\n") } } impl Mail { /// Creates a MAIL command pub fn new(sender: Option
, parameters: Vec) -> Mail { Mail { sender, parameters } } } /// RCPT command #[derive(PartialEq, Clone, Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Rcpt { recipient: Address, parameters: Vec, } impl Display for Rcpt { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "RCPT TO:<{}>", self.recipient)?; for parameter in &self.parameters { write!(f, " {}", parameter)?; } f.write_str("\r\n") } } impl Rcpt { /// Creates an RCPT command pub fn new(recipient: Address, parameters: Vec) -> Rcpt { Rcpt { recipient, parameters, } } } /// DATA command #[derive(PartialEq, Clone, Debug, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Data; impl Display for Data { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str("DATA\r\n") } } /// QUIT command #[derive(PartialEq, Clone, Debug, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Quit; impl Display for Quit { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str("QUIT\r\n") } } /// NOOP command #[derive(PartialEq, Clone, Debug, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Noop; impl Display for Noop { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str("NOOP\r\n") } } /// HELP command #[derive(PartialEq, Clone, Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Help { argument: Option, } impl Display for Help { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str("HELP")?; if let Some(argument) = &self.argument { write!(f, " {}", argument)?; } f.write_str("\r\n") } } impl Help { /// Creates an HELP command pub fn new(argument: Option) -> Help { Help { argument } } } /// VRFY command #[derive(PartialEq, Clone, Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Vrfy { argument: String, } impl Display for Vrfy { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "VRFY {}\r\n", self.argument) } } impl Vrfy { /// Creates a VRFY command pub fn new(argument: String) -> Vrfy { Vrfy { argument } } } /// EXPN command #[derive(PartialEq, Clone, Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Expn { argument: String, } impl Display for Expn { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "EXPN {}\r\n", self.argument) } } impl Expn { /// Creates an EXPN command pub fn new(argument: String) -> Expn { Expn { argument } } } /// RSET command #[derive(PartialEq, Clone, Debug, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Rset; impl Display for Rset { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str("RSET\r\n") } } /// AUTH command #[derive(PartialEq, Clone, Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Auth { mechanism: Mechanism, credentials: Credentials, challenge: Option, response: Option, } impl Display for Auth { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let encoded_response = self.response.as_ref().map(base64::encode); if self.mechanism.supports_initial_response() { write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?; } else { match encoded_response { Some(response) => f.write_str(&response)?, None => write!(f, "AUTH {}", self.mechanism)?, } } f.write_str("\r\n") } } impl Auth { /// Creates an AUTH command (from a challenge if provided) pub fn new( mechanism: Mechanism, credentials: Credentials, challenge: Option, ) -> Result { let response = if mechanism.supports_initial_response() || challenge.is_some() { Some(mechanism.response(&credentials, challenge.as_deref())?) } else { None }; Ok(Auth { mechanism, credentials, challenge, response, }) } /// Creates an AUTH command from a response that needs to be a /// valid challenge (with 334 response code) pub fn new_from_response( mechanism: Mechanism, credentials: Credentials, response: &Response, ) -> Result { if !response.has_code(334) { return Err(error::response("Expecting a challenge")); } let encoded_challenge = response .first_word() .ok_or_else(|| error::response("Could not read auth challenge"))?; #[cfg(feature = "tracing")] tracing::debug!("auth encoded challenge: {}", encoded_challenge); let decoded_base64 = base64::decode(&encoded_challenge).map_err(error::response)?; let decoded_challenge = String::from_utf8(decoded_base64).map_err(error::response)?; #[cfg(feature = "tracing")] tracing::debug!("auth decoded challenge: {}", decoded_challenge); let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?); Ok(Auth { mechanism, credentials, challenge: Some(decoded_challenge), response, }) } } #[cfg(test)] mod test { use std::str::FromStr; use super::*; use crate::transport::smtp::extension::MailBodyParameter; #[test] fn test_display() { let id = ClientId::Domain("localhost".to_string()); let email = Address::from_str("test@example.com").unwrap(); let mail_parameter = MailParameter::Other { keyword: "TEST".to_string(), value: Some("value".to_string()), }; let rcpt_parameter = RcptParameter::Other { keyword: "TEST".to_string(), value: Some("value".to_string()), }; assert_eq!(format!("{}", Ehlo::new(id)), "EHLO localhost\r\n"); assert_eq!( format!("{}", Mail::new(Some(email.clone()), vec![])), "MAIL FROM:\r\n" ); assert_eq!(format!("{}", Mail::new(None, vec![])), "MAIL FROM:<>\r\n"); assert_eq!( format!( "{}", Mail::new(Some(email.clone()), vec![MailParameter::Size(42)]) ), "MAIL FROM: SIZE=42\r\n" ); assert_eq!( format!( "{}", Mail::new( Some(email.clone()), vec![ MailParameter::Size(42), MailParameter::Body(MailBodyParameter::EightBitMime), mail_parameter, ], ) ), "MAIL FROM: SIZE=42 BODY=8BITMIME TEST=value\r\n" ); assert_eq!( format!("{}", Rcpt::new(email.clone(), vec![])), "RCPT TO:\r\n" ); assert_eq!( format!("{}", Rcpt::new(email, vec![rcpt_parameter])), "RCPT TO: TEST=value\r\n" ); assert_eq!(format!("{}", Quit), "QUIT\r\n"); assert_eq!(format!("{}", Data), "DATA\r\n"); assert_eq!(format!("{}", Noop), "NOOP\r\n"); assert_eq!(format!("{}", Help::new(None)), "HELP\r\n"); assert_eq!( format!("{}", Help::new(Some("test".to_string()))), "HELP test\r\n" ); assert_eq!( format!("{}", Vrfy::new("test".to_string())), "VRFY test\r\n" ); assert_eq!( format!("{}", Expn::new("test".to_string())), "EXPN test\r\n" ); assert_eq!(format!("{}", Rset), "RSET\r\n"); let credentials = Credentials::new("user".to_string(), "password".to_string()); assert_eq!( format!( "{}", Auth::new(Mechanism::Plain, credentials.clone(), None).unwrap() ), "AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n" ); assert_eq!( format!( "{}", Auth::new(Mechanism::Login, credentials, None).unwrap() ), "AUTH LOGIN\r\n" ); } }