Compare commits

..

48 Commits

Author SHA1 Message Date
01e1ca927e chore: Release
All checks were successful
Continuous integration / Test Suite (push) Successful in 44s
Continuous integration / Check (push) Successful in 2m0s
Continuous integration / Trunk (push) Successful in 40s
Continuous integration / Rustfmt (push) Successful in 1m0s
Continuous integration / build (push) Successful in 51s
Continuous integration / Disallow unused dependencies (push) Successful in 2m34s
2025-02-23 11:47:04 -08:00
1cc52d6c96 web: show X-Original-To: if To: is missing, fallback to Delivered-To: 2025-02-23 11:46:21 -08:00
e6b3a5b5a9 notmuch & server: plumb Delivered-To and X-Original-To headers 2025-02-23 09:37:09 -08:00
bc4b15a5aa chore: Release
All checks were successful
Continuous integration / Check (push) Successful in 38s
Continuous integration / Test Suite (push) Successful in 42s
Continuous integration / Trunk (push) Successful in 39s
Continuous integration / Rustfmt (push) Successful in 31s
Continuous integration / Disallow unused dependencies (push) Successful in 55s
Continuous integration / build (push) Successful in 2m40s
2025-02-22 17:58:37 -08:00
00f61cf6be server: recursively descend email threads to find all unread recipients 2025-02-22 17:58:07 -08:00
52e24437bd chore: Release
All checks were successful
Continuous integration / Check (push) Successful in 38s
Continuous integration / Test Suite (push) Successful in 45s
Continuous integration / Trunk (push) Successful in 39s
Continuous integration / Rustfmt (push) Successful in 51s
Continuous integration / build (push) Successful in 51s
Continuous integration / Disallow unused dependencies (push) Successful in 2m25s
2025-02-22 17:27:54 -08:00
393ffc8506 notmuch: normalize unread_recipients to lower case 2025-02-22 17:27:30 -08:00
2b6cb6ec6e chore: Release
All checks were successful
Continuous integration / Check (push) Successful in 42s
Continuous integration / Test Suite (push) Successful in 44s
Continuous integration / Trunk (push) Successful in 39s
Continuous integration / Rustfmt (push) Successful in 58s
Continuous integration / build (push) Successful in 53s
Continuous integration / Disallow unused dependencies (push) Successful in 2m37s
2025-02-22 17:24:31 -08:00
0cba3a624c web: add de/select all checkbox with tristate 2025-02-22 17:24:18 -08:00
73433711ca fix(deps): update rust crate xtracing to 0.3.0
All checks were successful
Continuous integration / Check (pull_request) Successful in 38s
Continuous integration / Test Suite (pull_request) Successful in 43s
Continuous integration / Rustfmt (pull_request) Successful in 31s
Continuous integration / build (pull_request) Successful in 51s
Continuous integration / Trunk (pull_request) Successful in 2m32s
Continuous integration / Disallow unused dependencies (pull_request) Successful in 57s
Continuous integration / Test Suite (push) Successful in 49s
Continuous integration / Trunk (push) Successful in 40s
Continuous integration / Check (push) Successful in 1m54s
Continuous integration / Rustfmt (push) Successful in 32s
Continuous integration / Disallow unused dependencies (push) Successful in 59s
Continuous integration / build (push) Successful in 2m28s
2025-02-23 00:02:30 +00:00
965afa6871 Merge pull request 'fix(deps): update rust crate seed_hooks to 0.4.0' (#48) from renovate/seed_hooks-0.x into master
All checks were successful
Continuous integration / Test Suite (push) Successful in 44s
Continuous integration / Check (push) Successful in 1m45s
Continuous integration / Trunk (push) Successful in 40s
Continuous integration / Rustfmt (push) Successful in 1m3s
Continuous integration / build (push) Successful in 53s
Continuous integration / Disallow unused dependencies (push) Successful in 2m33s
Reviewed-on: #48
2025-02-22 15:49:50 -08:00
e70dbaf917 fix(deps): update rust crate seed_hooks to 0.4.0
All checks were successful
Continuous integration / Check (push) Successful in 39s
Continuous integration / Test Suite (push) Successful in 44s
Continuous integration / Rustfmt (push) Successful in 32s
Continuous integration / Trunk (push) Successful in 1m23s
Continuous integration / build (push) Successful in 51s
Continuous integration / Check (pull_request) Successful in 38s
Continuous integration / Test Suite (pull_request) Successful in 44s
Continuous integration / Disallow unused dependencies (push) Successful in 2m38s
Continuous integration / Trunk (pull_request) Successful in 38s
Continuous integration / build (pull_request) Successful in 50s
Continuous integration / Rustfmt (pull_request) Successful in 55s
Continuous integration / Disallow unused dependencies (pull_request) Successful in 57s
2025-02-22 15:18:33 -08:00
6b4ce11743 fix(deps): update rust crate xtracing to v0.2.1
All checks were successful
Continuous integration / Check (pull_request) Successful in 37s
Continuous integration / Test Suite (pull_request) Successful in 43s
Continuous integration / Trunk (pull_request) Successful in 39s
Continuous integration / Rustfmt (pull_request) Successful in 31s
Continuous integration / Disallow unused dependencies (pull_request) Successful in 56s
Continuous integration / build (pull_request) Successful in 2m38s
Continuous integration / Check (push) Successful in 38s
Continuous integration / Trunk (push) Successful in 39s
Continuous integration / Rustfmt (push) Successful in 32s
Continuous integration / build (push) Successful in 49s
Continuous integration / Disallow unused dependencies (push) Successful in 57s
Continuous integration / Test Suite (push) Successful in 3m44s
2025-02-22 22:31:55 +00:00
d1980a55a7 fix(deps): update rust crate cacher to v0.1.5
All checks were successful
Continuous integration / Check (pull_request) Successful in 36s
Continuous integration / Test Suite (pull_request) Successful in 42s
Continuous integration / Trunk (pull_request) Successful in 38s
Continuous integration / build (pull_request) Successful in 50s
Continuous integration / Rustfmt (pull_request) Successful in 52s
Continuous integration / Disallow unused dependencies (pull_request) Successful in 56s
Continuous integration / Test Suite (push) Successful in 44s
Continuous integration / Trunk (push) Successful in 38s
Continuous integration / Check (push) Successful in 2m4s
Continuous integration / Rustfmt (push) Successful in 31s
Continuous integration / Disallow unused dependencies (push) Successful in 55s
Continuous integration / build (push) Successful in 2m41s
2025-02-22 21:16:46 +00:00
8b78b39d4c chore: Release
All checks were successful
Continuous integration / Check (push) Successful in 37s
Continuous integration / Test Suite (push) Successful in 42s
Continuous integration / Trunk (push) Successful in 38s
Continuous integration / Rustfmt (push) Successful in 31s
Continuous integration / Disallow unused dependencies (push) Successful in 55s
Continuous integration / build (push) Successful in 2m29s
2025-02-22 13:10:03 -08:00
ae17651eb5 Normalize Justfile config 2025-02-22 13:08:15 -08:00
22fd8409f6 chore: Release
All checks were successful
Continuous integration / Check (push) Successful in 40s
Continuous integration / Test Suite (push) Successful in 42s
Continuous integration / Trunk (push) Successful in 38s
Continuous integration / Rustfmt (push) Successful in 52s
Continuous integration / build (push) Successful in 50s
Continuous integration / Disallow unused dependencies (push) Successful in 2m21s
2025-02-22 12:41:57 -08:00
d0a4ba417f chore: Release 2025-02-22 12:41:30 -08:00
7b09b098a4 chore: Release 2025-02-22 12:41:15 -08:00
bd4c10a8fb Specify registry for all letterbox-* deps 2025-02-22 12:41:15 -08:00
ed3c5f152e chore: Release 2025-02-22 12:41:15 -08:00
63232d1e92 Publish only to xinu 2025-02-22 12:41:15 -08:00
4a3eba80d5 chore: Release 2025-02-22 12:41:15 -08:00
71d3745342 Try relative paths for letterbox-* deps 2025-02-22 12:41:14 -08:00
5fdc98633d chore: Release 2025-02-22 12:39:39 -08:00
57877f268d Set repository in workspace 2025-02-22 12:39:20 -08:00
871a93d58f Move most package metadata to workspace 2025-02-22 12:39:20 -08:00
4b7cbd4f9b chore: Release 2025-02-22 12:39:19 -08:00
aa2a9815df Add automatic per-email address unread folders 2025-02-22 12:38:57 -08:00
2e5b18a008 Fix cargo-udeps build step 2025-02-22 12:37:27 -08:00
d0a38114cc Add cargo-udeps build step 2025-02-22 12:37:27 -08:00
ccc1d516c7 fix(deps): update rust crate letterbox-notmuch to 0.8.0
All checks were successful
Continuous integration / Test Suite (pull_request) Successful in 40s
Continuous integration / Trunk (pull_request) Successful in 38s
Continuous integration / Check (pull_request) Successful in 1m44s
Continuous integration / Rustfmt (pull_request) Successful in 33s
Continuous integration / build (pull_request) Successful in 1m57s
Continuous integration / Check (push) Successful in 38s
Continuous integration / Trunk (push) Successful in 40s
Continuous integration / Test Suite (push) Successful in 1m45s
Continuous integration / Rustfmt (push) Successful in 33s
Continuous integration / build (push) Successful in 2m18s
2025-02-22 19:15:52 +00:00
246b710fdd fix(deps): update rust crate log to v0.4.26
All checks were successful
Continuous integration / Check (pull_request) Successful in 35s
Continuous integration / Test Suite (pull_request) Successful in 39s
Continuous integration / Trunk (pull_request) Successful in 37s
Continuous integration / Rustfmt (pull_request) Successful in 31s
Continuous integration / build (pull_request) Successful in 47s
Continuous integration / Test Suite (push) Successful in 40s
Continuous integration / Trunk (push) Successful in 38s
Continuous integration / Check (push) Successful in 1m49s
Continuous integration / Rustfmt (push) Successful in 32s
Continuous integration / build (push) Successful in 2m45s
2025-02-21 05:46:06 +00:00
1a21c9fa8e fix(deps): update rust crate uuid to v1.14.0
All checks were successful
Continuous integration / Check (pull_request) Successful in 53s
Continuous integration / Test Suite (pull_request) Successful in 39s
Continuous integration / Trunk (pull_request) Successful in 37s
Continuous integration / Rustfmt (pull_request) Successful in 33s
Continuous integration / build (pull_request) Successful in 1m18s
Continuous integration / Check (push) Successful in 35s
Continuous integration / Test Suite (push) Successful in 39s
Continuous integration / Trunk (push) Successful in 37s
Continuous integration / Rustfmt (push) Successful in 31s
Continuous integration / build (push) Successful in 45s
2025-02-21 00:30:51 +00:00
9fd912b1d4 fix(deps): update rust crate serde to v1.0.218
All checks were successful
Continuous integration / Test Suite (pull_request) Successful in 41s
Continuous integration / Trunk (pull_request) Successful in 38s
Continuous integration / Check (pull_request) Successful in 1m51s
Continuous integration / Rustfmt (pull_request) Successful in 32s
Continuous integration / build (pull_request) Successful in 3m5s
Continuous integration / Check (push) Successful in 48s
Continuous integration / Trunk (push) Successful in 38s
Continuous integration / Rustfmt (push) Successful in 31s
Continuous integration / build (push) Successful in 48s
Continuous integration / Test Suite (push) Successful in 2m52s
2025-02-20 05:31:10 +00:00
9ded32f97b fix(deps): update rust crate anyhow to v1.0.96
All checks were successful
Continuous integration / Test Suite (pull_request) Successful in 40s
Continuous integration / Trunk (pull_request) Successful in 37s
Continuous integration / Check (pull_request) Successful in 1m52s
Continuous integration / Rustfmt (pull_request) Successful in 32s
Continuous integration / build (pull_request) Successful in 2m7s
Continuous integration / Check (push) Successful in 35s
Continuous integration / Test Suite (push) Successful in 39s
Continuous integration / Rustfmt (push) Successful in 32s
Continuous integration / Trunk (push) Successful in 1m26s
Continuous integration / build (push) Successful in 46s
2025-02-20 03:16:55 +00:00
10aac046bc fix(deps): update rust crate serde_json to v1.0.139
All checks were successful
Continuous integration / Check (pull_request) Successful in 36s
Continuous integration / Test Suite (pull_request) Successful in 40s
Continuous integration / Rustfmt (pull_request) Successful in 31s
Continuous integration / Trunk (pull_request) Successful in 1m24s
Continuous integration / build (pull_request) Successful in 47s
Continuous integration / Test Suite (push) Successful in 40s
Continuous integration / Trunk (push) Successful in 37s
Continuous integration / Check (push) Successful in 1m42s
Continuous integration / Rustfmt (push) Successful in 31s
Continuous integration / build (push) Successful in 1m55s
2025-02-20 03:00:53 +00:00
f4527baf89 fix(deps): update rust crate seed_hooks to 0.4.0
All checks were successful
Continuous integration / Check (pull_request) Successful in 1m25s
Continuous integration / Test Suite (pull_request) Successful in 39s
Continuous integration / Rustfmt (pull_request) Successful in 33s
Continuous integration / build (pull_request) Successful in 46s
Continuous integration / Trunk (pull_request) Successful in 1m37s
Continuous integration / Check (push) Successful in 54s
Continuous integration / Trunk (push) Successful in 36s
Continuous integration / Rustfmt (push) Successful in 31s
Continuous integration / build (push) Successful in 46s
Continuous integration / Test Suite (push) Successful in 4m11s
2025-02-18 20:15:48 +00:00
11ec5bf747 fix(deps): update rust crate uuid to v1.13.2
All checks were successful
Continuous integration / Test Suite (pull_request) Successful in 40s
Continuous integration / Check (pull_request) Successful in 1m30s
Continuous integration / Trunk (pull_request) Successful in 37s
Continuous integration / Rustfmt (pull_request) Successful in 47s
Continuous integration / build (pull_request) Successful in 48s
Continuous integration / Check (push) Successful in 36s
Continuous integration / Trunk (push) Successful in 38s
Continuous integration / Rustfmt (push) Successful in 31s
Continuous integration / Test Suite (push) Successful in 2m8s
Continuous integration / build (push) Successful in 47s
2025-02-17 23:46:05 +00:00
6a53679755 fix(deps): update rust crate clap to v4.5.30
All checks were successful
Continuous integration / Check (pull_request) Successful in 37s
Continuous integration / Trunk (pull_request) Successful in 37s
Continuous integration / Rustfmt (pull_request) Successful in 31s
Continuous integration / Test Suite (pull_request) Successful in 2m0s
Continuous integration / build (pull_request) Successful in 46s
Continuous integration / Check (push) Successful in 35s
Continuous integration / Trunk (push) Successful in 38s
Continuous integration / Rustfmt (push) Successful in 31s
Continuous integration / Test Suite (push) Successful in 1m55s
Continuous integration / build (push) Successful in 47s
2025-02-17 19:15:50 +00:00
7bedec0692 chore(deps): lock file maintenance
All checks were successful
Continuous integration / Test Suite (pull_request) Successful in 41s
Continuous integration / Check (pull_request) Successful in 1m34s
Continuous integration / Trunk (pull_request) Successful in 37s
Continuous integration / Rustfmt (pull_request) Successful in 50s
Continuous integration / build (pull_request) Successful in 47s
Continuous integration / Check (push) Successful in 36s
Continuous integration / Trunk (push) Successful in 38s
Continuous integration / Rustfmt (push) Successful in 31s
Continuous integration / build (push) Successful in 47s
Continuous integration / Test Suite (push) Successful in 2m48s
2025-02-17 00:01:14 +00:00
78feb95811 chore: Release
All checks were successful
Continuous integration / Test Suite (push) Successful in 1m27s
Continuous integration / Check (push) Successful in 1m39s
Continuous integration / Trunk (push) Successful in 45s
Continuous integration / Rustfmt (push) Successful in 53s
Continuous integration / build (push) Successful in 1m10s
2025-02-15 14:49:11 -08:00
3aad2bb80e web: another attempt to fix progress bar 2025-02-15 14:47:32 -08:00
0df8de3661 web: use seed_hooks ability to create ev handlers 2025-02-15 14:47:32 -08:00
83ecc73fbd fix(deps): update rust crate seed_hooks to v0.1.16
All checks were successful
Continuous integration / Test Suite (pull_request) Successful in 41s
Continuous integration / Check (pull_request) Successful in 1m24s
Continuous integration / Trunk (pull_request) Successful in 39s
Continuous integration / Rustfmt (pull_request) Successful in 48s
Continuous integration / build (pull_request) Successful in 48s
Continuous integration / Check (push) Successful in 35s
Continuous integration / Trunk (push) Successful in 37s
Continuous integration / Test Suite (push) Successful in 1m41s
Continuous integration / Rustfmt (push) Successful in 31s
Continuous integration / build (push) Successful in 1m49s
2025-02-14 01:15:49 +00:00
c10313cd12 fix(deps): update rust crate letterbox-shared to 0.6.0
All checks were successful
Continuous integration / Test Suite (pull_request) Successful in 39s
Continuous integration / Check (pull_request) Successful in 1m24s
Continuous integration / Trunk (pull_request) Successful in 37s
Continuous integration / Rustfmt (pull_request) Successful in 48s
Continuous integration / build (pull_request) Successful in 46s
Continuous integration / Check (push) Successful in 35s
Continuous integration / Trunk (push) Successful in 37s
Continuous integration / Rustfmt (push) Successful in 32s
Continuous integration / Test Suite (push) Successful in 1m44s
Continuous integration / build (push) Successful in 46s
2025-02-13 23:31:34 +00:00
4c98bcd9cb Merge pull request 'fix(deps): update rust crate letterbox-notmuch to 0.6.0' (#34) from renovate/letterbox-notmuch-0.x into master
All checks were successful
Continuous integration / Check (push) Successful in 35s
Continuous integration / Trunk (push) Successful in 38s
Continuous integration / Rustfmt (push) Successful in 32s
Continuous integration / Test Suite (push) Successful in 1m52s
Continuous integration / build (push) Successful in 46s
Reviewed-on: #34
2025-02-13 15:17:39 -08:00
004de235a8 fix(deps): update rust crate letterbox-notmuch to 0.6.0
Some checks failed
renovate/artifacts Artifact file update failure
Continuous integration / Check (push) Successful in 36s
Continuous integration / Test Suite (push) Successful in 39s
Continuous integration / Trunk (push) Successful in 38s
Continuous integration / Rustfmt (push) Successful in 52s
Continuous integration / build (push) Successful in 47s
Continuous integration / Test Suite (pull_request) Successful in 40s
Continuous integration / Check (pull_request) Successful in 1m20s
Continuous integration / Trunk (pull_request) Successful in 37s
Continuous integration / Rustfmt (pull_request) Successful in 51s
Continuous integration / build (pull_request) Successful in 48s
2025-02-13 23:16:31 +00:00
16 changed files with 752 additions and 291 deletions

View File

@@ -49,3 +49,19 @@ jobs:
- uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@v1
- run: cargo build
udeps:
name: Disallow unused dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: nightly
- name: Run cargo-udeps
uses: aig787/cargo-udeps-action@v1
with:
version: 'latest'
args: '--all-targets'

533
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,14 @@ resolver = "2"
default-members = ["server"]
members = ["web", "server", "notmuch", "procmail2notmuch", "shared"]
[workspace.package]
authors = ["Bill Thiede <git@xinu.tv>"]
edition = "2021"
license = "UNLICENSED"
publish = ["xinu"]
version = "0.8.6"
repository = "https://git.z.xinu.tv/wathiede/letterbox"
[profile.dev]
opt-level = 1

View File

@@ -8,6 +8,9 @@ patch: (_release "patch")
sqlx-prepare:
cd server; cargo sqlx prepare && git add .sqlx; git commit -m "cargo sqlx prepare" .sqlx || true
pull:
git pull
_release level: sqlx-prepare
cargo-release release -x {{ level }} --workspace --no-confirm
_release level: pull sqlx-prepare
cargo-release release -x {{ level }} --workspace --no-confirm --registry=xinu

View File

@@ -1,16 +1,18 @@
[package]
name = "letterbox-notmuch"
version = "0.6.0"
edition = "2021"
exclude = ["/testdata"]
description = "Wrapper for calling notmuch cli"
license = "UNLICENSED"
repository = "https://git.z.xinu.tv/wathiede/letterbox"
publish = ["xinu"]
authors.workspace = true
edition.workspace = true
license.workspace = true
publish.workspace = true
repository.workspace = true
version.workspace = true
[dependencies]
log = "0.4.14"
mailparse = "0.16.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["unbounded_depth"] }
thiserror = "2.0.0"

View File

@@ -207,6 +207,7 @@
//! ```
use std::{
collections::HashMap,
ffi::OsStr,
io::{self},
path::{Path, PathBuf},
@@ -270,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,
}
@@ -459,6 +466,8 @@ pub enum NotmuchError {
StringUtf8Error(#[from] std::string::FromUtf8Error),
#[error("failed to parse str as int")]
ParseIntError(#[from] std::num::ParseIntError),
#[error("failed to parse mail: {0}")]
MailParseError(#[from] mailparse::MailParseError),
}
#[derive(Default)]
@@ -605,6 +614,80 @@ impl Notmuch {
Ok(serde_json::from_slice(&res)?)
}
#[instrument(skip_all)]
pub fn unread_recipients(&self) -> Result<HashMap<String, usize>, NotmuchError> {
let slice = self.run_notmuch([
"show",
"--include-html=false",
"--entire-thread=false",
"--body=false",
"--format=json",
// Arbitrary limit to prevent too much work
"--limit=1000",
"is:unread",
])?;
// Notmuch returns JSON with invalid unicode. So we lossy convert it to a string here and
// use that for parsing in rust.
let s = String::from_utf8_lossy(&slice);
let mut deserializer = serde_json::Deserializer::from_str(&s);
deserializer.disable_recursion_limit();
let ts: ThreadSet = serde::de::Deserialize::deserialize(&mut deserializer)?;
deserializer.end()?;
let mut r = HashMap::new();
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;
}
});
}
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)?;
}
}
}
Ok(r)
}
fn run_notmuch<I, S>(&self, args: I) -> Result<Vec<u8>, NotmuchError>
where
I: IntoIterator<Item = S>,

