Author: Kevin Schoon [me@kevinschoon.com]
Hash: 796a8504a17da413b99372d926a68b0b5c1346f2
Timestamp: Mon, 12 Aug 2024 22:20:01 +0000 (2 months ago)

+222 -119 +/-10 browse
clean up documentation and improve crate structure
1diff --git a/Cargo.lock b/Cargo.lock
2index 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
14index 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
35index 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
46index 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
60index 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
101index 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
156index 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
181index 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
319index 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
639index 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;