1use crate::authentication::Mechanism;
4use crate::error::Error;
5use crate::response::Response;
6use crate::util::XText;
7use std::collections::HashSet;
8use std::fmt::{self, Display, Formatter};
9use std::net::{Ipv4Addr, Ipv6Addr};
10use std::result::Result;
11
12#[derive(PartialEq, Eq, Clone, Debug)]
14pub enum ClientId {
15 Domain(String),
17 Ipv4(Ipv4Addr),
19 Ipv6(Ipv6Addr),
21}
22
23impl Default for ClientId {
24 fn default() -> Self {
25 Self::Ipv4(Ipv4Addr::new(127, 0, 0, 1))
34 }
35}
36
37impl Display for ClientId {
38 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
39 match *self {
40 ClientId::Domain(ref value) => f.write_str(value),
41 ClientId::Ipv4(ref value) => write!(f, "[{value}]"),
42 ClientId::Ipv6(ref value) => write!(f, "[IPv6:{value}]"),
43 }
44 }
45}
46
47impl ClientId {
48 pub fn new(domain: String) -> ClientId {
50 ClientId::Domain(domain)
51 }
52}
53
54#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
56pub enum Extension {
57 Pipelining,
61 EightBitMime,
65 SmtpUtfEight,
69 StartTls,
73 Authentication(Mechanism),
75}
76
77impl Display for Extension {
78 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
79 match *self {
80 Extension::Pipelining => write!(f, "PIPELINING"),
81 Extension::EightBitMime => write!(f, "8BITMIME"),
82 Extension::SmtpUtfEight => write!(f, "SMTPUTF8"),
83 Extension::StartTls => write!(f, "STARTTLS"),
84 Extension::Authentication(ref mechanism) => write!(f, "AUTH {mechanism}"),
85 }
86 }
87}
88
89#[derive(Clone, Debug, Eq, PartialEq)]
91pub struct ServerInfo {
92 pub name: String,
96 pub features: HashSet<Extension>,
100}
101
102impl Display for ServerInfo {
103 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
104 write!(
105 f,
106 "{} with {}",
107 self.name,
108 if self.features.is_empty() {
109 "no supported features".to_string()
110 } else {
111 format!("{:?}", self.features)
112 }
113 )
114 }
115}
116
117impl ServerInfo {
118 pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
120 let name = match response.first_word() {
121 Some(name) => name,
122 None => return Err(Error::ResponseParsing("Could not read server name")),
123 };
124
125 let mut features: HashSet<Extension> = HashSet::new();
126
127 for line in response.message.as_slice() {
128 if line.is_empty() {
129 continue;
130 }
131
132 let split: Vec<&str> = line.split_whitespace().collect();
133 match split.first().copied() {
134 Some("PIPELINING") => {
135 features.insert(Extension::Pipelining);
136 }
137 Some("8BITMIME") => {
138 features.insert(Extension::EightBitMime);
139 }
140 Some("SMTPUTF8") => {
141 features.insert(Extension::SmtpUtfEight);
142 }
143 Some("STARTTLS") => {
144 features.insert(Extension::StartTls);
145 }
146 Some("AUTH") => {
147 for &mechanism in split.iter().skip(1) {
148 match mechanism {
149 "PLAIN" => {
150 features.insert(Extension::Authentication(Mechanism::Plain));
151 }
152 "LOGIN" => {
153 features.insert(Extension::Authentication(Mechanism::Login));
154 }
155 "XOAUTH2" => {
156 features.insert(Extension::Authentication(Mechanism::Xoauth2));
157 }
158 _ => (),
159 }
160 }
161 }
162 _ => (),
163 };
164 }
165
166 Ok(ServerInfo {
167 name: name.to_string(),
168 features,
169 })
170 }
171
172 pub fn supports_feature(&self, keyword: Extension) -> bool {
174 self.features.contains(&keyword)
175 }
176
177 pub fn supports_auth_mechanism(&self, mechanism: Mechanism) -> bool {
179 self.features
180 .contains(&Extension::Authentication(mechanism))
181 }
182}
183
184#[derive(PartialEq, Eq, Clone, Debug)]
186pub enum MailParameter {
187 Body(MailBodyParameter),
189 Size(usize),
191 SmtpUtfEight,
193 Other {
195 keyword: String,
197 value: Option<String>,
199 },
200}
201
202impl Display for MailParameter {
203 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
204 match *self {
205 MailParameter::Body(ref value) => write!(f, "BODY={value}"),
206 MailParameter::Size(size) => write!(f, "SIZE={size}"),
207 MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
208 MailParameter::Other {
209 ref keyword,
210 value: Some(ref value),
211 } => write!(f, "{}={}", keyword, XText(value)),
212 MailParameter::Other {
213 ref keyword,
214 value: None,
215 } => f.write_str(keyword),
216 }
217 }
218}
219
220#[derive(PartialEq, Eq, Clone, Debug, Copy)]
222pub enum MailBodyParameter {
223 SevenBit,
225 EightBitMime,
227}
228
229impl Display for MailBodyParameter {
230 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
231 match *self {
232 MailBodyParameter::SevenBit => f.write_str("7BIT"),
233 MailBodyParameter::EightBitMime => f.write_str("8BITMIME"),
234 }
235 }
236}
237
238#[derive(PartialEq, Eq, Clone, Debug)]
240pub enum RcptParameter {
241 Other {
243 keyword: String,
245 value: Option<String>,
247 },
248}
249
250impl Display for RcptParameter {
251 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
252 match *self {
253 RcptParameter::Other {
254 ref keyword,
255 value: Some(ref value),
256 } => write!(f, "{}={}", keyword, XText(value)),
257 RcptParameter::Other {
258 ref keyword,
259 value: None,
260 } => f.write_str(keyword),
261 }
262 }
263}
264
265#[cfg(test)]
266mod test {
267
268 use super::{ClientId, Extension, ServerInfo};
269 use crate::authentication::Mechanism;
270 use crate::response::{Category, Code, Detail, Response, Severity};
271 use std::collections::HashSet;
272
273 #[test]
274 fn test_clientid_fmt() {
275 assert_eq!(
276 format!("{}", ClientId::new("test".to_string())),
277 "test".to_string()
278 );
279 }
280
281 #[test]
282 fn test_extension_fmt() {
283 assert_eq!(
284 format!("{}", Extension::Pipelining),
285 "PIPELINING".to_string()
286 );
287 assert_eq!(
288 format!("{}", Extension::EightBitMime),
289 "8BITMIME".to_string()
290 );
291 assert_eq!(
292 format!("{}", Extension::Authentication(Mechanism::Plain)),
293 "AUTH PLAIN".to_string()
294 );
295 }
296
297 #[test]
298 fn test_serverinfo_fmt() {
299 let mut eightbitmime = HashSet::new();
300 assert!(eightbitmime.insert(Extension::EightBitMime));
301
302 assert_eq!(
303 format!(
304 "{}",
305 ServerInfo {
306 name: "name".to_string(),
307 features: eightbitmime.clone(),
308 }
309 ),
310 "name with {EightBitMime}".to_string()
311 );
312
313 let empty = HashSet::new();
314
315 assert_eq!(
316 format!(
317 "{}",
318 ServerInfo {
319 name: "name".to_string(),
320 features: empty,
321 }
322 ),
323 "name with no supported features".to_string()
324 );
325
326 let mut plain = HashSet::new();
327 assert!(plain.insert(Extension::Authentication(Mechanism::Plain)));
328
329 assert_eq!(
330 format!(
331 "{}",
332 ServerInfo {
333 name: "name".to_string(),
334 features: plain.clone(),
335 }
336 ),
337 "name with {Authentication(Plain)}".to_string()
338 );
339 }
340
341 #[test]
342 fn test_serverinfo() {
343 let response = Response::new(
344 Code::new(
345 Severity::PositiveCompletion,
346 Category::Unspecified4,
347 Detail::One,
348 ),
349 vec![
350 "me".to_string(),
351 "8BITMIME".to_string(),
352 "SIZE 42".to_string(),
353 ],
354 );
355
356 let mut features = HashSet::new();
357 assert!(features.insert(Extension::EightBitMime));
358
359 let server_info = ServerInfo {
360 name: "me".to_string(),
361 features,
362 };
363
364 assert_eq!(ServerInfo::from_response(&response).unwrap(), server_info);
365
366 assert!(server_info.supports_feature(Extension::EightBitMime));
367 assert!(!server_info.supports_feature(Extension::StartTls));
368
369 let response2 = Response::new(
370 Code::new(
371 Severity::PositiveCompletion,
372 Category::Unspecified4,
373 Detail::One,
374 ),
375 vec![
376 "me".to_string(),
377 "AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_string(),
378 "8BITMIME".to_string(),
379 "SIZE 42".to_string(),
380 ],
381 );
382
383 let mut features2 = HashSet::new();
384 assert!(features2.insert(Extension::EightBitMime));
385 assert!(features2.insert(Extension::Authentication(Mechanism::Plain),));
386 assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
387
388 let server_info2 = ServerInfo {
389 name: "me".to_string(),
390 features: features2,
391 };
392
393 assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2);
394
395 assert!(server_info2.supports_feature(Extension::EightBitMime));
396 assert!(server_info2.supports_auth_mechanism(Mechanism::Plain));
397 assert!(!server_info2.supports_feature(Extension::StartTls));
398 }
399}