diff --git a/.gitignore b/.gitignore index 498dce1..c2fa1c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ .project -/target/ +target/ /Cargo.lock diff --git a/lettre/Cargo.toml b/lettre/Cargo.toml index a06d668..8cd4769 100644 --- a/lettre/Cargo.toml +++ b/lettre/Cargo.toml @@ -24,6 +24,7 @@ rust-crypto = "^0.2" serde = "^1.0" serde_json = "^1.0" serde_derive = "^1.0" +emailaddress = "^0.4" [dev-dependencies] env_logger = "^0.4" diff --git a/lettre/benches/transport_smtp.rs b/lettre/benches/transport_smtp.rs index 177a7d4..e511525 100644 --- a/lettre/benches/transport_smtp.rs +++ b/lettre/benches/transport_smtp.rs @@ -3,17 +3,19 @@ extern crate lettre; extern crate test; -use lettre::smtp::SmtpTransportBuilder; use lettre::{EmailTransport, SimpleSendableEmail}; +use lettre::smtp::SmtpTransportBuilder; #[bench] fn bench_simple_send(b: &mut test::Bencher) { let mut sender = SmtpTransportBuilder::new("127.0.0.1:2525").unwrap().build(); b.iter(|| { - let email = SimpleSendableEmail::new("user@localhost", - vec!["root@localhost"], - "id", - "Hello world"); + let email = SimpleSendableEmail::new( + "user@localhost", + vec!["root@localhost"], + "id", + "Hello world", + ); let result = sender.send(email); assert!(result.is_ok()); }); @@ -22,14 +24,16 @@ fn bench_simple_send(b: &mut test::Bencher) { #[bench] fn bench_reuse_send(b: &mut test::Bencher) { let mut sender = SmtpTransportBuilder::new("127.0.0.1:2525") - .unwrap() - .connection_reuse(true) - .build(); + .unwrap() + .connection_reuse(true) + .build(); b.iter(|| { - let email = SimpleSendableEmail::new("user@localhost", - vec!["root@localhost"], - "file_id", - "Hello file"); + let email = SimpleSendableEmail::new( + "user@localhost", + vec!["root@localhost"], + "file_id", + "Hello file", + ); let result = sender.send(email); assert!(result.is_ok()); }); diff --git a/lettre/examples/smtp.rs b/lettre/examples/smtp.rs index 84d8bf0..e138006 100644 --- a/lettre/examples/smtp.rs +++ b/lettre/examples/smtp.rs @@ -12,7 +12,10 @@ fn main() { ); // Open a local connection on port 25 - let mut mailer = SmtpTransportBuilder::localhost().unwrap().security_level(SecurityLevel::Opportunistic).build(); + let mut mailer = SmtpTransportBuilder::localhost() + .unwrap() + .security_level(SecurityLevel::Opportunistic) + .build(); // Send the email let result = mailer.send(email); diff --git a/lettre/src/lib.rs b/lettre/src/lib.rs index 8b6a303..7d07d79 100644 --- a/lettre/src/lib.rs +++ b/lettre/src/lib.rs @@ -13,6 +13,7 @@ extern crate hex; extern crate crypto; extern crate bufstream; extern crate openssl; +extern crate emailaddress; extern crate serde_json; extern crate serde; #[macro_use] diff --git a/lettre/src/smtp/authentication.rs b/lettre/src/smtp/authentication.rs index 9b1fb22..240ffac 100644 --- a/lettre/src/smtp/authentication.rs +++ b/lettre/src/smtp/authentication.rs @@ -9,6 +9,42 @@ use smtp::error::Error; use std::fmt; use std::fmt::{Display, Formatter}; +/// Convertable to user credentials +pub trait IntoCredentials { + /// Converts to a `Credentials` struct + fn into_credentials(self) -> Credentials; +} + +impl IntoCredentials for Credentials { + fn into_credentials(self) -> Credentials { + self + } +} + +impl, T: Into> IntoCredentials for (S, T) { + fn into_credentials(self) -> Credentials { + let (username, password) = self; + Credentials::new(username.into(), password.into()) + } +} + +/// Contains user credentials +#[derive(PartialEq, Eq, Clone, Hash, Debug)] +pub struct Credentials { + username: String, + password: String, +} + +impl Credentials { + /// Create a `Credentials` struct from username and password + pub fn new(username: String, password: String) -> Credentials { + Credentials { + username: username, + password: password, + } + } +} + /// Represents authentication mechanisms #[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)] pub enum Mechanism { @@ -52,15 +88,20 @@ impl Mechanism { /// challenge in some cases pub fn response( &self, - username: &str, - password: &str, + credentials: &Credentials, challenge: Option<&str>, ) -> Result { match *self { Mechanism::Plain => { match challenge { Some(_) => Err(Error::Client("This mechanism does not expect a challenge")), - None => Ok(format!("{}{}{}{}", NUL, username, NUL, password)), + None => Ok(format!( + "{}{}{}{}", + NUL, + credentials.username, + NUL, + credentials.password + )), } } Mechanism::Login => { @@ -70,11 +111,11 @@ impl Mechanism { }; if vec!["User Name", "Username:", "Username"].contains(&decoded_challenge) { - return Ok(username.to_string()); + return Ok(credentials.username.to_string()); } if vec!["Password", "Password:"].contains(&decoded_challenge) { - return Ok(password.to_string()); + return Ok(credentials.password.to_string()); } Err(Error::Client("Unrecognized challenge")) @@ -85,10 +126,14 @@ impl Mechanism { None => return Err(Error::Client("This mechanism does expect a challenge")), }; - let mut hmac = Hmac::new(Md5::new(), password.as_bytes()); + let mut hmac = Hmac::new(Md5::new(), credentials.password.as_bytes()); hmac.input(decoded_challenge.as_bytes()); - Ok(format!("{} {}", username, hmac.result().code().to_hex())) + Ok(format!( + "{} {}", + credentials.username, + hmac.result().code().to_hex() + )) } } } @@ -96,56 +141,53 @@ impl Mechanism { #[cfg(test)] mod test { - use super::Mechanism; + use super::{Credentials, Mechanism}; #[test] fn test_plain() { let mechanism = Mechanism::Plain; + let credentials = Credentials::new("username".to_string(), "password".to_string()); + assert_eq!( - mechanism.response("username", "password", None).unwrap(), + mechanism.response(&credentials, None).unwrap(), "\u{0}username\u{0}password" ); - assert!( - mechanism - .response("username", "password", Some("test")) - .is_err() - ); + assert!(mechanism.response(&credentials, Some("test")).is_err()); } #[test] fn test_login() { let mechanism = Mechanism::Login; + let credentials = Credentials::new("alice".to_string(), "wonderland".to_string()); + assert_eq!( - mechanism - .response("alice", "wonderland", Some("Username")) - .unwrap(), + mechanism.response(&credentials, Some("Username")).unwrap(), "alice" ); assert_eq!( - mechanism - .response("alice", "wonderland", Some("Password")) - .unwrap(), + mechanism.response(&credentials, Some("Password")).unwrap(), "wonderland" ); - assert!(mechanism.response("username", "password", None).is_err()); + assert!(mechanism.response(&credentials, None).is_err()); } #[test] fn test_cram_md5() { let mechanism = Mechanism::CramMd5; + let credentials = Credentials::new("alice".to_string(), "wonderland".to_string()); + assert_eq!( mechanism .response( - "alice", - "wonderland", + &credentials, Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=="), ) .unwrap(), "alice a540ebe4ef2304070bbc3c456c1f64c0" ); - assert!(mechanism.response("alice", "wonderland", None).is_err()); + assert!(mechanism.response(&credentials, None).is_err()); } } diff --git a/lettre/src/smtp/client/mod.rs b/lettre/src/smtp/client/mod.rs index b2aa02d..57e06e0 100644 --- a/lettre/src/smtp/client/mod.rs +++ b/lettre/src/smtp/client/mod.rs @@ -1,20 +1,22 @@ //! SMTP client -use base64; use bufstream::BufStream; use openssl::ssl::SslContext; use smtp::{CRLF, MESSAGE_ENDING}; -use smtp::authentication::Mechanism; +use smtp::authentication::{Credentials, Mechanism}; use smtp::client::net::{Connector, NetworkStream, Timeout}; +use smtp::commands::*; use smtp::error::{Error, SmtpResult}; use smtp::response::ResponseParser; use std::fmt::Debug; +use std::fmt::Display; use std::io; use std::io::{BufRead, Read, Write}; use std::net::ToSocketAddrs; use std::string::String; use std::time::Duration; + pub mod net; pub mod mock; @@ -69,7 +71,7 @@ impl Client { impl Client { /// Closes the SMTP transaction if possible pub fn close(&mut self) { - let _ = self.quit(); + let _ = self.smtp_command(QuitCommand); self.stream = None; } @@ -135,7 +137,7 @@ impl Client { /// Checks if the server is connected using the NOOP SMTP command #[cfg_attr(feature = "cargo-clippy", allow(wrong_self_convention))] pub fn is_connected(&mut self) -> bool { - self.noop().is_ok() + self.smtp_command(NoopCommand).is_ok() } /// Sends an SMTP command @@ -143,9 +145,9 @@ impl Client { self.send_server(command, CRLF) } - /// Sends a EHLO command - pub fn ehlo(&mut self, hostname: &str) -> SmtpResult { - self.command(&format!("EHLO {}", hostname)) + /// Sends an SMTP command + pub fn smtp_command(&mut self, command: C) -> SmtpResult { + self.send_server(&command.to_string(), "") } /// Sends a MAIL command @@ -161,106 +163,29 @@ impl Client { self.command(&format!("RCPT TO:<{}>", address)) } - /// Sends a DATA command - pub fn data(&mut self) -> SmtpResult { - self.command("DATA") - } + /// Sends an AUTH command with the given mechanism, and handles challenge if needed + pub fn auth(&mut self, mechanism: Mechanism, credentials: &Credentials) -> SmtpResult { - /// Sends a QUIT command - pub fn quit(&mut self) -> SmtpResult { - self.command("QUIT") - } + // TODO + let mut challenges = 10; + let mut response = self.smtp_command( + AuthCommand::new(mechanism, credentials.clone(), None)?, + )?; - /// Sends a NOOP command - pub fn noop(&mut self) -> SmtpResult { - self.command("NOOP") - } - - /// Sends a HELP command - pub fn help(&mut self, argument: Option<&str>) -> SmtpResult { - match argument { - Some(argument) => self.command(&format!("HELP {}", argument)), - None => self.command("HELP"), - } - } - - /// Sends a VRFY command - pub fn vrfy(&mut self, address: &str) -> SmtpResult { - self.command(&format!("VRFY {}", address)) - } - - /// Sends a EXPN command - pub fn expn(&mut self, address: &str) -> SmtpResult { - self.command(&format!("EXPN {}", address)) - } - - /// Sends a RSET command - pub fn rset(&mut self) -> SmtpResult { - self.command("RSET") - } - - /// Sends an AUTH command with the given mechanism - pub fn auth(&mut self, mechanism: Mechanism, username: &str, password: &str) -> SmtpResult { - - if mechanism.supports_initial_response() { - self.command(&format!( - "AUTH {} {}", + while challenges > 0 && response.has_code(334) { + challenges -= 1; + response = self.smtp_command(AuthCommand::new_from_response( mechanism, - base64::encode_config( - try!(mechanism.response(username, password, None)) - .as_bytes(), - base64::STANDARD, - ) - )) - } else { - let encoded_challenge = match try!(self.command(&format!("AUTH {}", mechanism))) - .first_word() { - Some(challenge) => challenge.to_string(), - None => return Err(Error::ResponseParsing("Could not read auth challenge")), - }; - - debug!("auth encoded challenge: {}", encoded_challenge); - - let decoded_challenge = match base64::decode(&encoded_challenge) { - Ok(challenge) => { - match String::from_utf8(challenge) { - Ok(value) => value, - Err(error) => return Err(Error::Utf8Parsing(error)), - } - } - Err(error) => return Err(Error::ChallengeParsing(error)), - }; - - debug!("auth decoded challenge: {}", decoded_challenge); - - let mut challenge_expected = 3; - - while challenge_expected > 0 { - let response = try!( - self.command(&base64::encode_config( - &try!(mechanism.response( - username, - password, - Some(&decoded_challenge), - )).as_bytes(), - base64::STANDARD, - )) - ); - - if !response.has_code(334) { - return Ok(response); - } - - challenge_expected -= 1; - } - - Err(Error::ResponseParsing("Unexpected number of challenges")) + credentials.clone(), + response, + )?)?; } - } - /// Sends a STARTTLS command - pub fn starttls(&mut self) -> SmtpResult { - self.command("STARTTLS") + if challenges == 0 { + Err(Error::ResponseParsing("Unexpected number of challenges")) + } else { + Ok(response) + } } /// Sends the message content diff --git a/lettre/src/smtp/commands.rs b/lettre/src/smtp/commands.rs new file mode 100644 index 0000000..6d21eba --- /dev/null +++ b/lettre/src/smtp/commands.rs @@ -0,0 +1,428 @@ +//! SMTP commands + +use base64; +use emailaddress::EmailAddress; +use smtp::CRLF; +use smtp::authentication::{Credentials, Mechanism}; +use smtp::error::Error; +use smtp::extension::{MailParameter, RcptParameter}; +use smtp::extension::ClientId; +use smtp::response::Response; +use std::fmt; +use std::fmt::{Display, Formatter}; + +/// EHLO command +#[derive(PartialEq, Clone, Debug)] +pub struct EhloCommand { + client_id: ClientId, +} + +impl Display for EhloCommand { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "EHLO {}", self.client_id)?; + f.write_str(CRLF) + } +} + +impl EhloCommand { + /// Creates a EHLO command + pub fn new(client_id: ClientId) -> EhloCommand { + EhloCommand { client_id: client_id } + } +} + +/// STARTTLS command +#[derive(PartialEq, Clone, Debug)] +pub struct StarttlsCommand; + +impl Display for StarttlsCommand { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("STARTTLS")?; + f.write_str(CRLF) + } +} + +/// MAIL command +#[derive(PartialEq, Clone, Debug)] +pub struct MailCommand { + sender: Option, + parameters: Vec, +} + +impl Display for MailCommand { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!( + f, + "MAIL FROM:<{}>", + match self.sender { + Some(ref address) => address.to_string(), + None => "".to_string(), + } + )?; + for parameter in &self.parameters { + write!(f, " {}", parameter)?; + } + f.write_str(CRLF) + } +} + +impl MailCommand { + /// Creates a MAIL command + pub fn new(sender: Option, parameters: Vec) -> MailCommand { + MailCommand { + sender: sender, + parameters: parameters, + } + } +} + +/// RCPT command +#[derive(PartialEq, Clone, Debug)] +pub struct RcptCommand { + recipient: EmailAddress, + parameters: Vec, +} + +impl Display for RcptCommand { + 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(CRLF) + } +} + +impl RcptCommand { + /// Creates an RCPT command + pub fn new(recipient: EmailAddress, parameters: Vec) -> RcptCommand { + RcptCommand { + recipient: recipient, + parameters: parameters, + } + } +} + +/// DATA command +#[derive(PartialEq, Clone, Debug)] +pub struct DataCommand; + +impl Display for DataCommand { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("DATA")?; + f.write_str(CRLF) + } +} + +/// QUIT command +#[derive(PartialEq, Clone, Debug)] +pub struct QuitCommand; + +impl Display for QuitCommand { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("QUIT")?; + f.write_str(CRLF) + } +} + +/// NOOP command +#[derive(PartialEq, Clone, Debug)] +pub struct NoopCommand; + +impl Display for NoopCommand { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("NOOP")?; + f.write_str(CRLF) + } +} + +/// HELP command +#[derive(PartialEq, Clone, Debug)] +pub struct HelpCommand { + argument: Option, +} + +impl Display for HelpCommand { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("HELP")?; + if self.argument.is_some() { + write!(f, " {}", self.argument.as_ref().unwrap())?; + } + f.write_str(CRLF) + } +} + +impl HelpCommand { + /// Creates an HELP command + pub fn new(argument: Option) -> HelpCommand { + HelpCommand { argument: argument } + } +} + +/// VRFY command +#[derive(PartialEq, Clone, Debug)] +pub struct VrfyCommand { + argument: String, +} + +impl Display for VrfyCommand { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "VRFY {}", self.argument)?; + f.write_str(CRLF) + } +} + +impl VrfyCommand { + /// Creates a VRFY command + pub fn new(argument: String) -> VrfyCommand { + VrfyCommand { argument: argument } + } +} + +/// EXPN command +#[derive(PartialEq, Clone, Debug)] +pub struct ExpnCommand { + argument: String, +} + +impl Display for ExpnCommand { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "EXPN {}", self.argument)?; + f.write_str(CRLF) + } +} + +impl ExpnCommand { + /// Creates an EXPN command + pub fn new(argument: String) -> ExpnCommand { + ExpnCommand { argument: argument } + } +} + +/// RSET command +#[derive(PartialEq, Clone, Debug)] +pub struct RsetCommand; + +impl Display for RsetCommand { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str("RSET")?; + f.write_str(CRLF) + } +} + +/// AUTH command +#[derive(PartialEq, Clone, Debug)] +pub struct AuthCommand { + mechanism: Mechanism, + credentials: Credentials, + challenge: Option, + response: Option, +} + +impl Display for AuthCommand { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let encoded_response = if self.response.is_some() { + Some(base64::encode_config( + self.response.as_ref().unwrap().as_bytes(), + base64::STANDARD, + )) + } else { + None + }; + + 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(CRLF) + } +} + +impl AuthCommand { + /// 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_ref().map(String::as_str), + )?) + } else { + None + }; + Ok(AuthCommand { + mechanism: mechanism, + credentials: credentials, + challenge: challenge, + response: 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::ResponseParsing("Expecting a challenge")); + } + + let encoded_challenge = match response.first_word() { + Some(challenge) => challenge.to_string(), + None => return Err(Error::ResponseParsing("Could not read auth challenge")), + }; + + debug!("auth encoded challenge: {}", encoded_challenge); + + let decoded_challenge = match base64::decode(&encoded_challenge) { + Ok(challenge) => { + match String::from_utf8(challenge) { + Ok(value) => value, + Err(error) => return Err(Error::Utf8Parsing(error)), + } + } + Err(error) => return Err(Error::ChallengeParsing(error)), + }; + + debug!("auth decoded challenge: {}", decoded_challenge); + + let response = Some(mechanism.response( + &credentials, + Some(decoded_challenge.as_ref()), + )?); + + Ok(AuthCommand { + mechanism: mechanism, + credentials: credentials, + challenge: Some(decoded_challenge), + response: response, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use smtp::extension::MailBodyParameter; + use smtp::response::Code; + use std::str::FromStr; + + #[test] + fn test_display() { + let id = ClientId::Domain("localhost".to_string()); + let email = EmailAddress::new("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!("{}", EhloCommand::new(id)), "EHLO localhost\r\n"); + assert_eq!( + format!("{}", MailCommand::new(Some(email.clone()), vec![])), + "MAIL FROM:\r\n" + ); + assert_eq!( + format!("{}", MailCommand::new(None, vec![])), + "MAIL FROM:<>\r\n" + ); + assert_eq!( + format!( + "{}", + MailCommand::new(Some(email.clone()), vec![MailParameter::Size(42)]) + ), + "MAIL FROM: SIZE=42\r\n" + ); + assert_eq!( + format!( + "{}", + MailCommand::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!("{}", RcptCommand::new(email.clone(), vec![])), + "RCPT TO:\r\n" + ); + assert_eq!( + format!("{}", RcptCommand::new(email.clone(), vec![rcpt_parameter])), + "RCPT TO: TEST=value\r\n" + ); + assert_eq!(format!("{}", QuitCommand), "QUIT\r\n"); + assert_eq!(format!("{}", DataCommand), "DATA\r\n"); + assert_eq!(format!("{}", NoopCommand), "NOOP\r\n"); + assert_eq!(format!("{}", HelpCommand::new(None)), "HELP\r\n"); + assert_eq!( + format!("{}", HelpCommand::new(Some("test".to_string()))), + "HELP test\r\n" + ); + assert_eq!( + format!("{}", VrfyCommand::new("test".to_string())), + "VRFY test\r\n" + ); + assert_eq!( + format!("{}", ExpnCommand::new("test".to_string())), + "EXPN test\r\n" + ); + assert_eq!(format!("{}", RsetCommand), "RSET\r\n"); + let credentials = Credentials::new("user".to_string(), "password".to_string()); + assert_eq!( + format!( + "{}", + AuthCommand::new(Mechanism::Plain, credentials.clone(), None).unwrap() + ), + "AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n" + ); + assert_eq!( + format!( + "{}", + AuthCommand::new( + Mechanism::CramMd5, + credentials.clone(), + Some("test".to_string()), + ).unwrap() + ), + "dXNlciAzMTYxY2NmZDdmMjNlMzJiYmMzZTQ4NjdmYzk0YjE4Nw==\r\n" + ); + assert_eq!( + format!( + "{}", + AuthCommand::new(Mechanism::Login, credentials.clone(), None).unwrap() + ), + "AUTH LOGIN\r\n" + ); + assert_eq!( + format!( + "{}", + AuthCommand::new_from_response( + Mechanism::CramMd5, + credentials.clone(), + Response::new(Code::from_str("334").unwrap(), vec!["dGVzdAo=".to_string()]), + ).unwrap() + ), + "dXNlciA1NTIzNThiMzExOWFjOWNkYzM2YWRiN2MxNWRmMWJkNw==\r\n" + ); + } +} diff --git a/lettre/src/smtp/extension.rs b/lettre/src/smtp/extension.rs index 88ba165..4b82e98 100644 --- a/lettre/src/smtp/extension.rs +++ b/lettre/src/smtp/extension.rs @@ -3,12 +3,14 @@ use smtp::authentication::Mechanism; use smtp::error::Error; use smtp::response::Response; +use smtp::util::XText; use std::collections::HashSet; use std::fmt; use std::fmt::{Display, Formatter}; use std::net::{Ipv4Addr, Ipv6Addr}; use std::result::Result; + /// Client identifier, the parameter to `EHLO` #[derive(PartialEq, Eq, Clone, Debug)] pub enum ClientId { @@ -156,6 +158,84 @@ impl ServerInfo { } } +/// A `MAIL FROM` extension parameter +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum MailParameter { + /// `BODY` parameter + Body(MailBodyParameter), + /// `SIZE` parameter + Size(usize), + /// Custom parameter + Other { + /// Parameter keyword + keyword: String, + /// Parameter value + value: Option, + }, +} + +impl Display for MailParameter { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match *self { + MailParameter::Body(ref value) => write!(f, "BODY={}", value), + MailParameter::Size(size) => write!(f, "SIZE={}", size), + MailParameter::Other { + ref keyword, + value: Some(ref value), + } => write!(f, "{}={}", keyword, XText(value)), + MailParameter::Other { + ref keyword, + value: None, + } => f.write_str(keyword), + } + } +} + +/// Values for the `BODY` parameter to `MAIL FROM` +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum MailBodyParameter { + /// `7BIT` + SevenBit, + /// `8BITMIME` + EightBitMime, +} + +impl Display for MailBodyParameter { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match *self { + MailBodyParameter::SevenBit => f.write_str("7BIT"), + MailBodyParameter::EightBitMime => f.write_str("8BITMIME"), + } + } +} + +/// A `RCPT TO` extension parameter +#[derive(PartialEq, Eq, Clone, Debug)] +pub enum RcptParameter { + /// Custom parameter + Other { + /// Parameter keyword + keyword: String, + /// Parameter value + value: Option, + }, +} + +impl Display for RcptParameter { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match *self { + RcptParameter::Other { + ref keyword, + value: Some(ref value), + } => write!(f, "{}={}", keyword, XText(value)), + RcptParameter::Other { + ref keyword, + value: None, + } => f.write_str(keyword), + } + } +} + #[cfg(test)] mod test { diff --git a/lettre/src/smtp/mod.rs b/lettre/src/smtp/mod.rs index 5bee0d9..355093d 100644 --- a/lettre/src/smtp/mod.rs +++ b/lettre/src/smtp/mod.rs @@ -43,7 +43,7 @@ //! ```rust,no_run //! use lettre::smtp::{SecurityLevel, SmtpTransport, //! SmtpTransportBuilder}; -//! use lettre::smtp::authentication::Mechanism; +//! use lettre::smtp::authentication::{Credentials, Mechanism}; //! use lettre::smtp::SUBMISSION_PORT; //! use lettre::{SimpleSendableEmail, EmailTransport}; //! use lettre::smtp::extension::ClientId; @@ -61,7 +61,7 @@ //! // Set the name sent during EHLO/HELO, default is `localhost` //! .hello_name(ClientId::Domain("my.hostname.tld".to_string())) //! // Add credentials for authentication -//! .credentials("username", "password") +//! .credentials(Credentials::new("username".to_string(), "password".to_string())) //! // Specify a TLS security level. You can also specify an SslContext with //! // .ssl_context(SslContext::Ssl23) //! .security_level(SecurityLevel::AlwaysEncrypt) @@ -92,34 +92,38 @@ //! use lettre::smtp::SMTP_PORT; //! use lettre::smtp::client::Client; //! use lettre::smtp::client::net::NetworkStream; +//! use lettre::smtp::extension::ClientId; +//! use lettre::smtp::commands::*; //! //! let mut email_client: Client = Client::new(); //! let _ = email_client.connect(&("localhost", SMTP_PORT), None); -//! let _ = email_client.ehlo("my_hostname"); +//! let _ = email_client.smtp_command(EhloCommand::new(ClientId::new("my_hostname".to_string()))); //! let _ = email_client.mail("user@example.com", None); //! let _ = email_client.rcpt("user@example.org"); -//! let _ = email_client.data(); +//! let _ = email_client.smtp_command(DataCommand); //! let _ = email_client.message("Test email"); -//! let _ = email_client.quit(); +//! let _ = email_client.smtp_command(QuitCommand); //! ``` use EmailTransport; use SendableEmail; use openssl::ssl::{SslContext, SslMethod}; -use smtp::authentication::Mechanism; +use smtp::authentication::{Credentials, Mechanism}; use smtp::client::Client; +use smtp::commands::*; use smtp::error::{Error, SmtpResult}; use smtp::extension::{ClientId, Extension, ServerInfo}; use std::net::{SocketAddr, ToSocketAddrs}; -use std::string::String; use std::time::Duration; pub mod extension; +pub mod commands; pub mod authentication; pub mod response; pub mod client; pub mod error; +pub mod util; // Registrated port numbers: // https://www.iana. @@ -179,7 +183,7 @@ pub struct SmtpTransportBuilder { /// Name sent during HELO or EHLO hello_name: ClientId, /// Credentials - credentials: Option<(String, String)>, + credentials: Option, /// Socket we are connecting to server_addr: SocketAddr, /// SSL context to use @@ -278,12 +282,8 @@ impl SmtpTransportBuilder { } /// Set the client credentials - pub fn credentials>( - mut self, - username: S, - password: S, - ) -> SmtpTransportBuilder { - self.credentials = Some((username.into(), password.into())); + pub fn credentials>(mut self, credentials: S) -> SmtpTransportBuilder { + self.credentials = Some(credentials.into()); self } @@ -379,7 +379,9 @@ impl SmtpTransport { pub fn get_ehlo(&mut self) -> SmtpResult { // Extended Hello let ehlo_response = try_smtp!( - self.client.ehlo(&self.client_info.hello_name.to_string()), + self.client.smtp_command(EhloCommand::new( + ClientId::new(self.client_info.hello_name.to_string()), + )), self ); @@ -434,7 +436,7 @@ impl EmailTransport for SmtpTransport { (&SecurityLevel::NeverEncrypt, _) => (), (&SecurityLevel::EncryptedWrapper, _) => (), (_, true) => { - try_smtp!(self.client.starttls(), self); + try_smtp!(self.client.smtp_command(StarttlsCommand), self); try_smtp!( self.client.upgrade_tls_stream( &self.client_info.ssl_context, @@ -450,8 +452,6 @@ impl EmailTransport for SmtpTransport { } if self.client_info.credentials.is_some() { - let (username, password) = self.client_info.credentials.clone().unwrap(); - let mut found = false; // Compute accepted mechanism @@ -476,7 +476,13 @@ impl EmailTransport for SmtpTransport { ) { found = true; - try_smtp!(self.client.auth(mechanism, &username, &password), self); + try_smtp!( + self.client.auth( + mechanism, + &self.client_info.credentials.as_ref().unwrap(), + ), + self + ); break; } } @@ -514,7 +520,7 @@ impl EmailTransport for SmtpTransport { } // Data - try_smtp!(self.client.data(), self); + try_smtp!(self.client.smtp_command(DataCommand), self); // Message content let message = email.message(); diff --git a/lettre/src/smtp/util.rs b/lettre/src/smtp/util.rs new file mode 100644 index 0000000..0c08c0b --- /dev/null +++ b/lettre/src/smtp/util.rs @@ -0,0 +1,47 @@ +//! Utils for string manipulation + +use std::fmt::{Display, Formatter, Result as FmtResult}; + +/// Encode a string as xtext +#[derive(Debug)] +pub struct XText<'a>(pub &'a str); + +impl<'a> Display for XText<'a> { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + let mut rest = self.0; + while let Some(idx) = rest.find(|c| c < '!' || c == '+' || c == '=') { + let (start, end) = rest.split_at(idx); + f.write_str(start)?; + + let mut end_iter = end.char_indices(); + let (_, c) = end_iter.next().expect("char"); + write!(f, "+{:X}", c as u8)?; + + if let Some((idx, _)) = end_iter.next() { + rest = &end[idx..]; + } else { + rest = ""; + } + } + f.write_str(rest) + } +} + + +#[cfg(test)] +mod tests { + use super::XText; + + #[test] + fn test() { + for (input, expect) in vec![ + ("bjorn", "bjorn"), + ("bjørn", "bjørn"), + ("Ø+= ❤️‰", "Ø+2B+3D+20❤️‰"), + ("+", "+2B"), + ] + { + assert_eq!(format!("{}", XText(input)), expect); + } + } +}