server: basic graphql thread show, no body support yet.
This commit is contained in:
@@ -21,6 +21,8 @@ async-graphql = { version = "6.0.11", features = ["log"] }
|
||||
async-graphql-rocket = "6.0.11"
|
||||
rocket_cors = "0.6.0"
|
||||
rayon = "1.8.0"
|
||||
memmap = "0.7.0"
|
||||
mailparse = "0.14.0"
|
||||
|
||||
[dependencies.rocket_contrib]
|
||||
version = "0.4.11"
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use std::{
|
||||
fs::File,
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
};
|
||||
|
||||
use async_graphql::{
|
||||
connection::{self, Connection, Edge},
|
||||
Context, EmptyMutation, EmptySubscription, Error, FieldResult, Object, Schema, SimpleObject,
|
||||
};
|
||||
use log::info;
|
||||
use mailparse::{addrparse, parse_mail, MailHeaderMap, ParsedMail};
|
||||
use memmap::MmapOptions;
|
||||
use notmuch::Notmuch;
|
||||
use rayon::prelude::*;
|
||||
|
||||
@@ -32,6 +37,31 @@ pub struct ThreadSummary {
|
||||
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)]
|
||||
struct Tag {
|
||||
name: String,
|
||||
@@ -134,6 +164,85 @@ impl QueryRoot {
|
||||
})
|
||||
.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>;
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user