feat(transport): Use types for SMTP commands
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,3 @@
|
||||
.project
|
||||
/target/
|
||||
target/
|
||||
/Cargo.lock
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
428
lettre/src/smtp/commands.rs
Normal 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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
47
lettre/src/smtp/util.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user