server: basic graphql thread show, no body support yet.

This commit is contained in:
Bill Thiede 2023-11-26 13:13:04 -08:00
parent 0737f5aac5
commit 447a4a3387
4 changed files with 161 additions and 2 deletions

45
Cargo.lock generated
View File

@ -342,6 +342,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "charset"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18e9079d1a12a2cc2bffb5db039c43661836ead4082120d5844f02555aca2d46"
dependencies = [
"base64 0.13.1",
"encoding_rs",
]
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.31" version = "0.4.31"
@ -596,6 +606,12 @@ dependencies = [
"syn 2.0.29", "syn 2.0.29",
] ]
[[package]]
name = "data-encoding"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]] [[package]]
name = "dbg" name = "dbg"
version = "1.0.4" version = "1.0.4"
@ -1610,6 +1626,17 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "mailparse"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b56570f5f8c0047260d1c8b5b331f62eb9c660b9dd4071a8c46f8c7d3f280aa"
dependencies = [
"charset",
"data-encoding",
"quoted_printable",
]
[[package]] [[package]]
name = "markup5ever" name = "markup5ever"
version = "0.10.1" version = "0.10.1"
@ -1645,6 +1672,16 @@ version = "2.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e" checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e"
[[package]]
name = "memmap"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b"
dependencies = [
"libc",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "memoffset" name = "memoffset"
version = "0.6.5" version = "0.6.5"
@ -2222,6 +2259,12 @@ dependencies = [
"proc-macro2 1.0.66", "proc-macro2 1.0.66",
] ]
[[package]]
name = "quoted_printable"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3866219251662ec3b26fc217e3e05bf9c4f84325234dfb96bf0bf840889e49"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.7.3" version = "0.7.3"
@ -2797,6 +2840,8 @@ dependencies = [
"async-graphql-rocket", "async-graphql-rocket",
"glog", "glog",
"log 0.4.20", "log 0.4.20",
"mailparse",
"memmap",
"notmuch", "notmuch",
"rayon", "rayon",
"rocket 0.5.0", "rocket 0.5.0",

View File

@ -561,7 +561,10 @@ impl Notmuch {
Ok(BufReader::new(child.stdout.take().unwrap()).lines()) Ok(BufReader::new(child.stdout.take().unwrap()).lines())
} }
// TODO(wathiede): implement tags() based on "notmuch search --output=tags '*'" pub fn files(&self, query: &str) -> Result<Lines<BufReader<ChildStdout>>, NotmuchError> {
let mut child = self.run_notmuch_pipe(["search", "--output=files", query])?;
Ok(BufReader::new(child.stdout.take().unwrap()).lines())
}
fn run_notmuch<I, S>(&self, args: I) -> Result<Vec<u8>, NotmuchError> fn run_notmuch<I, S>(&self, args: I) -> Result<Vec<u8>, NotmuchError>
where where

View File

@ -21,6 +21,8 @@ async-graphql = { version = "6.0.11", features = ["log"] }
async-graphql-rocket = "6.0.11" async-graphql-rocket = "6.0.11"
rocket_cors = "0.6.0" rocket_cors = "0.6.0"
rayon = "1.8.0" rayon = "1.8.0"
memmap = "0.7.0"
mailparse = "0.14.0"
[dependencies.rocket_contrib] [dependencies.rocket_contrib]
version = "0.4.11" version = "0.4.11"

View File

@ -1,10 +1,15 @@
use std::hash::{DefaultHasher, Hash, Hasher}; use std::{
fs::File,
hash::{DefaultHasher, Hash, Hasher},
};
use async_graphql::{ use async_graphql::{
connection::{self, Connection, Edge}, connection::{self, Connection, Edge},
Context, EmptyMutation, EmptySubscription, Error, FieldResult, Object, Schema, SimpleObject, Context, EmptyMutation, EmptySubscription, Error, FieldResult, Object, Schema, SimpleObject,
}; };
use log::info; use log::info;
use mailparse::{addrparse, parse_mail, MailHeaderMap, ParsedMail};
use memmap::MmapOptions;
use notmuch::Notmuch; use notmuch::Notmuch;
use rayon::prelude::*; use rayon::prelude::*;
@ -32,6 +37,31 @@ pub struct ThreadSummary {
pub tags: Vec<String>, pub tags: Vec<String>,
} }
#[derive(Debug, SimpleObject)]
pub struct Thread {
messages: Vec<Message>,
}
#[derive(Debug, SimpleObject)]
pub struct Message {
// First From header found in email
pub from: Option<Email>,
// All To headers found in email
pub to: Vec<Email>,
// All CC headers found in email
pub cc: Vec<Email>,
// First Subject header found in email
pub subject: Option<String>,
// Parsed Date header, if found and valid
pub timestamp: Option<i64>,
}
#[derive(Debug, SimpleObject)]
pub struct Email {
pub name: Option<String>,
pub addr: Option<String>,
}
#[derive(SimpleObject)] #[derive(SimpleObject)]
struct Tag { struct Tag {
name: String, name: String,
@ -134,6 +164,85 @@ impl QueryRoot {
}) })
.collect()) .collect())
} }
async fn thread<'ctx>(&self, ctx: &Context<'ctx>, thread_id: String) -> Result<Thread, Error> {
// TODO(wathiede): normalize all email addresses through an address book with preferred
// display names (that default to the most commonly seen name).
let nm = ctx.data_unchecked::<Notmuch>();
let mut messages = Vec::new();
for path in nm.files(&thread_id)? {
let path = path?;
let file = File::open(&path)?;
let mmap = unsafe { MmapOptions::new().map(&file)? };
let m = parse_mail(&mmap)?;
let from = if let Some(from) = m.headers.get_first_value("from") {
addrparse(&from)?.extract_single_info().map(|si| Email {
name: si.display_name,
addr: Some(si.addr),
})
} else {
None
};
let to = email_addresses(&path, &m, "to")?;
let cc = email_addresses(&path, &m, "cc")?;
let subject = m.headers.get_first_value("subject");
let timestamp = m
.headers
.get_first_value("date")
.and_then(|d| mailparse::dateparse(&d).ok());
messages.push(Message {
from,
to,
cc,
subject,
timestamp,
});
}
messages.reverse();
Ok(Thread { messages })
}
} }
pub type GraphqlSchema = Schema<QueryRoot, EmptyMutation, EmptySubscription>; pub type GraphqlSchema = Schema<QueryRoot, EmptyMutation, EmptySubscription>;
fn email_addresses(path: &str, m: &ParsedMail, header_name: &str) -> Result<Vec<Email>, Error> {
let mut addrs = Vec::new();
for header_value in m.headers.get_all_values(header_name) {
match mailparse::addrparse(&header_value) {
Ok(mal) => {
for ma in mal.into_inner() {
match ma {
mailparse::MailAddr::Group(gi) => {
if !gi.group_name.contains("ndisclosed") {
println!("[{path}][{header_name}] Group: {gi}");
}
}
mailparse::MailAddr::Single(s) => addrs.push(Email {
name: s.display_name,
addr: Some(s.addr),
}), //println!("Single: {s}"),
}
}
}
Err(_) => {
let v = header_value;
if v.matches('@').count() == 1 {
if v.matches('<').count() == 1 && v.ends_with('>') {
let idx = v.find('<').unwrap();
let addr = &v[idx + 1..v.len() - 1];
let name = &v[..idx];
addrs.push(Email {
name: Some(name.to_string()),
addr: Some(addr.to_string()),
});
}
} else {
addrs.push(Email {
name: Some(v),
addr: None,
});
}
}
}
}
Ok(addrs)
}