Commit
+198 -114 +/-8 browse
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 { |