From b8cce5debace87397a7791d4735f1ff0832229d4 Mon Sep 17 00:00:00 2001 From: Benedikt Terhechte Date: Thu, 21 Oct 2021 15:09:57 +0200 Subject: [PATCH] Large Scale UI Integration - Split UI up into multiple states - Changes throughout the codebase to accomodate that - All still messy but opening a folder and creating a database works now. --- Cargo.lock | 41 +++++ Cargo.toml | 1 + README.md | 8 + src/.DS_Store | Bin 0 -> 6148 bytes src/database/db.rs | 7 + src/database/sql.rs | 26 ++- src/gui/.DS_Store | Bin 0 -> 8196 bytes src/gui/app.rs | 52 +++--- src/gui/app_state/error.rs | 43 +++++ src/gui/app_state/import.rs | 165 +++++++++++++++-- src/gui/app_state/main.rs | 134 ++++++++++++++ src/gui/app_state/mod.rs | 166 ++++++++++++++++-- src/gui/app_state/startup.rs | 124 +++++++++++-- src/gui/app_state/visualize.rs | 115 ------------ src/importer/formats/apple_mail/filesystem.rs | 5 +- src/importer/formats/apple_mail/mod.rs | 8 +- src/importer/formats/gmailbackup/mod.rs | 2 +- src/importer/formats/mbox/mod.rs | 2 +- src/importer/formats/mod.rs | 4 +- src/importer/formats/shared/database.rs | 6 +- src/importer/formats/shared/parse.rs | 9 +- src/importer/importer.rs | 30 +++- src/importer/message_adapter.rs | 31 +++- src/importer/mod.rs | 3 + src/lib.rs | 5 +- src/model/link.rs | 1 + src/types/config.rs | 39 +++- 27 files changed, 803 insertions(+), 224 deletions(-) create mode 100644 src/.DS_Store create mode 100644 src/gui/.DS_Store create mode 100644 src/gui/app_state/error.rs create mode 100644 src/gui/app_state/main.rs delete mode 100644 src/gui/app_state/visualize.rs diff --git a/Cargo.lock b/Cargo.lock index 03320ed..7e2e430 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -488,6 +488,27 @@ dependencies = [ "syn", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if 1.0.0", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -868,6 +889,7 @@ dependencies = [ "rusqlite", "serde", "serde_json", + "shellexpand", "strum", "strum_macros", "thiserror", @@ -1694,6 +1716,16 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall", +] + [[package]] name = "regex" version = "1.5.4" @@ -1869,6 +1901,15 @@ dependencies = [ "libc", ] +[[package]] +name = "shellexpand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83bdb7831b2d85ddf4a7b148aa19d0587eddbe8671a436b7bd1182eaad0f2829" +dependencies = [ + "dirs-next", +] + [[package]] name = "siphasher" version = "0.3.7" diff --git a/Cargo.toml b/Cargo.toml index ee79996..b6d692c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ walkdir = "*" mbox-reader = "0.2.0" rfd = "0.5.1" rand = "0.8.4" +shellexpand = "*" [features] default = ["gui"] diff --git a/README.md b/README.md index 81bf859..ad6bc88 100644 --- a/README.md +++ b/README.md @@ -15,3 +15,11 @@ Update: It currently parses 632115 emails in ~56 seconds, so roughly `11.000` em - [ ] try `iced` or `druid` as well - [ ] maybe add blocking versions of the calls, too (in model) - [ ] abstract over `Fields` and backend + +- [ ] open database support +- [ ] add the read, send, filtres +- [ ] mbox is multiple files +- [ ] rename +- [ ] store last opened sqlite file +- [ ] save config into sqlite +- [ ] split up into multiple crates \ No newline at end of file diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..d29525a22b86210b932b14b3b4329cb20ce62fb2 GIT binary patch literal 6148 zcmeH~Jqp4=5QS$ngtmHOJS^?h6Xg{|JDnJFO02QDDRA531 zuRzEqg^zI56wHPO))TycF}?arq#hf1*pJCfqCS;o&S6IxA}k6!juY7fj?6~ zyWL^8#Y^Sc`tf>JKW5d|4G#L{2yZ_DNbD$H!QHT5Yys9}3!(z!kATa-Km~rPzzb)K B5pn>(path: P) -> Result { + // FIXME: if the file exists, we're re-opening it. + // this means we need to query the `meta` table + // to retrieve the contents of the config... + + dbg!(path.as_ref()); + #[allow(unused_mut)] let mut connection = Connection::open(path.as_ref())?; @@ -140,6 +146,7 @@ impl Database { fn create_tables(connection: &Connection) -> Result<()> { connection.execute(TBL_EMAILS, params![])?; connection.execute(TBL_ERRORS, params![])?; + connection.execute(TBL_META, params![])?; Ok(()) } } diff --git a/src/database/sql.rs b/src/database/sql.rs index 24e7d19..e7838d5 100644 --- a/src/database/sql.rs +++ b/src/database/sql.rs @@ -19,11 +19,6 @@ CREATE TABLE IF NOT EXISTS emails ( meta_is_seen BOOL NULL );"#; -pub const TBL_ERRORS: &str = r#" -CREATE TABLE IF NOT EXISTS errors ( - message TEXT NOT NULL -);"#; - pub const QUERY_EMAILS: &str = r#" INSERT INTO emails ( @@ -43,9 +38,30 @@ VALUES ) "#; +pub const TBL_ERRORS: &str = r#" +CREATE TABLE IF NOT EXISTS errors ( + message TEXT NOT NULL +);"#; + pub const QUERY_ERRORS: &str = r#" INSERT INTO errors (message) VALUES (?) "#; + +pub const TBL_META: &str = r#" +CREATE TABLE IF NOT EXISTS meta ( + key TEXT NOT NULL, + value TEXT NOT NULL +);"#; + +pub const QUERY_INSERT_META: &str = r#" +INSERT INTO meta + (key, value) +VALUES + (?, ?) +"#; + +pub const QUERY_SELECT_META: &str = r#" +SELECT key, value FROM meta"#; diff --git a/src/gui/.DS_Store b/src/gui/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..12eba6695f26ac7eb50c561566db086dea9b20ad GIT binary patch literal 8196 zcmeHMU2GIp6h3EK=*&Q2iu`m|p-VRifkoO>uF*&<#Yf>vdf_Q@KxP4q0Y7r7i4m9Y1Y3W`P@6hDj<%`Yp$B%k6H_F+6UY6aqz& zm6TRLI=a4jb3D4CdHq;CI=Z=~DIRUz*g7_*$ctj@w)Uh>*g3~NBQ}A-hX87)ZR+gQ z9T{HM&d>^()})Qv!_CR8o6B?K?zQavW3tpMb%KyId=a=oR@6EAp?-C6z{dw9&`;hi=kbV6)uT2==xzM=_r`g8?wxYSFY04 zUXQk`r31zIT2<{mK}gU%S^`^mbBn6?+3AdFYlb4sHa@88{Uxz$ZGSMhb(^XW&|AyV z7QH%*2g8XrT|dTVK9KhOuDt2cs;KE_br|U!BNV;Hl zl?kERXKbxB{xFU6mq`-#tX+oXyM;j5!-bmVlq`Zjy$lfrvMk3EtHoR4o>dkKW};`Q zO@~Hkg>BFcgJ3`z&cX9=9$tqFa2ei*kKj|d4maRS_zG^qkMJ}63ctZE1XOSVHsEq> z#2ChL6K=*W*nxX+AMVEk*oQ-S1drk|9L5~Fn8%ZN8lS)?@hN;7pTigN6?_$6!#D6E zzKfUeGG4)}_#u9RpW`=p6Tidn@h1r-Nve|ON)1v}S}rw8YosQLjs{tiDnT|R_)0Ie z)7{i~36A}wlXr@j$V0o;z5i;w+?XpO@BZP~arF@Yojy$aeUW0(VX3fwtx z`Foy*IEFSoa<95{rdaAD#G9vN23xI0Xh+pXnGmnd66^|frA%1YW(#(8Bra2QX%c7E zb&+O;Vo0kJ?CQt{Mb=O-o761|MUz%7*zM|0g$1|2O9a2Ny%XcpJx)-OUZ&`yUs9;v+$514>qJJd$KHAfLtZxRAPtQ z-;G_BSX4c$|98!N{(m9`b=LeR%=J;dXz5KnBKUc+=Mz_ vjuYkOIMIne4Cy{fs>~)4&~ZU&gxWuT5D?~n=Jr4PQ#H*06E*UGBme&n^U`sD literal 0 HcmV?d00001 diff --git a/src/gui/app.rs b/src/gui/app.rs index f825624..1fe945f 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -1,28 +1,24 @@ use eframe::{ egui::{self}, - epi::{self, Frame, Storage}, + epi::{self, App, Frame, Storage}, }; -use eyre::Result; +use eyre::{Report, Result}; -use super::app_state::{self, Import, Startup, Visualize}; +use super::app_state::StateUI; -pub enum GmailDBApp { - Startup { panel: Startup }, - Import { panel: Import }, - Visualize { panel: Visualize }, -} +pub struct GmailDBApp(StateUI); impl GmailDBApp { pub fn new() -> Result { // Temporarily create config without state machine - let config = app_state::make_temporary_ui_config(); - Ok(GmailDBApp::Import { - panel: Import::new(config), - }) + //let config = app_state::make_temporary_ui_config(); + // let config = crate::make_config(); + let state = StateUI::new(); + Ok(GmailDBApp(state)) } } -impl epi::App for GmailDBApp { +impl App for GmailDBApp { fn name(&self) -> &str { "Gmail DB" } @@ -49,23 +45,25 @@ impl epi::App for GmailDBApp { } fn update(&mut self, ctx: &egui::CtxRef, frame: &mut epi::Frame<'_>) { - match self { - GmailDBApp::Startup { panel } => Self::update_panel(panel, ctx, frame), - GmailDBApp::Import { panel } => Self::update_panel(panel, ctx, frame), - _ => panic!(), - } + self.0.update(ctx); + + // match self { + // GmailDBApp::Startup { panel } => Self::update_panel(panel, ctx, frame), + // GmailDBApp::Import { panel } => Self::update_panel(panel, ctx, frame), + // _ => panic!(), + // } // Resize the native window to be just the size we need it to be: frame.set_window_size(ctx.used_size()); } } -impl GmailDBApp { - fn update_panel(panel: impl egui::Widget, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) { - egui::CentralPanel::default() - .frame(egui::containers::Frame::none()) - .show(ctx, |ui| { - ui.add(panel); - }); - } -} +// impl GmailDBApp { +// fn update_panel(panel: impl egui::Widget, ctx: &egui::CtxRef, _frame: &mut epi::Frame<'_>) { +// egui::CentralPanel::default() +// .frame(egui::containers::Frame::none()) +// .show(ctx, |ui| { +// ui.add(panel); +// }); +// } +// } diff --git a/src/gui/app_state/error.rs b/src/gui/app_state/error.rs new file mode 100644 index 0000000..49e8895 --- /dev/null +++ b/src/gui/app_state/error.rs @@ -0,0 +1,43 @@ +use eframe::{ + egui, + egui::{Response, Widget}, +}; + +use super::super::widgets; +use super::{StateUI, StateUIAction, StateUIVariant}; +use crate::types::Config; + +pub struct ErrorUI { + /// The error to display + report: eyre::Report, + /// The config that led to this error, in order + /// to let the user `go back`. + /// As we might not have a config *yet* this is optional + config: Option, +} + +impl ErrorUI { + pub fn new(report: eyre::Report, config: Option) -> Self { + Self { report, config } + } +} +impl StateUIVariant for ErrorUI { + fn update_panel(&mut self, ctx: &egui::CtxRef) -> StateUIAction { + egui::CentralPanel::default() + .frame(egui::containers::Frame::none()) + .show(ctx, |ui| { + ui.add(|ui: &mut egui::Ui| self.ui(ui)); + }); + // If the user tapped the back button, go back to startup + StateUIAction::Nothing + } +} + +impl ErrorUI { + fn ui(&mut self, ui: &mut egui::Ui) -> Response { + // FIXME: Try again button + // that goes back to the `Startup` screen. + // somehow it should also fill it out again? + ui.add(widgets::ErrorBox(&self.report)) + } +} diff --git a/src/gui/app_state/import.rs b/src/gui/app_state/import.rs index c6da3fe..66f6d34 100644 --- a/src/gui/app_state/import.rs +++ b/src/gui/app_state/import.rs @@ -1,15 +1,28 @@ //! The startup form to configure what and how to import +use std::thread::JoinHandle; + use eframe::egui::epaint::Shadow; use eframe::egui::{self, vec2, Color32, Pos2, Rect, Response, Stroke, TextStyle, Vec2, Widget}; +use eyre::Result; use rand::seq::SliceRandom; use super::super::platform::platform_colors; use super::super::widgets::background::{shadow_background, AnimatedBackground}; +use super::{StateUI, StateUIAction, StateUIVariant}; use crate::types::Config; -use crate::types::FormatType; +use crate::{ + importer::{self, Adapter, State}, + types::FormatType, +}; -pub struct Import { +pub struct ImporterUI { + /// The config for this configuration config: Config, + /// The adapter handling the import + adapter: Adapter, + /// The handle to the adapter thread + /// As handle.join takes `self` it has to be optional + handle: Option>>, /// The animation divisions animation_divisions: usize, /// time counter @@ -24,10 +37,21 @@ pub struct Import { progress_blocks: Vec, /// The progress divisions progress_divisions: usize, + /// we're done importing + pub done_importing: bool, + /// Any errors during importing + pub importer_error: Option, } -impl Import { - pub fn new(config: Config) -> Self { +// impl super::StateUI for ImporterUI { +// fn next(&self) -> Option { +// self.importer_error.map(|e| super::MainApp::E) +// } +// } + +impl ImporterUI { + pub fn new(config: Config) -> Result { + let cloned_config = config.clone(); // Build a random distribution of elements // to animate the import process let mut rng = rand::thread_rng(); @@ -38,34 +62,101 @@ impl Import { let progress_block_count = (animation_divisions * progress_divisions) * (animation_divisions * progress_divisions); let mut progress_blocks: Vec = (0..progress_block_count).collect(); - dbg!(progress_block_count); progress_blocks.shuffle(&mut rng); - Self { + // The adapter that controls the syncing + let adapter = Adapter::new(); + + // Could not figure out how to build this properly + // with dynamic dispatch. (to abstract away the match) + // Will try again when I'm online. + let handle = match config.format { + FormatType::AppleMail => { + let importer = importer::applemail_importer(config); + adapter.process(importer)? + } + FormatType::GmailVault => { + let importer = importer::gmail_importer(config); + adapter.process(importer)? + } + FormatType::Mbox => { + let importer = importer::mbox_importer(config); + adapter.process(importer)? + } + }; + + Ok(Self { + config: cloned_config, + adapter, + handle: Some(handle), animation_divisions, - config, timer: 0.0, offset_counter: 0, intro_timer: 0.0, progress_blocks, progress_divisions, + done_importing: false, + importer_error: None, + }) + } +} +impl StateUIVariant for ImporterUI { + fn update_panel(&mut self, ctx: &egui::CtxRef) -> StateUIAction { + egui::CentralPanel::default() + .frame(egui::containers::Frame::none()) + .show(ctx, |ui| { + ui.add(|ui: &mut egui::Ui| self.ui(ui)); + }); + // If we generated an action above, return it + //self.action.take().unwrap_or(StateUIAction::Nothing) + match (self.importer_error.take(), self.done_importing) { + (Some(report), _) => StateUIAction::Error { + report, + config: self.config.clone(), + }, + (_, true) => StateUIAction::ImportDone { + config: self.config.clone(), + }, + (_, false) => StateUIAction::Nothing, } } } -impl Widget for &mut Import { - fn ui(self, ui: &mut egui::Ui) -> Response { +impl ImporterUI { + fn ui(&mut self, ui: &mut egui::Ui) -> Response { self.intro_timer += ui.input().unstable_dt as f64; let growth = self.intro_timer.clamp(0.0, 1.0); let available = ui.available_size(); - // We take the progress as a value fromt the blocks - // FIXME: temporary using intro timer - let p = ((self.intro_timer * 5.0) / 100.0); - let n = (self.progress_blocks.len() as f64 * p) as usize; - //println!("{} / {}", n, self.progress_blocks.len()); - let slice = &self.progress_blocks[0..=n]; + let (label, progress, writing, done) = match self.handle_adapter() { + Ok(n) => n, + Err(e) => { + self.importer_error = Some(e); + todo!(); + // return e; + } + }; + + if let Ok(Some(error)) = self.adapter.error() { + println!("Has error"); + self.importer_error = Some(error); + } + + if done { + // if we're done, the join handle should not lock + println!("Done!"); + if let Some(handle) = self.handle.take() { + println!("Wait Join Handle!"); + self.importer_error = handle.join().ok().map(|e| e.err()).flatten(); + } + println!("Done Importing"); + self.done_importing = true; + } + + let n = (self.progress_blocks.len() as f32 * progress) as usize; + let n = n.min(self.progress_blocks.len()); + let slice = &self.progress_blocks[0..n]; AnimatedBackground { divisions: self.animation_divisions, @@ -105,12 +196,50 @@ impl Widget for &mut Import { ui.centered_and_justified(|ui| { ui.vertical_centered_justified(|ui| { ui.heading("Import in Progress"); - let bar = egui::widgets::ProgressBar::new(0.5).animate(true); - ui.add(bar); - ui.small("133 / 1000"); + ui.add_space(10.0); + if writing { + let bar = egui::widgets::ProgressBar::new(1.0).animate(false); + ui.add(bar); + let bar = egui::widgets::ProgressBar::new(progress).animate(true); + ui.add(bar); + } else { + let bar = egui::widgets::ProgressBar::new(progress).animate(true); + ui.add(bar); + ui.add_space(20.0); + } + ui.small(label); }); }) }) .response } } + +impl ImporterUI { + /// Returns the current label, the progress (0-1), writing? (true), and done? (true) + fn handle_adapter(&mut self) -> Result<(String, f32, bool, bool)> { + let (mut label, progress, writing) = { + let write = self.adapter.write_count()?; + if write.count > 0 { + ( + format!("\rParsing emails {}/{}...", write.count, write.total), + (write.count as f32 / write.total as f32), + true, + ) + } else { + let read = self.adapter.read_count()?; + ( + format!("Reading emails {}/{}...", read.count, read.total), + (read.count as f32 / read.total as f32), + false, + ) + } + }; + + let State { done, finishing } = self.adapter.finished()?; + if finishing { + label = format!("Finishing Up"); + } + Ok((label, progress, writing, done)) + } +} diff --git a/src/gui/app_state/main.rs b/src/gui/app_state/main.rs new file mode 100644 index 0000000..ff27b87 --- /dev/null +++ b/src/gui/app_state/main.rs @@ -0,0 +1,134 @@ +use eframe::egui::{self, Response, Stroke, Widget}; +use eyre::{Report, Result}; + +use super::super::widgets::{self, FilterState, Spinner}; +use super::{StateUI, StateUIAction, StateUIVariant}; +use crate::types::Config; + +use crate::model::Engine; + +#[derive(Default)] +pub struct UIState { + pub show_emails: bool, + pub show_filters: bool, + pub show_export: bool, + pub action_close: bool, +} + +pub struct MainUI { + config: Config, + engine: Engine, + error: Option, + state: UIState, + filter_state: FilterState, + platform_custom_setup: bool, +} + +impl MainUI { + pub fn new(config: Config) -> Result { + let mut engine = Engine::new(&config)?; + engine.start()?; + Ok(Self { + config, + engine, + error: None, + state: UIState::default(), + filter_state: FilterState::new(), + platform_custom_setup: false, + }) + } +} + +impl StateUIVariant for MainUI { + fn update_panel(&mut self, ctx: &egui::CtxRef) -> super::StateUIAction { + // Avoid any processing if there is an unhandled error. + if self.error.is_none() { + self.error = self.engine.process().err(); + } + + if !self.platform_custom_setup { + self.platform_custom_setup = true; + self.error = super::super::platform::initial_update(&ctx).err(); + + // Make the UI a bit bigger + let pixels = ctx.pixels_per_point(); + ctx.set_pixels_per_point(pixels * 1.2) + } + + let platform_colors = super::super::platform::platform_colors(); + + let frame = egui::containers::Frame::none() + .fill(platform_colors.window_background_dark) + .stroke(Stroke::none()); + + egui::TopBottomPanel::top("my_panel") + .frame(frame) + .show(ctx, |ui| { + ui.add(super::super::navigation_bar::NavigationBar::new( + &mut self.engine, + &mut self.error, + &mut self.state, + &mut self.filter_state, + )); + }); + + if self.state.show_emails { + egui::SidePanel::right("my_left_panel") + .default_width(500.0) + .show(ctx, |ui| { + ui.add(super::super::mail_panel::MailPanel::new( + &mut self.engine, + &mut self.error, + )); + }); + } + + egui::CentralPanel::default() + .frame(egui::containers::Frame::none()) + .show(ctx, |ui| { + if self.engine.segmentations().is_empty() { + ui.centered_and_justified(|ui| { + ui.add(Spinner::new(egui::vec2(50.0, 50.0))); + }); + } else { + let stroke = Stroke::none(); + let fill = platform_colors.content_background_dark; + super::super::widgets::background::color_background( + ui, + 15.0, + stroke, + fill, + |ui| { + ui.vertical(|ui: &mut egui::Ui| { + ui.add(super::super::segmentation_bar::SegmentationBar::new( + &mut self.engine, + &mut self.error, + )); + ui.add(super::super::widgets::Rectangles::new( + &mut self.engine, + &mut self.error, + )); + }) + .response + }, + ); + } + }); + + // If we're waiting for a computation to succeed, we re-render again. + if self.engine.is_busy() { + ctx.request_repaint(); + } + + match (self.state.action_close, self.error.take()) { + (_, Some(error)) => StateUIAction::Error { + report: error, + config: self.config.clone(), + }, + (true, _) => StateUIAction::Close { + config: self.config.clone(), + }, + _ => StateUIAction::Nothing, + } + } +} diff --git a/src/gui/app_state/mod.rs b/src/gui/app_state/mod.rs index dd13217..01a4386 100644 --- a/src/gui/app_state/mod.rs +++ b/src/gui/app_state/mod.rs @@ -1,16 +1,156 @@ +mod error; mod import; +mod main; mod startup; -mod visualize; - -pub use import::Import; -pub use startup::Startup; -pub use visualize::{UIState, Visualize}; - -pub fn make_temporary_ui_config() -> crate::types::Config { - crate::types::Config::new( - "./db6.sql", - "", - "terhechte@me.com".to_string(), - crate::types::FormatType::AppleMail, - ) + +use std::path::PathBuf; + +use eframe::egui::{self, Widget}; +pub use error::ErrorUI; +use eyre::{Report, Result}; +pub use import::ImporterUI; +pub use main::{MainUI, UIState}; +pub use startup::StartupUI; + +use crate::types::{Config, FormatType}; + +pub enum StateUIAction { + CreateDatabase { + database_path: Option, + emails_folder_path: PathBuf, + sender_emails: Vec, + format: FormatType, + }, + OpenDatabase { + database_path: PathBuf, + }, + ImportDone { + config: Config, + }, + Close { + config: Config, + }, + Error { + report: Report, + config: Config, + }, + Nothing, +} + +// FIXME: Removve +// pub fn make_temporary_ui_config() -> crate::types::Config { +// crate::types::Config::new( +// "./db6.sql", +// "", +// vec!["terhechte@me.com".to_string()], +// crate::types::FormatType::AppleMail, +// ) +// } + +// pub enum MainApp { +// Startup { panel: StartupUI }, +// Import { panel: ImporterUI }, +// Main { panel: MainUI }, +// Error { panel: ErrorUI }, +// } + +/// This defines the state machine switches between the `MainApp` +/// states. +/// FIXME: I'm not particularly happy with this abstraction right now. +// impl MainApp { +// /// An Error state can always happen +// pub fn error(report: eyre::Report) -> MainApp { +// MainApp::Error { +// panel: ErrorUI(report), +// } +// } +// // pub fn import(startup: &) -> MainApp { + +// // } +// } +pub enum StateUI { + Startup(startup::StartupUI), + Import(import::ImporterUI), + Main(main::MainUI), + Error(error::ErrorUI), +} + +pub trait StateUIVariant { + fn update_panel(&mut self, ctx: &egui::CtxRef) -> StateUIAction; +} + +impl StateUI { + /// This proxies the `update` call to the individual calls in + /// the `app_state` types + pub fn update(&mut self, ctx: &egui::CtxRef) { + let response = match self { + StateUI::Startup(panel) => panel.update_panel(ctx), + StateUI::Import(panel) => panel.update_panel(ctx), + StateUI::Main(panel) => panel.update_panel(ctx), + StateUI::Error(panel) => panel.update_panel(ctx), + }; + match response { + StateUIAction::CreateDatabase { + database_path, + emails_folder_path, + sender_emails, + format, + } => { + *self = + self.create_database(database_path, emails_folder_path, sender_emails, format) + } + StateUIAction::OpenDatabase { database_path } => { + *self = self.open_database(database_path) + } + StateUIAction::ImportDone { config } => { + *self = match main::MainUI::new(config.clone()) { + Ok(n) => StateUI::Main(n), + Err(e) => StateUI::Error(ErrorUI::new(e, Some(config.clone()))), + }; + } + StateUIAction::Close { config } => { + *self = StateUI::Startup(StartupUI::from_config(config)); + } + StateUIAction::Error { report, config } => { + *self = StateUI::Error(error::ErrorUI::new(report, Some(config))) + } + StateUIAction::Nothing => (), + } + } +} + +impl StateUI { + pub fn new() -> StateUI { + StateUI::Startup(startup::StartupUI::default()) + } + + pub fn create_database( + &self, + database_path: Option, + emails_folder_path: PathBuf, + sender_emails: Vec, + format: FormatType, + ) -> StateUI { + let config = match Config::new(database_path, emails_folder_path, sender_emails, format) { + Ok(n) => n, + Err(e) => { + return StateUI::Error(error::ErrorUI::new(e, None)); + } + }; + + let importer = match import::ImporterUI::new(config.clone()) { + Ok(n) => n, + Err(e) => { + return StateUI::Error(error::ErrorUI::new(e, Some(config.clone()))); + } + }; + + return StateUI::Import(importer); + } + + pub fn open_database(&mut self, database_path: PathBuf) -> StateUI { + // FIXME: the database needs to be opened in order to figure + // out whether it is a correct DB, before we can head on + todo!() + } } diff --git a/src/gui/app_state/startup.rs b/src/gui/app_state/startup.rs index 5bac3c3..38d9335 100644 --- a/src/gui/app_state/startup.rs +++ b/src/gui/app_state/startup.rs @@ -7,10 +7,12 @@ use std::path::PathBuf; use super::super::platform::platform_colors; use super::super::widgets::background::{shadow_background, AnimatedBackground}; -use crate::types::FormatType; +use super::{StateUI, StateUIAction, StateUIVariant}; +use crate::database; +use crate::types::{Config, FormatType}; #[derive(Default)] -pub struct Startup { +pub struct StartupUI { /// Which importer format are we using format: FormatType, /// Where are the emails located @@ -26,10 +28,46 @@ pub struct Startup { timer: f64, /// recursive offset counter offset_counter: usize, + /// Potential error message to display to the user + error_message: Option, + /// The result of the actions + action: Option, } -impl Widget for &mut Startup { - fn ui(self, ui: &mut egui::Ui) -> Response { +impl StartupUI { + pub fn from_config(config: Config) -> Self { + let emails = if !config.sender_emails.is_empty() { + let mails: Vec = config.sender_emails.iter().map(|e| e.to_owned()).collect(); + Some(mails.join(", ")) + } else { + None + }; + Self { + format: config.format, + email_folder: Some(config.emails_folder_path), + database_path: Some(config.database_path), + save_to_disk: true, + email_address: emails, + ..Default::default() + } + } +} + +impl StateUIVariant for StartupUI { + fn update_panel(&mut self, ctx: &egui::CtxRef) -> super::StateUIAction { + egui::CentralPanel::default() + .frame(egui::containers::Frame::none()) + .show(ctx, |ui| { + ui.add(|ui: &mut egui::Ui| self.ui(ui)); + }); + // If we generated an action above, return it + self.action.take().unwrap_or(StateUIAction::Nothing) + } +} + +impl StartupUI { + /// Separated to have a less stuff happening + fn ui(&mut self, ui: &mut egui::Ui) -> Response { let available = ui.available_size(); AnimatedBackground { @@ -75,7 +113,10 @@ impl Widget for &mut Startup { let hyperlink_color = visuals.hyperlink_color; // placeholder text - let mut txt = "john@example.org".to_string(); + let mut txt = self + .email_address + .clone() + .unwrap_or("john@example.org".to_string()); let response = ui.allocate_ui_at_rect(center, |ui| { // We use a grid as that gives us more spacing opportunities @@ -105,7 +146,7 @@ impl Widget for &mut Startup { } if self.format == FormatType::AppleMail { if ui.button("or Mail.app default folder").clicked() { - self.email_folder = self.format.default_path().map(|e| e.to_path_buf()) + self.email_folder = self.format.default_path(); } } }); @@ -162,15 +203,31 @@ impl Widget for &mut Startup { let button_size1: Vec2 = ((center.width() / 2.0) - 25.0, 25.0).into(); let button_size2: Vec2 = ((center.width() / 2.0) - 25.0, 25.0).into(); ui.horizontal(|ui| { - ui.add_sized( + let response = ui.add_sized( button_size1, egui::Button::new("Start") - .enabled(self.email_folder.is_some()) + .enabled( + // if we have an email folder, + // and - if we want to save to disk - + // if we have a database path + self.email_folder.is_some() && + (self.save_to_disk == self.database_path.is_some()) + ) .text_color(Color32::WHITE), ); - ui.add_sized(button_size2, egui::Button::new("Or Open Database")); + if response.clicked() { + self.action_start(); + } + let response = ui.add_sized(button_size2, egui::Button::new("Or Open Database")); + if response.clicked() { + self.action_open_database(); + } }); ui.end_row(); + if let Some(ref e) = self.error_message { + let r = Color32::from_rgb(255, 0, 0); + ui.colored_label(r, e); + } }); }); @@ -178,7 +235,44 @@ impl Widget for &mut Startup { } } -impl Startup { +impl StartupUI { + fn action_start(&mut self) { + let email = match &self.email_folder { + Some(n) => n.clone(), + _ => return, + }; + + // Split by comma, remove whitespace + let emails: Vec = self + .email_address + .iter() + .map(|e| e.split(",").map(|e| e.trim().to_string()).collect()) + .collect(); + //.unwrap_or_default(); + + if !email.exists() { + self.error_message = Some(format!("Email folder doesn't exist")); + return; + } + + if self.save_to_disk && !self.database_path.is_some() { + self.error_message = Some(format!("Please select a database folder")); + return; + } + + self.action = Some(StateUIAction::CreateDatabase { + database_path: self.database_path.clone(), + emails_folder_path: email, + sender_emails: emails, + format: self.format, + }); + } + + fn action_open_database(&mut self) { + // somehow ask the database to open and return a config... + // this should rather lie in a in-between model-layer... + } + fn format_selection(&mut self, ui: &mut egui::Ui, width: f32) { let mut selected = self.format; let response = egui::ComboBox::from_id_source("mailbox_type_comboox") @@ -198,7 +292,7 @@ impl Startup { let default_path = self .format .default_path() - .unwrap_or(std::path::Path::new("~/")); + .unwrap_or(std::path::Path::new("~/").to_path_buf()); let folder = rfd::FileDialog::new() .set_directory(default_path) @@ -229,4 +323,12 @@ impl Startup { }; self.database_path = Some(path); } + + // fn set_default_folder(&self) -> PathBuf { + // let path = self.format.default_path() { + // Some(n) => n, + // } + + // self.email_folder = self.format.default_path().map(|e| e.to_path_buf()) + // } } diff --git a/src/gui/app_state/visualize.rs b/src/gui/app_state/visualize.rs deleted file mode 100644 index b754cad..0000000 --- a/src/gui/app_state/visualize.rs +++ /dev/null @@ -1,115 +0,0 @@ -use eframe::egui::{self, Response, Stroke, Widget}; -use eyre::Report; - -use super::super::widgets::{self, FilterState, Spinner}; - -use crate::model::Engine; - -#[derive(Default)] -pub struct UIState { - pub show_emails: bool, - pub show_filters: bool, - pub show_export: bool, - pub action_close: bool, -} - -pub struct Visualize { - engine: Engine, - error: Option, - state: UIState, - filter_state: FilterState, - platform_custom_setup: bool, -} - -impl Widget for &mut Visualize { - fn ui(self, ui: &mut egui::Ui) -> Response { - // Avoid any processing if there is an unhandled error. - if self.error.is_none() { - self.error = self.engine.process().err(); - } - - if !self.platform_custom_setup { - self.platform_custom_setup = true; - self.error = super::super::platform::initial_update(&ui.ctx()).err(); - - // Make the UI a bit bigger - let pixels = ui.ctx().pixels_per_point(); - ui.ctx().set_pixels_per_point(pixels * 1.2) - } - - let platform_colors = super::super::platform::platform_colors(); - - let response = if let Some(error) = self.error.as_ref() { - dbg!(&error); - egui::CentralPanel::default() - .show(ui.ctx(), |ui| ui.add(widgets::ErrorBox(error))) - .response - } else { - let frame = egui::containers::Frame::none() - .fill(platform_colors.window_background_dark) - .stroke(Stroke::none()); - - egui::TopBottomPanel::top("my_panel") - .frame(frame) - .show(ui.ctx(), |ui| { - ui.add(super::super::navigation_bar::NavigationBar::new( - &mut self.engine, - &mut self.error, - &mut self.state, - &mut self.filter_state, - )); - }); - - if self.state.show_emails { - egui::SidePanel::right("my_left_panel") - .default_width(500.0) - .show(ui.ctx(), |ui| { - ui.add(super::super::mail_panel::MailPanel::new( - &mut self.engine, - &mut self.error, - )); - }); - } - - egui::CentralPanel::default() - .frame(egui::containers::Frame::none()) - .show(ui.ctx(), |ui| { - if self.engine.segmentations().is_empty() { - ui.centered_and_justified(|ui| { - ui.add(Spinner::new(egui::vec2(50.0, 50.0))); - }); - } else { - let stroke = Stroke::none(); - let fill = platform_colors.content_background_dark; - super::super::widgets::background::color_background( - ui, - 15.0, - stroke, - fill, - |ui| { - ui.vertical(|ui: &mut egui::Ui| { - ui.add(super::super::segmentation_bar::SegmentationBar::new( - &mut self.engine, - &mut self.error, - )); - ui.add(super::super::widgets::Rectangles::new( - &mut self.engine, - &mut self.error, - )); - }) - .response - }, - ); - } - }) - .response - }; - - // If we're waiting for a computation to succeed, we re-render again. - if self.engine.is_busy() { - ui.ctx().request_repaint(); - } - - response - } -} diff --git a/src/importer/formats/apple_mail/filesystem.rs b/src/importer/formats/apple_mail/filesystem.rs index 5c92f47..d31af55 100644 --- a/src/importer/formats/apple_mail/filesystem.rs +++ b/src/importer/formats/apple_mail/filesystem.rs @@ -7,7 +7,7 @@ use rayon::prelude::*; use walkdir::WalkDir; use super::super::shared::filesystem::emails_in; -use super::super::MessageSender; +use super::super::{Message, MessageSender}; use crate::types::Config; use super::mail::Mail; @@ -32,7 +32,8 @@ pub fn read_emails(config: &Config, sender: MessageSender) -> Result> _ => None, }) .collect(); - let mails = folders + sender.send(Message::ReadTotal(folders.len()))?; + let mails: Vec = folders .into_par_iter() .filter_map( |path| match emails_in(path.clone(), sender.clone(), Mail::new) { diff --git a/src/importer/formats/apple_mail/mod.rs b/src/importer/formats/apple_mail/mod.rs index 3ad9c60..de76310 100644 --- a/src/importer/formats/apple_mail/mod.rs +++ b/src/importer/formats/apple_mail/mod.rs @@ -1,6 +1,9 @@ mod filesystem; mod mail; +use shellexpand; +use std::{path::PathBuf, str::FromStr}; + use super::{Config, ImporterFormat, MessageSender, Result}; #[derive(Default)] @@ -9,8 +12,9 @@ pub struct AppleMail {} impl ImporterFormat for AppleMail { type Item = mail::Mail; - fn default_path() -> Option<&'static std::path::Path> { - Some(std::path::Path::new("~/Library/Mail")) + fn default_path() -> Option { + let path = shellexpand::tilde("~/Library/Mail"); + Some(PathBuf::from_str(&path.to_string()).unwrap()) } fn emails(&self, config: &Config, sender: MessageSender) -> Result> { diff --git a/src/importer/formats/gmailbackup/mod.rs b/src/importer/formats/gmailbackup/mod.rs index 2398e1c..7aceffc 100644 --- a/src/importer/formats/gmailbackup/mod.rs +++ b/src/importer/formats/gmailbackup/mod.rs @@ -11,7 +11,7 @@ pub struct Gmail {} impl ImporterFormat for Gmail { type Item = raw_email::RawEmailEntry; - fn default_path() -> Option<&'static std::path::Path> { + fn default_path() -> Option { None } diff --git a/src/importer/formats/mbox/mod.rs b/src/importer/formats/mbox/mod.rs index ffb03b0..6b077e8 100644 --- a/src/importer/formats/mbox/mod.rs +++ b/src/importer/formats/mbox/mod.rs @@ -65,7 +65,7 @@ fn inner_emails(config: &Config) -> Result> { impl ImporterFormat for Mbox { type Item = Mail; - fn default_path() -> Option<&'static Path> { + fn default_path() -> Option { None } diff --git a/src/importer/formats/mod.rs b/src/importer/formats/mod.rs index e23df53..87e5430 100644 --- a/src/importer/formats/mod.rs +++ b/src/importer/formats/mod.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::path::PathBuf; pub use eyre::Result; @@ -23,7 +23,7 @@ pub trait ImporterFormat: Send + Sync { /// The default location path where the data for this format resides /// on system. If there is none (such as for mbox) return `None` - fn default_path() -> Option<&'static Path>; + fn default_path() -> Option; /// Return all the emails in this format. /// Use the sneder to give progress updates via the `ReadProgress` case. diff --git a/src/importer/formats/shared/database.rs b/src/importer/formats/shared/database.rs index 929f40a..3411eb6 100644 --- a/src/importer/formats/shared/database.rs +++ b/src/importer/formats/shared/database.rs @@ -20,8 +20,8 @@ pub fn into_database( bail!("Channel Failure {:?}", &e); } - // Create a new database connection - let database = Database::new(config.database_path.clone())?; + // Create a new database connection, just for writing + let database = Database::new(config.database_path.clone()).unwrap(); // Consume the connection to begin the import. It will return the `handle` to use for // waiting for the database to finish importing, and the `sender` to submit work. @@ -32,7 +32,7 @@ pub fn into_database( // in paralell.. .par_iter_mut() // parsing them - .map(|raw_mail| parse_email(raw_mail, config.sender_email.as_str())) + .map(|raw_mail| parse_email(raw_mail, &config.sender_emails)) // and inserting them into SQLite .for_each(|entry| { // Try to write the message into the database diff --git a/src/importer/formats/shared/parse.rs b/src/importer/formats/shared/parse.rs index de2fc34..730fc75 100644 --- a/src/importer/formats/shared/parse.rs +++ b/src/importer/formats/shared/parse.rs @@ -3,6 +3,7 @@ use email_parser::address::{Address, EmailAddress, Mailbox}; use eyre::{eyre, Result}; use std::borrow::Cow; +use std::collections::HashSet; use std::path::Path; use super::email::{EmailEntry, EmailMeta}; @@ -24,7 +25,7 @@ pub trait ParseableEmail: Send + Sized + Sync { pub fn parse_email( entry: &mut Entry, - config_sender_email: &str, + sender_emails: &HashSet, ) -> Result { if let Err(e) = entry.prepare() { tracing::error!("Prepare Error: {:?}", e); @@ -58,8 +59,10 @@ pub fn parse_email( // In order to determine the sender, we have to // build up the address again :-( - let is_send = - format!("{}@{}", sender_local_part, sender_domain).as_str() == config_sender_email; + let is_send = { + let email = format!("{}@{}", sender_local_part, sender_domain); + sender_emails.contains(&email) + }; Ok(EmailEntry { path: path.to_path_buf(), diff --git a/src/importer/importer.rs b/src/importer/importer.rs index bfb4e51..592e90e 100644 --- a/src/importer/importer.rs +++ b/src/importer/importer.rs @@ -1,14 +1,14 @@ use super::formats::shared; use super::{Config, ImporterFormat}; -use super::MessageReceiver; +use super::{Message, MessageReceiver}; use crossbeam_channel::{self, unbounded}; use eyre::Result; use std::thread::JoinHandle; pub trait Importerlike { - fn import(self) -> Result<(MessageReceiver, JoinHandle>)>; + fn import(self) -> Result<(MessageReceiver, JoinHandle>)>; } pub struct Importer { @@ -23,23 +23,35 @@ impl Importer { } impl Importerlike for Importer { - fn import(self) -> Result<(MessageReceiver, JoinHandle>)> { + fn import(self) -> Result<(MessageReceiver, JoinHandle>)> { let Importer { format, .. } = self; let (sender, receiver) = unbounded(); let config = self.config; - let handle: JoinHandle> = std::thread::spawn(move || { - let emails = format.emails(&config, sender.clone())?; - let processed = shared::database::into_database(&config, emails, sender.clone())?; - - Ok(processed) + let handle: JoinHandle> = std::thread::spawn(move || { + let outer_sender = sender.clone(); + let processed = move || { + let emails = format.emails(&config, sender.clone())?; + let processed = shared::database::into_database(&config, emails, sender.clone())?; + + Ok(processed) + }; + let result = processed(); + // Send the error away and map it to a crossbeam channel error + match result { + Ok(_) => Ok(()), + Err(e) => match outer_sender.send(Message::Error(e)) { + Ok(_) => Ok(()), + Err(e) => Err(eyre::Report::new(e)), + }, + } }); Ok((receiver, handle)) } } impl Importerlike for Box { - fn import(self) -> Result<(MessageReceiver, JoinHandle>)> { + fn import(self) -> Result<(MessageReceiver, JoinHandle>)> { (*self).import() } } diff --git a/src/importer/message_adapter.rs b/src/importer/message_adapter.rs index 64e362d..4fd8ea6 100644 --- a/src/importer/message_adapter.rs +++ b/src/importer/message_adapter.rs @@ -1,4 +1,4 @@ -use eyre::{bail, eyre, Result}; +use eyre::{bail, eyre, Report, Result}; use std::sync::{Arc, RwLock}; use std::thread::JoinHandle; @@ -7,7 +7,7 @@ use super::formats::ImporterFormat; use super::importer::Importerlike; use super::Message; -#[derive(Clone, Debug, Copy, Default)] +#[derive(Debug, Default)] struct Data { total_read: usize, read: usize, @@ -15,6 +15,7 @@ struct Data { write: usize, finishing: bool, done: bool, + error: Option, } #[derive(Clone, Debug, Copy)] @@ -69,7 +70,15 @@ impl Adapter { for entry in receiver.try_iter() { match entry { Message::ReadTotal(n) => write_guard.total_read = n, - Message::ReadOne => write_guard.read += 1, + Message::ReadOne => { + write_guard.read += 1; + // Depending on the implementation, we may receive read calls before + // the total size is known. We prevent division by zero by + // always setting the total to read + 1 in these cases + if write_guard.total_read <= write_guard.read { + write_guard.total_read = write_guard.read + 1; + } + } Message::WriteTotal(n) => write_guard.total_write = n, Message::WriteOne => write_guard.write += 1, Message::FinishingUp => write_guard.finishing = true, @@ -77,6 +86,9 @@ impl Adapter { write_guard.done = true; break 'outer; } + Message::Error(e) => { + write_guard.error = Some(e); + } }; } } @@ -111,4 +123,17 @@ impl Adapter { done: item.done, }) } + + pub fn error(&self) -> Result> { + // We take the error of out of the write lock only if there is an error. + let item = self.consumer_lock.read().map_err(|e| eyre!("{:?}", &e))?; + let is_error = item.error.is_some(); + drop(item); + if is_error { + let mut item = self.producer_lock.write().map_err(|e| eyre!("{:?}", &e))?; + Ok(item.error.take()) + } else { + Ok(None) + } + } } diff --git a/src/importer/mod.rs b/src/importer/mod.rs index 64c4375..57f7144 100644 --- a/src/importer/mod.rs +++ b/src/importer/mod.rs @@ -11,6 +11,7 @@ pub use message_adapter::*; use formats::ImporterFormat; /// The message that informs of the importers progress +#[derive(Debug)] pub enum Message { /// How much progress are we making on reading the contents /// of the emails. @@ -31,6 +32,8 @@ pub enum Message { FinishingUp, /// Finally, this indicates that we're done. Done, + /// An error happened during processing + Error(eyre::Report), } pub type MessageSender = crossbeam_channel::Sender; diff --git a/src/lib.rs b/src/lib.rs index 9d0964c..8bb96d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,7 +55,10 @@ pub fn make_config() -> types::Config { ); } - crate::types::Config::new(database, folder, sender.to_string(), format) + match crate::types::Config::new(Some(database), folder, vec![sender.to_string()], format) { + Ok(n) => n, + Err(r) => panic!("Error: {:?}", &r), + } } fn usage(error: &'static str) -> ! { diff --git a/src/model/link.rs b/src/model/link.rs index f920c45..c509446 100644 --- a/src/model/link.rs +++ b/src/model/link.rs @@ -69,6 +69,7 @@ impl Link { } pub(super) fn run(config: &Config) -> Result> { + // Create a new database connection, just for reading let database = Database::new(&config.database_path)?; let (input_sender, input_receiver) = unbounded(); let (output_sender, output_receiver) = unbounded(); diff --git a/src/types/config.rs b/src/types/config.rs index 3b4866a..d49ffd3 100644 --- a/src/types/config.rs +++ b/src/types/config.rs @@ -1,6 +1,9 @@ +use rand::Rng; use strum::{self, IntoEnumIterator}; use strum_macros::{EnumIter, IntoStaticStr}; +use std::collections::HashSet; +use std::iter::FromIterator; use std::path::{Path, PathBuf}; #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoStaticStr, EnumIter)] @@ -24,7 +27,7 @@ impl FormatType { } /// Forward the importer format location - pub fn default_path(&self) -> Option<&'static Path> { + pub fn default_path(&self) -> Option { use crate::importer::formats::{self, ImporterFormat}; match self { FormatType::AppleMail => formats::AppleMail::default_path(), @@ -63,19 +66,39 @@ pub struct Config { pub database_path: PathBuf, /// The path where the emails are pub emails_folder_path: PathBuf, - /// The address used to send emails - pub sender_email: String, + /// The addresses used to send emails + pub sender_emails: HashSet, /// The importer format we're using pub format: FormatType, } impl Config { - pub fn new>(db: A, mails: A, sender_email: String, format: FormatType) -> Self { - Config { - database_path: db.as_ref().to_path_buf(), + pub fn new>( + db: Option, + mails: A, + sender_emails: Vec, + format: FormatType, + ) -> eyre::Result { + // If we don't have a database path, we use a temporary folder. + let database_path = match db { + Some(n) => n.as_ref().to_path_buf(), + None => { + let number: u32 = rand::thread_rng().gen(); + let folder = "gmaildb"; + let filename = format!("{}.sqlite", number); + let mut temp_dir = std::env::temp_dir(); + temp_dir.push(folder); + // the folder has to be created + std::fs::create_dir_all(&temp_dir)?; + temp_dir.push(filename); + temp_dir + } + }; + Ok(Config { + database_path, emails_folder_path: mails.as_ref().to_path_buf(), - sender_email, + sender_emails: HashSet::from_iter(sender_emails.into_iter()), format, - } + }) } }