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"
|
||||
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]]
|
||||
name = "base16ct"
|
||||
version = "0.2.0"
|
||||
|
|
@ -587,6 +609,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
|
|
@ -676,6 +700,15 @@ dependencies = [
|
|||
"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]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
|
|
@ -1226,6 +1259,12 @@ dependencies = [
|
|||
"dtoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "eax"
|
||||
version = "0.5.0"
|
||||
|
|
@ -1553,6 +1592,12 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
|
|
@ -2188,6 +2233,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "js-sys"
|
||||
version = "0.3.85"
|
||||
|
|
@ -2939,6 +2994,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
|
|
@ -3362,11 +3427,14 @@ dependencies = [
|
|||
"pgp",
|
||||
"pwhash",
|
||||
"rand 0.8.5",
|
||||
"rcgen",
|
||||
"reqwest",
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"toml",
|
||||
"tracing",
|
||||
]
|
||||
|
|
@ -3601,6 +3669,19 @@ dependencies = [
|
|||
"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]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
|
|
@ -3702,6 +3783,20 @@ dependencies = [
|
|||
"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]]
|
||||
name = "ripemd"
|
||||
version = "0.1.3"
|
||||
|
|
@ -3788,6 +3883,21 @@ dependencies = [
|
|||
"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]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.14.0"
|
||||
|
|
@ -3797,6 +3907,18 @@ dependencies = [
|
|||
"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]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
|
|
@ -4237,9 +4359,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.4.1"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
|
|
@ -4500,6 +4622,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "toml"
|
||||
version = "1.0.2+spec-1.1.0"
|
||||
|
|
@ -4765,6 +4897,12 @@ dependencies = [
|
|||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
|
|
@ -5441,6 +5579,15 @@ version = "1.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
||||
[[package]]
|
||||
name = "yasna"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
|
||||
dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
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:
|
||||
the email list on top, the message preview on the bottom.
|
||||
|
||||
#### Safety features
|
||||
### Safety features
|
||||
- no html rendering (conversion to plain text)
|
||||
- no tracking pixels
|
||||
- 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)
|
||||
|
||||
---
|
||||
### Configuring
|
||||
# Configuring
|
||||
Sorry, there is no binary release for now. This setup requires that you have rust cargo installed.
|
||||
|
||||
Run the setup wizard:
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ pgp = { version = "0.15", default-features = false } # rpgp — OpenPGP decrypt
|
|||
chrono = "0.4"
|
||||
env_logger = "0.11"
|
||||
tracing = "0.1.44"
|
||||
rcgen = "0.13"
|
||||
tokio-rustls = "0.26"
|
||||
rustls = "0.23"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
keyring = { version = "3", features = ["apple-native"] }
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@
|
|||
/// Each accepted connection is handled in its own tokio task.
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tracing::{debug, error, info, warn};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
use tracing::{debug, error, warn};
|
||||
use crate::api::{ApiClient, LABEL_INBOX};
|
||||
use crate::{crypto, SharedState};
|
||||
|
||||
|
|
@ -18,12 +19,20 @@ use crate::{crypto, SharedState};
|
|||
pub async fn run_with_listener(
|
||||
state: SharedState,
|
||||
listener: TcpListener,
|
||||
acceptor: TlsAcceptor,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
loop {
|
||||
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);
|
||||
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}");
|
||||
}
|
||||
});
|
||||
|
|
@ -32,12 +41,15 @@ pub async fn run_with_listener(
|
|||
|
||||
// ── Per-connection handler ────────────────────────────────────────────────────
|
||||
|
||||
async fn handle_connection(
|
||||
socket: TcpStream,
|
||||
async fn handle_connection<S>(
|
||||
socket: S,
|
||||
state: SharedState,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (reader, mut writer) = socket.into_split();
|
||||
let mut lines = BufReader::new(reader).lines();
|
||||
) -> Result<(), Box<dyn std::error::Error>>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin + Send,
|
||||
{
|
||||
let (rd, mut writer) = tokio::io::split(socket);
|
||||
let mut lines = BufReader::new(rd).lines();
|
||||
let mut authenticated = false;
|
||||
|
||||
writer
|
||||
|
|
|
|||
|
|
@ -33,14 +33,15 @@ pub type SharedState = Arc<Mutex<BridgeState>>;
|
|||
|
||||
// ── 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.
|
||||
/// 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
|
||||
/// caller (tuimail's main thread) needs no Tokio runtime of its own.
|
||||
pub fn start(config: config::Config) -> Result<(), String> {
|
||||
let (ready_tx, ready_rx) = std::sync::mpsc::channel::<Result<(), String>>();
|
||||
pub fn start(config: config::Config) -> Result<Vec<u8>, String> {
|
||||
let (ready_tx, ready_rx) = std::sync::mpsc::channel::<Result<Vec<u8>, String>>();
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
|
|
@ -48,8 +49,8 @@ pub fn start(config: config::Config) -> Result<(), String> {
|
|||
.expect("tokio runtime");
|
||||
rt.block_on(async move {
|
||||
match run(config).await {
|
||||
Ok(()) => {
|
||||
let _ = ready_tx.send(Ok(()));
|
||||
Ok(cert_der) => {
|
||||
let _ = ready_tx.send(Ok(cert_der));
|
||||
// Keep the runtime alive so spawned server tasks keep running.
|
||||
std::future::pending::<()>().await;
|
||||
}
|
||||
|
|
@ -66,7 +67,7 @@ pub fn start(config: config::Config) -> Result<(), String> {
|
|||
|
||||
// ── 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 session = auth::authenticate(&client, &config.proton)
|
||||
.await
|
||||
|
|
@ -88,6 +89,32 @@ async fn run(config: config::Config) -> Result<(), String> {
|
|||
store.load_all(messages);
|
||||
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.
|
||||
let imap_listener = TcpListener::bind(("127.0.0.1", config.bridge.imap_port))
|
||||
.await
|
||||
|
|
@ -108,18 +135,19 @@ async fn run(config: config::Config) -> Result<(), String> {
|
|||
}));
|
||||
|
||||
let imap_state = Arc::clone(&state);
|
||||
let imap_acceptor = acceptor.clone();
|
||||
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}");
|
||||
}
|
||||
});
|
||||
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}");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(()) // ← ports are bound; start() unblocks here
|
||||
Ok(cert_der) // ← ports are bound; start() unblocks here
|
||||
}
|
||||
|
||||
// ── Key pool setup ────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ use crate::api::ApiClient;
|
|||
use crate::{SharedState, crypto};
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as B64;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
use tracing::error;
|
||||
|
||||
// ── Public entry point ────────────────────────────────────────────────────────
|
||||
|
|
@ -20,12 +21,20 @@ use tracing::error;
|
|||
pub async fn run_with_listener(
|
||||
state: SharedState,
|
||||
listener: TcpListener,
|
||||
acceptor: TlsAcceptor,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
loop {
|
||||
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);
|
||||
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}");
|
||||
}
|
||||
});
|
||||
|
|
@ -34,12 +43,15 @@ pub async fn run_with_listener(
|
|||
|
||||
// ── Per-connection handler ────────────────────────────────────────────────────
|
||||
|
||||
async fn handle_connection(
|
||||
socket: TcpStream,
|
||||
async fn handle_connection<S>(
|
||||
socket: S,
|
||||
state: SharedState,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (reader, mut writer) = socket.into_split();
|
||||
let mut lines = BufReader::new(reader).lines();
|
||||
) -> Result<(), Box<dyn std::error::Error>>
|
||||
where
|
||||
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 mail_from = String::new();
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ pub struct SmtpConfig {
|
|||
pub password: Option<String>,
|
||||
pub tls_mode: TlsMode,
|
||||
pub from: String,
|
||||
#[serde(skip)]
|
||||
pub tls_cert_der: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
|
|
@ -56,6 +58,8 @@ pub struct ImapConfig {
|
|||
#[serde(default)]
|
||||
pub password: Option<String>,
|
||||
pub use_tls: bool,
|
||||
#[serde(skip)]
|
||||
pub tls_cert_der: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
/// Mirror of `proton_bridge::config::ProtonConfig` — used for config parsing
|
||||
|
|
@ -150,7 +154,8 @@ impl Config {
|
|||
port: b.imap_port,
|
||||
username: "bridge".into(),
|
||||
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,
|
||||
username: "bridge".into(),
|
||||
password: b.local_password.clone(),
|
||||
tls_mode: TlsMode::None,
|
||||
tls_mode: TlsMode::Smtps,
|
||||
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())?;
|
||||
|
||||
if imap_cfg.use_tls {
|
||||
let tls = native_tls::TlsConnector::builder()
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut builder = native_tls::TlsConnector::builder();
|
||||
if let Some(ref der) = imap_cfg.tls_cert_der {
|
||||
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)
|
||||
.map_err(|e| e.to_string())?;
|
||||
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);
|
||||
});
|
||||
|
||||
// ── 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) ──
|
||||
if config.provider == Provider::Proton {
|
||||
let bridge_cfg = config.as_bridge_config().unwrap_or_else(|e| {
|
||||
|
|
@ -53,22 +63,15 @@ fn main() -> io::Result<()> {
|
|||
exit(1);
|
||||
});
|
||||
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}");
|
||||
exit(1);
|
||||
});
|
||||
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 {
|
||||
imap: Some(imap_cfg),
|
||||
smtp: Some(smtp_cfg),
|
||||
|
|
|
|||
|
|
@ -229,6 +229,7 @@ fn setup_imap(existing: Option<&Config>) -> Result<Config, String> {
|
|||
username: imap_user,
|
||||
password: None, // stored in keychain
|
||||
use_tls: imap_tls,
|
||||
tls_cert_der: None,
|
||||
}),
|
||||
smtp: Some(SmtpConfig {
|
||||
host: smtp_host,
|
||||
|
|
@ -237,6 +238,7 @@ fn setup_imap(existing: Option<&Config>) -> Result<Config, String> {
|
|||
password: None, // stored in keychain
|
||||
tls_mode: smtp_tls,
|
||||
from: smtp_from,
|
||||
tls_cert_der: None,
|
||||
}),
|
||||
proton: None,
|
||||
bridge: None,
|
||||
|
|
|
|||
12
src/smtp.rs
12
src/smtp.rs
|
|
@ -2,7 +2,7 @@ use std::sync::mpsc;
|
|||
use std::thread;
|
||||
use std::time::Duration;
|
||||
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 crate::config::{SmtpConfig, TlsMode};
|
||||
|
||||
|
|
@ -39,7 +39,15 @@ pub(crate) fn send_email(
|
|||
}
|
||||
TlsMode::Smtps => {
|
||||
// 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)
|
||||
.map_err(|e| e.to_string())?
|
||||
.port(cfg.port)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue