Commit
+1273 -998 +/-16 browse
1 | diff --git a/Cargo.lock b/Cargo.lock |
2 | index 3164742..ea2be38 100644 |
3 | --- a/Cargo.lock |
4 | +++ b/Cargo.lock |
5 | @@ -122,9 +122,9 @@ dependencies = [ |
6 | |
7 | [[package]] |
8 | name = "async-trait" |
9 | - version = "0.1.81" |
10 | + version = "0.1.83" |
11 | source = "registry+https://github.com/rust-lang/crates.io-index" |
12 | - checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" |
13 | + checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" |
14 | dependencies = [ |
15 | "proc-macro2", |
16 | "quote", |
17 | @@ -1112,8 +1112,10 @@ dependencies = [ |
18 | name = "maitred-debug" |
19 | version = "0.1.0" |
20 | dependencies = [ |
21 | + "async-trait", |
22 | "clap", |
23 | "futures", |
24 | + "maildir", |
25 | "maitred", |
26 | "serde", |
27 | "tokio", |
28 | diff --git a/cmd/maitred-debug/Cargo.toml b/cmd/maitred-debug/Cargo.toml |
29 | index 973dda8..779b87e 100644 |
30 | --- a/cmd/maitred-debug/Cargo.toml |
31 | +++ b/cmd/maitred-debug/Cargo.toml |
32 | @@ -4,10 +4,11 @@ version = "0.1.0" |
33 | edition = "2021" |
34 | |
35 | [dependencies] |
36 | + async-trait = "0.1.83" |
37 | clap = { version = "4.5.16", features = ["derive"] } |
38 | futures = "0.3.30" |
39 | - |
40 | - maitred = {path = "../../maitred"} |
41 | + maildir = "0.6.4" |
42 | + maitred = {path = "../../maitred", features = ["full"]} |
43 | serde = "1.0.209" |
44 | tokio = { version = "1.39.2", features = ["full"] } |
45 | toml = "0.8.19" |
46 | diff --git a/cmd/maitred-debug/src/main.rs b/cmd/maitred-debug/src/main.rs |
47 | index 6422912..b5f8044 100644 |
48 | --- a/cmd/maitred-debug/src/main.rs |
49 | +++ b/cmd/maitred-debug/src/main.rs |
50 | @@ -1,25 +1,19 @@ |
51 | + use std::collections::BTreeMap; |
52 | use std::fs::read_to_string; |
53 | use std::path::{Path, PathBuf}; |
54 | |
55 | use clap::Parser; |
56 | + use maildir::Maildir; |
57 | use toml::from_str; |
58 | use tracing::Level; |
59 | |
60 | mod config; |
61 | |
62 | - use maitred::auth::PlainAuthFunc; |
63 | - use maitred::delivery::{Delivery, DeliveryError, DeliveryFunc, Maildir}; |
64 | + use maitred::delivery::{Delivery, DeliveryError, DeliveryFunc}; |
65 | use maitred::mail_parser::Message; |
66 | use maitred::milter::MilterFunc; |
67 | - use maitred::{Envelope, Server, SessionOptions}; |
68 | - |
69 | - async fn print_message(envelope: &Envelope) -> Result<(), DeliveryError> { |
70 | - println!( |
71 | - "New SMTP Message:\n{}", |
72 | - String::from_utf8_lossy(envelope.body.raw_message()) |
73 | - ); |
74 | - Ok(()) |
75 | - } |
76 | + use maitred::server::Server; |
77 | + use maitred::session::Envelope; |
78 | |
79 | const LONG_ABOUT: &str = r#" |
80 | Maitred SMTP Demo Server |
81 | @@ -37,6 +31,55 @@ struct Args { |
82 | config: String, |
83 | } |
84 | |
85 | + /// FSDelivery stores incoming e-mail on the file system in the Maildir format |
86 | + /// for each address it's configured to handle. |
87 | + pub struct FSDelivery { |
88 | + maildirs: BTreeMap<String, Maildir>, |
89 | + } |
90 | + |
91 | + impl FSDelivery { |
92 | + /// Initialize a new Maildir on the file system. |
93 | + pub fn new(path: &Path, addresses: &[String]) -> Result<Self, std::io::Error> { |
94 | + let maildirs: Result<Vec<(String, Maildir)>, std::io::Error> = addresses |
95 | + .iter() |
96 | + .map(|address| { |
97 | + let mbox_dir = path.join(address); |
98 | + let maildir: Maildir = mbox_dir.into(); |
99 | + maildir.create_dirs()?; |
100 | + Ok((address.to_string(), maildir)) |
101 | + }) |
102 | + .collect(); |
103 | + let maildirs = maildirs?; |
104 | + Ok(FSDelivery { |
105 | + maildirs: maildirs.into_iter().collect(), |
106 | + }) |
107 | + } |
108 | + } |
109 | + |
110 | + #[async_trait::async_trait] |
111 | + impl Delivery for FSDelivery { |
112 | + async fn deliver(&self, message: &Envelope) -> Result<(), DeliveryError> { |
113 | + println!( |
114 | + "New SMTP Message:\n{}", |
115 | + String::from_utf8_lossy(message.body.raw_message()) |
116 | + ); |
117 | + for rcpt in message.rcpt_to.iter() { |
118 | + if let Some(maildir) = self.maildirs.get(&rcpt.email()) { |
119 | + maildir |
120 | + .store_new(message.body.raw_message()) |
121 | + .map_err(|e| match e { |
122 | + maildir::MaildirError::Io(io_err) => DeliveryError::Io(io_err), |
123 | + maildir::MaildirError::Utf8(_) => unreachable!(), |
124 | + maildir::MaildirError::Time(e) => DeliveryError::Server(e.to_string()), |
125 | + })?; |
126 | + } else { |
127 | + tracing::warn!("Ignoring unknown e-mail account: {}", rcpt); |
128 | + } |
129 | + } |
130 | + Ok(()) |
131 | + } |
132 | + } |
133 | + |
134 | #[tokio::main] |
135 | async fn main() -> Result<(), Box<dyn std::error::Error>> { |
136 | let args = Args::parse(); |
137 | @@ -48,52 +91,34 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { |
138 | .with_line_number(true) |
139 | .with_max_level(Level::DEBUG) |
140 | .init(); |
141 | - let maildir_path = PathBuf::from(&config.maildir); |
142 | let accounts = config.accounts.clone(); |
143 | let addresses: Vec<String> = accounts |
144 | .iter() |
145 | .map(|account| account.address.clone()) |
146 | .collect(); |
147 | // initialize maildirs before starting |
148 | - let _ = Maildir::new(maildir_path.as_path(), &addresses)?; |
149 | - // Set the subscriber as the default subscriber |
150 | - let mut session_opts = SessionOptions::default().plain_auth(PlainAuthFunc( |
151 | - |authcid: &str, authzid: &str, _passwd: &str| { |
152 | - println!("AUTHCID: {}, AUTHZID: {}", authcid, authzid); |
153 | - async move { Ok(()) } |
154 | - }, |
155 | - )); |
156 | + let delivery = FSDelivery::new(Path::new(&config.maildir), &addresses)?; |
157 | let mut mail_server = Server::default() |
158 | .address(&config.address) |
159 | .with_milter(MilterFunc(|message: &Message<'static>| { |
160 | let message = message.clone(); |
161 | async move { Ok(message.to_owned()) } |
162 | })) |
163 | - .with_delivery(DeliveryFunc(move |envelope: &Envelope| { |
164 | - let maildir_path = maildir_path.clone(); |
165 | - let addresses = addresses.clone(); |
166 | - let cloned = envelope.clone(); |
167 | - async move { |
168 | - print_message(&cloned).await?; |
169 | - let maildir = Maildir::new(maildir_path.as_path(), &addresses)?; |
170 | - maildir.deliver(&cloned).await?; |
171 | - Ok(()) |
172 | - } |
173 | - })) |
174 | + .with_delivery(delivery) |
175 | .dkim_verification(config.dkim.enabled) |
176 | .spf_verification(config.spf.enabled); |
177 | |
178 | if let Some(tls_config) = config.tls { |
179 | tracing::info!("TLS enabled"); |
180 | mail_server = mail_server.with_certificates(&tls_config.key, &tls_config.certificate); |
181 | - session_opts = session_opts.starttls_enabled(true); |
182 | + // session_opts = session_opts.starttls_enabled(true); |
183 | } |
184 | |
185 | if config.proxy_protocol.is_some_and(|enabled| enabled) { |
186 | mail_server = mail_server.proxy_protocol(true); |
187 | }; |
188 | |
189 | - mail_server = mail_server.with_session_opts(session_opts); |
190 | + // mail_server = mail_server.with_session_opts(session_opts); |
191 | mail_server.listen().await?; |
192 | Ok(()) |
193 | } |
194 | diff --git a/maitred.toml b/maitred.toml |
195 | index d09b5f6..785fa42 100644 |
196 | --- a/maitred.toml |
197 | +++ b/maitred.toml |
198 | @@ -17,9 +17,12 @@ key = "key.pem" |
199 | [spf] |
200 | enabled = false |
201 | |
202 | - # List of user accounts and their hard coded username / passwords |
203 | + |
204 | [[accounts]] |
205 | address = "demo-1@example.org" |
206 | |
207 | [[accounts]] |
208 | address = "demo-2@example.org" |
209 | + |
210 | + [[accounts]] |
211 | + address = "hello@example.org" |
212 | diff --git a/maitred/Cargo.toml b/maitred/Cargo.toml |
213 | index 76c4153..fb2c193 100644 |
214 | --- a/maitred/Cargo.toml |
215 | +++ b/maitred/Cargo.toml |
216 | @@ -30,3 +30,8 @@ url = "2.5.2" |
217 | |
218 | [dev-dependencies] |
219 | tracing-subscriber = "0.3.18" |
220 | + |
221 | + [features] |
222 | + default = [] |
223 | + full = ["server"] |
224 | + server = [] |
225 | diff --git a/maitred/src/auth.rs b/maitred/src/auth.rs |
226 | index 7a0c25d..133acff 100644 |
227 | --- a/maitred/src/auth.rs |
228 | +++ b/maitred/src/auth.rs |
229 | @@ -4,7 +4,8 @@ use async_trait::async_trait; |
230 | use base64::{prelude::*, DecodeError}; |
231 | use stringprep::{saslprep, Error as SaslPrepError}; |
232 | |
233 | - use crate::{smtp_response, Response}; |
234 | + use crate::smtp_response; |
235 | + use crate::session::Response; |
236 | use smtp_proto::Response as SmtpResponse; |
237 | |
238 | #[derive(Debug, thiserror::Error)] |
239 | diff --git a/maitred/src/delivery.rs b/maitred/src/delivery.rs |
240 | index 510b478..4e402f0 100644 |
241 | --- a/maitred/src/delivery.rs |
242 | +++ b/maitred/src/delivery.rs |
243 | @@ -1,9 +1,8 @@ |
244 | - use std::{collections::BTreeMap, future::Future, io::Error as IoError, path::Path}; |
245 | + use std::{future::Future, io::Error as IoError}; |
246 | |
247 | use async_trait::async_trait; |
248 | - use maildir::Maildir as MaildirInner; |
249 | |
250 | - use crate::Envelope; |
251 | + use crate::session::Envelope; |
252 | |
253 | #[derive(Debug, thiserror::Error)] |
254 | pub enum DeliveryError { |
255 | @@ -23,7 +22,7 @@ pub trait Delivery: Sync + Send { |
256 | } |
257 | |
258 | /// DeliveryFunc wraps an async closure implementing the Delivery trait. |
259 | - /// ```rust |
260 | + /// ```FIXME |
261 | /// use maitred::delivery::DeliveryFunc; |
262 | /// use maitred::Envelope; |
263 | /// |
264 | @@ -49,47 +48,3 @@ where |
265 | f.await |
266 | } |
267 | } |
268 | - |
269 | - /// Maildir stores incoming e-mail on the file system in the Maildir format. |
270 | - pub struct Maildir { |
271 | - maildirs: BTreeMap<String, MaildirInner>, |
272 | - } |
273 | - |
274 | - impl Maildir { |
275 | - /// Initialize a new Maildir on the file system. |
276 | - pub fn new(path: &Path, addresses: &[String]) -> Result<Self, std::io::Error> { |
277 | - let maildirs: Result<Vec<(String, MaildirInner)>, std::io::Error> = addresses |
278 | - .iter() |
279 | - .map(|address| { |
280 | - let mbox_dir = path.join(address); |
281 | - let maildir: MaildirInner = mbox_dir.into(); |
282 | - maildir.create_dirs()?; |
283 | - Ok((address.to_string(), maildir)) |
284 | - }) |
285 | - .collect(); |
286 | - let maildirs = maildirs?; |
287 | - Ok(Maildir { |
288 | - maildirs: maildirs.into_iter().collect(), |
289 | - }) |
290 | - } |
291 | - } |
292 | - |
293 | - #[async_trait] |
294 | - impl Delivery for Maildir { |
295 | - async fn deliver(&self, message: &Envelope) -> Result<(), DeliveryError> { |
296 | - for rcpt in message.rcpt_to.iter() { |
297 | - if let Some(maildir) = self.maildirs.get(&rcpt.email()) { |
298 | - maildir |
299 | - .store_new(message.body.raw_message()) |
300 | - .map_err(|e| match e { |
301 | - maildir::MaildirError::Io(io_err) => DeliveryError::Io(io_err), |
302 | - maildir::MaildirError::Utf8(_) => unreachable!(), |
303 | - maildir::MaildirError::Time(e) => DeliveryError::Server(e.to_string()), |
304 | - })?; |
305 | - } else { |
306 | - tracing::warn!("Ignoring unknown e-mail account: {}", rcpt); |
307 | - } |
308 | - } |
309 | - Ok(()) |
310 | - } |
311 | - } |
312 | diff --git a/maitred/src/error.rs b/maitred/src/error.rs |
313 | deleted file mode 100644 |
314 | index fcfc8c6..0000000 |
315 | --- a/maitred/src/error.rs |
316 | +++ /dev/null |
317 | @@ -1,10 +0,0 @@ |
318 | - /// Any fatal error that is encountered by the server that should cause it |
319 | - /// to shutdown and stop processing connections. |
320 | - #[derive(Debug, thiserror::Error)] |
321 | - pub enum Error { |
322 | - /// An IO related error such as not being able to bind to a TCP socket |
323 | - #[error("Io: {0}")] |
324 | - Io(#[from] std::io::Error), |
325 | - #[error("Proxy Protocol Error: {0}")] |
326 | - ProxyProtocol(#[from] proxy_header::Error) |
327 | - } |
328 | diff --git a/maitred/src/expand.rs b/maitred/src/expand.rs |
329 | index 5be33e2..4bb9f9b 100644 |
330 | --- a/maitred/src/expand.rs |
331 | +++ b/maitred/src/expand.rs |
332 | @@ -2,6 +2,10 @@ use std::future::Future; |
333 | |
334 | use async_trait::async_trait; |
335 | use email_address::EmailAddress; |
336 | + use smtp_proto::Response as SmtpResponse; |
337 | + |
338 | + use crate::session::Response; |
339 | + use crate::smtp_response; |
340 | |
341 | /// An error encountered while expanding a mail address |
342 | #[derive(Debug, thiserror::Error)] |
343 | @@ -13,6 +17,15 @@ pub enum ExpansionError { |
344 | #[error("Group Not Found: {0}")] |
345 | NotFound(String), |
346 | } |
347 | + #[allow(clippy::from_over_into)] |
348 | + impl Into<Response<String>> for ExpansionError { |
349 | + fn into(self) -> Response<String> { |
350 | + match self { |
351 | + ExpansionError::Server(_) => smtp_response!(500, 0, 0, 0, self.to_string()), |
352 | + ExpansionError::NotFound(_) => smtp_response!(404, 0, 0, 0, self.to_string()), |
353 | + } |
354 | + } |
355 | + } |
356 | |
357 | /// Expands a string representing a mailing list to an array of the associated |
358 | /// addresses within the list if it exists. NOTE: That this function should |
359 | diff --git a/maitred/src/lib.rs b/maitred/src/lib.rs |
360 | index 0510814..07d67d0 100644 |
361 | --- a/maitred/src/lib.rs |
362 | +++ b/maitred/src/lib.rs |
363 | @@ -3,13 +3,13 @@ |
364 | //! but also for general use. |
365 | //! |
366 | //! # Example SMTP Server |
367 | - //! ```rust |
368 | + //! ```rust,no_run |
369 | //! use maitred::auth::PlainAuthFunc; |
370 | - //! use maitred::delivery::{Delivery, DeliveryError, DeliveryFunc, Maildir}; |
371 | - //! use maitred::Error; |
372 | + //! use maitred::delivery::{Delivery, DeliveryError, DeliveryFunc}; |
373 | //! use maitred::mail_parser::Message; |
374 | //! use maitred::milter::MilterFunc; |
375 | - //! use maitred::{Envelope, Server, SessionOptions}; |
376 | + //! use maitred::server::{Server, ServerError}; |
377 | + //! use maitred::session::Envelope; |
378 | //! |
379 | //! use tracing::Level; |
380 | //! |
381 | @@ -28,7 +28,7 @@ |
382 | //! } |
383 | //! |
384 | //! #[tokio::main] |
385 | - //! async fn main() -> Result<(), Error> { |
386 | + //! async fn main() -> Result<(), ServerError> { |
387 | //! // Create a subscriber that logs events to the console |
388 | //! tracing_subscriber::fmt() |
389 | //! .compact() |
390 | @@ -38,16 +38,11 @@ |
391 | //! // Set the subscriber as the default subscriber |
392 | //! let mut mail_server = Server::default() |
393 | //! .address("127.0.0.1:2525") |
394 | - //! .with_milter(MilterFunc(|message: &Message<'static>| { |
395 | - //! let message = message.clone(); |
396 | - //! async move { Ok(message.to_owned()) } |
397 | - //! })) |
398 | //! .with_delivery(DeliveryFunc(|envelope: &Envelope| { |
399 | //! let envelope = envelope.clone(); |
400 | //! async move { print_message(&envelope).await } |
401 | - //! })) |
402 | - //! .with_session_opts(SessionOptions::default()); |
403 | - //! // mail_server.listen().await?; |
404 | + //! })); |
405 | + //! mail_server.listen().await?; |
406 | //! Ok(()) |
407 | //! } |
408 | //! ``` |
409 | @@ -56,7 +51,8 @@ pub use email_address; |
410 | pub use mail_parser; |
411 | pub use smtp_proto; |
412 | |
413 | - mod error; |
414 | + mod opportunistic; |
415 | + mod rewrite; |
416 | |
417 | /// SMTP Authentication |
418 | pub mod auth; |
419 | @@ -68,24 +64,19 @@ pub mod expand; |
420 | pub mod milter; |
421 | /// Callback for implementing SMPT command VRFY |
422 | pub mod verify; |
423 | + /// Low level SMTP session without network transport |
424 | + pub mod session; |
425 | |
426 | - mod rewrite; |
427 | - mod server; |
428 | - mod session; |
429 | + /// Full featured tokio based TCP server for handling SMTP sessions |
430 | + #[cfg(feature = "server")] |
431 | + pub mod server; |
432 | + #[cfg(feature = "server")] |
433 | mod transport; |
434 | + #[cfg(feature = "server")] |
435 | mod validation; |
436 | + #[cfg(feature = "server")] |
437 | mod worker; |
438 | |
439 | - use smtp_proto::Response as SmtpResponse; |
440 | - use transport::Response; |
441 | - |
442 | - pub use error::Error; |
443 | - pub use server::{Envelope, Server}; |
444 | - pub use session::{ |
445 | - SessionOptions, DEFAULT_CAPABILITIES, DEFAULT_GREETING, DEFAULT_HELP_BANNER, |
446 | - DEFAULT_MAXIMUM_MESSAGE_SIZE, |
447 | - }; |
448 | - |
449 | /// Generate a single smtp_response |
450 | macro_rules! smtp_response { |
451 | ($code:expr, $e1:expr, $e2:expr, $e3:expr, $name:expr) => { |
452 | diff --git a/maitred/src/opportunistic.rs b/maitred/src/opportunistic.rs |
453 | new file mode 100644 |
454 | index 0000000..4cf40e5 |
455 | --- /dev/null |
456 | +++ b/maitred/src/opportunistic.rs |
457 | @@ -0,0 +1,62 @@ |
458 | + use std::sync::Arc; |
459 | + |
460 | + use futures::SinkExt; |
461 | + use futures::StreamExt; |
462 | + use tokio::sync::Mutex; |
463 | + use tokio_rustls::server::TlsStream; |
464 | + use tokio_util::codec::Framed; |
465 | + |
466 | + use crate::session::Response; |
467 | + use crate::transport::{Command, Transport, TransportError}; |
468 | + |
469 | + /// Connection that is either over plain text or TLS |
470 | + pub(crate) trait Opportunistic { |
471 | + async fn send(&self, message: Response<String>) -> Result<(), TransportError>; |
472 | + async fn next(&self) -> Option<Result<Command, TransportError>>; |
473 | + } |
474 | + |
475 | + /// Framed SMTP Transport over Plain Text |
476 | + pub(crate) struct Plain<'a, T> |
477 | + where |
478 | + T: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin, |
479 | + { |
480 | + pub inner: Arc<Mutex<Framed<&'a mut T, Transport>>>, |
481 | + } |
482 | + |
483 | + impl<'a, T> Opportunistic for Plain<'a, T> |
484 | + where |
485 | + T: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin, |
486 | + { |
487 | + async fn send(&self, message: Response<String>) -> Result<(), TransportError> { |
488 | + let mut inner = self.inner.lock().await; |
489 | + inner.send(message).await |
490 | + } |
491 | + |
492 | + async fn next(&self) -> Option<Result<Command, TransportError>> { |
493 | + let mut inner = self.inner.lock().await; |
494 | + inner.next().await |
495 | + } |
496 | + } |
497 | + |
498 | + /// Framed SMTP Transport over TLS |
499 | + pub(crate) struct Tls<'a, T> |
500 | + where |
501 | + T: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin, |
502 | + { |
503 | + pub inner: Arc<Mutex<Framed<TlsStream<&'a mut T>, Transport>>>, |
504 | + } |
505 | + |
506 | + impl<'a, T> Opportunistic for Tls<'a, T> |
507 | + where |
508 | + T: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin, |
509 | + { |
510 | + async fn send(&self, message: Response<String>) -> Result<(), TransportError> { |
511 | + let mut inner = self.inner.lock().await; |
512 | + inner.send(message).await |
513 | + } |
514 | + |
515 | + async fn next(&self) -> Option<Result<Command, TransportError>> { |
516 | + let mut inner = self.inner.lock().await; |
517 | + inner.next().await |
518 | + } |
519 | + } |
520 | diff --git a/maitred/src/server.rs b/maitred/src/server.rs |
521 | index b1acee3..2977285 100644 |
522 | --- a/maitred/src/server.rs |
523 | +++ b/maitred/src/server.rs |
524 | @@ -8,13 +8,11 @@ use std::time::Duration; |
525 | use crossbeam_deque::Injector; |
526 | use crossbeam_deque::Stealer; |
527 | use crossbeam_deque::Worker as WorkQueue; |
528 | - use email_address::EmailAddress; |
529 | use futures::SinkExt; |
530 | use futures::StreamExt; |
531 | use mail_auth::Resolver; |
532 | - use mail_parser::Message; |
533 | use proxy_header::{ParseConfig, ProxyHeader}; |
534 | - use smtp_proto::Request; |
535 | + use smtp_proto::Response as SmtpResponse; |
536 | use tokio::net::TcpListener; |
537 | use tokio::sync::mpsc::Sender; |
538 | use tokio::sync::Mutex; |
539 | @@ -23,15 +21,17 @@ use tokio::time::timeout; |
540 | use tokio_rustls::{rustls, TlsAcceptor}; |
541 | use tokio_stream::{self as stream}; |
542 | use tokio_util::codec::Framed; |
543 | - use url::Host; |
544 | |
545 | + use crate::auth::PlainAuth; |
546 | use crate::delivery::Delivery; |
547 | - use crate::error::Error; |
548 | + use crate::expand::Expansion; |
549 | use crate::milter::Milter; |
550 | - use crate::session::{Session, SessionOptions}; |
551 | + use crate::opportunistic::{Opportunistic, Plain, Tls}; |
552 | + use crate::session::{Envelope, Response, Session}; |
553 | use crate::transport::{Command, Transport, TransportError}; |
554 | + use crate::validation::Validation; |
555 | + use crate::verify::Verify; |
556 | use crate::worker::Worker; |
557 | - use crate::{Response, SmtpResponse}; |
558 | |
559 | /// The default port the server will listen on if none was specified in it's |
560 | /// configuration options. |
561 | @@ -41,20 +41,10 @@ pub const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:2525"; |
562 | /// the connection. |
563 | pub const DEFAULT_GLOBAL_TIMEOUT_SECS: u64 = 300; |
564 | |
565 | - /// check if the final command is QUIT |
566 | - fn is_quit(reqs: &[Request<String>]) -> bool { |
567 | - reqs.last().is_some_and(|req| matches!(req, Request::Quit)) |
568 | - } |
569 | - |
570 | - fn is_starttls(reqs: &[Request<String>]) -> bool { |
571 | - reqs.last() |
572 | - .is_some_and(|req| matches!(req, Request::StartTls)) |
573 | - } |
574 | - |
575 | /// Top level error encountered while processing a client connection, causes |
576 | /// a warning to be logged but is not fatal. |
577 | #[derive(Debug, thiserror::Error)] |
578 | - pub(crate) enum ServerError { |
579 | + pub enum ServerError { |
580 | /// An IO related error such as not being able to bind to a TCP socket |
581 | #[error("Io: {0}")] |
582 | Io(#[from] std::io::Error), |
583 | @@ -65,31 +55,14 @@ pub(crate) enum ServerError { |
584 | Timeout(u64), |
585 | #[error("Failed to configure TLS: {0}")] |
586 | TlsConfiguration(#[from] rustls::Error), |
587 | - } |
588 | - |
589 | - /// Session details to be passed internally for processing |
590 | - #[derive(Clone, Debug)] |
591 | - pub struct Envelope { |
592 | - pub body: Message<'static>, |
593 | - pub mail_from: EmailAddress, |
594 | - pub rcpt_to: Vec<EmailAddress>, |
595 | - pub hostname: Host, |
596 | - } |
597 | - |
598 | - impl From<&Session> for Envelope { |
599 | - fn from(value: &Session) -> Self { |
600 | - Envelope { |
601 | - body: value.body.clone().unwrap(), |
602 | - mail_from: value.mail_from.clone().unwrap(), |
603 | - rcpt_to: value.rcpt_to.clone().unwrap(), |
604 | - hostname: value.hostname.clone().unwrap(), |
605 | - } |
606 | - } |
607 | + #[error("Proxy Protocol Error: {0}")] |
608 | + ProxyProtocol(#[from] proxy_header::Error), |
609 | } |
610 | |
611 | /// Action for controlling a TCP session |
612 | pub(crate) enum Action { |
613 | Continue, |
614 | + Enqueue, |
615 | Shutdown, |
616 | TlsUpgrade, |
617 | } |
618 | @@ -100,16 +73,20 @@ pub(crate) enum Action { |
619 | pub struct Server { |
620 | address: String, |
621 | global_timeout: Duration, |
622 | - options: Option<crate::session::SessionOptions>, |
623 | + pipelining: bool, |
624 | milter: Option<Arc<dyn Milter>>, |
625 | delivery: Option<Arc<dyn Delivery>>, |
626 | n_threads: usize, |
627 | shutdown_handles: Vec<Sender<bool>>, |
628 | dkim_verification: bool, |
629 | spf_verification: bool, |
630 | + list_expansion: Option<Arc<dyn Expansion>>, |
631 | + verification: Option<Arc<dyn Verify>>, |
632 | + plain_auth: Option<Arc<dyn PlainAuth>>, |
633 | resolver: Option<Arc<Mutex<Resolver>>>, |
634 | tls_certificates: Option<(PathBuf, PathBuf)>, |
635 | proxy_protocol: bool, |
636 | + session: Session, |
637 | } |
638 | |
639 | impl Default for Server { |
640 | @@ -117,16 +94,20 @@ impl Default for Server { |
641 | Server { |
642 | address: DEFAULT_LISTEN_ADDR.to_string(), |
643 | global_timeout: Duration::from_secs(DEFAULT_GLOBAL_TIMEOUT_SECS), |
644 | - options: None, |
645 | + pipelining: true, |
646 | milter: None, |
647 | delivery: None, |
648 | n_threads: std::thread::available_parallelism().unwrap().into(), |
649 | shutdown_handles: vec![], |
650 | dkim_verification: false, |
651 | spf_verification: false, |
652 | + list_expansion: None, |
653 | + plain_auth: None, |
654 | + verification: None, |
655 | resolver: None, |
656 | tls_certificates: None, |
657 | proxy_protocol: false, |
658 | + session: Session::default(), |
659 | } |
660 | } |
661 | } |
662 | @@ -146,11 +127,10 @@ impl Server { |
663 | self |
664 | } |
665 | |
666 | - /// Set session level options that affect the behavior of individual SMTP |
667 | - /// sessions. Most custom behavior is implemented here but not specifying |
668 | - /// any options will provide a limited but functional server. |
669 | - pub fn with_session_opts(mut self, opts: SessionOptions) -> Self { |
670 | - self.options = Some(opts); |
671 | + /// If piplining is supported in the transport, typically should be yes |
672 | + /// but the session could explicitly disable it. |
673 | + pub fn pipelining(mut self, enabled: bool) -> Self { |
674 | + self.pipelining = enabled; |
675 | self |
676 | } |
677 | |
678 | @@ -172,6 +152,22 @@ impl Server { |
679 | self |
680 | } |
681 | |
682 | + pub fn list_expansion<T>(mut self, expansion: T) -> Self |
683 | + where |
684 | + T: crate::expand::Expansion + 'static, |
685 | + { |
686 | + self.list_expansion = Some(Arc::new(expansion)); |
687 | + self |
688 | + } |
689 | + |
690 | + pub fn verification<T>(mut self, verification: T) -> Self |
691 | + where |
692 | + T: crate::verify::Verify + 'static, |
693 | + { |
694 | + self.verification = Some(Arc::new(verification)); |
695 | + self |
696 | + } |
697 | + |
698 | /// Perform DKIM Verification |
699 | pub fn dkim_verification(mut self, enabled: bool) -> Self { |
700 | self.dkim_verification = enabled; |
701 | @@ -184,6 +180,14 @@ impl Server { |
702 | self |
703 | } |
704 | |
705 | + pub fn plain_auth<T>(mut self, plain_auth: T) -> Self |
706 | + where |
707 | + T: crate::auth::PlainAuth + 'static, |
708 | + { |
709 | + self.plain_auth = Some(Arc::new(plain_auth)); |
710 | + self |
711 | + } |
712 | + |
713 | /// TLS Certificates, implies that the server should listen for TLS |
714 | /// connections and maybe support STARTTLS if configured in the Session |
715 | /// options. |
716 | @@ -192,7 +196,7 @@ impl Server { |
717 | self |
718 | } |
719 | |
720 | - /// Enable support for HAProxy's |
721 | + /// Enable support for HAProxy's |
722 | /// [Proxy Protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) |
723 | pub fn proxy_protocol(mut self, enabled: bool) -> Self { |
724 | self.proxy_protocol = enabled; |
725 | @@ -214,70 +218,135 @@ impl Server { |
726 | } |
727 | |
728 | /// drive the session forward |
729 | - async fn next<T>( |
730 | + async fn on_frame( |
731 | &self, |
732 | - framed: &mut Framed<T, Transport>, |
733 | + conn: impl Opportunistic, |
734 | session: &mut Session, |
735 | - queue: Arc<Injector<Envelope>>, |
736 | - tls_active: bool, |
737 | - ) -> Result<Action, ServerError> |
738 | - where |
739 | - T: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin, |
740 | - { |
741 | - match timeout(self.global_timeout, framed.next()).await { |
742 | - Ok(Some(Ok(Command::Requests(commands)))) => { |
743 | - let shutdown = is_quit(commands.as_slice()); |
744 | - let starttls = is_starttls(commands.as_slice()); |
745 | - for command in commands { |
746 | - match session.process(&command).await { |
747 | - Ok(responses) => { |
748 | + ) -> Result<Action, ServerError> { |
749 | + match timeout(self.global_timeout, conn.next()).await { |
750 | + Ok(Some(Ok(Command::Requests(requests)))) => { |
751 | + for request in requests { |
752 | + let action = session.next(Some(&request)); |
753 | + match action { |
754 | + crate::session::Action::Send(response) => { |
755 | + conn.send(response).await?; |
756 | + } |
757 | + crate::session::Action::SendMany(responses) => { |
758 | for response in responses { |
759 | - framed.send(response).await?; |
760 | + conn.send(response).await?; |
761 | } |
762 | } |
763 | - Err(e) => { |
764 | - tracing::warn!("Client error: {:?}", e); |
765 | - let fatal = e.is_fatal(); |
766 | - framed.send(e).await?; |
767 | - if fatal { |
768 | - return Ok(Action::Shutdown); |
769 | - } else { |
770 | - return Ok(Action::Continue); |
771 | + crate::session::Action::BDat { |
772 | + initial_response, |
773 | + cb, |
774 | + } => { |
775 | + conn.send(initial_response).await?; |
776 | + match conn.next().await { |
777 | + Some(Ok(Command::Payload(payload))) => match cb(payload) { |
778 | + crate::session::Action::Send(response) => { |
779 | + conn.send(response).await?; |
780 | + } |
781 | + _ => unreachable!(), |
782 | + }, |
783 | + _ => unreachable!(), |
784 | } |
785 | } |
786 | + crate::session::Action::Data { |
787 | + initial_response, |
788 | + cb, |
789 | + } => { |
790 | + conn.send(initial_response).await?; |
791 | + match conn.next().await { |
792 | + Some(Ok(Command::Payload(payload))) => match cb(payload) { |
793 | + crate::session::Action::Send(response) => { |
794 | + conn.send(response).await?; |
795 | + return Ok(Action::Enqueue); |
796 | + } |
797 | + _ => unreachable!(), |
798 | + }, |
799 | + _ => unreachable!(), |
800 | + } |
801 | + } |
802 | + crate::session::Action::SpfVerification { |
803 | + ip_addr, |
804 | + helo_domain, |
805 | + host_domain, |
806 | + mail_from, |
807 | + cb, |
808 | + } => { |
809 | + let resolver = self.resolver.as_ref().expect("resolver not configured"); |
810 | + let resolver = resolver.lock().await; |
811 | + match cb(Validation(resolver) |
812 | + .verify_spf(ip_addr, &helo_domain, &host_domain, mail_from.as_str()) |
813 | + .await) |
814 | + { |
815 | + crate::session::Action::Send(response) => { |
816 | + conn.send(response).await?; |
817 | + return Ok(Action::Continue); |
818 | + } |
819 | + _ => unreachable!(), |
820 | + } |
821 | + } |
822 | + crate::session::Action::PlainAuth { |
823 | + authcid, |
824 | + authzid, |
825 | + password, |
826 | + cb, |
827 | + } => { |
828 | + let plain_auth = self |
829 | + .plain_auth |
830 | + .as_ref() |
831 | + .expect("authentication not available"); |
832 | + match cb(plain_auth.authenticate(&authcid, &authzid, &password).await) { |
833 | + crate::session::Action::Send(response) => { |
834 | + conn.send(response).await?; |
835 | + return Ok(Action::Continue); |
836 | + } |
837 | + _ => unreachable!(), |
838 | + } |
839 | + } |
840 | + crate::session::Action::Verify { address, cb } => { |
841 | + let verification = self |
842 | + .verification |
843 | + .as_ref() |
844 | + .expect("verification not available"); |
845 | + match cb(verification.verify(&address).await) { |
846 | + crate::session::Action::Send(response) => { |
847 | + conn.send(response).await?; |
848 | + return Ok(Action::Continue); |
849 | + } |
850 | + _ => unreachable!(), |
851 | + } |
852 | + } |
853 | + crate::session::Action::Expand { address, cb } => { |
854 | + let expansion = self |
855 | + .list_expansion |
856 | + .as_ref() |
857 | + .expect("expansion not available"); |
858 | + match cb(expansion.expand(&address).await) { |
859 | + crate::session::Action::Send(response) => { |
860 | + conn.send(response).await?; |
861 | + return Ok(Action::Continue); |
862 | + } |
863 | + _ => unreachable!(), |
864 | + } |
865 | + } |
866 | + crate::session::Action::StartTls(response) => { |
867 | + // Go ahead |
868 | + conn.send(response).await?; |
869 | + return Ok(Action::TlsUpgrade); |
870 | + } |
871 | + crate::session::Action::Quit(response) => { |
872 | + conn.send(response).await?; |
873 | + return Ok(Action::Shutdown); |
874 | + } |
875 | } |
876 | } |
877 | - if starttls { |
878 | - if tls_active { |
879 | - tracing::warn!( |
880 | - "Client attempted to upgrade to TLS but they already have TLS" |
881 | - ); |
882 | - framed.send(crate::session::tls_already_active()).await?; |
883 | - return Ok(Action::Continue); |
884 | - } |
885 | - Ok(Action::TlsUpgrade) |
886 | - } else if shutdown { |
887 | - Ok(Action::Shutdown) |
888 | - } else { |
889 | - Ok(Action::Continue) |
890 | - } |
891 | + Ok(Action::Continue) |
892 | } |
893 | - Ok(Some(Ok(Command::Payload(payload)))) => match session.handle_data(&payload).await { |
894 | - Ok(responses) => { |
895 | - for response in responses { |
896 | - framed.send(response).await?; |
897 | - } |
898 | - queue.push(Envelope::from(&session.clone())); |
899 | - Ok(Action::Continue) |
900 | - } |
901 | - Err(response) => { |
902 | - tracing::warn!("Error handling message payload: {:?}", response); |
903 | - framed.send(response).await?; |
904 | - Ok(Action::Continue) |
905 | - } |
906 | - }, |
907 | + Ok(Some(Ok(Command::Payload(_)))) => unreachable!(), |
908 | Ok(Some(Err(err))) => { |
909 | - tracing::warn!("Client Error: {}", err); |
910 | + tracing::warn!("Transport Error: {}", err); |
911 | let response = match err { |
912 | crate::transport::TransportError::PipelineNotEnabled => { |
913 | crate::smtp_response!(500, 0, 0, 0, "Pipelining is not enabled") |
914 | @@ -288,65 +357,88 @@ impl Server { |
915 | // IO Errors considered fatal for the entire session |
916 | crate::transport::TransportError::Io(e) => return Err(ServerError::Io(e)), |
917 | }; |
918 | - framed.send(response).await?; |
919 | + conn.send(response).await?; |
920 | Ok(Action::Continue) |
921 | } |
922 | Ok(None) => Ok(Action::Shutdown), |
923 | - Err(e) => { |
924 | - tracing::warn!("Client connection exceeded: {:?}", self.global_timeout); |
925 | - framed.send(crate::session::timeout(&e.to_string())).await?; |
926 | + Err(elapsed) => { |
927 | + tracing::warn!("Client timeout: {}", elapsed); |
928 | + conn.send(crate::session::timeout(&elapsed.to_string())) |
929 | + .await?; |
930 | Err(ServerError::Timeout(self.global_timeout.as_secs())) |
931 | } |
932 | } |
933 | } |
934 | |
935 | - /// Serve a plain SMTP connection that may be upgradable to TLS. |
936 | async fn serve_plain<T>( |
937 | &self, |
938 | stream: &mut T, |
939 | msg_queue: Arc<Injector<Envelope>>, |
940 | - pipelining: bool, |
941 | remote_addr: SocketAddr, |
942 | ) -> Result<(), ServerError> |
943 | where |
944 | T: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin, |
945 | { |
946 | - let mut session = Session::default() |
947 | - .with_options( |
948 | - self.options |
949 | - .clone() |
950 | - .unwrap_or_default() |
951 | - .ip_addr(remote_addr.ip()), |
952 | - ) |
953 | - .resolver(self.resolver.clone()) |
954 | - .spf_verification(self.spf_verification); |
955 | - |
956 | - let greeting = session.greeting(); |
957 | - |
958 | - let transport = Transport::default().pipelining(pipelining); |
959 | - |
960 | - let mut framed = Framed::new(&mut *stream, transport.clone()); |
961 | + let mut session = self |
962 | + .session |
963 | + .clone() |
964 | + .client_ip(remote_addr.ip()) |
965 | + .starttls(self.tls_certificates.is_some()) |
966 | + .expn_enabled(self.list_expansion.is_some()); |
967 | + |
968 | + let mut framed = Framed::new( |
969 | + &mut *stream, |
970 | + Transport::default().pipelining(self.pipelining), |
971 | + ); |
972 | + |
973 | + // initialize the connection with a greeting |
974 | + match session.next(None) { |
975 | + crate::session::Action::Send(response) => { |
976 | + framed.send(response).await?; |
977 | + } |
978 | + _ => unreachable!(), |
979 | + } |
980 | |
981 | - framed.send(greeting).await?; |
982 | + let framed = Arc::new(Mutex::new(framed)); |
983 | |
984 | loop { |
985 | match self |
986 | - .next(&mut framed, &mut session, msg_queue.clone(), false) |
987 | + .on_frame( |
988 | + Plain { |
989 | + inner: framed.clone(), |
990 | + }, |
991 | + &mut session, |
992 | + ) |
993 | .await? |
994 | { |
995 | Action::Continue => {} |
996 | + Action::Enqueue => { |
997 | + msg_queue.push(session.envelope()); |
998 | + } |
999 | Action::Shutdown => return Ok(()), |
1000 | Action::TlsUpgrade => { |
1001 | let acceptor = TlsAcceptor::from(Arc::new(self.rustls_config().await?)); |
1002 | let tls_stream = acceptor.accept(&mut *stream).await?; |
1003 | - let mut tls_framed = |
1004 | - Framed::new(tls_stream, transport.clone()); |
1005 | + let tls_framed = |
1006 | + Framed::new(tls_stream, Transport::default().pipelining(self.pipelining)); |
1007 | + let tls_framed = Arc::new(Mutex::new(tls_framed)); |
1008 | + // Per the RFC after TLS is established the session is |
1009 | + // reset. |
1010 | + let mut tls_session = session.clone().tls_active(true); |
1011 | loop { |
1012 | match self |
1013 | - .next(&mut tls_framed, &mut session, msg_queue.clone(), true) |
1014 | + .on_frame( |
1015 | + Tls { |
1016 | + inner: tls_framed.clone(), |
1017 | + }, |
1018 | + &mut tls_session, |
1019 | + ) |
1020 | .await? |
1021 | { |
1022 | Action::Continue => {} |
1023 | + Action::Enqueue => { |
1024 | + msg_queue.push(session.envelope()); |
1025 | + } |
1026 | Action::Shutdown => return Ok(()), |
1027 | Action::TlsUpgrade => unreachable!(), |
1028 | } |
1029 | @@ -406,7 +498,7 @@ impl Server { |
1030 | }); |
1031 | } |
1032 | |
1033 | - pub async fn listen(&mut self) -> Result<(), Error> { |
1034 | + pub async fn listen(&mut self) -> Result<(), ServerError> { |
1035 | let listener = TcpListener::bind(&self.address).await?; |
1036 | tracing::info!("Mail server listening @ {}", self.address); |
1037 | self.resolver = if self.spf_verification || self.dkim_verification { |
1038 | @@ -440,13 +532,8 @@ impl Server { |
1039 | } else { |
1040 | addr |
1041 | }; |
1042 | - let pipelining = self |
1043 | - .options |
1044 | - .as_ref() |
1045 | - .is_some_and(|opts| opts.capabilities & smtp_proto::EXT_PIPELINING != 0) |
1046 | - || self.options.is_none(); |
1047 | match self |
1048 | - .serve_plain(&mut socket, global_queue.clone(), pipelining, addr) |
1049 | + .serve_plain(&mut socket, global_queue.clone(), addr) |
1050 | .await |
1051 | { |
1052 | Ok(_) => { |
1053 | @@ -463,8 +550,6 @@ impl Server { |
1054 | #[cfg(test)] |
1055 | mod test { |
1056 | |
1057 | - use crate::SessionOptions; |
1058 | - |
1059 | use super::*; |
1060 | |
1061 | use std::io; |
1062 | @@ -535,15 +620,13 @@ mod test { |
1063 | ], |
1064 | ..Default::default() |
1065 | }; |
1066 | - let server = Server::default() |
1067 | - // turn off all extended capabilities |
1068 | - .with_session_opts(SessionOptions::default().capabilities(0)); |
1069 | + let server = Server::default(); |
1070 | + // turn off all extended capabilities |
1071 | let global_queue = Arc::new(Injector::<Envelope>::new()); |
1072 | server |
1073 | .serve_plain( |
1074 | &mut stream, |
1075 | global_queue.clone(), |
1076 | - false, |
1077 | SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 25)), |
1078 | ) |
1079 | .await |
1080 | @@ -575,7 +658,6 @@ mod test { |
1081 | .serve_plain( |
1082 | &mut stream, |
1083 | global_queue.clone(), |
1084 | - false, |
1085 | SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 25)), |
1086 | ) |
1087 | .await |
1088 | diff --git a/maitred/src/session.rs b/maitred/src/session.rs |
1089 | index 8ec2e7c..51d1c2d 100644 |
1090 | --- a/maitred/src/session.rs |
1091 | +++ b/maitred/src/session.rs |
1092 | @@ -1,24 +1,18 @@ |
1093 | + use std::fmt::Display; |
1094 | use std::net::IpAddr; |
1095 | - use std::rc::Rc; |
1096 | - use std::result::Result as StdResult; |
1097 | use std::str::FromStr; |
1098 | - use std::sync::Arc; |
1099 | |
1100 | use bytes::Bytes; |
1101 | use email_address::EmailAddress; |
1102 | |
1103 | - use mail_auth::Resolver; |
1104 | use mail_parser::{Message, MessageParser}; |
1105 | use smtp_proto::{EhloResponse, Request, Response as SmtpResponse}; |
1106 | - use tokio::sync::Mutex; |
1107 | use url::Host; |
1108 | |
1109 | - use crate::auth::{AuthData, PlainAuth}; |
1110 | - use crate::expand::Expansion; |
1111 | + use crate::auth::{AuthData, AuthError}; |
1112 | + use crate::expand::ExpansionError; |
1113 | use crate::smtp_response; |
1114 | - use crate::transport::Response; |
1115 | - use crate::validation::Validation; |
1116 | - use crate::verify::Verify; |
1117 | + use crate::verify::VerifyError; |
1118 | |
1119 | /// Default help banner returned from a HELP command without any parameters |
1120 | pub const DEFAULT_HELP_BANNER: &str = r#" |
1121 | @@ -43,9 +37,168 @@ pub const DEFAULT_CAPABILITIES: u32 = smtp_proto::EXT_SIZE |
1122 | | smtp_proto::EXT_PIPELINING |
1123 | | smtp_proto::EXT_8BIT_MIME; |
1124 | |
1125 | + #[derive(Debug, Clone)] |
1126 | + pub enum Response<T> |
1127 | + where |
1128 | + T: Display, |
1129 | + { |
1130 | + General(SmtpResponse<T>), |
1131 | + Ehlo(EhloResponse<T>), |
1132 | + } |
1133 | + |
1134 | + impl Response<String> { |
1135 | + pub fn is_fatal(&self) -> bool { |
1136 | + match self { |
1137 | + Response::General(resp) => resp.code >= 500, |
1138 | + Response::Ehlo(_) => false, |
1139 | + } |
1140 | + } |
1141 | + } |
1142 | + |
1143 | + impl<T> PartialEq for Response<T> |
1144 | + where |
1145 | + T: Display, |
1146 | + { |
1147 | + fn eq(&self, other: &Self) -> bool { |
1148 | + match self { |
1149 | + Response::General(req) => match other { |
1150 | + Response::General(other) => req.to_string() == other.to_string(), |
1151 | + Response::Ehlo(_) => false, |
1152 | + }, |
1153 | + Response::Ehlo(req) => match other { |
1154 | + Response::General(_) => false, |
1155 | + Response::Ehlo(other) => { |
1156 | + // FIXME |
1157 | + req.capabilities == other.capabilities |
1158 | + && req.hostname.to_string() == other.hostname.to_string() |
1159 | + && req.deliver_by == other.deliver_by |
1160 | + && req.size == other.size |
1161 | + && req.auth_mechanisms == other.auth_mechanisms |
1162 | + && req.future_release_datetime.eq(&req.future_release_datetime) |
1163 | + && req.future_release_interval.eq(&req.future_release_interval) |
1164 | + } |
1165 | + }, |
1166 | + } |
1167 | + } |
1168 | + } |
1169 | + |
1170 | + impl<T> Eq for Response<T> where T: Display {} |
1171 | + |
1172 | + /// An Envelope containing an e-mail message created from the session |
1173 | + #[derive(Clone, Debug)] |
1174 | + pub struct Envelope { |
1175 | + pub body: Message<'static>, |
1176 | + pub mail_from: EmailAddress, |
1177 | + pub rcpt_to: Vec<EmailAddress>, |
1178 | + pub hostname: Host, |
1179 | + } |
1180 | + |
1181 | + pub enum Action<'a> { |
1182 | + Send(Response<String>), |
1183 | + SendMany(Vec<Response<String>>), |
1184 | + BDat { |
1185 | + initial_response: Response<String>, |
1186 | + cb: Box<dyn FnOnce(Bytes) -> Action<'a> + 'a>, |
1187 | + }, |
1188 | + Data { |
1189 | + initial_response: Response<String>, |
1190 | + cb: Box<dyn FnOnce(Bytes) -> Action<'a> + 'a>, |
1191 | + }, |
1192 | + SpfVerification { |
1193 | + ip_addr: IpAddr, |
1194 | + helo_domain: String, |
1195 | + host_domain: String, |
1196 | + mail_from: EmailAddress, |
1197 | + cb: Box<dyn FnOnce(bool) -> Action<'a> + 'a>, |
1198 | + }, |
1199 | + PlainAuth { |
1200 | + authcid: String, |
1201 | + authzid: String, |
1202 | + password: String, |
1203 | + cb: Box<dyn FnOnce(Result<(), AuthError>) -> Action<'a> + 'a>, |
1204 | + }, |
1205 | + Verify { |
1206 | + address: EmailAddress, |
1207 | + cb: Box<dyn FnOnce(Result<(), VerifyError>) -> Action<'a> + 'a>, |
1208 | + }, |
1209 | + Expand { |
1210 | + address: String, |
1211 | + cb: Box<dyn FnOnce(Result<Vec<EmailAddress>, ExpansionError>) -> Action<'a> + 'a>, |
1212 | + }, |
1213 | + StartTls(Response<String>), |
1214 | + Quit(Response<String>), |
1215 | + } |
1216 | + |
1217 | + impl Display for Action<'_> { |
1218 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
1219 | + match self { |
1220 | + Action::Send(response) => match response { |
1221 | + Response::General(response) => { |
1222 | + f.write_fmt(format_args!("Send:{}", &response.to_string())) |
1223 | + } |
1224 | + Response::Ehlo(ehlo_response) => { |
1225 | + f.write_fmt(format_args!("Send:{:?}", ehlo_response)) |
1226 | + } |
1227 | + }, |
1228 | + Action::SendMany(vec) => { |
1229 | + f.write_str("Send Many:\n")?; |
1230 | + vec.iter().for_each(|message| match message { |
1231 | + Response::General(response) => f |
1232 | + .write_fmt(format_args!("{}\n", &response.to_string())) |
1233 | + .unwrap(), |
1234 | + Response::Ehlo(_ehlo_response) => unreachable!(), |
1235 | + }); |
1236 | + Ok(()) |
1237 | + } |
1238 | + Action::BDat { |
1239 | + initial_response, |
1240 | + cb: _, |
1241 | + } => match initial_response { |
1242 | + Response::General(response) => f.write_fmt(format_args!("BDat:\n{}", response)), |
1243 | + Response::Ehlo(_ehlo_response) => unreachable!(), |
1244 | + }, |
1245 | + Action::Data { |
1246 | + initial_response, |
1247 | + cb: _, |
1248 | + } => match initial_response { |
1249 | + Response::General(response) => f.write_fmt(format_args!("Data:\n{}", response)), |
1250 | + Response::Ehlo(_ehlo_response) => unreachable!(), |
1251 | + }, |
1252 | + Action::SpfVerification { |
1253 | + ip_addr, |
1254 | + helo_domain, |
1255 | + host_domain, |
1256 | + mail_from, |
1257 | + cb: _, |
1258 | + } => f.write_fmt(format_args!( |
1259 | + "Spf: ip={}, domain={}, us={}, mail={}", |
1260 | + ip_addr, helo_domain, host_domain, mail_from |
1261 | + )), |
1262 | + Action::PlainAuth { |
1263 | + authcid, |
1264 | + authzid, |
1265 | + password: _, |
1266 | + cb: _, |
1267 | + } => f.write_fmt(format_args!("Plain Auth: {} {}", authcid, authzid)), |
1268 | + Action::Verify { address, cb: _ } => f.write_fmt(format_args!("Verify: {}", address)), |
1269 | + Action::Expand { address, cb: _ } => f.write_fmt(format_args!("Expand: {}", address)), |
1270 | + Action::StartTls(response) => match response { |
1271 | + Response::General(response) => f.write_str(&response.to_string()), |
1272 | + Response::Ehlo(_ehlo_response) => unreachable!(), |
1273 | + }, |
1274 | + Action::Quit(response) => match response { |
1275 | + Response::General(response) => { |
1276 | + f.write_fmt(format_args!("Quit: {}", &response.to_string())) |
1277 | + } |
1278 | + Response::Ehlo(_ehlo_response) => unreachable!(), |
1279 | + }, |
1280 | + } |
1281 | + } |
1282 | + } |
1283 | + |
1284 | /// Result generated as part of an SMTP session, an Err indicates a session |
1285 | /// level error that will be returned to the client. |
1286 | - pub type Result = StdResult<Vec<Response<String>>, Response<String>>; |
1287 | + // pub type Result = StdResult<Action, Response<String>>; |
1288 | |
1289 | /// If the session was started with HELO or ELHO. |
1290 | #[derive(Clone)] |
1291 | @@ -54,13 +207,6 @@ enum Mode { |
1292 | Extended, |
1293 | } |
1294 | |
1295 | - /// Type of data transfer mode in use. |
1296 | - #[derive(Clone)] |
1297 | - enum DataTransfer { |
1298 | - Data, |
1299 | - Bdat, |
1300 | - } |
1301 | - |
1302 | /// Sent when the connection exceeds the maximum configured timeout |
1303 | pub fn timeout(message: &str) -> Response<String> { |
1304 | smtp_response!(421, 4, 4, 2, format!("Timeout exceeded: {}", message)) |
1305 | @@ -125,41 +271,77 @@ fn parse_host(host: &str) -> String { |
1306 | } |
1307 | } |
1308 | |
1309 | - /// Session level options that configure individual SMTP transactions |
1310 | + /// session runtime flags |
1311 | + #[derive(Clone, Default)] |
1312 | + struct Flags { |
1313 | + authentication: bool, |
1314 | + starttls: bool, |
1315 | + vrfy: bool, |
1316 | + expn: bool, |
1317 | + spf: bool, |
1318 | + } |
1319 | + |
1320 | + /// State machine that corresponds to a single SMTP session, calls to next |
1321 | + /// return actions that the caller is expected to implement in a transport. |
1322 | #[derive(Clone)] |
1323 | - pub struct SessionOptions { |
1324 | - pub our_hostname: String, |
1325 | - pub maximum_size: u64, |
1326 | - pub capabilities: u32, |
1327 | - pub help_banner: String, |
1328 | - pub greeting: String, |
1329 | - pub list_expansion: Option<Arc<dyn Expansion>>, |
1330 | - pub verification: Option<Arc<dyn Verify>>, |
1331 | - pub plain_auth: Option<Arc<dyn PlainAuth>>, |
1332 | - pub ip_addr: Option<IpAddr>, |
1333 | - pub starttls_enabled: Option<bool>, |
1334 | + pub struct Session { |
1335 | + /// message body |
1336 | + pub body: Option<Message<'static>>, |
1337 | + /// mailto address |
1338 | + pub mail_from: Option<EmailAddress>, |
1339 | + /// rcpt address |
1340 | + pub rcpt_to: Option<Vec<EmailAddress>>, |
1341 | + pub hostname: Option<Host>, |
1342 | + initialized: Option<Mode>, |
1343 | + // previously ran commands |
1344 | + // TODO pipeline still partially broken |
1345 | + history: Vec<Request<String>>, |
1346 | + |
1347 | + // session opts |
1348 | + our_hostname: Option<String>, // required |
1349 | + client_ip: Option<IpAddr>, |
1350 | + maximum_size: u64, |
1351 | + capabilities: u32, |
1352 | + help_banner: String, |
1353 | + greeting: String, |
1354 | + tls_active: bool, |
1355 | + |
1356 | + spf_verified_host: Option<String>, |
1357 | + authenticated_id: Option<String>, |
1358 | + flags: Flags, |
1359 | } |
1360 | |
1361 | - impl Default for SessionOptions { |
1362 | + impl Default for Session { |
1363 | fn default() -> Self { |
1364 | - SessionOptions { |
1365 | - our_hostname: String::default(), |
1366 | + Session { |
1367 | + body: None, |
1368 | + mail_from: None, |
1369 | + rcpt_to: None, |
1370 | + hostname: None, |
1371 | + initialized: None, |
1372 | + history: Vec::new(), |
1373 | + our_hostname: None, |
1374 | + client_ip: None, |
1375 | maximum_size: DEFAULT_MAXIMUM_MESSAGE_SIZE, |
1376 | capabilities: DEFAULT_CAPABILITIES, |
1377 | help_banner: DEFAULT_HELP_BANNER.to_string(), |
1378 | greeting: DEFAULT_GREETING.to_string(), |
1379 | - list_expansion: None, |
1380 | - verification: None, |
1381 | - plain_auth: None, |
1382 | - ip_addr: None, |
1383 | - starttls_enabled: None, |
1384 | + tls_active: false, |
1385 | + spf_verified_host: None, |
1386 | + authenticated_id: None, |
1387 | + flags: Flags::default(), |
1388 | } |
1389 | } |
1390 | } |
1391 | |
1392 | - impl SessionOptions { |
1393 | + impl Session { |
1394 | pub fn our_hostname(mut self, hostname: &str) -> Self { |
1395 | - self.our_hostname = hostname.to_string(); |
1396 | + self.our_hostname = Some(hostname.to_string()); |
1397 | + self |
1398 | + } |
1399 | + |
1400 | + pub fn spf_verification(mut self, verify_spf: bool) -> Self { |
1401 | + self.flags.spf = verify_spf; |
1402 | self |
1403 | } |
1404 | |
1405 | @@ -178,82 +360,40 @@ impl SessionOptions { |
1406 | self |
1407 | } |
1408 | |
1409 | - pub fn starttls_enabled(mut self, enabled: bool) -> Self { |
1410 | - if enabled { |
1411 | - self.capabilities |= smtp_proto::EXT_START_TLS; |
1412 | - } |
1413 | - self.starttls_enabled = Some(enabled); |
1414 | + pub fn greeting_banner(mut self, greeting: &str) -> Self { |
1415 | + self.greeting = greeting.to_string(); |
1416 | self |
1417 | } |
1418 | |
1419 | - pub fn list_expansion<T>(mut self, expansion: T) -> Self |
1420 | - where |
1421 | - T: crate::expand::Expansion + 'static, |
1422 | - { |
1423 | - self.list_expansion = Some(Arc::new(expansion)); |
1424 | - self |
1425 | - } |
1426 | - |
1427 | - pub fn verification<T>(mut self, verification: T) -> Self |
1428 | - where |
1429 | - T: crate::verify::Verify + 'static, |
1430 | - { |
1431 | - self.verification = Some(Arc::new(verification)); |
1432 | + pub fn authentication(mut self, enabled: bool) -> Self { |
1433 | + self.flags.authentication = enabled; |
1434 | + self.capabilities |= smtp_proto::EXT_AUTH; |
1435 | self |
1436 | } |
1437 | |
1438 | - pub fn plain_auth<T>(mut self, plain_auth: T) -> Self |
1439 | - where |
1440 | - T: crate::auth::PlainAuth + 'static, |
1441 | - { |
1442 | - self.capabilities |= smtp_proto::EXT_AUTH; |
1443 | - self.plain_auth = Some(Arc::new(plain_auth)); |
1444 | + pub fn client_ip(mut self, client_ip: IpAddr) -> Self { |
1445 | + self.client_ip = Some(client_ip); |
1446 | self |
1447 | } |
1448 | |
1449 | - pub fn ip_addr(mut self, ip_addr: IpAddr) -> Self { |
1450 | - self.ip_addr = Some(ip_addr); |
1451 | + pub fn starttls(mut self, enabled: bool) -> Self { |
1452 | + self.flags.starttls = enabled; |
1453 | + self.capabilities |= smtp_proto::EXT_START_TLS; |
1454 | self |
1455 | } |
1456 | - } |
1457 | - |
1458 | - /// Stateful connection that coresponds to a single SMTP session. |
1459 | - #[derive(Clone, Default)] |
1460 | - pub(crate) struct Session { |
1461 | - /// message body |
1462 | - pub body: Option<Message<'static>>, |
1463 | - /// mailto address |
1464 | - pub mail_from: Option<EmailAddress>, |
1465 | - /// rcpt address |
1466 | - pub rcpt_to: Option<Vec<EmailAddress>>, |
1467 | - pub hostname: Option<Host>, |
1468 | - // If an active data transfer is taking place |
1469 | - data_transfer: Option<DataTransfer>, |
1470 | - initialized: Option<Mode>, |
1471 | - spf_verification: bool, |
1472 | - auth_initialized: bool, |
1473 | - // session options |
1474 | - opts: Rc<SessionOptions>, |
1475 | - // previously ran commands |
1476 | - // TODO pipeline still partially broken |
1477 | - history: Vec<Request<String>>, |
1478 | - resolver: Option<Arc<Mutex<Resolver>>>, |
1479 | - } |
1480 | |
1481 | - impl Session { |
1482 | - pub fn spf_verification(mut self, verify_spf: bool) -> Self { |
1483 | - self.spf_verification = verify_spf; |
1484 | + pub fn vrfy_enabled(mut self, enabled: bool) -> Self { |
1485 | + self.flags.vrfy = enabled; |
1486 | self |
1487 | } |
1488 | |
1489 | - pub fn resolver(mut self, resolver: Option<Arc<Mutex<Resolver>>>) -> Self { |
1490 | - self.resolver = resolver; |
1491 | + pub fn expn_enabled(mut self, enabled: bool) -> Self { |
1492 | + self.flags.expn = enabled; |
1493 | self |
1494 | } |
1495 | |
1496 | - /// Configure a session with various options that effect it's behavior. |
1497 | - pub fn with_options(mut self, opts: SessionOptions) -> Self { |
1498 | - self.opts = Rc::new(opts); |
1499 | + pub fn tls_active(mut self, active: bool) -> Self { |
1500 | + self.tls_active = active; |
1501 | self |
1502 | } |
1503 | |
1504 | @@ -265,18 +405,24 @@ impl Session { |
1505 | self.rcpt_to = None; |
1506 | // FIXME: is the hostname reset? |
1507 | // self.hostname = None; |
1508 | - self.data_transfer = None; |
1509 | self.history = Vec::new(); |
1510 | + self.spf_verified_host = None; |
1511 | } |
1512 | + |
1513 | /// A greeting must be sent at the start of an SMTP connection when it is |
1514 | /// first initialized. |
1515 | + /// FIXME |
1516 | pub fn greeting(&self) -> Response<String> { |
1517 | smtp_response!( |
1518 | 220, |
1519 | 2, |
1520 | 0, |
1521 | 0, |
1522 | - format!("{} {}", self.opts.our_hostname, self.opts.greeting) |
1523 | + format!( |
1524 | + "{} {}", |
1525 | + self.our_hostname.clone().expect("hostname not configured"), |
1526 | + self.greeting |
1527 | + ) |
1528 | ) |
1529 | } |
1530 | |
1531 | @@ -285,11 +431,11 @@ impl Session { |
1532 | self.initialized |
1533 | .as_ref() |
1534 | .is_some_and(|mode| matches!(mode, Mode::Extended)) |
1535 | - && self.opts.capabilities & capability != 0 |
1536 | + && self.capabilities & capability != 0 |
1537 | } |
1538 | |
1539 | /// Ensure that the session has been initialized otherwise return an error |
1540 | - fn check_initialized(&self) -> StdResult<(), Response<String>> { |
1541 | + fn check_initialized(&self) -> Result<(), Response<String>> { |
1542 | if self.initialized.is_none() { |
1543 | return Err(smtp_response!( |
1544 | 500, |
1545 | @@ -303,274 +449,328 @@ impl Session { |
1546 | } |
1547 | |
1548 | /// checks if 8BITMIME is supported |
1549 | - fn check_body(&self, body: &[u8]) -> StdResult<(), Response<String>> { |
1550 | + fn check_body(&self, body: &[u8]) -> Result<(), Response<String>> { |
1551 | if !self.has_capability(smtp_proto::EXT_8BIT_MIME) && !body.is_ascii() { |
1552 | return Err(smtp_response!( |
1553 | 500, |
1554 | 0, |
1555 | 0, |
1556 | 0, |
1557 | - "Non ascii characters found in message body" |
1558 | + "Non ASCII characters found in message body" |
1559 | )); |
1560 | } |
1561 | Ok(()) |
1562 | } |
1563 | |
1564 | - pub async fn handle_data(&mut self, data: &Bytes) -> Result { |
1565 | - self.check_initialized()?; |
1566 | - let transfer_mode = self |
1567 | - .data_transfer |
1568 | - .as_ref() |
1569 | - .expect("transfer is not initalized"); |
1570 | - match transfer_mode { |
1571 | - DataTransfer::Data => { |
1572 | - let message_payload = data.to_vec(); |
1573 | - self.check_body(&message_payload)?; |
1574 | - let parser = MessageParser::new(); |
1575 | - match parser.parse(&message_payload) { |
1576 | - Some(msg) => { |
1577 | - self.body = Some(msg.into_owned()); |
1578 | - self.data_transfer = None; |
1579 | - Ok(vec![smtp_response!(250, 0, 0, 0, "OK")]) |
1580 | - } |
1581 | - None => { |
1582 | - self.data_transfer = None; |
1583 | - Ok(vec![smtp_response!( |
1584 | - 500, |
1585 | - 0, |
1586 | - 0, |
1587 | - 0, |
1588 | - "Cannot parse message payload".to_string() |
1589 | - )]) |
1590 | - } |
1591 | - } |
1592 | - } |
1593 | - DataTransfer::Bdat => { |
1594 | - let message_payload = data.to_vec(); |
1595 | - self.check_body(&message_payload)?; |
1596 | - let parser = MessageParser::new(); |
1597 | - match parser.parse(&message_payload) { |
1598 | - Some(msg) => { |
1599 | - self.body = Some(msg.into_owned()); |
1600 | - self.data_transfer = None; |
1601 | - Ok(vec![smtp_response!(250, 0, 0, 0, "OK")]) |
1602 | - } |
1603 | - None => { |
1604 | - self.data_transfer = None; |
1605 | - Ok(vec![smtp_response!( |
1606 | - 500, |
1607 | - 0, |
1608 | - 0, |
1609 | - 0, |
1610 | - "Cannot parse message payload".to_string() |
1611 | - )]) |
1612 | - } |
1613 | - } |
1614 | - } |
1615 | + pub fn envelope(&self) -> Envelope { |
1616 | + Envelope { |
1617 | + body: self.body.clone().unwrap(), |
1618 | + mail_from: self.mail_from.clone().unwrap(), |
1619 | + rcpt_to: self.rcpt_to.clone().unwrap(), |
1620 | + hostname: self.hostname.clone().unwrap(), |
1621 | } |
1622 | } |
1623 | |
1624 | - /// Statefully process the SMTP command with optional data payload, any |
1625 | - /// error returned is passed back to the caller. |
1626 | - /// NOTE: |
1627 | - /// Data transfers are detected in the transport level and handled by two |
1628 | - /// calls to process(). The first call contains the empty data request to |
1629 | - /// indicate that the process is starting and the second one contains the |
1630 | - /// parsed bytes from the transfer. |
1631 | - /// FIXME: Not at all reasonable yet |
1632 | - pub async fn process(&mut self, req: &Request<String>) -> Result { |
1633 | - self.history.push(req.clone()); |
1634 | + /// Process the SMTP command returning the action sometimes with a callback |
1635 | + /// that the implementor needs to take. |
1636 | + pub fn next(&mut self, req: Option<&Request<String>>) -> Action<'_> { |
1637 | + if let Some(req) = req { |
1638 | + self.history.push(req.clone()); |
1639 | + } |
1640 | match req { |
1641 | - Request::Ehlo { host } => { |
1642 | - self.hostname = Some( |
1643 | - Host::parse(&parse_host(host)) |
1644 | - .map_err(|e| smtp_response!(500, 0, 0, 0, e.to_string()))?, |
1645 | - ); |
1646 | + None => { |
1647 | + tracing::info!("Sending initial greeting"); |
1648 | + Action::Send(smtp_response!( |
1649 | + 220, |
1650 | + 2, |
1651 | + 0, |
1652 | + 0, |
1653 | + format!( |
1654 | + "{} {}", |
1655 | + self.our_hostname.clone().unwrap_or_default(), |
1656 | + self.greeting |
1657 | + ) |
1658 | + )) |
1659 | + } |
1660 | + Some(Request::Ehlo { host }) => { |
1661 | + match Host::parse(&parse_host(host)) { |
1662 | + Ok(hostname) => { |
1663 | + self.hostname = Some(hostname); |
1664 | + } |
1665 | + Err(e) => return Action::Send(smtp_response!(500, 0, 0, 0, e.to_string())), |
1666 | + }; |
1667 | self.reset(); |
1668 | self.initialized = Some(Mode::Extended); |
1669 | let mut resp = EhloResponse::new(format!("Hello {}", host)); |
1670 | - resp.capabilities = self.opts.capabilities; |
1671 | - resp.size = self.opts.maximum_size as usize; |
1672 | - if self.opts.plain_auth.is_some() { |
1673 | + resp.capabilities = self.capabilities; |
1674 | + resp.size = self.maximum_size as usize; |
1675 | + if self.flags.authentication { |
1676 | resp.auth_mechanisms = smtp_proto::AUTH_PLAIN; |
1677 | } |
1678 | - Ok(vec![Response::Ehlo(resp)]) |
1679 | + Action::Send(Response::Ehlo(resp)) |
1680 | } |
1681 | - Request::Lhlo { host } => { |
1682 | - self.hostname = Some( |
1683 | - Host::parse(&parse_host(host)).map_err(|e| smtp_response!(500, 0, 0, 0, e))?, |
1684 | - ); |
1685 | + Some(Request::Lhlo { host }) => { |
1686 | + match Host::parse(&parse_host(host)) { |
1687 | + Ok(hostname) => { |
1688 | + self.hostname = Some(hostname); |
1689 | + } |
1690 | + Err(e) => return Action::Send(smtp_response!(500, 0, 0, 0, e.to_string())), |
1691 | + }; |
1692 | self.reset(); |
1693 | self.initialized = Some(Mode::Legacy); |
1694 | - Ok(vec![smtp_response!( |
1695 | - 250, |
1696 | - 0, |
1697 | - 0, |
1698 | - 0, |
1699 | - format!("Hello {}", host) |
1700 | - )]) |
1701 | + Action::Send(smtp_response!(250, 0, 0, 0, format!("Hello {}", host))) |
1702 | } |
1703 | - Request::Helo { host } => { |
1704 | - self.hostname = Some( |
1705 | - Host::parse(&parse_host(host)) |
1706 | - .map_err(|e| smtp_response!(500, 0, 0, 0, e.to_string()))?, |
1707 | - ); |
1708 | + Some(Request::Helo { host }) => { |
1709 | + match Host::parse(&parse_host(host)) { |
1710 | + Ok(hostname) => { |
1711 | + self.hostname = Some(hostname); |
1712 | + } |
1713 | + Err(e) => return Action::Send(smtp_response!(500, 0, 0, 0, e.to_string())), |
1714 | + }; |
1715 | self.reset(); |
1716 | self.initialized = Some(Mode::Legacy); |
1717 | - Ok(vec![smtp_response!( |
1718 | - 250, |
1719 | - 0, |
1720 | - 0, |
1721 | - 0, |
1722 | - format!("Hello {}", host) |
1723 | - )]) |
1724 | + Action::Send(smtp_response!(250, 0, 0, 0, format!("Hello {}", host))) |
1725 | } |
1726 | - Request::Mail { from } => { |
1727 | - self.check_initialized()?; |
1728 | - let mail_from = EmailAddress::from_str(from.address.as_str()).map_err(|e| { |
1729 | - smtp_response!( |
1730 | - 500, |
1731 | - 0, |
1732 | - 0, |
1733 | - 0, |
1734 | - format!("cannot parse: {} {}", from.address, e) |
1735 | - ) |
1736 | - })?; |
1737 | + Some(Request::Mail { from }) => { |
1738 | + if let Some(err) = self.check_initialized().err() { |
1739 | + return Action::Send(err); |
1740 | + } |
1741 | + let mail_from = match EmailAddress::from_str(&from.address) { |
1742 | + Ok(addr) => addr, |
1743 | + Err(e) => { |
1744 | + return Action::Send(smtp_response!( |
1745 | + 500, |
1746 | + 0, |
1747 | + 0, |
1748 | + 0, |
1749 | + format!("cannot parse: {} {}", from.address, e) |
1750 | + )) |
1751 | + } |
1752 | + }; |
1753 | self.mail_from = Some(mail_from.clone()); |
1754 | - if self.spf_verification { |
1755 | + if self.flags.spf { |
1756 | tracing::info!("Running SPF Validation"); |
1757 | - let ip_addr = self.opts.ip_addr.ok_or(smtp_response!( |
1758 | - 500, |
1759 | - 0, |
1760 | - 0, |
1761 | - 0, |
1762 | - "Client has no IP Address" |
1763 | - ))?; |
1764 | - let helo_domain = self |
1765 | - .hostname |
1766 | + let ip_addr = match self.client_ip { |
1767 | + Some(ip_addr) => ip_addr, |
1768 | + None => { |
1769 | + return Action::Send(smtp_response!( |
1770 | + 500, |
1771 | + 0, |
1772 | + 0, |
1773 | + 0, |
1774 | + "Client has no IP Address" |
1775 | + )) |
1776 | + } |
1777 | + }; |
1778 | + let helo_domain = match &self.hostname { |
1779 | + Some(helo_domain) => helo_domain.to_string(), |
1780 | + None => { |
1781 | + return Action::Send(smtp_response!( |
1782 | + 500, |
1783 | + 0, |
1784 | + 0, |
1785 | + 0, |
1786 | + "hostname is not specified" |
1787 | + )) |
1788 | + } |
1789 | + }; |
1790 | + let host_domain = self |
1791 | + .our_hostname |
1792 | .clone() |
1793 | - .ok_or(smtp_response!(500, 0, 0, 0, "hostname is not specified"))? |
1794 | - .to_string(); |
1795 | - let our_domain = self.opts.our_hostname.clone(); |
1796 | - let resolver = self.resolver.as_ref().expect("Resolver not configured"); |
1797 | - let resolver = resolver.lock().await; |
1798 | - let pass = Validation(resolver) |
1799 | - .verify_spf(ip_addr, &helo_domain, &our_domain, mail_from.as_str()) |
1800 | - .await; |
1801 | - if !pass { |
1802 | - return Err(smtp_response!(500, 0, 0, 0, "SPF Verification Failed")); |
1803 | + .expect("session hostname not specified"); |
1804 | + let inner = self; |
1805 | + Action::SpfVerification { |
1806 | + ip_addr, |
1807 | + helo_domain: helo_domain.clone(), |
1808 | + host_domain, |
1809 | + mail_from: mail_from.clone(), |
1810 | + cb: Box::new(move |success| { |
1811 | + if success { |
1812 | + inner.spf_verified_host = Some(helo_domain.clone()); |
1813 | + Action::Send(smtp_response!(250, 0, 0, 0, "OK")) |
1814 | + } else { |
1815 | + Action::Send(smtp_response!( |
1816 | + 500, |
1817 | + 0, |
1818 | + 0, |
1819 | + 0, |
1820 | + "SPF Verification Failed" |
1821 | + )) |
1822 | + } |
1823 | + }), |
1824 | } |
1825 | + } else { |
1826 | + Action::Send(smtp_response!(250, 0, 0, 0, "OK")) |
1827 | } |
1828 | - Ok(vec![smtp_response!(250, 0, 0, 0, "OK")]) |
1829 | } |
1830 | - Request::Rcpt { to } => { |
1831 | - self.check_initialized()?; |
1832 | - let rcpt_to = EmailAddress::from_str(to.address.as_str()).map_err(|e| { |
1833 | - smtp_response!(500, 0, 0, 0, format!("cannot parse: {} {}", to.address, e)) |
1834 | - })?; |
1835 | + Some(Request::Rcpt { to }) => { |
1836 | + if let Some(err) = self.check_initialized().err() { |
1837 | + return Action::Send(err); |
1838 | + } |
1839 | + let rcpt_to = match EmailAddress::from_str(to.address.as_str()) { |
1840 | + Ok(rcpt_to) => rcpt_to, |
1841 | + Err(e) => { |
1842 | + return Action::Send(smtp_response!( |
1843 | + 500, |
1844 | + 0, |
1845 | + 0, |
1846 | + 0, |
1847 | + format!("cannot parse: {} {}", to.address, e) |
1848 | + )) |
1849 | + } |
1850 | + }; |
1851 | if let Some(ref mut rcpts) = self.rcpt_to { |
1852 | rcpts.push(rcpt_to.clone()); |
1853 | } else { |
1854 | self.rcpt_to = Some(vec![rcpt_to.clone()]); |
1855 | } |
1856 | - Ok(vec![smtp_response!(250, 0, 0, 0, "OK")]) |
1857 | + Action::Send(smtp_response!(250, 0, 0, 0, "OK")) |
1858 | } |
1859 | - Request::Bdat { |
1860 | + Some(Request::Bdat { |
1861 | chunk_size: _, |
1862 | is_last: _, |
1863 | - } => { |
1864 | - self.check_initialized()?; |
1865 | - tracing::info!("Initializing data transfer mode"); |
1866 | - self.data_transfer = Some(DataTransfer::Bdat); |
1867 | - Ok(vec![smtp_response!( |
1868 | - 354, |
1869 | - 0, |
1870 | - 0, |
1871 | - 0, |
1872 | - "Starting BDAT data transfer".to_string() |
1873 | - )]) |
1874 | + }) => { |
1875 | + if let Some(err) = self.check_initialized().err() { |
1876 | + return Action::Send(err); |
1877 | + } |
1878 | + let inner = self; |
1879 | + tracing::info!("Starting binary data transfer"); |
1880 | + Action::BDat { |
1881 | + initial_response: smtp_response!( |
1882 | + 354, |
1883 | + 0, |
1884 | + 0, |
1885 | + 0, |
1886 | + "Starting BDAT data transfer".to_string() |
1887 | + ), |
1888 | + cb: Box::new(move |payload| { |
1889 | + let copied = payload.to_vec(); |
1890 | + if let Err(response) = inner.check_body(&copied) { |
1891 | + return Action::Send(response); |
1892 | + }; |
1893 | + let parser = MessageParser::new(); |
1894 | + match parser.parse(&copied) { |
1895 | + Some(message) => { |
1896 | + inner.body = Some(message.into_owned()); |
1897 | + Action::Send(smtp_response!(250, 0, 0, 0, "OK")) |
1898 | + } |
1899 | + None => Action::Send(smtp_response!( |
1900 | + 500, |
1901 | + 0, |
1902 | + 0, |
1903 | + 0, |
1904 | + "Cannot parse message payload" |
1905 | + )), |
1906 | + } |
1907 | + }), |
1908 | + } |
1909 | } |
1910 | // After an AUTH command has been successfully completed, no more |
1911 | // AUTH commands may be issued in the same session. After a |
1912 | // successful AUTH command completes, a server MUST reject any |
1913 | // further AUTH commands with a 503 reply. |
1914 | - Request::Auth { |
1915 | + Some(Request::Auth { |
1916 | mechanism, |
1917 | initial_response, |
1918 | - } => { |
1919 | - if let Some(auth_fn) = &self.opts.plain_auth { |
1920 | + }) => { |
1921 | + if let Some(err) = self.check_initialized().err() { |
1922 | + return Action::Send(err); |
1923 | + } |
1924 | + if self.flags.authentication { |
1925 | if *mechanism != smtp_proto::AUTH_PLAIN { |
1926 | // only plain auth is supported |
1927 | - return Err(smtp_response!(504, 5, 5, 4, "Auth Not Supported")); |
1928 | + return Action::Send(smtp_response!(504, 5, 5, 4, "Auth Not Supported")); |
1929 | + } |
1930 | + let auth_data = match AuthData::try_from(initial_response.as_str()) { |
1931 | + Ok(auth_data) => auth_data, |
1932 | + Err(e) => return Action::Send(e.into()), |
1933 | + }; |
1934 | + // TODO: Let the auth callback return this instead |
1935 | + let authcid = auth_data.authcid().clone(); |
1936 | + let inner = self; |
1937 | + Action::PlainAuth { |
1938 | + authcid: auth_data.authcid(), |
1939 | + authzid: auth_data.authzid(), |
1940 | + password: auth_data.passwd(), |
1941 | + cb: Box::new(move |result| match result { |
1942 | + Ok(_) => { |
1943 | + tracing::info!("Successfully authenticated"); |
1944 | + inner.authenticated_id = Some(authcid); |
1945 | + Action::Send(smtp_response!(235, 2, 7, 0, "OK")) |
1946 | + } |
1947 | + Err(e) => Action::Send(e.into()), |
1948 | + }), |
1949 | } |
1950 | - let auth_data = |
1951 | - AuthData::try_from(initial_response.as_str()).map_err(|e| e.into())?; |
1952 | - |
1953 | - auth_fn |
1954 | - .authenticate( |
1955 | - &auth_data.authcid(), |
1956 | - &auth_data.authzid(), |
1957 | - &auth_data.passwd(), |
1958 | - ) |
1959 | - .await |
1960 | - .map_err(|e| e.into())?; |
1961 | - |
1962 | - tracing::info!("Successfully authenticated"); |
1963 | - |
1964 | - self.auth_initialized = true; |
1965 | - |
1966 | - Ok(vec![smtp_response!(235, 2, 7, 0, "OK")]) |
1967 | } else { |
1968 | - Err(smtp_response!(504, 5, 5, 4, "Auth Not Supported")) |
1969 | + Action::Send(smtp_response!(504, 5, 5, 4, "Auth Not Supported")) |
1970 | } |
1971 | } |
1972 | - Request::Noop { value: _ } => { |
1973 | - self.check_initialized()?; |
1974 | - Ok(vec![smtp_response!(250, 0, 0, 0, "OK".to_string())]) |
1975 | + Some(Request::Noop { value: _ }) => { |
1976 | + if let Some(err) = self.check_initialized().err() { |
1977 | + return Action::Send(err); |
1978 | + } |
1979 | + Action::Send(smtp_response!(250, 0, 0, 0, "OK".to_string())) |
1980 | } |
1981 | - Request::Vrfy { value } => { |
1982 | - if let Some(verifier) = &self.opts.verification { |
1983 | - let address = EmailAddress::from_str(value.as_str()).map_err(|e| { |
1984 | - smtp_response!(500, 0, 0, 0, format!("cannot parse: {} {}", value, e)) |
1985 | - })?; |
1986 | - match verifier.verify(&address).await { |
1987 | - Ok(_) => Ok(vec![smtp_response!(250, 0, 0, 0, "OK".to_string())]), |
1988 | - Err(e) => Err(smtp_response!(500, 0, 0, 0, e.to_string())), |
1989 | + Some(Request::Vrfy { value }) => { |
1990 | + if let Some(err) = self.check_initialized().err() { |
1991 | + return Action::Send(err); |
1992 | + } |
1993 | + if self.flags.vrfy { |
1994 | + let address = match EmailAddress::from_str(value) { |
1995 | + Ok(addr) => addr, |
1996 | + Err(e) => { |
1997 | + return Action::Send(smtp_response!( |
1998 | + 500, |
1999 | + 0, |
2000 | + 0, |
2001 | + 0, |
2002 | + format!("cannot parse: {} {}", value, e) |
2003 | + )) |
2004 | + } |
2005 | + }; |
2006 | + Action::Verify { |
2007 | + address, |
2008 | + cb: Box::new(move |result| match result { |
2009 | + Ok(_) => Action::Send(smtp_response!(200, 0, 0, 0, "OK")), |
2010 | + Err(e) => Action::Send(e.into()), |
2011 | + }), |
2012 | } |
2013 | } else { |
2014 | - Err(smtp_response!(500, 0, 0, 0, "No such address")) |
2015 | + Action::Send(smtp_response!(500, 0, 0, 0, "VRFY Unavailable")) |
2016 | } |
2017 | } |
2018 | - Request::Expn { value } => { |
2019 | - if let Some(expn) = &self.opts.list_expansion { |
2020 | - match expn.expand(value).await { |
2021 | - Ok(addresses) => { |
2022 | - let mut result = vec![smtp_response!(250, 0, 0, 0, "OK")]; |
2023 | - result.extend( |
2024 | - addresses |
2025 | - .iter() |
2026 | - .map(|addr| smtp_response!(250, 0, 0, 0, addr.to_string())), |
2027 | - ); |
2028 | - Ok(result) |
2029 | - } |
2030 | - Err(e) => Err(smtp_response!(500, 0, 0, 0, e.to_string())), |
2031 | + Some(Request::Expn { value }) => { |
2032 | + if let Some(err) = self.check_initialized().err() { |
2033 | + return Action::Send(err); |
2034 | + } |
2035 | + if self.flags.expn && self.authenticated_id.is_some() { |
2036 | + Action::Expand { |
2037 | + address: value.clone(), |
2038 | + cb: Box::new(move |result| match result { |
2039 | + Ok(addresses) => { |
2040 | + let mut responses = vec![smtp_response!(250, 0, 0, 0, "OK")]; |
2041 | + responses.extend( |
2042 | + addresses |
2043 | + .iter() |
2044 | + .map(|addr| smtp_response!(250, 0, 0, 0, addr.to_string())), |
2045 | + ); |
2046 | + Action::SendMany(responses) |
2047 | + } |
2048 | + Err(e) => Action::Send(e.into()), |
2049 | + }), |
2050 | } |
2051 | } else { |
2052 | - Err(smtp_response!(500, 0, 0, 0, "Server does not support EXPN")) |
2053 | + Action::Send(smtp_response!(500, 0, 0, 0, "EXPN Unavailable")) |
2054 | } |
2055 | } |
2056 | - Request::Help { value } => { |
2057 | - self.check_initialized()?; |
2058 | + Some(Request::Help { value }) => { |
2059 | + if let Some(err) = self.check_initialized().err() { |
2060 | + return Action::Send(err); |
2061 | + } |
2062 | if value.is_empty() { |
2063 | - Ok(vec![smtp_response!( |
2064 | - 250, |
2065 | - 0, |
2066 | - 0, |
2067 | - 0, |
2068 | - self.opts.help_banner.to_string() |
2069 | - )]) |
2070 | + Action::Send(smtp_response!(250, 0, 0, 0, self.help_banner)) |
2071 | } else { |
2072 | - Err(smtp_response!( |
2073 | + Action::Send(smtp_response!( |
2074 | 500, |
2075 | 0, |
2076 | 0, |
2077 | @@ -579,18 +779,22 @@ impl Session { |
2078 | )) |
2079 | } |
2080 | } |
2081 | - Request::Etrn { name: _ } => Err(smtp_response!(500, 0, 0, 0, "ETRN is not supported")), |
2082 | - Request::Atrn { domains: _ } => { |
2083 | - Err(smtp_response!(500, 0, 0, 0, "ATRN is not supported")) |
2084 | + Some(Request::Etrn { name: _ }) => { |
2085 | + Action::Send(smtp_response!(500, 0, 0, 0, "ETRN is not supported")) |
2086 | } |
2087 | - Request::Burl { uri: _, is_last: _ } => { |
2088 | - Err(smtp_response!(500, 0, 0, 0, "BURL is not supported")) |
2089 | + Some(Request::Atrn { domains: _ }) => { |
2090 | + Action::Send(smtp_response!(500, 0, 0, 0, "ATRN is not supported")) |
2091 | } |
2092 | - Request::StartTls => { |
2093 | - if self.opts.starttls_enabled.is_some_and(|enabled| enabled) { |
2094 | - Ok(vec![smtp_response!(220, 0, 0, 0, "Go ahead")]) |
2095 | + Some(Request::Burl { uri: _, is_last: _ }) => { |
2096 | + Action::Send(smtp_response!(500, 0, 0, 0, "BURL is not supported")) |
2097 | + } |
2098 | + Some(Request::StartTls) => { |
2099 | + if self.flags.starttls && !self.tls_active { |
2100 | + Action::StartTls(smtp_response!(220, 0, 0, 0, "Go ahead")) |
2101 | + } else if self.flags.starttls && self.tls_active { |
2102 | + Action::Send(tls_already_active()) |
2103 | } else { |
2104 | - Err(smtp_response!( |
2105 | + Action::Send(smtp_response!( |
2106 | 500, |
2107 | 0, |
2108 | 0, |
2109 | @@ -599,428 +803,395 @@ impl Session { |
2110 | )) |
2111 | } |
2112 | } |
2113 | - Request::Data => { |
2114 | - self.check_initialized()?; |
2115 | - tracing::info!("Initializing data transfer mode"); |
2116 | - self.data_transfer = Some(DataTransfer::Data); |
2117 | - Ok(vec![smtp_response!( |
2118 | - 354, |
2119 | - 0, |
2120 | - 0, |
2121 | - 0, |
2122 | - "Reading data input, end the message with <CRLF>.<CRLF>".to_string() |
2123 | - )]) |
2124 | + Some(Request::Data) => { |
2125 | + if let Some(err) = self.check_initialized().err() { |
2126 | + return Action::Send(err); |
2127 | + } |
2128 | + tracing::info!("Starting data transfer"); |
2129 | + let inner = self; |
2130 | + Action::Data { |
2131 | + initial_response: smtp_response!( |
2132 | + 354, |
2133 | + 0, |
2134 | + 0, |
2135 | + 0, |
2136 | + "Reading data input, end the message with <CRLF>.<CRLF>".to_string() |
2137 | + ), |
2138 | + cb: Box::new(move |payload| { |
2139 | + let copied = payload.to_vec(); |
2140 | + if let Err(response) = inner.check_body(&copied) { |
2141 | + return Action::Send(response); |
2142 | + }; |
2143 | + let parser = MessageParser::new(); |
2144 | + match parser.parse(&copied) { |
2145 | + Some(message) => { |
2146 | + inner.body = Some(message.into_owned()); |
2147 | + Action::Send(smtp_response!(250, 0, 0, 0, "OK")) |
2148 | + } |
2149 | + None => Action::Send(smtp_response!( |
2150 | + 500, |
2151 | + 0, |
2152 | + 0, |
2153 | + 0, |
2154 | + "Cannot parse message payload" |
2155 | + )), |
2156 | + } |
2157 | + }), |
2158 | + } |
2159 | } |
2160 | - Request::Rset => { |
2161 | - self.check_initialized()?; |
2162 | + Some(Request::Rset) => { |
2163 | + if let Some(err) = self.check_initialized().err() { |
2164 | + return Action::Send(err); |
2165 | + } |
2166 | self.reset(); |
2167 | - Ok(vec![smtp_response!(200, 0, 0, 0, "".to_string())]) |
2168 | + Action::Send(smtp_response!(200, 0, 0, 0, "".to_string())) |
2169 | } |
2170 | - Request::Quit => Ok(vec![smtp_response!(221, 0, 0, 0, "Ciao!".to_string())]), |
2171 | + Some(Request::Quit) => Action::Quit(smtp_response!(221, 0, 0, 0, "Ciao!".to_string())), |
2172 | } |
2173 | } |
2174 | } |
2175 | |
2176 | #[cfg(test)] |
2177 | mod test { |
2178 | - use futures::stream::{self, StreamExt}; |
2179 | - use smtp_proto::{MailFrom, RcptTo}; |
2180 | - use tokio::sync::Mutex; |
2181 | + |
2182 | + use base64::engine::general_purpose::STANDARD; |
2183 | + use base64::{prelude::*, DecodeError}; |
2184 | + use smtp_proto::MailFrom; |
2185 | |
2186 | use super::*; |
2187 | |
2188 | const EXAMPLE_HOSTNAME: &str = "example.org"; |
2189 | |
2190 | - struct TestCase { |
2191 | - pub request: Request<String>, |
2192 | - pub payload: Option<Bytes>, |
2193 | - pub expected: Result, |
2194 | - } |
2195 | - |
2196 | - /// process all commands returning their response |
2197 | - async fn process_all(session: &Mutex<Session>, commands: &[TestCase]) { |
2198 | - let stream = stream::iter(commands); |
2199 | - stream.enumerate().for_each(|(i, command)| { |
2200 | - async move { |
2201 | - let mut session = session.lock().await; |
2202 | - println!("Running command {}/{}", i, commands.len()); |
2203 | - let response = if let Some(payload) = &command.payload { |
2204 | - session.handle_data(payload).await |
2205 | - } else { |
2206 | - session.process(&command.request).await |
2207 | - }; |
2208 | - println!("Response: {:?}", response); |
2209 | - match response { |
2210 | - Ok(actual_response) => { |
2211 | - match &command.expected { |
2212 | - Ok(expected_response) => { |
2213 | - if !actual_response.eq(expected_response) { |
2214 | - panic!( |
2215 | - "Unexpected response:\n\nActual: {:?}\nExpected: {:?}\n", |
2216 | - actual_response, expected_response |
2217 | - ); |
2218 | - } |
2219 | - } |
2220 | - Err(expected_err) => { |
2221 | - panic!( |
2222 | - "Expected an error but got valid response:\n\nResponse: {:?}\nExpected Error: {:?}", |
2223 | - actual_response, expected_err |
2224 | - ); |
2225 | - }, |
2226 | - } |
2227 | - } |
2228 | - Err(actual_err) => { |
2229 | - match &command.expected { |
2230 | - Ok(response) => { |
2231 | - panic!( |
2232 | - "Expected a valid response but got error:\nExpected: {:?}\nError: {:?}", |
2233 | - response, actual_err, |
2234 | - ); |
2235 | - }, |
2236 | - Err(expected_err) => { |
2237 | - if !actual_err.eq(expected_err) { |
2238 | - panic!("Expected error does not match:\n\nActual: {:?}\n Expected: {:?}", actual_err, expected_err); |
2239 | - } |
2240 | - }, |
2241 | + fn equal(actual: &Action<'_>, expected: &Action<'_>) -> bool { |
2242 | + let is_equal = match actual { |
2243 | + Action::Send(response) => { |
2244 | + matches!(expected, Action::Send(other) if response.eq(other)) |
2245 | + } |
2246 | + Action::SendMany(actual) => match expected { |
2247 | + Action::SendMany(expected) => actual.iter().enumerate().all(|(i, resp)| { |
2248 | + if let Some(expected_resp) = expected.get(i) { |
2249 | + resp.eq(expected_resp) |
2250 | + } else { |
2251 | + false |
2252 | } |
2253 | - } |
2254 | - }; |
2255 | + }), |
2256 | + _ => false, |
2257 | + }, |
2258 | + Action::BDat { |
2259 | + initial_response, |
2260 | + cb, |
2261 | + } => todo!(), |
2262 | + Action::Data { |
2263 | + initial_response, |
2264 | + cb, |
2265 | + } => todo!(), |
2266 | + Action::SpfVerification { |
2267 | + ip_addr, |
2268 | + helo_domain, |
2269 | + host_domain, |
2270 | + mail_from, |
2271 | + cb, |
2272 | + } => todo!(), |
2273 | + Action::PlainAuth { |
2274 | + authcid, |
2275 | + authzid, |
2276 | + password, |
2277 | + cb, |
2278 | + } => todo!(), |
2279 | + Action::Verify { address, cb } => todo!(), |
2280 | + Action::Expand { address, cb } => todo!(), |
2281 | + Action::StartTls(response) => todo!(), |
2282 | + Action::Quit(response) => { |
2283 | + matches!(expected, Action::Quit(other) if response.eq(other)) |
2284 | } |
2285 | - }).await; |
2286 | + }; |
2287 | + |
2288 | + if !is_equal { |
2289 | + println!("Responses Differ:"); |
2290 | + println!("Expected:"); |
2291 | + println!("{}", expected); |
2292 | + println!("Actual:"); |
2293 | + println!("{}", actual); |
2294 | + return false; |
2295 | + }; |
2296 | + |
2297 | + true |
2298 | } |
2299 | |
2300 | - #[tokio::test] |
2301 | - async fn test_hello_quit() { |
2302 | - let requests = &[ |
2303 | - TestCase { |
2304 | - request: Request::Helo { |
2305 | - host: EXAMPLE_HOSTNAME.to_string(), |
2306 | - }, |
2307 | - payload: None, |
2308 | - expected: Ok(vec![smtp_response!( |
2309 | - 250, |
2310 | - 0, |
2311 | - 0, |
2312 | - 0, |
2313 | - String::from("Hello example.org") |
2314 | - )]), |
2315 | - }, |
2316 | - TestCase { |
2317 | - request: Request::Quit {}, |
2318 | - payload: None, |
2319 | - expected: Ok(vec![smtp_response!(221, 0, 0, 0, String::from("Ciao!"))]), |
2320 | - }, |
2321 | - ]; |
2322 | - let session = Mutex::new(Session::default()); |
2323 | - process_all(&session, requests).await; |
2324 | - let session = session.lock().await; |
2325 | - // session should contain both requests |
2326 | + #[test] |
2327 | + fn session_greeting() { |
2328 | + let mut session = Session::default(); |
2329 | + assert!(matches!(session.next(None), Action::Send(_))) |
2330 | + } |
2331 | + |
2332 | + #[test] |
2333 | + fn session_hello_quit() { |
2334 | + let mut session = Session::default(); |
2335 | + assert!(equal( |
2336 | + &session.next(Some(&Request::Helo { |
2337 | + host: EXAMPLE_HOSTNAME.to_string(), |
2338 | + })), |
2339 | + &Action::Send(smtp_response!( |
2340 | + 250, |
2341 | + 0, |
2342 | + 0, |
2343 | + 0, |
2344 | + String::from("Hello example.org") |
2345 | + )), |
2346 | + )); |
2347 | + assert!(equal( |
2348 | + &session.next(Some(&Request::Quit {})), |
2349 | + &Action::Quit(smtp_response!(221, 0, 0, 0, String::from("Ciao!"))), |
2350 | + )); |
2351 | + |
2352 | assert!(session |
2353 | .hostname |
2354 | .as_ref() |
2355 | .is_some_and(|hostname| hostname.to_string() == EXAMPLE_HOSTNAME)); |
2356 | } |
2357 | |
2358 | - #[tokio::test] |
2359 | - async fn test_command_with_no_hello() { |
2360 | - let requests = &[TestCase { |
2361 | - request: Request::Mail { |
2362 | + #[test] |
2363 | + fn session_command_with_no_helo() { |
2364 | + let mut session = Session::default(); |
2365 | + assert!(equal( |
2366 | + &session.next(Some(&Request::Mail { |
2367 | from: MailFrom { |
2368 | address: String::from("fuu@example.org"), |
2369 | ..Default::default() |
2370 | - }, |
2371 | - }, |
2372 | - payload: None, |
2373 | - expected: Err(smtp_response!( |
2374 | + } |
2375 | + })), |
2376 | + &Action::Send(smtp_response!( |
2377 | 500, |
2378 | 5, |
2379 | 5, |
2380 | 1, |
2381 | String::from("It's polite to say EHLO first") |
2382 | - )), |
2383 | - }]; |
2384 | - let session = Mutex::new( |
2385 | - Session::default() |
2386 | - .with_options(SessionOptions::default().our_hostname(EXAMPLE_HOSTNAME)), |
2387 | - ); |
2388 | - process_all(&session, requests).await; |
2389 | + )) |
2390 | + )) |
2391 | } |
2392 | |
2393 | - #[tokio::test] |
2394 | - async fn test_expand() { |
2395 | - let requests = &[ |
2396 | - TestCase { |
2397 | - request: Request::Helo { |
2398 | - host: EXAMPLE_HOSTNAME.to_string(), |
2399 | - }, |
2400 | - payload: None, |
2401 | - expected: Ok(vec![smtp_response!( |
2402 | - 250, |
2403 | - 0, |
2404 | - 0, |
2405 | - 0, |
2406 | - String::from("Hello example.org") |
2407 | - )]), |
2408 | - }, |
2409 | - TestCase { |
2410 | - request: Request::Expn { |
2411 | - value: "mailing-list".to_string(), |
2412 | - }, |
2413 | - payload: None, |
2414 | - expected: Ok(vec![ |
2415 | - smtp_response!(250, 0, 0, 0, "OK"), |
2416 | - smtp_response!(250, 0, 0, 0, "Fuu <fuu@bar.com>"), |
2417 | - smtp_response!(250, 0, 0, 0, "Baz <baz@qux.com>"), |
2418 | - ]), |
2419 | - }, |
2420 | - TestCase { |
2421 | - request: Request::Quit {}, |
2422 | - payload: None, |
2423 | - expected: Ok(vec![smtp_response!(221, 0, 0, 0, String::from("Ciao!"))]), |
2424 | - }, |
2425 | - ]; |
2426 | - let session = Mutex::new(Session::default().with_options( |
2427 | - SessionOptions::default().list_expansion(crate::expand::ExpansionFunc(|name: &str| { |
2428 | - let name = name.to_string(); |
2429 | - async move { |
2430 | - assert!(name == "mailing-list"); |
2431 | - Ok(vec![ |
2432 | - EmailAddress::new_unchecked("Fuu <fuu@bar.com>"), |
2433 | - EmailAddress::new_unchecked("Baz <baz@qux.com>"), |
2434 | - ]) |
2435 | - } |
2436 | + #[test] |
2437 | + fn session_authenticate() { |
2438 | + let session = &mut Session::default().authentication(true); |
2439 | + assert!(equal( |
2440 | + &session.next(Some(&Request::Helo { |
2441 | + host: EXAMPLE_HOSTNAME.to_string(), |
2442 | })), |
2443 | + &Action::Send(smtp_response!( |
2444 | + 250, |
2445 | + 0, |
2446 | + 0, |
2447 | + 0, |
2448 | + String::from("Hello example.org") |
2449 | + )), |
2450 | )); |
2451 | - process_all(&session, requests).await; |
2452 | - // session should contain both requests |
2453 | - let session = session.lock().await; |
2454 | + |
2455 | + { |
2456 | + let auth = session.next(Some(&Request::Auth { |
2457 | + mechanism: smtp_proto::AUTH_PLAIN, |
2458 | + initial_response: STANDARD.encode(b"\0hello\0world"), |
2459 | + })); |
2460 | + match auth { |
2461 | + Action::PlainAuth { |
2462 | + authcid, |
2463 | + authzid, |
2464 | + password, |
2465 | + cb, |
2466 | + } => { |
2467 | + assert!(authcid == "hello"); |
2468 | + assert!(authzid == "hello"); |
2469 | + assert!(password == "world"); |
2470 | + assert!(equal( |
2471 | + &cb(Ok(())), |
2472 | + &Action::Send(smtp_response!(235, 2, 7, 0, "OK")) |
2473 | + )); |
2474 | + } |
2475 | + _ => panic!("Unexpected response"), |
2476 | + }; |
2477 | + }; |
2478 | + |
2479 | assert!(session |
2480 | - .hostname |
2481 | + .authenticated_id |
2482 | .as_ref() |
2483 | - .is_some_and(|hostname| hostname.to_string() == EXAMPLE_HOSTNAME)); |
2484 | + .is_some_and(|id| id == "hello")); |
2485 | } |
2486 | |
2487 | - #[tokio::test] |
2488 | - async fn test_verify() { |
2489 | - let requests = &[ |
2490 | - TestCase { |
2491 | - request: Request::Helo { |
2492 | - host: EXAMPLE_HOSTNAME.to_string(), |
2493 | - }, |
2494 | - payload: None, |
2495 | - expected: Ok(vec![smtp_response!( |
2496 | - 250, |
2497 | - 0, |
2498 | - 0, |
2499 | - 0, |
2500 | - String::from("Hello example.org") |
2501 | - )]), |
2502 | - }, |
2503 | - TestCase { |
2504 | - request: Request::Vrfy { |
2505 | - value: "Fuu <bar@baz.com>".to_string(), |
2506 | - }, |
2507 | - payload: None, |
2508 | - expected: Ok(vec![smtp_response!(250, 0, 0, 0, "OK")]), |
2509 | - }, |
2510 | - TestCase { |
2511 | - request: Request::Quit {}, |
2512 | - payload: None, |
2513 | - expected: Ok(vec![smtp_response!(221, 0, 0, 0, String::from("Ciao!"))]), |
2514 | - }, |
2515 | - ]; |
2516 | - let session = Mutex::new(Session::default().with_options( |
2517 | - SessionOptions::default().verification(crate::verify::VerifyFunc( |
2518 | - |addr: &EmailAddress| { |
2519 | - let addr = addr.clone(); |
2520 | - async move { |
2521 | - assert!(addr.email() == "bar@baz.com"); |
2522 | - Ok(()) |
2523 | - } |
2524 | - }, |
2525 | - )), |
2526 | - )); |
2527 | - process_all(&session, requests).await; |
2528 | - // session should contain both requests |
2529 | - let session = session.lock().await; |
2530 | - assert!(session |
2531 | - .hostname |
2532 | - .as_ref() |
2533 | - .is_some_and(|hostname| hostname.to_string() == EXAMPLE_HOSTNAME)); |
2534 | + #[test] |
2535 | + fn session_expand() { |
2536 | + let session = &mut Session::default().authentication(true).expn_enabled(true); |
2537 | + session.initialized = Some(Mode::Extended); |
2538 | + session.authenticated_id = Some("hello".to_string()); |
2539 | + match session.next(Some(&Request::Expn { |
2540 | + value: String::from("group@baz.com"), |
2541 | + })) { |
2542 | + Action::Expand { address: _, cb } => { |
2543 | + assert!(equal( |
2544 | + &cb(Ok(vec![ |
2545 | + EmailAddress::new_unchecked("fuu@bar.com"), |
2546 | + EmailAddress::new_unchecked("baz@qux.com") |
2547 | + ])), |
2548 | + &Action::SendMany(vec![ |
2549 | + smtp_response!(250, 0, 0, 0, "OK"), |
2550 | + smtp_response!(250, 0, 0, 0, "fuu@bar.com"), |
2551 | + smtp_response!(250, 0, 0, 0, "baz@qux.com") |
2552 | + ]) |
2553 | + )); |
2554 | + } |
2555 | + _ => panic!("Unexpected response"), |
2556 | + }; |
2557 | } |
2558 | |
2559 | - #[tokio::test] |
2560 | - async fn test_non_ascii_characters() { |
2561 | - let mut expected_ehlo_response = EhloResponse::new(String::from("Hello example.org")); |
2562 | - expected_ehlo_response.capabilities = DEFAULT_CAPABILITIES; |
2563 | - expected_ehlo_response.size = DEFAULT_MAXIMUM_MESSAGE_SIZE as usize; |
2564 | - let requests = &[ |
2565 | - TestCase { |
2566 | - request: Request::Helo { |
2567 | - host: EXAMPLE_HOSTNAME.to_string(), |
2568 | - }, |
2569 | - payload: None, |
2570 | - expected: Ok(vec![smtp_response!( |
2571 | - 250, |
2572 | - 0, |
2573 | - 0, |
2574 | - 0, |
2575 | - String::from("Hello example.org") |
2576 | - )]), |
2577 | - }, |
2578 | - TestCase { |
2579 | - request: Request::Data {}, |
2580 | - payload: None, |
2581 | - expected: Ok(vec![smtp_response!( |
2582 | - 354, |
2583 | - 0, |
2584 | - 0, |
2585 | - 0, |
2586 | - "Reading data input, end the message with <CRLF>.<CRLF>" |
2587 | - )]), |
2588 | - }, |
2589 | - TestCase { |
2590 | - request: Request::Data {}, |
2591 | - payload: Some(Bytes::from_static( |
2592 | - r#"Subject: Hello World |
2593 | + #[test] |
2594 | + fn session_verify() { |
2595 | + let session = &mut Session::default().authentication(true).vrfy_enabled(true); |
2596 | + session.initialized = Some(Mode::Extended); |
2597 | + session.authenticated_id = Some("hello".to_string()); |
2598 | + match session.next(Some(&Request::Vrfy { |
2599 | + value: String::from("qux@baz.com"), |
2600 | + })) { |
2601 | + Action::Verify { address, cb } => { |
2602 | + assert!(address.to_string() == "qux@baz.com"); |
2603 | + assert!(equal( |
2604 | + &cb(Ok(())), |
2605 | + &Action::Send(smtp_response!(200, 0, 0, 0, "OK")) |
2606 | + )); |
2607 | + } |
2608 | + _ => panic!("Unexpected response"), |
2609 | + }; |
2610 | + } |
2611 | + |
2612 | + #[test] |
2613 | + fn session_non_ascii_characters_legacy_smtp() { |
2614 | + let session = &mut Session::default(); |
2615 | + // non-extended sessions cannot accept non-ascii characters |
2616 | + session.initialized = Some(Mode::Legacy); |
2617 | + session.mail_from = Some(EmailAddress::new_unchecked("fuu@bar.com")); |
2618 | + match session.next(Some(&Request::Data {})) { |
2619 | + Action::Data { |
2620 | + initial_response, |
2621 | + cb, |
2622 | + } => { |
2623 | + assert!(equal( |
2624 | + &Action::Send(initial_response), |
2625 | + &Action::Send(smtp_response!( |
2626 | + 354, |
2627 | + 0, |
2628 | + 0, |
2629 | + 0, |
2630 | + "Reading data input, end the message with <CRLF>.<CRLF>" |
2631 | + )) |
2632 | + )); |
2633 | + let action = cb(Bytes::from_static( |
2634 | + r#" |
2635 | + Subject: Hello World |
2636 | 😍😍😍 |
2637 | "# |
2638 | .as_bytes(), |
2639 | - )), |
2640 | - expected: Err(smtp_response!( |
2641 | - 500, |
2642 | - 0, |
2643 | - 0, |
2644 | - 0, |
2645 | - "Non ascii characters found in message body" |
2646 | - )), |
2647 | - }, |
2648 | - // upgrade the connection to extended mode |
2649 | - TestCase { |
2650 | - request: Request::Ehlo { |
2651 | - host: EXAMPLE_HOSTNAME.to_string(), |
2652 | - }, |
2653 | - payload: None, |
2654 | - expected: Ok(vec![Response::Ehlo(expected_ehlo_response)]), |
2655 | - }, |
2656 | - TestCase { |
2657 | - request: Request::Data {}, |
2658 | - payload: None, |
2659 | - expected: Ok(vec![smtp_response!( |
2660 | - 354, |
2661 | - 0, |
2662 | - 0, |
2663 | - 0, |
2664 | - "Reading data input, end the message with <CRLF>.<CRLF>" |
2665 | - )]), |
2666 | - }, |
2667 | - TestCase { |
2668 | - request: Request::Data {}, |
2669 | - payload: Some(Bytes::from_static( |
2670 | - r#"Subject: Hello World |
2671 | + )); |
2672 | + assert!(equal( |
2673 | + &action, |
2674 | + &Action::Send(smtp_response!( |
2675 | + 500, |
2676 | + 0, |
2677 | + 0, |
2678 | + 0, |
2679 | + "Non ASCII characters found in message body" |
2680 | + )) |
2681 | + )) |
2682 | + } |
2683 | + _ => panic!("Unexpected response"), |
2684 | + }; |
2685 | + } |
2686 | + |
2687 | + #[test] |
2688 | + fn session_non_ascii_characters_extended_smtp() { |
2689 | + let session = &mut Session::default(); |
2690 | + // non-extended sessions cannot accept non-ascii characters |
2691 | + session.initialized = Some(Mode::Extended); |
2692 | + session.mail_from = Some(EmailAddress::new_unchecked("fuu@bar.com")); |
2693 | + match session.next(Some(&Request::Data {})) { |
2694 | + Action::Data { |
2695 | + initial_response, |
2696 | + cb, |
2697 | + } => { |
2698 | + assert!(equal( |
2699 | + &Action::Send(initial_response), |
2700 | + &Action::Send(smtp_response!( |
2701 | + 354, |
2702 | + 0, |
2703 | + 0, |
2704 | + 0, |
2705 | + "Reading data input, end the message with <CRLF>.<CRLF>" |
2706 | + )) |
2707 | + )); |
2708 | + let action = cb(Bytes::from_static( |
2709 | + r#" |
2710 | + Subject: Hello World |
2711 | 😍😍😍 |
2712 | "# |
2713 | .as_bytes(), |
2714 | - )), |
2715 | - expected: Ok(vec![smtp_response!(250, 0, 0, 0, "OK")]), |
2716 | - }, |
2717 | - ]; |
2718 | - let session = Mutex::new( |
2719 | - Session::default().with_options( |
2720 | - SessionOptions::default() |
2721 | - .our_hostname(EXAMPLE_HOSTNAME) |
2722 | - .capabilities(DEFAULT_CAPABILITIES), |
2723 | - ), |
2724 | - ); |
2725 | - process_all(&session, requests).await; |
2726 | + )); |
2727 | + assert!(equal( |
2728 | + &action, |
2729 | + &Action::Send(smtp_response!(250, 0, 0, 0, "OK")) |
2730 | + )) |
2731 | + } |
2732 | + _ => panic!("Unexpected response"), |
2733 | + }; |
2734 | } |
2735 | |
2736 | - #[tokio::test] |
2737 | - async fn test_email_with_body() { |
2738 | - let requests = &[ |
2739 | - TestCase { |
2740 | - request: Request::Helo { |
2741 | - host: EXAMPLE_HOSTNAME.to_string(), |
2742 | - }, |
2743 | - payload: None, |
2744 | - expected: Ok(vec![smtp_response!(250, 0, 0, 0, "Hello example.org")]), |
2745 | - }, |
2746 | - TestCase { |
2747 | - request: Request::Mail { |
2748 | - from: MailFrom { |
2749 | - address: String::from("fuu@example.org"), |
2750 | - ..Default::default() |
2751 | - }, |
2752 | - }, |
2753 | - payload: None, |
2754 | - expected: Ok(vec![smtp_response!(250, 0, 0, 0, "OK")]), |
2755 | - }, |
2756 | - TestCase { |
2757 | - request: Request::Rcpt { |
2758 | - to: RcptTo { |
2759 | - address: String::from("bar@example.org"), |
2760 | - ..Default::default() |
2761 | - }, |
2762 | - }, |
2763 | - payload: None, |
2764 | - expected: Ok(vec![smtp_response!(250, 0, 0, 0, "OK")]), |
2765 | - }, |
2766 | - // initiate data transfer |
2767 | - TestCase { |
2768 | - request: Request::Data {}, |
2769 | - payload: None, |
2770 | - expected: Ok(vec![smtp_response!( |
2771 | - 354, |
2772 | - 0, |
2773 | - 0, |
2774 | - 0, |
2775 | - "Reading data input, end the message with <CRLF>.<CRLF>" |
2776 | - )]), |
2777 | - }, |
2778 | - // send the actual payload |
2779 | - TestCase { |
2780 | - request: Request::Data {}, |
2781 | - payload: Some(Bytes::from_static( |
2782 | - br#"Subject: Hello World |
2783 | + #[test] |
2784 | + fn session_message_body_ok() { |
2785 | + let session = &mut Session::default(); |
2786 | + // non-extended sessions cannot accept non-ascii characters |
2787 | + session.initialized = Some(Mode::Extended); |
2788 | + session.mail_from = Some(EmailAddress::new_unchecked("fuu@bar.com")); |
2789 | + { |
2790 | + match session.next(Some(&Request::Data {})) { |
2791 | + Action::Data { |
2792 | + initial_response, |
2793 | + cb, |
2794 | + } => { |
2795 | + assert!(equal( |
2796 | + &Action::Send(initial_response), |
2797 | + &Action::Send(smtp_response!( |
2798 | + 354, |
2799 | + 0, |
2800 | + 0, |
2801 | + 0, |
2802 | + "Reading data input, end the message with <CRLF>.<CRLF>" |
2803 | + )) |
2804 | + )); |
2805 | + let action = cb(Bytes::from_static( |
2806 | + r#"To: <baz@qux.com> |
2807 | + Subject: Hello World |
2808 | |
2809 | This is an e-mail from a test case! |
2810 | |
2811 | Note that it doesn't end with a "." since that parsing happens as part of the |
2812 | - transport rather than the session. |
2813 | - "#, |
2814 | - )), |
2815 | - expected: Ok(vec![smtp_response!(250, 0, 0, 0, "OK")]), |
2816 | - }, |
2817 | - ]; |
2818 | - let session = Mutex::new( |
2819 | - Session::default() |
2820 | - .with_options(SessionOptions::default().our_hostname(EXAMPLE_HOSTNAME)), |
2821 | - ); |
2822 | - process_all(&session, requests).await; |
2823 | - let session = session.lock().await; |
2824 | - assert!(session |
2825 | - .mail_from |
2826 | - .as_ref() |
2827 | - .is_some_and(|mail_from| mail_from.email() == "fuu@example.org")); |
2828 | - assert!(session.rcpt_to.as_ref().is_some_and(|rcpts| rcpts |
2829 | - .first() |
2830 | - .is_some_and(|rcpt_to| rcpt_to.email() == "bar@example.org"))); |
2831 | - assert!(session.body.as_ref().is_some_and(|body| { |
2832 | - body.subject() |
2833 | - .is_some_and(|subject| subject == "Hello World") |
2834 | - })); |
2835 | - } |
2836 | + transport rather than the session. 🩷 |
2837 | + "# |
2838 | + .as_bytes(), |
2839 | + )); |
2840 | + assert!(equal( |
2841 | + &action, |
2842 | + &Action::Send(smtp_response!(250, 0, 0, 0, "OK")) |
2843 | + )) |
2844 | + } |
2845 | + _ => panic!("Unexpected response"), |
2846 | + }; |
2847 | + }; |
2848 | |
2849 | - #[tokio::test] |
2850 | - pub async fn test_domain_parsing() { |
2851 | - let mut session = Session::default(); |
2852 | - for host in ["127.0.0.1", "[127.0.0.1]", "example.org", "IPv6: ::1"] { |
2853 | - session |
2854 | - .process(&Request::Ehlo { |
2855 | - host: host.to_string(), |
2856 | - }) |
2857 | - .await |
2858 | - .unwrap(); |
2859 | - } |
2860 | + let message_body = session.body.clone().unwrap(); |
2861 | + |
2862 | + assert!(message_body |
2863 | + .to() |
2864 | + .is_some_and(|to| to.first().is_some_and(|to| to |
2865 | + .address |
2866 | + .as_ref() |
2867 | + .is_some_and(|addr| { addr == "baz@qux.com" })))); |
2868 | + assert!(message_body |
2869 | + .subject() |
2870 | + .is_some_and(|subject| subject == "Hello World")); |
2871 | } |
2872 | } |
2873 | diff --git a/maitred/src/transport.rs b/maitred/src/transport.rs |
2874 | index 3a7650b..9a62879 100644 |
2875 | --- a/maitred/src/transport.rs |
2876 | +++ b/maitred/src/transport.rs |
2877 | @@ -3,11 +3,13 @@ use std::{fmt::Display, io::Write}; |
2878 | use bytes::{Bytes, BytesMut}; |
2879 | use smtp_proto::request::receiver::{BdatReceiver, DataReceiver, RequestReceiver}; |
2880 | use smtp_proto::Error as SmtpError; |
2881 | - pub use smtp_proto::{EhloResponse, Request, Response as SmtpResponse}; |
2882 | + use smtp_proto::Request; |
2883 | use tokio_util::codec::{Decoder, Encoder}; |
2884 | |
2885 | + use crate::session::Response; |
2886 | + |
2887 | #[derive(Debug, thiserror::Error)] |
2888 | - pub(crate) enum TransportError { |
2889 | + pub enum TransportError { |
2890 | /// Returned when a client attempts to send multiple commands sequentially |
2891 | /// to the server without waiting for a response but piplining isn't |
2892 | /// enabled. |
2893 | @@ -21,53 +23,6 @@ pub(crate) enum TransportError { |
2894 | Io(#[from] std::io::Error), |
2895 | } |
2896 | |
2897 | - #[derive(Debug, Clone)] |
2898 | - pub enum Response<T> |
2899 | - where |
2900 | - T: Display, |
2901 | - { |
2902 | - General(SmtpResponse<T>), |
2903 | - Ehlo(EhloResponse<T>), |
2904 | - } |
2905 | - |
2906 | - impl Response<String> { |
2907 | - pub fn is_fatal(&self) -> bool { |
2908 | - match self { |
2909 | - Response::General(resp) => resp.code >= 500, |
2910 | - Response::Ehlo(_) => false, |
2911 | - } |
2912 | - } |
2913 | - } |
2914 | - |
2915 | - impl<T> PartialEq for Response<T> |
2916 | - where |
2917 | - T: Display, |
2918 | - { |
2919 | - fn eq(&self, other: &Self) -> bool { |
2920 | - match self { |
2921 | - Response::General(req) => match other { |
2922 | - Response::General(other) => req.to_string() == other.to_string(), |
2923 | - Response::Ehlo(_) => false, |
2924 | - }, |
2925 | - Response::Ehlo(req) => match other { |
2926 | - Response::General(_) => false, |
2927 | - Response::Ehlo(other) => { |
2928 | - // FIXME |
2929 | - req.capabilities == other.capabilities |
2930 | - && req.hostname.to_string() == other.hostname.to_string() |
2931 | - && req.deliver_by == other.deliver_by |
2932 | - && req.size == other.size |
2933 | - && req.auth_mechanisms == other.auth_mechanisms |
2934 | - && req.future_release_datetime.eq(&req.future_release_datetime) |
2935 | - && req.future_release_interval.eq(&req.future_release_interval) |
2936 | - } |
2937 | - }, |
2938 | - } |
2939 | - } |
2940 | - } |
2941 | - |
2942 | - impl<T> Eq for Response<T> where T: Display {} |
2943 | - |
2944 | struct Wrapper<'a>(&'a mut BytesMut); |
2945 | |
2946 | impl Write for Wrapper<'_> { |
2947 | @@ -88,7 +43,7 @@ pub(crate) enum Receiver { |
2948 | |
2949 | /// Command from the client with an optional attached payload. |
2950 | #[derive(Debug)] |
2951 | - pub(crate) enum Command { |
2952 | + pub enum Command { |
2953 | Requests(Vec<Request<String>>), |
2954 | Payload(Bytes), |
2955 | } |
2956 | @@ -105,7 +60,7 @@ impl Display for Command { |
2957 | /// Line oriented transport |
2958 | /// TODO: BINARYMIME |
2959 | #[derive(Default)] |
2960 | - pub(crate) struct Transport { |
2961 | + pub struct Transport { |
2962 | receiver: Option<Box<Receiver>>, |
2963 | buf: Vec<u8>, |
2964 | pipelining: bool, |
2965 | @@ -133,6 +88,7 @@ impl Encoder<Response<String>> for Transport { |
2966 | type Error = TransportError; |
2967 | |
2968 | fn encode(&mut self, item: Response<String>, dst: &mut BytesMut) -> Result<(), Self::Error> { |
2969 | + tracing::debug!("Writing response: {:?}", item); |
2970 | match item { |
2971 | Response::General(item) => { |
2972 | item.write(Wrapper(dst))?; |
2973 | diff --git a/maitred/src/verify.rs b/maitred/src/verify.rs |
2974 | index cd93573..6497908 100644 |
2975 | --- a/maitred/src/verify.rs |
2976 | +++ b/maitred/src/verify.rs |
2977 | @@ -2,6 +2,10 @@ use std::future::Future; |
2978 | |
2979 | use async_trait::async_trait; |
2980 | use email_address::EmailAddress; |
2981 | + use smtp_proto::Response as SmtpResponse; |
2982 | + |
2983 | + use crate::session::Response; |
2984 | + use crate::smtp_response; |
2985 | |
2986 | /// An error encountered while verifying an e-mail address |
2987 | #[derive(Debug, thiserror::Error)] |
2988 | @@ -21,6 +25,20 @@ pub enum VerifyError { |
2989 | }, |
2990 | } |
2991 | |
2992 | + #[allow(clippy::from_over_into)] |
2993 | + impl Into<Response<String>> for VerifyError { |
2994 | + fn into(self) -> Response<String> { |
2995 | + match self { |
2996 | + VerifyError::Server(_) => smtp_response!(500, 0, 0, 0, self.to_string()), |
2997 | + VerifyError::NotFound(_) => smtp_response!(404, 0, 0, 0, self.to_string()), |
2998 | + VerifyError::Ambiguous { |
2999 | + email: _, |
3000 | + alternatives: _, |
3001 | + } => smtp_response!(500, 0, 0, 0, self.to_string()), |
3002 | + } |
3003 | + } |
3004 | + } |
3005 | + |
3006 | /// Verify that the given e-mail address exists on the server. Servers may |
3007 | /// choose to implement nothing or not use this option at all if desired. |
3008 | #[async_trait] |
3009 | diff --git a/maitred/src/worker.rs b/maitred/src/worker.rs |
3010 | index 293d5b1..b015d2c 100644 |
3011 | --- a/maitred/src/worker.rs |
3012 | +++ b/maitred/src/worker.rs |
3013 | @@ -8,8 +8,9 @@ use tokio::sync::{mpsc::Receiver, Mutex}; |
3014 | use crate::delivery::Delivery; |
3015 | use crate::milter::Milter; |
3016 | use crate::rewrite::Rewrite; |
3017 | + use crate::server::ServerError; |
3018 | + use crate::session::Envelope; |
3019 | use crate::validation::Validation; |
3020 | - use crate::{Envelope, Error}; |
3021 | |
3022 | const HEADER_DKIM_RESULT: &str = "Maitred-Dkim-Result"; |
3023 | |
3024 | @@ -19,7 +20,6 @@ const HEADER_DKIM_RESULT: &str = "Maitred-Dkim-Result"; |
3025 | /// Sequentially applying milters in the order they were configured |
3026 | /// Running DKIM verification |
3027 | /// ARC Verficiation |
3028 | - /// SPF Verification |
3029 | pub(crate) struct Worker { |
3030 | pub milter: Option<Arc<dyn Milter>>, |
3031 | pub delivery: Option<Arc<dyn Delivery>>, |
3032 | @@ -45,7 +45,7 @@ impl Worker { |
3033 | }) |
3034 | } |
3035 | |
3036 | - pub async fn process(&mut self) -> Result<(), Error> { |
3037 | + pub async fn process(&mut self) -> Result<(), ServerError> { |
3038 | let mut ticker = |
3039 | tokio::time::interval_at(tokio::time::Instant::now(), Duration::from_millis(800)); |
3040 |