]> git.r.bdr.sh - rbdr/olden-mail/commitdiff
Add 1.0.0
authorRuben Beltran del Rio <redacted>
Sat, 25 Jan 2025 09:50:26 +0000 (10:50 +0100)
committerRuben Beltran del Rio <redacted>
Sat, 25 Jan 2025 09:50:26 +0000 (10:50 +0100)
.build.yml [new file with mode: 0644]
Cargo.lock
Cargo.toml
README.md [new file with mode: 0644]
man/olden-mail.1 [new file with mode: 0644]
src/configuration.rs
src/main.rs
src/proxy.rs

diff --git a/.build.yml b/.build.yml
new file mode 100644 (file)
index 0000000..3f8ae0d
--- /dev/null
@@ -0,0 +1,27 @@
+image: archlinux
+packages:
+  - make
+  - rsync
+  - coreutils
+  - lld
+  - rustup
+  - aarch64-linux-gnu-gcc
+  - tar
+  - gzip
+sources:
+  - git@git.sr.ht:~rbdr/olden-mail
+secrets:
+  - 89d3b676-25d6-4942-8231-38b73aa62bf6
+  - 0b0d3e5e-fbdc-41d0-97ed-ee654fe797ff
+tasks:
+  - set_rust: |
+      cd olden-mail
+      make set_rust
+  - install_coverage_tool: |
+      cargo install cargo-tarpaulin
+  - install_builders: |
+      cargo install cargo-generate-rpm
+      cargo install cargo-deb
+  - package: |
+      cd page
+      make ci
index 2a7a12961edc9b89c5fee4e4157cc9de5420a02d..75e3b667cbf892bbf86212ff6bfaa19960ab30bd 100644 (file)
@@ -2,6 +2,65 @@
 # It is not intended for manual editing.
 version = 4
 
 # It is not intended for manual editing.
 version = 4
 
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
+dependencies = [
+ "anstyle",
+ "once_cell",
+ "windows-sys",
+]
+
 [[package]]
 name = "bitflags"
 version = "2.8.0"
 [[package]]
 name = "bitflags"
 version = "2.8.0"
@@ -23,6 +82,18 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+
 [[package]]
 name = "core-foundation"
 version = "0.9.4"
 [[package]]
 name = "core-foundation"
 version = "0.9.4"
@@ -39,6 +110,39 @@ version = "0.8.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
 
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
 
+[[package]]
+name = "ctrlc"
+version = "3.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3"
+dependencies = [
+ "nix",
+ "windows-sys",
+]
+
+[[package]]
+name = "env_filter"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "humantime",
+ "log",
+]
+
 [[package]]
 name = "errno"
 version = "0.3.10"
 [[package]]
 name = "errno"
 version = "0.3.10"
@@ -81,6 +185,18 @@ dependencies = [
  "wasi",
 ]
 
  "wasi",
 ]
 
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
 [[package]]
 name = "libc"
 version = "0.2.169"
 [[package]]
 name = "libc"
 version = "0.2.169"
@@ -99,6 +215,12 @@ version = "0.4.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
 
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
 
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
 [[package]]
 name = "native-tls"
 version = "0.2.12"
 [[package]]
 name = "native-tls"
 version = "0.2.12"
@@ -116,11 +238,27 @@ dependencies = [
  "tempfile",
 ]
 
  "tempfile",
 ]
 
+[[package]]
+name = "nix"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
 [[package]]
 name = "olden-mail"
 [[package]]
 name = "olden-mail"
-version = "0.1.0"
+version = "1.0.0"
 dependencies = [
 dependencies = [
+ "ctrlc",
+ "env_logger",
+ "log",
  "native-tls",
  "native-tls",
+ "thiserror",
 ]
 
 [[package]]
 ]
 
 [[package]]
@@ -197,6 +335,35 @@ dependencies = [
  "proc-macro2",
 ]
 
  "proc-macro2",
 ]
 
+[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
 [[package]]
 name = "rustix"
 version = "0.38.44"
 [[package]]
 name = "rustix"
 version = "0.38.44"
@@ -273,12 +440,38 @@ dependencies = [
  "windows-sys",
 ]
 
  "windows-sys",
 ]
 
