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 {