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