+[[package]]
+name = "thiserror"
+version = "2.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "unicode-ident"
 version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243"
 
 [[package]]
 name = "unicode-ident"
 version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243"
 
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
 [[package]]
 name = "vcpkg"
 version = "0.2.15"
 [[package]]
 name = "vcpkg"
 version = "0.2.15"
index a7114b20deceeffe381efaf5c5b92b33496296d8..918043157ea54d066ae868ac322133973c2713fc 100644 (file)
@@ -1,7 +1,35 @@
 [package]
 name = "olden-mail"
 [package]
 name = "olden-mail"
-version = "0.1.0"
+version = "1.0.0"
 edition = "2021"
 edition = "2021"
+license = "AGPL-3.0-or-later"
+description = "IMAP & SMTP proxy to connect vintage clients to modern e-mail."
+homepage = "https://r.bdr.sh/olden-mail.html"
+authors = ["Rubén Beltrán del Río <olden.mail@r.bdr.sh>"]
 
 [dependencies]
 
 [dependencies]
+ctrlc = "3.4.5"
+env_logger = "0.11.6"
+log = "0.4.25"
 native-tls = "0.2.12"
 native-tls = "0.2.12"
+thiserror = "2.0.11"
+
+[profile.release]
+strip = true
+lto = true
+panic = "abort"
+
+[package.metadata.generate-rpm]
+assets = [
+    { source = "target/release/olden-mail", dest = "/usr/bin/olden-mail", mode = "755" },
+    { source = "man/olden-mail.1", dest = "/usr/share/man/man1/olden-mail.1", mode = "644" },
+]
+
+[package.metadata.deb]
+assets = [
+    ["target/release/olden-mail", "/usr/bin/olden-mail", "755" ],
+    ["man/olden-mail.1", "/usr/share/man/man1/olden-mail.1", "644" ],
+]
+
+[lints.clippy]
+pedantic = "warn"
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..6c580f7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,66 @@
+# olden-mail
+
+`olden-mail` is an IMAP & SMTP proxy that allows you to connect to SSL
+enabled mail servers using insecure plaintext connections.
+
+This is of course very insecure, but it's intended to allow vintage
+computers that don't have SSL capability or ther right ciphers to still
+be used for e-mail.
+
+## Installation
+
+### Pre-built binaries and packages
+
+Binaries are available for macos and linux, for both aarch64 and x86_64 
+from https://build.r.bdr.sh/olden-mail, including `.deb` and `.rpm`
+packages.
+
+### Homebrew
+
+You can also install this on macos via homebrew.
+
+```
+% brew tap rbdr/apps git@git.sr.ht:~rbdr/homebrew-apps
+% brew install rbdr/apps/olden-mail
+```
+
+### From source
+
+You can run `make` for a debug build, or `make -e profile=release` for a
+release build.
+
+## Usage
+
+The proxy requires you to set environment variables, but otherwise takes
+no options.
+
+* `LOCAL_IMAP_PORT` u16, the port in which the server will listen for
+IMAP clients. Defaults to 143.
+* `LOCAL_IMAP_BIND_ADDRESS` String, the address on which to listen for
+IMAP clients. Defaults to 0.0.0.0.
+* `REMOTE_IMAP_PORT` u16, the port to which the server will forward the
+IMAP messages. Defaults to 993.
+* `REMOTE_IMAP_HOST` String, the host to which the server will forward the
+IMAP messages. Required.
+
+* `LOCAL_SMTP_PORT` u16, the port in which the server will listen for
+SMTP clients. Defaults to 25.
+* `LOCAL_SMTP_BIND_ADDRESS` String, the address on which to listen for
+SMTP clients. Defaults to 0.0.0.0.
+* `REMOTE_SMTP_PORT` u16, the port to which the server will forward the
+SMTP messages. Defaults to 465.
+* `REMOTE_SMTP_HOST` String, the host to which the server will forward the
+SMTP messages. Required.
+
+This means the minimum invocation is this (Shown here with inline
+environment variables)
+
+```
+% REMOTE_IMAP_DOMAIN=imap.coolmailsite.example REMOTE_SMTP_DOMAIN=smtp.coolmailsite.example olden-mail
+```
+
+## Debugging
+
+You can control how much it prints by setting `RUST_LOG`. Setting it to
+`debug` will output the whole protocol stream. The default level is
+`info`.
diff --git a/man/olden-mail.1 b/man/olden-mail.1
new file mode 100644 (file)
index 0000000..e72a08e
--- /dev/null
@@ -0,0 +1,44 @@
+.TH OLDEN-MAIL 1 "2025-01-25" "1.0.0" "Olden Mail Manual"
+.SH NAME
+olden-mail \- IMAP & SMTP proxy to connect vintage clients to modern e-mail.
+.SH SYNOPSIS
+.B olden-mail
+.SH DESCRIPTION
+.PP
+`olden-mail` is an IMAP & SMTP proxy that allows you to connect to SSL
+enabled mail servers using insecure plaintext connections in computers that
+don't support SSL or have older ciphers.
+.SH ENVIRONMENT VARIABLES
+You can configure the proxy through environment variables.
+.TP
+.B LOCAL_IMAP_PORT
+The port on which to listen for IMAP clients. Defaults to 143.
+.TP
+.B LOCAL_IMAP_BIND_ADDRESS
+The address on which to listen for IMAP clients. Defaults to 0.0.0.0.
+.TP
+.B REMOTE_IMAP_PORT
+The TLS enabled remote port to which we will proxy the connection. Defaults to
+993.
+.TP
+.B REMOTE_IMAP_HOST
+The remote host to which we will proxy the connection. Required.
+.TP
+.B LOCAL_SMTP_PORT
+The port on which to listen for SMTP clients. Defaults to 25.
+.TP
+.B LOCAL_SMTP_BIND_ADDRESS
+The address on which to listen for SMTP clients. Defaults to 0.0.0.0.
+.TP
+.B REMOTE_SMTP_PORT
+The TLS enabled remote port to which we will proxy the connection. Defaults to
+465.
+.TP
+.B REMOTE_SMTP_HOST
+The remote host to which we will proxy the connection. Required.
+.SH VERSION
+.BR 1.0.0
+.SH HOMEPAGE
+.I https://r.bdr.sh/olden-mail.html
+.SH AUTHORS
+\ Rubén Beltrán del Río <\fIolden.mail@r.bdr.sh\fP>
index 143759438bac1e9213a80094640a7a760acf0de5..af5c46d815fd54c1bf413a7c74a4aec487cc50e2 100644 (file)
@@ -1,45 +1,78 @@
+//! # Configuration
+//!
+//! This module defines the configuration used to initialize both proxies.
+
+use log::error;
 use std::env;
 use std::env;
