use crate::smtp::authentication::Mechanism;
use crate::smtp::error::Error;
use crate::smtp::response::Response;
use crate::smtp::util::XText;
use hostname::get as get_hostname;
use std::collections::HashSet;
use std::fmt::{self, Display, Formatter};
use std::net::{Ipv4Addr, Ipv6Addr};
use std::result::Result;
const DEFAULT_DOMAIN_CLIENT_ID: &str = "localhost";
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(
feature = "serde-impls",
derive(serde_derive::Serialize, serde_derive::Deserialize)
)]
pub enum ClientId {
Domain(String),
Ipv4(Ipv4Addr),
Ipv6(Ipv6Addr),
}
impl Display for ClientId {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
ClientId::Domain(ref value) => f.write_str(value),
ClientId::Ipv4(ref value) => write!(f, "{}", value),
ClientId::Ipv6(ref value) => write!(f, "{}", value),
}
}
}
impl ClientId {
pub fn new(domain: String) -> ClientId {
ClientId::Domain(domain)
}
pub fn hostname() -> ClientId {
ClientId::Domain(
get_hostname()
.map(|s| {
s.into_string()
.unwrap_or_else(|_| DEFAULT_DOMAIN_CLIENT_ID.to_string())
})
.unwrap_or_else(|_| DEFAULT_DOMAIN_CLIENT_ID.to_string()),
)
}
}
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
#[cfg_attr(
feature = "serde-impls",
derive(serde_derive::Serialize, serde_derive::Deserialize)
)]
pub enum Extension {
EightBitMime,
SmtpUtfEight,
StartTls,
Authentication(Mechanism),
}
impl Display for Extension {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
Extension::EightBitMime => write!(f, "8BITMIME"),
Extension::SmtpUtfEight => write!(f, "SMTPUTF8"),
Extension::StartTls => write!(f, "STARTTLS"),
Extension::Authentication(ref mechanism) => write!(f, "AUTH {}", mechanism),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[cfg_attr(
feature = "serde-impls",
derive(serde_derive::Serialize, serde_derive::Deserialize)
)]
pub struct ServerInfo {
pub name: String,
pub features: HashSet<Extension>,
}
impl Display for ServerInfo {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
"{} with {}",
self.name,
if self.features.is_empty() {
"no supported features".to_string()
} else {
format!("{:?}", self.features)
}
)
}
}
impl ServerInfo {
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
let name = match response.first_word() {
Some(name) => name,
None => return Err(Error::ResponseParsing("Could not read server name")),
};
let mut features: HashSet<Extension> = HashSet::new();
for line in response.message.as_slice() {
if line.is_empty() {
continue;
}
let split: Vec<&str> = line.split_whitespace().collect();
match split[0] {
"8BITMIME" => {
features.insert(Extension::EightBitMime);
}
"SMTPUTF8" => {
features.insert(Extension::SmtpUtfEight);
}
"STARTTLS" => {
features.insert(Extension::StartTls);
}
"AUTH" => {
for &mechanism in &split[1..] {
match mechanism {
"PLAIN" => {
features.insert(Extension::Authentication(Mechanism::Plain));
}
"LOGIN" => {
features.insert(Extension::Authentication(Mechanism::Login));
}
"XOAUTH2" => {
features.insert(Extension::Authentication(Mechanism::Xoauth2));
}
_ => (),
}
}
}
_ => (),
};
}
Ok(ServerInfo {
name: name.to_string(),
features,
})
}
pub fn supports_feature(&self, keyword: Extension) -> bool {
self.features.contains(&keyword)
}
pub fn supports_auth_mechanism(&self, mechanism: Mechanism) -> bool {
self.features
.contains(&Extension::Authentication(mechanism))
}
}
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(
feature = "serde-impls",
derive(serde_derive::Serialize, serde_derive::Deserialize)
)]
pub enum MailParameter {
Body(MailBodyParameter),
Size(usize),
SmtpUtfEight,
Other {
keyword: String,
value: Option<String>,
},
}
impl Display for MailParameter {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
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),
}
}
}
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
#[cfg_attr(
feature = "serde-impls",
derive(serde_derive::Serialize, serde_derive::Deserialize)
)]
pub enum MailBodyParameter {
SevenBit,
EightBitMime,
}
impl Display for MailBodyParameter {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match *self {
MailBodyParameter::SevenBit => f.write_str("7BIT"),
MailBodyParameter::EightBitMime => f.write_str("8BITMIME"),
}
}
}
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(
feature = "serde-impls",
derive(serde_derive::Serialize, serde_derive::Deserialize)
)]
pub enum RcptParameter {
Other {
keyword: String,
value: Option<String>,
},
}
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),
}
}
}
#[cfg(test)]
mod test {
use super::{ClientId, Extension, ServerInfo};
use crate::smtp::authentication::Mechanism;
use crate::smtp::response::{Category, Code, Detail, Response, Severity};
use std::collections::HashSet;
#[test]
fn test_clientid_fmt() {
assert_eq!(
format!("{}", ClientId::new("test".to_string())),
"test".to_string()
);
}
#[test]
fn test_extension_fmt() {
assert_eq!(
format!("{}", Extension::EightBitMime),
"8BITMIME".to_string()
);
assert_eq!(
format!("{}", Extension::Authentication(Mechanism::Plain)),
"AUTH PLAIN".to_string()
);
}
#[test]
fn test_serverinfo_fmt() {
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()
);
let empty = HashSet::new();
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()
);
}
#[test]
fn test_serverinfo() {
let response = Response::new(
Code::new(
Severity::PositiveCompletion,
Category::Unspecified4,
Detail::One,
),
vec![
"me".to_string(),
"8BITMIME".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,
};
assert_eq!(ServerInfo::from_response(&response).unwrap(), server_info);
assert!(server_info.supports_feature(Extension::EightBitMime));
assert!(!server_info.supports_feature(Extension::StartTls));
let response2 = Response::new(
Code::new(
Severity::PositiveCompletion,
Category::Unspecified4,
Detail::One,
),
vec![
"me".to_string(),
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_string(),
"8BITMIME".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::Xoauth2),));
let server_info2 = ServerInfo {
name: "me".to_string(),
features: features2,
};
assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2);
assert!(server_info2.supports_feature(Extension::EightBitMime));
assert!(server_info2.supports_auth_mechanism(Mechanism::Plain));
assert!(!server_info2.supports_feature(Extension::StartTls));
}
}