style(all): Run stable rustfmt and remove redundant field names in structs
This commit is contained in:
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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())?;
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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>"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(()),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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!(
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
|
||||||
Reference in New Issue
Block a user