+use std::fmt::Debug;
 use std::sync::Arc;
 use std::sync::Arc;
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum ConfigurationError {
+    #[error("Environment variable {0} not set.")]
+    MissingEnvVar(String),
+    #[error("Failed to parse {0}.")]
+    ParseError(String),
+}
 
 
+/// Configuration for any proxy
+#[derive(Debug)]
 pub struct ProxyConfiguration {
     pub local_port: u16,
 pub struct ProxyConfiguration {
     pub local_port: u16,
-    pub remote_domain: String,
+    pub bind_address: String,
+    pub remote_host: String,
     pub remote_port: u16,
     pub protocol: &'static str,
 }
 
     pub remote_port: u16,
     pub protocol: &'static str,
 }
 
+/// Aggregated configuration for both proxies, already in a reference counter.
 pub struct Configuration {
     pub imap_configuration: Arc<ProxyConfiguration>,
     pub smtp_configuration: Arc<ProxyConfiguration>,
 }
 
 impl Configuration {
 pub struct Configuration {
     pub imap_configuration: Arc<ProxyConfiguration>,
     pub smtp_configuration: Arc<ProxyConfiguration>,
 }
 
 impl Configuration {
-    pub fn new() -> Self {
-        Configuration {
+    /// Creates a new configuration object by reading the environment
+    /// variables. Exits if the right ones aren't found.
+    pub fn new() -> Result<Self, ConfigurationError> {
+        Ok(Configuration {
             imap_configuration: Arc::new(ProxyConfiguration {
             imap_configuration: Arc::new(ProxyConfiguration {
-                local_port: env::var("LOCAL_IMAP_PORT")
-                    .expect("LOCAL_IMAP_PORT not set")
-                    .parse()
-                    .expect("Invalid LOCAL_IMAP_PORT"),
-                remote_domain: env::var("REMOTE_IMAP_DOMAIN").expect("REMOTE_IMAP_DOMAIN not set"),
-                remote_port: env::var("REMOTE_IMAP_PORT")
-                    .expect("REMOTE_IMAP_PORT not set")
-                    .parse()
-                    .expect("Invalid REMOTE_IMAP_PORT"),
+                local_port: get_env_number("LOCAL_IMAP_PORT", 143)?,
+                bind_address: get_env_var("LOCAL_IMAP_BIND_ADDRESS", Some("0.0.0.0".to_string()))?,
+                remote_host: get_env_var("REMOTE_IMAP_DOMAIN", None)?,
+                remote_port: get_env_number("REMOTE_IMAP_PORT", 993)?,
                 protocol: "IMAP",
             }),
             smtp_configuration: Arc::new(ProxyConfiguration {
                 protocol: "IMAP",
             }),
             smtp_configuration: Arc::new(ProxyConfiguration {
-                local_port: env::var("LOCAL_SMTP_PORT")
-                    .expect("LOCAL_SMTP_PORT not set")
-                    .parse()
-                    .expect("Invalid LOCAL_SMTP_PORT"),
-                remote_domain: env::var("REMOTE_SMTP_DOMAIN").expect("REMOTE_SMTP_DOMAIN not set"),
-                remote_port: env::var("REMOTE_SMTP_PORT")
-                    .expect("REMOTE_SMTP_PORT not set")
-                    .parse()
-                    .expect("Invalid REMOTE_SMTP_PORT"),
+                local_port: get_env_number("LOCAL_SMTP_PORT", 25)?,
+                bind_address: get_env_var("LOCAL_SMTP_BIND_ADDRESS", Some("0.0.0.0".to_string()))?,
+                remote_host: get_env_var("REMOTE_SMTP_DOMAIN", None)?,
+                remote_port: get_env_number("REMOTE_SMTP_PORT", 993)?,
                 protocol: "SMTP",
             }),
                 protocol: "SMTP",
             }),
-        }
+        })
+    }
+}
+
+/// Get an environment variable or return an error.
+fn get_env_var(name: &str, default: Option<String>) -> Result<String, ConfigurationError> {
+    match env::var(name) {
+        Ok(value) => Ok(value),
+        Err(_) => match default {
+            Some(default_value) => Ok(default_value),
+            None => Err(ConfigurationError::MissingEnvVar(name.to_string())),
+        },
+    }
+}
+
+/// Get an environment variable and parse it as a number. Return a default
+/// if not set.
+fn get_env_number(name: &str, default: u16) -> Result<u16, ConfigurationError> {
+    match env::var(name) {
+        Ok(value) => value
+            .parse()
+            .map_err(|_| ConfigurationError::ParseError(name.to_string())),
+        Err(_) => Ok(default),
     }
 }
     }
 }
