Author: Kevin Schoon [me@kevinschoon.com]
Hash: 93848f7eea54d802dad625fb132ba81fb35093d5
Timestamp: Sun, 01 Sep 2024 21:06:58 +0000 (1 month ago)

+41 -12 +/-3 browse
fix host parsing per rfc
1diff --git a/cmd/maitred-debug/src/main.rs b/cmd/maitred-debug/src/main.rs
2index 97457be..03008f2 100644
3--- a/cmd/maitred-debug/src/main.rs
4+++ b/cmd/maitred-debug/src/main.rs
5 @@ -6,15 +6,9 @@ use maitred::{
6 };
7
8 async fn print_message(message: Message<'static>) -> Result<(), DeliveryError> {
9- println!("New SMTP Message:");
10- println!("{:?}", message.headers());
11- println!("Subject: {:?}", message.subject());
12 println!(
13- "{}",
14- message
15- .body_text(0)
16- .map(|text| String::from_utf8_lossy(text.as_bytes()).to_string())
17- .unwrap_or_default()
18+ "New SMTP Message:\n{}",
19+ String::from_utf8_lossy(message.raw_message())
20 );
21 Ok(())
22 }
23 diff --git a/maitred/src/auth.rs b/maitred/src/auth.rs
24index cea772e..7a0c25d 100644
25--- a/maitred/src/auth.rs
26+++ b/maitred/src/auth.rs
27 @@ -134,7 +134,6 @@ impl TryFrom<&str> for AuthData {
28 }
29 raw_data[n].push(*ch);
30 }
31- println!("N: {}", n);
32 if n == 0 {
33 return Err(AuthError::NotEnoughFields);
34 }
35 diff --git a/maitred/src/session.rs b/maitred/src/session.rs
36index 00e2d53..f5fd268 100644
37--- a/maitred/src/session.rs
38+++ b/maitred/src/session.rs
39 @@ -59,6 +59,24 @@ pub fn timeout(message: &str) -> Response<String> {
40 smtp_response!(421, 4, 4, 2, format!("Timeout exceeded: {}", message))
41 }
42
43+ /// Extract a host from HELO/EHLO per RFC5321 4.1.3
44+ fn parse_host(host: &str) -> String {
45+ // confusingly the url library determines if an address is IPv6 by checking
46+ // for [ ] but SMTP uses "tags" to determine this.
47+ let n_periods = host
48+ .chars()
49+ .fold(0, |accm, c| if c == '.' { accm + 1 } else { accm });
50+ if n_periods == 3 {
51+ host.trim_start_matches("[")
52+ .trim_end_matches("]")
53+ .to_string()
54+ } else if host.contains("IPv6:") {
55+ format!("[{}]", host.replace("IPv6:", "").trim())
56+ } else {
57+ host.to_string()
58+ }
59+ }
60+
61 /// Session level options that configure individual SMTP transactions
62 #[derive(Clone)]
63 pub struct SessionOptions {
64 @@ -283,7 +301,8 @@ impl Session {
65 match req {
66 Request::Ehlo { host } => {
67 self.hostname = Some(
68- Host::parse(host).map_err(|e| smtp_response!(500, 0, 0, 0, e.to_string()))?,
69+ Host::parse(&parse_host(host))
70+ .map_err(|e| smtp_response!(500, 0, 0, 0, e.to_string()))?,
71 );
72 self.reset();
73 self.initialized = Some(Mode::Extended);
74 @@ -310,7 +329,7 @@ impl Session {
75 }
76 Request::Helo { host } => {
77 self.hostname = Some(
78- Host::parse(host).map_err(|e| smtp_response!(500, 0, 0, 0, e.to_string()))?,
79+ Host::parse(&parse_host(host)).map_err(|e| smtp_response!(500, 0, 0, 0, e.to_string()))?,
80 );
81 self.reset();
82 self.initialized = Some(Mode::Legacy);
83 @@ -380,7 +399,11 @@ impl Session {
84 AuthData::try_from(initial_response.as_str()).map_err(|e| e.into())?;
85
86 auth_fn
87- .authenticate(&auth_data.authcid(), &auth_data.authzid(), &auth_data.passwd())
88+ .authenticate(
89+ &auth_data.authcid(),
90+ &auth_data.authzid(),
91+ &auth_data.passwd(),
92+ )
93 .await
94 .map_err(|e| e.into())?;
95
96 @@ -876,4 +899,17 @@ transport rather than the session.
97 .is_some_and(|subject| subject == "Hello World")
98 }));
99 }
100+
101+ #[tokio::test]
102+ pub async fn test_domain_parsing() {
103+ let mut session = Session::default();
104+ for host in ["127.0.0.1", "[127.0.0.1]", "example.org", "IPv6: ::1"] {
105+ session
106+ .process(&Request::Ehlo {
107+ host: host.to_string(),
108+ })
109+ .await
110+ .unwrap();
111+ }
112+ }
113 }