feat(transport): More precise error descriptions
This commit is contained in:
@@ -4,10 +4,12 @@ use lettre::{EmailTransport, SimpleSendableEmail};
|
||||
use lettre::smtp::SmtpTransportBuilder;
|
||||
|
||||
fn main() {
|
||||
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",
|
||||
);
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mut mailer = SmtpTransportBuilder::localhost().unwrap().build();
|
||||
|
||||
@@ -64,10 +64,12 @@ impl EmailTransport<FileResult> for FileEmailTransport {
|
||||
|
||||
let mut f = try!(File::create(file.as_path()));
|
||||
|
||||
let log_line = format!("{}: from=<{}> to=<{}>\n",
|
||||
email.message_id(),
|
||||
email.from(),
|
||||
email.to().join("> to=<"));
|
||||
let log_line = format!(
|
||||
"{}: from=<{}> to=<{}>\n",
|
||||
email.message_id(),
|
||||
email.from(),
|
||||
email.to().join("> to=<")
|
||||
);
|
||||
|
||||
try!(f.write_all(log_line.as_bytes()));
|
||||
try!(f.write_all(email.message().as_bytes()));
|
||||
|
||||
@@ -54,11 +54,12 @@ pub struct SimpleSendableEmail {
|
||||
|
||||
impl SimpleSendableEmail {
|
||||
/// Returns a new email
|
||||
pub fn new(from_address: &str,
|
||||
to_addresses: Vec<&str>,
|
||||
message_id: &str,
|
||||
message: &str)
|
||||
-> SimpleSendableEmail {
|
||||
pub fn new(
|
||||
from_address: &str,
|
||||
to_addresses: Vec<&str>,
|
||||
message_id: &str,
|
||||
message: &str,
|
||||
) -> SimpleSendableEmail {
|
||||
SimpleSendableEmail {
|
||||
from: from_address.to_string(),
|
||||
to: to_addresses.iter().map(|s| s.to_string()).collect(),
|
||||
|
||||
@@ -45,17 +45,17 @@ impl SendmailTransport {
|
||||
impl EmailTransport<SendmailResult> for SendmailTransport {
|
||||
fn send<T: SendableEmail>(&mut self, email: T) -> SendmailResult {
|
||||
// Spawn the sendmail command
|
||||
let mut process = try!(Command::new(&self.command)
|
||||
.args(&["-i", "-f", &email.from(), &email.to().join(" ")])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn());
|
||||
let mut process = try!(
|
||||
Command::new(&self.command)
|
||||
.args(&["-i", "-f", &email.from(), &email.to().join(" ")])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
);
|
||||
|
||||
match process
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(email.message().as_bytes()) {
|
||||
match process.stdin.as_mut().unwrap().write_all(
|
||||
email.message().as_bytes(),
|
||||
) {
|
||||
Ok(_) => (),
|
||||
Err(error) => return Err(From::from(error)),
|
||||
}
|
||||
|
||||
@@ -26,11 +26,15 @@ pub enum Mechanism {
|
||||
|
||||
impl Display for Mechanism {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "{}", match *self {
|
||||
Mechanism::Plain => "PLAIN",
|
||||
Mechanism::Login => "LOGIN",
|
||||
Mechanism::CramMd5 => "CRAM-MD5",
|
||||
})
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match *self {
|
||||
Mechanism::Plain => "PLAIN",
|
||||
Mechanism::Login => "LOGIN",
|
||||
Mechanism::CramMd5 => "CRAM-MD5",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,11 +50,12 @@ impl Mechanism {
|
||||
|
||||
/// Returns the string to send to the server, using the provided username, password and
|
||||
/// challenge in some cases
|
||||
pub fn response(&self,
|
||||
username: &str,
|
||||
password: &str,
|
||||
challenge: Option<&str>)
|
||||
-> Result<String, Error> {
|
||||
pub fn response(
|
||||
&self,
|
||||
username: &str,
|
||||
password: &str,
|
||||
challenge: Option<&str>,
|
||||
) -> Result<String, Error> {
|
||||
match *self {
|
||||
Mechanism::Plain => {
|
||||
match challenge {
|
||||
@@ -97,25 +102,33 @@ mod test {
|
||||
fn test_plain() {
|
||||
let mechanism = Mechanism::Plain;
|
||||
|
||||
assert_eq!(mechanism.response("username", "password", None).unwrap(),
|
||||
"\u{0}username\u{0}password");
|
||||
assert!(mechanism
|
||||
.response("username", "password", Some("test"))
|
||||
.is_err());
|
||||
assert_eq!(
|
||||
mechanism.response("username", "password", None).unwrap(),
|
||||
"\u{0}username\u{0}password"
|
||||
);
|
||||
assert!(
|
||||
mechanism
|
||||
.response("username", "password", Some("test"))
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_login() {
|
||||
let mechanism = Mechanism::Login;
|
||||
|
||||
assert_eq!(mechanism
|
||||
.response("alice", "wonderland", Some("Username"))
|
||||
.unwrap(),
|
||||
"alice");
|
||||
assert_eq!(mechanism
|
||||
.response("alice", "wonderland", Some("Password"))
|
||||
.unwrap(),
|
||||
"wonderland");
|
||||
assert_eq!(
|
||||
mechanism
|
||||
.response("alice", "wonderland", Some("Username"))
|
||||
.unwrap(),
|
||||
"alice"
|
||||
);
|
||||
assert_eq!(
|
||||
mechanism
|
||||
.response("alice", "wonderland", Some("Password"))
|
||||
.unwrap(),
|
||||
"wonderland"
|
||||
);
|
||||
assert!(mechanism.response("username", "password", None).is_err());
|
||||
}
|
||||
|
||||
@@ -123,12 +136,16 @@ mod test {
|
||||
fn test_cram_md5() {
|
||||
let mechanism = Mechanism::CramMd5;
|
||||
|
||||
assert_eq!(mechanism
|
||||
.response("alice",
|
||||
"wonderland",
|
||||
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=="))
|
||||
.unwrap(),
|
||||
"alice a540ebe4ef2304070bbc3c456c1f64c0");
|
||||
assert_eq!(
|
||||
mechanism
|
||||
.response(
|
||||
"alice",
|
||||
"wonderland",
|
||||
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=="),
|
||||
)
|
||||
.unwrap(),
|
||||
"alice a540ebe4ef2304070bbc3c456c1f64c0"
|
||||
);
|
||||
assert!(mechanism.response("alice", "wonderland", None).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,10 +107,11 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
|
||||
}
|
||||
|
||||
/// Connects to the configured server
|
||||
pub fn connect<A: ToSocketAddrs>(&mut self,
|
||||
addr: &A,
|
||||
ssl_context: Option<&SslContext>)
|
||||
-> SmtpResult {
|
||||
pub fn connect<A: ToSocketAddrs>(
|
||||
&mut self,
|
||||
addr: &A,
|
||||
ssl_context: Option<&SslContext>,
|
||||
) -> SmtpResult {
|
||||
// Connect should not be called when the client is already connected
|
||||
if self.stream.is_some() {
|
||||
return_err!("The connection is already established", self);
|
||||
@@ -202,16 +203,18 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
|
||||
pub fn auth(&mut self, mechanism: Mechanism, username: &str, password: &str) -> SmtpResult {
|
||||
|
||||
if mechanism.supports_initial_response() {
|
||||
self.command(&format!("AUTH {} {}",
|
||||
mechanism,
|
||||
base64::encode_config(try!(mechanism.response(username,
|
||||
password,
|
||||
None))
|
||||
.as_bytes(),
|
||||
base64::STANDARD)))
|
||||
self.command(&format!(
|
||||
"AUTH {} {}",
|
||||
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() {
|
||||
.first_word() {
|
||||
Some(challenge) => challenge,
|
||||
None => return Err(Error::ResponseParsing("Could not read auth challenge")),
|
||||
};
|
||||
@@ -233,12 +236,16 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
|
||||
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)));
|
||||
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);
|
||||
@@ -317,15 +324,19 @@ mod test {
|
||||
fn test_remove_crlf() {
|
||||
assert_eq!(remove_crlf("\r\n"), "");
|
||||
assert_eq!(remove_crlf("EHLO my_name\r\n"), "EHLO my_name");
|
||||
assert_eq!(remove_crlf("EHLO my_name\r\nSIZE 42\r\n"),
|
||||
"EHLO my_nameSIZE 42");
|
||||
assert_eq!(
|
||||
remove_crlf("EHLO my_name\r\nSIZE 42\r\n"),
|
||||
"EHLO my_nameSIZE 42"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_crlf() {
|
||||
assert_eq!(escape_crlf("\r\n"), "<CR><LF>");
|
||||
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CR><LF>");
|
||||
assert_eq!(escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
|
||||
"EHLO my_name<CR><LF>SIZE 42<CR><LF>");
|
||||
assert_eq!(
|
||||
escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
|
||||
"EHLO my_name<CR><LF>SIZE 42<CR><LF>"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,9 @@ impl NetworkStream {
|
||||
NetworkStream::Tcp(ref s) => s.peer_addr(),
|
||||
NetworkStream::Ssl(ref s) => s.get_ref().peer_addr(),
|
||||
NetworkStream::Mock(_) => {
|
||||
Ok(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 80)))
|
||||
Ok(SocketAddr::V4(
|
||||
SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 80),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,9 +89,11 @@ impl Connector for NetworkStream {
|
||||
Some(context) => {
|
||||
match Ssl::new(context) {
|
||||
Ok(ssl) => {
|
||||
ssl.connect(tcp_stream)
|
||||
.map(NetworkStream::Ssl)
|
||||
.map_err(|e| io::Error::new(ErrorKind::Other, e))
|
||||
ssl.connect(tcp_stream).map(NetworkStream::Ssl).map_err(
|
||||
|e| {
|
||||
io::Error::new(ErrorKind::Other, e)
|
||||
},
|
||||
)
|
||||
}
|
||||
Err(e) => Err(io::Error::new(ErrorKind::Other, e)),
|
||||
}
|
||||
|
||||
@@ -43,14 +43,26 @@ impl Display for Error {
|
||||
impl StdError for Error {
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
Transient(_) => "a transient error occured during the SMTP transaction",
|
||||
Permanent(_) => "a permanent error occured during the SMTP transaction",
|
||||
ResponseParsing(_) => "an error occured while parsing an SMTP response",
|
||||
ChallengeParsing(_) => "an error occured while parsing an SMTP AUTH challenge",
|
||||
Utf8Parsing(_) => "an error occured while parsing an SMTP response as UTF8",
|
||||
// Try to display the first line of the server's response that usually
|
||||
// contains a short humanly readable error message
|
||||
Transient(ref e) => {
|
||||
match e.first_line() {
|
||||
Some(line) => line,
|
||||
None => "undetailed transient error during SMTP transaction",
|
||||
}
|
||||
}
|
||||
Permanent(ref e) => {
|
||||
match e.first_line() {
|
||||
Some(line) => line,
|
||||
None => "undetailed permanent error during SMTP transaction",
|
||||
}
|
||||
}
|
||||
ResponseParsing(ref e) => e,
|
||||
ChallengeParsing(ref e) => e.description(),
|
||||
Utf8Parsing(ref e) => e.description(),
|
||||
Resolution => "could not resolve hostname",
|
||||
Client(_) => "an unknown error occured",
|
||||
Io(_) => "an I/O error occured",
|
||||
Client(ref e) => e,
|
||||
Io(ref e) => e.description(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,14 +53,16 @@ pub struct ServerInfo {
|
||||
|
||||
impl Display for ServerInfo {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f,
|
||||
"{} with {}",
|
||||
self.name,
|
||||
if self.features.is_empty() {
|
||||
"no supported features".to_string()
|
||||
} else {
|
||||
format!("{:?}", self.features)
|
||||
})
|
||||
write!(
|
||||
f,
|
||||
"{} with {}",
|
||||
self.name,
|
||||
if self.features.is_empty() {
|
||||
"no supported features".to_string()
|
||||
} else {
|
||||
format!("{:?}", self.features)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,9 +107,9 @@ impl ServerInfo {
|
||||
}
|
||||
|
||||
Ok(ServerInfo {
|
||||
name: name,
|
||||
features: features,
|
||||
})
|
||||
name: name,
|
||||
features: features,
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks if the server supports an ESMTP feature
|
||||
@@ -117,8 +119,9 @@ impl ServerInfo {
|
||||
|
||||
/// Checks if the server supports an ESMTP feature
|
||||
pub fn supports_auth_mechanism(&self, mechanism: Mechanism) -> bool {
|
||||
self.features
|
||||
.contains(&Extension::Authentication(mechanism))
|
||||
self.features.contains(
|
||||
&Extension::Authentication(mechanism),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,10 +135,14 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_extension_fmt() {
|
||||
assert_eq!(format!("{}", Extension::EightBitMime),
|
||||
"8BITMIME".to_string());
|
||||
assert_eq!(format!("{}", Extension::Authentication(Mechanism::Plain)),
|
||||
"AUTH PLAIN".to_string());
|
||||
assert_eq!(
|
||||
format!("{}", Extension::EightBitMime),
|
||||
"8BITMIME".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Extension::Authentication(Mechanism::Plain)),
|
||||
"AUTH PLAIN".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -143,40 +150,55 @@ mod test {
|
||||
let mut eightbitmime = HashSet::new();
|
||||
assert!(eightbitmime.insert(Extension::EightBitMime));
|
||||
|
||||
assert_eq!(format!("{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
features: eightbitmime.clone(),
|
||||
}),
|
||||
"name with {EightBitMime}".to_string());
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
features: eightbitmime.clone(),
|
||||
}
|
||||
),
|
||||
"name with {EightBitMime}".to_string()
|
||||
);
|
||||
|
||||
let empty = HashSet::new();
|
||||
|
||||
assert_eq!(format!("{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
features: empty,
|
||||
}),
|
||||
"name with no supported features".to_string());
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
features: empty,
|
||||
}
|
||||
),
|
||||
"name with no supported features".to_string()
|
||||
);
|
||||
|
||||
let mut plain = HashSet::new();
|
||||
assert!(plain.insert(Extension::Authentication(Mechanism::Plain)));
|
||||
|
||||
assert_eq!(format!("{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
features: plain.clone(),
|
||||
}),
|
||||
"name with {Authentication(Plain)}".to_string());
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
features: plain.clone(),
|
||||
}
|
||||
),
|
||||
"name with {Authentication(Plain)}".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serverinfo() {
|
||||
let response =
|
||||
Response::new(Code::new(Severity::PositiveCompletion, Category::Unspecified4, 1),
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()]);
|
||||
let response = Response::new(
|
||||
Code::new(Severity::PositiveCompletion, Category::Unspecified4, 1),
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
);
|
||||
|
||||
let mut features = HashSet::new();
|
||||
assert!(features.insert(Extension::EightBitMime));
|
||||
@@ -192,17 +214,24 @@ mod test {
|
||||
assert!(!server_info.supports_feature(&Extension::StartTls));
|
||||
assert!(!server_info.supports_auth_mechanism(Mechanism::CramMd5));
|
||||
|
||||
let response2 =
|
||||
Response::new(Code::new(Severity::PositiveCompletion, Category::Unspecified4, 1),
|
||||
vec!["me".to_string(),
|
||||
"AUTH PLAIN CRAM-MD5 OTHER".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()]);
|
||||
let response2 = Response::new(
|
||||
Code::new(Severity::PositiveCompletion, Category::Unspecified4, 1),
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"AUTH PLAIN CRAM-MD5 OTHER".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
);
|
||||
|
||||
let mut features2 = HashSet::new();
|
||||
assert!(features2.insert(Extension::EightBitMime));
|
||||
assert!(features2.insert(Extension::Authentication(Mechanism::Plain)));
|
||||
assert!(features2.insert(Extension::Authentication(Mechanism::CramMd5)));
|
||||
assert!(features2.insert(
|
||||
Extension::Authentication(Mechanism::Plain),
|
||||
));
|
||||
assert!(features2.insert(
|
||||
Extension::Authentication(Mechanism::CramMd5),
|
||||
));
|
||||
|
||||
let server_info2 = ServerInfo {
|
||||
name: "me".to_string(),
|
||||
|
||||
@@ -203,17 +203,17 @@ impl SmtpTransportBuilder {
|
||||
match addresses.next() {
|
||||
Some(addr) => {
|
||||
Ok(SmtpTransportBuilder {
|
||||
server_addr: addr,
|
||||
ssl_context: SslContext::builder(SslMethod::tls()).unwrap().build(),
|
||||
security_level: SecurityLevel::AlwaysEncrypt,
|
||||
smtp_utf8: false,
|
||||
credentials: None,
|
||||
connection_reuse_count_limit: 100,
|
||||
connection_reuse: false,
|
||||
hello_name: "localhost".to_string(),
|
||||
authentication_mechanism: None,
|
||||
timeout: Some(Duration::new(60, 0)),
|
||||
})
|
||||
server_addr: addr,
|
||||
ssl_context: SslContext::builder(SslMethod::tls()).unwrap().build(),
|
||||
security_level: SecurityLevel::AlwaysEncrypt,
|
||||
smtp_utf8: false,
|
||||
credentials: None,
|
||||
connection_reuse_count_limit: 100,
|
||||
connection_reuse: false,
|
||||
hello_name: "localhost".to_string(),
|
||||
authentication_mechanism: None,
|
||||
timeout: Some(Duration::new(60, 0)),
|
||||
})
|
||||
}
|
||||
None => Err(From::from("Could not resolve hostname")),
|
||||
}
|
||||
@@ -277,10 +277,11 @@ impl SmtpTransportBuilder {
|
||||
}
|
||||
|
||||
/// Set the client credentials
|
||||
pub fn credentials<S: Into<String>>(mut self,
|
||||
username: S,
|
||||
password: S)
|
||||
-> SmtpTransportBuilder {
|
||||
pub fn credentials<S: Into<String>>(
|
||||
mut self,
|
||||
username: S,
|
||||
password: S,
|
||||
) -> SmtpTransportBuilder {
|
||||
self.credentials = Some((username.into(), password.into()));
|
||||
self
|
||||
}
|
||||
@@ -416,11 +417,12 @@ impl EmailTransport<SmtpResult> for SmtpTransport {
|
||||
|
||||
try!(self.get_ehlo());
|
||||
|
||||
match (&self.client_info.security_level,
|
||||
self.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(&Extension::StartTls)) {
|
||||
match (
|
||||
&self.client_info.security_level,
|
||||
self.server_info.as_ref().unwrap().supports_feature(
|
||||
&Extension::StartTls,
|
||||
),
|
||||
) {
|
||||
(&SecurityLevel::AlwaysEncrypt, false) => {
|
||||
return Err(From::from("Could not encrypt connection, aborting"))
|
||||
}
|
||||
@@ -429,9 +431,12 @@ impl EmailTransport<SmtpResult> for SmtpTransport {
|
||||
(&SecurityLevel::EncryptedWrapper, _) => (),
|
||||
(_, true) => {
|
||||
try_smtp!(self.client.starttls(), self);
|
||||
try_smtp!(self.client
|
||||
.upgrade_tls_stream(&self.client_info.ssl_context),
|
||||
self);
|
||||
try_smtp!(
|
||||
self.client.upgrade_tls_stream(
|
||||
&self.client_info.ssl_context,
|
||||
),
|
||||
self
|
||||
);
|
||||
|
||||
debug!("connection encrypted");
|
||||
|
||||
@@ -462,10 +467,10 @@ impl EmailTransport<SmtpResult> for SmtpTransport {
|
||||
};
|
||||
|
||||
for mechanism in accepted_mechanisms {
|
||||
if self.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_auth_mechanism(mechanism) {
|
||||
if self.server_info.as_ref().unwrap().supports_auth_mechanism(
|
||||
mechanism,
|
||||
)
|
||||
{
|
||||
found = true;
|
||||
try_smtp!(self.client.auth(mechanism, &username, &password), self);
|
||||
break;
|
||||
@@ -479,14 +484,14 @@ impl EmailTransport<SmtpResult> for SmtpTransport {
|
||||
}
|
||||
|
||||
// Mail
|
||||
let mail_options = match (self.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(&Extension::EightBitMime),
|
||||
self.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(&Extension::SmtpUtfEight)) {
|
||||
let mail_options = match (
|
||||
self.server_info.as_ref().unwrap().supports_feature(
|
||||
&Extension::EightBitMime,
|
||||
),
|
||||
self.server_info.as_ref().unwrap().supports_feature(
|
||||
&Extension::SmtpUtfEight,
|
||||
),
|
||||
) {
|
||||
(true, true) => Some("BODY=8BITMIME SMTPUTF8"),
|
||||
(true, false) => Some("BODY=8BITMIME"),
|
||||
(false, _) => None,
|
||||
@@ -516,23 +521,26 @@ impl EmailTransport<SmtpResult> for SmtpTransport {
|
||||
self.state.connection_reuse_count += 1;
|
||||
|
||||
// Log the message
|
||||
info!("{}: conn_use={}, size={}, status=sent ({})",
|
||||
message_id,
|
||||
self.state.connection_reuse_count,
|
||||
message.len(),
|
||||
result
|
||||
.as_ref()
|
||||
.ok()
|
||||
.unwrap()
|
||||
.message()
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap_or(&"no response".to_string()));
|
||||
info!(
|
||||
"{}: conn_use={}, size={}, status=sent ({})",
|
||||
message_id,
|
||||
self.state.connection_reuse_count,
|
||||
message.len(),
|
||||
result
|
||||
.as_ref()
|
||||
.ok()
|
||||
.unwrap()
|
||||
.message()
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap_or(&"no response".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
// Test if we can reuse the existing connection
|
||||
if (!self.client_info.connection_reuse) ||
|
||||
(self.state.connection_reuse_count >= self.client_info.connection_reuse_count_limit) {
|
||||
(self.state.connection_reuse_count >= self.client_info.connection_reuse_count_limit)
|
||||
{
|
||||
self.reset();
|
||||
}
|
||||
|
||||
|
||||
@@ -29,19 +29,25 @@ impl FromStr for Severity {
|
||||
"3" => Ok(PositiveIntermediate),
|
||||
"4" => Ok(TransientNegativeCompletion),
|
||||
"5" => Ok(PermanentNegativeCompletion),
|
||||
_ => Err(Error::ResponseParsing("First digit must be between 2 and 5")),
|
||||
_ => Err(Error::ResponseParsing(
|
||||
"First digit must be between 2 and 5",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Severity {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
write!(f, "{}", match *self {
|
||||
PositiveCompletion => 2,
|
||||
PositiveIntermediate => 3,
|
||||
TransientNegativeCompletion => 4,
|
||||
PermanentNegativeCompletion => 5,
|
||||
})
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match *self {
|
||||
PositiveCompletion => 2,
|
||||
PositiveIntermediate => 3,
|
||||
TransientNegativeCompletion => 4,
|
||||
PermanentNegativeCompletion => 5,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,26 +78,32 @@ impl FromStr for Category {
|
||||
"3" => Ok(Unspecified3),
|
||||
"4" => Ok(Unspecified4),
|
||||
"5" => Ok(MailSystem),
|
||||
_ => Err(Error::ResponseParsing("Second digit must be between 0 and 5")),
|
||||
_ => Err(Error::ResponseParsing(
|
||||
"Second digit must be between 0 and 5",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Category {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
write!(f, "{}", match *self {
|
||||
Syntax => 0,
|
||||
Information => 1,
|
||||
Connections => 2,
|
||||
Unspecified3 => 3,
|
||||
Unspecified4 => 4,
|
||||
MailSystem => 5,
|
||||
})
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match *self {
|
||||
Syntax => 0,
|
||||
Information => 1,
|
||||
Connections => 2,
|
||||
Unspecified3 => 3,
|
||||
Unspecified4 => 4,
|
||||
MailSystem => 5,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a 3 digit SMTP response code
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
pub struct Code {
|
||||
/// First digit of the response code
|
||||
severity: Severity,
|
||||
@@ -101,26 +113,36 @@ pub struct Code {
|
||||
detail: u8,
|
||||
}
|
||||
|
||||
impl Display for Code {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
write!(f, "{}{}{}", self.severity, self.category, self.detail)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Code {
|
||||
type Err = Error;
|
||||
|
||||
#[inline]
|
||||
fn from_str(s: &str) -> result::Result<Code, Error> {
|
||||
if s.len() == 3 {
|
||||
match (s[0..1].parse::<Severity>(),
|
||||
s[1..2].parse::<Category>(),
|
||||
s[2..3].parse::<u8>()) {
|
||||
match (
|
||||
s[0..1].parse::<Severity>(),
|
||||
s[1..2].parse::<Category>(),
|
||||
s[2..3].parse::<u8>(),
|
||||
) {
|
||||
(Ok(severity), Ok(category), Ok(detail)) => {
|
||||
Ok(Code {
|
||||
severity: severity,
|
||||
category: category,
|
||||
detail: detail,
|
||||
})
|
||||
severity: severity,
|
||||
category: category,
|
||||
detail: detail,
|
||||
})
|
||||
}
|
||||
_ => Err(Error::ResponseParsing("Could not parse response code")),
|
||||
}
|
||||
} else {
|
||||
Err(Error::ResponseParsing("Wrong code length (should be 3 digit)"))
|
||||
Err(Error::ResponseParsing(
|
||||
"Wrong code length (should be 3 digit)",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,11 +156,6 @@ impl Code {
|
||||
detail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the reply code
|
||||
pub fn code(&self) -> String {
|
||||
format!("{}{}{}", self.severity, self.category, self.detail)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses an SMTP response
|
||||
@@ -156,14 +173,18 @@ impl ResponseParser {
|
||||
pub fn read_line(&mut self, line: &str) -> result::Result<bool, Error> {
|
||||
|
||||
if line.len() < 3 {
|
||||
return Err(Error::ResponseParsing("Wrong code length (should be 3 digit)"));
|
||||
return Err(Error::ResponseParsing(
|
||||
"Wrong code length (should be 3 digit)",
|
||||
));
|
||||
}
|
||||
|
||||
match self.code {
|
||||
Some(ref code) => {
|
||||
if code.code() != line[0..3] {
|
||||
return Err(Error::ResponseParsing("Response code has changed during a \
|
||||
reponse"));
|
||||
if code.to_string() != line[0..3] {
|
||||
return Err(Error::ResponseParsing(
|
||||
"Response code has changed during a \
|
||||
reponse",
|
||||
));
|
||||
}
|
||||
}
|
||||
None => self.code = Some(try!(line[0..3].parse::<Code>())),
|
||||
@@ -182,8 +203,10 @@ impl ResponseParser {
|
||||
match self.code {
|
||||
Some(code) => Ok(Response::new(code, self.message)),
|
||||
None => {
|
||||
Err(Error::ResponseParsing("Incomplete response, could not read response \
|
||||
code"))
|
||||
Err(Error::ResponseParsing(
|
||||
"Incomplete response, could not read response \
|
||||
code",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,6 +246,11 @@ impl Response {
|
||||
self.message.clone()
|
||||
}
|
||||
|
||||
/// Returns the response code
|
||||
pub fn code(&self) -> Code {
|
||||
self.code
|
||||
}
|
||||
|
||||
/// Returns the severity (i.e. 1st digit)
|
||||
pub fn severity(&self) -> Severity {
|
||||
self.code.severity
|
||||
@@ -238,14 +266,9 @@ impl Response {
|
||||
self.code.detail
|
||||
}
|
||||
|
||||
/// Returns the reply code
|
||||
fn code(&self) -> String {
|
||||
self.code.code()
|
||||
}
|
||||
|
||||
/// Tests code equality
|
||||
pub fn has_code(&self, code: u16) -> bool {
|
||||
self.code() == format!("{}", code)
|
||||
self.code.to_string() == format!("{}", code)
|
||||
}
|
||||
|
||||
/// Returns only the first word of the message if possible
|
||||
@@ -258,7 +281,15 @@ impl Response {
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns only the line word of the message if possible
|
||||
pub fn first_line(&self) -> Option<&str> {
|
||||
if self.message.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(&self.message[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,10 +299,14 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_severity_from_str() {
|
||||
assert_eq!("2".parse::<Severity>().unwrap(),
|
||||
Severity::PositiveCompletion);
|
||||
assert_eq!("4".parse::<Severity>().unwrap(),
|
||||
Severity::TransientNegativeCompletion);
|
||||
assert_eq!(
|
||||
"2".parse::<Severity>().unwrap(),
|
||||
Severity::PositiveCompletion
|
||||
);
|
||||
assert_eq!(
|
||||
"4".parse::<Severity>().unwrap(),
|
||||
Severity::TransientNegativeCompletion
|
||||
);
|
||||
assert!("1".parse::<Severity>().is_err());
|
||||
}
|
||||
|
||||
@@ -294,71 +329,89 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_code_new() {
|
||||
assert_eq!(Code::new(Severity::TransientNegativeCompletion,
|
||||
Category::Connections,
|
||||
0),
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::Connections,
|
||||
detail: 0,
|
||||
});
|
||||
assert_eq!(
|
||||
Code::new(
|
||||
Severity::TransientNegativeCompletion,
|
||||
Category::Connections,
|
||||
0,
|
||||
),
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::Connections,
|
||||
detail: 0,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_from_str() {
|
||||
assert_eq!("421".parse::<Code>().unwrap(),
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::Connections,
|
||||
detail: 1,
|
||||
});
|
||||
assert_eq!(
|
||||
"421".parse::<Code>().unwrap(),
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::Connections,
|
||||
detail: 1,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_code() {
|
||||
fn test_code_display() {
|
||||
let code = Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::Connections,
|
||||
detail: 1,
|
||||
};
|
||||
|
||||
assert_eq!(code.code(), "421");
|
||||
assert_eq!(code.to_string(), "421");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_new() {
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()]),
|
||||
Response {
|
||||
code: Code {
|
||||
severity: Severity::PositiveCompletion,
|
||||
category: Category::Unspecified4,
|
||||
detail: 1,
|
||||
},
|
||||
message: vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()],
|
||||
});
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![]),
|
||||
Response {
|
||||
code: Code {
|
||||
severity: Severity::PositiveCompletion,
|
||||
category: Category::Unspecified4,
|
||||
detail: 1,
|
||||
},
|
||||
message: vec![],
|
||||
});
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
),
|
||||
Response {
|
||||
code: Code {
|
||||
severity: Severity::PositiveCompletion,
|
||||
category: Category::Unspecified4,
|
||||
detail: 1,
|
||||
},
|
||||
message: vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![],
|
||||
),
|
||||
Response {
|
||||
code: Code {
|
||||
severity: Severity::PositiveCompletion,
|
||||
category: Category::Unspecified4,
|
||||
detail: 1,
|
||||
},
|
||||
message: vec![],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -372,208 +425,286 @@ mod test {
|
||||
|
||||
let response = parser.response().unwrap();
|
||||
|
||||
assert_eq!(response,
|
||||
Response {
|
||||
code: Code {
|
||||
severity: Severity::PositiveCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: 0,
|
||||
},
|
||||
message: vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
"AUTH PLAIN CRAM-MD5".to_string()],
|
||||
});
|
||||
assert_eq!(
|
||||
response,
|
||||
Response {
|
||||
code: Code {
|
||||
severity: Severity::PositiveCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: 0,
|
||||
},
|
||||
message: vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
"AUTH PLAIN CRAM-MD5".to_string(),
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_is_positive() {
|
||||
assert!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.is_positive());
|
||||
assert!(!Response::new(Code {
|
||||
severity: "5".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.is_positive());
|
||||
assert!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
).is_positive()
|
||||
);
|
||||
assert!(!Response::new(
|
||||
Code {
|
||||
severity: "5".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
).is_positive());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_message() {
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.message(),
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()]);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
).message(),
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
]
|
||||
);
|
||||
let empty_message: Vec<String> = vec![];
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![])
|
||||
.message(),
|
||||
empty_message);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![],
|
||||
).message(),
|
||||
empty_message
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_severity() {
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.severity(),
|
||||
Severity::PositiveCompletion);
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "5".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.severity(),
|
||||
Severity::PermanentNegativeCompletion);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
).severity(),
|
||||
Severity::PositiveCompletion
|
||||
);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: "5".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
).severity(),
|
||||
Severity::PermanentNegativeCompletion
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_category() {
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.category(),
|
||||
Category::Unspecified4);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
).category(),
|
||||
Category::Unspecified4
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_detail() {
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.detail(),
|
||||
1);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
).detail(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_code() {
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.code(),
|
||||
"241");
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
).code()
|
||||
.to_string(),
|
||||
"241"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_has_code() {
|
||||
assert!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.has_code(241));
|
||||
assert!(!Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.has_code(251));
|
||||
assert!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
).has_code(241)
|
||||
);
|
||||
assert!(!Response::new(
|
||||
Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
).has_code(251));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_first_word() {
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.first_word(),
|
||||
Some("me".to_string()));
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me mo".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.first_word(),
|
||||
Some("me".to_string()));
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![])
|
||||
.first_word(),
|
||||
None);
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![" ".to_string()])
|
||||
.first_word(),
|
||||
None);
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![" ".to_string()])
|
||||
.first_word(),
|
||||
None);
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["".to_string()])
|
||||
.first_word(),
|
||||
None);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
).first_word(),
|
||||
Some("me".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![
|
||||
"me mo".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
).first_word(),
|
||||
Some("me".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![],
|
||||
).first_word(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![" ".to_string()],
|
||||
).first_word(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![" ".to_string()],
|
||||
).first_word(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["".to_string()],
|
||||
).first_word(),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,10 +38,12 @@ pub type StubResult = Result<(), error::Error>;
|
||||
impl EmailTransport<StubResult> for StubEmailTransport {
|
||||
fn send<T: SendableEmail>(&mut self, email: T) -> StubResult {
|
||||
|
||||
info!("{}: from=<{}> to=<{:?}>",
|
||||
email.message_id(),
|
||||
email.from(),
|
||||
email.to());
|
||||
info!(
|
||||
"{}: from=<{}> to=<{:?}>",
|
||||
email.message_id(),
|
||||
email.from(),
|
||||
email.to()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,12 @@ use std::io::Read;
|
||||
#[test]
|
||||
fn file_transport() {
|
||||
let mut sender = FileEmailTransport::new(temp_dir());
|
||||
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.clone());
|
||||
assert!(result.is_ok());
|
||||
|
||||
@@ -24,10 +26,14 @@ fn file_transport() {
|
||||
let mut buffer = String::new();
|
||||
let _ = f.read_to_string(&mut buffer);
|
||||
|
||||
assert_eq!(buffer,
|
||||
format!("{}: from=<user@localhost> to=<root@localhost>\n{}",
|
||||
message_id,
|
||||
email.message()));
|
||||
assert_eq!(
|
||||
buffer,
|
||||
format!(
|
||||
"{}: from=<user@localhost> to=<root@localhost>\n{}",
|
||||
message_id,
|
||||
email.message()
|
||||
)
|
||||
);
|
||||
|
||||
remove_file(file).unwrap();
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@ use lettre::sendmail::SendmailTransport;
|
||||
#[test]
|
||||
fn sendmail_transport_simple() {
|
||||
let mut sender = SendmailTransport::new();
|
||||
let email = SimpleSendableEmail::new("user@localhost",
|
||||
vec!["root@localhost"],
|
||||
"sendmail_id",
|
||||
"Hello sendmail");
|
||||
let email = SimpleSendableEmail::new(
|
||||
"user@localhost",
|
||||
vec!["root@localhost"],
|
||||
"sendmail_id",
|
||||
"Hello sendmail",
|
||||
);
|
||||
|
||||
let result = sender.send(email);
|
||||
println!("{:?}", result);
|
||||
|
||||
@@ -10,10 +10,12 @@ fn smtp_transport_simple() {
|
||||
.unwrap()
|
||||
.security_level(SecurityLevel::Opportunistic)
|
||||
.build();
|
||||
let email = SimpleSendableEmail::new("user@localhost",
|
||||
vec!["root@localhost"],
|
||||
"smtp_id",
|
||||
"Hello smtp");
|
||||
let email = SimpleSendableEmail::new(
|
||||
"user@localhost",
|
||||
vec!["root@localhost"],
|
||||
"smtp_id",
|
||||
"Hello smtp",
|
||||
);
|
||||
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
|
||||
@@ -6,10 +6,12 @@ use lettre::stub::StubEmailTransport;
|
||||
#[test]
|
||||
fn stub_transport() {
|
||||
let mut sender = StubEmailTransport;
|
||||
let email = SimpleSendableEmail::new("user@localhost",
|
||||
vec!["root@localhost"],
|
||||
"stub_id",
|
||||
"Hello stub");
|
||||
let email = SimpleSendableEmail::new(
|
||||
"user@localhost",
|
||||
vec!["root@localhost"],
|
||||
"stub_id",
|
||||
"Hello stub",
|
||||
);
|
||||
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
|
||||
Reference in New Issue
Block a user