index 858e4602dd99543a1b2a27baa74393088703cc9c..863b2eef928240c9ba809f8ea8f1967af6d6d635 100644 (file)
+//! # olden-mail
+//!
+//! `olden-mail` is an IMAP & SMTP proxy that allows you to connect to SSL
+//! enabled mail servers using insecure plaintext connections.
+//!
+//! This is of course very insecure, but it's intended to allow vintage
+//! computers that don't have SSL capability or ther right ciphers to still
+//! be used for e-mail.
+//!
+//! ## Installation
+//!
+//! ### Pre-built binaries and packages
+//!
+//! Binaries are available for macos and linux, for both aarch64 and x86_64
+//! from https://build.r.bdr.sh/olden-mail, including `.deb` and `.rpm`
+//! packages.
+//!
+//! ### Homebrew
+//!
+//! You can also install this on macos via homebrew.
+//!
+//! ```
+//! % brew tap rbdr/apps git@git.sr.ht:~rbdr/homebrew-apps
+//! % brew install rbdr/apps/olden-mail
+//! ```
+//!
+//! ### From source
+//!
+//! You can run `make` for a debug build, or `make -e profile=release` for a
+//! release build.
+//!
+//! ## Usage
+//!
+//! The proxy requires you to set environment variables, but otherwise takes
+//! no options.
+//!
+//! * `LOCAL_IMAP_PORT` u16, the port in which the server will listen for
+//! IMAP clients. Defaults to 143.
+//! * `LOCAL_IMAP_BIND_ADDRESS` String, the address on which to listen for
+//! IMAP clients. Defaults to 0.0.0.0.
+//! * `REMOTE_IMAP_PORT` u16, the port to which the server will forward the
+//! IMAP messages. Defaults to 993.
+//! * `REMOTE_IMAP_HOST` String, the host to which the server will forward the
+//! IMAP messages. Required.
+//!
+//! * `LOCAL_SMTP_PORT` u16, the port in which the server will listen for
+//! SMTP clients. Defaults to 25.
+//! * `LOCAL_SMTP_BIND_ADDRESS` String, the address on which to listen for
+//! SMTP clients. Defaults to 0.0.0.0.
+//! * `REMOTE_SMTP_PORT` u16, the port to which the server will forward the
+//! SMTP messages. Defaults to 465.
+//! * `REMOTE_SMTP_HOST` String, the host to which the server will forward the
+//! SMTP messages. Required.
+//!
+//! This means the minimum invocation is this (Shown here with inline
+//! environment variables)
+//!
+//! ```
+//! % REMOTE_IMAP_DOMAIN=imap.coolmailsite.example REMOTE_SMTP_DOMAIN=smtp.coolmailsite.example olden-mail
+//! ```
+//!
+//! ## Debugging
+//!
+//! You can control how much it prints by setting `RUST_LOG`. Setting it to
+//! `debug` will output the whole protocol stream. The default level is
+//! `info`.
+
+use env_logger::{Builder, Env};
+use log::{error, info};
+use std::process::exit;
+use std::sync::mpsc;
+
 mod configuration;
 mod proxy;
 
 use configuration::Configuration;
 mod configuration;
 mod proxy;
 
 use configuration::Configuration;
