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:
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "tutabridge-gui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.84.0"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tutabridge-core = { path = "../crates/bridge" }
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-shell = "2"
|
||||
tokio = { version = "1.43", features = ["full"] }
|
||||
tokio-rustls = { version = "0.26", features = ["ring"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/niceda/tauri/dev/crates/tauri-utils/schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default capabilities for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"shell:allow-open",
|
||||
"core:event:default",
|
||||
"core:event:allow-listen",
|
||||
"core:event:allow-emit"
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"default":{"identifier":"default","description":"Default capabilities for the main window","local":true,"windows":["main"],"permissions":["core:default","shell:allow-open","core:event:default","core:event:allow-listen","core:event:allow-emit"]}}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 360 B |
Binary file not shown.
|
After Width: | Height: | Size: 856 B |
Binary file not shown.
|
After Width: | Height: | Size: 104 B |
@@ -0,0 +1,80 @@
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
use tokio::sync::Mutex;
|
||||
use tutabridge_core::bridge::{BridgeHandle, BridgeStats, BridgeStatus};
|
||||
use tutabridge_core::config::{self, Config};
|
||||
use tutabridge_core::tuta;
|
||||
|
||||
pub type BridgeState = Arc<Mutex<BridgeHandle>>;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_config() -> Result<Config, String> {
|
||||
match config::load_config() {
|
||||
Ok(Some(cfg)) => Ok(cfg),
|
||||
Ok(None) => Ok(Config::default()),
|
||||
Err(e) => Err(format!("Failed to load config: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn save_config(config: Config) -> Result<(), String> {
|
||||
config::save_config(&config).map_err(|e| format!("Failed to save config: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn has_saved_session() -> Result<bool, String> {
|
||||
let cfg = match config::load_config() {
|
||||
Ok(Some(cfg)) if !cfg.email.is_empty() => cfg,
|
||||
_ => return Ok(false),
|
||||
};
|
||||
Ok(tuta::has_saved_session(&cfg.email))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn start_bridge(
|
||||
password: Option<String>,
|
||||
state: State<'_, BridgeState>,
|
||||
) -> Result<(), String> {
|
||||
let mut cfg = match config::load_config() {
|
||||
Ok(Some(cfg)) if !cfg.email.is_empty() => cfg,
|
||||
_ => return Err("No config found — save config first".into()),
|
||||
};
|
||||
|
||||
config::ensure_bridge_password(&mut cfg).map_err(|e| format!("Bridge password setup failed: {e}"))?;
|
||||
|
||||
let mut handle = state.lock().await;
|
||||
handle.start(cfg, password, None).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn stop_bridge(state: State<'_, BridgeState>) -> Result<(), String> {
|
||||
let mut handle = state.lock().await;
|
||||
handle.stop().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_status(state: State<'_, BridgeState>) -> Result<BridgeStatus, String> {
|
||||
let handle = state.lock().await;
|
||||
Ok(handle.status().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_stats(state: State<'_, BridgeState>) -> Result<BridgeStats, String> {
|
||||
let handle = state.lock().await;
|
||||
Ok(handle.stats().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_bridge_password() -> Result<Option<String>, String> {
|
||||
let cfg = config::load_config().map_err(|e| e.to_string())?;
|
||||
Ok(cfg.and_then(|c| c.bridge_password))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn regenerate_bridge_password() -> Result<String, String> {
|
||||
let mut cfg = config::load_config()
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or("No config found")?;
|
||||
config::regenerate_bridge_password(&mut cfg).map_err(|e| e.to_string())
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
mod commands;
|
||||
|
||||
use std::sync::Arc;
|
||||
use commands::BridgeState;
|
||||
use tauri::Manager;
|
||||
use tokio::sync::Mutex;
|
||||
use tutabridge_core::bridge::BridgeHandle;
|
||||
|
||||
fn main() {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug"))
|
||||
.init();
|
||||
|
||||
tokio_rustls::rustls::crypto::ring::default_provider()
|
||||
.install_default()
|
||||
.expect("Failed to install TLS crypto provider");
|
||||
|
||||
let handle = BridgeHandle::new();
|
||||
let log_rx = handle.subscribe_logs();
|
||||
let shared = Arc::new(Mutex::new(handle));
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.manage(shared as BridgeState)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::get_config,
|
||||
commands::save_config,
|
||||
commands::has_saved_session,
|
||||
commands::start_bridge,
|
||||
commands::stop_bridge,
|
||||
commands::get_status,
|
||||
commands::get_stats,
|
||||
commands::get_bridge_password,
|
||||
commands::regenerate_bridge_password,
|
||||
])
|
||||
.setup(|app| {
|
||||
let app_handle = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
stream_logs(app_handle, log_rx).await;
|
||||
});
|
||||
|
||||
let state = app.state::<BridgeState>().inner().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
auto_start(state).await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error running TutaBridge");
|
||||
}
|
||||
|
||||
async fn auto_start(state: Arc<Mutex<BridgeHandle>>) {
|
||||
use tutabridge_core::config;
|
||||
use tutabridge_core::tuta;
|
||||
|
||||
let mut cfg = match config::load_config() {
|
||||
Ok(Some(cfg)) if !cfg.email.is_empty() => cfg,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
// Ensure bridge password exists (so the UI can display it)
|
||||
if let Err(e) = config::ensure_bridge_password(&mut cfg) {
|
||||
log::warn!("Bridge password setup failed: {e}");
|
||||
}
|
||||
|
||||
if !tuta::has_saved_session(&cfg.email) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut handle = state.lock().await;
|
||||
if let Err(e) = handle.start(cfg, None, None).await {
|
||||
log::warn!("Auto-start failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn stream_logs(
|
||||
app: tauri::AppHandle,
|
||||
mut rx: tokio::sync::broadcast::Receiver<String>,
|
||||
) {
|
||||
use tauri::Emitter;
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(line) => {
|
||||
let _ = app.emit("bridge://log", &line);
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
|
||||
let _ = app.emit("bridge://log", &format!("... skipped {n} log lines"));
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/niceda/tauri/dev/crates/tauri-config-schema/schema.json",
|
||||
"productName": "TutaBridge",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.tutabridge.app",
|
||||
"build": {
|
||||
"frontendDist": "../ui/dist",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeDevCommand": "cd ../ui && npm run dev",
|
||||
"beforeBuildCommand": "cd ../ui && npm run build"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "TutaBridge",
|
||||
"width": 700,
|
||||
"height": 520,
|
||||
"resizable": true,
|
||||
"center": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user