Use notmuch crate in server and web.

This commit is contained in:
Bill Thiede 2021-10-29 20:10:06 -07:00
parent bec6f0f333
commit ad996643c9
6 changed files with 894 additions and 220 deletions

889
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,4 +2,10 @@
members = [ members = [
"web", "web",
"server", "server",
"notmuch",
] ]
[profile.release]
lto = true
opt-level = 'z'
codegen-units = 1

View File

@ -6,6 +6,15 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
rocket = "0.5.0-rc.1" rocket = { version = "0.5.0-rc.1", features = [ "json" ] }
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", branch = "master" } rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", branch = "master" }
notmuch = { path = "../notmuch" }
serde_json = "1.0.64"
thiserror = "1.0.26"
serde = { version = "1.0", features = ["derive"] }
log = "0.4.14"
[dependencies.rocket_contrib]
version = "0.4.10"
default-features = false
features = ["json"]

View File

@ -1,12 +1,12 @@
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;
use std::error::Error; use std::{error::Error, process::Command, str::FromStr};
use std::process::{Command, Output};
use std::str::FromStr;
use rocket::{http::Method, Route}; use rocket::{response::Debug, serde::json::Json, State};
use rocket_cors::{AllowedHeaders, AllowedOrigins, Guard, Responder}; use rocket_cors::{AllowedHeaders, AllowedOrigins};
use notmuch::{Notmuch, NotmuchError, SearchSummary, ThreadSet};
#[get("/")] #[get("/")]
fn hello() -> &'static str { fn hello() -> &'static str {
@ -14,29 +14,23 @@ fn hello() -> &'static str {
} }
#[get("/search/<query>")] #[get("/search/<query>")]
async fn search(query: &str) -> std::io::Result<Vec<u8>> { async fn search(
//format!("Search for '{}'", query) nm: &State<Notmuch>,
let mut cmd = Command::new("notmuch"); query: &str,
let cmd = cmd.args(["search", "--format=json", "--limit=20", query]); ) -> Result<Json<SearchSummary>, Debug<NotmuchError>> {
dbg!(&cmd); let res = nm.search(query)?;
let out = cmd.output()?; Ok(Json(res))
Ok(out.stdout)
} }
#[get("/show/<query>")] #[get("/show/<query>")]
async fn show(query: &str) -> std::io::Result<Vec<u8>> { async fn show(nm: &State<Notmuch>, query: &str) -> Result<Json<ThreadSet>, Debug<NotmuchError>> {
//format!("Search for '{}'", query) let res = nm.show(query)?;
let mut cmd = Command::new("notmuch"); Ok(Json(res))
let cmd = cmd.args(["show", "--format=json", "--body=true", query]);
dbg!(&cmd);
let out = cmd.output()?;
Ok(out.stdout)
} }
#[rocket::main] #[rocket::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<(), Box<dyn Error>> {
let allowed_origins = AllowedOrigins::all(); let allowed_origins = AllowedOrigins::all();
// You can also deserialize this
let cors = rocket_cors::CorsOptions { let cors = rocket_cors::CorsOptions {
allowed_origins, allowed_origins,
allowed_methods: vec!["Get"] allowed_methods: vec!["Get"]
@ -52,6 +46,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
rocket::build() rocket::build()
.mount("/", routes![hello, search, show]) .mount("/", routes![hello, search, show])
.attach(cors) .attach(cors)
.manage(Notmuch::with_config("../notmuch/testdata/notmuch.config"))
.launch() .launch()
.await?; .await?;

View File

@ -20,13 +20,8 @@ console_error_panic_hook = "0.1.6"
log = "0.4.14" log = "0.4.14"
seed = "0.8.0" seed = "0.8.0"
console_log = {git = "http://git.z.xinu.tv/wathiede/console_log"} console_log = {git = "http://git.z.xinu.tv/wathiede/console_log"}
serde = "1.0.126" serde = { version = "1.0", features = ["derive"] }
notmuch = {path = "../notmuch"}
[profile.release]
lto = true
opt-level = 'z'
codegen-units = 1
[package.metadata.wasm-pack.profile.release] [package.metadata.wasm-pack.profile.release]
wasm-opt = ['-Os'] wasm-opt = ['-Os']

View File

@ -6,7 +6,8 @@
use log::{error, info, Level}; use log::{error, info, Level};
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use serde::Deserialize; use serde::Deserialize;
use web_sys::HtmlInputElement;
use notmuch::{Message, SearchSummary, ThreadSet};
// ------ ------ // ------ ------
// Init // Init
@ -19,6 +20,7 @@ fn init(_: Url, orders: &mut impl Orders<Msg>) -> Model {
.perform_cmd(async { Msg::SearchResult(search_request("is:unread").await) }); .perform_cmd(async { Msg::SearchResult(search_request("is:unread").await) });
Model { Model {
search_results: None, search_results: None,
show_results: None,
} }
} }
@ -28,41 +30,8 @@ fn init(_: Url, orders: &mut impl Orders<Msg>) -> Model {
// `Model` describes our app state. // `Model` describes our app state.
struct Model { struct Model {
search_results: Option<Vec<SearchResult>>, search_results: Option<SearchSummary>,
} show_results: Option<ThreadSet>,
/*
[
{
"thread": "0000000000022fa5",
"timestamp": 1634947104,
"date_relative": "Yest. 16:58",
"matched": 1,
"total": 1,
"authors": "Blue Cross Blue Shield Claims Administrator",
"subject": "BCBS Settlement Claim Received",
"query": [
"id:20211022235824.232afe38e3e07950@bcbssettlement.com",
null
],
"tags": [
"inbox"
]
}
]
*/
#[derive(Deserialize, Debug)]
struct SearchResult {
thread: String,
timestamp: u64,
date_relative: String,
matched: isize,
total: isize,
authors: String,
subject: String,
// TODO(wathiede): what are these?
//query:Vec<_>,
tags: Vec<String>,
} }
// ------ ------ // ------ ------
@ -73,31 +42,48 @@ struct SearchResult {
// `Msg` describes the different events you can modify state with. // `Msg` describes the different events you can modify state with.
enum Msg { enum Msg {
SearchRequest(String), SearchRequest(String),
// TODO(wathiede): replace String with serde json decoded struct SearchResult(fetch::Result<SearchSummary>),
SearchResult(fetch::Result<Vec<SearchResult>>), ShowRequest(String),
ShowResult(fetch::Result<ThreadSet>),
} }
// `update` describes how to handle each `Msg`. // `update` describes how to handle each `Msg`.
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) { fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg { match msg {
Msg::SearchRequest(query) => { Msg::SearchRequest(query) => {
model.show_results = None;
orders orders
.skip() .skip()
.perform_cmd(async move { Msg::SearchResult(search_request(&query).await) }); .perform_cmd(async move { Msg::SearchResult(search_request(&query).await) });
} }
Msg::SearchResult(Ok(response_data)) => { Msg::SearchResult(Ok(response_data)) => {
info!("fetch ok {:?}", response_data); info!("fetch ok {:#?}", response_data);
model.search_results = Some(response_data); model.search_results = Some(response_data);
} }
Msg::SearchResult(Err(fetch_error)) => { Msg::SearchResult(Err(fetch_error)) => {
error!("fetch failed {:?}", fetch_error); error!("fetch failed {:?}", fetch_error);
} }
Msg::ShowRequest(query) => {
model.show_results = None;
orders
.skip()
.perform_cmd(async move { Msg::ShowResult(show_request(&query).await) });
}
Msg::ShowResult(Ok(response_data)) => {
info!("fetch ok {:#?}", response_data);
model.show_results = Some(response_data);
}
Msg::ShowResult(Err(fetch_error)) => {
error!("fetch failed {:?}", fetch_error);
}
} }
} }
async fn search_request(query: &str) -> fetch::Result<Vec<SearchResult>> { async fn search_request(query: &str) -> fetch::Result<SearchSummary> {
Request::new(get_search_request_url(query)) Request::new(get_search_request_url(query))
.method(Method::Get) .method(Method::Get)
.fetch() .fetch()
@ -111,22 +97,104 @@ fn get_search_request_url(query: &str) -> String {
format!("http://nixos-07:9345/search/{}", query) format!("http://nixos-07:9345/search/{}", query)
} }
async fn show_request(query: &str) -> fetch::Result<ThreadSet> {
Request::new(get_show_request_url(query))
.method(Method::Get)
.fetch()
.await?
.check_status()?
.json()
.await
}
fn get_show_request_url(query: &str) -> String {
format!("http://nixos-07:9345/show/{}", query)
}
// ------ ------ // ------ ------
// View // View
// ------ ------ // ------ ------
fn view_message(msg: Option<&Message>) -> Node<Msg> {
if let Some(msg) = msg {
div![
dl![
dt!["Subject"],
dd![&msg.headers.subject],
dt!["From"],
dd![&msg.headers.from],
dt!["To"],
dd![&msg.headers.to],
dt!["CC"],
dd![&msg.headers.cc],
dt!["BCC"],
dd![&msg.headers.bcc],
dt!["Reply-To"],
dd![&msg.headers.reply_to],
dt!["Date"],
dd![&msg.headers.date],
],
dl![
dt!["MessageId"],
dd![&msg.id],
dt!["Match"],
dd![if msg.r#match { "true" } else { "false" }],
dt!["Excluded"],
dd![if msg.excluded { "true" } else { "false" }],
dt!["Filename"],
dd![&msg.filename],
dt!["Timestamp"],
dd![msg.timestamp.to_string()],
dt!["Date"],
dd![&msg.date_relative],
dt!["Tags"],
dd![format!("{:?}", msg.tags)],
],
// msg.body.iter().map(|part| pre![part])
pre![format!("{:#?}", msg.body)]
]
} else {
div![h2!["No message"]]
}
}
// `view` describes what to display. // `view` describes what to display.
fn view(model: &Model) -> Node<Msg> { fn view(model: &Model) -> Node<Msg> {
let results = if let Some(res) = &model.search_results { let content = if let Some(show_results) = &model.show_results {
div![res.iter().map(|r| p![ div![show_results
h3![&r.subject, " ", small![&r.date_relative]], .0
div![span![&r.authors]] .iter()
.enumerate()
.map(|(thread_idx, thread)| div![
h2![format!("thread {}", thread_idx)],
thread
.0
.iter()
.enumerate()
.map(|(thread_node_idx, thread_node)| div![
h3![format!("thread node {}", thread_node_idx)],
view_message(thread_node.0.as_ref())
])
])] ])]
} else if let Some(search_results) = &model.search_results {
let rows = search_results.0.iter().map(|r| {
let tid = r.thread.clone();
tr![
td![
&r.authors,
IF!(r.total>1 => small![" ", r.total.to_string()]),
IF!(r.tags.contains(&"attachment".to_string()) => "📎"),
],
td![&r.subject],
td![&r.date_relative],
ev(Ev::Click, move |_| Msg::ShowRequest(tid)),
]
});
div![table![tr![th!["From"], th!["Subject"], th!["Date"]], rows]]
} else { } else {
div![] div![h1!["Loading"]]
}; };
div![ div![
"Do Something: ",
button![ button![
"Unread", "Unread",
ev(Ev::Click, |_| Msg::SearchRequest("is:unread".to_string())), ev(Ev::Click, |_| Msg::SearchRequest("is:unread".to_string())),
@ -138,7 +206,7 @@ fn view(model: &Model) -> Node<Msg> {
}, },
input_ev(Ev::Input, Msg::SearchRequest), input_ev(Ev::Input, Msg::SearchRequest),
], ],
results, content
] ]
} }