feat(transport): More precise error descriptions

This commit is contained in:
Alexis Mousset
2017-06-17 12:33:46 +02:00
parent d3a4e353b1
commit 153af016e7
16 changed files with 700 additions and 469 deletions

View File

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

View File

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

View File

@@ -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(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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