style(all): Run stable rustfmt and remove redundant field names in structs

This commit is contained in:
Alexis Mousset
2018-03-03 00:27:56 +01:00
parent 96e4f845ec
commit 9d68629bb6
24 changed files with 832 additions and 599 deletions

View File

@@ -9,17 +9,19 @@ use lettre::smtp::ConnectionReuseParameters;
#[bench] #[bench]
fn bench_simple_send(b: &mut test::Bencher) { fn bench_simple_send(b: &mut test::Bencher) {
let mut sender = SmtpTransport::builder("127.0.0.1:2525", ClientSecurity::None).unwrap() let mut sender = SmtpTransport::builder("127.0.0.1:2525", ClientSecurity::None)
.build(); .unwrap()
.build();
b.iter(|| { b.iter(|| {
let email = let email = SimpleSendableEmail::new(
SimpleSendableEmail::new(EmailAddress::new("user@localhost".to_string()), EmailAddress::new("user@localhost".to_string()),
vec![EmailAddress::new("root@localhost".to_string())], vec![EmailAddress::new("root@localhost".to_string())],
"id".to_string(), "id".to_string(),
"Hello world".to_string()); "Hello world".to_string(),
let result = sender.send(&email); );
assert!(result.is_ok()); let result = sender.send(&email);
}); assert!(result.is_ok());
});
} }
#[bench] #[bench]
@@ -29,13 +31,14 @@ fn bench_reuse_send(b: &mut test::Bencher) {
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited) .connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
.build(); .build();
b.iter(|| { b.iter(|| {
let email = let email = SimpleSendableEmail::new(
SimpleSendableEmail::new(EmailAddress::new("user@localhost".to_string()), EmailAddress::new("user@localhost".to_string()),
vec![EmailAddress::new("root@localhost".to_string())], vec![EmailAddress::new("root@localhost".to_string())],
"id".to_string(), "id".to_string(),
"Hello world".to_string()); "Hello world".to_string(),
let result = sender.send(&email); );
assert!(result.is_ok()); let result = sender.send(&email);
}); assert!(result.is_ok());
});
sender.close() sender.close()
} }

View File

@@ -6,14 +6,17 @@ use lettre::{EmailAddress, EmailTransport, SimpleSendableEmail, SmtpTransport};
fn main() { fn main() {
env_logger::init(); env_logger::init();
let email = SimpleSendableEmail::new(EmailAddress::new("user@localhost".to_string()), let email = SimpleSendableEmail::new(
vec![EmailAddress::new("root@localhost".to_string())], EmailAddress::new("user@localhost".to_string()),
"file_id".to_string(), vec![EmailAddress::new("root@localhost".to_string())],
"Hello ß☺ example".to_string()); "file_id".to_string(),
"Hello ß☺ example".to_string(),
);
// Open a local connection on port 25 // Open a local connection on port 25
let mut mailer = SmtpTransport::builder_unencrypted_localhost().unwrap() let mut mailer = SmtpTransport::builder_unencrypted_localhost()
.build(); .unwrap()
.build();
// Send the email // Send the email
let result = mailer.send(&email); let result = mailer.send(&email);

View File

@@ -40,10 +40,12 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, FileResult> for FileEmailTransport
let mut message_content = String::new(); let mut message_content = String::new();
let _ = email.message().read_to_string(&mut message_content); let _ = email.message().read_to_string(&mut message_content);
let simple_email = SimpleSendableEmail::new(email.from().clone(), let simple_email = SimpleSendableEmail::new(
email.to().clone(), email.from().clone(),
email.message_id().clone(), email.to().clone(),
message_content); email.message_id().clone(),
message_content,
);
f.write_all(serde_json::to_string(&simple_email)?.as_bytes())?; f.write_all(serde_json::to_string(&simple_email)?.as_bytes())?;

View File

@@ -101,15 +101,18 @@ pub struct SimpleSendableEmail {
impl SimpleSendableEmail { impl SimpleSendableEmail {
/// Returns a new email /// Returns a new email
pub fn new(from_address: EmailAddress, pub fn new(
to_addresses: Vec<EmailAddress>, from_address: EmailAddress,
message_id: String, to_addresses: Vec<EmailAddress>,
message: String) message_id: String,
-> SimpleSendableEmail { message: String,
SimpleSendableEmail { from: from_address, ) -> SimpleSendableEmail {
to: to_addresses, SimpleSendableEmail {
message_id: message_id, from: from_address,
message: message.into_bytes(), } to: to_addresses,
message_id,
message: message.into_bytes(),
}
} }
} }

View File

@@ -18,12 +18,16 @@ pub struct SendmailTransport {
impl SendmailTransport { impl SendmailTransport {
/// Creates a new transport with the default `/usr/sbin/sendmail` command /// Creates a new transport with the default `/usr/sbin/sendmail` command
pub fn new() -> SendmailTransport { pub fn new() -> SendmailTransport {
SendmailTransport { command: "/usr/sbin/sendmail".to_string(), } SendmailTransport {
command: "/usr/sbin/sendmail".to_string(),
}
} }
/// Creates a new transport to the given sendmail command /// Creates a new transport to the given sendmail command
pub fn new_with_command<S: Into<String>>(command: S) -> SendmailTransport { pub fn new_with_command<S: Into<String>>(command: S) -> SendmailTransport {
SendmailTransport { command: command.into(), } SendmailTransport {
command: command.into(),
}
} }
} }
@@ -31,21 +35,25 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SendmailResult> for SendmailTranspo
fn send<U: SendableEmail<'a, T> + 'a>(&mut self, email: &'a U) -> SendmailResult { fn send<U: SendableEmail<'a, T> + 'a>(&mut self, email: &'a U) -> SendmailResult {
// Spawn the sendmail command // Spawn the sendmail command
let to_addresses: Vec<String> = email.to().iter().map(|x| x.to_string()).collect(); let to_addresses: Vec<String> = email.to().iter().map(|x| x.to_string()).collect();
let mut process = Command::new(&self.command).args(&["-i", let mut process = Command::new(&self.command)
"-f", .args(&[
&email.from().to_string(), "-i",
&to_addresses.join(" ")]) "-f",
.stdin(Stdio::piped()) &email.from().to_string(),
.stdout(Stdio::piped()) &to_addresses.join(" "),
.spawn()?; ])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()?;
let mut message_content = String::new(); let mut message_content = String::new();
let _ = email.message().read_to_string(&mut message_content); let _ = email.message().read_to_string(&mut message_content);
match process.stdin match process
.as_mut() .stdin
.unwrap() .as_mut()
.write_all(message_content.as_bytes()) .unwrap()
.write_all(message_content.as_bytes())
{ {
Ok(_) => (), Ok(_) => (),
Err(error) => return Err(From::from(error)), Err(error) => return Err(From::from(error)),

View File

@@ -59,8 +59,7 @@ pub struct Credentials {
impl Credentials { impl Credentials {
/// Create a `Credentials` struct from username and password /// Create a `Credentials` struct from username and password
pub fn new(username: String, password: String) -> Credentials { pub fn new(username: String, password: String) -> Credentials {
Credentials { username: username, Credentials { username, password }
password: password, }
} }
} }
@@ -82,12 +81,16 @@ pub enum Mechanism {
impl Display for Mechanism { impl Display for Mechanism {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", match *self { write!(
Mechanism::Plain => "PLAIN", f,
Mechanism::Login => "LOGIN", "{}",
#[cfg(feature = "crammd5-auth")] match *self {
Mechanism::CramMd5 => "CRAM-MD5", Mechanism::Plain => "PLAIN",
}) Mechanism::Login => "LOGIN",
#[cfg(feature = "crammd5-auth")]
Mechanism::CramMd5 => "CRAM-MD5",
}
)
} }
} }
@@ -105,23 +108,19 @@ impl Mechanism {
/// Returns the string to send to the server, using the provided username, password and /// Returns the string to send to the server, using the provided username, password and
/// challenge in some cases /// challenge in some cases
pub fn response(&self, pub fn response(
credentials: &Credentials, &self,
challenge: Option<&str>) credentials: &Credentials,
-> Result<String, Error> { challenge: Option<&str>,
) -> Result<String, Error> {
match *self { match *self {
Mechanism::Plain => { Mechanism::Plain => match challenge {
match challenge { Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")), None => Ok(format!(
None => { "{}{}{}{}",
Ok(format!("{}{}{}{}", NUL, credentials.username, NUL, credentials.password
NUL, )),
credentials.username, },
NUL,
credentials.password))
}
}
}
Mechanism::Login => { Mechanism::Login => {
let decoded_challenge = match challenge { let decoded_challenge = match challenge {
Some(challenge) => challenge, Some(challenge) => challenge,
@@ -148,9 +147,11 @@ impl Mechanism {
let mut hmac = Hmac::new(Md5::new(), credentials.password.as_bytes()); let mut hmac = Hmac::new(Md5::new(), credentials.password.as_bytes());
hmac.input(decoded_challenge.as_bytes()); hmac.input(decoded_challenge.as_bytes());
Ok(format!("{} {}", Ok(format!(
credentials.username, "{} {}",
hex::encode(hmac.result().code()))) credentials.username,
hex::encode(hmac.result().code())
))
} }
} }
} }
@@ -166,8 +167,10 @@ mod test {
let credentials = Credentials::new("username".to_string(), "password".to_string()); let credentials = Credentials::new("username".to_string(), "password".to_string());
assert_eq!(mechanism.response(&credentials, None).unwrap(), assert_eq!(
"\u{0}username\u{0}password"); mechanism.response(&credentials, None).unwrap(),
"\u{0}username\u{0}password"
);
assert!(mechanism.response(&credentials, Some("test")).is_err()); assert!(mechanism.response(&credentials, Some("test")).is_err());
} }
@@ -177,10 +180,14 @@ mod test {
let credentials = Credentials::new("alice".to_string(), "wonderland".to_string()); let credentials = Credentials::new("alice".to_string(), "wonderland".to_string());
assert_eq!(mechanism.response(&credentials, Some("Username")).unwrap(), assert_eq!(
"alice"); mechanism.response(&credentials, Some("Username")).unwrap(),
assert_eq!(mechanism.response(&credentials, Some("Password")).unwrap(), "alice"
"wonderland"); );
assert_eq!(
mechanism.response(&credentials, Some("Password")).unwrap(),
"wonderland"
);
assert!(mechanism.response(&credentials, None).is_err()); assert!(mechanism.response(&credentials, None).is_err());
} }

View File

@@ -20,13 +20,17 @@ impl Default for MockStream {
impl MockStream { impl MockStream {
pub fn new() -> MockStream { pub fn new() -> MockStream {
MockStream { reader: Arc::new(Mutex::new(MockCursor::new(Vec::new()))), MockStream {
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))), } reader: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
}
} }
pub fn with_vec(vec: Vec<u8>) -> MockStream { pub fn with_vec(vec: Vec<u8>) -> MockStream {
MockStream { reader: Arc::new(Mutex::new(MockCursor::new(vec))), MockStream {
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))), } reader: Arc::new(Mutex::new(MockCursor::new(vec))),
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
}
} }
pub fn take_vec(&mut self) -> Vec<u8> { pub fn take_vec(&mut self) -> Vec<u8> {

View File

@@ -139,10 +139,11 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
} }
/// Connects to the configured server /// Connects to the configured server
pub fn connect<A: ToSocketAddrs>(&mut self, pub fn connect<A: ToSocketAddrs>(
addr: &A, &mut self,
tls_parameters: Option<&ClientTlsParameters>) addr: &A,
-> SmtpResult { tls_parameters: Option<&ClientTlsParameters>,
) -> SmtpResult {
// Connect should not be called when the client is already connected // Connect should not be called when the client is already connected
if self.stream.is_some() { if self.stream.is_some() {
return_err!("The connection is already established", self); return_err!("The connection is already established", self);
@@ -177,9 +178,11 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
while challenges > 0 && response.has_code(334) { while challenges > 0 && response.has_code(334) {
challenges -= 1; challenges -= 1;
response = self.command(AuthCommand::new_from_response(mechanism, response = self.command(AuthCommand::new_from_response(
credentials.clone(), mechanism,
&response)?)?; credentials.clone(),
&response,
)?)?;
} }
if challenges == 0 { if challenges == 0 {
@@ -233,8 +236,10 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
self.stream.as_mut().unwrap().write_all(string)?; self.stream.as_mut().unwrap().write_all(string)?;
self.stream.as_mut().unwrap().flush()?; self.stream.as_mut().unwrap().flush()?;
debug!("Wrote: {}", debug!(
escape_crlf(String::from_utf8_lossy(string).as_ref())); "Wrote: {}",
escape_crlf(String::from_utf8_lossy(string).as_ref())
);
Ok(()) Ok(())
} }
@@ -282,15 +287,19 @@ mod test {
assert!(codec.encode(b"test\n", &mut buf).is_ok()); assert!(codec.encode(b"test\n", &mut buf).is_ok());
assert!(codec.encode(b".test\n", &mut buf).is_ok()); assert!(codec.encode(b".test\n", &mut buf).is_ok());
assert!(codec.encode(b"test", &mut buf).is_ok()); assert!(codec.encode(b"test", &mut buf).is_ok());
assert_eq!(String::from_utf8(buf).unwrap(), assert_eq!(
"test\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest"); String::from_utf8(buf).unwrap(),
"test\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest"
);
} }
#[test] #[test]
fn test_escape_crlf() { fn test_escape_crlf() {
assert_eq!(escape_crlf("\r\n"), "<CRLF>"); assert_eq!(escape_crlf("\r\n"), "<CRLF>");
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CRLF>"); assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CRLF>");
assert_eq!(escape_crlf("EHLO my_name\r\nSIZE 42\r\n"), assert_eq!(
"EHLO my_name<CRLF>SIZE 42<CRLF>"); escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
"EHLO my_name<CRLF>SIZE 42<CRLF>"
);
} }
} }