-use proxy::create_proxy;
+use proxy::ProxyServer;
 
 fn main() {
 
 fn main() {
-    let configuration = Configuration::new();
+    Builder::from_env(Env::default().default_filter_or("info"))
+        .format_timestamp_millis()
+        .init();
+
+    let configuration = Configuration::new().unwrap_or_else(|error| {
+        error!("{error}");
+        exit(1);
+    });
+
+    let (tx, rx) = mpsc::channel();
+
+    let mut imap_proxy = ProxyServer::new(configuration.imap_configuration);
+    let mut smtp_proxy = ProxyServer::new(configuration.smtp_configuration);
 
 
-    let imap_proxy = create_proxy(configuration.imap_configuration);
-    let smtp_proxy = create_proxy(configuration.smtp_configuration);
+    ctrlc::set_handler(move || {
+        info!("Shutting down...");
+        imap_proxy.shutdown();
+        smtp_proxy.shutdown();
+        let _ = tx.send(());
+    })
+    .unwrap_or_else(|e| {
+        error!("Error setting Ctrl-C handler: {e}");
+        std::process::exit(1);
+    });
 
 
-    imap_proxy.join().unwrap();
-    smtp_proxy.join().unwrap();
+    // Block until we get a shutdown
+    rx.recv().unwrap_or(());
 }
 }
