Use notmuch crate in server and web.
This commit is contained in:
parent
bec6f0f333
commit
ad996643c9
889
Cargo.lock
generated
889
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -2,4 +2,10 @@
|
|||||||
members = [
|
members = [
|
||||||
"web",
|
"web",
|
||||||
"server",
|
"server",
|
||||||
|
"notmuch",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
opt-level = 'z'
|
||||||
|
codegen-units = 1
|
||||||
|
|||||||
@ -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"]
|
||||||
|
|||||||
@ -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?;
|
||||||
|
|
||||||
|
|||||||
@ -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']
|
||||||
|
|||||||
162
web/src/lib.rs
162
web/src/lib.rs
@ -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
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user