Author: Kevin Schoon [me@kevinschoon.com]
Hash: b375baaa6a4aa5065e75ac8cae7c83c63cbb0fd9
Timestamp: Mon, 12 Aug 2024 18:39:25 +0000 (2 months ago)

+198 -114 +/-8 browse
finish implementing VRFY and EXPN
1diff --git a/README.md b/README.md
2index d48e59b..0f6394c 100644
3--- a/README.md
4+++ b/README.md
5 @@ -39,8 +39,8 @@ for _absolutely nothing_ that is important.
6 | BDAT | ✅ | |
7 | DATA | ✅ | |
8 | AUTH | ❌ | No authentication mechanisms currently supported |
9- | VRFY | ❌ | |
10- | EXPN | ❌ | |
11+ | VRFY | ✅ | |
12+ | EXPN | ✅ | |
13 | STARTTLS | ❌ | For the moment there is no plan to implement STARTTLS |
14
15
16 diff --git a/maitred/src/addresses.rs b/maitred/src/addresses.rs
17deleted file mode 100644
18index db8c8a3..0000000
19--- a/maitred/src/addresses.rs
20+++ /dev/null
21 @@ -1,14 +0,0 @@
22- use std::fmt::Display;
23-
24- use email_address::EmailAddress;
25-
26- /// Array of resolved e-mail addresses that are associated with a mailing list
27- #[derive(Debug)]
28- pub struct Addresses(pub Vec<EmailAddress>);
29-
30- impl Display for Addresses {
31- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32- let addresses: Vec<String> = self.0.iter().map(|address| address.to_string()).collect();
33- write!(f, "{}", addresses.join("\n"))
34- }
35- }
36 diff --git a/maitred/src/expand.rs b/maitred/src/expand.rs
37new file mode 100644
38index 0000000..85bc3db
39--- /dev/null
40+++ b/maitred/src/expand.rs
41 @@ -0,0 +1,40 @@
42+ use std::result::Result as StdResult;
43+
44+ use email_address::EmailAddress;
45+
46+ pub type Result = StdResult<Vec<EmailAddress>, Error>;
47+
48+ /// An error encountered while expanding a mail address
49+ #[derive(Debug, thiserror::Error)]
50+ pub enum Error {
51+ /// Indicates an unspecified error that occurred during expansion
52+ #[error("Internal Server Error: {0}")]
53+ Server(String),
54+ /// Indicates that no group exists with the specified name
55+ #[error("Group Not Found: {0}")]
56+ NotFound(String),
57+ }
58+
59+ /// Expands a string representing a mailing list to an array of the associated
60+ /// addresses within the list if it exists. NOTE: That this function should
61+ /// only be called with proper authentication otherwise it could be used to
62+ /// harvest e-mail addresses.
63+ pub trait Expansion {
64+ /// Expand the group into an array of members
65+ fn expand(&self, name: &str) -> Result;
66+ }
67+
68+ /// Wrapper type implementing the Expansion trait
69+ pub struct Func<F>(pub F)
70+ where
71+ F: Fn(&str) -> Result;
72+
73+ impl<F> Expansion for Func<F>
74+ where
75+ F: Fn(&str) -> Result,
76+ {
77+ fn expand(&self, name: &str) -> Result {
78+ let f = &self.0;
79+ f(name)
80+ }
81+ }
82 diff --git a/maitred/src/expansion.rs b/maitred/src/expansion.rs
83deleted file mode 100644
84index 16d4588..0000000
85--- a/maitred/src/expansion.rs
86+++ /dev/null
87 @@ -1,40 +0,0 @@
88- use std::result::Result as StdResult;
89-
90- use crate::addresses::Addresses;
91-
92- pub type Result = StdResult<Addresses, Error>;
93-
94- /// An error encountered while expanding a mail address
95- #[derive(Debug, thiserror::Error)]
96- pub enum Error {
97- /// Indicates an unspecified error that occurred during expansion
98- #[error("Internal Server Error: {0}")]
99- Server(String),
100- /// Indicates that no group exists with the specified name
101- #[error("Group Not Found: {0}")]
102- NotFound(String),
103- }
104-
105- /// Expands a string representing a mailing list to an array of the associated
106- /// addresses within the list if it exists. NOTE: That this function should
107- /// only be called with proper authentication otherwise it could be used to
108- /// harvest e-mail addresses.
109- pub trait Expansion {
110- /// Expand the group into an array of members
111- fn expand(&self, name: &str) -> Result;
112- }
113-
114- /// Wrapper type implementing the Expansion trait
115- pub struct Func<F>(pub F)
116- where
117- F: Fn(&str) -> Result;
118-
119- impl<F> Expansion for Func<F>
120- where
121- F: Fn(&str) -> Result,
122- {
123- fn expand(&self, name: &str) -> Result {
124- let f = &self.0;
125- f(name)
126- }
127- }
128 diff --git a/maitred/src/lib.rs b/maitred/src/lib.rs
129index 7ea9531..e7a0a99 100644
130--- a/maitred/src/lib.rs
131+++ b/maitred/src/lib.rs
132 @@ -1,13 +1,12 @@
133- mod addresses;
134 mod error;
135- mod expansion;
136+ mod expand;
137 mod pipeline;
138 mod server;
139 mod session;
140 mod transport;
141 mod verify;
142
143- use smtp_proto::Request;
144+ use smtp_proto::{Request, Response as SmtpResponse};
145
146 /// Low Level SMTP protocol is exported for convenience
147 pub use smtp_proto;
148 @@ -22,18 +21,6 @@ use transport::Response;
149 #[derive(Clone, Debug)]
150 pub(crate) struct Chunk(pub Vec<Response<String>>);
151
152- impl Chunk {
153- pub fn new() -> Self {
154- Chunk(vec![])
155- }
156- }
157-
158- impl PartialEq for Chunk {
159- fn eq(&self, other: &Self) -> bool {
160- self.0.len() == other.0.len() && self.0.iter().zip(other.0.iter()).all(|(a, b)| a == b)
161- }
162- }
163-
164 /// Generate a single smtp_response
165 macro_rules! smtp_response {
166 ($code:expr, $e1:expr, $e2:expr, $e3:expr, $name:expr) => {
167 @@ -83,3 +70,57 @@ macro_rules! smtp_chunk_err {
168 };
169 }
170 pub(crate) use smtp_chunk_err;
171+
172+ impl Chunk {
173+ pub fn new() -> Self {
174+ Chunk(vec![])
175+ }
176+ }
177+
178+ impl PartialEq for Chunk {
179+ fn eq(&self, other: &Self) -> bool {
180+ self.0.len() == other.0.len() && self.0.iter().zip(other.0.iter()).all(|(a, b)| a == b)
181+ }
182+ }
183+
184+ impl From<crate::verify::Error> for Chunk {
185+ fn from(value: crate::verify::Error) -> Self {
186+ match value {
187+ crate::verify::Error::Server(e) => Chunk(vec![smtp_response!(500, 0, 0, 0, e)]),
188+ crate::verify::Error::NotFound(e) => Chunk(vec![smtp_response!(500, 0, 0, 0, e)]),
189+ crate::verify::Error::Ambiguous {
190+ email,
191+ alternatives,
192+ } => {
193+ let mut result = vec![smtp_response!(
194+ 500,
195+ 0,
196+ 0,
197+ 0,
198+ format!("Username {} Ambigious", email)
199+ )];
200+ result.extend(
201+ alternatives
202+ .iter()
203+ .map(|alt| smtp_response!(500, 0, 0, 0, alt.to_string())),
204+ );
205+ Chunk(result)
206+ }
207+ }
208+ }
209+ }
210+
211+ impl From<crate::expand::Error> for Chunk {
212+ fn from(value: crate::expand::Error) -> Self {
213+ match value {
214+ expand::Error::Server(message) => Chunk(vec![smtp_response!(500, 0, 0, 0, message)]),
215+ expand::Error::NotFound(name) => Chunk(vec![smtp_response!(
216+ 500,
217+ 0,
218+ 0,
219+ 0,
220+ format!("Cannot find: {}", name)
221+ )]),
222+ }
223+ }
224+ }
225 diff --git a/maitred/src/pipeline.rs b/maitred/src/pipeline.rs
226index cc99703..4933030 100644
227--- a/maitred/src/pipeline.rs
228+++ b/maitred/src/pipeline.rs
229 @@ -62,10 +62,7 @@ impl Pipeline {
230 .expect("to results called without history");
231 if last_command.1.is_ok() && mail_from_ok && rcpt_to_ok_count > 0 {
232 flatten(&self.history)
233- } else if !mail_from_ok {
234- self.history.pop();
235- flatten(&self.history)
236- } else if !rcpt_to_ok_count <= 0 {
237+ } else if !mail_from_ok || rcpt_to_ok_count <= 0{
238 self.history.pop();
239 flatten(&self.history)
240 } else {
241 diff --git a/maitred/src/session.rs b/maitred/src/session.rs
242index d1598b7..e6d8324 100644
243--- a/maitred/src/session.rs
244+++ b/maitred/src/session.rs
245 @@ -7,7 +7,7 @@ use mail_parser::MessageParser;
246 use smtp_proto::{EhloResponse, Request, Response as SmtpResponse};
247 use url::Host;
248
249- use crate::expansion::Expansion;
250+ use crate::expand::Expansion;
251 use crate::transport::Response;
252 use crate::verify::Verify;
253 use crate::{smtp_chunk, smtp_chunk_err, smtp_chunk_ok};
254 @@ -84,14 +84,17 @@ impl Session {
255
256 pub fn list_expansion<T>(mut self, expansion: T) -> Self
257 where
258- T: crate::expansion::Expansion + 'static,
259+ T: crate::expand::Expansion + 'static,
260 {
261 self.list_expansion = Some(Box::new(expansion));
262 self
263 }
264
265- pub fn verification(mut self, verification: Box<dyn Verify>) -> Self {
266- self.verification = Some(verification);
267+ pub fn verification<T>(mut self, verification: T) -> Self
268+ where
269+ T: crate::verify::Verify + 'static,
270+ {
271+ self.verification = Some(Box::new(verification));
272 self
273 }
274
275 @@ -253,19 +256,9 @@ impl Session {
276 })?;
277 match verifier.verify(&address) {
278 Ok(_) => {
279- smtp_chunk_ok!(200, 0, 0, 0, "Ok".to_string())
280+ smtp_chunk_ok!(250, 0, 0, 0, "OK".to_string())
281 }
282- Err(e) => match e {
283- crate::verify::Error::Server(e) => {
284- smtp_chunk_err!(500, 0, 0, 0, e.to_string())
285- }
286- crate::verify::Error::NotFound(e) => {
287- smtp_chunk_err!(500, 0, 0, 0, e.to_string())
288- }
289- crate::verify::Error::Ambiguous(alternatives) => {
290- smtp_chunk_err!(500, 0, 0, 0, alternatives.to_string())
291- }
292- },
293+ Err(e) => Err(e.into()),
294 }
295 } else {
296 smtp_chunk_err!(500, 0, 0, 0, "No such address")
297 @@ -275,28 +268,15 @@ impl Session {
298 if let Some(expn) = &self.list_expansion {
299 match expn.expand(value) {
300 Ok(addresses) => {
301- smtp_chunk_ok!(250, 0, 0, 0, addresses.to_string())
302+ let mut result = vec![smtp_response!(250, 0, 0, 0, "OK")];
303+ result.extend(
304+ addresses
305+ .iter()
306+ .map(|addr| smtp_response!(250, 0, 0, 0, addr.to_string())),
307+ );
308+ Ok(Chunk(result))
309 }
310- Err(err) => match err {
311- crate::expansion::Error::Server(message) => {
312- smtp_chunk_err!(
313- 500,
314- 0,
315- 0,
316- 0,
317- format!("Internal mailing list error: {}", message)
318- )
319- }
320- crate::expansion::Error::NotFound(name) => {
321- smtp_chunk_err!(
322- 500,
323- 0,
324- 0,
325- 0,
326- format!("No such mailing list: {}", name)
327- )
328- }
329- },
330+ Err(e) => Err(e.into()),
331 }
332 } else {
333 smtp_chunk_err!(500, 0, 0, 0, "Server does not support EXPN")
334 @@ -470,6 +450,83 @@ mod test {
335 }
336
337 #[test]
338+ fn test_expand() {
339+ let requests = &[
340+ TestCase {
341+ request: Request::Helo {
342+ host: EXAMPLE_HOSTNAME.to_string(),
343+ },
344+ payload: None,
345+ expected: smtp_chunk_ok!(250, 0, 0, 0, String::from("Hello example.org")),
346+ },
347+ TestCase {
348+ request: Request::Expn {
349+ value: "mailing-list".to_string(),
350+ },
351+ payload: None,
352+ expected: Ok(Chunk(vec![
353+ smtp_response!(250, 0, 0, 0, "OK"),
354+ smtp_response!(250, 0, 0, 0, "Fuu <fuu@bar.com>"),
355+ smtp_response!(250, 0, 0, 0, "Baz <baz@qux.com>"),
356+ ])),
357+ },
358+ TestCase {
359+ request: Request::Quit {},
360+ payload: None,
361+ expected: smtp_chunk_ok!(221, 0, 0, 0, String::from("Ciao!")),
362+ },
363+ ];
364+ let mut session = Session::default().list_expansion(crate::expand::Func(|name: &str| {
365+ assert!(name == "mailing-list");
366+ Ok(vec![
367+ EmailAddress::new_unchecked("Fuu <fuu@bar.com>"),
368+ EmailAddress::new_unchecked("Baz <baz@qux.com>"),
369+ ])
370+ }));
371+ process_all(&mut session, requests);
372+ // session should contain both requests
373+ assert!(session
374+ .hostname
375+ .is_some_and(|hostname| hostname.to_string() == EXAMPLE_HOSTNAME));
376+ }
377+
378+ #[test]
379+ fn test_verify() {
380+ let requests = &[
381+ TestCase {
382+ request: Request::Helo {
383+ host: EXAMPLE_HOSTNAME.to_string(),
384+ },
385+ payload: None,
386+ expected: smtp_chunk_ok!(250, 0, 0, 0, String::from("Hello example.org")),
387+ },
388+ TestCase {
389+ request: Request::Vrfy {
390+ value: "Fuu <bar@baz.com>".to_string(),
391+ },
392+ payload: None,
393+ expected: Ok(Chunk(vec![
394+ smtp_response!(250, 0, 0, 0, "OK"),
395+ ])),
396+ },
397+ TestCase {
398+ request: Request::Quit {},
399+ payload: None,
400+ expected: smtp_chunk_ok!(221, 0, 0, 0, String::from("Ciao!")),
401+ },
402+ ];
403+ let mut session = Session::default().verification(crate::verify::Func(|addr: &EmailAddress| {
404+ assert!(addr.email() == "bar@baz.com");
405+ Ok(())
406+ }));
407+ process_all(&mut session, requests);
408+ // session should contain both requests
409+ assert!(session
410+ .hostname
411+ .is_some_and(|hostname| hostname.to_string() == EXAMPLE_HOSTNAME));
412+ }
413+
414+ #[test]
415 fn test_non_ascii_characters() {
416 let mut expected_ehlo_response = EhloResponse::new(String::from("Hello example.org"));
417 expected_ehlo_response.capabilities = crate::server::DEFAULT_CAPABILITIES;
418 diff --git a/maitred/src/verify.rs b/maitred/src/verify.rs
419index 44224cb..b37507b 100644
420--- a/maitred/src/verify.rs
421+++ b/maitred/src/verify.rs
422 @@ -15,8 +15,11 @@ pub enum Error {
423 NotFound(String),
424 /// Indicates that the input as ambigious and multiple addresses are
425 /// associated with the string.
426- #[error("Name is Ambiguous: {0}")]
427- Ambiguous(EmailAddress),
428+ #[error("Name is Ambiguous: {email}")]
429+ Ambiguous {
430+ email: EmailAddress,
431+ alternatives: Vec<EmailAddress>,
432+ },
433 }
434
435 pub trait Verify {