Commit
+159 -89 +/-6 browse
1 | diff --git a/Cargo.lock b/Cargo.lock |
2 | index b378456..f34642f 100644 |
3 | --- a/Cargo.lock |
4 | +++ b/Cargo.lock |
5 | @@ -36,6 +36,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
6 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" |
7 | |
8 | [[package]] |
9 | + name = "async-trait" |
10 | + version = "0.1.81" |
11 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
12 | + checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" |
13 | + dependencies = [ |
14 | + "proc-macro2", |
15 | + "quote", |
16 | + "syn", |
17 | + ] |
18 | + |
19 | + [[package]] |
20 | name = "autocfg" |
21 | version = "1.3.0" |
22 | source = "registry+https://github.com/rust-lang/crates.io-index" |
23 | @@ -270,6 +281,7 @@ dependencies = [ |
24 | name = "maitred" |
25 | version = "0.1.0" |
26 | dependencies = [ |
27 | + "async-trait", |
28 | "bytes", |
29 | "email_address", |
30 | "futures", |
31 | diff --git a/maitred/Cargo.toml b/maitred/Cargo.toml |
32 | index f2a1adb..587470b 100644 |
33 | --- a/maitred/Cargo.toml |
34 | +++ b/maitred/Cargo.toml |
35 | @@ -4,6 +4,7 @@ version = "0.1.0" |
36 | edition = "2021" |
37 | |
38 | [dependencies] |
39 | + async-trait = "0.1.81" |
40 | bytes = "1.6.1" |
41 | email_address = "0.2.9" |
42 | futures = "0.3.30" |
43 | diff --git a/maitred/src/expand.rs b/maitred/src/expand.rs |
44 | index 0cfcaa7..7a75201 100644 |
45 | --- a/maitred/src/expand.rs |
46 | +++ b/maitred/src/expand.rs |
47 | @@ -1,5 +1,6 @@ |
48 | use std::result::Result as StdResult; |
49 | |
50 | + use async_trait::async_trait; |
51 | use email_address::EmailAddress; |
52 | |
53 | /// Result type containing any of the associated e-mail addresses with the |
54 | @@ -21,9 +22,10 @@ pub enum Error { |
55 | /// addresses within the list if it exists. NOTE: That this function should |
56 | /// only be called with proper authentication otherwise it could be used to |
57 | /// harvest e-mail addresses. |
58 | + #[async_trait] |
59 | pub trait Expansion { |
60 | /// Expand the group into an array of members |
61 | - fn expand(&self, name: &str) -> Result; |
62 | + async fn expand(&self, name: &str) -> Result; |
63 | } |
64 | |
65 | /// Helper wrapper implementing the Expansion trait |
66 | @@ -43,11 +45,12 @@ pub struct Func<F>(pub F) |
67 | where |
68 | F: Fn(&str) -> Result; |
69 | |
70 | + #[async_trait] |
71 | impl<F> Expansion for Func<F> |
72 | where |
73 | - F: Fn(&str) -> Result, |
74 | + F: Fn(&str) -> Result + Sync, |
75 | { |
76 | - fn expand(&self, name: &str) -> Result { |
77 | + async fn expand(&self, name: &str) -> Result { |
78 | let f = &self.0; |
79 | f(name) |
80 | } |
81 | diff --git a/maitred/src/server.rs b/maitred/src/server.rs |
82 | index 4b63e82..a78b7ec 100644 |
83 | --- a/maitred/src/server.rs |
84 | +++ b/maitred/src/server.rs |
85 | @@ -4,6 +4,7 @@ use std::time::Duration; |
86 | use bytes::Bytes; |
87 | use futures::SinkExt; |
88 | use smtp_proto::Request; |
89 | + use tokio::sync::Mutex; |
90 | use tokio::{net::TcpListener, time::timeout}; |
91 | use tokio_stream::StreamExt; |
92 | use tokio_util::codec::Framed; |
93 | @@ -11,7 +12,7 @@ use tokio_util::codec::Framed; |
94 | use crate::error::Error; |
95 | use crate::pipeline::Pipeline; |
96 | use crate::session::Session; |
97 | - use crate::transport::Transport; |
98 | + use crate::transport::{Response, Transport}; |
99 | use crate::Chunk; |
100 | |
101 | /// The default port the server will listen on if none was specified in it's |
102 | @@ -24,14 +25,15 @@ const DEFAULT_GLOBAL_TIMEOUT_SECS: u64 = 300; |
103 | |
104 | /// Apply pipelining if running in extended mode and configured to support it |
105 | struct ConditionalPipeline<'a> { |
106 | - pub session: &'a mut Session, |
107 | + pub session: &'a Mutex<Session>, |
108 | pub pipeline: &'a mut Pipeline, |
109 | } |
110 | |
111 | impl ConditionalPipeline<'_> { |
112 | - pub fn apply(&mut self, req: &Request<String>, data: Option<&Bytes>) -> Chunk { |
113 | - let response = self.session.process(req, data); |
114 | - if self.session.has_capability(smtp_proto::EXT_PIPELINING) && self.session.is_extended() { |
115 | + pub async fn apply(&mut self, req: &Request<String>, data: Option<&Bytes>) -> Chunk { |
116 | + let mut session = self.session.lock().await; |
117 | + let response = session.process(req, data).await; |
118 | + if session.has_capability(smtp_proto::EXT_PIPELINING) && session.is_extended() { |
119 | self.pipeline.process(req, &response) |
120 | } else { |
121 | match response { |
122 | @@ -100,22 +102,15 @@ impl Server { |
123 | self |
124 | } |
125 | |
126 | - async fn process<T>(&self, mut framed: Framed<T, Transport>) -> Result<Session, Error> |
127 | + async fn process<T>( |
128 | + &self, |
129 | + mut framed: Framed<T, Transport>, |
130 | + pipeline: &mut ConditionalPipeline<'_>, |
131 | + greeting: Response<String>, |
132 | + ) -> Result<(), Error> |
133 | where |
134 | T: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin, |
135 | { |
136 | - let mut session = Session::default(); |
137 | - if let Some(opts) = &self.options { |
138 | - session = session.with_options(opts.clone()); |
139 | - } |
140 | - |
141 | - let greeting = session.greeting(); |
142 | - |
143 | - let mut pipelined = ConditionalPipeline { |
144 | - session: &mut session, |
145 | - pipeline: &mut Pipeline::default(), |
146 | - }; |
147 | - |
148 | // send inital server greeting |
149 | framed.send(greeting).await?; |
150 | |
151 | @@ -130,7 +125,8 @@ impl Server { |
152 | if matches!(command.0, Request::Quit) { |
153 | finished = true; |
154 | } |
155 | - let responses = pipelined.apply(&command.0, command.1.as_ref()); |
156 | + let responses = |
157 | + pipeline.apply(&command.0, command.1.as_ref()).await; |
158 | for response in responses.0.into_iter() { |
159 | framed.send(response).await?; |
160 | } |
161 | @@ -157,7 +153,7 @@ impl Server { |
162 | } |
163 | } |
164 | tracing::info!("Connection closed"); |
165 | - Ok(session) |
166 | + Ok(()) |
167 | } |
168 | |
169 | pub async fn listen(&self) -> Result<(), Error> { |
170 | @@ -168,7 +164,21 @@ impl Server { |
171 | let addr = socket.local_addr()?; |
172 | tracing::info!("Accepted connection on: {:?}", addr); |
173 | let framed = Framed::new(socket, Transport::default()); |
174 | - if let Err(err) = self.process(framed).await { |
175 | + let mut session = Session::default(); |
176 | + if let Some(opts) = &self.options { |
177 | + session = session.with_options(opts.clone()); |
178 | + } |
179 | + |
180 | + let greeting = session.greeting(); |
181 | + |
182 | + let session = Mutex::new(session); |
183 | + |
184 | + let mut pipelined = ConditionalPipeline { |
185 | + session: &session, |
186 | + pipeline: &mut Pipeline::default(), |
187 | + }; |
188 | + |
189 | + if let Err(err) = self.process(framed, &mut pipelined, greeting).await { |
190 | tracing::warn!("Client encountered an error: {:?}", err); |
191 | } |
192 | } |
193 | @@ -249,11 +259,24 @@ mod test { |
194 | }; |
195 | let server = Server::new("example.org"); |
196 | let framed = Framed::new(stream, Transport::default()); |
197 | - let session = server.process(framed).await.unwrap(); |
198 | + let session = Session::default(); |
199 | + let greeting = session.greeting(); |
200 | + let session = Mutex::new(session); |
201 | + |
202 | + let mut pipelined = ConditionalPipeline { |
203 | + session: &session, |
204 | + pipeline: &mut Pipeline::default(), |
205 | + }; |
206 | + server |
207 | + .process(framed, &mut pipelined, greeting) |
208 | + .await |
209 | + .unwrap(); |
210 | + let session = session.lock().await; |
211 | assert!(session |
212 | .mail_from |
213 | + .as_ref() |
214 | .is_some_and(|mail_from| mail_from.email() == "fuu@bar.com")); |
215 | - assert!(session.rcpt_to.is_some_and(|rcpts| rcpts |
216 | + assert!(session.rcpt_to.as_ref().is_some_and(|rcpts| rcpts |
217 | .first() |
218 | .is_some_and(|rcpt_to| rcpt_to.email() == "baz@qux.com"))); |
219 | } |
220 | diff --git a/maitred/src/session.rs b/maitred/src/session.rs |
221 | index 03d6fce..17ebb65 100644 |
222 | --- a/maitred/src/session.rs |
223 | +++ b/maitred/src/session.rs |
224 | @@ -1,9 +1,11 @@ |
225 | use std::rc::Rc; |
226 | use std::result::Result as StdResult; |
227 | use std::str::FromStr; |
228 | + use std::sync::Arc; |
229 | |
230 | use bytes::Bytes; |
231 | use email_address::EmailAddress; |
232 | + |
233 | use mail_parser::MessageParser; |
234 | use smtp_proto::{EhloResponse, Request, Response as SmtpResponse}; |
235 | use url::Host; |
236 | @@ -70,8 +72,8 @@ pub struct Options { |
237 | pub capabilities: u32, |
238 | pub help_banner: String, |
239 | pub greeting: String, |
240 | - pub list_expansion: Option<Rc<dyn Expansion>>, |
241 | - pub verification: Option<Rc<dyn Verify>>, |
242 | + pub list_expansion: Option<Arc<dyn Expansion>>, |
243 | + pub verification: Option<Arc<dyn Verify>>, |
244 | } |
245 | |
246 | impl Default for Options { |
247 | @@ -113,7 +115,7 @@ impl Options { |
248 | where |
249 | T: crate::expand::Expansion + 'static, |
250 | { |
251 | - self.list_expansion = Some(Rc::new(expansion)); |
252 | + self.list_expansion = Some(Arc::new(expansion)); |
253 | self |
254 | } |
255 | |
256 | @@ -121,7 +123,7 @@ impl Options { |
257 | where |
258 | T: crate::verify::Verify + 'static, |
259 | { |
260 | - self.verification = Some(Rc::new(verification)); |
261 | + self.verification = Some(Arc::new(verification)); |
262 | self |
263 | } |
264 | } |
265 | @@ -219,7 +221,7 @@ impl Session { |
266 | /// indicate that the process is starting and the second one contains the |
267 | /// parsed bytes from the transfer. |
268 | /// FIXME: Not at all reasonable yet |
269 | - pub fn process(&mut self, req: &Request<String>, data: Option<&Bytes>) -> Result { |
270 | + pub async fn process(&mut self, req: &Request<String>, data: Option<&Bytes>) -> Result { |
271 | match req { |
272 | Request::Ehlo { host } => { |
273 | self.hostname = |
274 | @@ -321,7 +323,7 @@ impl Session { |
275 | let address = EmailAddress::from_str(value.as_str()).map_err(|e| { |
276 | smtp_chunk!(500, 0, 0, 0, format!("cannot parse: {} {}", value, e)) |
277 | })?; |
278 | - match verifier.verify(&address) { |
279 | + match verifier.verify(&address).await { |
280 | Ok(_) => { |
281 | smtp_chunk_ok!(250, 0, 0, 0, "OK".to_string()) |
282 | } |
283 | @@ -333,7 +335,7 @@ impl Session { |
284 | } |
285 | Request::Expn { value } => { |
286 | if let Some(expn) = &self.opts.list_expansion { |
287 | - match expn.expand(value) { |
288 | + match expn.expand(value).await { |
289 | Ok(addresses) => { |
290 | let mut result = vec![smtp_response!(250, 0, 0, 0, "OK")]; |
291 | result.extend( |
292 | @@ -420,9 +422,13 @@ impl Session { |
293 | |
294 | #[cfg(test)] |
295 | mod test { |
296 | - use super::*; |
297 | + use std::sync::Arc; |
298 | |
299 | + use futures::stream::{self, StreamExt}; |
300 | use smtp_proto::{MailFrom, RcptTo}; |
301 | + use tokio::sync::Mutex; |
302 | + |
303 | + use super::*; |
304 | |
305 | const EXAMPLE_HOSTNAME: &str = "example.org"; |
306 | |
307 | @@ -433,10 +439,13 @@ mod test { |
308 | } |
309 | |
310 | /// process all commands returning their response |
311 | - fn process_all(session: &mut Session, commands: &[TestCase]) { |
312 | - commands.iter().enumerate().for_each(|(i, command)| { |
313 | + async fn process_all(session: &Mutex<Session>, commands: &[TestCase]) { |
314 | + let stream = stream::iter(commands); |
315 | + stream.enumerate().for_each(|(i, command)| { |
316 | + async move { |
317 | + let mut session = session.lock().await; |
318 | println!("Running command {}/{}", i, commands.len()); |
319 | - let response = session.process(&command.request, command.payload.as_ref()); |
320 | + let response = session.process(&command.request, command.payload.as_ref()).await; |
321 | println!("Response: {:?}", response); |
322 | match response { |
323 | Ok(actual_response) => { |
324 | @@ -472,12 +481,13 @@ mod test { |
325 | }, |
326 | } |
327 | } |
328 | + }; |
329 | } |
330 | - }) |
331 | + }).await; |
332 | } |
333 | |
334 | - #[test] |
335 | - fn test_hello_quit() { |
336 | + #[tokio::test] |
337 | + async fn test_hello_quit() { |
338 | let requests = &[ |
339 | TestCase { |
340 | request: Request::Helo { |
341 | @@ -492,16 +502,18 @@ mod test { |
342 | expected: smtp_chunk_ok!(221, 0, 0, 0, String::from("Ciao!")), |
343 | }, |
344 | ]; |
345 | - let mut session = Session::default(); |
346 | - process_all(&mut session, requests); |
347 | + let session = Mutex::new(Session::default()); |
348 | + process_all(&session, requests).await; |
349 | + let session = session.lock().await; |
350 | // session should contain both requests |
351 | assert!(session |
352 | .hostname |
353 | + .as_ref() |
354 | .is_some_and(|hostname| hostname.to_string() == EXAMPLE_HOSTNAME)); |
355 | } |
356 | |
357 | - #[test] |
358 | - fn test_command_with_no_hello() { |
359 | + #[tokio::test] |
360 | + async fn test_command_with_no_hello() { |
361 | let requests = &[TestCase { |
362 | request: Request::Mail { |
363 | from: MailFrom { |
364 | @@ -512,13 +524,15 @@ mod test { |
365 | payload: None, |
366 | expected: smtp_chunk_err!(500, 0, 0, 0, String::from("It's polite to say EHLO first")), |
367 | }]; |
368 | - let mut session = Session::default() |
369 | - .with_options(Options::default().our_hostname(EXAMPLE_HOSTNAME).into()); |
370 | - process_all(&mut session, requests); |
371 | + let session = Mutex::new( |
372 | + Session::default() |
373 | + .with_options(Options::default().our_hostname(EXAMPLE_HOSTNAME).into()), |
374 | + ); |
375 | + process_all(&session, requests).await; |
376 | } |
377 | |
378 | - #[test] |
379 | - fn test_expand() { |
380 | + #[tokio::test] |
381 | + async fn test_expand() { |
382 | let requests = &[ |
383 | TestCase { |
384 | request: Request::Helo { |
385 | @@ -544,26 +558,30 @@ mod test { |
386 | expected: smtp_chunk_ok!(221, 0, 0, 0, String::from("Ciao!")), |
387 | }, |
388 | ]; |
389 | - let mut session = Session::default().with_options( |
390 | - Options::default() |
391 | - .list_expansion(crate::expand::Func(|name: &str| { |
392 | - assert!(name == "mailing-list"); |
393 | - Ok(vec![ |
394 | - EmailAddress::new_unchecked("Fuu <fuu@bar.com>"), |
395 | - EmailAddress::new_unchecked("Baz <baz@qux.com>"), |
396 | - ]) |
397 | - })) |
398 | - .into(), |
399 | + let session = Mutex::new( |
400 | + Session::default().with_options( |
401 | + Options::default() |
402 | + .list_expansion(crate::expand::Func(|name: &str| { |
403 | + assert!(name == "mailing-list"); |
404 | + Ok(vec![ |
405 | + EmailAddress::new_unchecked("Fuu <fuu@bar.com>"), |
406 | + EmailAddress::new_unchecked("Baz <baz@qux.com>"), |
407 | + ]) |
408 | + })) |
409 | + .into(), |
410 | + ), |
411 | ); |
412 | - process_all(&mut session, requests); |
413 | + process_all(&session, requests).await; |
414 | // session should contain both requests |
415 | + let session = session.lock().await; |
416 | assert!(session |
417 | .hostname |
418 | + .as_ref() |
419 | .is_some_and(|hostname| hostname.to_string() == EXAMPLE_HOSTNAME)); |
420 | } |
421 | |
422 | - #[test] |
423 | - fn test_verify() { |
424 | + #[tokio::test] |
425 | + async fn test_verify() { |
426 | let requests = &[ |
427 | TestCase { |
428 | request: Request::Helo { |
429 | @@ -585,23 +603,27 @@ mod test { |
430 | expected: smtp_chunk_ok!(221, 0, 0, 0, String::from("Ciao!")), |
431 | }, |
432 | ]; |
433 | - let mut session = Session::default().with_options( |
434 | - Options::default() |
435 | - .verification(crate::verify::Func(|addr: &EmailAddress| { |
436 | - assert!(addr.email() == "bar@baz.com"); |
437 | - Ok(()) |
438 | - })) |
439 | - .into(), |
440 | + let session = Mutex::new( |
441 | + Session::default().with_options( |
442 | + Options::default() |
443 | + .verification(crate::verify::Func(|addr: &EmailAddress| { |
444 | + assert!(addr.email() == "bar@baz.com"); |
445 | + Ok(()) |
446 | + })) |
447 | + .into(), |
448 | + ), |
449 | ); |
450 | - process_all(&mut session, requests); |
451 | + process_all(&session, requests).await; |
452 | // session should contain both requests |
453 | + let session = session.lock().await; |
454 | assert!(session |
455 | .hostname |
456 | + .as_ref() |
457 | .is_some_and(|hostname| hostname.to_string() == EXAMPLE_HOSTNAME)); |
458 | } |
459 | |
460 | - #[test] |
461 | - fn test_non_ascii_characters() { |
462 | + #[tokio::test] |
463 | + async fn test_non_ascii_characters() { |
464 | let mut expected_ehlo_response = EhloResponse::new(String::from("Hello example.org")); |
465 | expected_ehlo_response.capabilities = DEFAULT_CAPABILITIES; |
466 | expected_ehlo_response.size = DEFAULT_MAXIMUM_MESSAGE_SIZE as usize; |
467 | @@ -670,17 +692,19 @@ mod test { |
468 | expected: smtp_chunk_ok!(250, 0, 0, 0, "OK"), |
469 | }, |
470 | ]; |
471 | - let mut session = Session::default().with_options( |
472 | - Options::default() |
473 | - .our_hostname(EXAMPLE_HOSTNAME) |
474 | - .capabilities(DEFAULT_CAPABILITIES) |
475 | - .into(), |
476 | + let session = Mutex::new( |
477 | + Session::default().with_options( |
478 | + Options::default() |
479 | + .our_hostname(EXAMPLE_HOSTNAME) |
480 | + .capabilities(DEFAULT_CAPABILITIES) |
481 | + .into(), |
482 | + ), |
483 | ); |
484 | - process_all(&mut session, requests); |
485 | + process_all(&session, requests).await; |
486 | } |
487 | |
488 | - #[test] |
489 | - fn test_email_with_body() { |
490 | + #[tokio::test] |
491 | + async fn test_email_with_body() { |
492 | let requests = &[ |
493 | TestCase { |
494 | request: Request::Helo { |
495 | @@ -736,17 +760,21 @@ transport rather than the session. |
496 | expected: smtp_chunk_ok!(250, 0, 0, 0, "OK"), |
497 | }, |
498 | ]; |
499 | - let mut session = Session::default() |
500 | - .with_options(Options::default().our_hostname(EXAMPLE_HOSTNAME).into()); |
501 | - process_all(&mut session, requests); |
502 | + let session = Mutex::new( |
503 | + Session::default() |
504 | + .with_options(Options::default().our_hostname(EXAMPLE_HOSTNAME).into()), |
505 | + ); |
506 | + process_all(&session, requests).await; |
507 | + let session = session.lock().await; |
508 | assert!(session |
509 | .mail_from |
510 | + .as_ref() |
511 | .is_some_and(|mail_from| mail_from.email() == "fuu@example.org")); |
512 | - assert!(session.rcpt_to.is_some_and(|rcpts| rcpts |
513 | + assert!(session.rcpt_to.as_ref().is_some_and(|rcpts| rcpts |
514 | .first() |
515 | .is_some_and(|rcpt_to| rcpt_to.email() == "bar@example.org"))); |
516 | - assert!(session.body.is_some_and(|body| { |
517 | - let message = MessageParser::new().parse(&body).unwrap(); |
518 | + assert!(session.body.as_ref().is_some_and(|body| { |
519 | + let message = MessageParser::new().parse(body).unwrap(); |
520 | message |
521 | .subject() |
522 | .is_some_and(|subject| subject == "Hello World") |
523 | diff --git a/maitred/src/verify.rs b/maitred/src/verify.rs |
524 | index 7aa2742..e4efb1c 100644 |
525 | --- a/maitred/src/verify.rs |
526 | +++ b/maitred/src/verify.rs |
527 | @@ -1,5 +1,6 @@ |
528 | use std::result::Result as StdResult; |
529 | |
530 | + use async_trait::async_trait; |
531 | use email_address::EmailAddress; |
532 | |
533 | /// Result indicating the VRFY command was successful and the user was |
534 | @@ -26,9 +27,10 @@ pub enum Error { |
535 | |
536 | /// Verify that the given e-mail address exists on the server. Servers may |
537 | /// choose to implement nothing or not use this option at all if desired. |
538 | + #[async_trait] |
539 | pub trait Verify { |
540 | /// Verify the e-mail address on the server |
541 | - fn verify(&self, address: &EmailAddress) -> Result; |
542 | + async fn verify(&self, address: &EmailAddress) -> Result; |
543 | } |
544 | |
545 | /// Helper wrapper implementing the Verify trait. |
546 | @@ -36,11 +38,12 @@ pub struct Func<F>(pub F) |
547 | where |
548 | F: Fn(&EmailAddress) -> Result; |
549 | |
550 | + #[async_trait] |
551 | impl<F> Verify for Func<F> |
552 | where |
553 | - F: Fn(&EmailAddress) -> Result, |
554 | + F: Fn(&EmailAddress) -> Result + Sync, |
555 | { |
556 | - fn verify(&self, address: &EmailAddress) -> Result { |
557 | + async fn verify(&self, address: &EmailAddress) -> Result { |
558 | let f = &self.0; |
559 | f(address) |
560 | } |