secured bridge communication
This commit is contained in:
parent
9375b5b96f
commit
5a3ac2eed9
11 changed files with 274 additions and 49 deletions
151
Cargo.lock
generated
151
Cargo.lock
generated
|
|
@ -336,6 +336,28 @@ version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aws-lc-rs"
|
||||||
|
version = "1.16.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9"
|
||||||
|
dependencies = [
|
||||||
|
"aws-lc-sys",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aws-lc-sys"
|
||||||
|
version = "0.37.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cmake",
|
||||||
|
"dunce",
|
||||||
|
"fs_extra",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base16ct"
|
name = "base16ct"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -587,6 +609,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
|
"jobserver",
|
||||||
|
"libc",
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -676,6 +700,15 @@ dependencies = [
|
||||||
"digest 0.10.7",
|
"digest 0.10.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cmake"
|
||||||
|
version = "0.1.57"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
|
@ -1226,6 +1259,12 @@ dependencies = [
|
||||||
"dtoa",
|
"dtoa",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dunce"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "eax"
|
name = "eax"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
|
@ -1553,6 +1592,12 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fs_extra"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
|
|
@ -2188,6 +2233,16 @@ dependencies = [
|
||||||
"syn 2.0.116",
|
"syn 2.0.116",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jobserver"
|
||||||
|
version = "0.1.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.85"
|
version = "0.3.85"
|
||||||
|
|
@ -2939,6 +2994,16 @@ dependencies = [
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pem"
|
||||||
|
version = "3.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pem-rfc7468"
|
name = "pem-rfc7468"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
|
@ -3362,11 +3427,14 @@ dependencies = [
|
||||||
"pgp",
|
"pgp",
|
||||||
"pwhash",
|
"pwhash",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
"rcgen",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
"rustls",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
"toml",
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
@ -3601,6 +3669,19 @@ dependencies = [
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rcgen"
|
||||||
|
version = "0.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
|
||||||
|
dependencies = [
|
||||||
|
"pem",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"time",
|
||||||
|
"yasna",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
|
|
@ -3702,6 +3783,20 @@ dependencies = [
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.17.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"libc",
|
||||||
|
"untrusted",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ripemd"
|
name = "ripemd"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
|
|
@ -3788,6 +3883,21 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.23.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
||||||
|
dependencies = [
|
||||||
|
"aws-lc-rs",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"rustls-webpki",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pki-types"
|
name = "rustls-pki-types"
|
||||||
version = "1.14.0"
|
version = "1.14.0"
|
||||||
|
|
@ -3797,6 +3907,18 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.103.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
|
||||||
|
dependencies = [
|
||||||
|
"aws-lc-rs",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
|
|
@ -4237,9 +4359,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.4.1"
|
version = "2.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
|
|
@ -4500,6 +4622,16 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-rustls"
|
||||||
|
version = "0.26.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||||
|
dependencies = [
|
||||||
|
"rustls",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "1.0.2+spec-1.1.0"
|
version = "1.0.2+spec-1.1.0"
|
||||||
|
|
@ -4765,6 +4897,12 @@ dependencies = [
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
|
|
@ -5441,6 +5579,15 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yasna"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
|
||||||
|
dependencies = [
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
# tuimail — User Guide
|
# Tuimail
|
||||||
|
|
||||||
Tuimail is a terminal email client. It shows your inbox in a split-pane view:
|
Tuimail is a terminal email client. It shows your inbox in a split-pane view:
|
||||||
the email list on top, the message preview on the bottom.
|
the email list on top, the message preview on the bottom.
|
||||||
|
|
||||||
#### Safety features
|
### Safety features
|
||||||
- no html rendering (conversion to plain text)
|
- no html rendering (conversion to plain text)
|
||||||
- no tracking pixels
|
- no tracking pixels
|
||||||
- no javascript
|
- no javascript
|
||||||
|
|
@ -12,7 +12,7 @@ the email list on top, the message preview on the bottom.
|
||||||
- safe communication to remote hosts (standard TLS, PGP for Proton)
|
- safe communication to remote hosts (standard TLS, PGP for Proton)
|
||||||
|
|
||||||
---
|
---
|
||||||
### Configuring
|
# Configuring
|
||||||
Sorry, there is no binary release for now. This setup requires that you have rust cargo installed.
|
Sorry, there is no binary release for now. This setup requires that you have rust cargo installed.
|
||||||
|
|
||||||
Run the setup wizard:
|
Run the setup wizard:
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@ pgp = { version = "0.15", default-features = false } # rpgp — OpenPGP decrypt
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
|
rcgen = "0.13"
|
||||||
|
tokio-rustls = "0.26"
|
||||||
|
rustls = "0.23"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
keyring = { version = "3", features = ["apple-native"] }
|
keyring = { version = "3", features = ["apple-native"] }
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@
|
||||||
/// Each accepted connection is handled in its own tokio task.
|
/// Each accepted connection is handled in its own tokio task.
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
use tokio::net::TcpListener;
|
||||||
use tracing::{debug, error, info, warn};
|
use tokio_rustls::TlsAcceptor;
|
||||||
|
use tracing::{debug, error, warn};
|
||||||
use crate::api::{ApiClient, LABEL_INBOX};
|
use crate::api::{ApiClient, LABEL_INBOX};
|
||||||
use crate::{crypto, SharedState};
|
use crate::{crypto, SharedState};
|
||||||
|
|
||||||
|
|
@ -18,12 +19,20 @@ use crate::{crypto, SharedState};
|
||||||
pub async fn run_with_listener(
|
pub async fn run_with_listener(
|
||||||
state: SharedState,
|
state: SharedState,
|
||||||
listener: TcpListener,
|
listener: TcpListener,
|
||||||
|
acceptor: TlsAcceptor,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
loop {
|
loop {
|
||||||
let (socket, _addr) = listener.accept().await?;
|
let (socket, _addr) = listener.accept().await?;
|
||||||
|
let tls_stream = match acceptor.accept(socket).await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
error!("IMAP TLS handshake failed: {e}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
let state = Arc::clone(&state);
|
let state = Arc::clone(&state);
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = handle_connection(socket, state).await {
|
if let Err(e) = handle_connection(tls_stream, state).await {
|
||||||
error!("IMAP connection error: {e}");
|
error!("IMAP connection error: {e}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -32,12 +41,15 @@ pub async fn run_with_listener(
|
||||||
|
|
||||||
// ── Per-connection handler ────────────────────────────────────────────────────
|
// ── Per-connection handler ────────────────────────────────────────────────────
|
||||||
|
|
||||||
async fn handle_connection(
|
async fn handle_connection<S>(
|
||||||
socket: TcpStream,
|
socket: S,
|
||||||
state: SharedState,
|
state: SharedState,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>>
|
||||||
let (reader, mut writer) = socket.into_split();
|
where
|
||||||
let mut lines = BufReader::new(reader).lines();
|
S: AsyncRead + AsyncWrite + Unpin + Send,
|
||||||
|
{
|
||||||
|
let (rd, mut writer) = tokio::io::split(socket);
|
||||||
|
let mut lines = BufReader::new(rd).lines();
|
||||||
let mut authenticated = false;
|
let mut authenticated = false;
|
||||||
|
|
||||||
writer
|
writer
|
||||||
|
|
|
||||||
|
|
@ -33,14 +33,15 @@ pub type SharedState = Arc<Mutex<BridgeState>>;
|
||||||
|
|
||||||
// ── Public entry point ────────────────────────────────────────────────────────
|
// ── Public entry point ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Authenticate with ProtonMail, bind IMAP and SMTP ports, then return `Ok(())`.
|
/// Authenticate with ProtonMail, bind IMAP and SMTP ports, then return the
|
||||||
|
/// self-signed TLS certificate (DER bytes) used for local bridge communication.
|
||||||
/// The servers keep running in background Tokio tasks on a dedicated OS thread.
|
/// The servers keep running in background Tokio tasks on a dedicated OS thread.
|
||||||
/// Returns `Err` if authentication or port binding fails.
|
/// Returns `Err` if authentication, cert generation, or port binding fails.
|
||||||
///
|
///
|
||||||
/// This is the integration entry point for tuimail. It is synchronous so the
|
/// This is the integration entry point for tuimail. It is synchronous so the
|
||||||
/// caller (tuimail's main thread) needs no Tokio runtime of its own.
|
/// caller (tuimail's main thread) needs no Tokio runtime of its own.
|
||||||
pub fn start(config: config::Config) -> Result<(), String> {
|
pub fn start(config: config::Config) -> Result<Vec<u8>, String> {
|
||||||
let (ready_tx, ready_rx) = std::sync::mpsc::channel::<Result<(), String>>();
|
let (ready_tx, ready_rx) = std::sync::mpsc::channel::<Result<Vec<u8>, String>>();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
|
|
@ -48,8 +49,8 @@ pub fn start(config: config::Config) -> Result<(), String> {
|
||||||
.expect("tokio runtime");
|
.expect("tokio runtime");
|
||||||
rt.block_on(async move {
|
rt.block_on(async move {
|
||||||
match run(config).await {
|
match run(config).await {
|
||||||
Ok(()) => {
|
Ok(cert_der) => {
|
||||||
let _ = ready_tx.send(Ok(()));
|
let _ = ready_tx.send(Ok(cert_der));
|
||||||
// Keep the runtime alive so spawned server tasks keep running.
|
// Keep the runtime alive so spawned server tasks keep running.
|
||||||
std::future::pending::<()>().await;
|
std::future::pending::<()>().await;
|
||||||
}
|
}
|
||||||
|
|
@ -66,7 +67,7 @@ pub fn start(config: config::Config) -> Result<(), String> {
|
||||||
|
|
||||||
// ── Async startup ─────────────────────────────────────────────────────────────
|
// ── Async startup ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async fn run(config: config::Config) -> Result<(), String> {
|
async fn run(config: config::Config) -> Result<Vec<u8>, String> {
|
||||||
let client = auth::build_client().map_err(|e| format!("HTTP client: {e}"))?;
|
let client = auth::build_client().map_err(|e| format!("HTTP client: {e}"))?;
|
||||||
let session = auth::authenticate(&client, &config.proton)
|
let session = auth::authenticate(&client, &config.proton)
|
||||||
.await
|
.await
|
||||||
|
|
@ -88,6 +89,32 @@ async fn run(config: config::Config) -> Result<(), String> {
|
||||||
store.load_all(messages);
|
store.load_all(messages);
|
||||||
info!("Inbox: {} messages ({total} total)", store.count());
|
info!("Inbox: {} messages ({total} total)", store.count());
|
||||||
|
|
||||||
|
// Generate a self-signed TLS cert for local bridge communication.
|
||||||
|
// Use an IP SAN for 127.0.0.1 so TLS hostname validation works when
|
||||||
|
// connecting by IP address (which is what native_tls/lettre require).
|
||||||
|
let key_pair = rcgen::KeyPair::generate()
|
||||||
|
.map_err(|e| format!("key gen: {e}"))?;
|
||||||
|
let mut params = rcgen::CertificateParams::new(Vec::<String>::new())
|
||||||
|
.map_err(|e| format!("cert params: {e}"))?;
|
||||||
|
params.subject_alt_names.push(rcgen::SanType::IpAddress(
|
||||||
|
std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
|
||||||
|
));
|
||||||
|
let cert = params
|
||||||
|
.self_signed(&key_pair)
|
||||||
|
.map_err(|e| format!("cert gen: {e}"))?;
|
||||||
|
let cert_der: Vec<u8> = cert.der().to_vec();
|
||||||
|
let key_der: Vec<u8> = key_pair.serialize_der();
|
||||||
|
|
||||||
|
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
|
||||||
|
let server_cfg = rustls::ServerConfig::builder()
|
||||||
|
.with_no_client_auth()
|
||||||
|
.with_single_cert(
|
||||||
|
vec![CertificateDer::from(cert_der.clone())],
|
||||||
|
PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_der)),
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("rustls config: {e}"))?;
|
||||||
|
let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(server_cfg));
|
||||||
|
|
||||||
// Bind ports before spawning tasks — ports are occupied when start() returns.
|
// Bind ports before spawning tasks — ports are occupied when start() returns.
|
||||||
let imap_listener = TcpListener::bind(("127.0.0.1", config.bridge.imap_port))
|
let imap_listener = TcpListener::bind(("127.0.0.1", config.bridge.imap_port))
|
||||||
.await
|
.await
|
||||||
|
|
@ -108,18 +135,19 @@ async fn run(config: config::Config) -> Result<(), String> {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let imap_state = Arc::clone(&state);
|
let imap_state = Arc::clone(&state);
|
||||||
|
let imap_acceptor = acceptor.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = imap_server::run_with_listener(imap_state, imap_listener).await {
|
if let Err(e) = imap_server::run_with_listener(imap_state, imap_listener, imap_acceptor).await {
|
||||||
eprintln!("IMAP server error: {e}");
|
eprintln!("IMAP server error: {e}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = smtp_server::run_with_listener(state, smtp_listener).await {
|
if let Err(e) = smtp_server::run_with_listener(state, smtp_listener, acceptor).await {
|
||||||
error!("SMTP server error: {e}");
|
error!("SMTP server error: {e}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(()) // ← ports are bound; start() unblocks here
|
Ok(cert_der) // ← ports are bound; start() unblocks here
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Key pool setup ────────────────────────────────────────────────────────────
|
// ── Key pool setup ────────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,9 @@ use crate::api::ApiClient;
|
||||||
use crate::{SharedState, crypto};
|
use crate::{SharedState, crypto};
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use base64::engine::general_purpose::STANDARD as B64;
|
use base64::engine::general_purpose::STANDARD as B64;
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
use tokio::net::TcpListener;
|
||||||
|
use tokio_rustls::TlsAcceptor;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
// ── Public entry point ────────────────────────────────────────────────────────
|
// ── Public entry point ────────────────────────────────────────────────────────
|
||||||
|
|
@ -20,12 +21,20 @@ use tracing::error;
|
||||||
pub async fn run_with_listener(
|
pub async fn run_with_listener(
|
||||||
state: SharedState,
|
state: SharedState,
|
||||||
listener: TcpListener,
|
listener: TcpListener,
|
||||||
|
acceptor: TlsAcceptor,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
loop {
|
loop {
|
||||||
let (socket, _addr) = listener.accept().await?;
|
let (socket, _addr) = listener.accept().await?;
|
||||||
|
let tls_stream = match acceptor.accept(socket).await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
error!("SMTP TLS handshake failed: {e}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
let state = Arc::clone(&state);
|
let state = Arc::clone(&state);
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = handle_connection(socket, state).await {
|
if let Err(e) = handle_connection(tls_stream, state).await {
|
||||||
error!("SMTP connection error: {e}");
|
error!("SMTP connection error: {e}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -34,12 +43,15 @@ pub async fn run_with_listener(
|
||||||
|
|
||||||
// ── Per-connection handler ────────────────────────────────────────────────────
|
// ── Per-connection handler ────────────────────────────────────────────────────
|
||||||
|
|
||||||
async fn handle_connection(
|
async fn handle_connection<S>(
|
||||||
socket: TcpStream,
|
socket: S,
|
||||||
state: SharedState,
|
state: SharedState,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>>
|
||||||
let (reader, mut writer) = socket.into_split();
|
where
|
||||||
let mut lines = BufReader::new(reader).lines();
|
S: AsyncRead + AsyncWrite + Unpin + Send,
|
||||||
|
{
|
||||||
|
let (rd, mut writer) = tokio::io::split(socket);
|
||||||
|
let mut lines = BufReader::new(rd).lines();
|
||||||
|
|
||||||
let mut authenticated = false;
|
let mut authenticated = false;
|
||||||
let mut mail_from = String::new();
|
let mut mail_from = String::new();
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,8 @@ pub struct SmtpConfig {
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
pub tls_mode: TlsMode,
|
pub tls_mode: TlsMode,
|
||||||
pub from: String,
|
pub from: String,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub tls_cert_der: Option<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
|
@ -56,6 +58,8 @@ pub struct ImapConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub password: Option<String>,
|
pub password: Option<String>,
|
||||||
pub use_tls: bool,
|
pub use_tls: bool,
|
||||||
|
#[serde(skip)]
|
||||||
|
pub tls_cert_der: Option<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mirror of `proton_bridge::config::ProtonConfig` — used for config parsing
|
/// Mirror of `proton_bridge::config::ProtonConfig` — used for config parsing
|
||||||
|
|
@ -150,7 +154,8 @@ impl Config {
|
||||||
port: b.imap_port,
|
port: b.imap_port,
|
||||||
username: "bridge".into(),
|
username: "bridge".into(),
|
||||||
password: b.local_password.clone(),
|
password: b.local_password.clone(),
|
||||||
use_tls: false,
|
use_tls: true,
|
||||||
|
tls_cert_der: None, // injected by main.rs after start()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -178,8 +183,9 @@ impl Config {
|
||||||
port: b.smtp_port,
|
port: b.smtp_port,
|
||||||
username: "bridge".into(),
|
username: "bridge".into(),
|
||||||
password: b.local_password.clone(),
|
password: b.local_password.clone(),
|
||||||
tls_mode: TlsMode::None,
|
tls_mode: TlsMode::Smtps,
|
||||||
from,
|
from,
|
||||||
|
tls_cert_der: None, // injected by main.rs after start()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,13 @@ pub(crate) fn connect(config: &Config) -> Result<ImapSession, String> {
|
||||||
tcp.set_write_timeout(Some(IO_TIMEOUT)).map_err(|e| e.to_string())?;
|
tcp.set_write_timeout(Some(IO_TIMEOUT)).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
if imap_cfg.use_tls {
|
if imap_cfg.use_tls {
|
||||||
let tls = native_tls::TlsConnector::builder()
|
let mut builder = native_tls::TlsConnector::builder();
|
||||||
.build()
|
if let Some(ref der) = imap_cfg.tls_cert_der {
|
||||||
.map_err(|e| e.to_string())?;
|
let cert = native_tls::Certificate::from_der(der)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
builder.add_root_certificate(cert);
|
||||||
|
}
|
||||||
|
let tls = builder.build().map_err(|e| e.to_string())?;
|
||||||
let tls_stream = tls.connect(&imap_cfg.host, tcp)
|
let tls_stream = tls.connect(&imap_cfg.host, tcp)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let password = imap_cfg.password.as_deref().unwrap_or("");
|
let password = imap_cfg.password.as_deref().unwrap_or("");
|
||||||
|
|
|
||||||
23
src/main.rs
23
src/main.rs
|
|
@ -46,6 +46,16 @@ fn main() -> io::Result<()> {
|
||||||
exit(1);
|
exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Derive effective IMAP/SMTP configs for the chosen provider ──
|
||||||
|
let mut imap_cfg = config.effective_imap().unwrap_or_else(|e| {
|
||||||
|
eprintln!("{e}");
|
||||||
|
exit(1);
|
||||||
|
});
|
||||||
|
let mut smtp_cfg = config.effective_smtp().unwrap_or_else(|e| {
|
||||||
|
eprintln!("{e}");
|
||||||
|
exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
// ── Start bridge if needed (before entering raw mode so output is clean) ──
|
// ── Start bridge if needed (before entering raw mode so output is clean) ──
|
||||||
if config.provider == Provider::Proton {
|
if config.provider == Provider::Proton {
|
||||||
let bridge_cfg = config.as_bridge_config().unwrap_or_else(|e| {
|
let bridge_cfg = config.as_bridge_config().unwrap_or_else(|e| {
|
||||||
|
|
@ -53,22 +63,15 @@ fn main() -> io::Result<()> {
|
||||||
exit(1);
|
exit(1);
|
||||||
});
|
});
|
||||||
eprint!("Starting ProtonMail bridge...");
|
eprint!("Starting ProtonMail bridge...");
|
||||||
proton_bridge::start(bridge_cfg).unwrap_or_else(|e| {
|
let cert_der = proton_bridge::start(bridge_cfg).unwrap_or_else(|e| {
|
||||||
eprintln!("\nBridge failed to start: {e}");
|
eprintln!("\nBridge failed to start: {e}");
|
||||||
exit(1);
|
exit(1);
|
||||||
});
|
});
|
||||||
eprintln!(" ready.");
|
eprintln!(" ready.");
|
||||||
|
imap_cfg.tls_cert_der = Some(cert_der.clone());
|
||||||
|
smtp_cfg.tls_cert_der = Some(cert_der);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Derive effective IMAP/SMTP configs for the chosen provider ──
|
|
||||||
let imap_cfg = config.effective_imap().unwrap_or_else(|e| {
|
|
||||||
eprintln!("{e}");
|
|
||||||
exit(1);
|
|
||||||
});
|
|
||||||
let smtp_cfg = config.effective_smtp().unwrap_or_else(|e| {
|
|
||||||
eprintln!("{e}");
|
|
||||||
exit(1);
|
|
||||||
});
|
|
||||||
let runtime_config = Config {
|
let runtime_config = Config {
|
||||||
imap: Some(imap_cfg),
|
imap: Some(imap_cfg),
|
||||||
smtp: Some(smtp_cfg),
|
smtp: Some(smtp_cfg),
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,7 @@ fn setup_imap(existing: Option<&Config>) -> Result<Config, String> {
|
||||||
username: imap_user,
|
username: imap_user,
|
||||||
password: None, // stored in keychain
|
password: None, // stored in keychain
|
||||||
use_tls: imap_tls,
|
use_tls: imap_tls,
|
||||||
|
tls_cert_der: None,
|
||||||
}),
|
}),
|
||||||
smtp: Some(SmtpConfig {
|
smtp: Some(SmtpConfig {
|
||||||
host: smtp_host,
|
host: smtp_host,
|
||||||
|
|
@ -237,6 +238,7 @@ fn setup_imap(existing: Option<&Config>) -> Result<Config, String> {
|
||||||
password: None, // stored in keychain
|
password: None, // stored in keychain
|
||||||
tls_mode: smtp_tls,
|
tls_mode: smtp_tls,
|
||||||
from: smtp_from,
|
from: smtp_from,
|
||||||
|
tls_cert_der: None,
|
||||||
}),
|
}),
|
||||||
proton: None,
|
proton: None,
|
||||||
bridge: None,
|
bridge: None,
|
||||||
|
|
|
||||||
12
src/smtp.rs
12
src/smtp.rs
|
|
@ -2,7 +2,7 @@ use std::sync::mpsc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use lettre::transport::smtp::authentication::Credentials;
|
use lettre::transport::smtp::authentication::Credentials;
|
||||||
use lettre::transport::smtp::client::{Tls, TlsParameters};
|
use lettre::transport::smtp::client::{Certificate, Tls, TlsParameters};
|
||||||
use lettre::{Message, SmtpTransport, Transport};
|
use lettre::{Message, SmtpTransport, Transport};
|
||||||
use crate::config::{SmtpConfig, TlsMode};
|
use crate::config::{SmtpConfig, TlsMode};
|
||||||
|
|
||||||
|
|
@ -39,7 +39,15 @@ pub(crate) fn send_email(
|
||||||
}
|
}
|
||||||
TlsMode::Smtps => {
|
TlsMode::Smtps => {
|
||||||
// SMTPS: TLS from the first byte (port 465)
|
// SMTPS: TLS from the first byte (port 465)
|
||||||
let tls = TlsParameters::new(cfg.host.clone()).map_err(|e| e.to_string())?;
|
let tls = if let Some(ref der) = cfg.tls_cert_der {
|
||||||
|
let cert = Certificate::from_der(der.clone()).map_err(|e| e.to_string())?;
|
||||||
|
TlsParameters::builder(cfg.host.clone())
|
||||||
|
.add_root_certificate(cert)
|
||||||
|
.build_native()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
} else {
|
||||||
|
TlsParameters::new(cfg.host.clone()).map_err(|e| e.to_string())?
|
||||||
|
};
|
||||||
SmtpTransport::relay(&cfg.host)
|
SmtpTransport::relay(&cfg.host)
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
.port(cfg.port)
|
.port(cfg.port)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue