1use std::{
4 collections::HashSet,
5 fmt::{self, Display, Formatter},
6 net::{Ipv4Addr, Ipv6Addr},
7};
8
9use crate::transport::smtp::{
10 authentication::Mechanism,
11 error::{self, Error},
12 response::Response,
13 util::XText,
14};
15
16#[derive(PartialEq, Eq, Clone, Debug)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19#[non_exhaustive]
20pub enum ClientId {
21 Domain(String),
23 Ipv4(Ipv4Addr),
25 Ipv6(Ipv6Addr),
27}
28
29const LOCALHOST_CLIENT: ClientId = ClientId::Ipv4(Ipv4Addr::new(127, 0, 0, 1));
30
31impl Default for ClientId {
32 fn default() -> Self {
33 #[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
53impl Display for ClientId {
54 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
55 match self {
56 Self::Domain(value) => f.write_str(value),
57 Self::Ipv4(value) => write!(f, "[{value}]"),
58 Self::Ipv6(value) => write!(f, "[IPv6:{value}]"),
59 }
60 }
61}
62
63impl ClientId {
64 #[doc(hidden)]
65 #[deprecated(since = "0.10.0", note = "Please use ClientId::Domain(domain) instead")]
66 pub fn new(domain: String) -> Self {
68 Self::Domain(domain)
69 }
70}
71
72#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
75#[non_exhaustive]
76pub enum Extension {
77 EightBitMime,
81 SmtpUtfEight,
85 StartTls,
89 Authentication(Mechanism),
91}
92
93impl Display for Extension {
94 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(mechanism) => write!(f, "AUTH {mechanism}"),
100 }
101 }
102}
103
104#[derive(Clone, Debug, Eq, PartialEq, Default)]
106#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
107pub struct ServerInfo {
108 name: String,
112 features: HashSet<Extension>,
116}
117
118impl Display for ServerInfo {
119 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
120 let features = if self.features.is_empty() {
121 "no supported features".to_owned()
122 } else {
123 format!("{:?}", self.features)
124 };
125 write!(f, "{} with {}", self.name, features)
126 }
127}
128
129impl ServerInfo {
130 pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
132 let Some(name) = response.first_word() else {
133 return Err(error::response("Could not read server name"));
134 };
135
136 let mut features: HashSet<Extension> = HashSet::new();
137
138 for line in response.message() {
139 if line.is_empty() {
140 continue;
141 }
142
143 let mut split = line.split_whitespace();
144 match split.next().unwrap() {
145 "8BITMIME" => {
146 features.insert(Extension::EightBitMime);
147 }
148 "SMTPUTF8" => {
149 features.insert(Extension::SmtpUtfEight);
150 }
151 "STARTTLS" => {
152 features.insert(Extension::StartTls);
153 }
154 "AUTH" => {
155 for mechanism in split {
156 match mechanism {
157 "PLAIN" => {
158 features.insert(Extension::Authentication(Mechanism::Plain));
159 }
160 "LOGIN" => {
161 features.insert(Extension::Authentication(Mechanism::Login));
162 }
163 "XOAUTH2" => {
164 features.insert(Extension::Authentication(Mechanism::Xoauth2));
165 }
166 _ => (),
167 }
168 }
169 }
170 _ => (),
171 }
172 }
173
174 Ok(ServerInfo {
175 name: name.to_owned(),
176 features,
177 })
178 }
179
180 pub fn supports_feature(&self, keyword: Extension) -> bool {
182 self.features.contains(&keyword)
183 }
184
185 pub fn supports_auth_mechanism(&self, mechanism: Mechanism) -> bool {
187 self.features
188 .contains(&Extension::Authentication(mechanism))
189 }
190
191 pub fn get_auth_mechanism(&self, mechanisms: &[Mechanism]) -> Option<Mechanism> {
193 for mechanism in mechanisms {
194 if self.supports_auth_mechanism(*mechanism) {
195 return Some(*mechanism);
196 }
197 }
198 None
199 }
200
201 pub fn name(&self) -> &str {
203 self.name.as_ref()
204 }
205}
206
207#[derive(PartialEq, Eq, Clone, Debug)]
209#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
210pub enum MailParameter {
211 Body(MailBodyParameter),
213 Size(usize),
215 SmtpUtfEight,
217 Other {
219 keyword: String,
221 value: Option<String>,
223 },
224}
225
226impl Display for MailParameter {
227 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
228 match self {
229 MailParameter::Body(value) => write!(f, "BODY={value}"),
230 MailParameter::Size(size) => write!(f, "SIZE={size}"),
231 MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
232 MailParameter::Other {
233 keyword,
234 value: Some(value),
235 } => write!(f, "{}={}", keyword, XText(value)),
236 MailParameter::Other {
237 keyword,
238 value: None,
239 } => f.write_str(keyword),
240 }
241 }
242}
243
244#[derive(PartialEq, Eq, Clone, Debug, Copy)]
246#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
247pub enum MailBodyParameter {
248 SevenBit,
250 EightBitMime,
252}
253
254impl Display for MailBodyParameter {
255 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
256 match *self {
257 MailBodyParameter::SevenBit => f.write_str("7BIT"),
258 MailBodyParameter::EightBitMime => f.write_str("8BITMIME"),
259 }
260 }
261}
262
263#[derive(PartialEq, Eq, Clone, Debug)]
265#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
266pub enum RcptParameter {
267 Other {
269 keyword: String,
271 value: Option<String>,
273 },
274}
275
276impl Display for RcptParameter {
277 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
278 match &self {
279 RcptParameter::Other {
280 keyword,
281 value: Some(value),
282 } => write!(f, "{keyword}={}", XText(value)),
283 RcptParameter::Other {
284 keyword,
285 value: None,
286 } => f.write_str(keyword),
287 }
288 }
289}
290
291#[cfg(test)]
292mod test {
293 use super::*;
294 use crate::transport::smtp::response::{Category, Code, Detail, Severity};
295
296 #[test]
297 fn test_clientid_fmt() {
298 assert_eq!(
299 format!("{}", ClientId::Domain("test".to_owned())),
300 "test".to_owned()
301 );
302 assert_eq!(format!("{LOCALHOST_CLIENT}"), "[127.0.0.1]".to_owned());
303 }
304
305 #[test]
306 fn test_extension_fmt() {
307 assert_eq!(
308 format!("{}", Extension::EightBitMime),
309 "8BITMIME".to_owned()
310 );
311 assert_eq!(
312 format!("{}", Extension::Authentication(Mechanism::Plain)),
313 "AUTH PLAIN".to_owned()
314 );
315 }
316
317 #[test]
318 fn test_serverinfo_fmt() {
319 let mut eightbitmime = HashSet::new();
320 assert!(eightbitmime.insert(Extension::EightBitMime));
321
322 assert_eq!(
323 format!(
324 "{}",
325 ServerInfo {
326 name: "name".to_owned(),
327 features: eightbitmime,
328 }
329 ),
330 "name with {EightBitMime}".to_owned()
331 );
332
333 let empty = HashSet::new();
334
335 assert_eq!(
336 format!(
337 "{}",
338 ServerInfo {
339 name: "name".to_owned(),
340 features: empty,
341 }
342 ),
343 "name with no supported features".to_owned()
344 );
345
346 let mut plain = HashSet::new();
347 assert!(plain.insert(Extension::Authentication(Mechanism::Plain)));
348
349 assert_eq!(
350 format!(
351 "{}",
352 ServerInfo {
353 name: "name".to_owned(),
354 features: plain,
355 }
356 ),
357 "name with {Authentication(Plain)}".to_owned()
358 );
359 }
360
361 #[test]
362 fn test_serverinfo() {
363 let response = Response::new(
364 Code::new(
365 Severity::PositiveCompletion,
366 Category::Unspecified4,
367 Detail::One,
368 ),
369 vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned()],
370 );
371
372 let mut features = HashSet::new();
373 assert!(features.insert(Extension::EightBitMime));
374
375 let server_info = ServerInfo {
376 name: "me".to_owned(),
377 features,
378 };
379
380 assert_eq!(ServerInfo::from_response(&response).unwrap(), server_info);
381
382 assert!(server_info.supports_feature(Extension::EightBitMime));
383 assert!(!server_info.supports_feature(Extension::StartTls));
384
385 let response2 = Response::new(
386 Code::new(
387 Severity::PositiveCompletion,
388 Category::Unspecified4,
389 Detail::One,
390 ),
391 vec![
392 "me".to_owned(),
393 "AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_owned(),
394 "8BITMIME".to_owned(),
395 "SIZE 42".to_owned(),
396 ],
397 );
398
399 let mut features2 = HashSet::new();
400 assert!(features2.insert(Extension::EightBitMime));
401 assert!(features2.insert(Extension::Authentication(Mechanism::Plain),));
402 assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
403
404 let server_info2 = ServerInfo {
405 name: "me".to_owned(),
406 features: features2,
407 };
408
409 assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2);
410
411 assert!(server_info2.supports_feature(Extension::EightBitMime));
412 assert!(server_info2.supports_auth_mechanism(Mechanism::Plain));
413 assert!(!server_info2.supports_feature(Extension::StartTls));
414 }
415}