index ce40e6ed1ebb1bc5dec98fa693d35141589b7ead..c6954a06c11e5867aab8f831c6e8f7868135a7ba 100644 (file)
+//! # Proxy Module
+//!
+//! This module has the actual proxy functionality, exposed through
+//! `ProxyServer`. The proxy consists of a local unencrypted TCP stream
+//! and a remote TLS stream. Messages are passed between them via two
+//! threads.
+//!
+//! Each new client connection spawns two threads:
+//! - **Client to Server Thread**: Forwards data from client -> TLS server
+//! - **Serveer to Client Thread**: Forwards data from TLS server -> client
+//!
+//! Finally, the ProxyServer may be shutdown by calling `.shutdown()`,
+//! this will stop new connections and wait for it to finish.
+//!
+//! # Example
+//!
+//! ```
+//! use std::sync::Arc;
+//! use crate::configuration::ProxyConfiguration;
+//! use crate::proxy::ProxyServer;
+//!
+//! let config = Arc::new(ProxyConfiguration {
+//!     protocol: "IMAP".to_string(),
+//!     local_port: 143,
+//!     remote_domain: "imap.example.com".to_string(),
+//!     remote_port: 993,
+//! });
+//!
+//! let mut server = ProxyServer::new(config);
+//! // The server runs in a background thread. To shut down gracefully:
+//! server.shutdown();
+//! ```
+use log::{debug, error, info};
 use native_tls::TlsConnector;
 use native_tls::TlsConnector;
-use std::io::{Read, Write};
+use std::io::{ErrorKind, Read, Write};
 use std::net::{TcpListener, TcpStream};
 use std::net::{TcpListener, TcpStream};
-use std::sync::Arc;
-use std::thread::{spawn, JoinHandle};
+use std::sync::{
+    atomic::{AtomicBool, Ordering},
+    Arc, Mutex,
+};
+use std::thread::{sleep, spawn, JoinHandle};
+use std::time::Duration;
 
 use crate::configuration::ProxyConfiguration;
 
 
 use crate::configuration::ProxyConfiguration;
 
-pub fn create_proxy(configuration: Arc<ProxyConfiguration>) -> JoinHandle<()> {
-    let cloned_configuration = Arc::clone(&configuration);
-    spawn(move || {
-        run_proxy(cloned_configuration);
-    })
+/// A proxy server that listens for plaintext connections and forwards them
+/// via TLS.
+///
+/// Creating a new `ProxyServer` spawns a dedicated thread that:
+/// - Binds to a local port (non-blocking mode).
+/// - Spawns additional threads for each incoming client connection.
+/// - Manages connection-lifetime until it receives a shutdown signal.
+pub struct ProxyServer {
+    running: Arc<AtomicBool>,
+    thread_handle: Option<JoinHandle<()>>,
 }
 
 }
 
-fn run_proxy(configuration: Arc<ProxyConfiguration>) {
-    let listener = TcpListener::bind(format!("0.0.0.0:{}", configuration.local_port)).unwrap();
+impl ProxyServer {
+    /// Creates a new `ProxyServer` for the given `ProxyConfiguration` and
+    /// immediately starts it.
+    ///
+    /// # Arguments
+    ///
+    /// * `configuration` - Shared (Arc) `ProxyConfiguration`
+    ///
+    /// # Returns
+    ///
+    /// A `ProxyServer` instance that will keep running until its `.shutdown()`
+    /// method is called, or an error occurs.
+    pub fn new(configuration: Arc<ProxyConfiguration>) -> Self {
+        let running = Arc::new(AtomicBool::new(true));
+        let running_clone = Arc::clone(&running);
 
 
-    println!("Proxy listening on port {}", configuration.local_port);
+        let thread_handle = spawn(move || {
+            run_proxy(configuration, running_clone);
+        });
 
 
-    for stream in listener.incoming() {
-        match stream {
-            Ok(stream) => {
-                let cloned_configuration = Arc::clone(&configuration);
-                spawn(move || {
-                    handle_client(stream, cloned_configuration);
+        ProxyServer {
+            running,
+            thread_handle: Some(thread_handle),
+        }
+    }
+
+    /// Signals this proxy to stop accepting new connections and waits
+    /// for all active connection threads to complete.
+    pub fn shutdown(&mut self) {
+        self.running.store(false, Ordering::SeqCst);
+        if let Some(handle) = self.thread_handle.take() {
+            let _ = handle.join();
+        }
+    }
+}
+
+/// The main loop that listens for incoming (plaintext) connections on
+/// `configuration.bind_address:configuration.local_port`.
+fn run_proxy(configuration: Arc<ProxyConfiguration>, running: Arc<AtomicBool>) {
+    let listener = match TcpListener::bind(format!(
+        "{}:{}",
+        configuration.bind_address, configuration.local_port
+    )) {
+        Ok(l) => l,
+        Err(e) => {
+            error!("Failed to bind to port {}: {}", configuration.local_port, e);
+            return;
+        }
+    };
+    listener.set_nonblocking(true).unwrap();
+
+    info!(
+        "{} proxy listening on port {}:{}",
+        configuration.protocol, configuration.bind_address, configuration.local_port
+    );
+
+    // Keep track of active connections so we can join them on shutdown
+    let mut active_threads = vec![];
+
+    while running.load(Ordering::SeqCst) {
+        match listener.accept() {
+            Ok((stream, addr)) => {
+                info!("New {} connection from {}", configuration.protocol, addr);
+
+                let configuration_clone = Arc::clone(&configuration);
+                let handle = spawn(move || {
+                    handle_client(stream, configuration_clone);
                 });
                 });
+                active_threads.push(handle);
+            }
+            Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
+                // No pending connection; sleep briefly then loop again
+                sleep(Duration::from_millis(100));
+                continue;
             }
             Err(e) => {
             }
             Err(e) => {
-                eprintln!("Failed to accept connection: {}", e);
+                error!("Error accepting connection: {}", e);
+                break;
             }
         }
             }
         }
+
+        // Clean up any finished threads
+        active_threads.retain(|thread| !thread.is_finished());
+
+        // Potential Improvement: Configure thread limit.
+        if active_threads.len() >= 50 {
+            sleep(Duration::from_millis(100));
+        }
+    }
+
+    // On shutdown, wait for all threads to finish
+    for thread in active_threads {
+        let _ = thread.join();
     }
 }
 
     }
 }
 
