1 use std::collections::HashMap;
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
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
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
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
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
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 }
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 {
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
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
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 }
124 }
125 Ok((method, path, headers))
126 }
127
128 fn memchr_crlfcrlf(buf: &[u8]) -> bool {
129 buf.windows(4).any(|w| w == b"\r\n\r\n")
130 }
131
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) => {
140 auth.handle_client(stream);
141 }
142 Err(e) => eprintln!("accept error: {e}"),
143 }
144 }
145 Ok(())
146 }