diff --git a/src/smtp.rs b/src/smtp.rs new file mode 100644 index 0000000..3d28b7c --- /dev/null +++ b/src/smtp.rs @@ -0,0 +1,323 @@ +// Copyright 2014 Alexis Mousset. See the COPYRIGHT +// file at the top-level directory of this distribution. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! SMTP commands and ESMTP features library + +use std::io::net::ip::Port; + +/// Default SMTP port +pub static SMTP_PORT: Port = 25; +//pub static SMTPS_PORT: Port = 465; +//pub static SUBMISSION_PORT: Port = 587; + +/// The word separator for SMTP transactions +pub static SP: &'static str = " "; + +/// The line ending for SMTP transactions +pub static CRLF: &'static str = "\r\n"; + +/// A module +pub mod smtp_command { + use std::fmt::{Show, Formatter, Result}; + + /// Supported SMTP commands + /// + /// We do not implement the following SMTP commands, as they were deprecated in RFC 5321 + /// and must not be used by clients: + /// SEND, SOML, SAML, TURN + #[deriving(Eq,Clone)] + pub enum SmtpCommand { + /// Extended Hello command + ExtendedHello(T), + /// Hello command + Hello(T), + /// Mail command, takes optionnal options + Mail(T, Option>), + /// Recipient command, takes optionnal options + Recipient(T, Option>), + /// Data command + Data, + /// Reset command + Reset, + /// Verify command + Verify(T), + /// Expand command + Expand(T), + /// Help command, takes optionnal options + Help(Option), + /// Noop command + Noop, + /// Quit command + Quit, + + } + + impl Show for SmtpCommand { + fn fmt(&self, f: &mut Formatter) -> Result { + f.buf.write(match *self { + ExtendedHello(ref my_hostname) => + format!("EHLO {}", my_hostname.clone()), + Hello(ref my_hostname) => + format!("HELO {}", my_hostname.clone()), + Mail(ref from_address, None) => + format!("MAIL FROM:<{}>", from_address.clone()), + Mail(ref from_address, Some(ref options)) => + format!("MAIL FROM:<{}> {}", from_address.clone(), options.connect(" ")), + Recipient(ref to_address, None) => + format!("RCPT TO:<{}>", to_address.clone()), + Recipient(ref to_address, Some(ref options)) => + format!("RCPT TO:<{}> {}", to_address.clone(), options.connect(" ")), + Data => "DATA".to_owned(), + Reset => "RSET".to_owned(), + Verify(ref address) => + format!("VRFY {}", address.clone()), + Expand(ref address) => + format!("EXPN {}", address.clone()), + Help(None) => "HELP".to_owned(), + Help(Some(ref argument)) => + format!("HELP {}", argument.clone()), + Noop => "NOOP".to_owned(), + Quit => "QUIT".to_owned(), + }.as_bytes()) + } + } +} + +/// This is a module +pub mod esmtp_parameter { + use std::from_str::FromStr; + use std::fmt::{Show, Formatter, Result}; + + /// Supported ESMTP keywords + #[deriving(Eq,Clone)] + pub enum EsmtpParameter { + /// 8BITMIME keyword + /// + /// RFC 6152 : https://tools.ietf.org/html/rfc6152 + EightBitMime, + /// SIZE keyword + /// + /// RFC 1427 : https://tools.ietf.org/html/rfc1427 + Size(uint) + } + + impl Show for EsmtpParameter { + fn fmt(&self, f: &mut Formatter) -> Result { + f.buf.write( + match self { + &EightBitMime => "8BITMIME".to_owned(), + &Size(ref size) => format!("SIZE={}", size) + }.as_bytes() + ) + } + } + + impl FromStr for EsmtpParameter { + fn from_str(s: &str) -> Option { + let splitted : Vec<&str> = s.splitn(' ', 1).collect(); + match splitted.len() { + 1 => match *splitted.get(0) { + "8BITMIME" => Some(EightBitMime), + _ => None + }, + 2 => match (*splitted.get(0), from_str::(*splitted.get(1))) { + ("SIZE", Some(size)) => Some(Size(size)), + _ => None + }, + _ => None + } + } + } + + impl EsmtpParameter { + /// Checks if the ESMTP keyword is the same + pub fn same_keyword_as(&self, other: EsmtpParameter) -> bool { + if *self == other { + return true; + } + match (*self, other) { + (Size(_), Size(_)) => true, + _ => false + } + } + } +} +/// This is a module +pub mod smtp_response { + use std::from_str::FromStr; + use std::fmt::{Show, Formatter, Result}; + use common::remove_trailing_crlf; + use std::result; + + /// Contains an SMTP reply, with separed code and message + /// + /// We do accept messages containing only a code, to comply with RFC5321 + #[deriving(Clone, Eq)] + pub struct SmtpResponse { + /// Server response code + pub code: u16, + /// Server response string + pub message: Option + } + + impl Show for SmtpResponse { + fn fmt(&self, f: &mut Formatter) -> Result { + f.buf.write( + match self.clone().message { + Some(message) => format!("{} {}", self.code.to_str(), message), + None => self.code.to_str() + }.as_bytes() + ) + } + } + + // FromStr ? + impl FromStr for SmtpResponse { + fn from_str(s: &str) -> Option> { + // If the string is too short to be a response code + if s.len() < 3 { + None + // If we have only a code, with or without a trailing space + } else if s.len() == 3 || (s.len() == 4 && s.slice(3,4) == " ") { + match from_str::(s.slice_to(3)) { + Some(code) => Some(SmtpResponse{ + code: code, + message: None + }), + None => None + + } + // If we have a code and a message + } else { + match ( + from_str::(s.slice_to(3)), + vec!(" ", "-").contains(&s.slice(3,4)), + StrBuf::from_str(remove_trailing_crlf(s.slice_from(4).to_owned())) + ) { + (Some(code), true, message) => Some(SmtpResponse{ + code: code, + message: Some(message) + }), + _ => None + + } + } + } + } + + impl SmtpResponse { + /// Checks the presence of the response code in the array of expected codes. + pub fn with_code(&self, expected_codes: Vec) -> result::Result,SmtpResponse> { + let response = self.clone(); + if expected_codes.contains(&self.code) { + Ok(response) + } else { + Err(response) + } + } + } +} + +#[cfg(test)] +mod test { + use super::smtp_response::SmtpResponse; + use super::esmtp_parameter; + use super::esmtp_parameter::EsmtpParameter; + use super::smtp_command; + use super::smtp_command::SmtpCommand; + + #[test] + fn test_command_fmt() { + let noop: SmtpCommand = smtp_command::Noop; + assert_eq!(format!("{}", noop), "NOOP".to_owned()); + assert_eq!(format!("{}", smtp_command::ExtendedHello("me")), "EHLO me".to_owned()); + assert_eq!(format!("{}", + smtp_command::Mail("test", Some(vec!("option")))), "MAIL FROM: option".to_owned() + ); + } + + #[test] + fn test_esmtp_parameter_same_keyword_as() { + assert_eq!(esmtp_parameter::EightBitMime.same_keyword_as(esmtp_parameter::EightBitMime), true); + assert_eq!(esmtp_parameter::Size(42).same_keyword_as(esmtp_parameter::Size(42)), true); + assert_eq!(esmtp_parameter::Size(42).same_keyword_as(esmtp_parameter::Size(43)), true); + assert_eq!(esmtp_parameter::Size(42).same_keyword_as(esmtp_parameter::EightBitMime), false); + } + + #[test] + fn test_esmtp_parameter_fmt() { + assert_eq!(format!("{}", esmtp_parameter::EightBitMime), "8BITMIME".to_owned()); + assert_eq!(format!("{}", esmtp_parameter::Size(42)), "SIZE=42".to_owned()); + } + + #[test] + fn test_esmtp_parameter_from_str() { + assert_eq!(from_str::("8BITMIME"), Some(esmtp_parameter::EightBitMime)); + assert_eq!(from_str::("SIZE 42"), Some(esmtp_parameter::Size(42))); + assert_eq!(from_str::("SIZ 42"), None); + assert_eq!(from_str::("SIZE 4a2"), None); + // TODO: accept trailing spaces ? + assert_eq!(from_str::("SIZE 42 "), None); + } + + #[test] + fn test_smtp_response_fmt() { + assert_eq!(format!("{}", SmtpResponse{code: 200, message: Some("message")}), "200 message".to_owned()); + } + + #[test] + fn test_smtp_response_from_str() { + assert_eq!(from_str::>("200 response message"), + Some(SmtpResponse{ + code: 200, + message: Some(StrBuf::from_str("response message")) + }) + ); + assert_eq!(from_str::>("200-response message"), + Some(SmtpResponse{ + code: 200, + message: Some(StrBuf::from_str("response message")) + }) + ); + assert_eq!(from_str::>("200"), + Some(SmtpResponse{ + code: 200, + message: None + }) + ); + assert_eq!(from_str::>("200 "), + Some(SmtpResponse{ + code: 200, + message: None + }) + ); + assert_eq!(from_str::>("200-response\r\nmessage"), + Some(SmtpResponse{ + code: 200, + message: Some(StrBuf::from_str("response\r\nmessage")) + }) + ); + assert_eq!(from_str::>("2000response message"), None); + assert_eq!(from_str::>("20a response message"), None); + assert_eq!(from_str::>("20 "), None); + assert_eq!(from_str::>("20"), None); + assert_eq!(from_str::>("2"), None); + assert_eq!(from_str::>(""), None); + } + + #[test] + fn test_smtp_response_with_code() { + assert_eq!(SmtpResponse{code: 200, message: Some("message")}.with_code(vec!(200)), + Ok(SmtpResponse{code: 200, message: Some("message")})); + assert_eq!(SmtpResponse{code: 400, message: Some("message")}.with_code(vec!(200)), + Err(SmtpResponse{code: 400, message: Some("message")})); + assert_eq!(SmtpResponse{code: 200, message: Some("message")}.with_code(vec!(200, 300)), + Ok(SmtpResponse{code: 200, message: Some("message")})); + } +}