Compare commits
5 Commits
letterbox-
...
letterbox-
| Author | SHA1 | Date | |
|---|---|---|---|
| 01e1ca927e | |||
| 1cc52d6c96 | |||
| e6b3a5b5a9 | |||
| bc4b15a5aa | |||
| 00f61cf6be |
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -2965,7 +2965,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "letterbox-notmuch"
|
||||
version = "0.8.4"
|
||||
version = "0.8.6"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
@@ -2980,14 +2980,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "letterbox-procmail2notmuch"
|
||||
version = "0.8.4"
|
||||
version = "0.8.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "letterbox-server"
|
||||
version = "0.8.4"
|
||||
version = "0.8.6"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"anyhow",
|
||||
@@ -3030,7 +3030,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "letterbox-shared"
|
||||
version = "0.8.4"
|
||||
version = "0.8.6"
|
||||
dependencies = [
|
||||
"build-info",
|
||||
"letterbox-notmuch",
|
||||
@@ -3039,7 +3039,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "letterbox-web"
|
||||
version = "0.8.4"
|
||||
version = "0.8.6"
|
||||
dependencies = [
|
||||
"build-info",
|
||||
"build-info-build",
|
||||
|
||||
@@ -8,7 +8,7 @@ authors = ["Bill Thiede <git@xinu.tv>"]
|
||||
edition = "2021"
|
||||
license = "UNLICENSED"
|
||||
publish = ["xinu"]
|
||||
version = "0.8.4"
|
||||
version = "0.8.6"
|
||||
repository = "https://git.z.xinu.tv/wathiede/letterbox"
|
||||
|
||||
[profile.dev]
|
||||
|
||||
@@ -271,6 +271,12 @@ pub struct Headers {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bcc: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(alias = "Delivered-To")]
|
||||
pub delivered_to: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(alias = "X-Original-To")]
|
||||
pub x_original_to: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reply_to: Option<String>,
|
||||
pub date: String,
|
||||
}
|
||||
@@ -628,33 +634,54 @@ impl Notmuch {
|
||||
let ts: ThreadSet = serde::de::Deserialize::deserialize(&mut deserializer)?;
|
||||
deserializer.end()?;
|
||||
let mut r = HashMap::new();
|
||||
for t in ts.0 {
|
||||
for tn in t.0 {
|
||||
let Some(msg) = tn.0 else {
|
||||
continue;
|
||||
};
|
||||
let mut addrs = vec![];
|
||||
let hdr = msg.headers.to;
|
||||
fn collect_from_thread_node(
|
||||
r: &mut HashMap<String, usize>,
|
||||
tn: &ThreadNode,
|
||||
) -> Result<(), NotmuchError> {
|
||||
let Some(msg) = &tn.0 else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut addrs = vec![];
|
||||
let hdr = &msg.headers.to;
|
||||
if let Some(to) = hdr {
|
||||
addrs.push(to);
|
||||
} else {
|
||||
let hdr = &msg.headers.x_original_to;
|
||||
if let Some(to) = hdr {
|
||||
addrs.push(to);
|
||||
} else {
|
||||
let hdr = &msg.headers.delivered_to;
|
||||
if let Some(to) = hdr {
|
||||
addrs.push(to);
|
||||
};
|
||||
};
|
||||
let hdr = msg.headers.cc;
|
||||
if let Some(cc) = hdr {
|
||||
addrs.push(cc);
|
||||
};
|
||||
for recipient in addrs {
|
||||
mailparse::addrparse(&recipient)?
|
||||
.into_inner()
|
||||
.iter()
|
||||
.for_each(|a| {
|
||||
let mailparse::MailAddr::Single(si) = a else {
|
||||
return;
|
||||
};
|
||||
let addr = &si.addr;
|
||||
if addr == "couchmoney@gmail.com" || addr.ends_with("@xinu.tv") {
|
||||
*r.entry(addr.to_lowercase()).or_default() += 1;
|
||||
}
|
||||
});
|
||||
};
|
||||
let hdr = &msg.headers.cc;
|
||||
if let Some(cc) = hdr {
|
||||
addrs.push(cc);
|
||||
};
|
||||
for recipient in addrs {
|
||||
mailparse::addrparse(&recipient)?
|
||||
.into_inner()
|
||||
.iter()
|
||||
.for_each(|a| {
|
||||
let mailparse::MailAddr::Single(si) = a else {
|
||||
return;
|
||||
};
|
||||
let addr = &si.addr;
|
||||
|
||||
if addr == "couchmoney@gmail.com" || addr.ends_with("@xinu.tv") {
|
||||
*r.entry(addr.to_lowercase()).or_default() += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
for t in ts.0 {
|
||||
for tn in t.0 {
|
||||
collect_from_thread_node(&mut r, &tn)?;
|
||||
for sub_tn in tn.1 {
|
||||
collect_from_thread_node(&mut r, &sub_tn)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@ urlencoding = "2.1.3"
|
||||
#xtracing = { path = "../../xtracing" }
|
||||
#xtracing = { git = "http://git-private.h.xinu.tv/wathiede/xtracing.git" }
|
||||
xtracing = { version = "0.3.0", registry = "xinu" }
|
||||
letterbox-notmuch = { version = "0.8.4", path = "../notmuch", registry = "xinu" }
|
||||
letterbox-shared = { version = "0.8.4", path = "../shared", registry = "xinu" }
|
||||
letterbox-notmuch = { version = "0.8.6", path = "../notmuch", registry = "xinu" }
|
||||
letterbox-shared = { version = "0.8.6", path = "../shared", registry = "xinu" }
|
||||
|
||||
[build-dependencies]
|
||||
build-info-build = "0.0.39"
|
||||
|
||||
@@ -95,6 +95,10 @@ pub struct Message {
|
||||
pub to: Vec<Email>,
|
||||
// All CC headers found in email
|
||||
pub cc: Vec<Email>,
|
||||
// X-Original-To header found in email
|
||||
pub x_original_to: Option<Email>,
|
||||
// Delivered-To header found in email
|
||||
pub delivered_to: Option<Email>,
|
||||
// First Subject header found in email
|
||||
pub subject: Option<String>,
|
||||
// Parsed Date header, if found and valid
|
||||
@@ -286,7 +290,7 @@ impl QueryRoot {
|
||||
Ok(letterbox_shared::build_version(bi))
|
||||
}
|
||||
#[instrument(skip_all, fields(query=query))]
|
||||
#[instrument(skip_all, fields(query=query, request_id=request_id()))]
|
||||
#[instrument(skip_all, fields(query=query, rid=request_id()))]
|
||||
async fn count<'ctx>(&self, ctx: &Context<'ctx>, query: String) -> Result<usize, Error> {
|
||||
let nm = ctx.data_unchecked::<Notmuch>();
|
||||
let pool = ctx.data_unchecked::<PgPool>();
|
||||
@@ -309,7 +313,7 @@ impl QueryRoot {
|
||||
|
||||
// TODO: this function doesn't get parallelism, possibly because notmuch is sync and blocks,
|
||||
// rewrite that with tokio::process:Command
|
||||
#[instrument(skip_all, fields(query=query, request_id=request_id()))]
|
||||
#[instrument(skip_all, fields(query=query, rid=request_id()))]
|
||||
async fn search<'ctx>(
|
||||
&self,
|
||||
ctx: &Context<'ctx>,
|
||||
@@ -467,7 +471,7 @@ impl QueryRoot {
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[instrument(skip_all, fields(request_id=request_id()))]
|
||||
#[instrument(skip_all, fields(rid=request_id()))]
|
||||
async fn tags<'ctx>(&self, ctx: &Context<'ctx>) -> FieldResult<Vec<Tag>> {
|
||||
let nm = ctx.data_unchecked::<Notmuch>();
|
||||
let pool = ctx.data_unchecked::<PgPool>();
|
||||
@@ -476,7 +480,7 @@ impl QueryRoot {
|
||||
tags.append(&mut nm::tags(nm, needs_unread)?);
|
||||
Ok(tags)
|
||||
}
|
||||
#[instrument(skip_all, fields(thread_id=thread_id, request_id=request_id()))]
|
||||
#[instrument(skip_all, fields(thread_id=thread_id, rid=request_id()))]
|
||||
async fn thread<'ctx>(&self, ctx: &Context<'ctx>, thread_id: String) -> Result<Thread, Error> {
|
||||
let nm = ctx.data_unchecked::<Notmuch>();
|
||||
let cacher = ctx.data_unchecked::<FilesystemCacher>();
|
||||
@@ -553,7 +557,7 @@ async fn tantivy_search(
|
||||
pub struct Mutation;
|
||||
#[Object]
|
||||
impl Mutation {
|
||||
#[instrument(skip_all, fields(query=query, unread=unread, request_id=request_id()))]
|
||||
#[instrument(skip_all, fields(query=query, unread=unread, rid=request_id()))]
|
||||
async fn set_read_status<'ctx>(
|
||||
&self,
|
||||
ctx: &Context<'ctx>,
|
||||
@@ -572,7 +576,7 @@ impl Mutation {
|
||||
nm::set_read_status(nm, &query, unread).await?;
|
||||
Ok(true)
|
||||
}
|
||||
#[instrument(skip_all, fields(query=query, tag=tag, request_id=request_id()))]
|
||||
#[instrument(skip_all, fields(query=query, tag=tag, rid=request_id()))]
|
||||
async fn tag_add<'ctx>(
|
||||
&self,
|
||||
ctx: &Context<'ctx>,
|
||||
@@ -584,7 +588,7 @@ impl Mutation {
|
||||
nm.tag_add(&tag, &query)?;
|
||||
Ok(true)
|
||||
}
|
||||
#[instrument(skip_all, fields(query=query, tag=tag, request_id=request_id()))]
|
||||
#[instrument(skip_all, fields(query=query, tag=tag, rid=request_id()))]
|
||||
async fn tag_remove<'ctx>(
|
||||
&self,
|
||||
ctx: &Context<'ctx>,
|
||||
@@ -607,7 +611,7 @@ impl Mutation {
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
#[instrument(skip_all, fields(request_id=request_id()))]
|
||||
#[instrument(skip_all, fields(rid=request_id()))]
|
||||
async fn refresh<'ctx>(&self, ctx: &Context<'ctx>) -> Result<bool, Error> {
|
||||
let nm = ctx.data_unchecked::<Notmuch>();
|
||||
let cacher = ctx.data_unchecked::<FilesystemCacher>();
|
||||
|
||||
@@ -773,7 +773,19 @@ impl Query {
|
||||
for uid in &self.uids {
|
||||
parts.push(uid.clone());
|
||||
}
|
||||
parts.extend(self.remainder.clone());
|
||||
for r in &self.remainder {
|
||||
// Rewrite "to:" to include ExtraTo:. ExtraTo: is configured in
|
||||
// notmuch-config to index Delivered-To and X-Original-To headers.
|
||||
if r.starts_with("to:") {
|
||||
parts.push("(".to_string());
|
||||
parts.push(r.to_string());
|
||||
parts.push("OR".to_string());
|
||||
parts.push(r.replace("to:", "ExtraTo:"));
|
||||
parts.push(")".to_string());
|
||||
} else {
|
||||
parts.push(r.to_string());
|
||||
}
|
||||
}
|
||||
parts.join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,6 +196,8 @@ pub async fn thread(
|
||||
|
||||
let to = email_addresses(&path, &m, "to")?;
|
||||
let cc = email_addresses(&path, &m, "cc")?;
|
||||
let delivered_to = email_addresses(&path, &m, "delivered-to")?.pop();
|
||||
let x_original_to = email_addresses(&path, &m, "x-original-to")?.pop();
|
||||
let subject = m.headers.get_first_value("subject");
|
||||
let timestamp = m
|
||||
.headers
|
||||
@@ -315,6 +317,8 @@ pub async fn thread(
|
||||
body,
|
||||
path,
|
||||
attachments,
|
||||
delivered_to,
|
||||
x_original_to,
|
||||
});
|
||||
}
|
||||
messages.reverse();
|
||||
|
||||
@@ -12,5 +12,5 @@ version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
build-info = "0.0.39"
|
||||
letterbox-notmuch = { version = "0.8.4", path = "../notmuch", registry = "xinu" }
|
||||
letterbox-notmuch = { version = "0.8.6", path = "../notmuch", registry = "xinu" }
|
||||
serde = { version = "1.0.147", features = ["derive"] }
|
||||
|
||||
@@ -33,8 +33,8 @@ wasm-bindgen = "=0.2.100"
|
||||
uuid = { version = "1.13.1", features = [
|
||||
"js",
|
||||
] } # direct dep to set js feature, prevents Rng issues
|
||||
letterbox-shared = { version = "0.8.4", path = "../shared", registry = "xinu" }
|
||||
letterbox-notmuch = { version = "0.8.4", path = "../notmuch", registry = "xinu" }
|
||||
letterbox-shared = { version = "0.8.6", path = "../shared", registry = "xinu" }
|
||||
letterbox-notmuch = { version = "0.8.6", path = "../notmuch", registry = "xinu" }
|
||||
seed_hooks = { version = "0.4.0", registry = "xinu" }
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
|
||||
@@ -671,6 +671,30 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"deprecationReason": null,
|
||||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"name": "xOriginalTo",
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Email",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"deprecationReason": null,
|
||||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"name": "deliveredTo",
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Email",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"deprecationReason": null,
|
||||
|
||||
@@ -31,6 +31,14 @@ query ShowThreadQuery($threadId: String!) {
|
||||
name
|
||||
addr
|
||||
}
|
||||
xOriginalTo {
|
||||
name
|
||||
addr
|
||||
}
|
||||
deliveredTo {
|
||||
name
|
||||
addr
|
||||
}
|
||||
timestamp
|
||||
body {
|
||||
__typename
|
||||
|
||||
@@ -519,6 +519,48 @@ fn render_open_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node<
|
||||
})
|
||||
]
|
||||
]),
|
||||
IF!(msg.to.is_empty() && msg.x_original_to.is_some()=>div![
|
||||
C!["text-xs"],
|
||||
span![
|
||||
C!["font-semibold"],
|
||||
"Original To: "
|
||||
],
|
||||
span![
|
||||
msg.x_original_to.as_ref().map(|to| {
|
||||
let ShowThreadQueryThreadOnEmailThreadMessagesXOriginalTo { name, addr } = to;
|
||||
span![
|
||||
addr.as_ref().map(|addr| attrs! {
|
||||
At::Title => addr
|
||||
}),
|
||||
name.as_ref().unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown)),
|
||||
" ",
|
||||
addr.as_ref().map(|addr| copy_text_widget(&addr)),
|
||||
" "
|
||||
]
|
||||
})
|
||||
]
|
||||
]),
|
||||
IF!(msg.to.is_empty() && msg.x_original_to.is_none() && msg.delivered_to.is_some() => div![
|
||||
C!["text-xs"],
|
||||
span![
|
||||
C!["font-semibold"],
|
||||
"Delivered To: "
|
||||
],
|
||||
span![
|
||||
msg.delivered_to.as_ref().map(|to| {
|
||||
let ShowThreadQueryThreadOnEmailThreadMessagesDeliveredTo { name, addr } = to;
|
||||
span![
|
||||
addr.as_ref().map(|addr| attrs! {
|
||||
At::Title => addr
|
||||
}),
|
||||
name.as_ref().unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown)),
|
||||
" ",
|
||||
addr.as_ref().map(|addr| copy_text_widget(&addr)),
|
||||
" "
|
||||
]
|
||||
})
|
||||
]
|
||||
]),
|
||||
IF!(!msg.cc.is_empty() =>div![
|
||||
C!["text-xs"],
|
||||
span![
|
||||
@@ -614,6 +656,48 @@ fn render_closed_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Nod
|
||||
],
|
||||
" "
|
||||
]),
|
||||
IF!(msg.to.is_empty() && msg.x_original_to.is_some()=>div![
|
||||
C!["text-xs"],
|
||||
span![
|
||||
C!["font-semibold"],
|
||||
"Original To: "
|
||||
],
|
||||
span![
|
||||
msg.x_original_to.as_ref().map(|to| {
|
||||
let ShowThreadQueryThreadOnEmailThreadMessagesXOriginalTo { name, addr } = to;
|
||||
span![
|
||||
addr.as_ref().map(|addr| attrs! {
|
||||
At::Title => addr
|
||||
}),
|
||||
name.as_ref().unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown)),
|
||||
" ",
|
||||
addr.as_ref().map(|addr| copy_text_widget(&addr)),
|
||||
" "
|
||||
]
|
||||
})
|
||||
]
|
||||
]),
|
||||
IF!(msg.to.is_empty() && msg.x_original_to.is_none() && msg.delivered_to.is_some() => div![
|
||||
C!["text-xs"],
|
||||
span![
|
||||
C!["font-semibold"],
|
||||
"Delivered To: "
|
||||
],
|
||||
span![
|
||||
msg.delivered_to.as_ref().map(|to| {
|
||||
let ShowThreadQueryThreadOnEmailThreadMessagesDeliveredTo { name, addr } = to;
|
||||
span![
|
||||
addr.as_ref().map(|addr| attrs! {
|
||||
At::Title => addr
|
||||
}),
|
||||
name.as_ref().unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown)),
|
||||
" ",
|
||||
addr.as_ref().map(|addr| copy_text_widget(&addr)),
|
||||
" "
|
||||
]
|
||||
})
|
||||
]
|
||||
]),
|
||||
IF!(!msg.cc.is_empty() => div![
|
||||
C!["text-xs", "max-w-full", "overflow-clip", "text-ellipsis"],
|
||||
span![
|
||||
|
||||
Reference in New Issue
Block a user