handle incoming and outgoing mail
Seto committed
Oct 14, 2025 at 12:55 UTC
0092d989a8ef6062a74a4ac24a9c7d41c828f43f
2 files changed
+123
-42
crates/nginx-auth-http/src/main.rs
+109
-40
@@ -1,77 +1,146 @@
1
-use base64::{engine::general_purpose, Engine as _};
1
use std::collections::HashMap;
3
-use std::io::prelude::*;
2
+use std::io::{Read, Write};
3
use std::net::{TcpListener, TcpStream};
4
5
struct AuthServer {
6
users: HashMap<String, String>,
7
+ upstream_host: String,
8
+ smtp_port: u16,
9
+ imap_port: u16,
10
}
11
12
impl AuthServer {
13
fn new() -> Self {
14
let mut users = HashMap::new();
15
+ // TODO: replace with secure storage (DB/KMS). This is only a demo.
16
users.insert("admin".to_string(), "your_admin_password".to_string());
17
users.insert("user".to_string(), "your_user_password".to_string());
18
16
- AuthServer { users }
19
+ AuthServer {
20
+ users,
21
+ upstream_host: "127.0.0.1".to_string(),
22
+ smtp_port: 2525, // Your smbcloud-mail-smtp SMTP server
23
+ imap_port: 1143, // Your smbcloud-mail-smtp IMAP server
24
+ }
25
}
26
19
- fn handle_request(&self, mut stream: TcpStream) {
20
- let mut buffer = [0; 1024];
21
- stream.read(&mut buffer).unwrap();
27
+ fn handle_client(&self, mut stream: TcpStream) {
28
+ let Ok((method, _path, headers)) = read_http_request(&mut stream) else {
29
+ let _ = stream.write_all(b"HTTP/1.1 500 Internal Server Error\r\n\r\n");
30
+ let _ = stream.flush();
31
+ return;
32
+ };
33
23
- let request = String::from_utf8_lossy(&buffer[..]);
24
- let lines: Vec<&str> = request.lines().collect();
34
+ if method != "GET" && method != "POST" {
35
+ let _ = stream.write_all(b"HTTP/1.1 405 Method Not Allowed\r\n\r\n");
36
+ let _ = stream.flush();
37
+ return;
38
+ }
39
26
- // Find Authorization header
27
- let auth_header = lines
28
- .iter()
29
- .find(|line| line.to_lowercase().starts_with("authorization:"))
30
- .map(|line| line.split_once(": ").unwrap_or(("", "")).1);
40
+ let method_hdr = get_hdr(&headers, "auth-method").unwrap_or("plain");
41
+ let user = get_hdr(&headers, "auth-user").unwrap_or("");
42
+ let pass = get_hdr(&headers, "auth-pass").unwrap_or("");
43
+ let proto = get_hdr(&headers, "auth-protocol").unwrap_or("smtp");
44
32
- let response = if let Some(auth) = auth_header {
33
- if auth.starts_with("Basic ") {
34
- match self.validate_credentials(&auth[6..]) {
35
- true => "HTTP/1.1 200 OK\r\n\r\n",
36
- false => "HTTP/1.1 401 Unauthorized\r\n\r\n",
45
+ // Validate credentials
46
+ let mut ok = false;
47
+ if method_hdr.eq_ignore_ascii_case("plain") {
48
+ if !user.is_empty() && !pass.is_empty() {
49
+ if let Some(stored) = self.users.get(user) {
50
+ ok = stored == pass;
51
}
38
- } else {
39
- "HTTP/1.1 401 Unauthorized\r\n\r\n"
52
}
53
+ }
54
+
55
+ if ok {
56
+ let (host, port) = match proto {
57
+ p if p.eq_ignore_ascii_case("smtp") => (&self.upstream_host, self.smtp_port),
58
+ p if p.eq_ignore_ascii_case("imap") => (&self.upstream_host, self.imap_port),
59
+ _ => (&self.upstream_host, self.smtp_port),
60
+ };
61
+
62
+ let resp = format!(
63
+ "HTTP/1.1 200 OK\r\n\
64
+ Auth-Status: OK\r\n\
65
+ Auth-Server: {host}\r\n\
66
+ Auth-Port: {port}\r\n\
67
+ Auth-User: {user}\r\n\
68
+ Auth-Wait: 0\r\n\
69
+ \r\n"
70
+ );
71
+ let _ = stream.write_all(resp.as_bytes());
72
+ let _ = stream.flush();
73
} else {
42
- "HTTP/1.1 401 Unauthorized\r\n\r\n"
43
- };
74
+ let resp =
75
+ "HTTP/1.1 200 OK\r\nAuth-Status: Invalid login or password\r\nAuth-Wait: 2\r\n\r\n";
76
+ let _ = stream.write_all(resp.as_bytes());
77
+ let _ = stream.flush();
78
+ }
79
+ }
80
+}
81
+
82
+fn get_hdr<'a>(headers: &'a Vec<(String, String)>, name: &str) -> Option<&'a str> {
83
+ headers
84
+ .iter()
85
+ .find(|(k, _)| k.eq_ignore_ascii_case(name))
86
+ .map(|(_, v)| v.as_str())
87
+}
88
45
- stream.write_all(response.as_bytes()).unwrap();
46
- stream.flush().unwrap();
89
+fn read_http_request(
90
+ stream: &mut TcpStream,
91
+) -> Result<(String, String, Vec<(String, String)>), ()> {
92
+ let mut buf = Vec::with_capacity(4096);
93
+ let mut tmp = [0u8; 1024];
94
+ loop {
95
+ let n = stream.read(&mut tmp).map_err(|_| ())?;
96
+ if n == 0 {
97
+ break;
98
+ }
99
+ buf.extend_from_slice(&tmp[..n]);
100
+ if memchr_crlfcrlf(&buf) {
101
+ break;
102
+ }
103
+ if buf.len() > 64 * 1024 {
104
+ break;
105
+ }
106
}
107
49
- fn validate_credentials(&self, encoded_creds: &str) -> bool {
50
- if let Ok(decoded) = general_purpose::STANDARD.decode(encoded_creds) {
51
- if let Ok(creds_str) = String::from_utf8(decoded) {
52
- if let Some((username, password)) = creds_str.split_once(':') {
53
- return self.users.get(username).map_or(false, |p| p == password);
54
- }
55
- }
108
+ let req = String::from_utf8_lossy(&buf);
109
+ let mut lines = req.split("\r\n");
110
+
111
+ let req_line = lines.next().ok_or(())?;
112
+ let mut parts = req_line.split_whitespace();
113
+ let method = parts.next().unwrap_or("").to_string();
114
+ let path = parts.next().unwrap_or("/").to_string();
115
+
116
+ let mut headers = Vec::new();
117
+ for line in lines {
118
+ if line.is_empty() {
119
+ break;
120
+ }
121
+ if let Some((k, v)) = line.split_once(':') {
122
+ headers.push((k.trim().to_string(), v.trim().to_string()));
123
}
57
- false
124
}
125
+ Ok((method, path, headers))
126
}
127
61
-fn main() {
62
- let auth_server = AuthServer::new();
63
- let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
128
+fn memchr_crlfcrlf(buf: &[u8]) -> bool {
129
+ buf.windows(4).any(|w| w == b"\r\n\r\n")
130
+}
131
65
- println!("Auth server running on port 8080");
132
+fn main() -> std::io::Result<()> {
133
+ let auth = AuthServer::new();
134
+ let listener = TcpListener::bind("127.0.0.1:8080")?;
135
+ eprintln!("Mail auth_http server listening on 127.0.0.1:8080");
136
137
for stream in listener.incoming() {
138
match stream {
139
Ok(stream) => {
70
- auth_server.handle_request(stream);
71
- }
72
- Err(e) => {
73
- println!("Error: {}", e);
140
+ auth.handle_client(stream);
141
}
142
+ Err(e) => eprintln!("accept error: {e}"),
143
}
144
}
145
+ Ok(())
146
}
docs/nginx/config.conf
+14
-2
@@ -14,10 +14,22 @@ mail {
14
ssl_session_cache shared:SSL:10m;
15
ssl_session_timeout 10m;
16
17
+ # Port 25: INCOMING mail (no auth required)
18
server {
18
- listen 2525;
19
+ listen 25;
20
protocol smtp;
20
- smtp_auth login plain cram-md5;
21
+ starttls on;
22
+ proxy on;
23
+ # NO smtp_auth here - accept mail from anyone
24
+ }
25
+
26
+ # Port 587: OUTGOING mail (auth required for your users)
27
+ server {
28
+ listen 587;
29
+ protocol smtp;
30
+ smtp_auth login plain;
31
+ starttls on;
32
+ proxy on;
33
}
34
35
server {