1 //! ESMTP features 2 3 use crate::transport::smtp::{ 4 authentication::Mechanism, 5 error::{self, Error}, 6 response::Response, 7 util::XText, 8 }; 9 use std::{ 10 collections::HashSet, 11 fmt::{self, Display, Formatter}, 12 net::{Ipv4Addr, Ipv6Addr}, 13 result::Result, 14 }; 15 16 /// Client identifier, the parameter to `EHLO` 17 #[derive(PartialEq, Eq, Clone, Debug)] 18 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 19 #[non_exhaustive] 20 pub enum ClientId { 21 /// A fully-qualified domain name 22 Domain(String), 23 /// An IPv4 address 24 Ipv4(Ipv4Addr), 25 /// An IPv6 address 26 Ipv6(Ipv6Addr), 27 } 28 29 const LOCALHOST_CLIENT: ClientId = ClientId::Ipv4(Ipv4Addr::new(127, 0, 0, 1)); 30 31 impl Default for ClientId { default() -> Self32 fn default() -> Self { 33 // https://tools.ietf.org/html/rfc5321#section-4.1.4 34 // 35 // The SMTP client MUST, if possible, ensure that the domain parameter 36 // to the EHLO command is a primary host name as specified for this 37 // command in Section 2.3.5. If this is not possible (e.g., when the 38 // client's address is dynamically assigned and the client does not have 39 // an obvious name), an address literal SHOULD be substituted for the 40 // domain name. 41 #[cfg(feature = "hostname")] 42 { 43 hostname::get() 44 .ok() 45 .and_then(|s| s.into_string().map(Self::Domain).ok()) 46 .unwrap_or(LOCALHOST_CLIENT) 47 } 48 #[cfg(not(feature = "hostname"))] 49 LOCALHOST_CLIENT 50 } 51 } 52 53 impl Display for ClientId { fmt(&self, f: &mut Formatter<'_>) -> fmt::Result54 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 55 match *self { 56 Self::Domain(ref value) => f.write_str(value), 57 Self::Ipv4(ref value) => write!(f, "[{}]", value), 58 Self::Ipv6(ref value) => write!(f, "[IPv6:{}]", value), 59 } 60 } 61 } 62 63 impl ClientId { 64 #[doc(hidden)] 65 #[deprecated(since = "0.10.0", note = "Please use ClientId::Domain(domain) instead")] 66 /// Creates a new `ClientId` from a fully qualified domain name new(domain: String) -> Self67 pub fn new(domain: String) -> Self { 68 Self::Domain(domain) 69 } 70 } 71 72 /// Supported ESMTP keywords 73 #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)] 74 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 75 #[non_exhaustive] 76 pub enum Extension { 77 /// 8BITMIME keyword 78 /// 79 /// Defined in [RFC 6152](https://tools.ietf.org/html/rfc6152) 80 EightBitMime, 81 /// SMTPUTF8 keyword 82 /// 83 /// Defined in [RFC 6531](https://tools.ietf.org/html/rfc6531) 84 SmtpUtfEight, 85 /// STARTTLS keyword 86 /// 87 /// Defined in [RFC 2487](https://tools.ietf.org/html/rfc2487) 88 StartTls, 89 /// AUTH mechanism 90 Authentication(Mechanism), 91 } 92 93 impl Display for Extension { fmt(&self, f: &mut Formatter<'_>) -> fmt::Result94 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 95 match *self { 96 Extension::EightBitMime => f.write_str("8BITMIME"), 97 Extension::SmtpUtfEight => f.write_str("SMTPUTF8"), 98 Extension::StartTls => f.write_str("STARTTLS"), 99 Extension::Authentication(ref mechanism) => write!(f, "AUTH {}", mechanism), 100 } 101 } 102 } 103 104 /// Contains information about an SMTP server 105 #[derive(Clone, Debug, Eq, PartialEq, Default)] 106 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 107 pub struct ServerInfo { 108 /// Server name 109 /// 110 /// The name given in the server banner 111 name: String, 112 /// ESMTP features supported by the server 113 /// 114 /// It contains the features supported by the server and known by the `Extension` module. 115 features: HashSet<Extension>, 116 } 117 118 impl Display for ServerInfo { fmt(&self, f: &mut Formatter<'_>) -> fmt::Result119 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 120 let features = if self.features.is_empty() { 121 "no supported features".to_string() 122 } else { 123 format!("{:?}", self.features) 124 }; 125 write!(f, "{} with {}", self.name, features) 126 } 127 } 128 129 impl ServerInfo { 130 /// Parses a EHLO response to create a `ServerInfo` from_response(response: &Response) -> Result<ServerInfo, Error>131 pub fn from_response(response: &Response) -> Result<ServerInfo, Error> { 132 let name = match response.first_word() { 133 Some(name) => name, 134 None => return Err(error::response("Could not read server name")), 135 }; 136 137 let mut features: HashSet<Extension> = HashSet::new(); 138 139 for line in response.message() { 140 if line.is_empty() { 141 continue; 142 } 143 144 let mut split = line.split_whitespace(); 145 match split.next().unwrap() { 146 "8BITMIME" => { 147 features.insert(Extension::EightBitMime); 148 } 149 "SMTPUTF8" => { 150 features.insert(Extension::SmtpUtfEight); 151 } 152 "STARTTLS" => { 153 features.insert(Extension::StartTls); 154 } 155 "AUTH" => { 156 for mechanism in split { 157 match mechanism { 158 "PLAIN" => { 159 features.insert(Extension::Authentication(Mechanism::Plain)); 160 } 161 "LOGIN" => { 162 features.insert(Extension::Authentication(Mechanism::Login)); 163 } 164 "XOAUTH2" => { 165 features.insert(Extension::Authentication(Mechanism::Xoauth2)); 166 } 167 _ => (), 168 } 169 } 170 } 171 _ => (), 172 }; 173 } 174 175 Ok(ServerInfo { 176 name: name.to_string(), 177 features, 178 }) 179 } 180 181 /// Checks if the server supports an ESMTP feature supports_feature(&self, keyword: Extension) -> bool182 pub fn supports_feature(&self, keyword: Extension) -> bool { 183 self.features.contains(&keyword) 184 } 185 186 /// Checks if the server supports an ESMTP feature supports_auth_mechanism(&self, mechanism: Mechanism) -> bool187 pub fn supports_auth_mechanism(&self, mechanism: Mechanism) -> bool { 188 self.features 189 .contains(&Extension::Authentication(mechanism)) 190 } 191 192 /// Gets a compatible mechanism from list get_auth_mechanism(&self, mechanisms: &[Mechanism]) -> Option<Mechanism>193 pub fn get_auth_mechanism(&self, mechanisms: &[Mechanism]) -> Option<Mechanism> { 194 for mechanism in mechanisms { 195 if self.supports_auth_mechanism(*mechanism) { 196 return Some(*mechanism); 197 } 198 } 199 None 200 } 201 202 /// The name given in the server banner name(&self) -> &str203 pub fn name(&self) -> &str { 204 self.name.as_ref() 205 } 206 } 207 208 /// A `MAIL FROM` extension parameter 209 #[derive(PartialEq, Eq, Clone, Debug)] 210 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 211 pub enum MailParameter { 212 /// `BODY` parameter 213 Body(MailBodyParameter), 214 /// `SIZE` parameter 215 Size(usize), 216 /// `SMTPUTF8` parameter 217 SmtpUtfEight, 218 /// Custom parameter 219 Other { 220 /// Parameter keyword 221 keyword: String, 222 /// Parameter value 223 value: Option<String>, 224 }, 225 } 226 227 impl Display for MailParameter { fmt(&self, f: &mut Formatter<'_>) -> fmt::Result228 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 229 match *self { 230 MailParameter::Body(ref value) => write!(f, "BODY={}", value), 231 MailParameter::Size(size) => write!(f, "SIZE={}", size), 232 MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"), 233 MailParameter::Other { 234 ref keyword, 235 value: Some(ref value), 236 } => write!(f, "{}={}", keyword, XText(value)), 237 MailParameter::Other { 238 ref keyword, 239 value: None, 240 } => f.write_str(keyword), 241 } 242 } 243 } 244 245 /// Values for the `BODY` parameter to `MAIL FROM` 246 #[derive(PartialEq, Eq, Clone, Debug, Copy)] 247 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 248 pub enum MailBodyParameter { 249 /// `7BIT` 250 SevenBit, 251 /// `8BITMIME` 252 EightBitMime, 253 } 254 255 impl Display for MailBodyParameter { fmt(&self, f: &mut Formatter<'_>) -> fmt::Result256 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 257 match *self { 258 MailBodyParameter::SevenBit => f.write_str("7BIT"), 259 MailBodyParameter::EightBitMime => f.write_str("8BITMIME"), 260 } 261 } 262 } 263 264 /// A `RCPT TO` extension parameter 265 #[derive(PartialEq, Eq, Clone, Debug)] 266 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 267 pub enum RcptParameter { 268 /// Custom parameter 269 Other { 270 /// Parameter keyword 271 keyword: String, 272 /// Parameter value 273 value: Option<String>, 274 }, 275 } 276 277 impl Display for RcptParameter { fmt(&self, f: &mut Formatter<'_>) -> fmt::Result278 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 279 match *self { 280 RcptParameter::Other { 281 ref keyword, 282 value: Some(ref value), 283 } => write!(f, "{}={}", keyword, XText(value)), 284 RcptParameter::Other { 285 ref keyword, 286 value: None, 287 } => f.write_str(keyword), 288 } 289 } 290 } 291 292 #[cfg(test)] 293 mod test { 294 295 use super::*; 296 use crate::transport::smtp::{ 297 authentication::Mechanism, 298 response::{Category, Code, Detail, Response, Severity}, 299 }; 300 use std::collections::HashSet; 301 302 #[test] test_clientid_fmt()303 fn test_clientid_fmt() { 304 assert_eq!( 305 format!("{}", ClientId::Domain("test".to_string())), 306 "test".to_string() 307 ); 308 assert_eq!(format!("{}", LOCALHOST_CLIENT), "[127.0.0.1]".to_string()); 309 } 310 311 #[test] test_extension_fmt()312 fn test_extension_fmt() { 313 assert_eq!( 314 format!("{}", Extension::EightBitMime), 315 "8BITMIME".to_string() 316 ); 317 assert_eq!( 318 format!("{}", Extension::Authentication(Mechanism::Plain)), 319 "AUTH PLAIN".to_string() 320 ); 321 } 322 323 #[test] test_serverinfo_fmt()324 fn test_serverinfo_fmt() { 325 let mut eightbitmime = HashSet::new(); 326 assert!(eightbitmime.insert(Extension::EightBitMime)); 327 328 assert_eq!( 329 format!( 330 "{}", 331 ServerInfo { 332 name: "name".to_string(), 333 features: eightbitmime, 334 } 335 ), 336 "name with {EightBitMime}".to_string() 337 ); 338 339 let empty = HashSet::new(); 340 341 assert_eq!( 342 format!( 343 "{}", 344 ServerInfo { 345 name: "name".to_string(), 346 features: empty, 347 } 348 ), 349 "name with no supported features".to_string() 350 ); 351 352 let mut plain = HashSet::new(); 353 assert!(plain.insert(Extension::Authentication(Mechanism::Plain))); 354 355 assert_eq!( 356 format!( 357 "{}", 358 ServerInfo { 359 name: "name".to_string(), 360 features: plain, 361 } 362 ), 363 "name with {Authentication(Plain)}".to_string() 364 ); 365 } 366 367 #[test] test_serverinfo()368 fn test_serverinfo() { 369 let response = Response::new( 370 Code::new( 371 Severity::PositiveCompletion, 372 Category::Unspecified4, 373 Detail::One, 374 ), 375 vec![ 376 "me".to_string(), 377 "8BITMIME".to_string(), 378 "SIZE 42".to_string(), 379 ], 380 ); 381 382 let mut features = HashSet::new(); 383 assert!(features.insert(Extension::EightBitMime)); 384 385 let server_info = ServerInfo { 386 name: "me".to_string(), 387 features, 388 }; 389 390 assert_eq!(ServerInfo::from_response(&response).unwrap(), server_info); 391 392 assert!(server_info.supports_feature(Extension::EightBitMime)); 393 assert!(!server_info.supports_feature(Extension::StartTls)); 394 395 let response2 = Response::new( 396 Code::new( 397 Severity::PositiveCompletion, 398 Category::Unspecified4, 399 Detail::One, 400 ), 401 vec![ 402 "me".to_string(), 403 "AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_string(), 404 "8BITMIME".to_string(), 405 "SIZE 42".to_string(), 406 ], 407 ); 408 409 let mut features2 = HashSet::new(); 410 assert!(features2.insert(Extension::EightBitMime)); 411 assert!(features2.insert(Extension::Authentication(Mechanism::Plain),)); 412 assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),)); 413 414 let server_info2 = ServerInfo { 415 name: "me".to_string(), 416 features: features2, 417 }; 418 419 assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2); 420 421 assert!(server_info2.supports_feature(Extension::EightBitMime)); 422 assert!(server_info2.supports_auth_mechanism(Mechanism::Plain)); 423 assert!(!server_info2.supports_feature(Extension::StartTls)); 424 } 425 } 426