mirror of
https://github.com/spartanz51/tutabridge.git
synced 2026-06-24 10:54:32 +02:00
Restructure into Cargo workspace with Tauri desktop GUI (#1)
Split the bridge into a tutabridge-core crate, a Tauri v2 desktop app (src-tauri) and a React/TS UI (ui), keeping the CLI entrypoint at the workspace root. Add encrypted local storage (SQLCipher metadata index + encrypted .eml files) so mail persists across launches and only the delta is fetched. Wire the bridge to the Tuta Rust SDK via the tuta-repo submodule (batch loading, MailDetailsBlob reading, interactive 2FA login). Implement SMTP sending: build the draft and send it through Tuta's DraftService/SendDraftService, mirroring the web client (body in compressedBodyText, non-empty sender/recipient names, populated SendDraftParameters). Add unit tests for the draft/send payload building.
This commit is contained in:
+96
-15
@@ -1,12 +1,6 @@
|
||||
mod config;
|
||||
mod tuta;
|
||||
mod imap;
|
||||
mod mail;
|
||||
mod smtp;
|
||||
mod tls;
|
||||
|
||||
use std::sync::Arc;
|
||||
use log::info;
|
||||
use tutabridge_core::{config, store::LocalStore, sync, tls, tuta, imap, smtp};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
@@ -16,7 +10,29 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||
|
||||
let cfg = config::load_or_create_config().map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let mut cfg = match config::load_config().map_err(|e| anyhow::anyhow!("{e}"))? {
|
||||
Some(cfg) if !cfg.email.is_empty() => cfg,
|
||||
_ => {
|
||||
use std::io::{BufRead, Write};
|
||||
print!("Tuta email address: ");
|
||||
std::io::stdout().flush()?;
|
||||
let mut email = String::new();
|
||||
std::io::stdin().lock().read_line(&mut email)?;
|
||||
let email = email.trim().to_string();
|
||||
if email.is_empty() {
|
||||
anyhow::bail!("Email address is required");
|
||||
}
|
||||
let cfg = config::Config {
|
||||
email,
|
||||
..Default::default()
|
||||
};
|
||||
config::save_config(&cfg).map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
cfg
|
||||
}
|
||||
};
|
||||
|
||||
let bridge_password = config::ensure_bridge_password(&mut cfg)
|
||||
.map_err(|e| anyhow::anyhow!("Bridge password setup failed: {e}"))?;
|
||||
info!("TutaBridge starting...");
|
||||
|
||||
let tls_acceptor = tls::load_or_create_tls_acceptor()
|
||||
@@ -26,26 +42,91 @@ async fn main() -> anyhow::Result<()> {
|
||||
info!("IMAP will listen on 127.0.0.1:{}", cfg.imap_port);
|
||||
info!("SMTP will listen on 127.0.0.1:{}", cfg.smtp_port);
|
||||
|
||||
let session = tuta::login(&cfg).await.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
let session: Arc<dyn tuta::MailBackend> = Arc::new(session);
|
||||
let totp_cb = tuta::TwoFactorCallback::Totp(Box::new(|| {
|
||||
use std::io::{BufRead, Write};
|
||||
print!("TOTP code: ");
|
||||
std::io::stdout().flush()?;
|
||||
let mut code_str = String::new();
|
||||
std::io::stdin().lock().read_line(&mut code_str)?;
|
||||
let code: u32 = code_str
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|_| "Invalid TOTP code — must be a number")?;
|
||||
Ok(code)
|
||||
}));
|
||||
|
||||
// Try keyring session first, only prompt for password if needed
|
||||
let session = match tuta::login_with_2fa(&cfg, None, Some(totp_cb)).await {
|
||||
Ok(s) => s,
|
||||
Err(_) => {
|
||||
let password = rpassword::prompt_password(format!("Password for {}: ", cfg.email))?;
|
||||
let totp_cb2 = tuta::TwoFactorCallback::Totp(Box::new(|| {
|
||||
use std::io::{BufRead, Write};
|
||||
print!("TOTP code: ");
|
||||
std::io::stdout().flush()?;
|
||||
let mut code_str = String::new();
|
||||
std::io::stdin().lock().read_line(&mut code_str)?;
|
||||
let code: u32 = code_str
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|_| "Invalid TOTP code — must be a number")?;
|
||||
Ok(code)
|
||||
}));
|
||||
tuta::login_with_2fa(&cfg, Some(&password), Some(totp_cb2))
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?
|
||||
}
|
||||
};
|
||||
info!("Logged in as {}", cfg.email);
|
||||
|
||||
let imap_session = session.clone();
|
||||
let smtp_session = session.clone();
|
||||
let storage_key = session.derive_storage_key().await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
info!("Storage encryption key derived");
|
||||
|
||||
let local_store = LocalStore::open(
|
||||
&config::store_db_path(),
|
||||
&config::store_mails_dir(),
|
||||
storage_key,
|
||||
).map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
if !local_store.verify_key() {
|
||||
info!("Storage key changed — resetting local cache");
|
||||
let _ = local_store.reset();
|
||||
}
|
||||
let local_store = Arc::new(local_store);
|
||||
info!("Local store opened");
|
||||
|
||||
let backend: Arc<dyn tuta::MailBackend> = Arc::new(session);
|
||||
|
||||
let store = sync::MailStore::new();
|
||||
let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
|
||||
|
||||
let imap_tls = tls_acceptor.clone();
|
||||
let smtp_tls = tls_acceptor;
|
||||
|
||||
let imap_handle = tokio::spawn(imap::serve(cfg.imap_port, imap_session, imap_tls));
|
||||
let smtp_handle = tokio::spawn(smtp::serve(cfg.smtp_port, smtp_session, smtp_tls));
|
||||
let pw = cfg.bridge_password.clone();
|
||||
|
||||
let syncer_handle = tokio::spawn(sync::run_syncer(
|
||||
store.clone(), local_store, backend.clone(), cfg.sync_limit, shutdown_rx,
|
||||
));
|
||||
let imap_handle = tokio::spawn(imap::serve(
|
||||
cfg.imap_port, store.clone(), backend.clone(), imap_tls, pw.clone(),
|
||||
));
|
||||
let smtp_handle = tokio::spawn(smtp::serve(cfg.smtp_port, backend.clone(), smtp_tls, pw));
|
||||
|
||||
info!("Bridge is running. Configure Thunderbird with:");
|
||||
info!(" IMAP server: 127.0.0.1:{} (SSL/TLS)", cfg.imap_port);
|
||||
info!(" SMTP server: 127.0.0.1:{} (SSL/TLS)", cfg.smtp_port);
|
||||
info!(" Username: {}", cfg.email);
|
||||
info!(" Password: (any password — bridge handles auth)");
|
||||
info!(" Password: {}", bridge_password);
|
||||
info!(" Accept the self-signed certificate when prompted");
|
||||
|
||||
tokio::select! {
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
info!("Shutting down...");
|
||||
let _ = shutdown_tx.send(true);
|
||||
syncer_handle.abort();
|
||||
Ok(())
|
||||
}
|
||||
r = imap_handle => r.map_err(|e| anyhow::anyhow!("{e}"))?.map_err(|e| anyhow::anyhow!("{e}")),
|
||||
r = smtp_handle => r.map_err(|e| anyhow::anyhow!("{e}"))?.map_err(|e| anyhow::anyhow!("{e}")),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user