View File

@@ -1,11 +1,12 @@
[package]
name = "letterbox-procmail2notmuch"
version = "0.6.0"
edition = "2021"
description = "Tool for generating notmuch rules from procmail"
license = "UNLICENSED"
repository = "https://git.z.xinu.tv/wathiede/letterbox"
publish = ["xinu"]
authors.workspace = true
edition.workspace = true
license.workspace = true
publish.workspace = true
repository.workspace = true
version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@@ -1,12 +1,13 @@
[package]
name = "letterbox-server"
version = "0.6.0"
edition = "2021"
default-run = "letterbox-server"
description = "Backend for letterbox"
license = "UNLICENSED"
repository = "https://git.z.xinu.tv/wathiede/letterbox"
publish = ["xinu"]
authors.workspace = true
edition.workspace = true
license.workspace = true
publish.workspace = true
repository.workspace = true
version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -46,9 +47,9 @@ url = "2.5.2"
urlencoding = "2.1.3"
#xtracing = { path = "../../xtracing" }
#xtracing = { git = "http://git-private.h.xinu.tv/wathiede/xtracing.git" }
xtracing = { version = "0.2.0", registry = "xinu" }
letterbox-notmuch = { version = "0.1.0", registry = "xinu" }
letterbox-shared = { version = "0.3.0", registry = "xinu" }
xtracing = { version = "0.3.0", 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"

View File

@@ -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>();

View File

@@ -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(" ")
}
}
@@ -793,7 +805,17 @@ impl FromStr for Query {
if word == "is:unread" {
unread_only = true
} else if word.starts_with("tag:") {
tags.push(word["tag:".len()..].to_string());
let t = &word["tag:".len()..];
// Per-address emails are faked as `tag:@<domain>/<username>`, rewrite to `to:` form
if t.starts_with('@') && t.contains('.') {
let t = match t.split_once('/') {
None => format!("to:{t}"),
Some((domain, user)) => format!("to:{user}{domain}"),
};
remainder.push(t);
} else {
tags.push(t.to_string());
};
/*
} else if word.starts_with("tag:") {

View File

@@ -1,11 +1,7 @@
use std::{
collections::HashMap,
fs::File,
hash::{DefaultHasher, Hash, Hasher},
time::Instant,
};
use std::{collections::HashMap, fs::File};
use letterbox_notmuch::Notmuch;
use letterbox_shared::compute_color;
use log::{error, info, warn};
use mailparse::{parse_content_type, parse_mail, MailHeader, MailHeaderMap, ParsedMail};
use memmap::MmapOptions;
@@ -107,7 +103,6 @@ pub async fn search(
#[instrument(name="nm::tags", skip_all, fields(needs_unread=needs_unread))]
pub fn tags(nm: &Notmuch, needs_unread: bool) -> Result<Vec<Tag>, ServerError> {
let now = Instant::now();
let unread_msg_cnt: HashMap<String, usize> = if needs_unread {
// 10000 is an arbitrary number, if there's more than 10k unread messages, we'll
// get an inaccurate count.
@@ -123,13 +118,11 @@ pub fn tags(nm: &Notmuch, needs_unread: bool) -> Result<Vec<Tag>, ServerError> {
} else {
HashMap::new()
};
let tags = nm
let tags: Vec<_> = nm
.tags()?
.into_iter()
.map(|tag| {
let mut hasher = DefaultHasher::new();
tag.hash(&mut hasher);
let hex = format!("#{:06x}", hasher.finish() % (1 << 24));
let hex = compute_color(&tag);
let unread = if needs_unread {
*unread_msg_cnt.get(&tag).unwrap_or(&0)
} else {
@@ -142,8 +135,24 @@ pub fn tags(nm: &Notmuch, needs_unread: bool) -> Result<Vec<Tag>, ServerError> {
unread,
}
})
.chain(
nm.unread_recipients()?
.into_iter()
.filter_map(|(name, unread)| {
let Some(idx) = name.find('@') else {
return None;
};
let name = format!("{}/{}", &name[idx..], &name[..idx]);
let bg_color = compute_color(&name);
Some(Tag {
name,
fg_color: "white".to_string(),
bg_color,
unread,
})
}),
)
.collect();
info!("Fetching tags took {} seconds", now.elapsed().as_secs_f32());
Ok(tags)
}
@@ -187,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
@@ -306,6 +317,8 @@ pub async fn thread(
body,
path,
attachments,
delivered_to,
x_original_to,
});
}
messages.reverse();

View File

@@ -1,15 +1,16 @@
[package]
name = "letterbox-shared"
version = "0.6.0"
edition = "2021"
description = "Shared module for letterbox"
license = "UNLICENSED"
repository = "https://git.z.xinu.tv/wathiede/letterbox"
publish = ["xinu"]
authors.workspace = true
edition.workspace = true
license.workspace = true
publish.workspace = true
repository.workspace = true
version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
build-info = "0.0.39"
letterbox-notmuch = { version = "0.1.0", registry = "xinu" }
letterbox-notmuch = { version = "0.8.6", path = "../notmuch", registry = "xinu" }
serde = { version = "1.0.147", features = ["derive"] }

View File

@@ -1,12 +1,12 @@
[package]
version = "0.6.0"
name = "letterbox-web"
authors = ["Bill Thiede <git@xinu.tv>"]
edition = "2021"
description = "Web frontend for letterbox"
license = "UNLICENSED"
repository = "https://git.z.xinu.tv/wathiede/letterbox"
publish = ["xinu"]
authors.workspace = true
edition.workspace = true
license.workspace = true
publish.workspace = true
repository.workspace = true
version.workspace = true
[build-dependencies]
build-info-build = "0.0.39"
@@ -33,9 +33,9 @@ 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.3.0", registry = "xinu" }
letterbox-notmuch = { version = "0.3.0", registry = "xinu" }
seed_hooks = { version = "0.1.13", 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]
wasm-opt = ['-Os']

View File

@@ -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,

View File

@@ -31,6 +31,14 @@ query ShowThreadQuery($threadId: String!) {
name
addr
}
xOriginalTo {
name
addr
}
deliveredTo {
name
addr
}
timestamp
body {
__typename

View File

@@ -1,4 +1,4 @@
use std::collections::HashSet;
use std::{cmp::Ordering, collections::HashSet};
use chrono::{DateTime, Datelike, Duration, Local, Utc};
use human_format::{Formatter, Scales};
@@ -6,8 +6,8 @@ use itertools::Itertools;
use letterbox_shared::compute_color;
use log::{debug, error, info};
use seed::{prelude::*, *};
use seed_hooks::{state_access::CloneState, topo, use_state};
use web_sys::HtmlElement;
use seed_hooks::{state_access::CloneState, topo, use_state, StateAccessEventHandlers};
use web_sys::{HtmlElement, HtmlInputElement};
use crate::{
api::urls,
@@ -60,6 +60,7 @@ mod tw_classes {
"accent-green-600",
"appearance-none",
"checked:appearance-auto",
"indeterminate:appearance-auto",
"rounded",
"border",
"border-neutral-500",
@@ -95,7 +96,6 @@ pub fn view(model: &Model) -> Node<Msg> {
"lg:flex-nowrap",
"w-full"
],
reading_progress(model.read_completion_ratio),
div![
C!["w-full", "lg:w-48", "flex-none", "flex", "flex-col"],
tags(model),
@@ -109,7 +109,8 @@ pub fn view(model: &Model) -> Node<Msg> {
view_header(&model.query, &model.refreshing_state, true),
content,
view_header(&model.query, &model.refreshing_state, false),
]
],
reading_progress(model.read_completion_ratio),
]
}
@@ -135,7 +136,6 @@ fn search_results(
tags.remove(idx);
};
let is_unread = unread_idx.is_some();
// TODO: add check-all button, and tristate indicator
div![
C![
"flex",
@@ -189,11 +189,12 @@ fn search_results(
]
});
let show_bulk_edit = !selected_threads.is_empty();
let all_selected = selected_threads.len() == results.len();
div![
C!["flex", "flex-col", "flex-auto", "p-4"],
search_toolbar(count, pager, show_bulk_edit),
search_toolbar(count, pager, show_bulk_edit, all_selected),
div![rows],
search_toolbar(count, pager, show_bulk_edit),
search_toolbar(count, pager, show_bulk_edit, all_selected),
]
}
@@ -310,47 +311,71 @@ fn human_age(timestamp: i64) -> String {
datetime
}
#[topo::nested]
fn search_toolbar(
count: usize,
pager: &FrontPageQuerySearchPageInfo,
show_bulk_edit: bool,
all_selected: bool,
) -> Node<Msg> {
let indeterminate = show_bulk_edit && !all_selected;
let tristate_el: ElRef<HtmlInputElement> = use_state(|| Default::default()).get();
let tri = el_ref(&tristate_el);
if let Some(tri) = tri.get() {
info!(
"setting tristate to {indeterminate}, current {}",
tri.indeterminate()
);
tri.set_indeterminate(indeterminate);
}
nav![
C!["p-4", "flex", "w-full", "justify-between"],
C!["py-4", "flex", "w-full", "justify-between"],
div![
C!["gap-2", "flex"],
IF!(show_bulk_edit =>
C!["gap-2", "flex", IF!(!show_bulk_edit => "invisible")],
div![
button![
C![&tw_classes::BUTTON, "rounded-r-none"],
attrs!{At::Title => "Mark as read"},
span![i![C!["far", "fa-envelope-open"]]],
span![C!["pl-2", "hidden", "md:inline"], "Read"],
ev(Ev::Click, |_| Msg::SelectionMarkAsRead)
],
button![
C![&tw_classes::BUTTON, "rounded-l-none"],
attrs!{At::Title => "Mark as unread"},
span![i![C!["far", "fa-envelope"]]],
span![C!["pl-2", "hidden", "md:inline"], "Unread"],
ev(Ev::Click, |_| Msg::SelectionMarkAsUnread)
]
]),
IF!(show_bulk_edit =>
C!["flex", "items-center", "mr-4"],
input![
tri,
C![&tw_classes::CHECKBOX],
attrs! {
At::Type=>"checkbox",
At::Checked=>all_selected,
}
],
ev(Ev::Input, move |_| {
if all_selected {
Msg::SelectionSetNone
} else {
Msg::SelectionSetAll
}
}),
],
div![
button![
C![&tw_classes::BUTTON, "text-red-500"],
attrs!{At::Title => "Mark as spam"},
span![i![C!["far", "fa-hand"]]],
span![C!["pl-2", "hidden", "md:inline"], "Spam"],
ev(Ev::Click, |_|
Msg::MultiMsg(vec![
Msg::SelectionAddTag("Spam".to_string()),
Msg::SelectionMarkAsRead
])
)
button![
C![&tw_classes::BUTTON, "rounded-r-none"],
attrs! {At::Title => "Mark as read"},
span![i![C!["far", "fa-envelope-open"]]],
span![C!["pl-2", "hidden", "md:inline"], "Read"],
ev(Ev::Click, |_| Msg::SelectionMarkAsRead)
],
button![
C![&tw_classes::BUTTON, "rounded-l-none"],
attrs! {At::Title => "Mark as unread"},
span![i![C!["far", "fa-envelope"]]],
span![C!["pl-2", "hidden", "md:inline"], "Unread"],
ev(Ev::Click, |_| Msg::SelectionMarkAsUnread)
]
]),
],
div![button![
C![&tw_classes::BUTTON, "text-red-500"],
attrs! {At::Title => "Mark as spam"},
span![i![C!["far", "fa-hand"]]],
span![C!["pl-2", "hidden", "md:inline"], "Spam"],
ev(Ev::Click, |_| Msg::MultiMsg(vec![
Msg::SelectionAddTag("Spam".to_string()),
Msg::SelectionMarkAsRead
]))
]],
],
div![
C!["flex", "gap-2", "items-center"],
@@ -494,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![
@@ -589,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![
@@ -857,9 +966,7 @@ fn view_content_tree(content_tree: &str) -> Node<Msg> {
}
]],
" Debug",
ev(Ev::Click, move |_| {
debug_open.set(!debug_open.get());
})
debug_open.on_click(|d| *d = !*d)
],
IF!(debug_open.get() =>
pre![C!["NOTPORTED","content-tree"], content_tree]),
@@ -879,6 +986,7 @@ fn view_header(
false
};
let query = Url::decode_uri_component(query).unwrap_or("".to_string());
nav![
C!["flex", "px-4", "pt-4", "overflow-hidden"],
a![
@@ -1008,11 +1116,23 @@ pub fn tags(model: &Model) -> Node<Msg> {
}
tag_els
}
let unread = model
let mut unread = model
.tags
.as_ref()
.map(|tags| tags.iter().filter(|t| t.unread > 0).collect())
.unwrap_or(Vec::new());
unread.sort_by(|a, b| {
let r = if a.name.starts_with('@') && b.name.starts_with('@') {
a.name.cmp(&b.name)
} else if a.name.starts_with('@') {
Ordering::Less
} else if b.name.starts_with('@') {
Ordering::Greater
} else {
a.name.cmp(&b.name)
};
return r;
});
let tags_open = use_state(|| false);
let force_tags_open = unread.is_empty();
aside![
@@ -1030,9 +1150,7 @@ pub fn tags(model: &Model) -> Node<Msg> {
"fa-angle-down"
}
]]),
ev(Ev::Click, move |_| {
tags_open.set(!tags_open.get());
})
tags_open.on_click(|t| *t = !*t)
],
div![
IF!(force_tags_open||tags_open.get() => model.tags.as_ref().map(|tags| view_tags(tags.iter(),false))),