Permission error on macOS, small improvements

On macOS, a special permission is needed to access the mail directory.
This commit adds support to help users to add this permission with
a screenshot and guides.

This was a bit involved because the image should only be loaded on macOS
and because loading images in egui is quite a pain.
pull/1/head
Benedikt Terhechte 3 years ago
parent b82d1d769d
commit bb20d3d41d

149
Cargo.lock generated

@ -33,6 +33,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler32"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]]
name = "ahash"
version = "0.7.4"
@ -130,7 +136,7 @@ dependencies = [
"cc",
"cfg-if 1.0.0",
"libc",
"miniz_oxide",
"miniz_oxide 0.4.4",
"object",
"rustc-demangle",
]
@ -159,6 +165,18 @@ version = "3.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538"
[[package]]
name = "bytemuck"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72957246c41db82b8ef88a5486143830adeb8227ef9837740bdec67724cf2c5b"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "cairo-sys-rs"
version = "0.14.9"
@ -269,6 +287,12 @@ dependencies = [
"objc",
]
[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "copypasta"
version = "0.7.1"
@ -477,6 +501,16 @@ dependencies = [
"syn",
]
[[package]]
name = "deflate"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174"
dependencies = [
"adler32",
"byteorder",
]
[[package]]
name = "derivative"
version = "2.2.0"
@ -671,7 +705,7 @@ dependencies = [
"cfg-if 1.0.0",
"crc32fast",
"libc",
"miniz_oxide",
"miniz_oxide 0.4.4",
]
[[package]]
@ -876,6 +910,7 @@ dependencies = [
"emlx",
"eyre",
"flate2",
"image",
"lazy_static",
"lru",
"mbox-reader",
@ -970,6 +1005,21 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "image"
version = "0.23.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"num-iter",
"num-rational",
"num-traits",
"png",
]
[[package]]
name = "indenter"
version = "0.3.3"
@ -1124,15 +1174,6 @@ dependencies = [
"libc",
]
[[package]]
name = "matchers"
version = "0.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1"
dependencies = [
"regex-automata",
]
[[package]]
name = "mbox-reader"
version = "0.2.0"
@ -1191,6 +1232,15 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c835948974f68e0bd58636fc6c5b1fbff7b297e3046f11b3b3c18bbac012c6d"
[[package]]
name = "miniz_oxide"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
dependencies = [
"adler32",
]
[[package]]
name = "miniz_oxide"
version = "0.4.4"
@ -1350,6 +1400,28 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.14"
@ -1584,6 +1656,18 @@ dependencies = [
"xml-rs",
]
[[package]]
name = "png"
version = "0.16.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6"
dependencies = [
"bitflags",
"crc32fast",
"deflate",
"miniz_oxide 0.3.7",
]
[[package]]
name = "ppv-lite86"
version = "0.2.10"
@ -1737,15 +1821,6 @@ dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.25"
@ -2093,9 +2168,9 @@ dependencies = [
[[package]]
name = "tracing"
version = "0.1.28"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84f96e095c0c82419687c20ddf5cb3eadb61f4e1405923c9dc8e53a1adacbda8"
checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
dependencies = [
"cfg-if 1.0.0",
"pin-project-lite",
@ -2105,9 +2180,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.16"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98863d0dd09fa59a1b79c6750ad80dbda6b75f4e71c437a6a1a8cb91a8bcbd77"
checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e"
dependencies = [
"proc-macro2",
"quote",
@ -2116,9 +2191,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.20"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46125608c26121c81b0c6d693eab5a420e416da7e43c426d2e8f7df8da8a3acf"
checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4"
dependencies = [
"lazy_static",
]
@ -2134,36 +2209,18 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-serde"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb65ea441fbb84f9f6748fd496cf7f63ec9af5bca94dd86456978d055e8eb28b"
dependencies = [
"serde",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.2.24"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdd0568dbfe3baf7048b7908d2b32bca0d81cd56bec6d2a8f894b01d74f86be3"
checksum = "5cf865b5ddc38e503a29c41c4843e616a73028ae18c637bc3eb2afaef4909c84"
dependencies = [
"ansi_term",
"chrono",
"lazy_static",
"matchers",
"regex",
"serde",
"serde_json",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
"tracing-serde",
]
[[package]]

@ -26,8 +26,8 @@ nisi ut aliquip ex ea commodo consequat.
[dependencies]
eyre = "0.6.5"
thiserror = "1.0.29"
tracing = "0.1.28"
tracing-subscriber = "0.2.24"
tracing = "0.1.29"
tracing-subscriber = "0.3.0"
rusqlite = {version = "0.25.3", features = ["chrono", "trace", "serde_json"]}
regex = "1.5.3"
flate2 = "1.0.22"
@ -51,6 +51,7 @@ mbox-reader = "0.2.0"
rfd = "0.5.1"
rand = "0.8.4"
shellexpand = "*"
image = { version = "0.23", default-features = false, features = ["png"] }
[features]
default = ["gui"]

@ -62,7 +62,9 @@ fn main() -> Result<()> {
}
fn handle_adapter(adapter: &Adapter) -> Result<bool> {
let State { done, finishing } = adapter.finished()?;
let State {
done, finishing, ..
} = adapter.finished()?;
if done {
return Ok(true);
}

@ -1,5 +1,6 @@
#[cfg(feature = "gui")]
fn main() {
gmaildb::setup_tracing();
gmaildb::gui::run_gui();
}

@ -4,9 +4,10 @@ use eyre::{bail, Report, Result};
use rusqlite::{self, params, Connection, Statement};
use core::panic;
use std::{path::Path, thread::JoinHandle};
use std::{collections::HashMap, path::Path, thread::JoinHandle};
use super::{query::Query, query_result::QueryResult, sql::*, DBMessage};
use crate::types::Config;
use crate::{database::RowConversion, importer::EmailEntry};
#[derive(Debug)]
@ -15,12 +16,15 @@ pub struct Database {
}
impl Database {
/// Open a database and try to retrieve a config from the information stored in there
pub fn config<P: AsRef<Path>>(path: P) -> Result<Config> {
let database = Self::new(path.as_ref())?;
let fields = database.select_config_fields()?;
Config::from_fields(path.as_ref(), fields)
}
/// Open database at path `Path`.
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
// 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...
#[allow(unused_mut)]
let mut connection = Connection::open(path.as_ref())?;
@ -40,6 +44,13 @@ impl Database {
})
}
pub fn save_config(&self, config: Config) -> Result<()> {
let fields = config
.into_fields()
.ok_or(eyre::eyre!("Could not create fields from config"))?;
self.insert_config_fields(fields)
}
pub fn query(&self, query: &super::query::Query) -> Result<Vec<QueryResult>> {
use rusqlite::params_from_iter;
let c = match &self.connection {
@ -147,6 +158,42 @@ impl Database {
connection.execute(TBL_META, params![])?;
Ok(())
}
fn select_config_fields(&self) -> Result<HashMap<String, serde_json::Value>> {
let connection = match &self.connection {
Some(n) => n,
None => bail!("No connection to database available in query"),
};
let mut stmt = connection.prepare(&QUERY_SELECT_META)?;
let mut query_results = HashMap::new();
let mut rows = stmt.query([])?;
while let Some(row) = rows.next()? {
let (k, v) = match (
row.get::<_, String>("key"),
row.get::<_, serde_json::Value>("value"),
) {
(Ok(k), Ok(v)) => (k, v),
(a, b) => {
tracing::error!("Invalid row data. Missing fields key and or value:\nkey: {:?}\nvalue: {:?}\n", a, b);
continue;
}
};
query_results.insert(k, v);
}
Ok(query_results)
}
fn insert_config_fields(&self, fields: HashMap<String, serde_json::Value>) -> Result<()> {
let connection = match &self.connection {
Some(n) => n,
None => bail!("No connection to database available in query"),
};
let mut stmt = connection.prepare(&QUERY_INSERT_META)?;
for (key, value) in fields {
stmt.execute(params![key, value])?;
}
Ok(())
}
}
fn insert_mail(statement: &mut Statement, entry: &EmailEntry) -> Result<()> {

@ -4,10 +4,13 @@ use eframe::{
};
use super::app_state::StateUI;
use super::textures::Textures;
pub struct GmailDBApp {
state: StateUI,
platform_custom_setup: bool,
textures: Option<Textures>,
}
impl GmailDBApp {
@ -16,6 +19,7 @@ impl GmailDBApp {
GmailDBApp {
state,
platform_custom_setup: false,
textures: None,
}
}
}
@ -25,12 +29,7 @@ impl App for GmailDBApp {
"Gmail DB"
}
fn setup(
&mut self,
ctx: &egui::CtxRef,
_frame: &mut Frame<'_>,
_storage: Option<&dyn Storage>,
) {
fn setup(&mut self, ctx: &egui::CtxRef, frame: &mut Frame<'_>, _storage: Option<&dyn Storage>) {
super::platform::setup(ctx);
// Adapt to the platform colors
@ -38,6 +37,9 @@ impl App for GmailDBApp {
let mut visuals = egui::Visuals::dark();
visuals.widgets.noninteractive.bg_fill = platform_colors.window_background_dark;
ctx.set_visuals(visuals);
// Load textures
self.textures = Some(Textures::populated(frame));
}
fn update(&mut self, ctx: &egui::CtxRef, frame: &mut epi::Frame<'_>) {
@ -54,7 +56,7 @@ impl App for GmailDBApp {
}
}
self.state.update(ctx);
self.state.update(ctx, &self.textures);
// Resize the native window to be just the size we need it to be:
frame.set_window_size(ctx.used_size());

@ -3,6 +3,7 @@ use eframe::{
egui::{vec2, Response},
};
use super::Textures;
use super::{StateUIAction, StateUIVariant};
use crate::types::Config;
@ -28,7 +29,7 @@ impl ErrorUI {
}
}
impl StateUIVariant for ErrorUI {
fn update_panel(&mut self, ctx: &egui::CtxRef) -> StateUIAction {
fn update_panel(&mut self, ctx: &egui::CtxRef, _textures: &Option<Textures>) -> StateUIAction {
egui::CentralPanel::default()
.frame(egui::containers::Frame::group(&ctx.style()).margin(vec2(32.0, 32.0)))
.show(ctx, |ui| {
@ -46,8 +47,6 @@ impl StateUIVariant for ErrorUI {
impl ErrorUI {
fn ui(&mut self, ui: &mut egui::Ui) -> Response {
//let available = ui.available_size();
let width = 250.0;
ui.vertical_centered(|ui| {

@ -2,12 +2,13 @@
use std::thread::JoinHandle;
use eframe::egui::epaint::Shadow;
use eframe::egui::{self, Color32, Pos2, Rect, Response, Stroke};
use eframe::egui::{self, Color32, Pos2, Rect, Response, Stroke, Vec2};
use eyre::Result;
use rand::seq::SliceRandom;
use super::super::platform::platform_colors;
use super::super::widgets::background::{shadow_background, AnimatedBackground};
use super::Textures;
use super::{StateUIAction, StateUIVariant};
use crate::types::Config;
use crate::{
@ -41,6 +42,10 @@ pub struct ImporterUI {
pub done_importing: bool,
/// Any errors during importing
pub importer_error: Option<eyre::Report>,
/// On macOS, we lack the permission to the mail folder. This can be
/// fixed in preferences. We don't `cfg(...)` this to simplify the implementation
/// with less `cfg(...)`
missing_permissions: bool,
}
impl ImporterUI {
@ -91,15 +96,16 @@ impl ImporterUI {
progress_divisions,
done_importing: false,
importer_error: None,
missing_permissions: false,
})
}
}
impl StateUIVariant for ImporterUI {
fn update_panel(&mut self, ctx: &egui::CtxRef) -> StateUIAction {
fn update_panel(&mut self, ctx: &egui::CtxRef, textures: &Option<Textures>) -> StateUIAction {
egui::CentralPanel::default()
.frame(egui::containers::Frame::none())
.show(ctx, |ui| {
ui.add(|ui: &mut egui::Ui| self.ui(ui));
ui.add(|ui: &mut egui::Ui| self.ui(ui, textures));
});
// If we generated an action above, return it
match (self.importer_error.take(), self.done_importing) {
@ -116,7 +122,7 @@ impl StateUIVariant for ImporterUI {
}
impl ImporterUI {
fn ui(&mut self, ui: &mut egui::Ui) -> Response {
fn ui(&mut self, ui: &mut egui::Ui, textures: &Option<Textures>) -> Response {
// The speed with which we initially scale down.
self.intro_timer += (ui.input().unstable_dt as f64) * 2.0;
let growth = self.intro_timer.clamp(0.0, 1.0);
@ -124,7 +130,21 @@ impl ImporterUI {
let available = ui.available_size();
let (label, progress, writing, done) = match self.handle_adapter() {
Ok(n) => n,
Ok(state) => {
#[cfg(target_os = "macos")]
if state.missing_permissions {
self.missing_permissions = true;
}
let InternalAdapterState {
label,
progress,
writing,
done,
..
} = state;
(label, progress, writing, done)
}
Err(e) => {
// Generate a response signifying we're done - as there was an error
let response = (format!("Error {}", &e), 1.0, false, true);
@ -137,7 +157,7 @@ impl ImporterUI {
self.importer_error = Some(error);
}
if done {
if done && !self.missing_permissions {
// if we're done, the join handle should not lock
if let Some(handle) = self.handle.take() {
self.importer_error = handle.join().ok().map(|e| e.err()).flatten();
@ -149,13 +169,15 @@ impl ImporterUI {
let n = n.min(self.progress_blocks.len());
let slice = &self.progress_blocks[0..n];
AnimatedBackground {
divisions: self.animation_divisions,
animate_progress: Some((slice, self.progress_divisions)),
timer: &mut self.timer,
offset_counter: &mut self.offset_counter,
if !self.missing_permissions {
AnimatedBackground {
divisions: self.animation_divisions,
animate_progress: Some((slice, self.progress_divisions)),
timer: &mut self.timer,
offset_counter: &mut self.offset_counter,
}
.draw_background(ui, available);
}
.draw_background(ui, available);
let desired_height = 370.0 - (220.0 * growth) as f32;
let desired_size = egui::vec2(330.0, desired_height);
@ -174,41 +196,93 @@ impl ImporterUI {
let colors = platform_colors();
// Draw a backround with a shadow
shadow_background(
ui.painter(),
paint_rect,
colors.window_background_dark,
Stroke::new(1.0, Color32::from_gray(90)),
12.0,
Shadow::big_dark(),
);
if self.missing_permissions {
self.permission_ui(ui, textures)
} else {
shadow_background(
ui.painter(),
paint_rect,
colors.window_background_dark,
Stroke::new(1.0, Color32::from_gray(90)),
12.0,
Shadow::big_dark(),
);
ui.allocate_ui_at_rect(center, |ui| {
ui.centered_and_justified(|ui| {
ui.vertical_centered_justified(|ui| {
ui.heading("Import in Progress");
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);
ui.allocate_ui_at_rect(center, |ui| {
ui.centered_and_justified(|ui| {
if self.missing_permissions {
} else {
let bar = egui::widgets::ProgressBar::new(progress).animate(true);
ui.add(bar);
ui.add_space(20.0);
self.default_ui(ui, writing, progress, label);
}
ui.small(label);
});
})
})
.response
}
}
fn default_ui(
&mut self,
ui: &mut egui::Ui,
writing: bool,
progress: f32,
label: String,
) -> Response {
ui.vertical_centered_justified(|ui| {
ui.heading("Import in Progress");
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
}
#[cfg(target_os = "macos")]
fn permission_ui(&mut self, ui: &mut egui::Ui, textures: &Option<Textures>) -> Response {
let available = ui.available_size();
ui.vertical_centered_justified(|ui| {
ui.set_width(available.x - 50.0);
ui.add_space(25.0);
if let Some(textures) = textures {
let s = textures.missing_permissions_image.0;
let s = Vec2::new(s.x / 4.5, s.y / 4.5);
ui.image(textures.missing_permissions_image.1, s);
}
ui.heading("Missing Mail Permissions");
ui.add_space(10.0);
ui.label("You need to give `Postsack` Full Disk Access permissions so that it can access your mails.");
ui.label("You can do this in the System Preferences. See the following Screenshot.");
ui.add_space(10.0);
ui.label("Afterwards, restart Postsack");
ui.add_space(5.0);
if ui.add_sized((100.0, 30.0), egui::Button::new("Quit")).clicked() {
std::process::exit(0);
}
}).response
}
}
struct InternalAdapterState {
label: String,
progress: f32,
writing: bool,
done: bool,
#[cfg(target_os = "macos")]
missing_permissions: bool,
}
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)> {
/// Returns the current the adapter state.
fn handle_adapter(&mut self) -> Result<InternalAdapterState> {
let (mut label, progress, writing) = {
let write = self.adapter.write_count()?;
if write.count > 0 {
@ -227,10 +301,24 @@ impl ImporterUI {
}
};
let State { done, finishing } = self.adapter.finished()?;
#[cfg(target_os = "macos")]
let State {
done,
finishing,
#[cfg(target_os = "macos")]
missing_permissions,
} = self.adapter.finished()?;
if finishing {
label = format!("Finishing Up");
}
Ok((label, progress, writing, done))
Ok(InternalAdapterState {
label,
progress,
writing,
done,
#[cfg(target_os = "macos")]
missing_permissions,
})
}
}

@ -2,6 +2,7 @@ use eframe::egui::{self, Stroke};
use eyre::{Report, Result};
use super::super::widgets::{FilterState, Spinner};
use super::Textures;
use super::{StateUIAction, StateUIVariant};
use crate::types::Config;
@ -38,7 +39,11 @@ impl MainUI {
}
impl StateUIVariant for MainUI {
fn update_panel(&mut self, ctx: &egui::CtxRef) -> super::StateUIAction {
fn update_panel(
&mut self,
ctx: &egui::CtxRef,
_textures: &Option<Textures>,
) -> super::StateUIAction {
// Avoid any processing if there is an unhandled error.
if self.error.is_none() {
self.error = self.engine.process().err();

@ -5,6 +5,7 @@ mod startup;
use std::path::PathBuf;
pub use super::textures::Textures;
use eframe::egui::{self};
pub use error::ErrorUI;
use eyre::Report;
@ -45,7 +46,7 @@ pub enum StateUI {
}
pub trait StateUIVariant {
fn update_panel(&mut self, ctx: &egui::CtxRef) -> StateUIAction;
fn update_panel(&mut self, ctx: &egui::CtxRef, textures: &Option<Textures>) -> StateUIAction;
}
impl StateUI {
@ -56,12 +57,12 @@ impl StateUI {
/// This proxies the `update` call to the individual calls in
/// the `app_state` types
pub fn update(&mut self, ctx: &egui::CtxRef) {
pub fn update(&mut self, ctx: &egui::CtxRef, textures: &Option<Textures>) {
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),
StateUI::Startup(panel) => panel.update_panel(ctx, textures),
StateUI::Import(panel) => panel.update_panel(ctx, textures),
StateUI::Main(panel) => panel.update_panel(ctx, textures),
StateUI::Error(panel) => panel.update_panel(ctx, textures),
};
match response {
StateUIAction::CreateDatabase {
@ -112,6 +113,19 @@ impl StateUI {
}
};
self.importer_with_config(config)
}
pub fn open_database(&mut self, database_path: PathBuf) -> StateUI {
let config = match crate::database::Database::config(&database_path) {
Ok(config) => config,
Err(report) => return StateUI::Error(error::ErrorUI::new(report, None)),
};
self.importer_with_config(config)
}
fn importer_with_config(&self, config: Config) -> StateUI {
let importer = match import::ImporterUI::new(config.clone()) {
Ok(n) => n,
Err(e) => {
@ -121,10 +135,4 @@ impl StateUI {
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!()
}
}

@ -7,6 +7,7 @@ use std::path::PathBuf;
use super::super::platform::platform_colors;
use super::super::widgets::background::{shadow_background, AnimatedBackground};
use super::Textures;
use super::{StateUIAction, StateUIVariant};
use crate::types::{Config, FormatType};
@ -59,7 +60,11 @@ impl StartupUI {
}
impl StateUIVariant for StartupUI {
fn update_panel(&mut self, ctx: &egui::CtxRef) -> super::StateUIAction {
fn update_panel(
&mut self,
ctx: &egui::CtxRef,
_textures: &Option<Textures>,
) -> super::StateUIAction {
egui::CentralPanel::default()
.frame(egui::containers::Frame::none())
.show(ctx, |ui| {
@ -193,7 +198,7 @@ impl StartupUI {
if self.save_to_disk {
ui.horizontal(|ui| {
if ui.button("Output Location").clicked() {
self.open_database_dialog()
self.save_database_dialog()
}
if let Some(Some(Some(name))) = self.database_path.as_ref().map(|e| e.file_name().map(|e| e.to_str().map(|e| e.to_string()))) {
let label = egui::widgets::Label::new(name)
@ -253,7 +258,6 @@ impl StartupUI {
.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"));
@ -274,8 +278,13 @@ impl StartupUI {
}
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...
let path = match self.open_database_dialog() {
Some(n) => n,
None => return,
};
self.action = Some(StateUIAction::OpenDatabase {
database_path: path,
});
}
fn format_selection(&mut self, ui: &mut egui::Ui, width: f32) {
@ -310,7 +319,7 @@ impl StartupUI {
self.email_folder = Some(path);
}
fn open_database_dialog(&mut self) {
fn save_database_dialog(&mut self) {
let default_path = "~/Desktop/";
// FIXME: Not sure if this works
@ -326,14 +335,27 @@ impl StartupUI {
Some(path) => path,
None => return,
};
self.database_path = Some(path);
self.database_path = Some(path)
}
// fn set_default_folder(&self) -> PathBuf {
// let path = self.format.default_path() {
// Some(n) => n,
// }
fn open_database_dialog(&mut self) -> Option<PathBuf> {
let default_path = "~/Desktop/";
// FIXME: Not sure if this works
#[cfg(target_os = "windows")]
let default_path = "C:\\Users";
let filename = rfd::FileDialog::new()
.add_filter("sqlite", &["sqlite"])
.set_directory(default_path)
.pick_file();
// self.email_folder = self.format.default_path().map(|e| e.to_path_buf())
// }
let path = match filename {
Some(path) => path,
None => return None,
};
Some(path)
}
}

@ -4,6 +4,7 @@ mod mail_panel;
mod navigation_bar;
mod platform;
mod segmentation_bar;
mod textures;
pub(crate) mod widgets;
pub fn run_gui() {

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

@ -0,0 +1,59 @@
//! Image handling in egui is not particularly nice. We can only load them
//! when we have access to the `Frame` - so not deeper in the ui.
//! And then, loaded images can only be referenced by the texture id, not their
//! name. So loading images in `setup` and accessing them later only works if
//! the texture-id is accessible later on, so we need to forward the info.
//! This is complicated by the fact that the image in question is only
//! necessary on macOS and I'd rather not load it on Windows or Linux systems.
use eframe::{self, egui, epi};
use image;
/// Pre-loaded textures
pub struct Textures {
#[cfg(target_os = "macos")]
pub missing_permissions_image: (eframe::egui::Vec2, eframe::egui::TextureId),
}
impl Textures {
pub fn populated(frame: &mut epi::Frame<'_>) -> Textures {
#[cfg(target_os = "macos")]
{
let missing_permissions_image = install_missing_permission_image(
include_bytes!("resources/add_permissions.png"),
frame,
);
Textures {
missing_permissions_image,
}
}
#[cfg(not(target_os = "macos"))]
Textures {}
}
}
/// Load the permission image
// via: https://github.com/emilk/egui/blob/master/eframe/examples/image.rs
fn install_missing_permission_image(
image_data: &[u8],
frame: &mut epi::Frame<'_>,
) -> (egui::Vec2, egui::TextureId) {
use image::GenericImageView;
let image = image::load_from_memory(image_data).expect("Failed to load image");
let image_buffer = image.to_rgba8();
let size = (image.width() as usize, image.height() as usize);
let pixels = image_buffer.into_vec();
assert_eq!(size.0 * size.1 * 4, pixels.len());
let pixels: Vec<_> = pixels
.chunks_exact(4)
.map(|p| egui::Color32::from_rgba_unmultiplied(p[0], p[1], p[2], p[3]))
.collect();
// Allocate a texture:
let texture = frame
.tex_allocator()
.alloc_srgba_premultiplied(size, &pixels);
let size = egui::Vec2::new(size.0 as f32, size.1 as f32);
(size, texture)
}

@ -2,7 +2,7 @@
//! recursively drill down into the appropriate folder
//! until we find `emlx` files and return those.
use eyre::Result;
use eyre::{eyre, Result};
use rayon::prelude::*;
use walkdir::WalkDir;
@ -14,6 +14,28 @@ use super::mail::Mail;
use std::path::PathBuf;
pub fn read_emails(config: &Config, sender: MessageSender) -> Result<Vec<Mail>> {
// on macOS, we might need permission for the `Library` folder...
match std::fs::read_dir(&config.emails_folder_path) {
Ok(_) => (),
Err(e) => match e.kind() {
#[cfg(target_os = "macos")]
std::io::ErrorKind::PermissionDenied => {
tracing::info!("Could not read folder: {}", e);
if let Err(e) = sender.send(Message::MissingPermissions) {
tracing::error!("Error sending: {}", e);
}
// We should return early now, otherwise the code below will send a different
// error
return Ok(Vec::new());
}
_ => {
if let Err(e) = sender.send(Message::Error(eyre!("Error: {:?}", &e))) {
tracing::error!("Error sending: {}", e);
}
}
},
}
// As `walkdir` does not support `par_iter` (see https://www.reddit.com/r/rust/comments/6eif7r/walkdir_users_we_need_you/)
// - -we first collect all folders,
// then all sub-folders in those ending in mboxending in .mbox and then iterate over them in paralell
@ -21,14 +43,23 @@ pub fn read_emails(config: &Config, sender: MessageSender) -> Result<Vec<Mail>>
.into_iter()
.filter_map(|e| match e {
Ok(n)
if n.path().is_dir()
if dbg!(n.path()).is_dir()
&& n.path()
.to_str()
.map(|e| e.contains(".mbox"))
.unwrap_or(false) =>
{
tracing::trace!("Found folder {}", n.path().display());
Some(n.path().to_path_buf())
}
Err(e) => {
tracing::info!("Could not read folder: {}", e);
if let Err(e) = sender.send(Message::Error(eyre!("Could not read folder: {:?}", e)))
{
tracing::error!("Error sending error {}", e);
}
None
}
_ => None,
})
.collect();
@ -40,6 +71,13 @@ pub fn read_emails(config: &Config, sender: MessageSender) -> Result<Vec<Mail>>
Ok(n) => Some(n),
Err(e) => {
tracing::error!("{} {:?}", path.display(), &e);
if let Err(e) = sender.send(Message::Error(eyre!(
"Could read mails in {}: {:?}",
path.clone().display(),
e
))) {
tracing::error!("Error sending error {}", e);
}
None
}
},

@ -23,6 +23,11 @@ pub fn into_database<Mail: ParseableEmail + 'static>(
// Create a new database connection, just for writing
let database = Database::new(config.database_path.clone()).unwrap();
// Save the config into the database
if let Err(e) = database.save_config(config.clone()) {
bail!("Could not save config to database {:?}", &e);
}
// 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.
let (sender, handle) = database.import();

@ -37,6 +37,7 @@ impl<Format: ImporterFormat + 'static> Importerlike for Importer<Format> {
Ok(processed)
};
let result = processed();
// Send the error away and map it to a crossbeam channel error
match result {
Ok(_) => Ok(()),

@ -16,6 +16,8 @@ struct Data {
finishing: bool,
done: bool,
error: Option<Report>,
#[cfg(target_os = "macos")]
missing_permissions: bool,
}
#[derive(Clone, Debug, Copy)]
@ -28,6 +30,8 @@ pub struct Progress {
pub struct State {
pub finishing: bool,
pub done: bool,
#[cfg(target_os = "macos")]
pub missing_permissions: bool,
}
/// This can be initialized with a [`MessageSender`] and it will
@ -89,6 +93,10 @@ impl Adapter {
Message::Error(e) => {
write_guard.error = Some(e);
}
#[cfg(target_os = "macos")]
Message::MissingPermissions => {
write_guard.missing_permissions = true;
}
};
}
}
@ -121,6 +129,8 @@ impl Adapter {
Ok(State {
finishing: item.finishing,
done: item.done,
#[cfg(target_os = "macos")]
missing_permissions: item.missing_permissions,
})
}

@ -34,6 +34,10 @@ pub enum Message {
Done,
/// An error happened during processing
Error(eyre::Report),
/// A special case for macOS, where a permission error means we have to grant this app
/// the right to see the mail folder
#[cfg(target_os = "macos")]
MissingPermissions,
}
pub type MessageSender = crossbeam_channel::Sender<Message>;

@ -2,7 +2,8 @@
#[macro_use]
extern crate objc;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::fmt;
use tracing_subscriber::prelude::*;
pub mod database;
#[cfg(feature = "gui")]
@ -15,9 +16,10 @@ pub fn setup_tracing() {
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "error")
}
tracing_subscriber::fmt::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
let collector = tracing_subscriber::registry().with(fmt::layer().with_writer(std::io::stdout));
tracing::subscriber::set_global_default(collector).expect("Unable to set a global collector");
}
/// Create a config for the `cli` and validate the input

@ -1,10 +1,13 @@
use eyre::{eyre, Result};
use rand::Rng;
use serde_json::Value;
use strum::{self, IntoEnumIterator};
use strum_macros::{EnumIter, IntoStaticStr};
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::iter::FromIterator;
use std::path::{Path, PathBuf};
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoStaticStr, EnumIter)]
pub enum FormatType {
@ -51,7 +54,13 @@ impl Default for FormatType {
impl From<&String> for FormatType {
fn from(format: &String) -> Self {
match format.as_str() {
FormatType::from(format.as_str())
}
}
impl From<&str> for FormatType {
fn from(format: &str) -> Self {
match format {
"apple" => FormatType::AppleMail,
"gmailvault" => FormatType::GmailVault,
"mbox" => FormatType::Mbox,
@ -60,6 +69,16 @@ impl From<&String> for FormatType {
}
}
impl From<FormatType> for String {
fn from(format: FormatType) -> Self {
match format {
FormatType::AppleMail => "apple".to_owned(),
FormatType::GmailVault => "gmailvault".to_owned(),
FormatType::Mbox => "mbox".to_owned(),
}
}
}
#[derive(Debug, Clone)]
pub struct Config {
/// The path to where the database should be stored
@ -76,6 +95,51 @@ pub struct Config {
}
impl Config {
/// Construct a config from a hashmap of field values.
/// For missing fields, take a reasonable default value,
/// in order to be somewhat backwards compatible.
pub fn from_fields<P: AsRef<Path>>(path: P, fields: HashMap<String, Value>) -> Result<Config> {
// The following fields are of version 1.0, so they should aways exist
let emails_folder_path_str = fields
.get("emails_folder_path")
.ok_or(eyre!("Missing config field emails_folder_path"))?
.as_str()
.ok_or(eyre!("Invalid field type for emails_folder_path"))?;
let emails_folder_path = PathBuf::from_str(emails_folder_path_str).map_err(|e| {
eyre!(
"Invalid emails_folder_path: {}: {}",
&emails_folder_path_str,
e
)
})?;
let sender_emails: Vec<String> = fields
.get("sender_emails")
.map(|v| v.as_str().map(|e| e.to_string()))
.flatten()
.ok_or(eyre!("Missing config field sender_emails"))?
.split(",")
.map(|e| e.trim().to_owned())
.collect();
let format = fields
.get("format")
.map(|e| e.as_str())
.flatten()
.map(|e| FormatType::from(e))
.ok_or(eyre!("Missing config field format_type"))?;
let persistent = fields
.get("persistent")
.map(|e| e.as_bool())
.flatten()
.ok_or(eyre!("Missing config field persistent"))?;
Ok(Config {
database_path: path.as_ref().to_path_buf(),
emails_folder_path,
sender_emails: HashSet::from_iter(sender_emails.into_iter()),
format,
persistent,
})
}
pub fn new<A: AsRef<Path>>(
db: Option<A>,
mails: A,
@ -106,4 +170,29 @@ impl Config {
persistent,
})
}
pub fn into_fields(&self) -> Option<HashMap<String, Value>> {
let mut new = HashMap::new();
new.insert(
"database_path".to_owned(),
self.database_path.to_str()?.into(),
);
new.insert(
"emails_folder_path".to_owned(),
self.emails_folder_path.to_str()?.into(),
);
new.insert(
"sender_emails".to_owned(),
self.sender_emails
.iter()
.map(|e| e.clone())
.collect::<Vec<String>>()
.join(",")
.into(),
);
let format: String = self.format.into();
new.insert("format".to_owned(), format.into());
Some(new)
}
}

Loading…
Cancel
Save