Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f911dce12 | ||
|
|
47d6870d93 | ||
|
|
51de392086 | ||
|
|
fefb5f7978 | ||
|
|
5d125bdbdb | ||
|
|
a1bf0170db | ||
|
|
5bedba4b24 |
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
|
|
||||||
name = "smtp"
|
name = "smtp"
|
||||||
version = "0.1.2"
|
version = "0.3.0"
|
||||||
description = "Simple SMTP client"
|
description = "Simple SMTP client"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
documentation = "http://amousset.me/rust-smtp/smtp/"
|
documentation = "http://amousset.me/rust-smtp/smtp/"
|
||||||
@@ -18,6 +18,7 @@ rustc-serialize = "0.3"
|
|||||||
rust-crypto = "0.2"
|
rust-crypto = "0.2"
|
||||||
bufstream = "0.1"
|
bufstream = "0.1"
|
||||||
email = "0.0"
|
email = "0.0"
|
||||||
|
openssl = "0.6"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
env_logger = "0.3"
|
env_logger = "0.3"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ To use this library, add the following to your `Cargo.toml`:
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
smtp = "0.1"
|
smtp = "0.2"
|
||||||
```
|
```
|
||||||
|
|
||||||
License
|
License
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ pub enum Mecanism {
|
|||||||
|
|
||||||
impl Display for Mecanism {
|
impl Display for Mecanism {
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
write!(f, "{}",
|
write!(f,
|
||||||
match *self {
|
"{}",
|
||||||
Mecanism::Plain => "PLAIN",
|
match *self {
|
||||||
Mecanism::CramMd5 => "CRAM-MD5",
|
Mecanism::Plain => "PLAIN",
|
||||||
}
|
Mecanism::CramMd5 => "CRAM-MD5",
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,15 +43,22 @@ impl Mecanism {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the string to send to the server, using the provided username, password and challenge in some cases
|
/// Returns the string to send to the server, using the provided username, password and
|
||||||
pub fn response(&self, username: &str, password: &str, challenge: Option<&str>) -> Result<String, Error> {
|
/// challenge in some cases
|
||||||
|
pub fn response(&self,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
challenge: Option<&str>)
|
||||||
|
-> Result<String, Error> {
|
||||||
match *self {
|
match *self {
|
||||||
Mecanism::Plain => {
|
Mecanism::Plain => {
|
||||||
match challenge {
|
match challenge {
|
||||||
Some(_) => Err(Error::ClientError("This mecanism does not expect a challenge")),
|
Some(_) => Err(Error::ClientError("This mecanism does not expect a challenge")),
|
||||||
None => Ok(format!("{}{}{}{}", NUL, username, NUL, password).as_bytes().to_base64(base64::STANDARD)),
|
None => Ok(format!("{}{}{}{}", NUL, username, NUL, password)
|
||||||
|
.as_bytes()
|
||||||
|
.to_base64(base64::STANDARD)),
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Mecanism::CramMd5 => {
|
Mecanism::CramMd5 => {
|
||||||
let encoded_challenge = match challenge {
|
let encoded_challenge = match challenge {
|
||||||
Some(challenge) => challenge,
|
Some(challenge) => challenge,
|
||||||
@@ -66,8 +73,10 @@ impl Mecanism {
|
|||||||
let mut hmac = Hmac::new(Md5::new(), password.as_bytes());
|
let mut hmac = Hmac::new(Md5::new(), password.as_bytes());
|
||||||
hmac.input(&decoded_challenge);
|
hmac.input(&decoded_challenge);
|
||||||
|
|
||||||
Ok(format!("{} {}", username, hmac.result().code().to_hex()).as_bytes().to_base64(base64::STANDARD))
|
Ok(format!("{} {}", username, hmac.result().code().to_hex())
|
||||||
},
|
.as_bytes()
|
||||||
|
.to_base64(base64::STANDARD))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,7 +89,8 @@ mod test {
|
|||||||
fn test_plain() {
|
fn test_plain() {
|
||||||
let mecanism = Mecanism::Plain;
|
let mecanism = Mecanism::Plain;
|
||||||
|
|
||||||
assert_eq!(mecanism.response("username", "password", None).unwrap(), "AHVzZXJuYW1lAHBhc3N3b3Jk");
|
assert_eq!(mecanism.response("username", "password", None).unwrap(),
|
||||||
|
"AHVzZXJuYW1lAHBhc3N3b3Jk");
|
||||||
assert!(mecanism.response("username", "password", Some("test")).is_err());
|
assert!(mecanism.response("username", "password", Some("test")).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,9 +98,11 @@ mod test {
|
|||||||
fn test_cram_md5() {
|
fn test_cram_md5() {
|
||||||
let mecanism = Mecanism::CramMd5;
|
let mecanism = Mecanism::CramMd5;
|
||||||
|
|
||||||
assert_eq!(mecanism.response("alice", "wonderland",
|
assert_eq!(mecanism.response("alice",
|
||||||
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==")).unwrap(),
|
"wonderland",
|
||||||
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
|
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=="))
|
||||||
|
.unwrap(),
|
||||||
|
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
|
||||||
assert!(mecanism.response("alice", "wonderland", Some("tést")).is_err());
|
assert!(mecanism.response("alice", "wonderland", Some("tést")).is_err());
|
||||||
assert!(mecanism.response("alice", "wonderland", None).is_err());
|
assert!(mecanism.response("alice", "wonderland", None).is_err());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
//! SMTP client
|
//! SMTP client
|
||||||
|
|
||||||
use std::string::String;
|
use std::string::String;
|
||||||
use std::net::{SocketAddr, ToSocketAddrs};
|
use std::net::ToSocketAddrs;
|
||||||
use std::io::{BufRead, Read, Write};
|
use std::io::{BufRead, Read, Write};
|
||||||
|
use std::io;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use bufstream::BufStream;
|
use bufstream::BufStream;
|
||||||
|
use openssl::ssl::SslContext;
|
||||||
|
|
||||||
use response::ResponseParser;
|
use response::ResponseParser;
|
||||||
use authentication::Mecanism;
|
use authentication::Mecanism;
|
||||||
use error::{Error, SmtpResult};
|
use error::{Error, SmtpResult};
|
||||||
use client::net::{Connector, SmtpStream};
|
use client::net::{Connector, NetworkStream};
|
||||||
use {CRLF, MESSAGE_ENDING};
|
use {CRLF, MESSAGE_ENDING};
|
||||||
|
|
||||||
pub mod net;
|
pub mod net;
|
||||||
@@ -23,8 +26,9 @@ fn escape_dot(string: &str) -> String {
|
|||||||
format!(".{}", string)
|
format!(".{}", string)
|
||||||
} else {
|
} else {
|
||||||
string.to_string()
|
string.to_string()
|
||||||
}.replace("\r.", "\r..")
|
}
|
||||||
.replace("\n.", "\n..")
|
.replace("\r.", "\r..")
|
||||||
|
.replace("\n.", "\n..")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the string replacing all the CRLF with "\<CRLF\>"
|
/// Returns the string replacing all the CRLF with "\<CRLF\>"
|
||||||
@@ -40,12 +44,10 @@ fn remove_crlf(string: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Structure that implements the SMTP client
|
/// Structure that implements the SMTP client
|
||||||
pub struct Client<S: Write + Read = SmtpStream> {
|
pub struct Client<S: Write + Read = NetworkStream> {
|
||||||
/// TCP stream between client and server
|
/// TCP stream between client and server
|
||||||
/// Value is None before connection
|
/// Value is None before connection
|
||||||
stream: Option<BufStream<S>>,
|
stream: Option<BufStream<S>>,
|
||||||
/// Socket we are connecting to
|
|
||||||
server_addr: SocketAddr,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! return_err (
|
macro_rules! return_err (
|
||||||
@@ -54,39 +56,53 @@ macro_rules! return_err (
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
impl<S: Write + Read = SmtpStream> Client<S> {
|
impl<S: Write + Read = NetworkStream> Client<S> {
|
||||||
/// Creates a new SMTP client
|
/// Creates a new SMTP client
|
||||||
///
|
///
|
||||||
/// It does not connects to the server, but only creates the `Client`
|
/// It does not connects to the server, but only creates the `Client`
|
||||||
pub fn new<A: ToSocketAddrs>(addr: A) -> Result<Client<S>, Error> {
|
pub fn new() -> Client<S> {
|
||||||
let mut addresses = try!(addr.to_socket_addrs());
|
Client { stream: None }
|
||||||
|
|
||||||
match addresses.next() {
|
|
||||||
Some(addr) => Ok(Client {
|
|
||||||
stream: None,
|
|
||||||
server_addr: addr,
|
|
||||||
}),
|
|
||||||
None => Err(From::from("Could nor resolve hostname")),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: Connector + Write + Read = SmtpStream> Client<S> {
|
impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
|
||||||
/// Closes the SMTP transaction if possible
|
/// Closes the SMTP transaction if possible
|
||||||
pub fn close(&mut self) {
|
pub fn close(&mut self) {
|
||||||
let _ = self.quit();
|
let _ = self.quit();
|
||||||
self.stream = None;
|
self.stream = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the underlying stream
|
||||||
|
pub fn set_stream(&mut self, stream: S) {
|
||||||
|
self.stream = Some(BufStream::new(stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upgrades the underlying connection to SSL/TLS
|
||||||
|
pub fn upgrade_tls_stream(&mut self, ssl_context: &SslContext) -> io::Result<()> {
|
||||||
|
//let current_stream = self.stream.clone();
|
||||||
|
if self.stream.is_some() {
|
||||||
|
self.stream.as_mut().unwrap().get_mut().upgrade_tls(ssl_context)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Connects to the configured server
|
/// Connects to the configured server
|
||||||
pub fn connect(&mut self) -> SmtpResult {
|
pub fn connect<A: ToSocketAddrs>(&mut self, addr: &A) -> 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut addresses = try!(addr.to_socket_addrs());
|
||||||
|
|
||||||
|
let server_addr = match addresses.next() {
|
||||||
|
Some(addr) => addr,
|
||||||
|
None => return_err!("Could not resolve hostname", self),
|
||||||
|
};
|
||||||
|
|
||||||
// Try to connect
|
// Try to connect
|
||||||
self.stream = Some(BufStream::new(try!(Connector::connect(&self.server_addr))));
|
self.set_stream(try!(Connector::connect(&server_addr, None)));
|
||||||
|
|
||||||
self.get_reply()
|
self.get_reply()
|
||||||
}
|
}
|
||||||
@@ -101,12 +117,7 @@ impl<S: Connector + Write + Read = SmtpStream> Client<S> {
|
|||||||
self.send_server(command, CRLF)
|
self.send_server(command, CRLF)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a HELO command and fills `server_info`
|
/// Sends a EHLO command
|
||||||
pub fn helo(&mut self, hostname: &str) -> SmtpResult {
|
|
||||||
self.command(&format!("HELO {}", hostname))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends a EHLO command and fills `server_info`
|
|
||||||
pub fn ehlo(&mut self, hostname: &str) -> SmtpResult {
|
pub fn ehlo(&mut self, hostname: &str) -> SmtpResult {
|
||||||
self.command(&format!("EHLO {}", hostname))
|
self.command(&format!("EHLO {}", hostname))
|
||||||
}
|
}
|
||||||
@@ -166,19 +177,30 @@ impl<S: Connector + Write + Read = SmtpStream> Client<S> {
|
|||||||
pub fn auth(&mut self, mecanism: Mecanism, username: &str, password: &str) -> SmtpResult {
|
pub fn auth(&mut self, mecanism: Mecanism, username: &str, password: &str) -> SmtpResult {
|
||||||
|
|
||||||
if mecanism.supports_initial_response() {
|
if mecanism.supports_initial_response() {
|
||||||
self.command(&format!("AUTH {} {}", mecanism, try!(mecanism.response(username, password, None))))
|
self.command(&format!("AUTH {} {}",
|
||||||
|
mecanism,
|
||||||
|
try!(mecanism.response(username, password, None))))
|
||||||
} else {
|
} else {
|
||||||
let encoded_challenge = match try!(self.command("AUTH CRAM-MD5")).first_word() {
|
let encoded_challenge = match try!(self.command("AUTH CRAM-MD5")).first_word() {
|
||||||
Some(challenge) => challenge,
|
Some(challenge) => challenge,
|
||||||
None => return Err(Error::ResponseParsingError("Could not read CRAM challenge")),
|
None => return Err(Error::ResponseParsingError("Could not read CRAM challenge")),
|
||||||
};
|
};
|
||||||
|
|
||||||
let cram_response = try!(mecanism.response(username, password, Some(&encoded_challenge)));
|
debug!("CRAM challenge: {}", encoded_challenge);
|
||||||
|
|
||||||
self.command(&format!("AUTH CRAM-MD5 {}", cram_response))
|
let cram_response = try!(mecanism.response(username,
|
||||||
|
password,
|
||||||
|
Some(&encoded_challenge)));
|
||||||
|
|
||||||
|
self.command(&format!("{}", cram_response))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sends a STARTTLS command
|
||||||
|
pub fn starttls(&mut self) -> SmtpResult {
|
||||||
|
self.command("STARTTLS")
|
||||||
|
}
|
||||||
|
|
||||||
/// Sends the message content
|
/// Sends the message content
|
||||||
pub fn message(&mut self, message_content: &str) -> SmtpResult {
|
pub fn message(&mut self, message_content: &str) -> SmtpResult {
|
||||||
self.send_server(&escape_dot(message_content), MESSAGE_ENDING)
|
self.send_server(&escape_dot(message_content), MESSAGE_ENDING)
|
||||||
@@ -206,6 +228,8 @@ impl<S: Connector + Write + Read = SmtpStream> Client<S> {
|
|||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
try!(self.stream.as_mut().unwrap().read_line(&mut line));
|
try!(self.stream.as_mut().unwrap().read_line(&mut line));
|
||||||
|
|
||||||
|
debug!("Read: {}", escape_crlf(line.as_ref()));
|
||||||
|
|
||||||
while try!(parser.read_line(remove_crlf(line.as_ref()).as_ref())) {
|
while try!(parser.read_line(remove_crlf(line.as_ref()).as_ref())) {
|
||||||
line.clear();
|
line.clear();
|
||||||
try!(self.stream.as_mut().unwrap().read_line(&mut line));
|
try!(self.stream.as_mut().unwrap().read_line(&mut line));
|
||||||
@@ -236,19 +260,15 @@ mod test {
|
|||||||
fn test_remove_crlf() {
|
fn test_remove_crlf() {
|
||||||
assert_eq!(remove_crlf("\r\n"), "");
|
assert_eq!(remove_crlf("\r\n"), "");
|
||||||
assert_eq!(remove_crlf("EHLO my_name\r\n"), "EHLO my_name");
|
assert_eq!(remove_crlf("EHLO my_name\r\n"), "EHLO my_name");
|
||||||
assert_eq!(
|
assert_eq!(remove_crlf("EHLO my_name\r\nSIZE 42\r\n"),
|
||||||
remove_crlf("EHLO my_name\r\nSIZE 42\r\n"),
|
"EHLO my_nameSIZE 42");
|
||||||
"EHLO my_nameSIZE 42"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_escape_crlf() {
|
fn test_escape_crlf() {
|
||||||
assert_eq!(escape_crlf("\r\n"), "<CR><LF>");
|
assert_eq!(escape_crlf("\r\n"), "<CR><LF>");
|
||||||
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CR><LF>");
|
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CR><LF>");
|
||||||
assert_eq!(
|
assert_eq!(escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
|
||||||
escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
|
"EHLO my_name<CR><LF>SIZE 42<CR><LF>");
|
||||||
"EHLO my_name<CR><LF>SIZE 42<CR><LF>"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,94 @@
|
|||||||
//! A trait to represent a stream
|
//! A trait to represent a stream
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::io::{Read, Write, ErrorKind};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
|
use std::fmt;
|
||||||
|
use std::fmt::{Debug, Formatter};
|
||||||
|
use openssl::ssl::{SslContext, SslStream};
|
||||||
|
|
||||||
/// A trait for the concept of opening a stream
|
/// A trait for the concept of opening a stream
|
||||||
pub trait Connector {
|
pub trait Connector {
|
||||||
/// Opens a connection to the given IP socket
|
/// Opens a connection to the given IP socket
|
||||||
fn connect(addr: &SocketAddr) -> io::Result<Self>;
|
fn connect(addr: &SocketAddr, ssl_context: Option<&SslContext>) -> io::Result<Self>;
|
||||||
|
/// Upgrades to TLS connection
|
||||||
|
fn upgrade_tls(&mut self, ssl_context: &SslContext) -> io::Result<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Connector for SmtpStream {
|
impl Connector for NetworkStream {
|
||||||
fn connect(addr: &SocketAddr) -> io::Result<SmtpStream> {
|
fn connect(addr: &SocketAddr, ssl_context: Option<&SslContext>) -> io::Result<NetworkStream> {
|
||||||
TcpStream::connect(addr)
|
let tcp_stream = try!(TcpStream::connect(addr));
|
||||||
|
|
||||||
|
match ssl_context {
|
||||||
|
Some(context) => match SslStream::new(&context, tcp_stream) {
|
||||||
|
Ok(stream) => Ok(NetworkStream::Ssl(stream)),
|
||||||
|
Err(err) => Err(io::Error::new(ErrorKind::Other, err)),
|
||||||
|
},
|
||||||
|
None => Ok(NetworkStream::Plain(tcp_stream)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upgrade_tls(&mut self, ssl_context: &SslContext) -> io::Result<()> {
|
||||||
|
*self = match self.clone() {
|
||||||
|
NetworkStream::Plain(stream) => match SslStream::new(ssl_context, stream) {
|
||||||
|
Ok(ssl_stream) => NetworkStream::Ssl(ssl_stream),
|
||||||
|
Err(err) => return Err(io::Error::new(ErrorKind::Other, err)),
|
||||||
|
},
|
||||||
|
NetworkStream::Ssl(stream) => NetworkStream::Ssl(stream),
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents an atual SMTP network stream
|
|
||||||
//Used later for ssl
|
|
||||||
pub type SmtpStream = TcpStream;
|
|
||||||
|
|
||||||
|
/// Represents the different types of underlying network streams
|
||||||
|
pub enum NetworkStream {
|
||||||
|
/// Plain TCP
|
||||||
|
Plain(TcpStream),
|
||||||
|
/// SSL over TCP
|
||||||
|
Ssl(SslStream<TcpStream>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for NetworkStream {
|
||||||
|
#[inline]
|
||||||
|
fn clone(&self) -> NetworkStream {
|
||||||
|
match self {
|
||||||
|
&NetworkStream::Plain(ref stream) => NetworkStream::Plain(stream.try_clone().unwrap()),
|
||||||
|
&NetworkStream::Ssl(ref stream) => NetworkStream::Ssl(stream.try_clone().unwrap()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for NetworkStream {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
f.write_str("NetworkStream(_)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Read for NetworkStream {
|
||||||
|
#[inline]
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
match *self {
|
||||||
|
NetworkStream::Plain(ref mut stream) => stream.read(buf),
|
||||||
|
NetworkStream::Ssl(ref mut stream) => stream.read(buf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for NetworkStream {
|
||||||
|
#[inline]
|
||||||
|
fn write(&mut self, msg: &[u8]) -> io::Result<usize> {
|
||||||
|
match *self {
|
||||||
|
NetworkStream::Plain(ref mut stream) => stream.write(msg),
|
||||||
|
NetworkStream::Ssl(ref mut stream) => stream.write(msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
match *self {
|
||||||
|
NetworkStream::Plain(ref mut stream) => stream.flush(),
|
||||||
|
NetworkStream::Ssl(ref mut stream) => stream.flush(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
101
src/email.rs
101
src/email.rs
@@ -74,9 +74,7 @@ pub struct Email {
|
|||||||
|
|
||||||
impl Display for Email {
|
impl Display for Email {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
write!(f, "{}",
|
write!(f, "{}", self.message.as_string())
|
||||||
self.message.as_string()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +90,8 @@ impl EmailBuilder {
|
|||||||
message_id: current_message,
|
message_id: current_message,
|
||||||
};
|
};
|
||||||
|
|
||||||
match Header::new_with_value("Message-ID".to_string(), format!("<{}@rust-smtp>", current_message)) {
|
match Header::new_with_value("Message-ID".to_string(),
|
||||||
|
format!("<{}@rust-smtp>", current_message)) {
|
||||||
Ok(header) => email.message.headers.insert(header),
|
Ok(header) => email.message.headers.insert(header),
|
||||||
Err(_) => (),
|
Err(_) => (),
|
||||||
}
|
}
|
||||||
@@ -166,7 +165,7 @@ impl EmailBuilder {
|
|||||||
|
|
||||||
/// Adds a `Date` header with the given date
|
/// Adds a `Date` header with the given date
|
||||||
pub fn date(mut self, date: &Tm) -> EmailBuilder {
|
pub fn date(mut self, date: &Tm) -> EmailBuilder {
|
||||||
self.insert_header(("Date", Tm::rfc822(date).to_string().as_ref()));
|
self.insert_header(("Date", Tm::rfc822z(date).to_string().as_ref()));
|
||||||
self.date_issued = true;
|
self.date_issued = true;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -174,7 +173,7 @@ impl EmailBuilder {
|
|||||||
/// Build the Email
|
/// Build the Email
|
||||||
pub fn build(mut self) -> Email {
|
pub fn build(mut self) -> Email {
|
||||||
if !self.date_issued {
|
if !self.date_issued {
|
||||||
self.insert_header(("Date", Tm::rfc822(&now()).to_string().as_ref()));
|
self.insert_header(("Date", Tm::rfc822z(&now()).to_string().as_ref()));
|
||||||
}
|
}
|
||||||
self.content.message.update_headers();
|
self.content.message.update_headers();
|
||||||
self.content
|
self.content
|
||||||
@@ -239,7 +238,7 @@ impl SendableEmail for Email {
|
|||||||
if self.to.is_empty() {
|
if self.to.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(self.to.clone())
|
Some(self.to.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,22 +279,21 @@ mod test {
|
|||||||
message_id: current_message,
|
message_id: current_message,
|
||||||
};
|
};
|
||||||
|
|
||||||
email.message.headers.insert(
|
email.message.headers.insert(Header::new_with_value("Message-ID".to_string(),
|
||||||
Header::new_with_value("Message-ID".to_string(),
|
format!("<{}@rust-smtp>",
|
||||||
format!("<{}@rust-smtp>", current_message)
|
current_message))
|
||||||
).unwrap()
|
.unwrap());
|
||||||
);
|
|
||||||
|
|
||||||
email.message.headers.insert(
|
email.message
|
||||||
Header::new_with_value("To".to_string(), "to@example.com".to_string()).unwrap()
|
.headers
|
||||||
);
|
.insert(Header::new_with_value("To".to_string(), "to@example.com".to_string())
|
||||||
|
.unwrap());
|
||||||
|
|
||||||
email.message.body = "body".to_string();
|
email.message.body = "body".to_string();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(format!("{}", email),
|
||||||
format!("{}", email),
|
format!("Message-ID: <{}@rust-smtp>\r\nTo: to@example.com\r\n\r\nbody\r\n",
|
||||||
format!("Message-ID: <{}@rust-smtp>\r\nTo: to@example.com\r\n\r\nbody\r\n", current_message)
|
current_message));
|
||||||
);
|
|
||||||
assert_eq!(current_message.to_string(), email.message_id().unwrap());
|
assert_eq!(current_message.to_string(), email.message_id().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,21 +303,23 @@ mod test {
|
|||||||
let date_now = now();
|
let date_now = now();
|
||||||
|
|
||||||
let email = email_builder.to("user@localhost")
|
let email = email_builder.to("user@localhost")
|
||||||
.from("user@localhost")
|
.from("user@localhost")
|
||||||
.cc(("cc@localhost", "Alias"))
|
.cc(("cc@localhost", "Alias"))
|
||||||
.reply_to("reply@localhost")
|
.reply_to("reply@localhost")
|
||||||
.sender("sender@localhost")
|
.sender("sender@localhost")
|
||||||
.body("Hello World!")
|
.body("Hello World!")
|
||||||
.date(&date_now)
|
.date(&date_now)
|
||||||
.subject("Hello")
|
.subject("Hello")
|
||||||
.add_header(("X-test", "value"))
|
.add_header(("X-test", "value"))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(format!("{}", email),
|
||||||
format!("{}", email),
|
format!("Message-ID: <{}@rust-smtp>\r\nTo: <user@localhost>\r\nFrom: \
|
||||||
format!("Message-ID: <{}@rust-smtp>\r\nTo: <user@localhost>\r\nFrom: <user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\nReply-To: <reply@localhost>\r\nSender: <sender@localhost>\r\nDate: {}\r\nSubject: Hello\r\nX-test: value\r\n\r\nHello World!\r\n",
|
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\nReply-To: \
|
||||||
email.message_id().unwrap(), date_now.rfc822())
|
<reply@localhost>\r\nSender: <sender@localhost>\r\nDate: \
|
||||||
);
|
{}\r\nSubject: Hello\r\nX-test: value\r\n\r\nHello World!\r\n",
|
||||||
|
email.message_id().unwrap(),
|
||||||
|
date_now.rfc822z()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -328,28 +328,21 @@ mod test {
|
|||||||
let date_now = now();
|
let date_now = now();
|
||||||
|
|
||||||
let email = email_builder.to("user@localhost")
|
let email = email_builder.to("user@localhost")
|
||||||
.from("user@localhost")
|
.from("user@localhost")
|
||||||
.cc(("cc@localhost", "Alias"))
|
.cc(("cc@localhost", "Alias"))
|
||||||
.reply_to("reply@localhost")
|
.reply_to("reply@localhost")
|
||||||
.sender("sender@localhost")
|
.sender("sender@localhost")
|
||||||
.body("Hello World!")
|
.body("Hello World!")
|
||||||
.date(&date_now)
|
.date(&date_now)
|
||||||
.subject("Hello")
|
.subject("Hello")
|
||||||
.add_header(("X-test", "value"))
|
.add_header(("X-test", "value"))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(email.from_address().unwrap(),
|
||||||
email.from_address().unwrap(),
|
"sender@localhost".to_string());
|
||||||
"sender@localhost".to_string()
|
assert_eq!(email.to_addresses().unwrap(),
|
||||||
);
|
vec!["user@localhost".to_string(), "cc@localhost".to_string()]);
|
||||||
assert_eq!(
|
assert_eq!(email.message().unwrap(), format!("{}", email));
|
||||||
email.to_addresses().unwrap(),
|
|
||||||
vec!["user@localhost".to_string(), "cc@localhost".to_string()]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
email.message().unwrap(),
|
|
||||||
format!("{}", email)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ impl From<Response> for Error {
|
|||||||
match response.severity() {
|
match response.severity() {
|
||||||
Severity::TransientNegativeCompletion => TransientError(response),
|
Severity::TransientNegativeCompletion => TransientError(response),
|
||||||
Severity::PermanentNegativeCompletion => PermanentError(response),
|
Severity::PermanentNegativeCompletion => PermanentError(response),
|
||||||
_ => ClientError("Unknown error code")
|
_ => ClientError("Unknown error code"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
153
src/extension.rs
153
src/extension.rs
@@ -54,13 +54,13 @@ 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, "{} with {}",
|
write!(f,
|
||||||
self.name,
|
"{} with {}",
|
||||||
match self.features.is_empty() {
|
self.name,
|
||||||
true => "no supported features".to_string(),
|
match self.features.is_empty() {
|
||||||
false => format! ("{:?}", self.features),
|
true => "no supported features".to_string(),
|
||||||
}
|
false => format!("{:?}", self.features),
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,32 +69,42 @@ impl ServerInfo {
|
|||||||
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
|
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
|
||||||
let name = match response.first_word() {
|
let name = match response.first_word() {
|
||||||
Some(name) => name,
|
Some(name) => name,
|
||||||
None => return Err(Error::ResponseParsingError("Could not read server name"))
|
None => return Err(Error::ResponseParsingError("Could not read server name")),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut features: HashSet<Extension> = HashSet::new();
|
let mut features: HashSet<Extension> = HashSet::new();
|
||||||
|
|
||||||
for line in response.message() {
|
for line in response.message() {
|
||||||
|
|
||||||
let splitted : Vec<&str> = line.split_whitespace().collect();
|
let splitted: Vec<&str> = line.split_whitespace().collect();
|
||||||
let _ = match splitted[0] {
|
let _ = match splitted[0] {
|
||||||
"8BITMIME" => {features.insert(Extension::EightBitMime);},
|
"8BITMIME" => {
|
||||||
"SMTPUTF8" => {features.insert(Extension::SmtpUtfEight);},
|
features.insert(Extension::EightBitMime);
|
||||||
"STARTTLS" => {features.insert(Extension::StartTls);},
|
}
|
||||||
|
"SMTPUTF8" => {
|
||||||
|
features.insert(Extension::SmtpUtfEight);
|
||||||
|
}
|
||||||
|
"STARTTLS" => {
|
||||||
|
features.insert(Extension::StartTls);
|
||||||
|
}
|
||||||
"AUTH" => {
|
"AUTH" => {
|
||||||
for &mecanism in &splitted[1..] {
|
for &mecanism in &splitted[1..] {
|
||||||
match mecanism {
|
match mecanism {
|
||||||
"PLAIN" => {features.insert(Extension::Authentication(Mecanism::Plain));},
|
"PLAIN" => {
|
||||||
"CRAM-MD5" => {features.insert(Extension::Authentication(Mecanism::CramMd5));},
|
features.insert(Extension::Authentication(Mecanism::Plain));
|
||||||
|
}
|
||||||
|
"CRAM-MD5" => {
|
||||||
|
features.insert(Extension::Authentication(Mecanism::CramMd5));
|
||||||
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ServerInfo{
|
Ok(ServerInfo {
|
||||||
name: name,
|
name: name,
|
||||||
features: features,
|
features: features,
|
||||||
})
|
})
|
||||||
@@ -113,7 +123,7 @@ impl ServerInfo {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use super::{ServerInfo, Extension};
|
use super::{ServerInfo, Extension};
|
||||||
use authentication::Mecanism;
|
use authentication::Mecanism;
|
||||||
@@ -121,77 +131,90 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_extension_fmt() {
|
fn test_extension_fmt() {
|
||||||
assert_eq!(format!("{}", Extension::EightBitMime), "8BITMIME".to_string());
|
assert_eq!(format!("{}", Extension::EightBitMime),
|
||||||
assert_eq!(format!("{}", Extension::Authentication(Mecanism::Plain)), "AUTH PLAIN".to_string());
|
"8BITMIME".to_string());
|
||||||
|
assert_eq!(format!("{}", Extension::Authentication(Mecanism::Plain)),
|
||||||
|
"AUTH PLAIN".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_serverinfo_fmt() {
|
fn test_serverinfo_fmt() {
|
||||||
let mut eightbitmime = HashSet::new();
|
let mut eightbitmime = HashSet::new();
|
||||||
assert!(eightbitmime.insert(Extension::EightBitMime));
|
assert!(eightbitmime.insert(Extension::EightBitMime));
|
||||||
|
|
||||||
assert_eq!(format!("{}", ServerInfo{
|
assert_eq!(format!("{}",
|
||||||
name: "name".to_string(),
|
ServerInfo {
|
||||||
features: eightbitmime.clone()
|
name: "name".to_string(),
|
||||||
}), "name with {EightBitMime}".to_string());
|
features: eightbitmime.clone(),
|
||||||
|
}),
|
||||||
|
"name with {EightBitMime}".to_string());
|
||||||
|
|
||||||
let empty = HashSet::new();
|
let empty = HashSet::new();
|
||||||
|
|
||||||
assert_eq!(format!("{}", ServerInfo{
|
assert_eq!(format!("{}",
|
||||||
name: "name".to_string(),
|
ServerInfo {
|
||||||
features: empty,
|
name: "name".to_string(),
|
||||||
}), "name with no supported features".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(Mecanism::Plain)));
|
assert!(plain.insert(Extension::Authentication(Mecanism::Plain)));
|
||||||
|
|
||||||
assert_eq!(format!("{}", ServerInfo{
|
assert_eq!(format!("{}",
|
||||||
name: "name".to_string(),
|
ServerInfo {
|
||||||
features: plain.clone()
|
name: "name".to_string(),
|
||||||
}), "name with {Authentication(Plain)}".to_string());
|
features: plain.clone(),
|
||||||
|
}),
|
||||||
|
"name with {Authentication(Plain)}".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_serverinfo() {
|
fn test_serverinfo() {
|
||||||
let response = Response::new(
|
let response = Response::new(Code::new(Severity::PositiveCompletion,
|
||||||
Code::new(Severity::PositiveCompletion, Category::Unspecified4, 1),
|
Category::Unspecified4,
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
1),
|
||||||
);
|
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 {
|
let server_info = ServerInfo {
|
||||||
name: "me".to_string(),
|
name: "me".to_string(),
|
||||||
features: features,
|
features: features,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(ServerInfo::from_response(&response).unwrap(), server_info);
|
assert_eq!(ServerInfo::from_response(&response).unwrap(), server_info);
|
||||||
|
|
||||||
assert!(server_info.supports_feature(&Extension::EightBitMime));
|
assert!(server_info.supports_feature(&Extension::EightBitMime));
|
||||||
assert!(!server_info.supports_feature(&Extension::StartTls));
|
assert!(!server_info.supports_feature(&Extension::StartTls));
|
||||||
assert!(!server_info.supports_auth_mecanism(Mecanism::CramMd5));
|
assert!(!server_info.supports_auth_mecanism(Mecanism::CramMd5));
|
||||||
|
|
||||||
let response2 = Response::new(
|
let response2 = Response::new(Code::new(Severity::PositiveCompletion,
|
||||||
Code::new(Severity::PositiveCompletion, Category::Unspecified4, 1),
|
Category::Unspecified4,
|
||||||
vec!["me".to_string(), "AUTH PLAIN CRAM-MD5 OTHER".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
1),
|
||||||
);
|
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));
|
||||||
assert!(features2.insert(Extension::Authentication(Mecanism::Plain)));
|
assert!(features2.insert(Extension::Authentication(Mecanism::Plain)));
|
||||||
assert!(features2.insert(Extension::Authentication(Mecanism::CramMd5)));
|
assert!(features2.insert(Extension::Authentication(Mecanism::CramMd5)));
|
||||||
|
|
||||||
let server_info2 = ServerInfo {
|
let server_info2 = ServerInfo {
|
||||||
name: "me".to_string(),
|
name: "me".to_string(),
|
||||||
features: features2,
|
features: features2,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2);
|
assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2);
|
||||||
|
|
||||||
assert!(server_info2.supports_feature(&Extension::EightBitMime));
|
assert!(server_info2.supports_feature(&Extension::EightBitMime));
|
||||||
assert!(server_info2.supports_auth_mecanism(Mecanism::Plain));
|
assert!(server_info2.supports_auth_mecanism(Mecanism::Plain));
|
||||||
assert!(server_info2.supports_auth_mecanism(Mecanism::CramMd5));
|
assert!(server_info2.supports_auth_mecanism(Mecanism::CramMd5));
|
||||||
assert!(!server_info2.supports_feature(&Extension::StartTls));
|
assert!(!server_info2.supports_feature(&Extension::StartTls));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/lib.rs
23
src/lib.rs
@@ -2,17 +2,17 @@
|
|||||||
//!
|
//!
|
||||||
//! This client should tend to follow [RFC 5321](https://tools.ietf.org/html/rfc5321), but is still
|
//! This client should tend to follow [RFC 5321](https://tools.ietf.org/html/rfc5321), but is still
|
||||||
//! a work in progress. It is designed to efficiently send emails from an application to a
|
//! a work in progress. It is designed to efficiently send emails from an application to a
|
||||||
//! relay email server, as it relies as much as possible on the relay server for sanity and RFC compliance
|
//! relay email server, as it relies as much as possible on the relay server for sanity and RFC
|
||||||
//! checks.
|
//! compliance checks.
|
||||||
//!
|
//!
|
||||||
//! It implements the following extensions:
|
//! It implements the following extensions:
|
||||||
//!
|
//!
|
||||||
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
|
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
|
||||||
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN and CRAM-MD5 mecanisms
|
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN and CRAM-MD5 mecanisms
|
||||||
|
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
|
||||||
//!
|
//!
|
||||||
//! It will eventually implement the following extensions:
|
//! It will eventually implement the following extensions:
|
||||||
//!
|
//!
|
||||||
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
|
|
||||||
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
|
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
|
||||||
//!
|
//!
|
||||||
//! ## Architecture
|
//! ## Architecture
|
||||||
@@ -57,6 +57,7 @@
|
|||||||
//! use smtp::sender::{Sender, SenderBuilder};
|
//! use smtp::sender::{Sender, SenderBuilder};
|
||||||
//! use smtp::email::EmailBuilder;
|
//! use smtp::email::EmailBuilder;
|
||||||
//! use smtp::authentication::Mecanism;
|
//! use smtp::authentication::Mecanism;
|
||||||
|
//! use smtp::SUBMISSION_PORT;
|
||||||
//!
|
//!
|
||||||
//! let mut builder = EmailBuilder::new();
|
//! let mut builder = EmailBuilder::new();
|
||||||
//! builder = builder.to(("user@example.org", "Alias name"));
|
//! builder = builder.to(("user@example.org", "Alias name"));
|
||||||
@@ -72,11 +73,14 @@
|
|||||||
//! let email = builder.build();
|
//! let email = builder.build();
|
||||||
//!
|
//!
|
||||||
//! // Connect to a remote server on a custom port
|
//! // Connect to a remote server on a custom port
|
||||||
//! let mut sender = SenderBuilder::new(("server.tld", 10025)).unwrap()
|
//! let mut sender = SenderBuilder::new(("server.tld", SUBMISSION_PORT)).unwrap()
|
||||||
//! // Set the name sent during EHLO/HELO, default is `localhost`
|
//! // Set the name sent during EHLO/HELO, default is `localhost`
|
||||||
//! .hello_name("my.hostname.tld")
|
//! .hello_name("my.hostname.tld")
|
||||||
//! // Add credentials for authentication
|
//! // Add credentials for authentication
|
||||||
//! .credentials("username", "password")
|
//! .credentials("username", "password")
|
||||||
|
//! // Use TLS with STARTTLS, you can also specify a specific SSL context
|
||||||
|
//! // with `.ssl_context(context)`
|
||||||
|
//! .starttls()
|
||||||
//! // Configure accepted authetication mecanisms
|
//! // Configure accepted authetication mecanisms
|
||||||
//! .authentication_mecanisms(vec![Mecanism::CramMd5])
|
//! .authentication_mecanisms(vec![Mecanism::CramMd5])
|
||||||
//! // Enable connection reuse
|
//! // Enable connection reuse
|
||||||
@@ -119,12 +123,11 @@
|
|||||||
//!
|
//!
|
||||||
//! ```rust,no_run
|
//! ```rust,no_run
|
||||||
//! use smtp::client::Client;
|
//! use smtp::client::Client;
|
||||||
//! use smtp::client::net::SmtpStream;
|
|
||||||
//! use smtp::SMTP_PORT;
|
//! use smtp::SMTP_PORT;
|
||||||
//! use std::net::TcpStream;
|
//! use smtp::client::net::NetworkStream;
|
||||||
//!
|
//!
|
||||||
//! let mut email_client: Client<SmtpStream> = Client::new(("localhost", SMTP_PORT)).unwrap();
|
//! let mut email_client: Client<NetworkStream> = Client::new();
|
||||||
//! let _ = email_client.connect();
|
//! let _ = email_client.connect(&("localhost", SMTP_PORT));
|
||||||
//! let _ = email_client.ehlo("my_hostname");
|
//! let _ = email_client.ehlo("my_hostname");
|
||||||
//! let _ = email_client.mail("user@example.com", None);
|
//! let _ = email_client.mail("user@example.com", None);
|
||||||
//! let _ = email_client.rcpt("user@example.org");
|
//! let _ = email_client.rcpt("user@example.org");
|
||||||
@@ -135,13 +138,15 @@
|
|||||||
|
|
||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
#[macro_use] extern crate log;
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
extern crate rustc_serialize as serialize;
|
extern crate rustc_serialize as serialize;
|
||||||
extern crate crypto;
|
extern crate crypto;
|
||||||
extern crate time;
|
extern crate time;
|
||||||
extern crate uuid;
|
extern crate uuid;
|
||||||
extern crate email as email_format;
|
extern crate email as email_format;
|
||||||
extern crate bufstream;
|
extern crate bufstream;
|
||||||
|
extern crate openssl;
|
||||||
|
|
||||||
mod extension;
|
mod extension;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
|
|||||||
476
src/response.rs
476
src/response.rs
@@ -36,14 +36,14 @@ impl FromStr for Severity {
|
|||||||
|
|
||||||
impl Display for Severity {
|
impl Display for Severity {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
write!(f, "{}",
|
write!(f,
|
||||||
match *self {
|
"{}",
|
||||||
PositiveCompletion => 2,
|
match *self {
|
||||||
PositiveIntermediate => 3,
|
PositiveCompletion => 2,
|
||||||
TransientNegativeCompletion => 4,
|
PositiveIntermediate => 3,
|
||||||
PermanentNegativeCompletion => 5,
|
TransientNegativeCompletion => 4,
|
||||||
}
|
PermanentNegativeCompletion => 5,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,16 +81,16 @@ impl FromStr for Category {
|
|||||||
|
|
||||||
impl Display for Category {
|
impl Display for Category {
|
||||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||||
write!(f, "{}",
|
write!(f,
|
||||||
match *self {
|
"{}",
|
||||||
Syntax => 0,
|
match *self {
|
||||||
Information => 1,
|
Syntax => 0,
|
||||||
Connections => 2,
|
Information => 1,
|
||||||
Unspecified3 => 3,
|
Connections => 2,
|
||||||
Unspecified4 => 4,
|
Unspecified3 => 3,
|
||||||
MailSystem => 5,
|
Unspecified4 => 4,
|
||||||
}
|
MailSystem => 5,
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,8 +111,14 @@ impl FromStr for Code {
|
|||||||
#[inline]
|
#[inline]
|
||||||
fn from_str(s: &str) -> result::Result<Code, Error> {
|
fn from_str(s: &str) -> result::Result<Code, Error> {
|
||||||
if s.len() == 3 {
|
if s.len() == 3 {
|
||||||
match (s[0..1].parse::<Severity>(), s[1..2].parse::<Category>(), s[2..3].parse::<u8>()) {
|
match (s[0..1].parse::<Severity>(),
|
||||||
(Ok(severity), Ok(category), Ok(detail)) => Ok(Code {severity: severity, category: category, detail: detail}),
|
s[1..2].parse::<Category>(),
|
||||||
|
s[2..3].parse::<u8>()) {
|
||||||
|
(Ok(severity), Ok(category), Ok(detail)) => Ok(Code {
|
||||||
|
severity: severity,
|
||||||
|
category: category,
|
||||||
|
detail: detail,
|
||||||
|
}),
|
||||||
_ => return Err(Error::ResponseParsingError("Could not parse response code")),
|
_ => return Err(Error::ResponseParsingError("Could not parse response code")),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -144,7 +150,7 @@ pub struct ResponseParser {
|
|||||||
code: Option<Code>,
|
code: Option<Code>,
|
||||||
/// Server response string (optional)
|
/// Server response string (optional)
|
||||||
/// Handle multiline responses
|
/// Handle multiline responses
|
||||||
message: Vec<String>
|
message: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseParser {
|
impl ResponseParser {
|
||||||
@@ -166,10 +172,11 @@ impl ResponseParser {
|
|||||||
match self.code {
|
match self.code {
|
||||||
Some(ref code) => {
|
Some(ref code) => {
|
||||||
if code.code() != line[0..3] {
|
if code.code() != line[0..3] {
|
||||||
return Err(Error::ResponseParsingError("Response code has changed during a reponse"));
|
return Err(Error::ResponseParsingError("Response code has changed during a \
|
||||||
|
reponse"));
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
None => self.code = Some(try!(line[0..3].parse::<Code>()))
|
None => self.code = Some(try!(line[0..3].parse::<Code>())),
|
||||||
}
|
}
|
||||||
|
|
||||||
if line.len() > 4 {
|
if line.len() > 4 {
|
||||||
@@ -188,7 +195,8 @@ impl ResponseParser {
|
|||||||
pub fn response(self) -> SmtpResult {
|
pub fn response(self) -> SmtpResult {
|
||||||
match self.code {
|
match self.code {
|
||||||
Some(code) => Ok(Response::new(code, self.message)),
|
Some(code) => Ok(Response::new(code, self.message)),
|
||||||
None => Err(Error::ResponseParsingError("Incomplete response, could not read response code"))
|
None => Err(Error::ResponseParsingError("Incomplete response, could not read \
|
||||||
|
response code")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,7 +210,7 @@ pub struct Response {
|
|||||||
code: Code,
|
code: Code,
|
||||||
/// Server response string (optional)
|
/// Server response string (optional)
|
||||||
/// Handle multiline responses
|
/// Handle multiline responses
|
||||||
message: Vec<String>
|
message: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Response {
|
impl Response {
|
||||||
@@ -260,7 +268,7 @@ impl Response {
|
|||||||
false => match self.message[0].split_whitespace().next() {
|
false => match self.message[0].split_whitespace().next() {
|
||||||
Some(word) => Some(word.to_string()),
|
Some(word) => Some(word.to_string()),
|
||||||
None => None,
|
None => None,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,8 +279,10 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_severity_from_str() {
|
fn test_severity_from_str() {
|
||||||
assert_eq!("2".parse::<Severity>().unwrap(), Severity::PositiveCompletion);
|
assert_eq!("2".parse::<Severity>().unwrap(),
|
||||||
assert_eq!("4".parse::<Severity>().unwrap(), Severity::TransientNegativeCompletion);
|
Severity::PositiveCompletion);
|
||||||
|
assert_eq!("4".parse::<Severity>().unwrap(),
|
||||||
|
Severity::TransientNegativeCompletion);
|
||||||
assert!("1".parse::<Severity>().is_err());
|
assert!("1".parse::<Severity>().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,26 +305,24 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_code_new() {
|
fn test_code_new() {
|
||||||
assert_eq!(
|
assert_eq!(Code::new(Severity::TransientNegativeCompletion,
|
||||||
Code::new(Severity::TransientNegativeCompletion, Category::Connections, 0),
|
Category::Connections,
|
||||||
Code {
|
0),
|
||||||
severity: Severity::TransientNegativeCompletion,
|
Code {
|
||||||
category: Category::Connections,
|
severity: Severity::TransientNegativeCompletion,
|
||||||
detail: 0,
|
category: Category::Connections,
|
||||||
}
|
detail: 0,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_code_from_str() {
|
fn test_code_from_str() {
|
||||||
assert_eq!(
|
assert_eq!("421".parse::<Code>().unwrap(),
|
||||||
"421".parse::<Code>().unwrap(),
|
Code {
|
||||||
Code {
|
severity: Severity::TransientNegativeCompletion,
|
||||||
severity: Severity::TransientNegativeCompletion,
|
category: Category::Connections,
|
||||||
category: Category::Connections,
|
detail: 1,
|
||||||
detail: 1,
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -330,36 +338,38 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_response_new() {
|
fn test_response_new() {
|
||||||
assert_eq!(Response::new(
|
assert_eq!(Response::new(Code {
|
||||||
Code {
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
severity: "2".parse::<Severity>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
category: "4".parse::<Category>().unwrap(),
|
detail: 1,
|
||||||
detail: 1,
|
},
|
||||||
},
|
vec!["me".to_string(),
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
"8BITMIME".to_string(),
|
||||||
), Response {
|
"SIZE 42".to_string()]),
|
||||||
code: Code {
|
Response {
|
||||||
severity: Severity::PositiveCompletion,
|
code: Code {
|
||||||
category: Category::Unspecified4,
|
severity: Severity::PositiveCompletion,
|
||||||
detail: 1,
|
category: Category::Unspecified4,
|
||||||
},
|
detail: 1,
|
||||||
message: vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()],
|
},
|
||||||
});
|
message: vec!["me".to_string(),
|
||||||
assert_eq!(Response::new(
|
"8BITMIME".to_string(),
|
||||||
Code {
|
"SIZE 42".to_string()],
|
||||||
severity: "2".parse::<Severity>().unwrap(),
|
});
|
||||||
category: "4".parse::<Category>().unwrap(),
|
assert_eq!(Response::new(Code {
|
||||||
detail:1,
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
},
|
category: "4".parse::<Category>().unwrap(),
|
||||||
vec![]
|
detail: 1,
|
||||||
), Response {
|
},
|
||||||
code: Code {
|
vec![]),
|
||||||
severity: Severity::PositiveCompletion,
|
Response {
|
||||||
category: Category::Unspecified4,
|
code: Code {
|
||||||
detail: 1,
|
severity: Severity::PositiveCompletion,
|
||||||
},
|
category: Category::Unspecified4,
|
||||||
message: vec![],
|
detail: 1,
|
||||||
});
|
},
|
||||||
|
message: vec![],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -373,186 +383,206 @@ mod test {
|
|||||||
|
|
||||||
let response = parser.response().unwrap();
|
let response = parser.response().unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(response,
|
||||||
response,
|
Response {
|
||||||
Response {
|
code: Code {
|
||||||
code: Code {
|
severity: Severity::PositiveCompletion,
|
||||||
severity: Severity::PositiveCompletion,
|
category: Category::MailSystem,
|
||||||
category: Category::MailSystem,
|
detail: 0,
|
||||||
detail: 0,
|
},
|
||||||
},
|
message: vec!["me".to_string(),
|
||||||
message: vec!["me".to_string(), "8BITMIME".to_string(),
|
"8BITMIME".to_string(),
|
||||||
"SIZE 42".to_string(), "AUTH PLAIN CRAM-MD5".to_string()],
|
"SIZE 42".to_string(),
|
||||||
}
|
"AUTH PLAIN CRAM-MD5".to_string()],
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_response_is_positive() {
|
fn test_response_is_positive() {
|
||||||
assert!(Response::new(
|
assert!(Response::new(Code {
|
||||||
Code {
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
severity: "2".parse::<Severity>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
category: "4".parse::<Category>().unwrap(),
|
detail: 1,
|
||||||
detail:1,
|
},
|
||||||
},
|
vec!["me".to_string(),
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
"8BITMIME".to_string(),
|
||||||
).is_positive());
|
"SIZE 42".to_string()])
|
||||||
assert!(! Response::new(
|
.is_positive());
|
||||||
Code {
|
assert!(!Response::new(Code {
|
||||||
severity: "5".parse::<Severity>().unwrap(),
|
severity: "5".parse::<Severity>().unwrap(),
|
||||||
category: "4".parse::<Category>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
detail:1,
|
detail: 1,
|
||||||
},
|
},
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
vec!["me".to_string(),
|
||||||
).is_positive());
|
"8BITMIME".to_string(),
|
||||||
|
"SIZE 42".to_string()])
|
||||||
|
.is_positive());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_response_message() {
|
fn test_response_message() {
|
||||||
assert_eq!(Response::new(
|
assert_eq!(Response::new(Code {
|
||||||
Code {
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
severity: "2".parse::<Severity>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
category: "4".parse::<Category>().unwrap(),
|
detail: 1,
|
||||||
detail:1,
|
},
|
||||||
},
|
vec!["me".to_string(),
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
"8BITMIME".to_string(),
|
||||||
).message(), vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]);
|
"SIZE 42".to_string()])
|
||||||
|
.message(),
|
||||||
|
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]);
|
||||||
let empty_message: Vec<String> = vec![];
|
let empty_message: Vec<String> = vec![];
|
||||||
assert_eq!(Response::new(
|
assert_eq!(Response::new(Code {
|
||||||
Code {
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
severity: "2".parse::<Severity>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
category: "4".parse::<Category>().unwrap(),
|
detail: 1,
|
||||||
detail:1,
|
},
|
||||||
},
|
vec![])
|
||||||
vec![]
|
.message(),
|
||||||
).message(), empty_message);
|
empty_message);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_response_severity() {
|
fn test_response_severity() {
|
||||||
assert_eq!(Response::new(
|
assert_eq!(Response::new(Code {
|
||||||
Code {
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
severity: "2".parse::<Severity>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
category: "4".parse::<Category>().unwrap(),
|
detail: 1,
|
||||||
detail:1,
|
},
|
||||||
},
|
vec!["me".to_string(),
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
"8BITMIME".to_string(),
|
||||||
).severity(), Severity::PositiveCompletion);
|
"SIZE 42".to_string()])
|
||||||
assert_eq!(Response::new(
|
.severity(),
|
||||||
Code {
|
Severity::PositiveCompletion);
|
||||||
severity: "5".parse::<Severity>().unwrap(),
|
assert_eq!(Response::new(Code {
|
||||||
category: "4".parse::<Category>().unwrap(),
|
severity: "5".parse::<Severity>().unwrap(),
|
||||||
detail: 1,
|
category: "4".parse::<Category>().unwrap(),
|
||||||
},
|
detail: 1,
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
},
|
||||||
).severity(), Severity::PermanentNegativeCompletion);
|
vec!["me".to_string(),
|
||||||
|
"8BITMIME".to_string(),
|
||||||
|
"SIZE 42".to_string()])
|
||||||
|
.severity(),
|
||||||
|
Severity::PermanentNegativeCompletion);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_response_category() {
|
fn test_response_category() {
|
||||||
assert_eq!(Response::new(
|
assert_eq!(Response::new(Code {
|
||||||
Code {
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
severity: "2".parse::<Severity>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
category: "4".parse::<Category>().unwrap(),
|
detail: 1,
|
||||||
detail:1,
|
},
|
||||||
},
|
vec!["me".to_string(),
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
"8BITMIME".to_string(),
|
||||||
).category(), Category::Unspecified4);
|
"SIZE 42".to_string()])
|
||||||
|
.category(),
|
||||||
|
Category::Unspecified4);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_response_detail() {
|
fn test_response_detail() {
|
||||||
assert_eq!(Response::new(
|
assert_eq!(Response::new(Code {
|
||||||
Code {
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
severity: "2".parse::<Severity>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
category: "4".parse::<Category>().unwrap(),
|
detail: 1,
|
||||||
detail:1,
|
},
|
||||||
},
|
vec!["me".to_string(),
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
"8BITMIME".to_string(),
|
||||||
).detail(), 1);
|
"SIZE 42".to_string()])
|
||||||
|
.detail(),
|
||||||
|
1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_response_code() {
|
fn test_response_code() {
|
||||||
assert_eq!(Response::new(
|
assert_eq!(Response::new(Code {
|
||||||
Code {
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
severity: "2".parse::<Severity>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
category: "4".parse::<Category>().unwrap(),
|
detail: 1,
|
||||||
detail:1,
|
},
|
||||||
},
|
vec!["me".to_string(),
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
"8BITMIME".to_string(),
|
||||||
).code(), "241");
|
"SIZE 42".to_string()])
|
||||||
|
.code(),
|
||||||
|
"241");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_response_has_code() {
|
fn test_response_has_code() {
|
||||||
assert!(Response::new(
|
assert!(Response::new(Code {
|
||||||
Code {
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
severity: "2".parse::<Severity>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
category: "4".parse::<Category>().unwrap(),
|
detail: 1,
|
||||||
detail:1,
|
},
|
||||||
},
|
vec!["me".to_string(),
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
"8BITMIME".to_string(),
|
||||||
).has_code(241));
|
"SIZE 42".to_string()])
|
||||||
assert!(! Response::new(
|
.has_code(241));
|
||||||
Code {
|
assert!(!Response::new(Code {
|
||||||
severity: "2".parse::<Severity>().unwrap(),
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
category: "4".parse::<Category>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
detail:1,
|
detail: 1,
|
||||||
},
|
},
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
vec!["me".to_string(),
|
||||||
).has_code(251));
|
"8BITMIME".to_string(),
|
||||||
|
"SIZE 42".to_string()])
|
||||||
|
.has_code(251));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_response_first_word() {
|
fn test_response_first_word() {
|
||||||
assert_eq!(Response::new(
|
assert_eq!(Response::new(Code {
|
||||||
Code {
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
severity: "2".parse::<Severity>().unwrap(),
|
category: "4".parse::<Category>().unwrap(),
|
||||||
category: "4".parse::<Category>().unwrap(),
|
detail: 1,
|
||||||
detail:1,
|
},
|
||||||
},
|
vec!["me".to_string(),
|
||||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
"8BITMIME".to_string(),
|
||||||
).first_word(), Some("me".to_string()));
|
"SIZE 42".to_string()])
|
||||||
assert_eq!(Response::new(
|
.first_word(),
|
||||||
Code {
|
Some("me".to_string()));
|
||||||
severity: "2".parse::<Severity>().unwrap(),
|
assert_eq!(Response::new(Code {
|
||||||
category: "4".parse::<Category>().unwrap(),
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
detail:1,
|
category: "4".parse::<Category>().unwrap(),
|
||||||
},
|
detail: 1,
|
||||||
vec!["me mo".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
},
|
||||||
).first_word(), Some("me".to_string()));
|
vec!["me mo".to_string(),
|
||||||
assert_eq!(Response::new(
|
"8BITMIME".to_string(),
|
||||||
Code {
|
"SIZE 42".to_string()])
|
||||||
severity: "2".parse::<Severity>().unwrap(),
|
.first_word(),
|
||||||
category: "4".parse::<Category>().unwrap(),
|
Some("me".to_string()));
|
||||||
detail:1,
|
assert_eq!(Response::new(Code {
|
||||||
},
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
vec![]
|
category: "4".parse::<Category>().unwrap(),
|
||||||
).first_word(), None);
|
detail: 1,
|
||||||
assert_eq!(Response::new(
|
},
|
||||||
Code {
|
vec![])
|
||||||
severity: "2".parse::<Severity>().unwrap(),
|
.first_word(),
|
||||||
category: "4".parse::<Category>().unwrap(),
|
None);
|
||||||
detail:1,
|
assert_eq!(Response::new(Code {
|
||||||
},
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
vec![" ".to_string()]
|
category: "4".parse::<Category>().unwrap(),
|
||||||
).first_word(), None);
|
detail: 1,
|
||||||
assert_eq!(Response::new(
|
},
|
||||||
Code {
|
vec![" ".to_string()])
|
||||||
severity: "2".parse::<Severity>().unwrap(),
|
.first_word(),
|
||||||
category: "4".parse::<Category>().unwrap(),
|
None);
|
||||||
detail:1,
|
assert_eq!(Response::new(Code {
|
||||||
},
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
vec![" ".to_string()]
|
category: "4".parse::<Category>().unwrap(),
|
||||||
).first_word(), None);
|
detail: 1,
|
||||||
assert_eq!(Response::new(
|
},
|
||||||
Code {
|
vec![" ".to_string()])
|
||||||
severity: "2".parse::<Severity>().unwrap(),
|
.first_word(),
|
||||||
category: "4".parse::<Category>().unwrap(),
|
None);
|
||||||
detail:1,
|
assert_eq!(Response::new(Code {
|
||||||
},
|
severity: "2".parse::<Severity>().unwrap(),
|
||||||
vec!["".to_string()]
|
category: "4".parse::<Category>().unwrap(),
|
||||||
).first_word(), None);
|
detail: 1,
|
||||||
|
},
|
||||||
|
vec!["".to_string()])
|
||||||
|
.first_word(),
|
||||||
|
None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
141
src/sender.rs
141
src/sender.rs
@@ -3,12 +3,13 @@
|
|||||||
use std::string::String;
|
use std::string::String;
|
||||||
use std::net::{SocketAddr, ToSocketAddrs};
|
use std::net::{SocketAddr, ToSocketAddrs};
|
||||||
|
|
||||||
|
use openssl::ssl::{SslMethod, SslContext};
|
||||||
|
|
||||||
use SMTP_PORT;
|
use SMTP_PORT;
|
||||||
use extension::{Extension, ServerInfo};
|
use extension::{Extension, ServerInfo};
|
||||||
use error::{SmtpResult, Error};
|
use error::{SmtpResult, Error};
|
||||||
use email::SendableEmail;
|
use email::SendableEmail;
|
||||||
use client::Client;
|
use client::Client;
|
||||||
use client::net::SmtpStream;
|
|
||||||
use authentication::Mecanism;
|
use authentication::Mecanism;
|
||||||
|
|
||||||
/// Contains client configuration
|
/// Contains client configuration
|
||||||
@@ -25,6 +26,8 @@ pub struct SenderBuilder {
|
|||||||
credentials: Option<(String, String)>,
|
credentials: Option<(String, String)>,
|
||||||
/// Socket we are connecting to
|
/// Socket we are connecting to
|
||||||
server_addr: SocketAddr,
|
server_addr: SocketAddr,
|
||||||
|
/// SSL contexyt to use
|
||||||
|
ssl_context: Option<SslContext>,
|
||||||
/// List of authentication mecanism, sorted by priority
|
/// List of authentication mecanism, sorted by priority
|
||||||
authentication_mecanisms: Vec<Mecanism>,
|
authentication_mecanisms: Vec<Mecanism>,
|
||||||
}
|
}
|
||||||
@@ -33,20 +36,20 @@ pub struct SenderBuilder {
|
|||||||
impl SenderBuilder {
|
impl SenderBuilder {
|
||||||
/// Creates a new local SMTP client
|
/// Creates a new local SMTP client
|
||||||
pub fn new<A: ToSocketAddrs>(addr: A) -> Result<SenderBuilder, Error> {
|
pub fn new<A: ToSocketAddrs>(addr: A) -> Result<SenderBuilder, Error> {
|
||||||
let mut addresses = try!(addr.to_socket_addrs());
|
let mut addresses = try!(addr.to_socket_addrs());
|
||||||
|
|
||||||
|
match addresses.next() {
|
||||||
match addresses.next() {
|
Some(addr) => Ok(SenderBuilder {
|
||||||
Some(addr) => Ok(SenderBuilder {
|
server_addr: addr,
|
||||||
server_addr: addr,
|
ssl_context: None,
|
||||||
credentials: None,
|
credentials: None,
|
||||||
connection_reuse_count_limit: 100,
|
connection_reuse_count_limit: 100,
|
||||||
enable_connection_reuse: false,
|
enable_connection_reuse: false,
|
||||||
hello_name: "localhost".to_string(),
|
hello_name: "localhost".to_string(),
|
||||||
authentication_mecanisms: vec![Mecanism::CramMd5, Mecanism::Plain],
|
authentication_mecanisms: vec![Mecanism::CramMd5, Mecanism::Plain],
|
||||||
}),
|
}),
|
||||||
None => Err(From::from("Could nor resolve hostname")),
|
None => Err(From::from("Could nor resolve hostname")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new local SMTP client to port 25
|
/// Creates a new local SMTP client to port 25
|
||||||
@@ -54,6 +57,17 @@ impl SenderBuilder {
|
|||||||
SenderBuilder::new(("localhost", SMTP_PORT))
|
SenderBuilder::new(("localhost", SMTP_PORT))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Use STARTTLS with a specific context
|
||||||
|
pub fn ssl_context(mut self, ssl_context: SslContext) -> SenderBuilder {
|
||||||
|
self.ssl_context = Some(ssl_context);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Require SSL/TLS using STARTTLS
|
||||||
|
pub fn starttls(self) -> SenderBuilder {
|
||||||
|
self.ssl_context(SslContext::new(SslMethod::Tlsv1).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the name used during HELO or EHLO
|
/// Set the name used during HELO or EHLO
|
||||||
pub fn hello_name(mut self, name: &str) -> SenderBuilder {
|
pub fn hello_name(mut self, name: &str) -> SenderBuilder {
|
||||||
self.hello_name = name.to_string();
|
self.hello_name = name.to_string();
|
||||||
@@ -111,7 +125,7 @@ pub struct Sender {
|
|||||||
/// Information about the client
|
/// Information about the client
|
||||||
client_info: SenderBuilder,
|
client_info: SenderBuilder,
|
||||||
/// Low level client
|
/// Low level client
|
||||||
client: Client<SmtpStream>,
|
client: Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! try_smtp (
|
macro_rules! try_smtp (
|
||||||
@@ -134,8 +148,10 @@ impl Sender {
|
|||||||
///
|
///
|
||||||
/// It does not connects to the server, but only creates the `Sender`
|
/// It does not connects to the server, but only creates the `Sender`
|
||||||
pub fn new(builder: SenderBuilder) -> Sender {
|
pub fn new(builder: SenderBuilder) -> Sender {
|
||||||
let client: Client<SmtpStream> = Client::new(builder.server_addr).unwrap();
|
|
||||||
Sender{
|
let client = Client::new();
|
||||||
|
|
||||||
|
Sender {
|
||||||
client: client,
|
client: client,
|
||||||
server_info: None,
|
server_info: None,
|
||||||
client_info: builder,
|
client_info: builder,
|
||||||
@@ -162,6 +178,19 @@ impl Sender {
|
|||||||
self.client.close();
|
self.client.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the EHLO response and updates server information
|
||||||
|
pub fn get_ehlo(&mut self) -> SmtpResult {
|
||||||
|
// Extended Hello
|
||||||
|
let ehlo_response = try_smtp!(self.client.ehlo(&self.client_info.hello_name), self);
|
||||||
|
|
||||||
|
self.server_info = Some(try_smtp!(ServerInfo::from_response(&ehlo_response), self));
|
||||||
|
|
||||||
|
// Print server information
|
||||||
|
debug!("server {}", self.server_info.as_ref().unwrap());
|
||||||
|
|
||||||
|
Ok(ehlo_response)
|
||||||
|
}
|
||||||
|
|
||||||
/// Sends an email
|
/// Sends an email
|
||||||
pub fn send<T: SendableEmail>(&mut self, email: T) -> SmtpResult {
|
pub fn send<T: SendableEmail>(&mut self, email: T) -> SmtpResult {
|
||||||
// Check if the connection is still available
|
// Check if the connection is still available
|
||||||
@@ -173,49 +202,39 @@ impl Sender {
|
|||||||
|
|
||||||
// If there is a usable connection, test if the server answers and hello has been sent
|
// If there is a usable connection, test if the server answers and hello has been sent
|
||||||
if self.state.connection_reuse_count == 0 {
|
if self.state.connection_reuse_count == 0 {
|
||||||
try!(self.client.connect());
|
try!(self.client.connect(&self.client_info.server_addr));
|
||||||
|
|
||||||
// Log the connection
|
// Log the connection
|
||||||
info!("connection established to {}", self.client_info.server_addr);
|
info!("connection established to {}", self.client_info.server_addr);
|
||||||
|
|
||||||
// Extended Hello or Hello if needed
|
try!(self.get_ehlo());
|
||||||
let hello_response = match self.client.ehlo(&self.client_info.hello_name) {
|
|
||||||
Ok(response) => response,
|
|
||||||
Err(error) => match error {
|
|
||||||
Error::PermanentError(ref response) if response.has_code(550) => {
|
|
||||||
match self.client.helo(&self.client_info.hello_name) {
|
|
||||||
Ok(response) => response,
|
|
||||||
Err(error) => try_smtp!(Err(error), self)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
try_smtp!(Err(error), self)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
self.server_info = Some(try_smtp!(ServerInfo::from_response(&hello_response), self));
|
if self.client_info.ssl_context.is_some() {
|
||||||
|
try_smtp!(self.client.starttls(), self);
|
||||||
|
|
||||||
// Print server information
|
try!(self.client
|
||||||
debug!("server {}", self.server_info.as_ref().unwrap());
|
.upgrade_tls_stream(self.client_info.ssl_context.as_ref().unwrap()));
|
||||||
}
|
|
||||||
|
|
||||||
if self.client_info.credentials.is_some() && self.state.connection_reuse_count == 0 {
|
try!(self.get_ehlo());
|
||||||
let (username, password) = self.client_info.credentials.clone().unwrap();
|
}
|
||||||
|
|
||||||
let mut found = false;
|
if self.client_info.credentials.is_some() && self.state.connection_reuse_count == 0 {
|
||||||
|
let (username, password) = self.client_info.credentials.clone().unwrap();
|
||||||
|
|
||||||
for mecanism in self.client_info.authentication_mecanisms.clone() {
|
let mut found = false;
|
||||||
if self.server_info.as_ref().unwrap().supports_auth_mecanism(mecanism) {
|
|
||||||
found = true;
|
|
||||||
let result = self.client.auth(mecanism, &username, &password);
|
|
||||||
try_smtp!(result, self);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
for mecanism in self.client_info.authentication_mecanisms.clone() {
|
||||||
debug!("No supported authentication mecanisms available");
|
if self.server_info.as_ref().unwrap().supports_auth_mecanism(mecanism) {
|
||||||
}
|
found = true;
|
||||||
|
let result = self.client.auth(mecanism, &username, &password);
|
||||||
|
try_smtp!(result, self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
debug!("No supported authentication mecanisms available");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_message = try!(email.message_id().ok_or("Missing Message-ID"));
|
let current_message = try!(email.message_id().ok_or("Missing Message-ID"));
|
||||||
@@ -224,7 +243,10 @@ impl Sender {
|
|||||||
let message = try!(email.message().ok_or("Missing message"));
|
let message = try!(email.message().ok_or("Missing message"));
|
||||||
|
|
||||||
// Mail
|
// Mail
|
||||||
let mail_options = match self.server_info.as_ref().unwrap().supports_feature(&Extension::EightBitMime) {
|
let mail_options = match self.server_info
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.supports_feature(&Extension::EightBitMime) {
|
||||||
true => Some("BODY=8BITMIME"),
|
true => Some("BODY=8BITMIME"),
|
||||||
false => None,
|
false => None,
|
||||||
};
|
};
|
||||||
@@ -252,15 +274,22 @@ impl Sender {
|
|||||||
self.state.connection_reuse_count = self.state.connection_reuse_count + 1;
|
self.state.connection_reuse_count = self.state.connection_reuse_count + 1;
|
||||||
|
|
||||||
// Log the message
|
// Log the message
|
||||||
info!("{}: conn_use={}, size={}, status=sent ({})", current_message,
|
info!("{}: conn_use={}, size={}, status=sent ({})",
|
||||||
self.state.connection_reuse_count, message.len(),
|
current_message,
|
||||||
result.as_ref().ok().unwrap().message().iter().next().unwrap_or(&"no response".to_string())
|
self.state.connection_reuse_count,
|
||||||
);
|
message.len(),
|
||||||
|
result.as_ref()
|
||||||
|
.ok()
|
||||||
|
.unwrap()
|
||||||
|
.message()
|
||||||
|
.iter()
|
||||||
|
.next()
|
||||||
|
.unwrap_or(&"no response".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test if we can reuse the existing connection
|
// Test if we can reuse the existing connection
|
||||||
if (!self.client_info.enable_connection_reuse) ||
|
if (!self.client_info.enable_connection_reuse) ||
|
||||||
(self.state.connection_reuse_count >= self.client_info.connection_reuse_count_limit) {
|
(self.state.connection_reuse_count >= self.client_info.connection_reuse_count_limit) {
|
||||||
self.reset();
|
self.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user