255 lines
7.4 KiB
Rust
255 lines
7.4 KiB
Rust
//! SMTP client
|
|
|
|
use std::string::String;
|
|
use std::net::{SocketAddr, ToSocketAddrs};
|
|
use std::io::{BufRead, Read, Write};
|
|
|
|
use bufstream::BufStream;
|
|
|
|
use response::ResponseParser;
|
|
use authentication::Mecanism;
|
|
use error::{Error, SmtpResult};
|
|
use client::net::{Connector, SmtpStream};
|
|
use {CRLF, MESSAGE_ENDING};
|
|
|
|
pub mod net;
|
|
|
|
/// Returns the string after adding a dot at the beginning of each line starting with a dot
|
|
///
|
|
/// Reference : https://tools.ietf.org/html/rfc5321#page-62 (4.5.2. Transparency)
|
|
#[inline]
|
|
fn escape_dot(string: &str) -> String {
|
|
if string.starts_with(".") {
|
|
format!(".{}", string)
|
|
} else {
|
|
string.to_string()
|
|
}.replace("\r.", "\r..")
|
|
.replace("\n.", "\n..")
|
|
}
|
|
|
|
/// Returns the string replacing all the CRLF with "\<CRLF\>"
|
|
#[inline]
|
|
fn escape_crlf(string: &str) -> String {
|
|
string.replace(CRLF, "<CR><LF>")
|
|
}
|
|
|
|
/// Returns the string removing all the CRLF
|
|
#[inline]
|
|
fn remove_crlf(string: &str) -> String {
|
|
string.replace(CRLF, "")
|
|
}
|
|
|
|
/// Structure that implements the SMTP client
|
|
pub struct Client<S: Write + Read = SmtpStream> {
|
|
/// TCP stream between client and server
|
|
/// Value is None before connection
|
|
stream: Option<BufStream<S>>,
|
|
/// Socket we are connecting to
|
|
server_addr: SocketAddr,
|
|
}
|
|
|
|
macro_rules! return_err (
|
|
($err: expr, $client: ident) => ({
|
|
return Err(From::from($err))
|
|
})
|
|
);
|
|
|
|
impl<S: Write + Read = SmtpStream> Client<S> {
|
|
/// Creates a new SMTP client
|
|
///
|
|
/// It does not connects to the server, but only creates the `Client`
|
|
pub fn new<A: ToSocketAddrs>(addr: A) -> Result<Client<S>, Error> {
|
|
let mut addresses = try!(addr.to_socket_addrs());
|
|
|
|
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> {
|
|
/// Closes the SMTP transaction if possible
|
|
pub fn close(&mut self) {
|
|
let _ = self.quit();
|
|
self.stream = None;
|
|
}
|
|
|
|
/// Connects to the configured server
|
|
pub fn connect(&mut self) -> 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);
|
|
}
|
|
|
|
// Try to connect
|
|
self.stream = Some(BufStream::new(try!(Connector::connect(&self.server_addr))));
|
|
|
|
self.get_reply()
|
|
}
|
|
|
|
/// Checks if the server is connected using the NOOP SMTP command
|
|
pub fn is_connected(&mut self) -> bool {
|
|
self.noop().is_ok()
|
|
}
|
|
|
|
/// Sends an SMTP command
|
|
pub fn command(&mut self, command: &str) -> SmtpResult {
|
|
self.send_server(command, CRLF)
|
|
}
|
|
|
|
/// Send a HELO command and fills `server_info`
|
|
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 {
|
|
self.command(&format!("EHLO {}", hostname))
|
|
}
|
|
|
|
/// Sends a MAIL command
|
|
pub fn mail(&mut self, address: &str, options: Option<&str>) -> SmtpResult {
|
|
match options {
|
|
Some(ref options) => self.command(&format!("MAIL FROM:<{}> {}", address, options)),
|
|
None => self.command(&format!("MAIL FROM:<{}>", address)),
|
|
}
|
|
}
|
|
|
|
/// Sends a RCPT command
|
|
pub fn rcpt(&mut self, address: &str) -> SmtpResult {
|
|
self.command(&format!("RCPT TO:<{}>", address))
|
|
}
|
|
|
|
/// Sends a DATA command
|
|
pub fn data(&mut self) -> SmtpResult {
|
|
self.command("DATA")
|
|
}
|
|
|
|
/// Sends a QUIT command
|
|
pub fn quit(&mut self) -> SmtpResult {
|
|
self.command("QUIT")
|
|
}
|
|
|
|
/// Sends a NOOP command
|
|
pub fn noop(&mut self) -> SmtpResult {
|
|
self.command("NOOP")
|
|
}
|
|
|
|
/// Sends a HELP command
|
|
pub fn help(&mut self, argument: Option<&str>) -> SmtpResult {
|
|
match argument {
|
|
Some(ref argument) => self.command(&format!("HELP {}", argument)),
|
|
None => self.command("HELP"),
|
|
}
|
|
}
|
|
|
|
/// Sends a VRFY command
|
|
pub fn vrfy(&mut self, address: &str) -> SmtpResult {
|
|
self.command(&format!("VRFY {}", address))
|
|
}
|
|
|
|
/// Sends a EXPN command
|
|
pub fn expn(&mut self, address: &str) -> SmtpResult {
|
|
self.command(&format!("EXPN {}", address))
|
|
}
|
|
|
|
/// Sends a RSET command
|
|
pub fn rset(&mut self) -> SmtpResult {
|
|
self.command("RSET")
|
|
}
|
|
|
|
/// Sends an AUTH command with the given mecanism
|
|
pub fn auth(&mut self, mecanism: Mecanism, username: &str, password: &str) -> SmtpResult {
|
|
|
|
if mecanism.supports_initial_response() {
|
|
self.command(&format!("AUTH {} {}", mecanism, try!(mecanism.response(username, password, None))))
|
|
} else {
|
|
let encoded_challenge = match try!(self.command("AUTH CRAM-MD5")).first_word() {
|
|
Some(challenge) => challenge,
|
|
None => return Err(Error::ResponseParsingError("Could not read CRAM challenge")),
|
|
};
|
|
|
|
let cram_response = try!(mecanism.response(username, password, Some(&encoded_challenge)));
|
|
|
|
self.command(&format!("AUTH CRAM-MD5 {}", cram_response))
|
|
}
|
|
}
|
|
|
|
/// Sends the message content
|
|
pub fn message(&mut self, message_content: &str) -> SmtpResult {
|
|
self.send_server(&escape_dot(message_content), MESSAGE_ENDING)
|
|
}
|
|
|
|
/// Sends a string to the server and gets the response
|
|
fn send_server(&mut self, string: &str, end: &str) -> SmtpResult {
|
|
if self.stream.is_none() {
|
|
return Err(From::from("Connection closed"));
|
|
}
|
|
|
|
try!(write!(self.stream.as_mut().unwrap(), "{}{}", string, end));
|
|
try!(self.stream.as_mut().unwrap().flush());
|
|
|
|
debug!("Wrote: {}", escape_crlf(string));
|
|
|
|
self.get_reply()
|
|
}
|
|
|
|
/// Gets the SMTP response
|
|
fn get_reply(&mut self) -> SmtpResult {
|
|
|
|
let mut parser = ResponseParser::new();
|
|
|
|
let mut line = String::new();
|
|
try!(self.stream.as_mut().unwrap().read_line(&mut line));
|
|
|
|
while try!(parser.read_line(remove_crlf(line.as_ref()).as_ref())) {
|
|
line.clear();
|
|
try!(self.stream.as_mut().unwrap().read_line(&mut line));
|
|
}
|
|
|
|
let response = try!(parser.response());
|
|
|
|
match response.is_positive() {
|
|
true => Ok(response),
|
|
false => Err(From::from(response)),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::{escape_dot, remove_crlf, escape_crlf};
|
|
|
|
#[test]
|
|
fn test_escape_dot() {
|
|
assert_eq!(escape_dot(".test"), "..test");
|
|
assert_eq!(escape_dot("\r.\n.\r\n"), "\r..\n..\r\n");
|
|
assert_eq!(escape_dot("test\r\n.test\r\n"), "test\r\n..test\r\n");
|
|
assert_eq!(escape_dot("test\r\n.\r\ntest"), "test\r\n..\r\ntest");
|
|
}
|
|
|
|
#[test]
|
|
fn test_remove_crlf() {
|
|
assert_eq!(remove_crlf("\r\n"), "");
|
|
assert_eq!(remove_crlf("EHLO my_name\r\n"), "EHLO my_name");
|
|
assert_eq!(
|
|
remove_crlf("EHLO my_name\r\nSIZE 42\r\n"),
|
|
"EHLO my_nameSIZE 42"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_escape_crlf() {
|
|
assert_eq!(escape_crlf("\r\n"), "<CR><LF>");
|
|
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CR><LF>");
|
|
assert_eq!(
|
|
escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
|
|
"EHLO my_name<CR><LF>SIZE 42<CR><LF>"
|
|
);
|
|
}
|
|
}
|