-fn handle_client(mut client_stream: TcpStream, configuration: Arc<ProxyConfiguration>) {
-    let connector = TlsConnector::new().unwrap();
-    let remote_stream = TcpStream::connect(format!(
+/// Handles a single client connection by bridging it (plaintext) to a TLS connection.
+fn handle_client(client_stream: TcpStream, configuration: Arc<ProxyConfiguration>) {
+    if let Err(e) = client_stream.set_nonblocking(true) {
+        error!("Failed to set client stream to nonblocking: {}", e);
+        return;
+    }
+
+    let connector = match TlsConnector::new() {
+        Ok(c) => c,
+        Err(e) => {
+            error!("Failed to create TLS connector: {}", e);
+            return;
+        }
+    };
+
+    let remote_addr = format!(
         "{}:{}",
         "{}:{}",
-        configuration.remote_domain, configuration.remote_port
-    ))
-    .unwrap();
-    let mut remote_stream = connector
-        .connect(&configuration.remote_domain, remote_stream)
-        .unwrap();
-
-    let mut client_stream_clone = client_stream.try_clone().unwrap();
-    let mut remote_stream_clone = remote_stream.get_ref().try_clone().unwrap();
-
-    let cloned_configuration = Arc::clone(&configuration);
-    spawn(move || {
-        forward_stream(&mut client_stream, &mut remote_stream, cloned_configuration);
-    });
-    forward_stream(
-        &mut remote_stream_clone,
-        &mut client_stream_clone,
-        configuration,
+        configuration.remote_host, configuration.remote_port
     );
     );
-}
+    let tcp_stream = match TcpStream::connect(&remote_addr) {
+        Ok(stream) => stream,
+        Err(e) => {
+            error!("Failed to connect to {}: {}", remote_addr, e);
+            return;
+        }
+    };
+
+    let tls_stream = match connector.connect(&configuration.remote_host, tcp_stream) {
+        Ok(tls_stream) => tls_stream,
+        Err(e) => {
+            error!(
+                "TLS handshake to {} failed: {}",
+                configuration.remote_host, e
+            );
+            return;
+        }
+    };
+
+    // The nonblocking needs to be set AFTER the TLS handshake is completed.
+    // Otherwise the TLS handshake is interrupted.
+    if let Err(e) = tls_stream.get_ref().set_nonblocking(true) {
+        error!("Failed to set remote stream to nonblocking: {}", e);
+        return;
+    }
+
+    let tls_stream = Arc::new(Mutex::new(tls_stream));
+
+    let client_stream_clone = match client_stream.try_clone() {
+        Ok(s) => s,
+        Err(e) => {
+            error!("Failed to clone client stream: {}", e);
+            return;
+        }
+    };
+
+    // Client to Server Thread
+    let tls_stream_clone = Arc::clone(&tls_stream);
+    let client_to_server = spawn(move || {
+        debug!(">>> BEGIN");
+        let mut buffer = [0u8; 8192];
+        let mut client_reader = client_stream;
+        loop {
+            debug!(">");
+            let bytes_read = match client_reader.read(&mut buffer) {
+                Ok(0) => break,
+                Ok(n) => n,
+                Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
+                    sleep(Duration::from_millis(10));
+                    continue;
+                }
+                Err(error) => {
+                    debug!(">>> Error reading buffer {error}");
+                    break;
+                }
+            };
 
 
-fn forward_stream<R: Read, W: Write>(
-    from: &mut R,
-    to: &mut W,
-    configuration: Arc<ProxyConfiguration>,
-) {
-    let mut buffer = [0; 4096];
-    loop {
-        match from.read(&mut buffer) {
-            Ok(0) => break, // EOF
-            Ok(n) => {
-                if let Err(e) = to.write_all(&buffer[..n]) {
-                    eprintln!("{} proxy write error: {}", configuration.protocol, e);
+            let debug_str = String::from_utf8_lossy(&buffer[..bytes_read])
+                .replace('\n', "\\n")
+                .replace('\r', "\\r")
+                .replace('\t', "\\t");
+            debug!(">>> {}", debug_str);
+
+            // Lock the TLS stream and write the data to server
+            match tls_stream_clone.lock() {
+                Ok(mut tls_guard) => {
+                    if let Err(error) = tls_guard.write_all(&buffer[..bytes_read]) {
+                        debug!(">>> Error writing to server: {error}");
+                        break;
+                    }
+                    if let Err(error) = tls_guard.flush() {
+                        debug!(">>> Error flushing server connection: {error}");
+                        break;
+                    }
+                }
+                Err(error) => {
+                    debug!(">>> Error acquiring TLS stream lock: {error}");
                     break;
                 }
                     break;
                 }
-                if let Err(e) = to.flush() {
-                    eprintln!("{} proxy flush error: {}", configuration.protocol, e);
+            }
+        }
+    });
+
+    // Server to Client Thread
+    let tls_stream_clone = Arc::clone(&tls_stream);
+    let server_to_client = spawn(move || {
+        debug!("<<< BEGIN");
+        let mut buffer = [0u8; 8192];
+        let mut client_writer = client_stream_clone;
+        loop {
+            debug!("<");
+            // Lock the TLS stream and read from the server
+            let bytes_read = match tls_stream_clone.lock() {
+                Ok(mut tls_guard) => match tls_guard.read(&mut buffer) {
+                    Ok(0) => break, // TLS server closed
+                    Ok(n) => n,
+                    Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
+                        sleep(Duration::from_millis(10));
+                        continue;
+                    }
+                    Err(error) => {
+                        debug!("<<< Error reading buffer {error}");
+                        break;
+                    }
+                },
+                Err(error) => {
+                    debug!("<<< Error Cloning TLS {error}");
                     break;
                 }
                     break;
                 }
+            };
+
+            let debug_str = String::from_utf8_lossy(&buffer[..bytes_read])
+                .replace('\n', "\\n")
+                .replace('\r', "\\r")
+                .replace('\t', "\\t");
+            debug!("<<< {}", debug_str);
+
+            // Write decrypted data to client
+            if client_writer.write_all(&buffer[..bytes_read]).is_err() {
+                debug!("<<< ERR");
+                break;
             }
             }
-            Err(e) => {
-                eprintln!("{} proxy read error: {}", configuration.protocol, e);
+            if client_writer.flush().is_err() {
+                debug!("<<< ERR");
                 break;
             }
         }
                 break;
             }
         }
-    }
+    });
+
+    // Wait for both directions to finish
+    let _ = client_to_server.join();
+    let _ = server_to_client.join();
 }
 }