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

View File

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

View File

@ -6,7 +6,8 @@
use log::{error, info, Level};
use seed::{prelude::*, *};
use serde::Deserialize;
use web_sys::HtmlInputElement;
use notmuch::{Message, SearchSummary, ThreadSet};
// ------ ------
// Init
@ -19,6 +20,7 @@ fn init(_: Url, orders: &mut impl Orders<Msg>) -> Model {
.perform_cmd(async { Msg::SearchResult(search_request("is:unread").await) });
Model {
search_results: None,
show_results: None,
}
}
@ -28,41 +30,8 @@ fn init(_: Url, orders: &mut impl Orders<Msg>) -> Model {
// `Model` describes our app state.
struct Model {
search_results: Option<Vec<SearchResult>>,
}
/*
[
{
"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>,
search_results: Option<SearchSummary>,
show_results: Option<ThreadSet>,
}
// ------ ------
@ -73,31 +42,48 @@ struct SearchResult {
// `Msg` describes the different events you can modify state with.
enum Msg {
SearchRequest(String),
// TODO(wathiede): replace String with serde json decoded struct
SearchResult(fetch::Result<Vec<SearchResult>>),
SearchResult(fetch::Result<SearchSummary>),
ShowRequest(String),
ShowResult(fetch::Result<ThreadSet>),
}
// `update` describes how to handle each `Msg`.
fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
Msg::SearchRequest(query) => {
model.show_results = None;
orders
.skip()
.perform_cmd(async move { Msg::SearchResult(search_request(&query).await) });
}
Msg::SearchResult(Ok(response_data)) => {
info!("fetch ok {:?}", response_data);
info!("fetch ok {:#?}", response_data);
model.search_results = Some(response_data);
}
Msg::SearchResult(Err(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))
.method(Method::Get)
.fetch()
@ -111,22 +97,104 @@ fn get_search_request_url(query: &str) -> String {
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
// ------ ------
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.
fn view(model: &Model) -> Node<Msg> {
let results = if let Some(res) = &model.search_results {
div![res.iter().map(|r| p![
h3![&r.subject, " ", small![&r.date_relative]],
div![span![&r.authors]]
])]
let content = if let Some(show_results) = &model.show_results {
div![show_results
.0
.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 {
div![]
div![h1!["Loading"]]
};
div![
"Do Something: ",
button![
"Unread",
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),
],
results,
content
]
}