Commit
+199 -76 +/-4 browse
1 | diff --git a/maitred/src/lib.rs b/maitred/src/lib.rs |
2 | index a642e9f..6da0b2e 100644 |
3 | --- a/maitred/src/lib.rs |
4 | +++ b/maitred/src/lib.rs |
5 | @@ -3,5 +3,9 @@ mod server; |
6 | mod session; |
7 | mod transport; |
8 | |
9 | + /// Low Level SMTP protocol is exported for convenience |
10 | + pub use smtp_proto; |
11 | + |
12 | pub use error::Error; |
13 | pub use server::Server; |
14 | + |
15 | diff --git a/maitred/src/server.rs b/maitred/src/server.rs |
16 | index 72c47d0..84014ba 100644 |
17 | --- a/maitred/src/server.rs |
18 | +++ b/maitred/src/server.rs |
19 | @@ -1,4 +1,4 @@ |
20 | - use std::time::{Duration, Instant}; |
21 | + use std::time::Duration; |
22 | |
23 | use futures::SinkExt; |
24 | use smtp_proto::Request; |
25 | @@ -15,6 +15,27 @@ const DEFAULT_GREETING: &str = "Maitred ESMTP Server"; |
26 | // Maximum amount of time the server will wait for a command before closing |
27 | // the connection. |
28 | const DEFAULT_GLOBAL_TIMEOUT_SECS: u64 = 300; |
29 | + const DEFAULT_HELP_BANNER: &str = r#" |
30 | + Maitred ESMTP Server: |
31 | + see https://ayllu-forge.org/ayllu/maitred for more information. |
32 | + "#; |
33 | + |
34 | + /// Maximum message size the server will accept |
35 | + const DEFAULT_MAXIMUM_SIZE: u64 = 5_000_000; |
36 | + |
37 | + // target |
38 | + // 250-PIPELINING |
39 | + // 250-SIZE 10240000 |
40 | + // 250-VRFY |
41 | + // 250-ETRN |
42 | + // 250-ENHANCEDSTATUSCODES |
43 | + // 250-8BITMIME |
44 | + // 250-DSN |
45 | + // 250-SMTPUTF8 |
46 | + // 250 CHUNKING |
47 | + |
48 | + |
49 | + const DEFAULT_CAPABILITIES: u32 = smtp_proto::EXT_SIZE + smtp_proto::EXT_ENHANCED_STATUS_CODES; |
50 | |
51 | #[derive(Clone)] |
52 | struct Configuration { |
53 | @@ -22,6 +43,20 @@ struct Configuration { |
54 | pub hostname: String, |
55 | pub greeting: String, |
56 | pub global_timeout: Duration, |
57 | + pub help_banner: String, |
58 | + pub maximum_size: u64, |
59 | + capabilities: u32, |
60 | + } |
61 | + |
62 | + impl Configuration { |
63 | + pub fn session_opts(&self) -> SessionOptions { |
64 | + SessionOptions { |
65 | + hostname: self.hostname.clone(), |
66 | + capabilities: self.capabilities, |
67 | + help_banner: self.help_banner.clone(), |
68 | + maximum_size: self.maximum_size, |
69 | + } |
70 | + } |
71 | } |
72 | |
73 | impl Default for Configuration { |
74 | @@ -31,6 +66,9 @@ impl Default for Configuration { |
75 | hostname: String::default(), |
76 | greeting: DEFAULT_GREETING.to_string(), |
77 | global_timeout: Duration::from_secs(DEFAULT_GLOBAL_TIMEOUT_SECS), |
78 | + help_banner: DEFAULT_HELP_BANNER.to_string(), |
79 | + capabilities: DEFAULT_CAPABILITIES, |
80 | + maximum_size: DEFAULT_MAXIMUM_SIZE, |
81 | } |
82 | } |
83 | } |
84 | @@ -70,6 +108,11 @@ impl Server { |
85 | self |
86 | } |
87 | |
88 | + pub fn with_maximum_size(mut self, size: u64) -> Self { |
89 | + self.config.maximum_size = size; |
90 | + self |
91 | + } |
92 | + |
93 | async fn process<T>( |
94 | &self, |
95 | mut framed: Framed<T, Transport>, |
96 | @@ -143,15 +186,7 @@ impl Server { |
97 | let addr = socket.local_addr()?; |
98 | tracing::info!("Accepted connection on: {:?}", addr); |
99 | let framed = Framed::new(socket, Transport::default()); |
100 | - if let Err(err) = self |
101 | - .process( |
102 | - framed, |
103 | - SessionOptions { |
104 | - hostname: self.config.hostname.clone(), |
105 | - }, |
106 | - ) |
107 | - .await |
108 | - { |
109 | + if let Err(err) = self.process(framed, self.config.session_opts()).await { |
110 | tracing::warn!("Client encountered an error: {:?}", err); |
111 | } |
112 | } |
113 | @@ -237,6 +272,7 @@ mod test { |
114 | framed, |
115 | crate::session::Options { |
116 | hostname: "localhost".to_string(), |
117 | + ..Default::default() |
118 | }, |
119 | ) |
120 | .await |
121 | diff --git a/maitred/src/session.rs b/maitred/src/session.rs |
122 | index 80d5578..31d05dd 100644 |
123 | --- a/maitred/src/session.rs |
124 | +++ b/maitred/src/session.rs |
125 | @@ -1,11 +1,47 @@ |
126 | use std::result::Result as StdResult; |
127 | |
128 | + use crate::transport::Response; |
129 | use bytes::Bytes; |
130 | use mail_parser::MessageParser; |
131 | use melib::Address; |
132 | - use smtp_proto::{Request, Response}; |
133 | + use smtp_proto::{EhloResponse, Request, Response as SmtpResponse}; |
134 | use url::Host; |
135 | |
136 | + /// Generate an SMTP response |
137 | + macro_rules! smtp_response { |
138 | + ($code:expr, $e1:expr, $e2:expr, $e3:expr, $name:expr) => { |
139 | + Response::General(SmtpResponse::new($code, $e1, $e2, $e3, $name.to_string())) |
140 | + }; |
141 | + } |
142 | + |
143 | + /// Generate a successful SMTP response |
144 | + macro_rules! smtp_ok { |
145 | + ($code:expr, $e1:expr, $e2:expr, $e3:expr, $name:expr) => { |
146 | + Ok::<Response<String>, Response<String>>(Response::General(SmtpResponse::new( |
147 | + $code, |
148 | + $e1, |
149 | + $e2, |
150 | + $e3, |
151 | + $name.to_string(), |
152 | + ))) |
153 | + }; |
154 | + } |
155 | + |
156 | + /// Generate an SMTP response error |
157 | + macro_rules! smtp_err { |
158 | + ($code:expr, $e1:expr, $e2:expr, $e3:expr, $name:expr) => { |
159 | + Err::<Response<String>, Response<String>>(Response::General(SmtpResponse::new( |
160 | + $code, |
161 | + $e1, |
162 | + $e2, |
163 | + $e3, |
164 | + $name.to_string(), |
165 | + ))) |
166 | + }; |
167 | + } |
168 | + |
169 | + /// Result generated as part of an SMTP session, an Err indicates a session |
170 | + /// level error that will be returned to the client. |
171 | pub type Result = StdResult<Response<String>, Response<String>>; |
172 | |
173 | enum DataTransfer { |
174 | @@ -16,17 +52,23 @@ enum DataTransfer { |
175 | /// A greeting must be sent at the start of an SMTP connection when it is |
176 | /// first initialized. |
177 | pub fn greeting(hostname: &str, greeting: &str) -> Response<String> { |
178 | - Response::new(220, 2, 0, 0, format!("{} {}", hostname, greeting)) |
179 | + smtp_response!(220, 2, 0, 0, format!("{} {}", hostname, greeting)) |
180 | } |
181 | |
182 | /// Sent when the connection exceeds the maximum configured timeout |
183 | pub fn timeout(message: &str) -> Response<String> { |
184 | - Response::new(421, 4, 4, 2, format!("Timeout exceeded: {}", message)) |
185 | + smtp_response!(421, 4, 4, 2, format!("Timeout exceeded: {}", message)) |
186 | } |
187 | |
188 | /// Runtime options that influence server behavior |
189 | + #[derive(Default)] |
190 | pub(crate) struct Options { |
191 | pub hostname: String, |
192 | + /// Generic banner to show when the help command is sent without any |
193 | + /// arguments. |
194 | + pub help_banner: String, |
195 | + pub capabilities: u32, |
196 | + pub maximum_size: u64, |
197 | } |
198 | |
199 | /// Stateful connection that coresponds to a single SMTP session |
200 | @@ -52,6 +94,7 @@ impl Session { |
201 | self.mail_to = None; |
202 | self.rcpt_to = None; |
203 | self.hostname = None; |
204 | + self.data_transfer = None; |
205 | } |
206 | |
207 | /// Statefully process the SMTP command with optional data payload, any |
208 | @@ -71,42 +114,44 @@ impl Session { |
209 | self.history.push(req.clone()); |
210 | match req { |
211 | Request::Ehlo { host } => { |
212 | - self.hostname = Some( |
213 | - Host::parse(host).map_err(|e| Response::new(500, 0, 0, 0, e.to_string()))?, |
214 | - ); |
215 | - Ok(Response::new(250, 0, 0, 0, format!("Hello {}", host))) |
216 | + self.hostname = Some(Host::parse(host).map_err(|e| { |
217 | + Response::General(SmtpResponse::new(500, 0, 0, 0, e.to_string())) |
218 | + })?); |
219 | + let mut resp = EhloResponse::new(format!("Hello {}", host)); |
220 | + resp.capabilities = opts.capabilities; |
221 | + resp.size = opts.maximum_size as usize; |
222 | + Ok(Response::Ehlo(resp)) |
223 | } |
224 | Request::Lhlo { host } => { |
225 | - self.hostname = Some( |
226 | - Host::parse(host).map_err(|e| Response::new(500, 0, 0, 0, e.to_string()))?, |
227 | - ); |
228 | - Ok(Response::new(250, 0, 0, 0, format!("Hello {}", host))) |
229 | + self.hostname = |
230 | + Some(Host::parse(host).map_err(|e| smtp_response!(500, 0, 0, 0, e))?); |
231 | + smtp_ok!(250, 0, 0, 0, format!("Hello {}", host)) |
232 | } |
233 | Request::Helo { host } => { |
234 | self.hostname = Some( |
235 | - Host::parse(host).map_err(|e| Response::new(500, 0, 0, 0, e.to_string()))?, |
236 | + Host::parse(host).map_err(|e| smtp_response!(500, 0, 0, 0, e.to_string()))?, |
237 | ); |
238 | - Ok(Response::new(250, 0, 0, 0, format!("Hello {}", host))) |
239 | + smtp_ok!(250, 0, 0, 0, format!("Hello {}", host)) |
240 | } |
241 | Request::Mail { from } => { |
242 | let mail_to = Address::try_from(from.address.as_str()).map_err(|e| { |
243 | - Response::new( |
244 | + smtp_response!( |
245 | 500, |
246 | 0, |
247 | 0, |
248 | 0, |
249 | - format!("Cannot parse: {} {}", from.address, e), |
250 | + format!("cannot parse: {} {}", from.address, e) |
251 | ) |
252 | })?; |
253 | self.mail_to = Some(mail_to.clone()); |
254 | - Ok(Response::new(250, 0, 0, 0, "OK".to_string())) |
255 | + smtp_ok!(250, 0, 0, 0, "OK") |
256 | } |
257 | Request::Rcpt { to } => { |
258 | let mail_to = Address::try_from(to.address.as_str()).map_err(|e| { |
259 | - Response::new(500, 0, 0, 0, format!("Cannot parse: {} {}", to.address, e)) |
260 | + smtp_response!(500, 0, 0, 0, format!("Cannot parse: {} {}", to.address, e)) |
261 | })?; |
262 | self.mail_to = Some(mail_to.clone()); |
263 | - Ok(Response::new(250, 0, 0, 0, "OK".to_string())) |
264 | + smtp_ok!(250, 0, 0, 0, "OK") |
265 | } |
266 | Request::Bdat { |
267 | chunk_size: _, |
268 | @@ -122,14 +167,14 @@ impl Session { |
269 | data.expect("data returned without a payload").to_vec(); |
270 | let parser = MessageParser::new(); |
271 | let response = match parser.parse(&message_payload) { |
272 | - Some(_) => Ok(Response::new(250, 0, 0, 0, "OK".to_string())), |
273 | - None => Err(Response::new( |
274 | + Some(_) => smtp_ok!(250, 0, 0, 0, "OK"), |
275 | + None => smtp_err!( |
276 | 500, |
277 | 0, |
278 | 0, |
279 | 0, |
280 | - "Cannot parse message payload".to_string(), |
281 | - )), |
282 | + "Cannot parse message payload".to_string() |
283 | + ), |
284 | }?; |
285 | self.data_transfer = None; |
286 | self.body = Some(message_payload.clone()); |
287 | @@ -139,23 +184,29 @@ impl Session { |
288 | } else { |
289 | tracing::info!("Initializing data transfer mode"); |
290 | self.data_transfer = Some(DataTransfer::Bdat); |
291 | - Ok(Response::new( |
292 | - 354, |
293 | - 0, |
294 | - 0, |
295 | - 0, |
296 | - "Starting BDAT data transfer".to_string(), |
297 | - )) |
298 | + smtp_ok!(354, 0, 0, 0, "Starting BDAT data transfer".to_string()) |
299 | } |
300 | } |
301 | Request::Auth { |
302 | mechanism, |
303 | initial_response, |
304 | } => todo!(), |
305 | - Request::Noop { value } => todo!(), |
306 | + Request::Noop { value: _ } => smtp_ok!(250, 0, 0, 0, "OK".to_string(),), |
307 | Request::Vrfy { value } => todo!(), |
308 | Request::Expn { value } => todo!(), |
309 | - Request::Help { value } => todo!(), |
310 | + Request::Help { value } => { |
311 | + if value.is_empty() { |
312 | + smtp_ok!(250, 0, 0, 0, opts.help_banner.to_string()) |
313 | + } else { |
314 | + smtp_ok!( |
315 | + 250, |
316 | + 0, |
317 | + 0, |
318 | + 0, |
319 | + format!("Help for {} is not currently available", value) |
320 | + ) |
321 | + } |
322 | + } |
323 | Request::Etrn { name } => todo!(), |
324 | Request::Atrn { domains } => todo!(), |
325 | Request::Burl { uri, is_last } => todo!(), |
326 | @@ -171,14 +222,14 @@ impl Session { |
327 | data.expect("data returned without a payload").to_vec(); |
328 | let parser = MessageParser::new(); |
329 | let response = match parser.parse(&message_payload) { |
330 | - Some(_) => Ok(Response::new(250, 0, 0, 0, "OK".to_string())), |
331 | - None => Err(Response::new( |
332 | + Some(_) => smtp_ok!(250, 0, 0, 0, "OK".to_string()), |
333 | + None => smtp_err!( |
334 | 500, |
335 | 0, |
336 | 0, |
337 | 0, |
338 | - "Cannot parse message payload".to_string(), |
339 | - )), |
340 | + "Cannot parse message payload".to_string() |
341 | + ), |
342 | }?; |
343 | self.data_transfer = None; |
344 | self.body = Some(message_payload.clone()); |
345 | @@ -188,20 +239,20 @@ impl Session { |
346 | } else { |
347 | tracing::info!("Initializing data transfer mode"); |
348 | self.data_transfer = Some(DataTransfer::Data); |
349 | - Ok(Response::new( |
350 | + smtp_ok!( |
351 | 354, |
352 | 0, |
353 | 0, |
354 | 0, |
355 | - "Reading data input, end the message with <CRLF>.<CRLF>".to_string(), |
356 | - )) |
357 | + "Reading data input, end the message with <CRLF>.<CRLF>".to_string() |
358 | + ) |
359 | } |
360 | } |
361 | Request::Rset => { |
362 | self.reset(); |
363 | - Ok(Response::new(200, 0, 0, 0, "".to_string())) |
364 | + smtp_ok!(200, 0, 0, 0, "".to_string()) |
365 | } |
366 | - Request::Quit => Ok(Response::new(221, 0, 0, 0, "Ciao!".to_string())), |
367 | + Request::Quit => smtp_ok!(221, 0, 0, 0, "Ciao!".to_string()), |
368 | } |
369 | } |
370 | } |
371 | @@ -228,7 +279,6 @@ mod test { |
372 | println!("Response: {:?}", response); |
373 | match response { |
374 | Ok(actual_response) => { |
375 | - assert!(actual_response.code < 400); |
376 | match &command.expected { |
377 | Ok(expected_response) => { |
378 | if !actual_response.eq(expected_response) { |
379 | @@ -255,7 +305,6 @@ mod test { |
380 | ); |
381 | }, |
382 | Err(expected_err) => { |
383 | - assert!(actual_err.code > 300); |
384 | if !actual_err.eq(expected_err) { |
385 | panic!("Expected error does not match:\n\nActual: {:?}\n Expected: {:?}", actual_err, expected_err); |
386 | } |
387 | @@ -274,18 +323,12 @@ mod test { |
388 | host: EXAMPLE_HOSTNAME.to_string(), |
389 | }, |
390 | payload: None, |
391 | - expected: Ok(Response::new( |
392 | - 250, |
393 | - 0, |
394 | - 0, |
395 | - 0, |
396 | - String::from("Hello example.org"), |
397 | - )), |
398 | + expected: smtp_ok!(250, 0, 0, 0, String::from("Hello example.org")), |
399 | }, |
400 | TestCase { |
401 | request: Request::Quit {}, |
402 | payload: None, |
403 | - expected: Ok(Response::new(221, 0, 0, 0, String::from("Ciao!"))), |
404 | + expected: smtp_ok!(221, 0, 0, 0, String::from("Ciao!")), |
405 | }, |
406 | ]; |
407 | let mut session = Session::default(); |
408 | @@ -293,6 +336,7 @@ mod test { |
409 | &mut session, |
410 | &Options { |
411 | hostname: EXAMPLE_HOSTNAME.to_string(), |
412 | + ..Default::default() |
413 | }, |
414 | requests, |
415 | ); |
416 | @@ -308,13 +352,7 @@ mod test { |
417 | host: EXAMPLE_HOSTNAME.to_string(), |
418 | }, |
419 | payload: None, |
420 | - expected: Ok(Response::new( |
421 | - 250, |
422 | - 0, |
423 | - 0, |
424 | - 0, |
425 | - String::from("Hello example.org"), |
426 | - )), |
427 | + expected: smtp_ok!(250, 0, 0, 0, "Hello example.org"), |
428 | }, |
429 | TestCase { |
430 | request: Request::Mail { |
431 | @@ -324,7 +362,7 @@ mod test { |
432 | }, |
433 | }, |
434 | payload: None, |
435 | - expected: Ok(Response::new(250, 0, 0, 0, String::from("OK"))), |
436 | + expected: smtp_ok!(250, 0, 0, 0, "OK"), |
437 | }, |
438 | TestCase { |
439 | request: Request::Rcpt { |
440 | @@ -334,19 +372,19 @@ mod test { |
441 | }, |
442 | }, |
443 | payload: None, |
444 | - expected: Ok(Response::new(250, 0, 0, 0, String::from("OK"))), |
445 | + expected: smtp_ok!(250, 0, 0, 0, "OK"), |
446 | }, |
447 | // initiate data transfer |
448 | TestCase { |
449 | request: Request::Data {}, |
450 | payload: None, |
451 | - expected: Ok(Response::new( |
452 | + expected: smtp_ok!( |
453 | 354, |
454 | 0, |
455 | 0, |
456 | 0, |
457 | - String::from("Reading data input, end the message with <CRLF>.<CRLF>"), |
458 | - )), |
459 | + "Reading data input, end the message with <CRLF>.<CRLF>" |
460 | + ), |
461 | }, |
462 | // send the actual payload |
463 | TestCase { |
464 | @@ -360,7 +398,7 @@ Note that it doesn't end with a "." since that parsing happens as part of the |
465 | transport rather than the session. |
466 | "#, |
467 | )), |
468 | - expected: Ok(Response::new(250, 0, 0, 0, String::from("OK"))), |
469 | + expected: smtp_ok!(250, 0, 0, 0, "OK"), |
470 | }, |
471 | ]; |
472 | let mut session = Session::default(); |
473 | @@ -368,6 +406,7 @@ transport rather than the session. |
474 | &mut session, |
475 | &Options { |
476 | hostname: EXAMPLE_HOSTNAME.to_string(), |
477 | + ..Default::default() |
478 | }, |
479 | requests, |
480 | ); |
481 | diff --git a/maitred/src/transport.rs b/maitred/src/transport.rs |
482 | index fa59793..b69ac39 100644 |
483 | --- a/maitred/src/transport.rs |
484 | +++ b/maitred/src/transport.rs |
485 | @@ -1,11 +1,48 @@ |
486 | - use std::sync::Arc; |
487 | use std::{fmt::Display, io::Write}; |
488 | |
489 | use bytes::{Bytes, BytesMut}; |
490 | use smtp_proto::request::receiver::{BdatReceiver, DataReceiver, RequestReceiver}; |
491 | - pub use smtp_proto::{Request, Response}; |
492 | + pub use smtp_proto::{EhloResponse, Request, Response as SmtpResponse}; |
493 | use tokio_util::codec::{Decoder, Encoder}; |
494 | |
495 | + #[derive(Debug)] |
496 | + pub enum Response<T> |
497 | + where |
498 | + T: Display, |
499 | + { |
500 | + General(SmtpResponse<T>), |
501 | + Ehlo(EhloResponse<T>), |
502 | + } |
503 | + |
504 | + impl<T> PartialEq for Response<T> |
505 | + where |
506 | + T: Display, |
507 | + { |
508 | + fn eq(&self, other: &Self) -> bool { |
509 | + match self { |
510 | + Response::General(req) => match other { |
511 | + Response::General(other) => req.to_string() == other.to_string(), |
512 | + Response::Ehlo(_) => false, |
513 | + }, |
514 | + Response::Ehlo(req) => match other { |
515 | + Response::General(_) => false, |
516 | + Response::Ehlo(other) => { |
517 | + // FIXME |
518 | + req.capabilities == other.capabilities |
519 | + && req.hostname.to_string() == other.hostname.to_string() |
520 | + && req.deliver_by == other.deliver_by |
521 | + && req.size == other.size |
522 | + && req.auth_mechanisms == other.auth_mechanisms |
523 | + && req.future_release_datetime == req.future_release_datetime |
524 | + && req.future_release_interval == req.future_release_interval |
525 | + } |
526 | + }, |
527 | + } |
528 | + } |
529 | + } |
530 | + |
531 | + impl<T> Eq for Response<T> where T: Display {} |
532 | + |
533 | struct Wrapper<'a>(&'a mut BytesMut); |
534 | |
535 | impl Write for Wrapper<'_> { |
536 | @@ -46,7 +83,14 @@ impl Encoder<Response<String>> for Transport { |
537 | type Error = crate::Error; |
538 | |
539 | fn encode(&mut self, item: Response<String>, dst: &mut BytesMut) -> Result<(), Self::Error> { |
540 | - item.write(Wrapper(dst))?; |
541 | + match item { |
542 | + Response::General(item) => { |
543 | + item.write(Wrapper(dst))?; |
544 | + } |
545 | + Response::Ehlo(item) => { |
546 | + item.write(Wrapper(dst))?; |
547 | + } |
548 | + } |
549 | Ok(()) |
550 | } |
551 | } |