View File

@@ -18,8 +18,7 @@ pub struct ClientTlsParameters {
impl ClientTlsParameters { impl ClientTlsParameters {
/// Creates a `ClientTlsParameters` /// Creates a `ClientTlsParameters`
pub fn new(domain: String, connector: TlsConnector) -> ClientTlsParameters { pub fn new(domain: String, connector: TlsConnector) -> ClientTlsParameters {
ClientTlsParameters { connector: connector, ClientTlsParameters { connector, domain }
domain: domain, }
} }
} }
@@ -44,13 +43,10 @@ impl NetworkStream {
match *self { match *self {
NetworkStream::Tcp(ref s) => s.peer_addr(), NetworkStream::Tcp(ref s) => s.peer_addr(),
NetworkStream::Tls(ref s) => s.get_ref().peer_addr(), NetworkStream::Tls(ref s) => s.get_ref().peer_addr(),
NetworkStream::Mock(_) => { NetworkStream::Mock(_) => Ok(SocketAddr::V4(SocketAddrV4::new(
Ok(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, Ipv4Addr::new(127, 0, 0, 1),
0, 80,
0, ))),
1),
80)))
}
} }
} }
@@ -96,7 +92,7 @@ impl Write for NetworkStream {
pub trait Connector: Sized { pub trait Connector: Sized {
/// Opens a connection to the given IP socket /// Opens a connection to the given IP socket
fn connect(addr: &SocketAddr, tls_parameters: Option<&ClientTlsParameters>) fn connect(addr: &SocketAddr, tls_parameters: Option<&ClientTlsParameters>)
-> io::Result<Self>; -> io::Result<Self>;
/// Upgrades to TLS connection /// Upgrades to TLS connection
fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()>; fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()>;
/// Is the NetworkStream encrypted /// Is the NetworkStream encrypted
@@ -104,18 +100,18 @@ pub trait Connector: Sized {
} }
impl Connector for NetworkStream { impl Connector for NetworkStream {
fn connect(addr: &SocketAddr, fn connect(
tls_parameters: Option<&ClientTlsParameters>) addr: &SocketAddr,
-> io::Result<NetworkStream> { tls_parameters: Option<&ClientTlsParameters>,
) -> io::Result<NetworkStream> {
let tcp_stream = TcpStream::connect(addr)?; let tcp_stream = TcpStream::connect(addr)?;
match tls_parameters { match tls_parameters {
Some(context) => { Some(context) => context
context.connector .connector
.connect(context.domain.as_ref(), tcp_stream) .connect(context.domain.as_ref(), tcp_stream)
.map(NetworkStream::Tls) .map(NetworkStream::Tls)
.map_err(|e| io::Error::new(ErrorKind::Other, e)) .map_err(|e| io::Error::new(ErrorKind::Other, e)),
}
None => Ok(NetworkStream::Tcp(tcp_stream)), None => Ok(NetworkStream::Tcp(tcp_stream)),
} }
} }
@@ -123,15 +119,13 @@ impl Connector for NetworkStream {
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))] #[cfg_attr(feature = "cargo-clippy", allow(match_same_arms))]
fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()> { fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()> {
*self = match *self { *self = match *self {
NetworkStream::Tcp(ref mut stream) => { NetworkStream::Tcp(ref mut stream) => match tls_parameters
match tls_parameters.connector .connector
.connect(tls_parameters.domain.as_ref(), .connect(tls_parameters.domain.as_ref(), stream.try_clone().unwrap())
stream.try_clone().unwrap()) {
{ Ok(tls_stream) => NetworkStream::Tls(tls_stream),
Ok(tls_stream) => NetworkStream::Tls(tls_stream), Err(err) => return Err(io::Error::new(ErrorKind::Other, err)),
Err(err) => return Err(io::Error::new(ErrorKind::Other, err)), },
}
}
NetworkStream::Tls(_) => return Ok(()), NetworkStream::Tls(_) => return Ok(()),
NetworkStream::Mock(_) => return Ok(()), NetworkStream::Mock(_) => return Ok(()),
}; };

View File

@@ -26,7 +26,7 @@ impl Display for EhloCommand {
impl EhloCommand { impl EhloCommand {
/// Creates a EHLO command /// Creates a EHLO command
pub fn new(client_id: ClientId) -> EhloCommand { pub fn new(client_id: ClientId) -> EhloCommand {
EhloCommand { client_id: client_id, } EhloCommand { client_id }
} }
} }
@@ -44,16 +44,20 @@ impl Display for StarttlsCommand {
/// MAIL command /// MAIL command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug)]
pub struct MailCommand { pub struct MailCommand {
sender: Option<EmailAddress>, sender: Option<EmailAddress>,
parameters: Vec<MailParameter>, parameters: Vec<MailParameter>,
} }
impl Display for MailCommand { impl Display for MailCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "MAIL FROM:<{}>", match self.sender { write!(
Some(ref address) => address.to_string(), f,
None => "".to_string(), "MAIL FROM:<{}>",
})?; match self.sender {
Some(ref address) => address.to_string(),
None => "".to_string(),
}
)?;
for parameter in &self.parameters { for parameter in &self.parameters {
write!(f, " {}", parameter)?; write!(f, " {}", parameter)?;
} }
@@ -64,15 +68,14 @@ impl Display for MailCommand {
impl MailCommand { impl MailCommand {
/// Creates a MAIL command /// Creates a MAIL command
pub fn new(sender: Option<EmailAddress>, parameters: Vec<MailParameter>) -> MailCommand { pub fn new(sender: Option<EmailAddress>, parameters: Vec<MailParameter>) -> MailCommand {
MailCommand { sender: sender, MailCommand { sender, parameters }
parameters: parameters, }
} }
} }
/// RCPT command /// RCPT command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug)]
pub struct RcptCommand { pub struct RcptCommand {
recipient: EmailAddress, recipient: EmailAddress,
parameters: Vec<RcptParameter>, parameters: Vec<RcptParameter>,
} }
@@ -89,8 +92,10 @@ impl Display for RcptCommand {
impl RcptCommand { impl RcptCommand {
/// Creates an RCPT command /// Creates an RCPT command
pub fn new(recipient: EmailAddress, parameters: Vec<RcptParameter>) -> RcptCommand { pub fn new(recipient: EmailAddress, parameters: Vec<RcptParameter>) -> RcptCommand {
RcptCommand { recipient: recipient, RcptCommand {
parameters: parameters, } recipient,
parameters,
}
} }
} }
@@ -146,7 +151,7 @@ impl Display for HelpCommand {
impl HelpCommand { impl HelpCommand {
/// Creates an HELP command /// Creates an HELP command
pub fn new(argument: Option<String>) -> HelpCommand { pub fn new(argument: Option<String>) -> HelpCommand {
HelpCommand { argument: argument } HelpCommand { argument }
} }
} }
@@ -166,7 +171,7 @@ impl Display for VrfyCommand {
impl VrfyCommand { impl VrfyCommand {
/// Creates a VRFY command /// Creates a VRFY command
pub fn new(argument: String) -> VrfyCommand { pub fn new(argument: String) -> VrfyCommand {
VrfyCommand { argument: argument } VrfyCommand { argument }
} }
} }
@@ -186,7 +191,7 @@ impl Display for ExpnCommand {
impl ExpnCommand { impl ExpnCommand {
/// Creates an EXPN command /// Creates an EXPN command
pub fn new(argument: String) -> ExpnCommand { pub fn new(argument: String) -> ExpnCommand {
ExpnCommand { argument: argument } ExpnCommand { argument }
} }
} }
@@ -204,17 +209,19 @@ impl Display for RsetCommand {
/// AUTH command /// AUTH command
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug)]
pub struct AuthCommand { pub struct AuthCommand {
mechanism: Mechanism, mechanism: Mechanism,
credentials: Credentials, credentials: Credentials,
challenge: Option<String>, challenge: Option<String>,
response: Option<String>, response: Option<String>,
} }
impl Display for AuthCommand { impl Display for AuthCommand {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let encoded_response = if self.response.is_some() { let encoded_response = if self.response.is_some() {
Some(base64::encode_config(self.response.as_ref().unwrap().as_bytes(), Some(base64::encode_config(
base64::STANDARD)) self.response.as_ref().unwrap().as_bytes(),
base64::STANDARD,
))
} else { } else {
None None
}; };
@@ -233,27 +240,31 @@ impl Display for AuthCommand {
impl AuthCommand { impl AuthCommand {
/// Creates an AUTH command (from a challenge if provided) /// Creates an AUTH command (from a challenge if provided)
pub fn new(mechanism: Mechanism, pub fn new(
credentials: Credentials, mechanism: Mechanism,
challenge: Option<String>) credentials: Credentials,
-> Result<AuthCommand, Error> { challenge: Option<String>,
) -> Result<AuthCommand, Error> {
let response = if mechanism.supports_initial_response() || challenge.is_some() { let response = if mechanism.supports_initial_response() || challenge.is_some() {
Some(mechanism.response(&credentials, challenge.as_ref().map(String::as_str))?) Some(mechanism.response(&credentials, challenge.as_ref().map(String::as_str))?)
} else { } else {
None None
}; };
Ok(AuthCommand { mechanism: mechanism, Ok(AuthCommand {
credentials: credentials, mechanism,
challenge: challenge, credentials,
response: response, }) challenge,
response,
})
} }
/// Creates an AUTH command from a response that needs to be a /// Creates an AUTH command from a response that needs to be a
/// valid challenge (with 334 response code) /// valid challenge (with 334 response code)
pub fn new_from_response(mechanism: Mechanism, pub fn new_from_response(
credentials: Credentials, mechanism: Mechanism,
response: &Response) credentials: Credentials,
-> Result<AuthCommand, Error> { response: &Response,
) -> Result<AuthCommand, Error> {
if !response.has_code(334) { if !response.has_code(334) {
return Err(Error::ResponseParsing("Expecting a challenge")); return Err(Error::ResponseParsing("Expecting a challenge"));
} }
@@ -266,12 +277,10 @@ impl AuthCommand {
debug!("auth encoded challenge: {}", encoded_challenge); debug!("auth encoded challenge: {}", encoded_challenge);
let decoded_challenge = match base64::decode(&encoded_challenge) { let decoded_challenge = match base64::decode(&encoded_challenge) {
Ok(challenge) => { Ok(challenge) => match String::from_utf8(challenge) {
match String::from_utf8(challenge) { Ok(value) => value,
Ok(value) => value, Err(error) => return Err(Error::Utf8Parsing(error)),
Err(error) => return Err(Error::Utf8Parsing(error)), },
}
}
Err(error) => return Err(Error::ChallengeParsing(error)), Err(error) => return Err(Error::ChallengeParsing(error)),
}; };
@@ -279,10 +288,12 @@ impl AuthCommand {
let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?); let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
Ok(AuthCommand { mechanism: mechanism, Ok(AuthCommand {
credentials: credentials, mechanism,
challenge: Some(decoded_challenge), credentials,
response: response, }) challenge: Some(decoded_challenge),
response,
})
} }
} }
@@ -297,49 +308,77 @@ mod test {
fn test_display() { fn test_display() {
let id = ClientId::Domain("localhost".to_string()); let id = ClientId::Domain("localhost".to_string());
let email = EmailAddress::new("test@example.com".to_string()); let email = EmailAddress::new("test@example.com".to_string());
let mail_parameter = MailParameter::Other { keyword: "TEST".to_string(), let mail_parameter = MailParameter::Other {
value: Some("value".to_string()), }; keyword: "TEST".to_string(),
let rcpt_parameter = RcptParameter::Other { keyword: "TEST".to_string(), value: Some("value".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!("{}", EhloCommand::new(id)), "EHLO localhost\r\n");
assert_eq!(format!("{}", MailCommand::new(Some(email.clone()), vec![])), assert_eq!(
"MAIL FROM:<test@example.com>\r\n"); format!("{}", MailCommand::new(Some(email.clone()), vec![])),
assert_eq!(format!("{}", MailCommand::new(None, vec![])), "MAIL FROM:<test@example.com>\r\n"
"MAIL FROM:<>\r\n"); );
assert_eq!(format!("{}", assert_eq!(
MailCommand::new(Some(email.clone()), vec![MailParameter::Size(42)])), format!("{}", MailCommand::new(None, vec![])),
"MAIL FROM:<test@example.com> SIZE=42\r\n"); "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!( assert_eq!(
format!( format!(
"{}", "{}",
MailCommand::new( MailCommand::new(
Some(email.clone()), Some(email.clone()),
vec![MailParameter::Size(42), vec![
MailParameter::Body(MailBodyParameter::EightBitMime), MailParameter::Size(42),
mail_parameter], MailParameter::Body(MailBodyParameter::EightBitMime),
mail_parameter,
],
) )
), ),
"MAIL FROM:<test@example.com> SIZE=42 BODY=8BITMIME TEST=value\r\n" "MAIL FROM:<test@example.com> SIZE=42 BODY=8BITMIME TEST=value\r\n"
); );
assert_eq!(format!("{}", RcptCommand::new(email.clone(), vec![])), assert_eq!(
"RCPT TO:<test@example.com>\r\n"); format!("{}", RcptCommand::new(email.clone(), vec![])),
assert_eq!(format!("{}", RcptCommand::new(email.clone(), vec![rcpt_parameter])), "RCPT TO:<test@example.com>\r\n"
"RCPT TO:<test@example.com> TEST=value\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!("{}", QuitCommand), "QUIT\r\n");
assert_eq!(format!("{}", DataCommand), "DATA\r\n"); assert_eq!(format!("{}", DataCommand), "DATA\r\n");
assert_eq!(format!("{}", NoopCommand), "NOOP\r\n"); assert_eq!(format!("{}", NoopCommand), "NOOP\r\n");
assert_eq!(format!("{}", HelpCommand::new(None)), "HELP\r\n"); assert_eq!(format!("{}", HelpCommand::new(None)), "HELP\r\n");
assert_eq!(format!("{}", HelpCommand::new(Some("test".to_string()))), assert_eq!(
"HELP test\r\n"); format!("{}", HelpCommand::new(Some("test".to_string()))),
assert_eq!(format!("{}", VrfyCommand::new("test".to_string())), "HELP test\r\n"
"VRFY test\r\n"); );
assert_eq!(format!("{}", ExpnCommand::new("test".to_string())), assert_eq!(
"EXPN test\r\n"); 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"); assert_eq!(format!("{}", RsetCommand), "RSET\r\n");
let credentials = Credentials::new("user".to_string(), "password".to_string()); let credentials = Credentials::new("user".to_string(), "password".to_string());
assert_eq!(format!("{}", assert_eq!(
AuthCommand::new(Mechanism::Plain, credentials.clone(), None).unwrap()), format!(
"AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"); "{}",
AuthCommand::new(Mechanism::Plain, credentials.clone(), None).unwrap()
),
"AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"
);
#[cfg(feature = "crammd5-auth")] #[cfg(feature = "crammd5-auth")]
assert_eq!( assert_eq!(
format!( format!(
@@ -352,9 +391,13 @@ mod test {
), ),
"dXNlciAzMTYxY2NmZDdmMjNlMzJiYmMzZTQ4NjdmYzk0YjE4Nw==\r\n" "dXNlciAzMTYxY2NmZDdmMjNlMzJiYmMzZTQ4NjdmYzk0YjE4Nw==\r\n"
); );
assert_eq!(format!("{}", assert_eq!(
AuthCommand::new(Mechanism::Login, credentials.clone(), None).unwrap()), format!(
"AUTH LOGIN\r\n"); "{}",
AuthCommand::new(Mechanism::Login, credentials.clone(), None).unwrap()
),
"AUTH LOGIN\r\n"
);
#[cfg(feature = "crammd5-auth")] #[cfg(feature = "crammd5-auth")]
assert_eq!( assert_eq!(
format!( format!(

View File

@@ -52,18 +52,14 @@ impl StdError for Error {
match *self { match *self {
// Try to display the first line of the server's response that usually // Try to display the first line of the server's response that usually
// contains a short humanly readable error message // contains a short humanly readable error message
Transient(ref err) => { Transient(ref err) => match err.first_line() {
match err.first_line() { Some(line) => line,
Some(line) => line, None => "undetailed transient error during SMTP transaction",
None => "undetailed transient error during SMTP transaction", },
} Permanent(ref err) => match err.first_line() {
} Some(line) => line,
Permanent(ref err) => { None => "undetailed permanent error during SMTP transaction",
match err.first_line() { },
Some(line) => line,
None => "undetailed permanent error during SMTP transaction",
}
}
ResponseParsing(err) => err, ResponseParsing(err) => err,
ChallengeParsing(ref err) => err.description(), ChallengeParsing(ref err) => err.description(),
Utf8Parsing(ref err) => err.description(), Utf8Parsing(ref err) => err.description(),

View File

@@ -44,9 +44,9 @@ impl ClientId {
/// found /// found
pub fn hostname() -> ClientId { pub fn hostname() -> ClientId {
ClientId::Domain(match get_hostname() { ClientId::Domain(match get_hostname() {
Some(name) => name, Some(name) => name,
None => DEFAULT_EHLO_HOSTNAME.to_string(), None => DEFAULT_EHLO_HOSTNAME.to_string(),
}) })
} }
} }
@@ -95,14 +95,16 @@ pub struct ServerInfo {
impl Display for ServerInfo { impl Display for ServerInfo {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, write!(
"{} with {}", f,
self.name, "{} with {}",
if self.features.is_empty() { self.name,
"no supported features".to_string() if self.features.is_empty() {
} else { "no supported features".to_string()
format!("{:?}", self.features) } else {
}) format!("{:?}", self.features)
}
)
} }
} }
@@ -132,29 +134,29 @@ impl ServerInfo {
"STARTTLS" => { "STARTTLS" => {
features.insert(Extension::StartTls); features.insert(Extension::StartTls);
} }
"AUTH" => { "AUTH" => for &mechanism in &splitted[1..] {
for &mechanism in &splitted[1..] { match mechanism {
match mechanism { "PLAIN" => {
"PLAIN" => { features.insert(Extension::Authentication(Mechanism::Plain));
features.insert(Extension::Authentication(Mechanism::Plain));
}
"LOGIN" => {
features.insert(Extension::Authentication(Mechanism::Login));
}
#[cfg(feature = "crammd5-auth")]
"CRAM-MD5" => {
features.insert(Extension::Authentication(Mechanism::CramMd5));
}
_ => (),
} }
"LOGIN" => {
features.insert(Extension::Authentication(Mechanism::Login));
}
#[cfg(feature = "crammd5-auth")]
"CRAM-MD5" => {
features.insert(Extension::Authentication(Mechanism::CramMd5));
}
_ => (),
} }
} },
_ => (), _ => (),
}; };
} }
Ok(ServerInfo { name: name.to_string(), Ok(ServerInfo {
features: features, }) name: name.to_string(),
features,
})
} }
/// Checks if the server supports an ESMTP feature /// Checks if the server supports an ESMTP feature
@@ -164,7 +166,8 @@ impl ServerInfo {
/// Checks if the server supports an ESMTP feature /// Checks if the server supports an ESMTP feature
pub fn supports_auth_mechanism(&self, mechanism: Mechanism) -> bool { pub fn supports_auth_mechanism(&self, mechanism: Mechanism) -> bool {
self.features.contains(&Extension::Authentication(mechanism)) self.features
.contains(&Extension::Authentication(mechanism))
} }
} }
@@ -192,12 +195,14 @@ impl Display for MailParameter {
MailParameter::Body(ref value) => write!(f, "BODY={}", value), MailParameter::Body(ref value) => write!(f, "BODY={}", value),
MailParameter::Size(size) => write!(f, "SIZE={}", size), MailParameter::Size(size) => write!(f, "SIZE={}", size),
MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"), MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
MailParameter::Other { ref keyword, MailParameter::Other {
value: Some(ref value), } => { ref keyword,
write!(f, "{}={}", keyword, XText(value)) value: Some(ref value),
} } => write!(f, "{}={}", keyword, XText(value)),
MailParameter::Other { ref keyword, MailParameter::Other {
value: None, } => f.write_str(keyword), ref keyword,
value: None,
} => f.write_str(keyword),
} }
} }
} }
@@ -235,12 +240,14 @@ pub enum RcptParameter {
impl Display for RcptParameter { impl Display for RcptParameter {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self { match *self {
RcptParameter::Other { ref keyword, RcptParameter::Other {
value: Some(ref value), } => { ref keyword,
write!(f, "{}={}", keyword, XText(value)) value: Some(ref value),
} } => write!(f, "{}={}", keyword, XText(value)),
RcptParameter::Other { ref keyword, RcptParameter::Other {
value: None, } => f.write_str(keyword), ref keyword,
value: None,
} => f.write_str(keyword),
} }
} }
} }
@@ -255,16 +262,22 @@ mod test {
#[test] #[test]
fn test_clientid_fmt() { fn test_clientid_fmt() {
assert_eq!(format!("{}", ClientId::new("test".to_string())), assert_eq!(
"test".to_string()); format!("{}", ClientId::new("test".to_string())),
"test".to_string()
);
} }
#[test] #[test]
fn test_extension_fmt() { fn test_extension_fmt() {
assert_eq!(format!("{}", Extension::EightBitMime), assert_eq!(
"8BITMIME".to_string()); format!("{}", Extension::EightBitMime),
assert_eq!(format!("{}", Extension::Authentication(Mechanism::Plain)), "8BITMIME".to_string()
"AUTH PLAIN".to_string()); );
assert_eq!(
format!("{}", Extension::Authentication(Mechanism::Plain)),
"AUTH PLAIN".to_string()
);
} }
#[test] #[test]
@@ -272,41 +285,67 @@ mod test {
let mut eightbitmime = HashSet::new(); let mut eightbitmime = HashSet::new();
assert!(eightbitmime.insert(Extension::EightBitMime)); assert!(eightbitmime.insert(Extension::EightBitMime));
assert_eq!(format!("{}", assert_eq!(
ServerInfo { name: "name".to_string(), format!(
features: eightbitmime.clone(), }), "{}",
"name with {EightBitMime}".to_string()); ServerInfo {
name: "name".to_string(),
features: eightbitmime.clone(),
}
),
"name with {EightBitMime}".to_string()
);
let empty = HashSet::new(); let empty = HashSet::new();
assert_eq!(format!("{}", assert_eq!(
ServerInfo { name: "name".to_string(), format!(
features: empty, }), "{}",
"name with no supported features".to_string()); ServerInfo {
name: "name".to_string(),
features: empty,
}
),
"name with no supported features".to_string()
);
let mut plain = HashSet::new(); let mut plain = HashSet::new();
assert!(plain.insert(Extension::Authentication(Mechanism::Plain))); assert!(plain.insert(Extension::Authentication(Mechanism::Plain)));
assert_eq!(format!("{}", assert_eq!(
ServerInfo { name: "name".to_string(), format!(
features: plain.clone(), }), "{}",
"name with {Authentication(Plain)}".to_string()); ServerInfo {
name: "name".to_string(),
features: plain.clone(),
}
),
"name with {Authentication(Plain)}".to_string()
);
} }
#[test] #[test]
fn test_serverinfo() { fn test_serverinfo() {
let response = Response::new(Code::new(Severity::PositiveCompletion, let response = Response::new(
Category::Unspecified4, Code::new(
Detail::One), Severity::PositiveCompletion,
vec!["me".to_string(), Category::Unspecified4,
"8BITMIME".to_string(), Detail::One,
"SIZE 42".to_string()]); ),
vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
);
let mut features = HashSet::new(); let mut features = HashSet::new();
assert!(features.insert(Extension::EightBitMime)); assert!(features.insert(Extension::EightBitMime));
let server_info = ServerInfo { name: "me".to_string(), let server_info = ServerInfo {
features: features, }; name: "me".to_string(),
features: features,
};
assert_eq!(ServerInfo::from_response(&response).unwrap(), server_info); assert_eq!(ServerInfo::from_response(&response).unwrap(), server_info);
@@ -315,13 +354,19 @@ mod test {
#[cfg(feature = "crammd5-auth")] #[cfg(feature = "crammd5-auth")]
assert!(!server_info.supports_auth_mechanism(Mechanism::CramMd5)); assert!(!server_info.supports_auth_mechanism(Mechanism::CramMd5));
let response2 = Response::new(Code::new(Severity::PositiveCompletion, let response2 = Response::new(
Category::Unspecified4, Code::new(
Detail::One), Severity::PositiveCompletion,
vec!["me".to_string(), Category::Unspecified4,
"AUTH PLAIN CRAM-MD5 OTHER".to_string(), Detail::One,
"8BITMIME".to_string(), ),
"SIZE 42".to_string()]); vec![
"me".to_string(),
"AUTH PLAIN CRAM-MD5 OTHER".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
],
);
let mut features2 = HashSet::new(); let mut features2 = HashSet::new();
assert!(features2.insert(Extension::EightBitMime)); assert!(features2.insert(Extension::EightBitMime));
@@ -329,8 +374,10 @@ mod test {
#[cfg(feature = "crammd5-auth")] #[cfg(feature = "crammd5-auth")]
assert!(features2.insert(Extension::Authentication(Mechanism::CramMd5),)); assert!(features2.insert(Extension::Authentication(Mechanism::CramMd5),));
let server_info2 = ServerInfo { name: "me".to_string(), let server_info2 = ServerInfo {
features: features2, }; name: "me".to_string(),
features: features2,
};
assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2); assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2);

View File

@@ -120,22 +120,23 @@ impl SmtpTransportBuilder {
/// * No authentication /// * No authentication
/// * No SMTPUTF8 support /// * No SMTPUTF8 support
/// * A 60 seconds timeout for smtp commands /// * A 60 seconds timeout for smtp commands
pub fn new<A: ToSocketAddrs>(addr: A, pub fn new<A: ToSocketAddrs>(
security: ClientSecurity) addr: A,
-> Result<SmtpTransportBuilder, Error> { security: ClientSecurity,
) -> Result<SmtpTransportBuilder, Error> {
let mut addresses = addr.to_socket_addrs()?; let mut addresses = addr.to_socket_addrs()?;
match addresses.next() { match addresses.next() {
Some(addr) => { Some(addr) => Ok(SmtpTransportBuilder {
Ok(SmtpTransportBuilder { server_addr: addr, server_addr: addr,
security: security, security,
smtp_utf8: false, smtp_utf8: false,
credentials: None, credentials: None,
connection_reuse: ConnectionReuseParameters::NoReuse, connection_reuse: ConnectionReuseParameters::NoReuse,
hello_name: ClientId::hostname(), hello_name: ClientId::hostname(),
authentication_mechanism: None, authentication_mechanism: None,
timeout: Some(Duration::new(60, 0)), }) timeout: Some(Duration::new(60, 0)),
} }),
None => Err(Error::Resolution), None => Err(Error::Resolution),
} }
} }
@@ -153,9 +154,10 @@ impl SmtpTransportBuilder {
} }
/// Enable connection reuse /// Enable connection reuse
pub fn connection_reuse(mut self, pub fn connection_reuse(
parameters: ConnectionReuseParameters) mut self,
-> SmtpTransportBuilder { parameters: ConnectionReuseParameters,
) -> SmtpTransportBuilder {
self.connection_reuse = parameters; self.connection_reuse = parameters;
self self
} }
@@ -234,14 +236,17 @@ impl<'a> SmtpTransport {
let tls_parameters = let tls_parameters =
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap()); ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
SmtpTransportBuilder::new((domain, SUBMISSION_PORT), SmtpTransportBuilder::new(
ClientSecurity::Required(tls_parameters)) (domain, SUBMISSION_PORT),
ClientSecurity::Required(tls_parameters),
)
} }
/// Creates a new configurable builder /// Creates a new configurable builder
pub fn builder<A: ToSocketAddrs>(addr: A, pub fn builder<A: ToSocketAddrs>(
security: ClientSecurity) addr: A,
-> Result<SmtpTransportBuilder, Error> { security: ClientSecurity,
) -> Result<SmtpTransportBuilder, Error> {
SmtpTransportBuilder::new(addr, security) SmtpTransportBuilder::new(addr, security)
} }
@@ -256,20 +261,24 @@ impl<'a> SmtpTransport {
pub fn new(builder: SmtpTransportBuilder) -> SmtpTransport { pub fn new(builder: SmtpTransportBuilder) -> SmtpTransport {
let client = Client::new(); let client = Client::new();
SmtpTransport { client: client, SmtpTransport {
client,
server_info: None, server_info: None,
client_info: builder, client_info: builder,
state: State { panic: false, state: State {
connection_reuse_count: 0, }, } panic: false,
connection_reuse_count: 0,
},
}
} }
/// Gets the EHLO response and updates server information /// Gets the EHLO response and updates server information
fn ehlo(&mut self) -> SmtpResult { fn ehlo(&mut self) -> SmtpResult {
// Extended Hello // Extended Hello
let ehlo_response = try_smtp!( let ehlo_response = try_smtp!(
self.client.command(EhloCommand::new( self.client.command(EhloCommand::new(ClientId::new(
ClientId::new(self.client_info.hello_name.to_string()), self.client_info.hello_name.to_string()
)), ),)),
self self
); );
@@ -306,13 +315,13 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
} }
if self.state.connection_reuse_count == 0 { if self.state.connection_reuse_count == 0 {
self.client.connect(&self.client_info.server_addr, self.client.connect(
match self.client_info.security { &self.client_info.server_addr,
ClientSecurity::Wrapper(ref tls_parameters) => { match self.client_info.security {
Some(tls_parameters) ClientSecurity::Wrapper(ref tls_parameters) => Some(tls_parameters),
} _ => None,
_ => None, },
})?; )?;
self.client.set_timeout(self.client_info.timeout)?; self.client.set_timeout(self.client_info.timeout)?;
@@ -321,11 +330,13 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
self.ehlo()?; self.ehlo()?;
match (&self.client_info.security.clone(), match (
self.server_info.as_ref() &self.client_info.security.clone(),
.unwrap() self.server_info
.supports_feature(Extension::StartTls)) .as_ref()
{ .unwrap()
.supports_feature(Extension::StartTls),
) {
(&ClientSecurity::Required(_), false) => { (&ClientSecurity::Required(_), false) => {
return Err(From::from("Could not encrypt connection, aborting")) return Err(From::from("Could not encrypt connection, aborting"))
} }
@@ -360,9 +371,10 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
}; };
for mechanism in accepted_mechanisms { for mechanism in accepted_mechanisms {
if self.server_info.as_ref() if self.server_info
.unwrap() .as_ref()
.supports_auth_mechanism(mechanism) .unwrap()
.supports_auth_mechanism(mechanism)
{ {
found = true; found = true;
try_smtp!( try_smtp!(
@@ -383,30 +395,38 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
// Mail // Mail
let mut mail_options = vec![]; let mut mail_options = vec![];
if self.server_info.as_ref() if self.server_info
.unwrap() .as_ref()
.supports_feature(Extension::EightBitMime) .unwrap()
.supports_feature(Extension::EightBitMime)
{ {
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime)); mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
} }
if self.server_info.as_ref() if self.server_info
.unwrap() .as_ref()
.supports_feature(Extension::SmtpUtfEight) && self.client_info.smtp_utf8 .unwrap()
.supports_feature(Extension::SmtpUtfEight) && self.client_info.smtp_utf8
{ {
mail_options.push(MailParameter::SmtpUtfEight); mail_options.push(MailParameter::SmtpUtfEight);
} }
try_smtp!(self.client.command(MailCommand::new(Some(email.from().clone()), mail_options,)), try_smtp!(
self); self.client
.command(MailCommand::new(Some(email.from().clone()), mail_options,)),
self
);
// Log the mail command // Log the mail command
info!("{}: from=<{}>", message_id, email.from()); info!("{}: from=<{}>", message_id, email.from());
// Recipient // Recipient
for to_address in &email.to() { for to_address in &email.to() {
try_smtp!(self.client.command(RcptCommand::new(to_address.clone(), vec![]),), try_smtp!(
self); self.client
.command(RcptCommand::new(to_address.clone(), vec![]),),
self
);
// Log the rcpt command // Log the rcpt command
info!("{}: to=<{}>", message_id, to_address); info!("{}: to=<{}>", message_id, to_address);
} }
@@ -422,16 +442,19 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
self.state.connection_reuse_count += 1; self.state.connection_reuse_count += 1;
// Log the message // Log the message
info!("{}: conn_use={}, status=sent ({})", info!(
message_id, "{}: conn_use={}, status=sent ({})",
self.state.connection_reuse_count, message_id,
result.as_ref() self.state.connection_reuse_count,
.ok() result
.unwrap() .as_ref()
.message .ok()
.iter() .unwrap()
.next() .message
.unwrap_or(&"no response".to_string())); .iter()
.next()
.unwrap_or(&"no response".to_string())
);
} }
// Test if we can reuse the existing connection // Test if we can reuse the existing connection

View File

@@ -91,9 +91,11 @@ impl Display for Code {
impl Code { impl Code {
/// Creates a new `Code` structure /// Creates a new `Code` structure
pub fn new(severity: Severity, category: Category, detail: Detail) -> Code { pub fn new(severity: Severity, category: Category, detail: Detail) -> Code {
Code { severity: severity, Code {
category: category, severity,
detail: detail, } category,
detail,
}
} }
} }
@@ -124,8 +126,7 @@ impl FromStr for Response {
impl Response { impl Response {
/// Creates a new `Response` /// Creates a new `Response`
pub fn new(code: Code, message: Vec<String>) -> Response { pub fn new(code: Code, message: Vec<String>) -> Response {
Response { code: code, Response { code, message }
message: message, }
} }
/// Tells if the response is positive /// Tells if the response is positive
@@ -143,7 +144,8 @@ impl Response {
/// Returns only the first word of the message if possible /// Returns only the first word of the message if possible
pub fn first_word(&self) -> Option<&str> { pub fn first_word(&self) -> Option<&str> {
self.message.get(0) self.message
.get(0)
.and_then(|line| line.split_whitespace().next()) .and_then(|line| line.split_whitespace().next())
} }
@@ -155,11 +157,17 @@ impl Response {
// Parsers (originaly from tokio-smtp) // Parsers (originaly from tokio-smtp)
named!(parse_code<Code>, named!(
map!(tuple!(parse_severity, parse_category, parse_detail), parse_code<Code>,
|(severity, category, detail)| Code { severity: severity, map!(
category: category, tuple!(parse_severity, parse_category, parse_detail),
detail: detail, })); |(severity, category, detail)| Code {
severity,
category,
detail,
}
)
);
named!( named!(
parse_severity<Severity>, parse_severity<Severity>,
@@ -199,34 +207,47 @@ named!(
) )
); );
named!(parse_response<Response>, named!(
map_res!(tuple!(// Parse any number of continuation lines. parse_response<Response>,
many0!(tuple!(parse_code, map_res!(
preceded!(char!('-'), tuple!(
take_until_and_consume!(b"\r\n".as_ref())))), // Parse any number of continuation lines.
// Parse the final line. many0!(tuple!(
tuple!(parse_code, parse_code,
terminated!(opt!(preceded!(char!(' '), preceded!(char!('-'), take_until_and_consume!(b"\r\n".as_ref()))
take_until!(b"\r\n".as_ref()))), )),
crlf))), // Parse the final line.
|(lines, (last_code, last_line)): (Vec<_>, _)| { tuple!(
// Check that all codes are equal. parse_code,
if !lines.iter().all(|&(ref code, _)| *code == last_code) { terminated!(
return Err(()); opt!(preceded!(char!(' '), take_until!(b"\r\n".as_ref()))),
} crlf
)
)
),
|(lines, (last_code, last_line)): (Vec<_>, _)| {
// Check that all codes are equal.
if !lines.iter().all(|&(ref code, _)| *code == last_code) {
return Err(());
}
// Extract text from lines, and append last line. // Extract text from lines, and append last line.
let mut lines = lines.into_iter().map(|(_, text)| text).collect::<Vec<_>>(); let mut lines = lines.into_iter().map(|(_, text)| text).collect::<Vec<_>>();
if let Some(text) = last_line { if let Some(text) = last_line {
lines.push(text); lines.push(text);
} }
Ok(Response { code: last_code, Ok(Response {
message: lines.into_iter() code: last_code,
.map(|line| from_utf8(line).map(|s| s.to_string())) message: lines
.collect::<result::Result<Vec<_>, _>>() .into_iter()
.map_err(|_| ())?, }) .map(|line| from_utf8(line).map(|s| s.to_string()))
})); .collect::<result::Result<Vec<_>, _>>()
.map_err(|_| ())?,
})
}
)
);
#[cfg(test)] #[cfg(test)]
mod test { mod test {
@@ -244,19 +265,27 @@ mod test {
#[test] #[test]
fn test_code_new() { fn test_code_new() {
assert_eq!(Code::new(Severity::TransientNegativeCompletion, assert_eq!(
Category::Connections, Code::new(
Detail::Zero,), Severity::TransientNegativeCompletion,
Code { severity: Severity::TransientNegativeCompletion, Category::Connections,
category: Category::Connections, Detail::Zero,
detail: Detail::Zero, }); ),
Code {
severity: Severity::TransientNegativeCompletion,
category: Category::Connections,
detail: Detail::Zero,
}
);
} }
#[test] #[test]
fn test_code_display() { fn test_code_display() {
let code = Code { severity: Severity::TransientNegativeCompletion, let code = Code {
severity: Severity::TransientNegativeCompletion,
category: Category::Connections, category: Category::Connections,
detail: Detail::One, }; detail: Detail::One,
};
assert_eq!(code.to_string(), "421"); assert_eq!(code.to_string(), "421");
} }
@@ -264,14 +293,22 @@ mod test {
#[test] #[test]
fn test_response_from_str() { fn test_response_from_str() {
let raw_response = "250-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250 AUTH PLAIN CRAM-MD5\r\n"; let raw_response = "250-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250 AUTH PLAIN CRAM-MD5\r\n";
assert_eq!(raw_response.parse::<Response>().unwrap(), assert_eq!(
Response { code: Code { severity: Severity::PositiveCompletion, raw_response.parse::<Response>().unwrap(),
category: Category::MailSystem, Response {
detail: Detail::Zero, }, code: Code {
message: vec!["me".to_string(), severity: Severity::PositiveCompletion,
"8BITMIME".to_string(), category: Category::MailSystem,
"SIZE 42".to_string(), detail: Detail::Zero,
"AUTH PLAIN CRAM-MD5".to_string()], }); },
message: vec![
"me".to_string(),
"8BITMIME".to_string(),
"SIZE 42".to_string(),
"AUTH PLAIN CRAM-MD5".to_string(),
],
}
);
let wrong_code = "2506-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250 AUTH PLAIN CRAM-MD5\r\n"; let wrong_code = "2506-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250 AUTH PLAIN CRAM-MD5\r\n";
assert!(wrong_code.parse::<Response>().is_err()); assert!(wrong_code.parse::<Response>().is_err());

View File

@@ -33,11 +33,12 @@ mod tests {
#[test] #[test]
fn test() { fn test() {
for (input, expect) in vec![("bjorn", "bjorn"), for (input, expect) in vec![
("bjørn", "bjørn"), ("bjorn", "bjorn"),
("Ø+= ❤️‰", "Ø+2B+3D+20❤"), ("bjørn", "bjørn"),
("+", "+2B")] ("Ø+= ❤️‰", "Ø+2B+3D+20❤"),
{ ("+", "+2B"),
] {
assert_eq!(format!("{}", XText(input)), expect); assert_eq!(format!("{}", XText(input)), expect);
} }
} }

View File

@@ -15,7 +15,7 @@ pub struct StubEmailTransport {
impl StubEmailTransport { impl StubEmailTransport {
/// Creates a new transport that always returns the given response /// Creates a new transport that always returns the given response
pub fn new(response: StubResult) -> StubEmailTransport { pub fn new(response: StubResult) -> StubEmailTransport {
StubEmailTransport { response: response } StubEmailTransport { response }
} }
/// Creates a new transport that always returns a success response /// Creates a new transport that always returns a success response
@@ -29,10 +29,12 @@ pub type StubResult = Result<(), ()>;
impl<'a, T: Read + 'a> EmailTransport<'a, T, StubResult> for StubEmailTransport { impl<'a, T: Read + 'a> EmailTransport<'a, T, StubResult> for StubEmailTransport {
fn send<U: SendableEmail<'a, T>>(&mut self, email: &'a U) -> StubResult { fn send<U: SendableEmail<'a, T>>(&mut self, email: &'a U) -> StubResult {
info!("{}: from=<{}> to=<{:?}>", info!(
email.message_id(), "{}: from=<{}> to=<{:?}>",
email.from(), email.message_id(),
email.to()); email.from(),
email.to()
);
self.response self.response
} }
} }

View File

@@ -14,10 +14,12 @@ mod test {
#[test] #[test]
fn file_transport() { fn file_transport() {
let mut sender = FileEmailTransport::new(temp_dir()); let mut sender = FileEmailTransport::new(temp_dir());
let email = SimpleSendableEmail::new(EmailAddress::new("user@localhost".to_string()), let email = SimpleSendableEmail::new(
vec![EmailAddress::new("root@localhost".to_string())], EmailAddress::new("user@localhost".to_string()),
"file_id".to_string(), vec![EmailAddress::new("root@localhost".to_string())],
"Hello file".to_string()); "file_id".to_string(),
"Hello file".to_string(),
);
let result = sender.send(&email); let result = sender.send(&email);
assert!(result.is_ok()); assert!(result.is_ok());
@@ -27,9 +29,11 @@ mod test {
let mut buffer = String::new(); let mut buffer = String::new();
let _ = f.read_to_string(&mut buffer); let _ = f.read_to_string(&mut buffer);
assert_eq!(buffer, assert_eq!(
"{\"to\":[\"root@localhost\"],\"from\":\"user@localhost\",\"message_id\":\ buffer,
\"file_id\",\"message\":[72,101,108,108,111,32,102,105,108,101]}"); "{\"to\":[\"root@localhost\"],\"from\":\"user@localhost\",\"message_id\":\
\"file_id\",\"message\":[72,101,108,108,111,32,102,105,108,101]}"
);
remove_file(file).unwrap(); remove_file(file).unwrap();
} }

View File

@@ -10,10 +10,12 @@ mod test {
#[test] #[test]
fn sendmail_transport_simple() { fn sendmail_transport_simple() {
let mut sender = SendmailTransport::new(); let mut sender = SendmailTransport::new();
let email = SimpleSendableEmail::new(EmailAddress::new("user@localhost".to_string()), let email = SimpleSendableEmail::new(
vec![EmailAddress::new("root@localhost".to_string())], EmailAddress::new("user@localhost".to_string()),
"sendmail_id".to_string(), vec![EmailAddress::new("root@localhost".to_string())],
"Hello sendmail".to_string()); "sendmail_id".to_string(),
"Hello sendmail".to_string(),
);
let result = sender.send(&email); let result = sender.send(&email);
println!("{:?}", result); println!("{:?}", result);

View File

@@ -8,12 +8,15 @@ mod test {
#[test] #[test]
fn smtp_transport_simple() { fn smtp_transport_simple() {
let mut sender = SmtpTransport::builder("127.0.0.1:2525", ClientSecurity::None).unwrap() let mut sender = SmtpTransport::builder("127.0.0.1:2525", ClientSecurity::None)
.build(); .unwrap()
let email = SimpleSendableEmail::new(EmailAddress::new("user@localhost".to_string()), .build();
vec![EmailAddress::new("root@localhost".to_string())], let email = SimpleSendableEmail::new(
"smtp_id".to_string(), EmailAddress::new("user@localhost".to_string()),
"Hello smtp".to_string()); vec![EmailAddress::new("root@localhost".to_string())],
"smtp_id".to_string(),
"Hello smtp".to_string(),
);
sender.send(&email).unwrap(); sender.send(&email).unwrap();
} }

View File

@@ -8,10 +8,12 @@ fn stub_transport() {
let mut sender_ok = StubEmailTransport::new_positive(); let mut sender_ok = StubEmailTransport::new_positive();
let mut sender_ko = StubEmailTransport::new(Err(())); let mut sender_ko = StubEmailTransport::new(Err(()));
let email = SimpleSendableEmail::new(EmailAddress::new("user@localhost".to_string()), let email = SimpleSendableEmail::new(
vec![EmailAddress::new("root@localhost".to_string())], EmailAddress::new("user@localhost".to_string()),
"stub_id".to_string(), vec![EmailAddress::new("root@localhost".to_string())],
"Hello stub".to_string()); "stub_id".to_string(),
"Hello stub".to_string(),
);
sender_ok.send(&email).unwrap(); sender_ok.send(&email).unwrap();
sender_ko.send(&email).unwrap_err(); sender_ko.send(&email).unwrap_err();

View File

@@ -28,4 +28,4 @@ mime = "^0.3"
time = "^0.1" time = "^0.1"
uuid = { version = "^0.6", features = ["v4"] } uuid = { version = "^0.6", features = ["v4"] }
lettre = { version = "^0.8", path = "../lettre", default-features = false } lettre = { version = "^0.8", path = "../lettre", default-features = false }
base64 = "0.9.0" base64 = "^0.9"

View File

@@ -19,8 +19,9 @@ fn main() {
.unwrap(); .unwrap();
// Open a local connection on port 25 // Open a local connection on port 25
let mut mailer = SmtpTransport::builder_unencrypted_localhost().unwrap() let mut mailer = SmtpTransport::builder_unencrypted_localhost()
.build(); .unwrap()
.build();
// Send the email // Send the email
let result = mailer.send(&email); let result = mailer.send(&email);

View File

@@ -4,12 +4,12 @@
#![doc(html_root_url = "https://docs.rs/lettre_email/0.8.0")] #![doc(html_root_url = "https://docs.rs/lettre_email/0.8.0")]
#![deny(missing_docs, unsafe_code, unstable_features, warnings, missing_debug_implementations)] #![deny(missing_docs, unsafe_code, unstable_features, warnings, missing_debug_implementations)]
extern crate base64;
extern crate email as email_format; extern crate email as email_format;
extern crate lettre; extern crate lettre;
extern crate mime; extern crate mime;
extern crate time; extern crate time;
extern crate uuid; extern crate uuid;
extern crate base64;
pub mod error; pub mod error;
@@ -123,17 +123,17 @@ impl IntoEmail for SimpleEmail {
/// Simple representation of an email, useful for some transports /// Simple representation of an email, useful for some transports
#[derive(PartialEq, Eq, Clone, Debug, Default)] #[derive(PartialEq, Eq, Clone, Debug, Default)]
pub struct SimpleEmail { pub struct SimpleEmail {
from: Option<Mailbox>, from: Option<Mailbox>,
to: Vec<Mailbox>, to: Vec<Mailbox>,
cc: Vec<Mailbox>, cc: Vec<Mailbox>,
bcc: Vec<Mailbox>, bcc: Vec<Mailbox>,
reply_to: Option<Mailbox>, reply_to: Option<Mailbox>,
subject: Option<String>, subject: Option<String>,
date: Option<Tm>, date: Option<Tm>,
html: Option<String>, html: Option<String>,
text: Option<String>, text: Option<String>,
attachments: Vec<String>, attachments: Vec<String>,
headers: Vec<Header>, headers: Vec<Header>,
} }
impl SimpleEmail { impl SimpleEmail {
@@ -307,8 +307,10 @@ pub struct Envelope {
impl Envelope { impl Envelope {
/// Constructs an envelope with no receivers and an empty sender /// Constructs an envelope with no receivers and an empty sender
pub fn new() -> Self { pub fn new() -> Self {
Envelope { to: vec![], Envelope {
from: String::new(), } to: vec![],
from: String::new(),
}
} }
/// Adds a receiver /// Adds a receiver
pub fn to<S: Into<String>>(mut self, address: S) -> Self { pub fn to<S: Into<String>>(mut self, address: S) -> Self {
@@ -344,7 +346,9 @@ pub struct Email {
impl PartBuilder { impl PartBuilder {
/// Creates a new empty part /// Creates a new empty part
pub fn new() -> PartBuilder { pub fn new() -> PartBuilder {
PartBuilder { message: MimeMessage::new_blank_message(), } PartBuilder {
message: MimeMessage::new_blank_message(),
}
} }
/// Adds a generic header /// Adds a generic header
@@ -412,15 +416,17 @@ impl PartBuilder {
impl EmailBuilder { impl EmailBuilder {
/// Creates a new empty email /// Creates a new empty email
pub fn new() -> EmailBuilder { pub fn new() -> EmailBuilder {
EmailBuilder { message: PartBuilder::new(), EmailBuilder {
to_header: vec![], message: PartBuilder::new(),
from_header: vec![], to_header: vec![],
cc_header: vec![], from_header: vec![],
bcc_header: vec![], cc_header: vec![],
bcc_header: vec![],
reply_to_header: vec![], reply_to_header: vec![],
sender_header: None, sender_header: None,
envelope: None, envelope: None,
date_issued: false, } date_issued: false,
}
} }
/// Sets the email body /// Sets the email body
@@ -525,7 +531,8 @@ impl EmailBuilder {
/// Adds a `Subject` header /// Adds a `Subject` header
pub fn set_subject<S: Into<String>>(&mut self, subject: S) { pub fn set_subject<S: Into<String>>(&mut self, subject: S) {
self.message.add_header(("Subject".to_string(), subject.into())); self.message
.add_header(("Subject".to_string(), subject.into()));
} }
/// Adds a `Date` header with the given date /// Adds a `Date` header with the given date
@@ -536,27 +543,30 @@ impl EmailBuilder {
/// Adds a `Date` header with the given date /// Adds a `Date` header with the given date
pub fn set_date(&mut self, date: &Tm) { pub fn set_date(&mut self, date: &Tm) {
self.message.add_header(("Date", Tm::rfc822z(date).to_string())); self.message
.add_header(("Date", Tm::rfc822z(date).to_string()));
self.date_issued = true; self.date_issued = true;
} }
/// Adds an attachment to the email /// Adds an attachment to the email
pub fn attachment(mut self, pub fn attachment(
path: &Path, mut self,
filename: Option<&str>, path: &Path,
content_type: &Mime) filename: Option<&str>,
-> Result<EmailBuilder, Error> { content_type: &Mime,
) -> Result<EmailBuilder, Error> {
self.set_attachment(path, filename, content_type)?; self.set_attachment(path, filename, content_type)?;
Ok(self) Ok(self)
} }
/// Adds an attachment to the email /// Adds an attachment to the email
/// If filename is not provided, the name of the file will be used. /// If filename is not provided, the name of the file will be used.
pub fn set_attachment(&mut self, pub fn set_attachment(
path: &Path, &mut self,
filename: Option<&str>, path: &Path,
content_type: &Mime) filename: Option<&str>,
-> Result<(), Error> { content_type: &Mime,
) -> Result<(), Error> {
let file = File::open(path); let file = File::open(path);
let body = match file { let body = match file {
Ok(mut f) => { Ok(mut f) => {
@@ -576,27 +586,25 @@ impl EmailBuilder {
let actual_filename = match filename { let actual_filename = match filename {
Some(name) => name, Some(name) => name,
None => { None => match path.file_name() {
match path.file_name() { Some(name) => match name.to_str() {
Some(name) => { Some(name) => name,
match name.to_str() {
Some(name) => name,
None => return Err(Error::CannotParseFilename),
}
}
None => return Err(Error::CannotParseFilename), None => return Err(Error::CannotParseFilename),
} },
} None => return Err(Error::CannotParseFilename),
},
}; };
let encoded_body = base64::encode(&body); let encoded_body = base64::encode(&body);
let content = PartBuilder::new().body(encoded_body) let content = PartBuilder::new()
.header(("Content-Disposition", .body(encoded_body)
format!("attachment; filename=\"{}\"", .header((
actual_filename))) "Content-Disposition",
.header(("Content-Type", content_type.to_string())) format!("attachment; filename=\"{}\"", actual_filename),
.header(("Content-Transfer-Encoding", "base64")) ))
.build(); .header(("Content-Type", content_type.to_string()))
.header(("Content-Transfer-Encoding", "base64"))
.build();
self.set_message_type(MimeMultipartType::Mixed); self.set_message_type(MimeMultipartType::Mixed);
self.add_child(content); self.add_child(content);
@@ -635,7 +643,10 @@ impl EmailBuilder {
/// Sets the email body to plain text content /// Sets the email body to plain text content
pub fn set_text<S: Into<String>>(&mut self, body: S) { pub fn set_text<S: Into<String>>(&mut self, body: S) {
self.message.set_body(body); self.message.set_body(body);
self.message.add_header(("Content-Type", format!("{}", mime::TEXT_PLAIN_UTF_8).as_ref())); self.message.add_header((
"Content-Type",
format!("{}", mime::TEXT_PLAIN_UTF_8).as_ref(),
));
} }
/// Sets the email body to HTML content /// Sets the email body to HTML content
@@ -647,34 +658,41 @@ impl EmailBuilder {
/// Sets the email body to HTML content /// Sets the email body to HTML content
pub fn set_html<S: Into<String>>(&mut self, body: S) { pub fn set_html<S: Into<String>>(&mut self, body: S) {
self.message.set_body(body); self.message.set_body(body);
self.message.add_header(("Content-Type", format!("{}", mime::TEXT_HTML).as_ref())); self.message
.add_header(("Content-Type", format!("{}", mime::TEXT_HTML).as_ref()));
} }
/// Sets the email content /// Sets the email content
pub fn alternative<S: Into<String>, T: Into<String>>(mut self, pub fn alternative<S: Into<String>, T: Into<String>>(
body_html: S, mut self,
body_text: T) body_html: S,
-> EmailBuilder { body_text: T,
) -> EmailBuilder {
self.set_alternative(body_html, body_text); self.set_alternative(body_html, body_text);
self self
} }
/// Sets the email content /// Sets the email content
pub fn set_alternative<S: Into<String>, T: Into<String>>(&mut self, pub fn set_alternative<S: Into<String>, T: Into<String>>(
body_html: S, &mut self,
body_text: T) { body_html: S,
body_text: T,
) {
let mut alternate = PartBuilder::new(); let mut alternate = PartBuilder::new();
alternate.set_message_type(MimeMultipartType::Alternative); alternate.set_message_type(MimeMultipartType::Alternative);
let text = PartBuilder::new().body(body_text) let text = PartBuilder::new()
.header(("Content-Type", .body(body_text)
format!("{}", mime::TEXT_PLAIN_UTF_8).as_ref())) .header((
.build(); "Content-Type",
format!("{}", mime::TEXT_PLAIN_UTF_8).as_ref(),
))
.build();
let html = PartBuilder::new().body(body_html) let html = PartBuilder::new()
.header(("Content-Type", .body(body_html)
format!("{}", mime::TEXT_HTML).as_ref())) .header(("Content-Type", format!("{}", mime::TEXT_HTML).as_ref()))
.build(); .build();
alternate.add_child(text); alternate.add_child(text);
alternate.add_child(html); alternate.add_child(html);
@@ -726,17 +744,16 @@ impl EmailBuilder {
// we need to generate the envelope // we need to generate the envelope
let mut e = Envelope::new(); let mut e = Envelope::new();
// add all receivers in to_header and cc_header // add all receivers in to_header and cc_header
for receiver in self.to_header.iter() for receiver in self.to_header
.chain(self.cc_header.iter()) .iter()
.chain(self.bcc_header.iter()) .chain(self.cc_header.iter())
.chain(self.bcc_header.iter())
{ {
match *receiver { match *receiver {
Address::Mailbox(ref m) => e.add_to(m.address.clone()), Address::Mailbox(ref m) => e.add_to(m.address.clone()),
Address::Group(_, ref ms) => { Address::Group(_, ref ms) => for m in ms.iter() {
for m in ms.iter() { e.add_to(m.address.clone());
e.add_to(m.address.clone()); },
}
}
} }
} }
if e.to.is_empty() { if e.to.is_empty() {
@@ -769,7 +786,8 @@ impl EmailBuilder {
// Add the collected addresses as mailbox-list all at once. // Add the collected addresses as mailbox-list all at once.
// The unwraps are fine because the conversions for Vec<Address> never errs. // The unwraps are fine because the conversions for Vec<Address> never errs.
if !self.to_header.is_empty() { if !self.to_header.is_empty() {
self.message.add_header(Header::new_with_value("To".into(), self.to_header).unwrap()); self.message
.add_header(Header::new_with_value("To".into(), self.to_header).unwrap());
} }
if !self.from_header.is_empty() { if !self.from_header.is_empty() {
self.message self.message
@@ -778,10 +796,12 @@ impl EmailBuilder {
return Err(Error::MissingFrom); return Err(Error::MissingFrom);
} }
if !self.cc_header.is_empty() { if !self.cc_header.is_empty() {
self.message.add_header(Header::new_with_value("Cc".into(), self.cc_header).unwrap()); self.message
.add_header(Header::new_with_value("Cc".into(), self.cc_header).unwrap());
} }
if !self.bcc_header.is_empty() { if !self.bcc_header.is_empty() {
self.message.add_header(Header::new_with_value("Bcc".into(), self.bcc_header).unwrap()); self.message
.add_header(Header::new_with_value("Bcc".into(), self.bcc_header).unwrap());
} }
if !self.reply_to_header.is_empty() { if !self.reply_to_header.is_empty() {
self.message.add_header( self.message.add_header(
@@ -790,28 +810,33 @@ impl EmailBuilder {
} }
if !self.date_issued { if !self.date_issued {
self.message.add_header(("Date", Tm::rfc822z(&now()).to_string().as_ref())); self.message
.add_header(("Date", Tm::rfc822z(&now()).to_string().as_ref()));
} }
self.message.add_header(("MIME-Version", "1.0")); self.message.add_header(("MIME-Version", "1.0"));
let message_id = Uuid::new_v4(); let message_id = Uuid::new_v4();
if let Ok(header) = Header::new_with_value("Message-ID".to_string(), if let Ok(header) = Header::new_with_value(
format!("<{}.lettre@localhost>", message_id)) "Message-ID".to_string(),
{ format!("<{}.lettre@localhost>", message_id),
) {
self.message.add_header(header) self.message.add_header(header)
} }
Ok(Email { message: self.message.build().as_string().into_bytes(), Ok(Email {
envelope: envelope, message: self.message.build().as_string().into_bytes(),
message_id: message_id, }) envelope: envelope,
message_id: message_id,
})
} }
} }
impl<'a> SendableEmail<'a, &'a [u8]> for Email { impl<'a> SendableEmail<'a, &'a [u8]> for Email {
fn to(&self) -> Vec<EmailAddress> { fn to(&self) -> Vec<EmailAddress> {
self.envelope.to self.envelope
.to
.iter() .iter()
.map(|x| EmailAddress::new(x.clone())) .map(|x| EmailAddress::new(x.clone()))
.collect() .collect()
@@ -866,46 +891,56 @@ mod test {
let email_builder = SimpleEmail::default(); let email_builder = SimpleEmail::default();
let date_now = now(); let date_now = now();
let email = email_builder.to("user@localhost") let email = email_builder
.from("user@localhost") .to("user@localhost")
.cc(("cc@localhost", "Alias")) .from("user@localhost")
.reply_to("reply@localhost") .cc(("cc@localhost", "Alias"))
.text("Hello World!") .reply_to("reply@localhost")
.date(date_now.clone()) .text("Hello World!")
.subject("Hello") .date(date_now.clone())
.header(("X-test", "value")) .subject("Hello")
.into_email() .header(("X-test", "value"))
.unwrap(); .into_email()
.unwrap();
assert_eq!(format!("{}", String::from_utf8_lossy(email.message().as_ref())), assert_eq!(
format!("Subject: Hello\r\nContent-Type: text/plain; \ format!("{}", String::from_utf8_lossy(email.message().as_ref())),
charset=utf-8\r\nX-test: value\r\nTo: <user@localhost>\r\nFrom: \ format!(
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\nReply-To: \ "Subject: Hello\r\nContent-Type: text/plain; \
<reply@localhost>\r\nDate: {}\r\nMIME-Version: 1.0\r\nMessage-ID: \ charset=utf-8\r\nX-test: value\r\nTo: <user@localhost>\r\nFrom: \
<{}.lettre@localhost>\r\n\r\nHello World!\r\n", <user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\nReply-To: \
date_now.rfc822z(), <reply@localhost>\r\nDate: {}\r\nMIME-Version: 1.0\r\nMessage-ID: \
email.message_id())); <{}.lettre@localhost>\r\n\r\nHello World!\r\n",
date_now.rfc822z(),
email.message_id()
)
);
} }
#[test] #[test]
fn test_multiple_from() { fn test_multiple_from() {
let email_builder = EmailBuilder::new(); let email_builder = EmailBuilder::new();
let date_now = now(); let date_now = now();
let email = email_builder.to("anna@example.com") let email = email_builder
.from("dieter@example.com") .to("anna@example.com")
.from("joachim@example.com") .from("dieter@example.com")
.date(&date_now) .from("joachim@example.com")
.subject("Invitation") .date(&date_now)
.body("We invite you!") .subject("Invitation")
.build() .body("We invite you!")
.unwrap(); .build()
assert_eq!(format!("{}", String::from_utf8_lossy(email.message().as_ref())), .unwrap();
format!("Date: {}\r\nSubject: Invitation\r\nSender: \ assert_eq!(
<dieter@example.com>\r\nTo: <anna@example.com>\r\nFrom: \ format!("{}", String::from_utf8_lossy(email.message().as_ref())),
<dieter@example.com>, <joachim@example.com>\r\nMIME-Version: \ format!(
1.0\r\nMessage-ID: <{}.lettre@localhost>\r\n\r\nWe invite you!\r\n", "Date: {}\r\nSubject: Invitation\r\nSender: \
date_now.rfc822z(), <dieter@example.com>\r\nTo: <anna@example.com>\r\nFrom: \
email.message_id())); <dieter@example.com>, <joachim@example.com>\r\nMIME-Version: \
1.0\r\nMessage-ID: <{}.lettre@localhost>\r\n\r\nWe invite you!\r\n",
date_now.rfc822z(),
email.message_id()
)
);
} }
#[test] #[test]
@@ -913,28 +948,33 @@ mod test {
let email_builder = EmailBuilder::new(); let email_builder = EmailBuilder::new();
let date_now = now(); let date_now = now();
let email = email_builder.to("user@localhost") let email = email_builder
.from("user@localhost") .to("user@localhost")
.cc(("cc@localhost", "Alias")) .from("user@localhost")
.bcc("bcc@localhost") .cc(("cc@localhost", "Alias"))
.reply_to("reply@localhost") .bcc("bcc@localhost")
.sender("sender@localhost") .reply_to("reply@localhost")
.body("Hello World!") .sender("sender@localhost")
.date(&date_now) .body("Hello World!")
.subject("Hello") .date(&date_now)
.header(("X-test", "value")) .subject("Hello")
.build() .header(("X-test", "value"))
.unwrap(); .build()
.unwrap();
assert_eq!(format!("{}", String::from_utf8_lossy(email.message().as_ref())), assert_eq!(
format!("Date: {}\r\nSubject: Hello\r\nX-test: value\r\nSender: \ format!("{}", String::from_utf8_lossy(email.message().as_ref())),
<sender@localhost>\r\nTo: <user@localhost>\r\nFrom: \ format!(
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\n\ "Date: {}\r\nSubject: Hello\r\nX-test: value\r\nSender: \
Bcc: <bcc@localhost>\r\nReply-To: <reply@localhost>\r\n\ <sender@localhost>\r\nTo: <user@localhost>\r\nFrom: \
MIME-Version: 1.0\r\nMessage-ID: \ <user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\n\
<{}.lettre@localhost>\r\n\r\nHello World!\r\n", Bcc: <bcc@localhost>\r\nReply-To: <reply@localhost>\r\n\
date_now.rfc822z(), MIME-Version: 1.0\r\nMessage-ID: \
email.message_id())); <{}.lettre@localhost>\r\n\r\nHello World!\r\n",
date_now.rfc822z(),
email.message_id()
)
);
} }
#[test] #[test]
@@ -942,24 +982,29 @@ mod test {
let email_builder = EmailBuilder::new(); let email_builder = EmailBuilder::new();
let date_now = now(); let date_now = now();
let email = email_builder.to("user@localhost") let email = email_builder
.from("user@localhost") .to("user@localhost")
.cc(("cc@localhost", "Alias")) .from("user@localhost")
.bcc("bcc@localhost") .cc(("cc@localhost", "Alias"))
.reply_to("reply@localhost") .bcc("bcc@localhost")
.sender("sender@localhost") .reply_to("reply@localhost")
.body("Hello World!") .sender("sender@localhost")
.date(&date_now) .body("Hello World!")
.subject("Hello") .date(&date_now)
.header(("X-test", "value")) .subject("Hello")
.build() .header(("X-test", "value"))
.unwrap(); .build()
.unwrap();
assert_eq!(email.from().to_string(), "sender@localhost".to_string()); assert_eq!(email.from().to_string(), "sender@localhost".to_string());
assert_eq!(email.to(), assert_eq!(
vec![EmailAddress::new("user@localhost".to_string()), email.to(),
EmailAddress::new("cc@localhost".to_string()), vec![
EmailAddress::new("bcc@localhost".to_string())]); EmailAddress::new("user@localhost".to_string()),
EmailAddress::new("cc@localhost".to_string()),
EmailAddress::new("bcc@localhost".to_string()),
]
);
} }
} }

View File

@@ -1,6 +0,0 @@
write_mode = "Overwrite"
reorder_imports = true
reorder_imported_names = true
reorder_imports_in_group = true
struct_field_align_threshold = 20
indent_style = "Visual"