@@ -9,16 +9,14 @@ use lettre::smtp::ConnectionReuseParameters;
|
||||
|
||||
#[bench]
|
||||
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).unwrap()
|
||||
.build();
|
||||
b.iter(|| {
|
||||
let email = SimpleSendableEmail::new(
|
||||
EmailAddress::new("user@localhost".to_string()),
|
||||
let email =
|
||||
SimpleSendableEmail::new(EmailAddress::new("user@localhost".to_string()),
|
||||
vec![EmailAddress::new("root@localhost".to_string())],
|
||||
"id".to_string(),
|
||||
"Hello world".to_string(),
|
||||
);
|
||||
"Hello world".to_string());
|
||||
let result = sender.send(&email);
|
||||
assert!(result.is_ok());
|
||||
});
|
||||
@@ -31,12 +29,11 @@ fn bench_reuse_send(b: &mut test::Bencher) {
|
||||
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
|
||||
.build();
|
||||
b.iter(|| {
|
||||
let email = SimpleSendableEmail::new(
|
||||
EmailAddress::new("user@localhost".to_string()),
|
||||
let email =
|
||||
SimpleSendableEmail::new(EmailAddress::new("user@localhost".to_string()),
|
||||
vec![EmailAddress::new("root@localhost".to_string())],
|
||||
"id".to_string(),
|
||||
"Hello world".to_string(),
|
||||
);
|
||||
"Hello world".to_string());
|
||||
let result = sender.send(&email);
|
||||
assert!(result.is_ok());
|
||||
});
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
extern crate lettre;
|
||||
extern crate env_logger;
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::{EmailAddress, EmailTransport, SimpleSendableEmail, SmtpTransport};
|
||||
|
||||
fn main() {
|
||||
env_logger::init().unwrap();
|
||||
|
||||
let email = SimpleSendableEmail::new(
|
||||
EmailAddress::new("user@localhost".to_string()),
|
||||
let email = SimpleSendableEmail::new(EmailAddress::new("user@localhost".to_string()),
|
||||
vec![EmailAddress::new("root@localhost".to_string())],
|
||||
"file_id".to_string(),
|
||||
"Hello ß☺ example".to_string(),
|
||||
);
|
||||
"Hello ß☺ example".to_string());
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mut mailer = SmtpTransport::builder_unencrypted_localhost()
|
||||
.unwrap()
|
||||
let mut mailer = SmtpTransport::builder_unencrypted_localhost().unwrap()
|
||||
.build();
|
||||
// Send the email
|
||||
let result = mailer.send(&email);
|
||||
|
||||
@@ -70,16 +70,12 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, FileResult> for FileEmailTransport
|
||||
let mut message_content = String::new();
|
||||
let _ = email.message().read_to_string(&mut message_content);
|
||||
|
||||
let simple_email = SimpleSendableEmail::new(
|
||||
email.from().clone(),
|
||||
let simple_email = SimpleSendableEmail::new(email.from().clone(),
|
||||
email.to().clone(),
|
||||
email.message_id().clone(),
|
||||
message_content,
|
||||
);
|
||||
message_content);
|
||||
|
||||
f.write_all(
|
||||
serde_json::to_string(&simple_email)?.as_bytes(),
|
||||
)?;
|
||||
f.write_all(serde_json::to_string(&simple_email)?.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -7,28 +7,28 @@
|
||||
#![doc(html_root_url = "https://docs.rs/lettre/0.7.0")]
|
||||
#![deny(missing_docs, unsafe_code, unstable_features, warnings)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[cfg(feature = "crammd5-auth")]
|
||||
extern crate hex;
|
||||
#[cfg(feature = "crammd5-auth")]
|
||||
extern crate crypto;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
extern crate base64;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
extern crate bufstream;
|
||||
#[cfg(feature = "crammd5-auth")]
|
||||
extern crate crypto;
|
||||
#[cfg(feature = "crammd5-auth")]
|
||||
extern crate hex;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
extern crate hostname;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
extern crate native_tls;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
extern crate hostname;
|
||||
#[cfg(feature = "file-transport")]
|
||||
extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate nom;
|
||||
#[cfg(feature = "serde-impls")]
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
#[macro_use]
|
||||
extern crate nom;
|
||||
#[cfg(feature = "file-transport")]
|
||||
extern crate serde_json;
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub mod smtp;
|
||||
@@ -101,18 +101,15 @@ pub struct SimpleSendableEmail {
|
||||
|
||||
impl SimpleSendableEmail {
|
||||
/// Returns a new email
|
||||
pub fn new(
|
||||
from_address: EmailAddress,
|
||||
pub fn new(from_address: EmailAddress,
|
||||
to_addresses: Vec<EmailAddress>,
|
||||
message_id: String,
|
||||
message: String,
|
||||
) -> SimpleSendableEmail {
|
||||
SimpleSendableEmail {
|
||||
from: from_address,
|
||||
message: String)
|
||||
-> SimpleSendableEmail {
|
||||
SimpleSendableEmail { from: from_address,
|
||||
to: to_addresses,
|
||||
message_id: message_id,
|
||||
message: message.into_bytes(),
|
||||
}
|
||||
message: message.into_bytes(), }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,12 +33,12 @@ pub struct SendmailTransport {
|
||||
impl SendmailTransport {
|
||||
/// Creates a new transport with the default `/usr/sbin/sendmail` command
|
||||
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
|
||||
pub fn new_with_command<S: Into<String>>(command: S) -> SendmailTransport {
|
||||
SendmailTransport { command: command.into() }
|
||||
SendmailTransport { command: command.into(), }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,15 +46,10 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SendmailResult> for SendmailTranspo
|
||||
fn send<U: SendableEmail<'a, T> + 'a>(&mut self, email: &'a U) -> SendmailResult {
|
||||
// Spawn the sendmail command
|
||||
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).args(&["-i",
|
||||
"-f",
|
||||
&email.from().to_string(),
|
||||
&to_addresses.join(" "),
|
||||
],
|
||||
)
|
||||
&to_addresses.join(" ")])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?;
|
||||
@@ -62,9 +57,11 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SendmailResult> for SendmailTranspo
|
||||
let mut message_content = String::new();
|
||||
let _ = email.message().read_to_string(&mut message_content);
|
||||
|
||||
match process.stdin.as_mut().unwrap().write_all(
|
||||
message_content.as_bytes(),
|
||||
) {
|
||||
match process.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(message_content.as_bytes())
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(error) => return Err(From::from(error)),
|
||||
}
|
||||
|
||||
@@ -15,21 +15,20 @@ use std::fmt::{self, Display, Formatter};
|
||||
/// Accepted authentication mecanisms on an encrypted connection
|
||||
/// Trying LOGIN last as it is deprecated.
|
||||
#[cfg(feature = "crammd5-auth")]
|
||||
pub const DEFAULT_ENCRYPTED_MECHANISMS: &'static [Mechanism] =
|
||||
pub const DEFAULT_ENCRYPTED_MECHANISMS: &[Mechanism] =
|
||||
&[Mechanism::Plain, Mechanism::CramMd5, Mechanism::Login];
|
||||
/// Accepted authentication mecanisms on an encrypted connection
|
||||
/// Trying LOGIN last as it is deprecated.
|
||||
#[cfg(not(feature = "crammd5-auth"))]
|
||||
pub const DEFAULT_ENCRYPTED_MECHANISMS: &'static [Mechanism] =
|
||||
&[Mechanism::Plain, Mechanism::Login];
|
||||
pub const DEFAULT_ENCRYPTED_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
|
||||
|
||||
/// Accepted authentication mecanisms on an unencrypted connection
|
||||
#[cfg(feature = "crammd5-auth")]
|
||||
pub const DEFAULT_UNENCRYPTED_MECHANISMS: &'static [Mechanism] = &[Mechanism::CramMd5];
|
||||
pub const DEFAULT_UNENCRYPTED_MECHANISMS: &[Mechanism] = &[Mechanism::CramMd5];
|
||||
/// Accepted authentication mecanisms on an unencrypted connection
|
||||
/// When CRAMMD5 support is not enabled, no mechanisms are allowed.
|
||||
#[cfg(not(feature = "crammd5-auth"))]
|
||||
pub const DEFAULT_UNENCRYPTED_MECHANISMS: &'static [Mechanism] = &[];
|
||||
pub const DEFAULT_UNENCRYPTED_MECHANISMS: &[Mechanism] = &[];
|
||||
|
||||
|
||||
/// Convertable to user credentials
|
||||
@@ -61,10 +60,8 @@ pub struct Credentials {
|
||||
impl Credentials {
|
||||
/// Create a `Credentials` struct from username and password
|
||||
pub fn new(username: String, password: String) -> Credentials {
|
||||
Credentials {
|
||||
username: username,
|
||||
password: password,
|
||||
}
|
||||
Credentials { username: username,
|
||||
password: password, }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,16 +83,12 @@ pub enum Mechanism {
|
||||
|
||||
impl Display for Mechanism {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match *self {
|
||||
write!(f, "{}", match *self {
|
||||
Mechanism::Plain => "PLAIN",
|
||||
Mechanism::Login => "LOGIN",
|
||||
#[cfg(feature = "crammd5-auth")]
|
||||
Mechanism::CramMd5 => "CRAM-MD5",
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,22 +106,21 @@ impl Mechanism {
|
||||
|
||||
/// Returns the string to send to the server, using the provided username, password and
|
||||
/// challenge in some cases
|
||||
pub fn response(
|
||||
&self,
|
||||
pub fn response(&self,
|
||||
credentials: &Credentials,
|
||||
challenge: Option<&str>,
|
||||
) -> Result<String, Error> {
|
||||
challenge: Option<&str>)
|
||||
-> Result<String, Error> {
|
||||
match *self {
|
||||
Mechanism::Plain => {
|
||||
match challenge {
|
||||
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
|
||||
None => Ok(format!(
|
||||
"{}{}{}{}",
|
||||
None => {
|
||||
Ok(format!("{}{}{}{}",
|
||||
NUL,
|
||||
credentials.username,
|
||||
NUL,
|
||||
credentials.password
|
||||
)),
|
||||
credentials.password))
|
||||
}
|
||||
}
|
||||
}
|
||||
Mechanism::Login => {
|
||||
@@ -157,11 +149,9 @@ impl Mechanism {
|
||||
let mut hmac = Hmac::new(Md5::new(), credentials.password.as_bytes());
|
||||
hmac.input(decoded_challenge.as_bytes());
|
||||
|
||||
Ok(format!(
|
||||
"{} {}",
|
||||
Ok(format!("{} {}",
|
||||
credentials.username,
|
||||
hex::encode(hmac.result().code())
|
||||
))
|
||||
hex::encode(hmac.result().code())))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,10 +167,8 @@ mod test {
|
||||
|
||||
let credentials = Credentials::new("username".to_string(), "password".to_string());
|
||||
|
||||
assert_eq!(
|
||||
mechanism.response(&credentials, None).unwrap(),
|
||||
"\u{0}username\u{0}password"
|
||||
);
|
||||
assert_eq!(mechanism.response(&credentials, None).unwrap(),
|
||||
"\u{0}username\u{0}password");
|
||||
assert!(mechanism.response(&credentials, Some("test")).is_err());
|
||||
}
|
||||
|
||||
@@ -190,14 +178,10 @@ mod test {
|
||||
|
||||
let credentials = Credentials::new("alice".to_string(), "wonderland".to_string());
|
||||
|
||||
assert_eq!(
|
||||
mechanism.response(&credentials, Some("Username")).unwrap(),
|
||||
"alice"
|
||||
);
|
||||
assert_eq!(
|
||||
mechanism.response(&credentials, Some("Password")).unwrap(),
|
||||
"wonderland"
|
||||
);
|
||||
assert_eq!(mechanism.response(&credentials, Some("Username")).unwrap(),
|
||||
"alice");
|
||||
assert_eq!(mechanism.response(&credentials, Some("Password")).unwrap(),
|
||||
"wonderland");
|
||||
assert!(mechanism.response(&credentials, None).is_err());
|
||||
}
|
||||
|
||||
@@ -212,7 +196,7 @@ mod test {
|
||||
mechanism
|
||||
.response(
|
||||
&credentials,
|
||||
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=="),
|
||||
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==")
|
||||
)
|
||||
.unwrap(),
|
||||
"alice a540ebe4ef2304070bbc3c456c1f64c0"
|
||||
|
||||
@@ -20,17 +20,13 @@ impl Default for MockStream {
|
||||
|
||||
impl MockStream {
|
||||
pub fn new() -> MockStream {
|
||||
MockStream {
|
||||
reader: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
|
||||
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
|
||||
}
|
||||
MockStream { 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 {
|
||||
MockStream {
|
||||
reader: Arc::new(Mutex::new(MockCursor::new(vec))),
|
||||
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
|
||||
}
|
||||
MockStream { 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> {
|
||||
|
||||
@@ -64,7 +64,6 @@ impl ClientCodec {
|
||||
}
|
||||
Ok(buf.write_all(&frame[start..])?)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,7 +101,7 @@ impl<S: Write + Read> Client<S> {
|
||||
impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
|
||||
/// Closes the SMTP transaction if possible
|
||||
pub fn close(&mut self) {
|
||||
let _ = self.smtp_command(QuitCommand);
|
||||
let _ = self.command(QuitCommand);
|
||||
self.stream = None;
|
||||
}
|
||||
|
||||
@@ -140,11 +139,10 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
|
||||
}
|
||||
|
||||
/// Connects to the configured server
|
||||
pub fn connect<A: ToSocketAddrs>(
|
||||
&mut self,
|
||||
pub fn connect<A: ToSocketAddrs>(&mut self,
|
||||
addr: &A,
|
||||
tls_parameters: Option<&ClientTlsParameters>,
|
||||
) -> SmtpResult {
|
||||
tls_parameters: Option<&ClientTlsParameters>)
|
||||
-> SmtpResult {
|
||||
// Connect should not be called when the client is already connected
|
||||
if self.stream.is_some() {
|
||||
return_err!("The connection is already established", self);
|
||||
@@ -162,30 +160,26 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
|
||||
// Try to connect
|
||||
self.set_stream(Connector::connect(&server_addr, tls_parameters)?);
|
||||
|
||||
self.get_reply()
|
||||
self.read_response()
|
||||
}
|
||||
|
||||
/// Checks if the server is connected using the NOOP SMTP command
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(wrong_self_convention))]
|
||||
pub fn is_connected(&mut self) -> bool {
|
||||
self.smtp_command(NoopCommand).is_ok()
|
||||
self.command(NoopCommand).is_ok()
|
||||
}
|
||||
|
||||
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
|
||||
pub fn auth(&mut self, mechanism: Mechanism, credentials: &Credentials) -> SmtpResult {
|
||||
// TODO
|
||||
let mut challenges = 10;
|
||||
let mut response = self.smtp_command(
|
||||
AuthCommand::new(mechanism, credentials.clone(), None)?,
|
||||
)?;
|
||||
let mut response = self.command(AuthCommand::new(mechanism, credentials.clone(), None)?)?;
|
||||
|
||||
while challenges > 0 && response.has_code(334) {
|
||||
challenges -= 1;
|
||||
response = self.smtp_command(AuthCommand::new_from_response(
|
||||
mechanism,
|
||||
response = self.command(AuthCommand::new_from_response(mechanism,
|
||||
credentials.clone(),
|
||||
&response,
|
||||
)?)?;
|
||||
&response)?)?;
|
||||
}
|
||||
|
||||
if challenges == 0 {
|
||||
@@ -217,21 +211,21 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
|
||||
break;
|
||||
}
|
||||
|
||||
self.write_server(out_buf.as_slice())?;
|
||||
self.write(out_buf.as_slice())?;
|
||||
}
|
||||
|
||||
self.write_server(MESSAGE_ENDING.as_bytes())?;
|
||||
self.get_reply()
|
||||
self.write(MESSAGE_ENDING.as_bytes())?;
|
||||
self.read_response()
|
||||
}
|
||||
|
||||
/// Sends an SMTP command
|
||||
pub fn smtp_command<C: Display>(&mut self, command: C) -> SmtpResult {
|
||||
self.write_server(command.to_string().as_bytes())?;
|
||||
self.get_reply()
|
||||
pub fn command<C: Display>(&mut self, command: C) -> SmtpResult {
|
||||
self.write(command.to_string().as_bytes())?;
|
||||
self.read_response()
|
||||
}
|
||||
|
||||
/// Writes a string to the server
|
||||
fn write_server(&mut self, string: &[u8]) -> Result<(), Error> {
|
||||
fn write(&mut self, string: &[u8]) -> Result<(), Error> {
|
||||
if self.stream.is_none() {
|
||||
return Err(From::from("Connection closed"));
|
||||
}
|
||||
@@ -239,16 +233,13 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
|
||||
self.stream.as_mut().unwrap().write_all(string)?;
|
||||
self.stream.as_mut().unwrap().flush()?;
|
||||
|
||||
debug!(
|
||||
"Wrote: {}",
|
||||
escape_crlf(String::from_utf8_lossy(string).as_ref())
|
||||
);
|
||||
debug!("Wrote: {}",
|
||||
escape_crlf(String::from_utf8_lossy(string).as_ref()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the SMTP response
|
||||
fn get_reply(&mut self) -> SmtpResult {
|
||||
|
||||
fn read_response(&mut self) -> SmtpResult {
|
||||
let mut raw_response = String::new();
|
||||
let mut response = raw_response.parse::<Response>();
|
||||
|
||||
@@ -275,7 +266,7 @@ impl<S: Connector + Write + Read + Timeout + Debug> Client<S> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{ClientCodec, escape_crlf};
|
||||
use super::{escape_crlf, ClientCodec};
|
||||
|
||||
#[test]
|
||||
fn test_codec() {
|
||||
@@ -291,19 +282,15 @@ 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", &mut buf).is_ok());
|
||||
assert_eq!(
|
||||
String::from_utf8(buf).unwrap(),
|
||||
"test\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest"
|
||||
);
|
||||
assert_eq!(String::from_utf8(buf).unwrap(),
|
||||
"test\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_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\nSIZE 42\r\n"),
|
||||
"EHLO my_name<CRLF>SIZE 42<CRLF>"
|
||||
);
|
||||
assert_eq!(escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
|
||||
"EHLO my_name<CRLF>SIZE 42<CRLF>");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,16 +18,14 @@ pub struct ClientTlsParameters {
|
||||
impl ClientTlsParameters {
|
||||
/// Creates a `ClientTlsParameters`
|
||||
pub fn new(domain: String, connector: TlsConnector) -> ClientTlsParameters {
|
||||
ClientTlsParameters {
|
||||
connector: connector,
|
||||
domain: domain,
|
||||
}
|
||||
ClientTlsParameters { connector: connector,
|
||||
domain: domain, }
|
||||
}
|
||||
}
|
||||
|
||||
/// Accepted protocols by default.
|
||||
/// This removes TLS 1.0 compared to tls-native defaults.
|
||||
pub const DEFAULT_TLS_PROTOCOLS: &'static [Protocol] = &[Protocol::Tlsv11, Protocol::Tlsv12];
|
||||
pub const DEFAULT_TLS_PROTOCOLS: &[Protocol] = &[Protocol::Tlsv11, Protocol::Tlsv12];
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Represents the different types of underlying network streams
|
||||
@@ -47,9 +45,7 @@ impl NetworkStream {
|
||||
NetworkStream::Tcp(ref s) => s.peer_addr(),
|
||||
NetworkStream::Tls(ref s) => s.get_ref().peer_addr(),
|
||||
NetworkStream::Mock(_) => {
|
||||
Ok(SocketAddr::V4(
|
||||
SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 80),
|
||||
))
|
||||
Ok(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 80)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,16 +100,14 @@ pub trait Connector: Sized {
|
||||
}
|
||||
|
||||
impl Connector for NetworkStream {
|
||||
fn connect(
|
||||
addr: &SocketAddr,
|
||||
tls_parameters: Option<&ClientTlsParameters>,
|
||||
) -> io::Result<NetworkStream> {
|
||||
fn connect(addr: &SocketAddr,
|
||||
tls_parameters: Option<&ClientTlsParameters>)
|
||||
-> io::Result<NetworkStream> {
|
||||
let tcp_stream = TcpStream::connect(addr)?;
|
||||
|
||||
match tls_parameters {
|
||||
Some(context) => {
|
||||
context
|
||||
.connector
|
||||
context.connector
|
||||
.connect(context.domain.as_ref(), tcp_stream)
|
||||
.map(NetworkStream::Tls)
|
||||
.map_err(|e| io::Error::new(ErrorKind::Other, e))
|
||||
@@ -126,10 +120,10 @@ impl Connector for NetworkStream {
|
||||
fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()> {
|
||||
*self = match *self {
|
||||
NetworkStream::Tcp(ref mut stream) => {
|
||||
match tls_parameters.connector.connect(
|
||||
tls_parameters.domain.as_ref(),
|
||||
stream.try_clone().unwrap(),
|
||||
) {
|
||||
match tls_parameters.connector
|
||||
.connect(tls_parameters.domain.as_ref(),
|
||||
stream.try_clone().unwrap())
|
||||
{
|
||||
Ok(tls_stream) => NetworkStream::Tls(tls_stream),
|
||||
Err(err) => return Err(io::Error::new(ErrorKind::Other, err)),
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ impl Display for EhloCommand {
|
||||
impl EhloCommand {
|
||||
/// Creates a EHLO command
|
||||
pub fn new(client_id: ClientId) -> EhloCommand {
|
||||
EhloCommand { client_id: client_id }
|
||||
EhloCommand { client_id: client_id, }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,14 +50,10 @@ pub struct MailCommand {
|
||||
|
||||
impl Display for MailCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"MAIL FROM:<{}>",
|
||||
match self.sender {
|
||||
write!(f, "MAIL FROM:<{}>", match self.sender {
|
||||
Some(ref address) => address.to_string(),
|
||||
None => "".to_string(),
|
||||
}
|
||||
)?;
|
||||
})?;
|
||||
for parameter in &self.parameters {
|
||||
write!(f, " {}", parameter)?;
|
||||
}
|
||||
@@ -68,10 +64,8 @@ impl Display for MailCommand {
|
||||
impl MailCommand {
|
||||
/// Creates a MAIL command
|
||||
pub fn new(sender: Option<EmailAddress>, parameters: Vec<MailParameter>) -> MailCommand {
|
||||
MailCommand {
|
||||
sender: sender,
|
||||
parameters: parameters,
|
||||
}
|
||||
MailCommand { sender: sender,
|
||||
parameters: parameters, }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,10 +89,8 @@ impl Display for RcptCommand {
|
||||
impl RcptCommand {
|
||||
/// Creates an RCPT command
|
||||
pub fn new(recipient: EmailAddress, parameters: Vec<RcptParameter>) -> RcptCommand {
|
||||
RcptCommand {
|
||||
recipient: recipient,
|
||||
parameters: parameters,
|
||||
}
|
||||
RcptCommand { recipient: recipient,
|
||||
parameters: parameters, }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,10 +213,8 @@ pub struct AuthCommand {
|
||||
impl Display for AuthCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
let encoded_response = if self.response.is_some() {
|
||||
Some(base64::encode_config(
|
||||
self.response.as_ref().unwrap().as_bytes(),
|
||||
base64::STANDARD,
|
||||
))
|
||||
Some(base64::encode_config(self.response.as_ref().unwrap().as_bytes(),
|
||||
base64::STANDARD))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -243,34 +233,27 @@ impl Display for AuthCommand {
|
||||
|
||||
impl AuthCommand {
|
||||
/// Creates an AUTH command (from a challenge if provided)
|
||||
pub fn new(
|
||||
mechanism: Mechanism,
|
||||
pub fn new(mechanism: Mechanism,
|
||||
credentials: Credentials,
|
||||
challenge: Option<String>,
|
||||
) -> Result<AuthCommand, Error> {
|
||||
challenge: Option<String>)
|
||||
-> Result<AuthCommand, Error> {
|
||||
let response = if mechanism.supports_initial_response() || challenge.is_some() {
|
||||
Some(mechanism.response(
|
||||
&credentials,
|
||||
challenge.as_ref().map(String::as_str),
|
||||
)?)
|
||||
Some(mechanism.response(&credentials, challenge.as_ref().map(String::as_str))?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(AuthCommand {
|
||||
mechanism: mechanism,
|
||||
Ok(AuthCommand { mechanism: mechanism,
|
||||
credentials: credentials,
|
||||
challenge: challenge,
|
||||
response: response,
|
||||
})
|
||||
response: response, })
|
||||
}
|
||||
|
||||
/// Creates an AUTH command from a response that needs to be a
|
||||
/// valid challenge (with 334 response code)
|
||||
pub fn new_from_response(
|
||||
mechanism: Mechanism,
|
||||
pub fn new_from_response(mechanism: Mechanism,
|
||||
credentials: Credentials,
|
||||
response: &Response,
|
||||
) -> Result<AuthCommand, Error> {
|
||||
response: &Response)
|
||||
-> Result<AuthCommand, Error> {
|
||||
if !response.has_code(334) {
|
||||
return Err(Error::ResponseParsing("Expecting a challenge"));
|
||||
}
|
||||
@@ -294,17 +277,12 @@ impl AuthCommand {
|
||||
|
||||
debug!("auth decoded challenge: {}", decoded_challenge);
|
||||
|
||||
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 { mechanism: mechanism,
|
||||
credentials: credentials,
|
||||
challenge: Some(decoded_challenge),
|
||||
response: response,
|
||||
})
|
||||
response: response, })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,77 +297,49 @@ mod test {
|
||||
fn test_display() {
|
||||
let id = ClientId::Domain("localhost".to_string());
|
||||
let email = EmailAddress::new("test@example.com".to_string());
|
||||
let mail_parameter = MailParameter::Other {
|
||||
keyword: "TEST".to_string(),
|
||||
value: Some("value".to_string()),
|
||||
};
|
||||
let rcpt_parameter = RcptParameter::Other {
|
||||
keyword: "TEST".to_string(),
|
||||
value: Some("value".to_string()),
|
||||
};
|
||||
let mail_parameter = MailParameter::Other { keyword: "TEST".to_string(),
|
||||
value: Some("value".to_string()), };
|
||||
let rcpt_parameter = RcptParameter::Other { keyword: "TEST".to_string(),
|
||||
value: Some("value".to_string()), };
|
||||
assert_eq!(format!("{}", EhloCommand::new(id)), "EHLO localhost\r\n");
|
||||
assert_eq!(
|
||||
format!("{}", MailCommand::new(Some(email.clone()), vec![])),
|
||||
"MAIL FROM:<test@example.com>\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", MailCommand::new(None, vec![])),
|
||||
"MAIL FROM:<>\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
MailCommand::new(Some(email.clone()), vec![MailParameter::Size(42)])
|
||||
),
|
||||
"MAIL FROM:<test@example.com> SIZE=42\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}", MailCommand::new(Some(email.clone()), vec![])),
|
||||
"MAIL FROM:<test@example.com>\r\n");
|
||||
assert_eq!(format!("{}", MailCommand::new(None, vec![])),
|
||||
"MAIL FROM:<>\r\n");
|
||||
assert_eq!(format!("{}",
|
||||
MailCommand::new(Some(email.clone()), vec![MailParameter::Size(42)])),
|
||||
"MAIL FROM:<test@example.com> SIZE=42\r\n");
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
MailCommand::new(
|
||||
Some(email.clone()),
|
||||
vec![
|
||||
MailParameter::Size(42),
|
||||
vec![MailParameter::Size(42),
|
||||
MailParameter::Body(MailBodyParameter::EightBitMime),
|
||||
mail_parameter,
|
||||
],
|
||||
mail_parameter],
|
||||
)
|
||||
),
|
||||
"MAIL FROM:<test@example.com> SIZE=42 BODY=8BITMIME TEST=value\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", RcptCommand::new(email.clone(), vec![])),
|
||||
"RCPT TO:<test@example.com>\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", RcptCommand::new(email.clone(), vec![rcpt_parameter])),
|
||||
"RCPT TO:<test@example.com> TEST=value\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}", RcptCommand::new(email.clone(), vec![])),
|
||||
"RCPT TO:<test@example.com>\r\n");
|
||||
assert_eq!(format!("{}", RcptCommand::new(email.clone(), vec![rcpt_parameter])),
|
||||
"RCPT TO:<test@example.com> TEST=value\r\n");
|
||||
assert_eq!(format!("{}", QuitCommand), "QUIT\r\n");
|
||||
assert_eq!(format!("{}", DataCommand), "DATA\r\n");
|
||||
assert_eq!(format!("{}", NoopCommand), "NOOP\r\n");
|
||||
assert_eq!(format!("{}", HelpCommand::new(None)), "HELP\r\n");
|
||||
assert_eq!(
|
||||
format!("{}", HelpCommand::new(Some("test".to_string()))),
|
||||
"HELP test\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", VrfyCommand::new("test".to_string())),
|
||||
"VRFY test\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", ExpnCommand::new("test".to_string())),
|
||||
"EXPN test\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}", HelpCommand::new(Some("test".to_string()))),
|
||||
"HELP test\r\n");
|
||||
assert_eq!(format!("{}", VrfyCommand::new("test".to_string())),
|
||||
"VRFY test\r\n");
|
||||
assert_eq!(format!("{}", ExpnCommand::new("test".to_string())),
|
||||
"EXPN test\r\n");
|
||||
assert_eq!(format!("{}", RsetCommand), "RSET\r\n");
|
||||
let credentials = Credentials::new("user".to_string(), "password".to_string());
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
AuthCommand::new(Mechanism::Plain, credentials.clone(), None).unwrap()
|
||||
),
|
||||
"AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}",
|
||||
AuthCommand::new(Mechanism::Plain, credentials.clone(), None).unwrap()),
|
||||
"AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n");
|
||||
#[cfg(feature = "crammd5-auth")]
|
||||
assert_eq!(
|
||||
format!(
|
||||
@@ -402,13 +352,9 @@ mod test {
|
||||
),
|
||||
"dXNlciAzMTYxY2NmZDdmMjNlMzJiYmMzZTQ4NjdmYzk0YjE4Nw==\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
AuthCommand::new(Mechanism::Login, credentials.clone(), None).unwrap()
|
||||
),
|
||||
"AUTH LOGIN\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}",
|
||||
AuthCommand::new(Mechanism::Login, credentials.clone(), None).unwrap()),
|
||||
"AUTH LOGIN\r\n");
|
||||
#[cfg(feature = "crammd5-auth")]
|
||||
assert_eq!(
|
||||
format!(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! ESMTP features
|
||||
|
||||
use hostname::get_hostname;
|
||||
use smtp::authentication::Mechanism;
|
||||
use smtp::error::Error;
|
||||
use smtp::response::Response;
|
||||
@@ -9,6 +10,9 @@ use std::fmt::{self, Display, Formatter};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::result::Result;
|
||||
|
||||
/// Default ehlo clinet id
|
||||
pub const DEFAULT_EHLO_HOSTNAME: &str = "localhost";
|
||||
|
||||
/// Client identifier, the parameter to `EHLO`
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub enum ClientId {
|
||||
@@ -35,6 +39,15 @@ impl ClientId {
|
||||
pub fn new(domain: String) -> ClientId {
|
||||
ClientId::Domain(domain)
|
||||
}
|
||||
|
||||
/// Defines a `ClientId` with the current hostname, of `localhost` if hostname could not be
|
||||
/// found
|
||||
pub fn hostname() -> ClientId {
|
||||
ClientId::Domain(match get_hostname() {
|
||||
Some(name) => name,
|
||||
None => DEFAULT_EHLO_HOSTNAME.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Supported ESMTP keywords
|
||||
@@ -82,16 +95,14 @@ pub struct ServerInfo {
|
||||
|
||||
impl Display for ServerInfo {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
write!(f,
|
||||
"{} with {}",
|
||||
self.name,
|
||||
if self.features.is_empty() {
|
||||
"no supported features".to_string()
|
||||
} else {
|
||||
format!("{:?}", self.features)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,10 +153,8 @@ impl ServerInfo {
|
||||
};
|
||||
}
|
||||
|
||||
Ok(ServerInfo {
|
||||
name: name.to_string(),
|
||||
features: features,
|
||||
})
|
||||
Ok(ServerInfo { name: name.to_string(),
|
||||
features: features, })
|
||||
}
|
||||
|
||||
/// Checks if the server supports an ESMTP feature
|
||||
@@ -155,9 +164,7 @@ impl ServerInfo {
|
||||
|
||||
/// Checks if the server supports an ESMTP feature
|
||||
pub fn supports_auth_mechanism(&self, mechanism: Mechanism) -> bool {
|
||||
self.features.contains(
|
||||
&Extension::Authentication(mechanism),
|
||||
)
|
||||
self.features.contains(&Extension::Authentication(mechanism))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,14 +192,12 @@ impl Display for MailParameter {
|
||||
MailParameter::Body(ref value) => write!(f, "BODY={}", value),
|
||||
MailParameter::Size(size) => write!(f, "SIZE={}", size),
|
||||
MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
||||
MailParameter::Other {
|
||||
ref keyword,
|
||||
value: Some(ref value),
|
||||
} => write!(f, "{}={}", keyword, XText(value)),
|
||||
MailParameter::Other {
|
||||
ref keyword,
|
||||
value: None,
|
||||
} => f.write_str(keyword),
|
||||
MailParameter::Other { ref keyword,
|
||||
value: Some(ref value), } => {
|
||||
write!(f, "{}={}", keyword, XText(value))
|
||||
}
|
||||
MailParameter::Other { ref keyword,
|
||||
value: None, } => f.write_str(keyword),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,14 +235,12 @@ pub enum RcptParameter {
|
||||
impl Display for RcptParameter {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
RcptParameter::Other {
|
||||
ref keyword,
|
||||
value: Some(ref value),
|
||||
} => write!(f, "{}={}", keyword, XText(value)),
|
||||
RcptParameter::Other {
|
||||
ref keyword,
|
||||
value: None,
|
||||
} => f.write_str(keyword),
|
||||
RcptParameter::Other { ref keyword,
|
||||
value: Some(ref value), } => {
|
||||
write!(f, "{}={}", keyword, XText(value))
|
||||
}
|
||||
RcptParameter::Other { ref keyword,
|
||||
value: None, } => f.write_str(keyword),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,14 +255,10 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_extension_fmt() {
|
||||
assert_eq!(
|
||||
format!("{}", Extension::EightBitMime),
|
||||
"8BITMIME".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Extension::Authentication(Mechanism::Plain)),
|
||||
"AUTH PLAIN".to_string()
|
||||
);
|
||||
assert_eq!(format!("{}", Extension::EightBitMime),
|
||||
"8BITMIME".to_string());
|
||||
assert_eq!(format!("{}", Extension::Authentication(Mechanism::Plain)),
|
||||
"AUTH PLAIN".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -267,67 +266,41 @@ mod test {
|
||||
let mut eightbitmime = HashSet::new();
|
||||
assert!(eightbitmime.insert(Extension::EightBitMime));
|
||||
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
features: eightbitmime.clone(),
|
||||
}
|
||||
),
|
||||
"name with {EightBitMime}".to_string()
|
||||
);
|
||||
assert_eq!(format!("{}",
|
||||
ServerInfo { name: "name".to_string(),
|
||||
features: eightbitmime.clone(), }),
|
||||
"name with {EightBitMime}".to_string());
|
||||
|
||||
let empty = HashSet::new();
|
||||
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
features: empty,
|
||||
}
|
||||
),
|
||||
"name with no supported features".to_string()
|
||||
);
|
||||
assert_eq!(format!("{}",
|
||||
ServerInfo { name: "name".to_string(),
|
||||
features: empty, }),
|
||||
"name with no supported features".to_string());
|
||||
|
||||
let mut plain = HashSet::new();
|
||||
assert!(plain.insert(Extension::Authentication(Mechanism::Plain)));
|
||||
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
features: plain.clone(),
|
||||
}
|
||||
),
|
||||
"name with {Authentication(Plain)}".to_string()
|
||||
);
|
||||
assert_eq!(format!("{}",
|
||||
ServerInfo { name: "name".to_string(),
|
||||
features: plain.clone(), }),
|
||||
"name with {Authentication(Plain)}".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serverinfo() {
|
||||
let response = Response::new(
|
||||
Code::new(
|
||||
Severity::PositiveCompletion,
|
||||
let response = Response::new(Code::new(Severity::PositiveCompletion,
|
||||
Category::Unspecified4,
|
||||
Detail(1),
|
||||
),
|
||||
vec![
|
||||
"me".to_string(),
|
||||
Detail(1)),
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
);
|
||||
"SIZE 42".to_string()]);
|
||||
|
||||
let mut features = HashSet::new();
|
||||
assert!(features.insert(Extension::EightBitMime));
|
||||
|
||||
let server_info = ServerInfo {
|
||||
name: "me".to_string(),
|
||||
features: features,
|
||||
};
|
||||
let server_info = ServerInfo { name: "me".to_string(),
|
||||
features: features, };
|
||||
|
||||
assert_eq!(ServerInfo::from_response(&response).unwrap(), server_info);
|
||||
|
||||
@@ -336,34 +309,22 @@ mod test {
|
||||
#[cfg(feature = "crammd5-auth")]
|
||||
assert!(!server_info.supports_auth_mechanism(Mechanism::CramMd5));
|
||||
|
||||
let response2 = Response::new(
|
||||
Code::new(
|
||||
Severity::PositiveCompletion,
|
||||
let response2 = Response::new(Code::new(Severity::PositiveCompletion,
|
||||
Category::Unspecified4,
|
||||
Detail(1),
|
||||
),
|
||||
vec![
|
||||
"me".to_string(),
|
||||
Detail(1)),
|
||||
vec!["me".to_string(),
|
||||
"AUTH PLAIN CRAM-MD5 OTHER".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
);
|
||||
"SIZE 42".to_string()]);
|
||||
|
||||
let mut features2 = HashSet::new();
|
||||
assert!(features2.insert(Extension::EightBitMime));
|
||||
assert!(features2.insert(
|
||||
Extension::Authentication(Mechanism::Plain),
|
||||
));
|
||||
assert!(features2.insert(Extension::Authentication(Mechanism::Plain),));
|
||||
#[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(),
|
||||
features: features2,
|
||||
};
|
||||
let server_info2 = ServerInfo { name: "me".to_string(),
|
||||
features: features2, };
|
||||
|
||||
assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2);
|
||||
|
||||
|
||||
@@ -91,24 +91,23 @@
|
||||
//!
|
||||
//! let mut email_client: Client<NetworkStream> = Client::new();
|
||||
//! let _ = email_client.connect(&("localhost", SMTP_PORT), None);
|
||||
//! let _ = email_client.smtp_command(EhloCommand::new(ClientId::new("my_hostname".to_string())));
|
||||
//! let _ = email_client.smtp_command(
|
||||
//! let _ = email_client.command(EhloCommand::new(ClientId::new("my_hostname".to_string())));
|
||||
//! let _ = email_client.command(
|
||||
//! MailCommand::new(Some(EmailAddress::new("user@example.com".to_string())), vec![])
|
||||
//! );
|
||||
//! let _ = email_client.smtp_command(
|
||||
//! let _ = email_client.command(
|
||||
//! RcptCommand::new(EmailAddress::new("user@example.org".to_string()), vec![])
|
||||
//! );
|
||||
//! let _ = email_client.smtp_command(DataCommand);
|
||||
//! let _ = email_client.command(DataCommand);
|
||||
//! let _ = email_client.message(Box::new("Test email".as_bytes()));
|
||||
//! let _ = email_client.smtp_command(QuitCommand);
|
||||
//! let _ = email_client.command(QuitCommand);
|
||||
//! ```
|
||||
|
||||
use EmailTransport;
|
||||
use SendableEmail;
|
||||
use native_tls::TlsConnector;
|
||||
use hostname::get_hostname;
|
||||
use smtp::authentication::{Credentials, DEFAULT_ENCRYPTED_MECHANISMS,
|
||||
DEFAULT_UNENCRYPTED_MECHANISMS, Mechanism};
|
||||
use smtp::authentication::{Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS,
|
||||
DEFAULT_UNENCRYPTED_MECHANISMS};
|
||||
use smtp::client::Client;
|
||||
use smtp::client::net::ClientTlsParameters;
|
||||
use smtp::client::net::DEFAULT_TLS_PROTOCOLS;
|
||||
@@ -207,28 +206,24 @@ impl SmtpTransportBuilder {
|
||||
/// Defaults are:
|
||||
///
|
||||
/// * No connection reuse
|
||||
/// * "localhost" as EHLO name
|
||||
/// * No authentication
|
||||
/// * No SMTPUTF8 support
|
||||
/// * A 60 seconds timeout for smtp commands
|
||||
pub fn new<A: ToSocketAddrs>(
|
||||
addr: A,
|
||||
security: ClientSecurity,
|
||||
) -> Result<SmtpTransportBuilder, Error> {
|
||||
pub fn new<A: ToSocketAddrs>(addr: A,
|
||||
security: ClientSecurity)
|
||||
-> Result<SmtpTransportBuilder, Error> {
|
||||
let mut addresses = addr.to_socket_addrs()?;
|
||||
|
||||
match addresses.next() {
|
||||
Some(addr) => {
|
||||
Ok(SmtpTransportBuilder {
|
||||
server_addr: addr,
|
||||
Ok(SmtpTransportBuilder { server_addr: addr,
|
||||
security: security,
|
||||
smtp_utf8: false,
|
||||
credentials: None,
|
||||
connection_reuse: ConnectionReuseParameters::NoReuse,
|
||||
hello_name: ClientId::Domain(get_hostname().unwrap_or("localhost".to_string())),
|
||||
hello_name: ClientId::hostname(),
|
||||
authentication_mechanism: None,
|
||||
timeout: Some(Duration::new(60, 0)),
|
||||
})
|
||||
timeout: Some(Duration::new(60, 0)), })
|
||||
}
|
||||
None => Err(Error::Resolution),
|
||||
}
|
||||
@@ -247,10 +242,9 @@ impl SmtpTransportBuilder {
|
||||
}
|
||||
|
||||
/// Enable connection reuse
|
||||
pub fn connection_reuse(
|
||||
mut self,
|
||||
parameters: ConnectionReuseParameters,
|
||||
) -> SmtpTransportBuilder {
|
||||
pub fn connection_reuse(mut self,
|
||||
parameters: ConnectionReuseParameters)
|
||||
-> SmtpTransportBuilder {
|
||||
self.connection_reuse = parameters;
|
||||
self
|
||||
}
|
||||
@@ -323,24 +317,20 @@ impl<'a> SmtpTransport {
|
||||
/// Creates an encrypted transport over submission port, using the provided domain
|
||||
/// to validate TLS certificates.
|
||||
pub fn simple_builder(domain: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||
|
||||
let mut tls_builder = TlsConnector::builder()?;
|
||||
tls_builder.supported_protocols(DEFAULT_TLS_PROTOCOLS)?;
|
||||
|
||||
let tls_parameters =
|
||||
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
|
||||
|
||||
SmtpTransportBuilder::new(
|
||||
(domain, SUBMISSION_PORT),
|
||||
ClientSecurity::Required(tls_parameters),
|
||||
)
|
||||
SmtpTransportBuilder::new((domain, SUBMISSION_PORT),
|
||||
ClientSecurity::Required(tls_parameters))
|
||||
}
|
||||
|
||||
/// Creates a new configurable builder
|
||||
pub fn builder<A: ToSocketAddrs>(
|
||||
addr: A,
|
||||
security: ClientSecurity,
|
||||
) -> Result<SmtpTransportBuilder, Error> {
|
||||
pub fn builder<A: ToSocketAddrs>(addr: A,
|
||||
security: ClientSecurity)
|
||||
-> Result<SmtpTransportBuilder, Error> {
|
||||
SmtpTransportBuilder::new(addr, security)
|
||||
}
|
||||
|
||||
@@ -353,25 +343,20 @@ impl<'a> SmtpTransport {
|
||||
///
|
||||
/// It does not connect to the server, but only creates the `SmtpTransport`
|
||||
pub fn new(builder: SmtpTransportBuilder) -> SmtpTransport {
|
||||
|
||||
let client = Client::new();
|
||||
|
||||
SmtpTransport {
|
||||
client: client,
|
||||
SmtpTransport { client: client,
|
||||
server_info: None,
|
||||
client_info: builder,
|
||||
state: State {
|
||||
panic: false,
|
||||
connection_reuse_count: 0,
|
||||
},
|
||||
}
|
||||
state: State { panic: false,
|
||||
connection_reuse_count: 0, }, }
|
||||
}
|
||||
|
||||
/// Gets the EHLO response and updates server information
|
||||
pub fn get_ehlo(&mut self) -> SmtpResult {
|
||||
// Extended Hello
|
||||
let ehlo_response = try_smtp!(
|
||||
self.client.smtp_command(EhloCommand::new(
|
||||
self.client.command(EhloCommand::new(
|
||||
ClientId::new(self.client_info.hello_name.to_string()),
|
||||
)),
|
||||
self
|
||||
@@ -406,7 +391,6 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
|
||||
/// Sends an email
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(match_same_arms, cyclomatic_complexity))]
|
||||
fn send<U: SendableEmail<'a, T> + 'a>(&mut self, email: &'a U) -> SmtpResult {
|
||||
|
||||
// Extract email information
|
||||
let message_id = email.message_id();
|
||||
|
||||
@@ -416,13 +400,13 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
|
||||
}
|
||||
|
||||
if self.state.connection_reuse_count == 0 {
|
||||
self.client.connect(
|
||||
&self.client_info.server_addr,
|
||||
self.client.connect(&self.client_info.server_addr,
|
||||
match self.client_info.security {
|
||||
ClientSecurity::Wrapper(ref tls_parameters) => Some(tls_parameters),
|
||||
ClientSecurity::Wrapper(ref tls_parameters) => {
|
||||
Some(tls_parameters)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
)?;
|
||||
})?;
|
||||
|
||||
self.client.set_timeout(self.client_info.timeout)?;
|
||||
|
||||
@@ -431,21 +415,20 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
|
||||
|
||||
self.get_ehlo()?;
|
||||
|
||||
match (
|
||||
&self.client_info.security.clone(),
|
||||
self.server_info.as_ref().unwrap().supports_feature(
|
||||
Extension::StartTls,
|
||||
),
|
||||
) {
|
||||
match (&self.client_info.security.clone(),
|
||||
self.server_info.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(Extension::StartTls))
|
||||
{
|
||||
(&ClientSecurity::Required(_), false) => {
|
||||
return Err(From::from("Could not encrypt connection, aborting"))
|
||||
}
|
||||
(&ClientSecurity::Opportunistic(_), false) => (),
|
||||
(&ClientSecurity::None, _) => (),
|
||||
(&ClientSecurity::Wrapper(_), _) => (),
|
||||
(&ClientSecurity::Opportunistic(ref tls_parameters), true) |
|
||||
(&ClientSecurity::Required(ref tls_parameters), true) => {
|
||||
try_smtp!(self.client.smtp_command(StarttlsCommand), self);
|
||||
(&ClientSecurity::Opportunistic(ref tls_parameters), true)
|
||||
| (&ClientSecurity::Required(ref tls_parameters), true) => {
|
||||
try_smtp!(self.client.command(StarttlsCommand), self);
|
||||
try_smtp!(self.client.upgrade_tls_stream(tls_parameters), self);
|
||||
|
||||
debug!("connection encrypted");
|
||||
@@ -471,16 +454,13 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
|
||||
};
|
||||
|
||||
for mechanism in accepted_mechanisms {
|
||||
if self.server_info.as_ref().unwrap().supports_auth_mechanism(
|
||||
mechanism,
|
||||
)
|
||||
{
|
||||
if self.server_info.as_ref()
|
||||
.unwrap()
|
||||
.supports_auth_mechanism(mechanism) {
|
||||
found = true;
|
||||
try_smtp!(
|
||||
self.client.auth(
|
||||
mechanism,
|
||||
self.client_info.credentials.as_ref().unwrap(),
|
||||
),
|
||||
self.client
|
||||
.auth(mechanism, self.client_info.credentials.as_ref().unwrap(),),
|
||||
self
|
||||
);
|
||||
break;
|
||||
@@ -496,45 +476,34 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
|
||||
// Mail
|
||||
let mut mail_options = vec![];
|
||||
|
||||
if self.server_info.as_ref().unwrap().supports_feature(
|
||||
Extension::EightBitMime,
|
||||
)
|
||||
{
|
||||
if self.server_info.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(Extension::EightBitMime) {
|
||||
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
|
||||
}
|
||||
|
||||
if self.server_info.as_ref().unwrap().supports_feature(
|
||||
Extension::SmtpUtfEight,
|
||||
) && self.client_info.smtp_utf8
|
||||
{
|
||||
if self.server_info.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(Extension::SmtpUtfEight) && self.client_info.smtp_utf8 {
|
||||
mail_options.push(MailParameter::SmtpUtfEight);
|
||||
}
|
||||
|
||||
try_smtp!(
|
||||
self.client.smtp_command(MailCommand::new(
|
||||
Some(email.from().clone()),
|
||||
mail_options,
|
||||
)),
|
||||
self
|
||||
);
|
||||
try_smtp!(self.client.command(MailCommand::new(Some(email.from().clone()), mail_options,)),
|
||||
self);
|
||||
|
||||
// Log the mail command
|
||||
info!("{}: from=<{}>", message_id, email.from());
|
||||
|
||||
// Recipient
|
||||
for to_address in &email.to() {
|
||||
try_smtp!(
|
||||
self.client.smtp_command(
|
||||
RcptCommand::new(to_address.clone(), vec![]),
|
||||
),
|
||||
self
|
||||
);
|
||||
try_smtp!(self.client.command(RcptCommand::new(to_address.clone(), vec![]),),
|
||||
self);
|
||||
// Log the rcpt command
|
||||
info!("{}: to=<{}>", message_id, to_address);
|
||||
}
|
||||
|
||||
// Data
|
||||
try_smtp!(self.client.smtp_command(DataCommand), self);
|
||||
try_smtp!(self.client.command(DataCommand), self);
|
||||
|
||||
// Message content
|
||||
let result = self.client.message(email.message());
|
||||
@@ -544,25 +513,25 @@ impl<'a, T: Read + 'a> EmailTransport<'a, T, SmtpResult> for SmtpTransport {
|
||||
self.state.connection_reuse_count += 1;
|
||||
|
||||
// Log the message
|
||||
info!(
|
||||
"{}: conn_use={}, status=sent ({})",
|
||||
info!("{}: conn_use={}, status=sent ({})",
|
||||
message_id,
|
||||
self.state.connection_reuse_count,
|
||||
result
|
||||
.as_ref()
|
||||
result.as_ref()
|
||||
.ok()
|
||||
.unwrap()
|
||||
.message
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap_or(&"no response".to_string())
|
||||
);
|
||||
.unwrap_or(&"no response".to_string()));
|
||||
}
|
||||
|
||||
// Test if we can reuse the existing connection
|
||||
match self.client_info.connection_reuse {
|
||||
ConnectionReuseParameters::ReuseLimited(limit)
|
||||
if self.state.connection_reuse_count >= limit => self.reset(),
|
||||
if self.state.connection_reuse_count >= limit =>
|
||||
{
|
||||
self.reset()
|
||||
}
|
||||
ConnectionReuseParameters::NoReuse => self.reset(),
|
||||
_ => (),
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
//! message
|
||||
|
||||
use self::Severity::*;
|
||||
use nom::{ErrorKind as NomErrorKind, IResult as NomResult, crlf};
|
||||
|
||||
use nom::{crlf, ErrorKind as NomErrorKind, IResult as NomResult};
|
||||
use nom::simple_errors::Err as NomError;
|
||||
use std::fmt::{Display, Formatter, Result};
|
||||
use std::result;
|
||||
@@ -85,11 +84,9 @@ impl Code {
|
||||
panic!("The detail code must be between 0 and 9");
|
||||
}
|
||||
|
||||
Code {
|
||||
severity: severity,
|
||||
Code { severity: severity,
|
||||
category: category,
|
||||
detail: detail,
|
||||
}
|
||||
detail: detail, }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,10 +117,8 @@ impl FromStr for Response {
|
||||
impl Response {
|
||||
/// Creates a new `Response`
|
||||
pub fn new(code: Code, message: Vec<String>) -> Response {
|
||||
Response {
|
||||
code: code,
|
||||
message: message,
|
||||
}
|
||||
Response { code: code,
|
||||
message: message, }
|
||||
}
|
||||
|
||||
/// Tells if the response is positive
|
||||
@@ -141,9 +136,8 @@ impl Response {
|
||||
|
||||
/// Returns only the first word of the message if possible
|
||||
pub fn first_word(&self) -> Option<&str> {
|
||||
self.message.get(0).and_then(
|
||||
|line| line.split_whitespace().next(),
|
||||
)
|
||||
self.message.get(0)
|
||||
.and_then(|line| line.split_whitespace().next())
|
||||
}
|
||||
|
||||
/// Returns only the line of the message if possible
|
||||
@@ -155,19 +149,15 @@ impl Response {
|
||||
// Parsers (originaly from tokio-smtp)
|
||||
|
||||
named!(parse_code<Code>,
|
||||
map!(
|
||||
tuple!(parse_severity, parse_category, parse_detail),
|
||||
map!(tuple!(parse_severity, parse_category, parse_detail),
|
||||
|(severity, category, detail)| {
|
||||
Code {
|
||||
severity: severity,
|
||||
Code { severity: severity,
|
||||
category: category,
|
||||
detail: detail,
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
detail: detail, }
|
||||
}));
|
||||
|
||||
named!(parse_severity<Severity>,
|
||||
named!(
|
||||
parse_severity<Severity>,
|
||||
alt!(
|
||||
tag!("2") => { |_| Severity::PositiveCompletion } |
|
||||
tag!("3") => { |_| Severity::PositiveIntermediate } |
|
||||
@@ -176,7 +166,8 @@ named!(parse_severity<Severity>,
|
||||
)
|
||||
);
|
||||
|
||||
named!(parse_category<Category>,
|
||||
named!(
|
||||
parse_category<Category>,
|
||||
alt!(
|
||||
tag!("0") => { |_| Category::Syntax } |
|
||||
tag!("1") => { |_| Category::Information } |
|
||||
@@ -187,7 +178,8 @@ named!(parse_category<Category>,
|
||||
)
|
||||
);
|
||||
|
||||
named!(parse_detail<Detail>,
|
||||
named!(
|
||||
parse_detail<Detail>,
|
||||
alt!(
|
||||
tag!("0") => { |_| Detail(0) } |
|
||||
tag!("1") => { |_| Detail(1) } |
|
||||
@@ -203,32 +195,15 @@ named!(parse_detail<Detail>,
|
||||
);
|
||||
|
||||
named!(parse_response<Response>,
|
||||
map_res!(
|
||||
tuple!(
|
||||
// Parse any number of continuation lines.
|
||||
many0!(
|
||||
tuple!(
|
||||
parse_code,
|
||||
preceded!(
|
||||
char!('-'),
|
||||
take_until_and_consume!(b"\r\n".as_ref())
|
||||
)
|
||||
)
|
||||
),
|
||||
map_res!(tuple!(// Parse any number of continuation lines.
|
||||
many0!(tuple!(parse_code,
|
||||
preceded!(char!('-'),
|
||||
take_until_and_consume!(b"\r\n".as_ref())))),
|
||||
// Parse the final line.
|
||||
tuple!(
|
||||
parse_code,
|
||||
terminated!(
|
||||
opt!(
|
||||
preceded!(
|
||||
char!(' '),
|
||||
take_until!(b"\r\n".as_ref())
|
||||
)
|
||||
),
|
||||
crlf
|
||||
)
|
||||
)
|
||||
),
|
||||
tuple!(parse_code,
|
||||
terminated!(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) {
|
||||
@@ -236,23 +211,17 @@ named!(parse_response<Response>,
|
||||
}
|
||||
|
||||
// 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 {
|
||||
lines.push(text);
|
||||
}
|
||||
|
||||
Ok(Response {
|
||||
code: last_code,
|
||||
Ok(Response { code: last_code,
|
||||
message: lines.into_iter()
|
||||
.map(|line| from_utf8(line).map(|s| s.to_string()))
|
||||
.collect::<result::Result<Vec<_>, _>>()
|
||||
.map_err(|_| ())?,
|
||||
})
|
||||
}
|
||||
)
|
||||
);
|
||||
.map_err(|_| ())?, })
|
||||
}));
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
@@ -270,37 +239,27 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_code_new() {
|
||||
assert_eq!(
|
||||
Code::new(
|
||||
Severity::TransientNegativeCompletion,
|
||||
assert_eq!(Code::new(Severity::TransientNegativeCompletion,
|
||||
Category::Connections,
|
||||
Detail(0),
|
||||
),
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
Detail(0),),
|
||||
Code { severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::Connections,
|
||||
detail: Detail(0),
|
||||
}
|
||||
);
|
||||
detail: Detail(0), });
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_code_new_panic() {
|
||||
let _ = Code::new(
|
||||
Severity::TransientNegativeCompletion,
|
||||
let _ = Code::new(Severity::TransientNegativeCompletion,
|
||||
Category::Connections,
|
||||
Detail(11),
|
||||
);
|
||||
Detail(11));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_display() {
|
||||
let code = Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
let code = Code { severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::Connections,
|
||||
detail: Detail(1),
|
||||
};
|
||||
detail: Detail(1), };
|
||||
|
||||
assert_eq!(code.to_string(), "421");
|
||||
}
|
||||
@@ -308,32 +267,20 @@ mod test {
|
||||
#[test]
|
||||
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";
|
||||
assert_eq!(
|
||||
raw_response.parse::<Response>().unwrap(),
|
||||
Response {
|
||||
code: Code {
|
||||
severity: Severity::PositiveCompletion,
|
||||
assert_eq!(raw_response.parse::<Response>().unwrap(),
|
||||
Response { code: Code { severity: Severity::PositiveCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: Detail(0),
|
||||
},
|
||||
message: vec![
|
||||
"me".to_string(),
|
||||
detail: Detail(0), },
|
||||
message: vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
"AUTH PLAIN CRAM-MD5".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";
|
||||
assert!(
|
||||
wrong_code.parse::<Response>().is_err()
|
||||
);
|
||||
assert!(wrong_code.parse::<Response>().is_err());
|
||||
|
||||
let wrong_end = "250-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250-AUTH PLAIN CRAM-MD5\r\n";
|
||||
assert!(
|
||||
wrong_end.parse::<Response>().is_err()
|
||||
);
|
||||
assert!(wrong_end.parse::<Response>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -34,13 +34,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
for (input, expect) in vec![
|
||||
("bjorn", "bjorn"),
|
||||
for (input, expect) in vec![("bjorn", "bjorn"),
|
||||
("bjørn", "bjørn"),
|
||||
("Ø+= ❤️‰", "Ø+2B+3D+20❤️‰"),
|
||||
("+", "+2B"),
|
||||
]
|
||||
{
|
||||
("+", "+2B")] {
|
||||
assert_eq!(format!("{}", XText(input)), expect);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,13 +50,10 @@ pub type StubResult = Result<(), ()>;
|
||||
|
||||
impl<'a, T: Read + 'a> EmailTransport<'a, T, StubResult> for StubEmailTransport {
|
||||
fn send<U: SendableEmail<'a, T>>(&mut self, email: &'a U) -> StubResult {
|
||||
|
||||
info!(
|
||||
"{}: from=<{}> to=<{:?}>",
|
||||
info!("{}: from=<{}> to=<{:?}>",
|
||||
email.message_id(),
|
||||
email.from(),
|
||||
email.to()
|
||||
);
|
||||
email.to());
|
||||
self.response
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,10 @@ mod test {
|
||||
#[test]
|
||||
fn file_transport() {
|
||||
let mut sender = FileEmailTransport::new(temp_dir());
|
||||
let email = SimpleSendableEmail::new(
|
||||
EmailAddress::new("user@localhost".to_string()),
|
||||
let email = SimpleSendableEmail::new(EmailAddress::new("user@localhost".to_string()),
|
||||
vec![EmailAddress::new("root@localhost".to_string())],
|
||||
"file_id".to_string(),
|
||||
"Hello file".to_string(),
|
||||
);
|
||||
"Hello file".to_string());
|
||||
let result = sender.send(&email);
|
||||
assert!(result.is_ok());
|
||||
|
||||
@@ -29,11 +27,9 @@ mod test {
|
||||
let mut buffer = String::new();
|
||||
let _ = f.read_to_string(&mut buffer);
|
||||
|
||||
assert_eq!(
|
||||
buffer,
|
||||
assert_eq!(buffer,
|
||||
"{\"to\":[\"root@localhost\"],\"from\":\"user@localhost\",\"message_id\":\
|
||||
\"file_id\",\"message\":[72,101,108,108,111,32,102,105,108,101]}"
|
||||
);
|
||||
\"file_id\",\"message\":[72,101,108,108,111,32,102,105,108,101]}");
|
||||
|
||||
remove_file(file).unwrap();
|
||||
}
|
||||
|
||||
@@ -10,12 +10,10 @@ mod test {
|
||||
#[test]
|
||||
fn sendmail_transport_simple() {
|
||||
let mut sender = SendmailTransport::new();
|
||||
let email = SimpleSendableEmail::new(
|
||||
EmailAddress::new("user@localhost".to_string()),
|
||||
let email = SimpleSendableEmail::new(EmailAddress::new("user@localhost".to_string()),
|
||||
vec![EmailAddress::new("root@localhost".to_string())],
|
||||
"sendmail_id".to_string(),
|
||||
"Hello sendmail".to_string(),
|
||||
);
|
||||
"Hello sendmail".to_string());
|
||||
|
||||
let result = sender.send(&email);
|
||||
println!("{:?}", result);
|
||||
|
||||
@@ -8,15 +8,12 @@ mod test {
|
||||
|
||||
#[test]
|
||||
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).unwrap()
|
||||
.build();
|
||||
let email = SimpleSendableEmail::new(
|
||||
EmailAddress::new("user@localhost".to_string()),
|
||||
let email = SimpleSendableEmail::new(EmailAddress::new("user@localhost".to_string()),
|
||||
vec![EmailAddress::new("root@localhost".to_string())],
|
||||
"smtp_id".to_string(),
|
||||
"Hello smtp".to_string(),
|
||||
);
|
||||
"Hello smtp".to_string());
|
||||
|
||||
sender.send(&email).unwrap();
|
||||
}
|
||||
|
||||
@@ -8,12 +8,10 @@ fn stub_transport() {
|
||||
let mut sender_ok = StubEmailTransport::new_positive();
|
||||
let mut sender_ko = StubEmailTransport::new(Err(()));
|
||||
|
||||
let email = SimpleSendableEmail::new(
|
||||
EmailAddress::new("user@localhost".to_string()),
|
||||
let email = SimpleSendableEmail::new(EmailAddress::new("user@localhost".to_string()),
|
||||
vec![EmailAddress::new("root@localhost".to_string())],
|
||||
"stub_id".to_string(),
|
||||
"Hello stub".to_string(),
|
||||
);
|
||||
"Hello stub".to_string());
|
||||
|
||||
sender_ok.send(&email).unwrap();
|
||||
sender_ko.send(&email).unwrap_err();
|
||||
|
||||
@@ -19,8 +19,7 @@ fn main() {
|
||||
.unwrap();
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mut mailer = SmtpTransport::builder_unencrypted_localhost()
|
||||
.unwrap()
|
||||
let mut mailer = SmtpTransport::builder_unencrypted_localhost().unwrap()
|
||||
.build();
|
||||
// Send the email
|
||||
let result = mailer.send(&email);
|
||||
|
||||
@@ -55,11 +55,11 @@
|
||||
#![doc(html_root_url = "https://docs.rs/lettre_email/0.7.0")]
|
||||
#![deny(missing_docs, unsafe_code, unstable_features, warnings, missing_debug_implementations)]
|
||||
|
||||
extern crate email as email_format;
|
||||
extern crate lettre;
|
||||
extern crate mime;
|
||||
extern crate time;
|
||||
extern crate uuid;
|
||||
extern crate email as email_format;
|
||||
extern crate lettre;
|
||||
|
||||
pub mod error;
|
||||
|
||||
@@ -70,7 +70,7 @@ use mime::Mime;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
use time::{Tm, now};
|
||||
use time::{now, Tm};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Converts an address or an address with an alias to a `Header`
|
||||
@@ -359,10 +359,8 @@ pub struct Envelope {
|
||||
impl Envelope {
|
||||
/// Constructs an envelope with no receivers and an empty sender
|
||||
pub fn new() -> Self {
|
||||
Envelope {
|
||||
to: vec![],
|
||||
from: String::new(),
|
||||
}
|
||||
Envelope { to: vec![],
|
||||
from: String::new(), }
|
||||
}
|
||||
/// Adds a receiver
|
||||
pub fn to<S: Into<String>>(mut self, address: S) -> Self {
|
||||
@@ -398,7 +396,7 @@ pub struct Email {
|
||||
impl PartBuilder {
|
||||
/// Creates a new empty part
|
||||
pub fn new() -> PartBuilder {
|
||||
PartBuilder { message: MimeMessage::new_blank_message() }
|
||||
PartBuilder { message: MimeMessage::new_blank_message(), }
|
||||
}
|
||||
|
||||
/// Adds a generic header
|
||||
@@ -466,8 +464,7 @@ impl PartBuilder {
|
||||
impl EmailBuilder {
|
||||
/// Creates a new empty email
|
||||
pub fn new() -> EmailBuilder {
|
||||
EmailBuilder {
|
||||
message: PartBuilder::new(),
|
||||
EmailBuilder { message: PartBuilder::new(),
|
||||
to_header: vec![],
|
||||
from_header: vec![],
|
||||
cc_header: vec![],
|
||||
@@ -475,8 +472,7 @@ impl EmailBuilder {
|
||||
reply_to_header: vec![],
|
||||
sender_header: None,
|
||||
envelope: None,
|
||||
date_issued: false,
|
||||
}
|
||||
date_issued: false, }
|
||||
}
|
||||
|
||||
/// Sets the email body
|
||||
@@ -581,9 +577,7 @@ impl EmailBuilder {
|
||||
|
||||
/// Adds a `Subject` header
|
||||
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
|
||||
@@ -594,31 +588,27 @@ impl EmailBuilder {
|
||||
|
||||
/// Adds a `Date` header with the given date
|
||||
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;
|
||||
}
|
||||
|
||||
/// Adds an attachment to the email
|
||||
pub fn attachment(
|
||||
mut self,
|
||||
pub fn attachment(mut self,
|
||||
path: &Path,
|
||||
filename: Option<&str>,
|
||||
content_type: &Mime,
|
||||
) -> Result<EmailBuilder, Error> {
|
||||
content_type: &Mime)
|
||||
-> Result<EmailBuilder, Error> {
|
||||
self.set_attachment(path, filename, content_type)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Adds an attachment to the email
|
||||
/// If filename is not provided, the name of the file will be used.
|
||||
pub fn set_attachment(
|
||||
&mut self,
|
||||
pub fn set_attachment(&mut self,
|
||||
path: &Path,
|
||||
filename: Option<&str>,
|
||||
content_type: &Mime,
|
||||
) -> Result<(), Error> {
|
||||
content_type: &Mime)
|
||||
-> Result<(), Error> {
|
||||
let file = File::open(path);
|
||||
let body = match file {
|
||||
Ok(mut f) => {
|
||||
@@ -651,12 +641,10 @@ impl EmailBuilder {
|
||||
}
|
||||
};
|
||||
|
||||
let content = PartBuilder::new()
|
||||
.body(body)
|
||||
.header((
|
||||
"Content-Disposition",
|
||||
format!("attachment; filename=\"{}\"", actual_filename),
|
||||
))
|
||||
let content = PartBuilder::new().body(body)
|
||||
.header(("Content-Disposition",
|
||||
format!("attachment; filename=\"{}\"",
|
||||
actual_filename)))
|
||||
.header(("Content-Type", content_type.to_string()))
|
||||
.build();
|
||||
|
||||
@@ -697,10 +685,7 @@ impl EmailBuilder {
|
||||
/// Sets the email body to plain text content
|
||||
pub fn set_text<S: Into<String>>(&mut self, body: S) {
|
||||
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
|
||||
@@ -712,42 +697,33 @@ impl EmailBuilder {
|
||||
/// Sets the email body to HTML content
|
||||
pub fn set_html<S: Into<String>>(&mut self, body: S) {
|
||||
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
|
||||
pub fn alternative<S: Into<String>, T: Into<String>>(
|
||||
mut self,
|
||||
pub fn alternative<S: Into<String>, T: Into<String>>(mut self,
|
||||
body_html: S,
|
||||
body_text: T,
|
||||
) -> EmailBuilder {
|
||||
body_text: T)
|
||||
-> EmailBuilder {
|
||||
self.set_alternative(body_html, body_text);
|
||||
self
|
||||
}
|
||||
|
||||
/// 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>>(&mut self,
|
||||
body_html: S,
|
||||
body_text: T,
|
||||
) {
|
||||
body_text: T) {
|
||||
let mut alternate = PartBuilder::new();
|
||||
alternate.set_message_type(MimeMultipartType::Alternative);
|
||||
|
||||
let text = PartBuilder::new()
|
||||
.body(body_text)
|
||||
.header((
|
||||
"Content-Type",
|
||||
format!("{}", mime::TEXT_PLAIN_UTF_8).as_ref(),
|
||||
))
|
||||
let text = PartBuilder::new().body(body_text)
|
||||
.header(("Content-Type",
|
||||
format!("{}", mime::TEXT_PLAIN_UTF_8).as_ref()))
|
||||
.build();
|
||||
|
||||
let html = PartBuilder::new()
|
||||
.body(body_html)
|
||||
.header(("Content-Type", format!("{}", mime::TEXT_HTML).as_ref()))
|
||||
let html = PartBuilder::new().body(body_html)
|
||||
.header(("Content-Type",
|
||||
format!("{}", mime::TEXT_HTML).as_ref()))
|
||||
.build();
|
||||
|
||||
alternate.add_child(text);
|
||||
@@ -800,10 +776,9 @@ impl EmailBuilder {
|
||||
// we need to generate the envelope
|
||||
let mut e = Envelope::new();
|
||||
// add all receivers in to_header and cc_header
|
||||
for receiver in self.to_header.iter().chain(self.cc_header.iter()).chain(
|
||||
self.bcc_header.iter(),
|
||||
)
|
||||
{
|
||||
for receiver in self.to_header.iter()
|
||||
.chain(self.cc_header.iter())
|
||||
.chain(self.bcc_header.iter()) {
|
||||
match *receiver {
|
||||
Address::Mailbox(ref m) => e.add_to(m.address.clone()),
|
||||
Address::Group(_, ref ms) => {
|
||||
@@ -843,27 +818,16 @@ impl EmailBuilder {
|
||||
// Add the collected addresses as mailbox-list all at once.
|
||||
// The unwraps are fine because the conversions for Vec<Address> never errs.
|
||||
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() {
|
||||
self.message.add_header(
|
||||
Header::new_with_value("From".into(), self.from_header).unwrap(),
|
||||
);
|
||||
self.message
|
||||
.add_header(Header::new_with_value("From".into(), self.from_header).unwrap());
|
||||
} else {
|
||||
return Err(Error::MissingFrom);
|
||||
}
|
||||
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.reply_to_header.is_empty() {
|
||||
self.message.add_header(
|
||||
@@ -872,36 +836,27 @@ impl EmailBuilder {
|
||||
}
|
||||
|
||||
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"));
|
||||
|
||||
let message_id = Uuid::new_v4();
|
||||
|
||||
if let Ok(header) = Header::new_with_value(
|
||||
"Message-ID".to_string(),
|
||||
format!("<{}.lettre@localhost>", message_id),
|
||||
)
|
||||
{
|
||||
if let Ok(header) = Header::new_with_value("Message-ID".to_string(),
|
||||
format!("<{}.lettre@localhost>", message_id)) {
|
||||
self.message.add_header(header)
|
||||
}
|
||||
|
||||
Ok(Email {
|
||||
message: self.message.build().as_string().into_bytes(),
|
||||
Ok(Email { message: self.message.build().as_string().into_bytes(),
|
||||
envelope: envelope,
|
||||
message_id: message_id,
|
||||
})
|
||||
message_id: message_id, })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SendableEmail<'a, &'a [u8]> for Email {
|
||||
fn to(&self) -> Vec<EmailAddress> {
|
||||
self.envelope
|
||||
.to
|
||||
self.envelope.to
|
||||
.iter()
|
||||
.map(|x| EmailAddress::new(x.clone()))
|
||||
.collect()
|
||||
@@ -957,8 +912,7 @@ mod test {
|
||||
let email_builder = SimpleEmail::default();
|
||||
let date_now = now();
|
||||
|
||||
let email = email_builder
|
||||
.to("user@localhost")
|
||||
let email = email_builder.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.cc(("cc@localhost", "Alias"))
|
||||
.reply_to("reply@localhost")
|
||||
@@ -969,26 +923,21 @@ mod test {
|
||||
.into_email()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", String::from_utf8_lossy(email.message().as_ref())),
|
||||
format!(
|
||||
"Subject: Hello\r\nContent-Type: text/plain; \
|
||||
assert_eq!(format!("{}", String::from_utf8_lossy(email.message().as_ref())),
|
||||
format!("Subject: Hello\r\nContent-Type: text/plain; \
|
||||
charset=utf-8\r\nX-test: value\r\nTo: <user@localhost>\r\nFrom: \
|
||||
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\nReply-To: \
|
||||
<reply@localhost>\r\nDate: {}\r\nMIME-Version: 1.0\r\nMessage-ID: \
|
||||
<{}.lettre@localhost>\r\n\r\nHello World!\r\n",
|
||||
date_now.rfc822z(),
|
||||
email.message_id()
|
||||
)
|
||||
);
|
||||
email.message_id()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_from() {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = now();
|
||||
let email = email_builder
|
||||
.to("anna@example.com")
|
||||
let email = email_builder.to("anna@example.com")
|
||||
.from("dieter@example.com")
|
||||
.from("joachim@example.com")
|
||||
.date(&date_now)
|
||||
@@ -996,17 +945,13 @@ mod test {
|
||||
.body("We invite you!")
|
||||
.build()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
format!("{}", String::from_utf8_lossy(email.message().as_ref())),
|
||||
format!(
|
||||
"Date: {}\r\nSubject: Invitation\r\nSender: \
|
||||
assert_eq!(format!("{}", String::from_utf8_lossy(email.message().as_ref())),
|
||||
format!("Date: {}\r\nSubject: Invitation\r\nSender: \
|
||||
<dieter@example.com>\r\nTo: <anna@example.com>\r\nFrom: \
|
||||
<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()
|
||||
)
|
||||
);
|
||||
email.message_id()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1014,8 +959,7 @@ mod test {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = now();
|
||||
|
||||
let email = email_builder
|
||||
.to("user@localhost")
|
||||
let email = email_builder.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.cc(("cc@localhost", "Alias"))
|
||||
.reply_to("reply@localhost")
|
||||
@@ -1027,18 +971,14 @@ mod test {
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", String::from_utf8_lossy(email.message().as_ref())),
|
||||
format!(
|
||||
"Date: {}\r\nSubject: Hello\r\nX-test: value\r\nSender: \
|
||||
assert_eq!(format!("{}", String::from_utf8_lossy(email.message().as_ref())),
|
||||
format!("Date: {}\r\nSubject: Hello\r\nX-test: value\r\nSender: \
|
||||
<sender@localhost>\r\nTo: <user@localhost>\r\nFrom: \
|
||||
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\nReply-To: \
|
||||
<reply@localhost>\r\nMIME-Version: 1.0\r\nMessage-ID: \
|
||||
<{}.lettre@localhost>\r\n\r\nHello World!\r\n",
|
||||
date_now.rfc822z(),
|
||||
email.message_id()
|
||||
)
|
||||
);
|
||||
email.message_id()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1046,8 +986,7 @@ mod test {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = now();
|
||||
|
||||
let email = email_builder
|
||||
.to("user@localhost")
|
||||
let email = email_builder.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.cc(("cc@localhost", "Alias"))
|
||||
.bcc("bcc@localhost")
|
||||
@@ -1061,14 +1000,10 @@ mod test {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(email.from().to_string(), "sender@localhost".to_string());
|
||||
assert_eq!(
|
||||
email.to(),
|
||||
vec![
|
||||
EmailAddress::new("user@localhost".to_string()),
|
||||
assert_eq!(email.to(),
|
||||
vec![EmailAddress::new("user@localhost".to_string()),
|
||||
EmailAddress::new("cc@localhost".to_string()),
|
||||
EmailAddress::new("bcc@localhost".to_string()),
|
||||
]
|
||||
);
|
||||
EmailAddress::new("bcc@localhost".to_string())]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
write_mode = "Overwrite"
|
||||
reorder_imports = true
|
||||
reorder_imported_names = true
|
||||
reorder_imports_in_group = true
|
||||
struct_field_align_threshold = 20
|
||||
indent_style = "Visual"
|
||||
chain_indent = "Visual"
|
||||
|
||||
@@ -98,15 +98,15 @@ use lettre::smtp::commands::*;
|
||||
|
||||
let mut email_client: Client<NetworkStream> = Client::new();
|
||||
let _ = email_client.connect(&("localhost", SMTP_PORT), None);
|
||||
let _ = email_client.smtp_command(EhloCommand::new(ClientId::new("my_hostname".to_string())));
|
||||
let _ = email_client.smtp_command(
|
||||
let _ = email_client.command(EhloCommand::new(ClientId::new("my_hostname".to_string())));
|
||||
let _ = email_client.command(
|
||||
MailCommand::new(Some(EmailAddress::new("user@example.com".to_string())), vec![])
|
||||
);
|
||||
let _ = email_client.smtp_command(
|
||||
let _ = email_client.command(
|
||||
RcptCommand::new(EmailAddress::new("user@example.org".to_string()), vec![])
|
||||
);
|
||||
let _ = email_client.smtp_command(DataCommand);
|
||||
let _ = email_client.command(DataCommand);
|
||||
let _ = email_client.message(Box::new("Test email".as_bytes()));
|
||||
let _ = email_client.smtp_command(QuitCommand);
|
||||
let _ = email_client.command(QuitCommand);
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user