From 3af6f338ce41fb7b7e0bbbc9d17379f852b714de Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Mon, 7 Oct 2019 16:42:44 +0300 Subject: [PATCH] add sqlite3 feature WIP --- Cargo.lock | 53 ++++ melib/src/email.rs | 4 + ui/Cargo.toml | 4 +- ui/src/components/mail/listing/compact.rs | 108 +++----- .../components/mail/listing/conversations.rs | 259 ++++-------------- ui/src/conf/accounts.rs | 24 ++ ui/src/lib.rs | 5 + ui/src/search.rs | 61 +++++ ui/src/sqlite3.rs | 194 +++++++++++++ ui/src/state.rs | 8 + 10 files changed, 446 insertions(+), 274 deletions(-) create mode 100644 ui/src/search.rs create mode 100644 ui/src/sqlite3.rs diff --git a/Cargo.lock b/Cargo.lock index e73e33c8b..9d7d923cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -309,6 +309,16 @@ dependencies = [ "synstructure 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "filetime" version = "0.2.5" @@ -458,6 +468,20 @@ dependencies = [ "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "libsqlite3-sys" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "linkify" version = "0.3.1" @@ -474,6 +498,14 @@ dependencies = [ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "mac-notification-sys" version = "0.3.0" @@ -974,6 +1006,20 @@ dependencies = [ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rusqlite" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "fallible-iterator 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "fallible-streaming-iterator 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", + "libsqlite3-sys 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rustc-demangle" version = "0.1.15" @@ -1197,6 +1243,7 @@ dependencies = [ "nom 3.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "notify 4.0.12 (registry+https://github.com/rust-lang/crates.io-index)", "notify-rust 3.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rusqlite 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.92 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1336,6 +1383,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" "checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" "checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1" +"checksum fallible-iterator 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +"checksum fallible-streaming-iterator 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" "checksum filetime 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "2f8c63033fcba1f51ef744505b3cad42510432b904c062afa67ad7ece008429d" "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" "checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" @@ -1356,8 +1405,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum lazycell 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f" "checksum libc 0.2.64 (registry+https://github.com/rust-lang/crates.io-index)" = "74dfca3d9957906e8d1e6a0b641dc9a59848e793f1da2165889fd4f62d10d79c" "checksum libdbus-sys 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "18cb88963258d00f4962205dbb5933d82780d9962c8c8a064b651d2ad7189210" +"checksum libsqlite3-sys 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5e5b95e89c330291768dc840238db7f9e204fd208511ab6319b56193a7f2ae25" +"checksum linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83" "checksum linkify 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9ce9439c6f4a1092dc1861272bef01034891da39f13aa1cdcf40ca3e4081de5f" "checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6" +"checksum lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" "checksum mac-notification-sys 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3dfb6b71a9a89cd38b395d994214297447e8e63b1ba5708a9a2b0b1048ceda76" "checksum malloc_buf 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" "checksum memchr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" @@ -1408,6 +1460,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" "checksum redox_users 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3fe5204c3a17e97dde73f285d49be585df59ed84b50a872baf416e73b62c3828" "checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" +"checksum rusqlite 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2a194373ef527035645a1bc21b10dc2125f73497e6e155771233eb187aedd051" "checksum rustc-demangle 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "a7f4dccf6f4891ebcc0c39f9b6eb1a83b9bf5d747cb439ec6fba4f3b977038af" "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" "checksum ryu 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "b96a9549dc8d48f2c283938303c4b5a77aa29bfbc5b54b084fb1630408899a8f" diff --git a/melib/src/email.rs b/melib/src/email.rs index 39a4439e3..b6043f8fc 100644 --- a/melib/src/email.rs +++ b/melib/src/email.rs @@ -376,6 +376,10 @@ impl Envelope { let _strings: Vec = self.to.iter().map(|a| format!("{}", a)).collect(); _strings.join(", ") } + pub fn field_references_to_string(&self) -> String { + let _strings: Vec = self.references().iter().map(|a| a.to_string()).collect(); + _strings.join(", ") + } /// Requests bytes from backend and thus can fail pub fn bytes(&self, mut operation: Box) -> Result> { diff --git a/ui/Cargo.toml b/ui/Cargo.toml index 307df1bc3..746922be0 100644 --- a/ui/Cargo.toml +++ b/ui/Cargo.toml @@ -26,9 +26,11 @@ unicode-segmentation = "1.2.1" # >:c text_processing = { path = "../text_processing", version = "*" } libc = {version = "0.2.59", features = ["extra_traits",]} nix = "0.15.0" +rusqlite = {version = "0.20.0", optional =true } [features] -default = [] +default = ["sqlite3"] +sqlite3 = ["rusqlite"] # Print tracing logs as meli runs debug-tracing = [] diff --git a/ui/src/components/mail/listing/compact.rs b/ui/src/components/mail/listing/compact.rs index 8936efed0..6f79c5897 100644 --- a/ui/src/components/mail/listing/compact.rs +++ b/ui/src/components/mail/listing/compact.rs @@ -343,6 +343,7 @@ impl ListingTrait for CompactListing { } context.dirty_areas.push_back(area); } + fn filter(&mut self, filter_term: &str, context: &Context) { self.filtered_order.clear(); self.filtered_selection.clear(); @@ -351,84 +352,57 @@ impl ListingTrait for CompactListing { } self.filter_term.clear(); - let mut error: Option<(EnvelopeHash, MeliError)> = None; - for (i, h) in self.order.keys().enumerate() { - let account = &context.accounts[self.cursor_pos.0]; - let folder_hash = account[self.cursor_pos.1].unwrap().folder.hash(); - let threads = &account.collection.threads[&folder_hash]; - let env_hash = { - let thread_node = &threads.thread_nodes()[h]; - if let Some(i) = thread_node.message() { - i - } else { - let mut iter_ptr = thread_node.children()[0]; - while threads.thread_nodes()[&iter_ptr].message().is_none() { - iter_ptr = threads.thread_nodes()[&iter_ptr].children()[0]; + let account = &context.accounts[self.cursor_pos.0]; + let folder_hash = account[self.cursor_pos.1].unwrap().folder.hash(); + match crate::search::filter(filter_term, context, self.cursor_pos.0, folder_hash) { + Ok(results) => { + let threads = &account.collection.threads[&folder_hash]; + for env_hash in results { + if !account.collection.envelopes.contains_key(&env_hash) { + continue; + } + let env_hash_thread_hash = account.get_env(&env_hash).thread(); + if !threads.thread_nodes.contains_key(&env_hash_thread_hash) { + continue; + } + let thread_group = melib::find_thread_group( + &threads.thread_nodes, + threads.thread_nodes[&env_hash_thread_hash].thread_group(), + ); + if let Some(row) = self.order.get(&thread_group) { + self.filtered_selection.push(thread_group); + self.filtered_order.insert(thread_group, *row); } - threads.thread_nodes()[&iter_ptr].message().unwrap() } - }; - let envelope = &account.collection[&env_hash]; - if envelope.subject().contains(&filter_term) { - self.filtered_selection.push(*h); - self.filtered_order.insert(*h, i); - self.selection.insert(*h, false); - continue; } - if envelope.field_from_to_string().contains(&filter_term) { - self.filtered_selection.push(*h); - self.filtered_order.insert(*h, i); - self.selection.insert(*h, false); - continue; - } - let op = account.operation(env_hash); - let body = match envelope.body(op) { - Ok(b) => b, - Err(e) => { - error = Some((env_hash, e)); - break; - } - }; - let decoded = decode_rec(&body, None); - let body_text = String::from_utf8_lossy(&decoded); - if body_text.contains(&filter_term) { - self.filtered_selection.push(*h); - self.filtered_order.insert(*h, i); - self.selection.insert(*h, false); + Err(e) => { + self.length = 0; + let message = format!("Error: {}", e); + log( + format!("Failed to search for term {}: {}", filter_term, e,), + ERROR, + ); + self.data_columns.columns[0] = + CellBuffer::new_with_context(message.len(), 1, Cell::with_char(' '), context); + write_string_to_grid( + &message, + &mut self.data_columns.columns[0], + Color::Default, + Color::Default, + Attr::Default, + ((0, 0), (message.len() - 1, 0)), + false, + ); } } - if let Some((env_hash, error)) = error { - self.length = 0; - let message = format!("Error: {}", error.to_string()); - log( - format!( - "Failed to open envelope {}: {}", - context.accounts[self.new_cursor_pos.0] - .get_env(&env_hash) - .message_id_display(), - error.to_string() - ), - ERROR, - ); - self.data_columns.columns[0] = - CellBuffer::new_with_context(message.len(), 1, Cell::with_char(' '), context); - write_string_to_grid( - &message, - &mut self.data_columns.columns[0], - Color::Default, - Color::Default, - Attr::Default, - ((0, 0), (message.len() - 1, 0)), - false, - ); - } else if !self.filtered_selection.is_empty() { + if !self.filtered_selection.is_empty() { self.filter_term = filter_term.to_string(); self.cursor_pos.2 = std::cmp::min(self.filtered_selection.len() - 1, self.cursor_pos.2); self.length = self.filtered_selection.len(); } else { self.length = 0; - let message = format!("No results for `{}`.", filter_term); + let message = format!("No results for `{}`. Press ESC to go back.", filter_term); self.data_columns.columns[0] = CellBuffer::new_with_context(message.len(), 1, Cell::with_char(' '), context); write_string_to_grid( diff --git a/ui/src/components/mail/listing/conversations.rs b/ui/src/components/mail/listing/conversations.rs index 2dc370699..9e0881e5a 100644 --- a/ui/src/components/mail/listing/conversations.rs +++ b/ui/src/components/mail/listing/conversations.rs @@ -403,67 +403,51 @@ impl ListingTrait for ConversationsListing { } self.filter_term.clear(); - let mut error = None; - for (i, h) in self.order.keys().enumerate() { - let account = &context.accounts[self.cursor_pos.0]; - let folder_hash = account[self.cursor_pos.1].unwrap().folder.hash(); - let threads = &account.collection.threads[&folder_hash]; - let env_hash = { - let thread_node = &threads.thread_nodes()[h]; - if let Some(i) = thread_node.message() { - i - } else { - let mut iter_ptr = thread_node.children()[0]; - while threads.thread_nodes()[&iter_ptr].message().is_none() { - iter_ptr = threads.thread_nodes()[&iter_ptr].children()[0]; + let account = &context.accounts[self.cursor_pos.0]; + let folder_hash = account[self.cursor_pos.1].unwrap().folder.hash(); + match crate::search::filter(filter_term, context, self.cursor_pos.0, folder_hash) { + Ok(results) => { + let threads = &account.collection.threads[&folder_hash]; + for env_hash in results { + if !account.collection.envelopes.contains_key(&env_hash) { + continue; + } + let env_hash_thread_hash = account.get_env(&env_hash).thread(); + if !threads.thread_nodes.contains_key(&env_hash_thread_hash) { + continue; + } + let thread_group = melib::find_thread_group( + &threads.thread_nodes, + threads.thread_nodes[&env_hash_thread_hash].thread_group(), + ); + if let Some(row) = self.order.get(&thread_group) { + self.filtered_selection.push(thread_group); + self.filtered_order.insert(thread_group, *row); } - threads.thread_nodes()[&iter_ptr].message().unwrap() } - }; - let envelope = &account.collection[&env_hash]; - if envelope.subject().contains(&filter_term) { - self.filtered_selection.push(*h); - self.filtered_order.insert(*h, i); - self.selection.insert(*h, false); - continue; } - if envelope.field_from_to_string().contains(&filter_term) { - self.filtered_selection.push(*h); - self.filtered_order.insert(*h, i); - self.selection.insert(*h, false); - continue; - } - let op = account.operation(env_hash); - let body = match envelope.body(op) { - Ok(b) => b, - Err(e) => { - error = Some(e); - break; - } - }; - let decoded = decode_rec(&body, None); - let body_text = String::from_utf8_lossy(&decoded); - if body_text.contains(&filter_term) { - self.filtered_selection.push(*h); - self.filtered_order.insert(*h, i); - self.selection.insert(*h, false); + Err(e) => { + self.length = 0; + let message = format!("No results for `{}`.", filter_term); + log( + format!("Failed to search for term {}: {}", filter_term, e,), + ERROR, + ); + self.content = + CellBuffer::new_with_context(message.len(), 1, Cell::with_char(' '), context); + write_string_to_grid( + &message, + &mut self.content, + Color::Default, + Color::Default, + Attr::Default, + ((0, 0), (message.len() - 1, 0)), + false, + ); } } - if let Some(error) = error { - self.length = 0; - let message = format!("Error: {}", error.to_string()); - self.content = - CellBuffer::new_with_context(message.len(), 1, Cell::with_char(' '), context); - write_string_to_grid( - &message, - &mut self.content, - Color::Default, - Color::Default, - Attr::Default, - ((0, 0), (message.len() - 1, 0)), - false, - ); - } else if !self.filtered_selection.is_empty() { + + if !self.filtered_selection.is_empty() { self.filter_term = filter_term.to_string(); self.cursor_pos.2 = std::cmp::min(self.filtered_selection.len() - 1, self.cursor_pos.2); self.length = self.filtered_selection.len(); @@ -608,6 +592,10 @@ impl ConversationsListing { self.view = ThreadView::new(self.new_cursor_pos, None, context); } + self.redraw_list(context); + } + + fn redraw_list(&mut self, context: &mut Context) { let account = &context.accounts[self.cursor_pos.0]; let mailbox = &account[self.cursor_pos.1].unwrap(); @@ -619,7 +607,14 @@ impl ConversationsListing { let mut max_entry_columns = 0; threads.sort_by(self.sort, self.subsort, &account.collection); - for (idx, root_idx) in threads.root_iter().enumerate() { + + let mut threads_iter = if self.filtered_selection.is_empty() { + Box::new(threads.root_iter()) as Box> + } else { + Box::new(self.filtered_selection.iter().map(|h| *h)) + as Box> + }; + for (idx, root_idx) in threads_iter.enumerate() { self.length += 1; let thread_node = &threads.thread_nodes()[&root_idx]; let i = if let Some(i) = thread_node.message() { @@ -805,153 +800,6 @@ impl ConversationsListing { } } - fn draw_filtered_selection(&mut self, _context: &mut Context) { - /* - if self.filtered_selection.is_empty() { - return; - } - let account = &context.accounts[self.cursor_pos.0]; - let mailbox = if account[self.cursor_pos.1].is_available() { - account[self.cursor_pos.1].unwrap() - } else { - return; - }; - - let threads = &account.collection.threads[&mailbox.folder.hash()]; - self.length = 0; - let mut rows = Vec::with_capacity(1024); - let mut min_width = (0, 0, 0, 0, 0); - - for (idx, envelope_hash) in self.filtered_selection.iter().enumerate() { - self.length += 1; - let envelope: &Envelope = &context.accounts[self.cursor_pos.0].get_env(&envelope_hash); - let t_idx = envelope.thread(); - let strings = ConversationsListing::make_entry_string( - envelope, - threads[&envelope.thread()].len(), - idx, - threads.is_snoozed(t_idx), - ); - min_width.0 = cmp::max(min_width.0, strings.0.grapheme_width()); /* index */ - min_width.1 = cmp::max(min_width.1, strings.1.grapheme_width()); /* date */ - min_width.2 = cmp::max(min_width.2, strings.2.grapheme_width()); /* from */ - min_width.3 = cmp::max(min_width.3, strings.3.grapheme_width()); /* flags */ - min_width.4 = cmp::max(min_width.4, strings.4.grapheme_width()); /* subject */ - rows.push(strings); - } - - /* index column */ - self.data_columns.columns[0] = - CellBuffer::new(min_width.0, rows.len(), Cell::with_char(' ')); - /* date column */ - self.data_columns.columns[1] = - CellBuffer::new(min_width.1, rows.len(), Cell::with_char(' ')); - /* from column */ - self.data_columns.columns[2] = - CellBuffer::new(min_width.2, rows.len(), Cell::with_char(' ')); - /* flags column */ - self.data_columns.columns[3] = - CellBuffer::new(min_width.3, rows.len(), Cell::with_char(' ')); - /* subject column */ - self.data_columns.columns[4] = - CellBuffer::new(min_width.4, rows.len(), Cell::with_char(' ')); - - for ((idx, envelope_hash), strings) in self.filtered_selection.iter().enumerate().zip(rows) - { - let envelope: &Envelope = &context.accounts[self.cursor_pos.0].get_env(&envelope_hash); - let fg_color = if !envelope.is_seen() { - Color::Byte(0) - } else { - Color::Default - }; - let bg_color = if self.selection[&envelope_hash] { - Color::Byte(210) - } else if !envelope.is_seen() { - Color::Byte(251) - } else { - Color::Default - }; - let (x, _) = write_string_to_grid( - &strings.0, - &mut self.data_columns.columns[0], - fg_color, - bg_color, - Attr::Default, - ((0, idx), (min_width.0, idx)), - false, - ); - for x in x..min_width.0 { - self.data_columns.columns[0][(x, idx)].set_bg(bg_color); - } - let (x, _) = write_string_to_grid( - &strings.1, - &mut self.data_columns.columns[1], - fg_color, - bg_color, - Attr::Default, - ((0, idx), (min_width.1, idx)), - false, - ); - for x in x..min_width.1 { - self.data_columns.columns[1][(x, idx)].set_bg(bg_color); - } - let (x, _) = write_string_to_grid( - &strings.2, - &mut self.data_columns.columns[2], - fg_color, - bg_color, - Attr::Default, - ((0, idx), (min_width.2, idx)), - false, - ); - for x in x..min_width.2 { - self.data_columns.columns[2][(x, idx)].set_bg(bg_color); - } - let (x, _) = write_string_to_grid( - &strings.3, - &mut self.data_columns.columns[3], - fg_color, - bg_color, - Attr::Default, - ((0, idx), (min_width.3, idx)), - false, - ); - for x in x..min_width.3 { - self.data_columns.columns[3][(x, idx)].set_bg(bg_color); - } - let (x, _) = write_string_to_grid( - &strings.4, - &mut self.data_columns.columns[4], - fg_color, - bg_color, - Attr::Default, - ((0, idx), (min_width.4, idx)), - false, - ); - for x in x..min_width.4 { - self.data_columns.columns[4][(x, idx)].set_bg(bg_color); - } - match ( - threads.is_snoozed(envelope.thread()), - &context.accounts[self.cursor_pos.0] - .get_env(&envelope_hash) - .has_attachments(), - ) { - (true, true) => { - self.data_columns.columns[3][(0, idx)].set_fg(Color::Byte(103)); - self.data_columns.columns[3][(2, idx)].set_fg(Color::Red); - } - (true, false) => { - self.data_columns.columns[3][(0, idx)].set_fg(Color::Red); - } - (false, true) => { - self.data_columns.columns[3][(0, idx)].set_fg(Color::Byte(103)); - } - (false, false) => {} - } - } - */ - } fn get_thread_under_cursor(&self, cursor: usize, context: &Context) -> ThreadHash { let account = &context.accounts[self.cursor_pos.0]; let folder_hash = account[self.cursor_pos.1].unwrap().folder.hash(); @@ -1057,7 +905,7 @@ impl Component for ConversationsListing { }; if !self.filtered_selection.is_empty() { - self.draw_filtered_selection(context); + self.redraw_list(context); let (x, y) = write_string_to_grid( &format!("Filter (Press ESC to exit): {}", self.filter_term), grid, @@ -1074,7 +922,6 @@ impl Component for ConversationsListing { self.draw_list(grid, (set_y(upper_left, y + 1), bottom_right), context); self.dirty = false; - return; } if !self.row_updates.is_empty() { /* certain rows need to be updated (eg an unseen message was just set seen) diff --git a/ui/src/conf/accounts.rs b/ui/src/conf/accounts.rs index 425a38100..e4e560b99 100644 --- a/ui/src/conf/accounts.rs +++ b/ui/src/conf/accounts.rs @@ -98,6 +98,29 @@ impl MailboxEntry { false } } + + pub fn as_result(&self) -> Result<&Mailbox> { + match self { + MailboxEntry::Available(ref m) => Ok(m), + MailboxEntry::Parsing(ref m, _, _) => Ok(m), + MailboxEntry::Failed(ref e) => Err(MeliError::new(format!( + "Mailbox is not available: {}", + e.to_string() + ))), + } + } + + pub fn as_mut_result(&mut self) -> Result<&mut Mailbox> { + match self { + MailboxEntry::Available(ref mut m) => Ok(m), + MailboxEntry::Parsing(ref mut m, _, _) => Ok(m), + MailboxEntry::Failed(ref e) => Err(MeliError::new(format!( + "Mailbox is not available: {}", + e.to_string() + ))), + } + } + pub fn unwrap_mut(&mut self) -> &mut Mailbox { match self { MailboxEntry::Available(ref mut m) => m, @@ -105,6 +128,7 @@ impl MailboxEntry { e => panic!(format!("mailbox is not available! {:#}", e)), } } + pub fn unwrap(&self) -> &Mailbox { match self { MailboxEntry::Available(ref m) => m, diff --git a/ui/src/lib.rs b/ui/src/lib.rs index fdff540ec..a8a85e91d 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -69,6 +69,11 @@ pub use crate::conf::*; pub mod workers; pub use crate::workers::*; +#[cfg(feature = "sqlite3")] +pub mod sqlite3; + +pub mod search; + pub use crate::username::*; pub mod username { use libc; diff --git a/ui/src/search.rs b/ui/src/search.rs new file mode 100644 index 000000000..52d504196 --- /dev/null +++ b/ui/src/search.rs @@ -0,0 +1,61 @@ +/* + * meli - ui crate. + * + * Copyright 2017-2018 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +use crate::state::Context; +use melib::{backends::FolderHash, email::EnvelopeHash, error::Result, StackVec}; + +pub fn filter( + filter_term: &str, + context: &Context, + account_idx: usize, + folder_hash: FolderHash, +) -> Result> { + #[cfg(feature = "sqlite3")] + { + crate::sqlite3::search(filter_term, context, account_idx, folder_hash) + } + + #[cfg(not(feature = "sqlite3"))] + { + let mut ret = StackVec::new(); + + let account = &context.accounts[account_idx]; + for env_hash in account.folders[folder_hash].as_result()?.envelopes { + let envelope = &account.collection[&env_hash]; + if envelope.subject().contains(&filter_term) { + ret.push(env_hash); + continue; + } + if envelope.field_from_to_string().contains(&filter_term) { + ret.push(env_hash); + continue; + } + let op = account.operation(env_hash); + let body = envelope.body(op)?; + let decoded = decode_rec(&body, None); + let body_text = String::from_utf8_lossy(&decoded); + if body_text.contains(&filter_term) { + ret.push(env_hash); + } + } + ret + } +} diff --git a/ui/src/sqlite3.rs b/ui/src/sqlite3.rs new file mode 100644 index 000000000..e7ead33ae --- /dev/null +++ b/ui/src/sqlite3.rs @@ -0,0 +1,194 @@ +/* + * meli - sqlite3.rs + * + * Copyright 2019 Manos Pitsidianakis + * + * This file is part of meli. + * + * meli is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * meli is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with meli. If not, see . + */ + +use crate::state::Context; +use melib::{backends::FolderHash, email::EnvelopeHash, MeliError, Result, StackVec}; +use rusqlite::{params, Connection}; +use std::convert::TryInto; + +pub fn open_db(context: &crate::state::Context) -> Result { + let data_dir = + xdg::BaseDirectories::with_prefix("meli").map_err(|e| MeliError::new(e.to_string()))?; + let conn = Connection::open( + data_dir + .place_data_file("meli.db") + .map_err(|e| MeliError::new(e.to_string()))?, + ) + .map_err(|e| MeliError::new(e.to_string()))?; + //let conn = Connection::open_in_memory().map_err(|e| MeliError::new(e.to_string()))?; + + /* + * + pub struct Envelope { + date: String, + from: Vec
, + to: Vec
, + cc: Vec
, + bcc: Vec
, + subject: Option>, + message_id: MessageID, + in_reply_to: Option, + references: Option, + other_headers: FnvHashMap, + + timestamp: UnixTimestamp, + thread: ThreadHash, + + hash: EnvelopeHash, + + flags: Flag, + has_attachments: bool, + } + */ + + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS envelopes ( + id INTEGER PRIMARY KEY, + hash BLOB NOT NULL, + date TEXT NOT NULL, + _from TEXT NOT NULL, + _to TEXT NOT NULL, + cc TEXT NOT NULL, + bcc TEXT NOT NULL, + subject TEXT NOT NULL, + message_id TEXT NOT NULL, + in_reply_to TEXT NOT NULL, + _references TEXT NOT NULL, + flags INTEGER NOT NULL, + has_attachments BOOLEAN NOT NULL, + body_text TEXT NOT NULL, + timestamp BLOB NOT NULL + ); + + +CREATE INDEX IF NOT EXISTS envelope_timestamp_index ON envelopes (timestamp); +CREATE INDEX IF NOT EXISTS envelope__from_index ON envelopes (_from); +CREATE INDEX IF NOT EXISTS envelope__to_index ON envelopes (_to); +CREATE INDEX IF NOT EXISTS envelope_cc_index ON envelopes (cc); +CREATE INDEX IF NOT EXISTS envelope_bcc_index ON envelopes (bcc); +CREATE INDEX IF NOT EXISTS envelope_message_id_index ON envelopes (message_id); + + CREATE VIRTUAL TABLE IF NOT EXISTS fts USING fts5(subject, body_text, content=envelopes, content_rowid=id); + +-- Triggers to keep the FTS index up to date. +CREATE TRIGGER IF NOT EXISTS envelopes_ai AFTER INSERT ON envelopes BEGIN + INSERT INTO fts(rowid, subject, body_text) VALUES (new.id, new.subject, new.body_text); +END; + +CREATE TRIGGER IF NOT EXISTS envelopes_ad AFTER DELETE ON envelopes BEGIN + INSERT INTO fts(fts, rowid, subject, body_text) VALUES('delete', old.id, old.subject, old.body_text); +END; + +CREATE TRIGGER IF NOT EXISTS envelopes_au AFTER UPDATE ON envelopes BEGIN + INSERT INTO fts(fts, rowid, subject, body_text) VALUES('delete', old.id, old.subject, old.body_text); + INSERT INTO fts(rowid, subject, body_text) VALUES (new.id, new.subject, new.body_text); +END; ", + ) + .map_err(|e| MeliError::new(e.to_string()))?; + + Ok(conn) +} + +pub fn insert(context: &crate::state::Context) -> Result<()> { + let data_dir = + xdg::BaseDirectories::with_prefix("meli").map_err(|e| MeliError::new(e.to_string()))?; + let conn = Connection::open( + data_dir + .place_data_file("meli.db") + .map_err(|e| MeliError::new(e.to_string()))?, + ) + .map_err(|e| MeliError::new(e.to_string()))?; + for acc in context.accounts.iter() { + debug!("inserting {} envelopes", acc.collection.envelopes.len()); + for e in acc.collection.envelopes.values() { + conn.execute( + "INSERT OR REPLACE INTO envelopes (hash, date, _from, _to, cc, bcc, subject, message_id, in_reply_to, _references, flags, has_attachments, body_text, timestamp) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", + params![e.hash().to_be_bytes().to_vec(), e.date_as_str(), e.field_from_to_string(), e.field_to_to_string(), e.field_cc_to_string(), e.field_bcc_to_string(), e.subject().into_owned().trim_end_matches('\u{0}'), e.message_id_display().to_string(), e.in_reply_to_display().map(|f| f.to_string()).unwrap_or(String::new()), e.field_references_to_string(), i64::from(e.flags().bits()), if e.has_attachments() { 1 } else { 0 }, String::from("sdfsa"), e.hash().to_be_bytes().to_vec()], + ) + .map_err(|e| MeliError::new(e.to_string()))?; + } + } + + Ok(()) +} + +pub fn search( + term: &str, + _context: &Context, + _account_idx: usize, + _folder_hash: FolderHash, +) -> Result> { + let data_dir = + xdg::BaseDirectories::with_prefix("meli").map_err(|e| MeliError::new(e.to_string()))?; + let conn = Connection::open( + data_dir + .place_data_file("meli.db") + .map_err(|e| MeliError::new(e.to_string()))?, + ) + .map_err(|e| MeliError::new(e.to_string()))?; + let mut stmt= conn.prepare( + "SELECT hash FROM envelopes INNER JOIN fts ON fts.rowid = envelopes.id WHERE fts MATCH ?;") + .map_err(|e| MeliError::new(e.to_string()))?; + + let results = stmt + .query_map(&[term], |row| Ok(row.get(0)?)) + .map_err(|e| MeliError::new(e.to_string()))? + .map(|r: std::result::Result, rusqlite::Error>| { + Ok(u64::from_be_bytes( + r.map_err(|e| MeliError::new(e.to_string()))? + .as_slice() + .try_into() + .map_err(|e: std::array::TryFromSliceError| MeliError::new(e.to_string()))?, + )) + }) + .collect::>>(); + results +} + +pub fn from(term: &str) -> Result> { + let data_dir = + xdg::BaseDirectories::with_prefix("meli").map_err(|e| MeliError::new(e.to_string()))?; + let conn = Connection::open_with_flags( + data_dir + .place_data_file("meli.db") + .map_err(|e| MeliError::new(e.to_string()))?, + rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY, + ) + .map_err(|e| MeliError::new(e.to_string()))?; + let mut stmt = conn + .prepare("SELECT hash FROM envelopes WHERE _from LIKE ?;") + .map_err(|e| MeliError::new(e.to_string()))?; + + let results = stmt + .query_map(&[term.trim()], |row| Ok(row.get(0)?)) + .map_err(|e| MeliError::new(e.to_string()))? + .map(|r: std::result::Result, rusqlite::Error>| { + Ok(u64::from_be_bytes( + r.map_err(|e| MeliError::new(e.to_string()))? + .as_slice() + .try_into() + .map_err(|e: std::array::TryFromSliceError| MeliError::new(e.to_string()))?, + )) + }) + .collect::>>(); + results +} diff --git a/ui/src/state.rs b/ui/src/state.rs index d6cfb42a6..f83ef65fd 100644 --- a/ui/src/state.rs +++ b/ui/src/state.rs @@ -529,6 +529,14 @@ impl State { /// Convert user commands to actions/method calls. fn parse_command(&mut self, cmd: &str) { + if cmd == "insert" { + crate::sqlite3::insert(&self.context).unwrap(); + return; + } else if cmd.starts_with("_from") { + debug!(crate::sqlite3::from(&cmd["_from".len()..])); + return; + } + let result = parse_command(&cmd.as_bytes()).to_full_result(); if let Ok(v) = result {