Author:
Hash:
Timestamp:
+198 -114 +/-8 browse
Kevin Schoon [me@kevinschoon.com]
b375baaa6a4aa5065e75ac8cae7c83c63cbb0fd9
Mon, 12 Aug 2024 18:39:25 +0000 (1.3 years ago)
| 1 | diff --git a/README.md b/README.md |
| 2 | index d48e59b..0f6394c 100644 |
| 3 | --- a/README.md |
| 4 | +++ b/README.md |
| 5 | @@ -39,8 +39,8 @@ for _absolutely nothing_ that is important. |
| 6 | | BDAT | ✅ | | |
| 7 | | DATA | ✅ | | |
| 8 | | AUTH | ❌ | No authentication mechanisms currently supported | |
| 9 | - | VRFY | ❌ | | |
| 10 | - | EXPN | ❌ | | |
| 11 | + | VRFY | ✅ | | |
| 12 | + | EXPN | ✅ | | |
| 13 | | STARTTLS | ❌ | For the moment there is no plan to implement STARTTLS | |
| 14 | |
| 15 | |
| 16 | diff --git a/maitred/src/addresses.rs b/maitred/src/addresses.rs |
| 17 | deleted file mode 100644 |
| 18 | index db8c8a3..0000000 |
| 19 | --- a/maitred/src/addresses.rs |
| 20 | +++ /dev/null |
| 21 | @@ -1,14 +0,0 @@ |
| 22 | - use std::fmt::Display; |
| 23 | - |
| 24 | - use email_address::EmailAddress; |
| 25 | - |
| 26 | - /// Array of resolved e-mail addresses that are associated with a mailing list |
| 27 | - #[derive(Debug)] |
| 28 | - pub struct Addresses(pub Vec<EmailAddress>); |
| 29 | - |
| 30 | - impl Display for Addresses { |
| 31 | - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 32 | - let addresses: Vec<String> = self.0.iter().map(|address| address.to_string()).collect(); |
| 33 | - write!(f, "{}", addresses.join("\n")) |
| 34 | - } |
| 35 | - } |
| 36 | diff --git a/maitred/src/expand.rs b/maitred/src/expand.rs |
| 37 | new file mode 100644 |
| 38 | index 0000000..85bc3db |
| 39 | --- /dev/null |
| 40 | +++ b/maitred/src/expand.rs |
| 41 | @@ -0,0 +1,40 @@ |
| 42 | + use std::result::Result as StdResult; |
| 43 | + |
| 44 | + use email_address::EmailAddress; |
| 45 | + |
| 46 | + pub type Result = StdResult<Vec<EmailAddress>, Error>; |
| 47 | + |
| 48 | + /// An error encountered while expanding a mail address |
| 49 | + #[derive(Debug, thiserror::Error)] |
| 50 | + pub enum Error { |
| 51 | + /// Indicates an unspecified error that occurred during expansion |
| 52 | + #[error("Internal Server Error: {0}")] |
| 53 | + Server(String), |
| 54 | + /// Indicates that no group exists with the specified name |
| 55 | + #[error("Group Not Found: {0}")] |
| 56 | + NotFound(String), |
| 57 | + } |
| 58 | + |
| 59 | + /// Expands a string representing a mailing list to an array of the associated |
| 60 | + /// addresses within the list if it exists. NOTE: That this function should |
| 61 | + /// only be called with proper authentication otherwise it could be used to |
| 62 | + /// harvest e-mail addresses. |
| 63 | + pub trait Expansion { |
| 64 | + /// Expand the group into an array of members |
| 65 | + fn expand(&self, name: &str) -> Result; |
| 66 | + } |
| 67 | + |
| 68 | + /// Wrapper type implementing the Expansion trait |
| 69 | + pub struct Func<F>(pub F) |
| 70 | + where |
| 71 | + F: Fn(&str) -> Result; |
| 72 | + |
| 73 | + impl<F> Expansion for Func<F> |
| 74 | + where |
| 75 | + F: Fn(&str) -> Result, |
| 76 | + { |
| 77 | + fn expand(&self, name: &str) -> Result { |
| 78 | + let f = &self.0; |
| 79 | + f(name) |
| 80 | + } |
| 81 | + } |
| 82 | diff --git a/maitred/src/expansion.rs b/maitred/src/expansion.rs |
| 83 | deleted file mode 100644 |
| 84 | index 16d4588..0000000 |
| 85 | --- a/maitred/src/expansion.rs |
| 86 | +++ /dev/null |
| 87 | @@ -1,40 +0,0 @@ |
| 88 | - use std::result::Result as StdResult; |
| 89 | - |
| 90 | - use crate::addresses::Addresses; |
| 91 | - |
| 92 | - pub type Result = StdResult<Addresses, Error>; |
| 93 | - |
| 94 | - /// An error encountered while expanding a mail address |
| 95 | - #[derive(Debug, thiserror::Error)] |
| 96 | - pub enum Error { |
| 97 | - /// Indicates an unspecified error that occurred during expansion |
| 98 | - #[error("Internal Server Error: {0}")] |
| 99 | - Server(String), |
| 100 | - /// Indicates that no group exists with the specified name |
| 101 | - #[error("Group Not Found: {0}")] |
| 102 | - NotFound(String), |
| 103 | - } |
| 104 | - |
| 105 | - /// Expands a string representing a mailing list to an array of the associated |
| 106 | - /// addresses within the list if it exists. NOTE: That this function should |
| 107 | - /// only be called with proper authentication otherwise it could be used to |
| 108 | - /// harvest e-mail addresses. |
| 109 | - pub trait Expansion { |
| 110 | - /// Expand the group into an array of members |
| 111 | - fn expand(&self, name: &str) -> Result; |
| 112 | - } |
| 113 | - |
| 114 | - /// Wrapper type implementing the Expansion trait |
| 115 | - pub struct Func<F>(pub F) |
| 116 | - where |
| 117 | - F: Fn(&str) -> Result; |
| 118 | - |
| 119 | - impl<F> Expansion for Func<F> |
| 120 | - where |
| 121 | - F: Fn(&str) -> Result, |
| 122 | - { |
| 123 | - fn expand(&self, name: &str) -> Result { |
| 124 | - let f = &self.0; |
| 125 | - f(name) |
| 126 | - } |
| 127 | - } |
| 128 | diff --git a/maitred/src/lib.rs b/maitred/src/lib.rs |
| 129 | index 7ea9531..e7a0a99 100644 |
| 130 | --- a/maitred/src/lib.rs |
| 131 | +++ b/maitred/src/lib.rs |
| 132 | @@ -1,13 +1,12 @@ |
| 133 | - mod addresses; |
| 134 | mod error; |
| 135 | - mod expansion; |
| 136 | + mod expand; |
| 137 | mod pipeline; |
| 138 | mod server; |
| 139 | mod session; |
| 140 | mod transport; |
| 141 | mod verify; |
| 142 | |
| 143 | - use smtp_proto::Request; |
| 144 | + use smtp_proto::{Request, Response as SmtpResponse}; |
| 145 | |
| 146 | /// Low Level SMTP protocol is exported for convenience |
| 147 | pub use smtp_proto; |
| 148 | @@ -22,18 +21,6 @@ use transport::Response; |
| 149 | #[derive(Clone, Debug)] |
| 150 | pub(crate) struct Chunk(pub Vec<Response<String>>); |
| 151 | |
| 152 | - impl Chunk { |
| 153 | - pub fn new() -> Self { |
| 154 | - Chunk(vec![]) |
| 155 | - } |
| 156 | - } |
| 157 | - |
| 158 | - impl PartialEq for Chunk { |
| 159 | - fn eq(&self, other: &Self) -> bool { |
| 160 | - self.0.len() == other.0.len() && self.0.iter().zip(other.0.iter()).all(|(a, b)| a == b) |
| 161 | - } |
| 162 | - } |
| 163 | - |
| 164 | /// Generate a single smtp_response |
| 165 | macro_rules! smtp_response { |
| 166 | ($code:expr, $e1:expr, $e2:expr, $e3:expr, $name:expr) => { |
| 167 | @@ -83,3 +70,57 @@ macro_rules! smtp_chunk_err { |
| 168 | }; |
| 169 | } |
| 170 | pub(crate) use smtp_chunk_err; |
| 171 | + |
| 172 | + impl Chunk { |
| 173 | + pub fn new() -> Self { |
| 174 | + Chunk(vec![]) |
| 175 | + } |
| 176 | + } |
| 177 | + |
| 178 | + impl PartialEq for Chunk { |
| 179 | + fn eq(&self, other: &Self) -> bool { |
| 180 | + self.0.len() == other.0.len() && self.0.iter().zip(other.0.iter()).all(|(a, b)| a == b) |
| 181 | + } |
| 182 | + } |
| 183 | + |
| 184 | + impl From<crate::verify::Error> for Chunk { |
| 185 | + fn from(value: crate::verify::Error) -> Self { |
| 186 | + match value { |
| 187 | + crate::verify::Error::Server(e) => Chunk(vec![smtp_response!(500, 0, 0, 0, e)]), |
| 188 | + crate::verify::Error::NotFound(e) => Chunk(vec![smtp_response!(500, 0, 0, 0, e)]), |
| 189 | + crate::verify::Error::Ambiguous { |
| 190 | + email, |
| 191 | + alternatives, |
| 192 | + } => { |
| 193 | + let mut result = vec![smtp_response!( |
| 194 | + 500, |
| 195 | + 0, |
| 196 | + 0, |
| 197 | + 0, |
| 198 | + format!("Username {} Ambigious", email) |
| 199 | + )]; |
| 200 | + result.extend( |
| 201 | + alternatives |
| 202 | + .iter() |
| 203 | + .map(|alt| smtp_response!(500, 0, 0, 0, alt.to_string())), |
| 204 | + ); |
| 205 | + Chunk(result) |
| 206 | + } |
| 207 | + } |
| 208 | + } |
| 209 | + } |
| 210 | + |
| 211 | + impl From<crate::expand::Error> for Chunk { |
| 212 | + fn from(value: crate::expand::Error) -> Self { |
| 213 | + match value { |
| 214 | + expand::Error::Server(message) => Chunk(vec![smtp_response!(500, 0, 0, 0, message)]), |
| 215 | + expand::Error::NotFound(name) => Chunk(vec![smtp_response!( |
| 216 | + 500, |
| 217 | + 0, |
| 218 | + 0, |
| 219 | + 0, |
| 220 | + format!("Cannot find: {}", name) |
| 221 | + )]), |
| 222 | + } |
| 223 | + } |
| 224 | + } |
| 225 | diff --git a/maitred/src/pipeline.rs b/maitred/src/pipeline.rs |
| 226 | index cc99703..4933030 100644 |
| 227 | --- a/maitred/src/pipeline.rs |
| 228 | +++ b/maitred/src/pipeline.rs |
| 229 | @@ -62,10 +62,7 @@ impl Pipeline { |
| 230 | .expect("to results called without history"); |
| 231 | if last_command.1.is_ok() && mail_from_ok && rcpt_to_ok_count > 0 { |
| 232 | flatten(&self.history) |
| 233 | - } else if !mail_from_ok { |
| 234 | - self.history.pop(); |
| 235 | - flatten(&self.history) |
| 236 | - } else if !rcpt_to_ok_count <= 0 { |
| 237 | + } else if !mail_from_ok || rcpt_to_ok_count <= 0{ |
| 238 | self.history.pop(); |
| 239 | flatten(&self.history) |
| 240 | } else { |
| 241 | diff --git a/maitred/src/session.rs b/maitred/src/session.rs |
| 242 | index d1598b7..e6d8324 100644 |
| 243 | --- a/maitred/src/session.rs |
| 244 | +++ b/maitred/src/session.rs |
| 245 | @@ -7,7 +7,7 @@ use mail_parser::MessageParser; |
| 246 | use smtp_proto::{EhloResponse, Request, Response as SmtpResponse}; |
| 247 | use url::Host; |
| 248 | |
| 249 | - use crate::expansion::Expansion; |
| 250 | + use crate::expand::Expansion; |
| 251 | use crate::transport::Response; |
| 252 | use crate::verify::Verify; |
| 253 | use crate::{smtp_chunk, smtp_chunk_err, smtp_chunk_ok}; |
| 254 | @@ -84,14 +84,17 @@ impl Session { |
| 255 | |
| 256 | pub fn list_expansion<T>(mut self, expansion: T) -> Self |
| 257 | where |
| 258 | - T: crate::expansion::Expansion + 'static, |
| 259 | + T: crate::expand::Expansion + 'static, |
| 260 | { |
| 261 | self.list_expansion = Some(Box::new(expansion)); |
| 262 | self |
| 263 | } |
| 264 | |
| 265 | - pub fn verification(mut self, verification: Box<dyn Verify>) -> Self { |
| 266 | - self.verification = Some(verification); |
| 267 | + pub fn verification<T>(mut self, verification: T) -> Self |
| 268 | + where |
| 269 | + T: crate::verify::Verify + 'static, |
| 270 | + { |
| 271 | + self.verification = Some(Box::new(verification)); |
| 272 | self |
| 273 | } |
| 274 | |
| 275 | @@ -253,19 +256,9 @@ impl Session { |
| 276 | })?; |
| 277 | match verifier.verify(&address) { |
| 278 | Ok(_) => { |
| 279 | - smtp_chunk_ok!(200, 0, 0, 0, "Ok".to_string()) |
| 280 | + smtp_chunk_ok!(250, 0, 0, 0, "OK".to_string()) |
| 281 | } |
| 282 | - Err(e) => match e { |
| 283 | - crate::verify::Error::Server(e) => { |
| 284 | - smtp_chunk_err!(500, 0, 0, 0, e.to_string()) |
| 285 | - } |
| 286 | - crate::verify::Error::NotFound(e) => { |
| 287 | - smtp_chunk_err!(500, 0, 0, 0, e.to_string()) |
| 288 | - } |
| 289 | - crate::verify::Error::Ambiguous(alternatives) => { |
| 290 | - smtp_chunk_err!(500, 0, 0, 0, alternatives.to_string()) |
| 291 | - } |
| 292 | - }, |
| 293 | + Err(e) => Err(e.into()), |
| 294 | } |
| 295 | } else { |
| 296 | smtp_chunk_err!(500, 0, 0, 0, "No such address") |
| 297 | @@ -275,28 +268,15 @@ impl Session { |
| 298 | if let Some(expn) = &self.list_expansion { |
| 299 | match expn.expand(value) { |
| 300 | Ok(addresses) => { |
| 301 | - smtp_chunk_ok!(250, 0, 0, 0, addresses.to_string()) |
| 302 | + let mut result = vec![smtp_response!(250, 0, 0, 0, "OK")]; |
| 303 | + result.extend( |
| 304 | + addresses |
| 305 | + .iter() |
| 306 | + .map(|addr| smtp_response!(250, 0, 0, 0, addr.to_string())), |
| 307 | + ); |
| 308 | + Ok(Chunk(result)) |
| 309 | } |
| 310 | - Err(err) => match err { |
| 311 | - crate::expansion::Error::Server(message) => { |
| 312 | - smtp_chunk_err!( |
| 313 | - 500, |
| 314 | - 0, |
| 315 | - 0, |
| 316 | - 0, |
| 317 | - format!("Internal mailing list error: {}", message) |
| 318 | - ) |
| 319 | - } |
| 320 | - crate::expansion::Error::NotFound(name) => { |
| 321 | - smtp_chunk_err!( |
| 322 | - 500, |
| 323 | - 0, |
| 324 | - 0, |
| 325 | - 0, |
| 326 | - format!("No such mailing list: {}", name) |
| 327 | - ) |
| 328 | - } |
| 329 | - }, |
| 330 | + Err(e) => Err(e.into()), |
| 331 | } |
| 332 | } else { |
| 333 | smtp_chunk_err!(500, 0, 0, 0, "Server does not support EXPN") |
| 334 | @@ -470,6 +450,83 @@ mod test { |
| 335 | } |
| 336 | |
| 337 | #[test] |
| 338 | + fn test_expand() { |
| 339 | + let requests = &[ |
| 340 | + TestCase { |
| 341 | + request: Request::Helo { |
| 342 | + host: EXAMPLE_HOSTNAME.to_string(), |
| 343 | + }, |
| 344 | + payload: None, |
| 345 | + expected: smtp_chunk_ok!(250, 0, 0, 0, String::from("Hello example.org")), |
| 346 | + }, |
| 347 | + TestCase { |
| 348 | + request: Request::Expn { |
| 349 | + value: "mailing-list".to_string(), |
| 350 | + }, |
| 351 | + payload: None, |
| 352 | + expected: Ok(Chunk(vec![ |
| 353 | + smtp_response!(250, 0, 0, 0, "OK"), |
| 354 | + smtp_response!(250, 0, 0, 0, "Fuu <fuu@bar.com>"), |
| 355 | + smtp_response!(250, 0, 0, 0, "Baz <baz@qux.com>"), |
| 356 | + ])), |
| 357 | + }, |
| 358 | + TestCase { |
| 359 | + request: Request::Quit {}, |
| 360 | + payload: None, |
| 361 | + expected: smtp_chunk_ok!(221, 0, 0, 0, String::from("Ciao!")), |
| 362 | + }, |
| 363 | + ]; |
| 364 | + let mut session = Session::default().list_expansion(crate::expand::Func(|name: &str| { |
| 365 | + assert!(name == "mailing-list"); |
| 366 | + Ok(vec![ |
| 367 | + EmailAddress::new_unchecked("Fuu <fuu@bar.com>"), |
| 368 | + EmailAddress::new_unchecked("Baz <baz@qux.com>"), |
| 369 | + ]) |
| 370 | + })); |
| 371 | + process_all(&mut session, requests); |
| 372 | + // session should contain both requests |
| 373 | + assert!(session |
| 374 | + .hostname |
| 375 | + .is_some_and(|hostname| hostname.to_string() == EXAMPLE_HOSTNAME)); |
| 376 | + } |
| 377 | + |
| 378 | + #[test] |
| 379 | + fn test_verify() { |
| 380 | + let requests = &[ |
| 381 | + TestCase { |
| 382 | + request: Request::Helo { |
| 383 | + host: EXAMPLE_HOSTNAME.to_string(), |
| 384 | + }, |
| 385 | + payload: None, |
| 386 | + expected: smtp_chunk_ok!(250, 0, 0, 0, String::from("Hello example.org")), |
| 387 | + }, |
| 388 | + TestCase { |
| 389 | + request: Request::Vrfy { |
| 390 | + value: "Fuu <bar@baz.com>".to_string(), |
| 391 | + }, |
| 392 | + payload: None, |
| 393 | + expected: Ok(Chunk(vec![ |
| 394 | + smtp_response!(250, 0, 0, 0, "OK"), |
| 395 | + ])), |
| 396 | + }, |
| 397 | + TestCase { |
| 398 | + request: Request::Quit {}, |
| 399 | + payload: None, |
| 400 | + expected: smtp_chunk_ok!(221, 0, 0, 0, String::from("Ciao!")), |
| 401 | + }, |
| 402 | + ]; |
| 403 | + let mut session = Session::default().verification(crate::verify::Func(|addr: &EmailAddress| { |
| 404 | + assert!(addr.email() == "bar@baz.com"); |
| 405 | + Ok(()) |
| 406 | + })); |
| 407 | + process_all(&mut session, requests); |
| 408 | + // session should contain both requests |
| 409 | + assert!(session |
| 410 | + .hostname |
| 411 | + .is_some_and(|hostname| hostname.to_string() == EXAMPLE_HOSTNAME)); |
| 412 | + } |
| 413 | + |
| 414 | + #[test] |
| 415 | fn test_non_ascii_characters() { |
| 416 | let mut expected_ehlo_response = EhloResponse::new(String::from("Hello example.org")); |
| 417 | expected_ehlo_response.capabilities = crate::server::DEFAULT_CAPABILITIES; |
| 418 | diff --git a/maitred/src/verify.rs b/maitred/src/verify.rs |
| 419 | index 44224cb..b37507b 100644 |
| 420 | --- a/maitred/src/verify.rs |
| 421 | +++ b/maitred/src/verify.rs |
| 422 | @@ -15,8 +15,11 @@ pub enum Error { |
| 423 | NotFound(String), |
| 424 | /// Indicates that the input as ambigious and multiple addresses are |
| 425 | /// associated with the string. |
| 426 | - #[error("Name is Ambiguous: {0}")] |
| 427 | - Ambiguous(EmailAddress), |
| 428 | + #[error("Name is Ambiguous: {email}")] |
| 429 | + Ambiguous { |
| 430 | + email: EmailAddress, |
| 431 | + alternatives: Vec<EmailAddress>, |
| 432 | + }, |
| 433 | } |
| 434 | |
| 435 | pub trait Verify { |