Commit
+222 -119 +/-10 browse
1 | diff --git a/Cargo.lock b/Cargo.lock |
2 | index 802bfc3..b378456 100644 |
3 | --- a/Cargo.lock |
4 | +++ b/Cargo.lock |
5 | @@ -281,6 +281,7 @@ dependencies = [ |
6 | "tokio-stream", |
7 | "tokio-util", |
8 | "tracing", |
9 | + "tracing-subscriber", |
10 | "url", |
11 | ] |
12 | |
13 | diff --git a/cmd/maitred-debug/src/main.rs b/cmd/maitred-debug/src/main.rs |
14 | index b473484..e8ddae1 100644 |
15 | --- a/cmd/maitred-debug/src/main.rs |
16 | +++ b/cmd/maitred-debug/src/main.rs |
17 | @@ -1,4 +1,4 @@ |
18 | - use maitred::{Error, Server}; |
19 | + use maitred::{Error, Server, SessionOptions}; |
20 | use tracing::Level; |
21 | |
22 | #[tokio::main] |
23 | @@ -11,7 +11,9 @@ async fn main() -> Result<(), Error> { |
24 | .init(); |
25 | |
26 | // Set the subscriber as the default subscriber |
27 | - let mail_server = Server::new("localhost").address("127.0.0.1:2525"); |
28 | + let mail_server = Server::new("localhost") |
29 | + .address("127.0.0.1:2525") |
30 | + .with_session_opts(SessionOptions::default()); |
31 | mail_server.listen().await?; |
32 | Ok(()) |
33 | } |
34 | diff --git a/maitred/Cargo.toml b/maitred/Cargo.toml |
35 | index d39f001..f2a1adb 100644 |
36 | --- a/maitred/Cargo.toml |
37 | +++ b/maitred/Cargo.toml |
38 | @@ -16,3 +16,6 @@ tokio-stream = { version = "0.1.15", features = ["full"] } |
39 | tokio-util = { version = "0.7.11", features = ["full"] } |
40 | tracing = { version = "0.1.40", features = ["log"] } |
41 | url = "2.5.2" |
42 | + |
43 | + [dev-dependencies] |
44 | + tracing-subscriber = "0.3.18" |
45 | diff --git a/maitred/src/error.rs b/maitred/src/error.rs |
46 | index 739268d..4ef87fa 100644 |
47 | --- a/maitred/src/error.rs |
48 | +++ b/maitred/src/error.rs |
49 | @@ -3,6 +3,9 @@ use std::string::FromUtf8Error; |
50 | use smtp_proto::Error as SmtpError; |
51 | use url::ParseError; |
52 | |
53 | + |
54 | + /// Any fatal error that is encountered by the server that should cause it |
55 | + /// to shutdown and stop processing connections. |
56 | #[derive(Debug, thiserror::Error)] |
57 | pub enum Error { |
58 | #[error("Unspecified internal error: {0}")] |
59 | diff --git a/maitred/src/expand.rs b/maitred/src/expand.rs |
60 | index 85bc3db..0cfcaa7 100644 |
61 | --- a/maitred/src/expand.rs |
62 | +++ b/maitred/src/expand.rs |
63 | @@ -2,12 +2,14 @@ use std::result::Result as StdResult; |
64 | |
65 | use email_address::EmailAddress; |
66 | |
67 | + /// Result type containing any of the associated e-mail addresses with the |
68 | + /// given mailing list name. |
69 | pub type Result = StdResult<Vec<EmailAddress>, Error>; |
70 | |
71 | /// An error encountered while expanding a mail address |
72 | #[derive(Debug, thiserror::Error)] |
73 | pub enum Error { |
74 | - /// Indicates an unspecified error that occurred during expansion |
75 | + /// Indicates an unspecified error that occurred during expansion. |
76 | #[error("Internal Server Error: {0}")] |
77 | Server(String), |
78 | /// Indicates that no group exists with the specified name |
79 | @@ -24,7 +26,19 @@ pub trait Expansion { |
80 | fn expand(&self, name: &str) -> Result; |
81 | } |
82 | |
83 | - /// Wrapper type implementing the Expansion trait |
84 | + /// Helper wrapper implementing the Expansion trait |
85 | + /// # Example |
86 | + /// ```rust |
87 | + /// use email_address::EmailAddress; |
88 | + /// use maitred::ExpansionFunc; |
89 | + /// |
90 | + /// let my_expn_fn = ExpansionFunc(|name: &str| { |
91 | + /// Ok(vec![ |
92 | + /// EmailAddress::new_unchecked("fuu@bar.com"), |
93 | + /// EmailAddress::new_unchecked("baz@qux.com") |
94 | + /// ]) |
95 | + /// }); |
96 | + /// ``` |
97 | pub struct Func<F>(pub F) |
98 | where |
99 | F: Fn(&str) -> Result; |
100 | diff --git a/maitred/src/lib.rs b/maitred/src/lib.rs |
101 | index e7a0a99..e9a537b 100644 |
102 | --- a/maitred/src/lib.rs |
103 | +++ b/maitred/src/lib.rs |
104 | @@ -1,3 +1,26 @@ |
105 | + //! Maitred is a flexible and embedable SMTP server for handling e-mail from |
106 | + //! within a Rust program. |
107 | + //! # Example SMTP Server |
108 | + //! ```rust |
109 | + //! use maitred::{Error, Server}; |
110 | + //! use tracing::Level; |
111 | + //! |
112 | + //! #[tokio::main] |
113 | + //! async fn main() -> Result<(), Error> { |
114 | + //! // Create a subscriber that logs events to the console |
115 | + //! tracing_subscriber::fmt() |
116 | + //! .compact() |
117 | + //! .with_line_number(true) |
118 | + //! .with_max_level(Level::DEBUG) |
119 | + //! .init(); |
120 | + //! |
121 | + //! // Set the subscriber as the default subscriber |
122 | + //! let mail_server = Server::new("localhost").address("127.0.0.1:2525"); |
123 | + //! // mail_server.listen().await?; |
124 | + //! Ok(()) |
125 | + //! } |
126 | + //! ``` |
127 | + |
128 | mod error; |
129 | mod expand; |
130 | mod pipeline; |
131 | @@ -7,13 +30,19 @@ mod transport; |
132 | mod verify; |
133 | |
134 | use smtp_proto::{Request, Response as SmtpResponse}; |
135 | - |
136 | - /// Low Level SMTP protocol is exported for convenience |
137 | - pub use smtp_proto; |
138 | + use transport::Response; |
139 | |
140 | pub use error::Error; |
141 | + pub use expand::{Error as ExpansionError, Expansion, Func as ExpansionFunc}; |
142 | pub use server::Server; |
143 | - use transport::Response; |
144 | + pub use session::{ |
145 | + Options as SessionOptions, DEFAULT_CAPABILITIES, DEFAULT_GREETING, DEFAULT_HELP_BANNER, |
146 | + DEFAULT_MAXIMUM_MESSAGE_SIZE, |
147 | + }; |
148 | + pub use verify::{Error as VerifyError, Func as VerifyFunc, Verify}; |
149 | + |
150 | + pub use email_address; |
151 | + pub use smtp_proto; |
152 | |
153 | /// Chunk is a logical set of SMTP resposnes that might be generated from one |
154 | /// command or "pipelined" as the result of several commands sent by the client |
155 | diff --git a/maitred/src/pipeline.rs b/maitred/src/pipeline.rs |
156 | index 4933030..7ad02b3 100644 |
157 | --- a/maitred/src/pipeline.rs |
158 | +++ b/maitred/src/pipeline.rs |
159 | @@ -62,7 +62,7 @@ impl Pipeline { |
160 | .expect("to results called without history"); |
161 | if last_command.1.is_ok() && mail_from_ok && rcpt_to_ok_count > 0 { |
162 | flatten(&self.history) |
163 | - } else if !mail_from_ok || rcpt_to_ok_count <= 0{ |
164 | + } else if !mail_from_ok || rcpt_to_ok_count <= 0 { |
165 | self.history.pop(); |
166 | flatten(&self.history) |
167 | } else { |
168 | @@ -156,9 +156,8 @@ impl Pipeline { |
169 | mod test { |
170 | |
171 | use super::*; |
172 | - use crate::{ |
173 | - smtp_chunk_ok, smtp_proto::Response as SmtpResponse, transport::Response, Request, |
174 | - }; |
175 | + use crate::{smtp_chunk_ok, transport::Response, Request}; |
176 | + use smtp_proto::Response as SmtpResponse; |
177 | |
178 | #[test] |
179 | pub fn test_pipeline_basic() { |
180 | diff --git a/maitred/src/server.rs b/maitred/src/server.rs |
181 | index 507c947..4b63e82 100644 |
182 | --- a/maitred/src/server.rs |
183 | +++ b/maitred/src/server.rs |
184 | @@ -1,3 +1,4 @@ |
185 | + use std::rc::Rc; |
186 | use std::time::Duration; |
187 | |
188 | use bytes::Bytes; |
189 | @@ -13,34 +14,13 @@ use crate::session::Session; |
190 | use crate::transport::Transport; |
191 | use crate::Chunk; |
192 | |
193 | - const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:2525"; |
194 | - const DEFAULT_GREETING: &str = "Maitred ESMTP Server"; |
195 | + /// The default port the server will listen on if none was specified in it's |
196 | + /// configuration options. |
197 | + pub const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:2525"; |
198 | + |
199 | // Maximum amount of time the server will wait for a command before closing |
200 | // the connection. |
201 | const DEFAULT_GLOBAL_TIMEOUT_SECS: u64 = 300; |
202 | - const DEFAULT_HELP_BANNER: &str = r#" |
203 | - Maitred ESMTP Server: |
204 | - see https://ayllu-forge.org/ayllu/maitred for more information. |
205 | - "#; |
206 | - |
207 | - /// Maximum message size the server will accept |
208 | - const DEFAULT_MAXIMUM_SIZE: u64 = 5_000_000; |
209 | - |
210 | - // target |
211 | - // 250-PIPELINING |
212 | - // 250-SIZE 10240000 |
213 | - // 250-VRFY |
214 | - // 250-ETRN |
215 | - // 250-ENHANCEDSTATUSCODES |
216 | - // 250-8BITMIME |
217 | - // 250-DSN |
218 | - // 250-SMTPUTF8 |
219 | - // 250 CHUNKING |
220 | - |
221 | - pub const DEFAULT_CAPABILITIES: u32 = smtp_proto::EXT_SIZE |
222 | - | smtp_proto::EXT_ENHANCED_STATUS_CODES |
223 | - | smtp_proto::EXT_PIPELINING |
224 | - | smtp_proto::EXT_8BIT_MIME; |
225 | |
226 | /// Apply pipelining if running in extended mode and configured to support it |
227 | struct ConditionalPipeline<'a> { |
228 | @@ -68,14 +48,14 @@ impl ConditionalPipeline<'_> { |
229 | } |
230 | } |
231 | |
232 | + /// Server implements everything that is required to run an SMTP server by |
233 | + /// binding to the configured address and processing individual TCP connections |
234 | + /// as they are received. |
235 | pub struct Server { |
236 | address: String, |
237 | hostname: String, |
238 | - greeting: String, |
239 | global_timeout: Duration, |
240 | - help_banner: String, |
241 | - maximum_size: u64, |
242 | - capabilities: u32, |
243 | + options: Option<Rc<crate::session::Options>>, |
244 | } |
245 | |
246 | impl Default for Server { |
247 | @@ -83,11 +63,8 @@ impl Default for Server { |
248 | Server { |
249 | address: DEFAULT_LISTEN_ADDR.to_string(), |
250 | hostname: String::default(), |
251 | - greeting: DEFAULT_GREETING.to_string(), |
252 | global_timeout: Duration::from_secs(DEFAULT_GLOBAL_TIMEOUT_SECS), |
253 | - help_banner: DEFAULT_HELP_BANNER.to_string(), |
254 | - maximum_size: DEFAULT_MAXIMUM_SIZE, |
255 | - capabilities: DEFAULT_CAPABILITIES, |
256 | + options: None, |
257 | } |
258 | } |
259 | } |
260 | @@ -101,12 +78,6 @@ impl Server { |
261 | } |
262 | } |
263 | |
264 | - /// Greeting message returned from the server upon initial connection. |
265 | - pub fn greeting(mut self, greeting: &str) -> Self { |
266 | - self.greeting = greeting.to_string(); |
267 | - self |
268 | - } |
269 | - |
270 | /// Listener address for the SMTP server to bind to listen for incoming |
271 | /// connections. |
272 | pub fn address(mut self, address: &str) -> Self { |
273 | @@ -121,10 +92,11 @@ impl Server { |
274 | self |
275 | } |
276 | |
277 | - /// Set the maximum size of a message which if exceeded will result in |
278 | - /// rejection. |
279 | - pub fn maximum_size(mut self, size: u64) -> Self { |
280 | - self.maximum_size = size; |
281 | + /// Set session level options that affect the behavior of individual SMTP |
282 | + /// sessions. Most custom behavior is implemented here but not specifying |
283 | + /// any options will provide a limited but functional server. |
284 | + pub fn with_session_opts(mut self, opts: crate::session::Options) -> Self { |
285 | + self.options = Some(Rc::new(opts)); |
286 | self |
287 | } |
288 | |
289 | @@ -132,19 +104,20 @@ impl Server { |
290 | where |
291 | T: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin, |
292 | { |
293 | - let mut session = Session::default() |
294 | - .capabilities(self.capabilities) |
295 | - .maximum_size(self.maximum_size) |
296 | - .our_hostname(&self.hostname) |
297 | - .help_banner(&self.help_banner); |
298 | + let mut session = Session::default(); |
299 | + if let Some(opts) = &self.options { |
300 | + session = session.with_options(opts.clone()); |
301 | + } |
302 | + |
303 | + let greeting = session.greeting(); |
304 | + |
305 | let mut pipelined = ConditionalPipeline { |
306 | session: &mut session, |
307 | pipeline: &mut Pipeline::default(), |
308 | }; |
309 | + |
310 | // send inital server greeting |
311 | - framed |
312 | - .send(crate::session::greeting(&self.hostname, &self.greeting)) |
313 | - .await?; |
314 | + framed.send(greeting).await?; |
315 | |
316 | 'outer: loop { |
317 | let frame = timeout(self.global_timeout, framed.next()).await; |
318 | diff --git a/maitred/src/session.rs b/maitred/src/session.rs |
319 | index e6d8324..47a143e 100644 |
320 | --- a/maitred/src/session.rs |
321 | +++ b/maitred/src/session.rs |
322 | @@ -1,3 +1,4 @@ |
323 | + use std::rc::Rc; |
324 | use std::result::Result as StdResult; |
325 | use std::str::FromStr; |
326 | |
327 | @@ -13,6 +14,35 @@ use crate::verify::Verify; |
328 | use crate::{smtp_chunk, smtp_chunk_err, smtp_chunk_ok}; |
329 | use crate::{smtp_response, Chunk}; |
330 | |
331 | + /// Default help banner returned from a HELP command without any parameters |
332 | + pub const DEFAULT_HELP_BANNER: &str = r#" |
333 | + Maitred ESMTP Server: |
334 | + see https://ayllu-forge.org/ayllu/maitred for more information. |
335 | + "#; |
336 | + |
337 | + /// Maximum message size the server will accept. |
338 | + pub const DEFAULT_MAXIMUM_MESSAGE_SIZE: u64 = 5_000_000; |
339 | + |
340 | + /// Default greeting returned by the server upon initial connection. |
341 | + pub const DEFAULT_GREETING: &str = "Maitred ESMTP Server"; |
342 | + |
343 | + // TODO: |
344 | + // 250-PIPELINING |
345 | + // 250-SIZE 10240000 |
346 | + // 250-VRFY |
347 | + // 250-ETRN |
348 | + // 250-ENHANCEDSTATUSCODES |
349 | + // 250-8BITMIME |
350 | + // 250-DSN |
351 | + // 250-SMTPUTF8 |
352 | + // 250 CHUNKING |
353 | + |
354 | + /// Default SMTP capabilities advertised by the server |
355 | + pub const DEFAULT_CAPABILITIES: u32 = smtp_proto::EXT_SIZE |
356 | + | smtp_proto::EXT_ENHANCED_STATUS_CODES |
357 | + | smtp_proto::EXT_PIPELINING |
358 | + | smtp_proto::EXT_8BIT_MIME; |
359 | + |
360 | /// Result generated as part of an SMTP session, an Err indicates a session |
361 | /// level error that will be returned to the client. |
362 | pub type Result = StdResult<Chunk, Chunk>; |
363 | @@ -27,41 +57,38 @@ enum DataTransfer { |
364 | Bdat, |
365 | } |
366 | |
367 | - /// A greeting must be sent at the start of an SMTP connection when it is |
368 | - /// first initialized. |
369 | - pub fn greeting(hostname: &str, greeting: &str) -> Response<String> { |
370 | - smtp_response!(220, 2, 0, 0, format!("{} {}", hostname, greeting)) |
371 | - } |
372 | - |
373 | /// Sent when the connection exceeds the maximum configured timeout |
374 | pub fn timeout(message: &str) -> Response<String> { |
375 | smtp_response!(421, 4, 4, 2, format!("Timeout exceeded: {}", message)) |
376 | } |
377 | |
378 | - /// Stateful connection that coresponds to a single SMTP session |
379 | - #[derive(Default)] |
380 | - pub(crate) struct Session { |
381 | - /// message body |
382 | - pub body: Option<Vec<u8>>, |
383 | - /// mailto address |
384 | - pub mail_from: Option<EmailAddress>, |
385 | - /// rcpt address |
386 | - pub rcpt_to: Option<Vec<EmailAddress>>, |
387 | - pub hostname: Option<Host>, |
388 | - /// If an active data transfer is taking place |
389 | - data_transfer: Option<DataTransfer>, |
390 | - initialized: Option<Mode>, |
391 | + /// Session level options that configure individual SMTP transactions |
392 | + #[derive(Clone)] |
393 | + pub struct Options { |
394 | + pub our_hostname: String, |
395 | + pub maximum_size: u64, |
396 | + pub capabilities: u32, |
397 | + pub help_banner: String, |
398 | + pub greeting: String, |
399 | + pub list_expansion: Option<Rc<dyn Expansion>>, |
400 | + pub verification: Option<Rc<dyn Verify>>, |
401 | + } |
402 | |
403 | - // session options |
404 | - our_hostname: String, |
405 | - maximum_size: u64, |
406 | - capabilities: u32, |
407 | - help_banner: String, |
408 | - list_expansion: Option<Box<dyn Expansion>>, |
409 | - verification: Option<Box<dyn Verify>>, |
410 | + impl Default for Options { |
411 | + fn default() -> Self { |
412 | + Options { |
413 | + our_hostname: String::default(), |
414 | + maximum_size: DEFAULT_MAXIMUM_MESSAGE_SIZE, |
415 | + capabilities: DEFAULT_CAPABILITIES, |
416 | + help_banner: DEFAULT_HELP_BANNER.to_string(), |
417 | + greeting: DEFAULT_GREETING.to_string(), |
418 | + list_expansion: None, |
419 | + verification: None, |
420 | + } |
421 | + } |
422 | } |
423 | |
424 | - impl Session { |
425 | + impl Options { |
426 | pub fn our_hostname(mut self, hostname: &str) -> Self { |
427 | self.our_hostname = hostname.to_string(); |
428 | self |
429 | @@ -86,7 +113,7 @@ impl Session { |
430 | where |
431 | T: crate::expand::Expansion + 'static, |
432 | { |
433 | - self.list_expansion = Some(Box::new(expansion)); |
434 | + self.list_expansion = Some(Rc::new(expansion)); |
435 | self |
436 | } |
437 | |
438 | @@ -94,7 +121,32 @@ impl Session { |
439 | where |
440 | T: crate::verify::Verify + 'static, |
441 | { |
442 | - self.verification = Some(Box::new(verification)); |
443 | + self.verification = Some(Rc::new(verification)); |
444 | + self |
445 | + } |
446 | + } |
447 | + |
448 | + /// Stateful connection that coresponds to a single SMTP session |
449 | + #[derive(Default)] |
450 | + pub(crate) struct Session { |
451 | + /// message body |
452 | + pub body: Option<Vec<u8>>, |
453 | + /// mailto address |
454 | + pub mail_from: Option<EmailAddress>, |
455 | + /// rcpt address |
456 | + pub rcpt_to: Option<Vec<EmailAddress>>, |
457 | + pub hostname: Option<Host>, |
458 | + /// If an active data transfer is taking place |
459 | + data_transfer: Option<DataTransfer>, |
460 | + initialized: Option<Mode>, |
461 | + |
462 | + // session options |
463 | + opts: Rc<Options>, |
464 | + } |
465 | + |
466 | + impl Session { |
467 | + pub fn with_options(mut self, opts: Rc<Options>) -> Self { |
468 | + self.opts = opts; |
469 | self |
470 | } |
471 | |
472 | @@ -106,6 +158,17 @@ impl Session { |
473 | // self.hostname = None; |
474 | self.data_transfer = None; |
475 | } |
476 | + /// A greeting must be sent at the start of an SMTP connection when it is |
477 | + /// first initialized. |
478 | + pub fn greeting(&self) -> Response<String> { |
479 | + smtp_response!( |
480 | + 220, |
481 | + 2, |
482 | + 0, |
483 | + 0, |
484 | + format!("{} {}", self.opts.our_hostname, self.opts.greeting) |
485 | + ) |
486 | + } |
487 | |
488 | /// If the session is in extended mode i.e. EHLO was sent |
489 | pub fn is_extended(&self) -> bool { |
490 | @@ -119,7 +182,7 @@ impl Session { |
491 | self.initialized |
492 | .as_ref() |
493 | .is_some_and(|mode| matches!(mode, Mode::Extended)) |
494 | - && self.capabilities & capability != 0 |
495 | + && self.opts.capabilities & capability != 0 |
496 | } |
497 | |
498 | /// ensure that the session has been initialized otherwise return an error |
499 | @@ -160,8 +223,8 @@ impl Session { |
500 | self.reset(); |
501 | self.initialized = Some(Mode::Extended); |
502 | let mut resp = EhloResponse::new(format!("Hello {}", host)); |
503 | - resp.capabilities = self.capabilities; |
504 | - resp.size = self.maximum_size as usize; |
505 | + resp.capabilities = self.opts.capabilities; |
506 | + resp.size = self.opts.maximum_size as usize; |
507 | Ok(Chunk(vec![Response::Ehlo(resp)])) |
508 | } |
509 | Request::Lhlo { host } => { |
510 | @@ -250,7 +313,7 @@ impl Session { |
511 | smtp_chunk_ok!(250, 0, 0, 0, "OK".to_string()) |
512 | } |
513 | Request::Vrfy { value } => { |
514 | - if let Some(verifier) = &self.verification { |
515 | + if let Some(verifier) = &self.opts.verification { |
516 | let address = EmailAddress::from_str(value.as_str()).map_err(|e| { |
517 | smtp_chunk!(500, 0, 0, 0, format!("cannot parse: {} {}", value, e)) |
518 | })?; |
519 | @@ -265,7 +328,7 @@ impl Session { |
520 | } |
521 | } |
522 | Request::Expn { value } => { |
523 | - if let Some(expn) = &self.list_expansion { |
524 | + if let Some(expn) = &self.opts.list_expansion { |
525 | match expn.expand(value) { |
526 | Ok(addresses) => { |
527 | let mut result = vec![smtp_response!(250, 0, 0, 0, "OK")]; |
528 | @@ -285,7 +348,7 @@ impl Session { |
529 | Request::Help { value } => { |
530 | self.check_initialized()?; |
531 | if value.is_empty() { |
532 | - smtp_chunk_ok!(250, 0, 0, 0, self.help_banner.to_string()) |
533 | + smtp_chunk_ok!(250, 0, 0, 0, self.opts.help_banner.to_string()) |
534 | } else { |
535 | smtp_chunk_ok!( |
536 | 250, |
537 | @@ -445,7 +508,8 @@ mod test { |
538 | payload: None, |
539 | expected: smtp_chunk_err!(500, 0, 0, 0, String::from("It's polite to say EHLO first")), |
540 | }]; |
541 | - let mut session = Session::default().our_hostname(EXAMPLE_HOSTNAME); |
542 | + let mut session = Session::default() |
543 | + .with_options(Options::default().our_hostname(EXAMPLE_HOSTNAME).into()); |
544 | process_all(&mut session, requests); |
545 | } |
546 | |
547 | @@ -476,13 +540,17 @@ mod test { |
548 | expected: smtp_chunk_ok!(221, 0, 0, 0, String::from("Ciao!")), |
549 | }, |
550 | ]; |
551 | - let mut session = Session::default().list_expansion(crate::expand::Func(|name: &str| { |
552 | - assert!(name == "mailing-list"); |
553 | - Ok(vec![ |
554 | - EmailAddress::new_unchecked("Fuu <fuu@bar.com>"), |
555 | - EmailAddress::new_unchecked("Baz <baz@qux.com>"), |
556 | - ]) |
557 | - })); |
558 | + let mut session = Session::default().with_options( |
559 | + Options::default() |
560 | + .list_expansion(crate::expand::Func(|name: &str| { |
561 | + assert!(name == "mailing-list"); |
562 | + Ok(vec![ |
563 | + EmailAddress::new_unchecked("Fuu <fuu@bar.com>"), |
564 | + EmailAddress::new_unchecked("Baz <baz@qux.com>"), |
565 | + ]) |
566 | + })) |
567 | + .into(), |
568 | + ); |
569 | process_all(&mut session, requests); |
570 | // session should contain both requests |
571 | assert!(session |
572 | @@ -505,9 +573,7 @@ mod test { |
573 | value: "Fuu <bar@baz.com>".to_string(), |
574 | }, |
575 | payload: None, |
576 | - expected: Ok(Chunk(vec![ |
577 | - smtp_response!(250, 0, 0, 0, "OK"), |
578 | - ])), |
579 | + expected: Ok(Chunk(vec![smtp_response!(250, 0, 0, 0, "OK")])), |
580 | }, |
581 | TestCase { |
582 | request: Request::Quit {}, |
583 | @@ -515,10 +581,14 @@ mod test { |
584 | expected: smtp_chunk_ok!(221, 0, 0, 0, String::from("Ciao!")), |
585 | }, |
586 | ]; |
587 | - let mut session = Session::default().verification(crate::verify::Func(|addr: &EmailAddress| { |
588 | - assert!(addr.email() == "bar@baz.com"); |
589 | - Ok(()) |
590 | - })); |
591 | + let mut session = Session::default().with_options( |
592 | + Options::default() |
593 | + .verification(crate::verify::Func(|addr: &EmailAddress| { |
594 | + assert!(addr.email() == "bar@baz.com"); |
595 | + Ok(()) |
596 | + })) |
597 | + .into(), |
598 | + ); |
599 | process_all(&mut session, requests); |
600 | // session should contain both requests |
601 | assert!(session |
602 | @@ -529,7 +599,8 @@ mod test { |
603 | #[test] |
604 | fn test_non_ascii_characters() { |
605 | let mut expected_ehlo_response = EhloResponse::new(String::from("Hello example.org")); |
606 | - expected_ehlo_response.capabilities = crate::server::DEFAULT_CAPABILITIES; |
607 | + expected_ehlo_response.capabilities = DEFAULT_CAPABILITIES; |
608 | + expected_ehlo_response.size = DEFAULT_MAXIMUM_MESSAGE_SIZE as usize; |
609 | let requests = &[ |
610 | TestCase { |
611 | request: Request::Helo { |
612 | @@ -595,9 +666,12 @@ mod test { |
613 | expected: smtp_chunk_ok!(250, 0, 0, 0, "OK"), |
614 | }, |
615 | ]; |
616 | - let mut session = Session::default() |
617 | - .our_hostname(EXAMPLE_HOSTNAME) |
618 | - .capabilities(crate::server::DEFAULT_CAPABILITIES); |
619 | + let mut session = Session::default().with_options( |
620 | + Options::default() |
621 | + .our_hostname(EXAMPLE_HOSTNAME) |
622 | + .capabilities(DEFAULT_CAPABILITIES) |
623 | + .into(), |
624 | + ); |
625 | process_all(&mut session, requests); |
626 | } |
627 | |
628 | @@ -658,7 +732,8 @@ transport rather than the session. |
629 | expected: smtp_chunk_ok!(250, 0, 0, 0, "OK"), |
630 | }, |
631 | ]; |
632 | - let mut session = Session::default().our_hostname(EXAMPLE_HOSTNAME); |
633 | + let mut session = Session::default() |
634 | + .with_options(Options::default().our_hostname(EXAMPLE_HOSTNAME).into()); |
635 | process_all(&mut session, requests); |
636 | assert!(session |
637 | .mail_from |
638 | diff --git a/maitred/src/verify.rs b/maitred/src/verify.rs |
639 | index b37507b..7aa2742 100644 |
640 | --- a/maitred/src/verify.rs |
641 | +++ b/maitred/src/verify.rs |
642 | @@ -2,6 +2,8 @@ use std::result::Result as StdResult; |
643 | |
644 | use email_address::EmailAddress; |
645 | |
646 | + /// Result indicating the VRFY command was successful and the user was |
647 | + /// correctly identified by the server. |
648 | pub type Result = StdResult<(), Error>; |
649 | |
650 | /// An error encountered while verifying an e-mail address |
651 | @@ -22,12 +24,14 @@ pub enum Error { |
652 | }, |
653 | } |
654 | |
655 | + /// Verify that the given e-mail address exists on the server. Servers may |
656 | + /// choose to implement nothing or not use this option at all if desired. |
657 | pub trait Verify { |
658 | /// Verify the e-mail address on the server |
659 | fn verify(&self, address: &EmailAddress) -> Result; |
660 | } |
661 | |
662 | - /// Wrapper type implementing the Verify trait |
663 | + /// Helper wrapper implementing the Verify trait. |
664 | pub struct Func<F>(pub F) |
665 | where |
666 | F: Fn(&EmailAddress) -> Result; |