feat(transport): Use types for SMTP commands

This commit is contained in:
Alexis Mousset
2017-06-30 12:33:34 +02:00
parent b31fd465ad
commit 6840555473
11 changed files with 697 additions and 160 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,3 @@
.project
/target/
target/
/Cargo.lock

View File

@@ -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"

View File

@@ -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());
});

View File

@@ -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);

View File

@@ -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]

View File

@@ -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<S: Into<String>, T: Into<String>> 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<String, Error> {
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());
}
}

View File

@@ -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<S: Write + Read> Client<S> {
impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
/// 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<S: Connector + Write + Read + Timeout + Debug> Client<S> {
/// 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<S: Connector + Write + Read + Timeout + Debug> Client<S> {
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<C: Display>(&mut self, command: C) -> SmtpResult {
self.send_server(&command.to_string(), "")
}
/// Sends a MAIL command
@@ -161,106 +163,29 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
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

428
lettre/src/smtp/commands.rs Normal file
View File

@@ -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<EmailAddress>,
parameters: Vec<MailParameter>,
}
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<EmailAddress>, parameters: Vec<MailParameter>) -> MailCommand {
MailCommand {
sender: sender,
parameters: parameters,
}
}
}
/// RCPT command
#[derive(PartialEq, Clone, Debug)]
pub struct RcptCommand {
recipient: EmailAddress,
parameters: Vec<RcptParameter>,
}
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<RcptParameter>) -> 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<String>,
}
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<String>) -> 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<String>,
response: Option<String>,
}
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<String>,
) -> Result<AuthCommand, Error> {
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<AuthCommand, Error> {
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:<test@example.com>\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:<test@example.com> SIZE=42\r\n"
);
assert_eq!(
format!(
"{}",
MailCommand::new(
Some(email.clone()),
vec![
MailParameter::Size(42),
MailParameter::Body(MailBodyParameter::EightBitMime),
mail_parameter,
],
)
),
"MAIL FROM:<test@example.com> SIZE=42 BODY=8BITMIME TEST=value\r\n"
);
assert_eq!(
format!("{}", RcptCommand::new(email.clone(), vec![])),
"RCPT TO:<test@example.com>\r\n"
);
assert_eq!(
format!("{}", RcptCommand::new(email.clone(), vec![rcpt_parameter])),
"RCPT TO:<test@example.com> 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"
);
}
}

View File

@@ -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<String>,
},
}
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<String>,
},
}
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 {

View File

@@ -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<NetworkStream> = 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<Credentials>,
/// 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<S: Into<String>>(
mut self,
username: S,
password: S,
) -> SmtpTransportBuilder {
self.credentials = Some((username.into(), password.into()));
pub fn credentials<S: Into<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<SmtpResult> 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<SmtpResult> 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<SmtpResult> 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<SmtpResult> for SmtpTransport {
}
// Data
try_smtp!(self.client.data(), self);
try_smtp!(self.client.smtp_command(DataCommand), self);
// Message content
let message = email.message();

47
lettre/src/smtp/util.rs Normal file
View File

@@ -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);
}
}
}