Compare commits
8 Commits
b537f22c09
...
d07a0487e6
Author | SHA1 | Date |
---|---|---|
Manos Pitsidianakis | d07a0487e6 | |
Manos Pitsidianakis | f7039e1997 | |
Manos Pitsidianakis | 05333385a8 | |
Kevin Schoon | 0007bb30c5 | |
Kevin Schoon | 0216cc1276 | |
Kevin Schoon | a9a50f4659 | |
Kevin Schoon | 3a515c2718 | |
Kevin Schoon | 27dd84e1ff |
|
@ -2,6 +2,12 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "adler"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.7.6"
|
||||
|
@ -308,7 +314,7 @@ checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39"
|
|||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"headers",
|
||||
|
@ -504,6 +510,15 @@ version = "1.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "0.3.8"
|
||||
|
@ -787,7 +802,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "914c8c79fb560f238ef6429439a30023c862f7a28e688c58f7203f12b29970bd"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"clap_lex",
|
||||
"once_cell",
|
||||
"strsim",
|
||||
|
@ -921,6 +936,15 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.15"
|
||||
|
@ -1304,9 +1328,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
|
||||
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-streaming-iterator"
|
||||
|
@ -1361,6 +1385,16 @@ dependencies = [
|
|||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "float-cmp"
|
||||
version = "0.9.0"
|
||||
|
@ -1416,7 +1450,7 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"fsevent-sys",
|
||||
]
|
||||
|
||||
|
@ -1435,7 +1469,7 @@ version = "0.3.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"fuchsia-zircon-sys",
|
||||
]
|
||||
|
||||
|
@ -1579,7 +1613,7 @@ version = "0.17.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b7905cdfe33d31a88bb2e8419ddd054451f5432d1da9eaf2ac7804ee1ea12d5"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
"libgit2-sys",
|
||||
"log",
|
||||
|
@ -1647,7 +1681,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"bytes",
|
||||
"headers-core",
|
||||
"http",
|
||||
|
@ -1857,7 +1891,7 @@ version = "0.7.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
@ -2057,9 +2091,9 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
|
|||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.25.2"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa"
|
||||
checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
|
@ -2298,31 +2332,37 @@ checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40"
|
|||
|
||||
[[package]]
|
||||
name = "melib"
|
||||
version = "0.7.2"
|
||||
source = "git+https://github.com/meli/meli?rev=2447a2c#2447a2cbfeaa8d6f7ec11a2a8a6f3be1ff2fea58"
|
||||
version = "0.8.5-rc.3"
|
||||
source = "git+https://git.meli-email.org/meli/meli.git?rev=64e60cb#64e60cb0ee79841ab40e3dba94ac27150a264c5c"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"base64 0.13.1",
|
||||
"bincode",
|
||||
"bitflags",
|
||||
"bitflags 2.4.1",
|
||||
"data-encoding",
|
||||
"encoding",
|
||||
"encoding_rs",
|
||||
"flate2",
|
||||
"futures",
|
||||
"indexmap",
|
||||
"libc",
|
||||
"libloading",
|
||||
"log",
|
||||
"native-tls",
|
||||
"nix",
|
||||
"nom",
|
||||
"notify",
|
||||
"polling",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"socket2",
|
||||
"unicode-segmentation",
|
||||
"uuid",
|
||||
"xdg",
|
||||
"xdg-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2370,6 +2410,15 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
|
||||
dependencies = [
|
||||
"adler",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.6.23"
|
||||
|
@ -2460,7 +2509,7 @@ version = "0.24.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"memoffset",
|
||||
|
@ -2488,7 +2537,7 @@ version = "4.0.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"filetime",
|
||||
"fsevent",
|
||||
"fsevent-sys",
|
||||
|
@ -2628,7 +2677,7 @@ version = "0.10.52"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"cfg-if 1.0.0",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
|
@ -2875,7 +2924,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"cfg-if 1.0.0",
|
||||
"concurrent-queue",
|
||||
"libc",
|
||||
|
@ -3020,7 +3069,7 @@ version = "0.2.16"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3029,7 +3078,7 @@ version = "0.3.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3130,7 +3179,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
|
@ -3157,11 +3206,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.28.0"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a"
|
||||
checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.4.1",
|
||||
"chrono",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
|
@ -3196,7 +3245,7 @@ version = "0.37.19"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"errno",
|
||||
"io-lifetimes",
|
||||
"libc",
|
||||
|
@ -3312,7 +3361,7 @@ version = "2.8.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
|
@ -3848,7 +3897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
|
@ -3867,7 +3916,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "5d1d42a9b3f3ec46ba828e8d376aec14592ea199f70a06a548587ecd1c4ab658"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
|
@ -4399,12 +4448,6 @@ dependencies = [
|
|||
"home",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xdg-utils"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db9fefe62d5969721e2cfc529e6a760901cc0da422b6d67e7bfd18e69490dba6"
|
||||
|
||||
[[package]]
|
||||
name = "xz2"
|
||||
version = "0.1.7"
|
||||
|
|
|
@ -23,7 +23,7 @@ use std::{
|
|||
io::Write,
|
||||
};
|
||||
|
||||
use clap::ArgAction;
|
||||
use clap::{ArgAction, CommandFactory};
|
||||
use clap_mangen::{roff, Man};
|
||||
use roff::{bold, italic, roman, Inline, Roff};
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
pub use std::path::PathBuf;
|
||||
|
||||
pub use clap::{builder::TypedValueParser, Args, CommandFactory, Parser, Subcommand};
|
||||
pub use clap::{builder::TypedValueParser, Args, Parser, Subcommand};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
|
|
|
@ -27,7 +27,7 @@ use std::{
|
|||
|
||||
use mailpot::{
|
||||
melib,
|
||||
melib::{backends::maildir::MaildirPathTrait, smol, Envelope, EnvelopeHash},
|
||||
melib::{maildir::MaildirPathTrait, smol, Envelope, EnvelopeHash},
|
||||
models::{changesets::*, *},
|
||||
queue::{Queue, QueueEntry},
|
||||
transaction::TransactionBehavior,
|
||||
|
@ -637,6 +637,7 @@ pub fn flush_queue(db: &mut Connection, dry_run: bool, verbose: u8, debug: bool)
|
|||
let mut child = std::process::Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(cmd)
|
||||
.env("TO_ADDRESS", msg.to_addresses.clone())
|
||||
.stdout(Stdio::piped())
|
||||
.stdin(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
|
@ -667,6 +668,15 @@ pub fn flush_queue(db: &mut Connection, dry_run: bool, verbose: u8, debug: bool)
|
|||
process"
|
||||
))
|
||||
})?;
|
||||
let result = child.wait_with_output()?;
|
||||
if !result.status.success() {
|
||||
return Err(Error::new_external(format!(
|
||||
"{} proccess failed with exit code: {:?}\n{}",
|
||||
cmd,
|
||||
result.status.code(),
|
||||
String::from_utf8(result.stderr).unwrap()
|
||||
)));
|
||||
}
|
||||
Ok::<(), Error>(())
|
||||
})?;
|
||||
Ok(())
|
||||
|
@ -792,7 +802,7 @@ pub fn import_maildir(
|
|||
EnvelopeHash(hasher.finish())
|
||||
}
|
||||
let mut buf = Vec::with_capacity(4096);
|
||||
let files = melib::backends::maildir::MaildirType::list_mail_in_maildir_fs(maildir_path, true)
|
||||
let files = melib::maildir::MaildirType::list_mail_in_maildir_fs(maildir_path, true)
|
||||
.context("Could not parse files in maildir path")?;
|
||||
let mut ctr = 0;
|
||||
for file in files {
|
||||
|
|
|
@ -31,7 +31,7 @@ pub fn datetime_header_value_lint(db: &mut Connection, dry_run: bool) -> Result<
|
|||
let iter = stmt.query_map([], |row| {
|
||||
let pk: i64 = row.get("pk")?;
|
||||
let date_s: String = row.get("datetime")?;
|
||||
match melib::datetime::rfc822_to_timestamp(date_s.trim()) {
|
||||
match melib::utils::datetime::rfc822_to_timestamp(date_s.trim()) {
|
||||
Err(_) | Ok(0) => {
|
||||
let mut timestamp: i64 = row.get("timestamp")?;
|
||||
let created: i64 = row.get("created")?;
|
||||
|
@ -75,7 +75,11 @@ pub fn datetime_header_value_lint(db: &mut Connection, dry_run: bool) -> Result<
|
|||
{
|
||||
v.to_rfc2822()
|
||||
} else if let Some(v) = timestamp.map(|t| {
|
||||
melib::datetime::timestamp_to_string(t, Some(melib::datetime::RFC822_DATE), true)
|
||||
melib::utils::datetime::timestamp_to_string(
|
||||
t,
|
||||
Some(melib::utils::datetime::formats::RFC822_DATE),
|
||||
true,
|
||||
)
|
||||
}) {
|
||||
v
|
||||
} else if let Ok(v) =
|
||||
|
|
|
@ -109,11 +109,23 @@ fn test_out_queue_flush() {
|
|||
assert!(env.subject().starts_with(&format!("[{}] ", foo_chat.id)));
|
||||
let headers = env.other_headers();
|
||||
|
||||
assert_eq!(headers.get("List-Id"), Some(&foo_chat.id_header()));
|
||||
assert_eq!(headers.get("List-Help"), foo_chat.help_header().as_ref());
|
||||
assert_eq!(
|
||||
headers.get("List-Post"),
|
||||
foo_chat.post_header(Some(&post_policy)).as_ref()
|
||||
headers
|
||||
.get(melib::HeaderName::LIST_ID)
|
||||
.map(|header| header.to_string()),
|
||||
Some(foo_chat.id_header())
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(melib::HeaderName::LIST_HELP)
|
||||
.map(|header| header.to_string()),
|
||||
foo_chat.help_header()
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(melib::HeaderName::LIST_POST)
|
||||
.map(|header| header.to_string()),
|
||||
foo_chat.post_header(Some(&post_policy))
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -306,11 +318,21 @@ fn test_list_requests_submission() {
|
|||
let headers_fn = |env: &melib::Envelope| {
|
||||
let headers = env.other_headers();
|
||||
|
||||
assert_eq!(headers.get("List-Id"), Some(&foo_chat.id_header()));
|
||||
assert_eq!(headers.get("List-Help"), foo_chat.help_header().as_ref());
|
||||
assert_eq!(
|
||||
headers.get("List-Post"),
|
||||
foo_chat.post_header(Some(&post_policy)).as_ref()
|
||||
headers.get(melib::HeaderName::LIST_ID),
|
||||
Some(foo_chat.id_header().as_str())
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(melib::HeaderName::LIST_HELP)
|
||||
.map(|header| header.to_string()),
|
||||
foo_chat.help_header()
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(melib::HeaderName::LIST_POST)
|
||||
.map(|header| header.to_string()),
|
||||
foo_chat.post_header(Some(&post_policy))
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -18,10 +18,10 @@ anyhow = "1.0.58"
|
|||
chrono = { version = "^0.4", features = ["serde", ] }
|
||||
jsonschema = { version = "0.17", default-features = false }
|
||||
log = "0.4"
|
||||
melib = { version = "*", default-features = false, features = ["smtp", "unicode_algorithms", "maildir_backend"], git = "https://github.com/meli/meli", rev = "2447a2c" }
|
||||
melib = { default-features = false, features = ["mbox", "smtp", "unicode-algorithms", "maildir"], git = "https://git.meli-email.org/meli/meli.git", rev = "64e60cb" }
|
||||
minijinja = { version = "0.31.0", features = ["source", ] }
|
||||
percent-encoding = { version = "^2.1" }
|
||||
rusqlite = { version = "^0.28", features = ["bundled", "functions", "trace", "hooks", "serde_json", "array", "chrono", "unlock_notify"] }
|
||||
rusqlite = { version = "^0.30", features = ["bundled", "functions", "trace", "hooks", "serde_json", "array", "chrono", "unlock_notify"] }
|
||||
serde = { version = "^1", features = ["derive", ] }
|
||||
serde_json = "^1"
|
||||
thiserror = { version = "1.0.48", default-features = false }
|
||||
|
|
|
@ -32,6 +32,7 @@ use crate::{
|
|||
config::Configuration,
|
||||
errors::{ErrorKind::*, *},
|
||||
models::{changesets::MailingListChangeset, DbVal, ListOwner, MailingList, Post},
|
||||
StripCarets,
|
||||
};
|
||||
|
||||
/// A connection to a `mailpot` database.
|
||||
|
@ -592,6 +593,164 @@ impl Connection {
|
|||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Fetch the contents of a single thread in the form of `(depth, post)`
|
||||
/// where `depth` is the reply distance between a message and the thread
|
||||
/// root message.
|
||||
pub fn list_thread(&self, list_pk: i64, root: &str) -> Result<Vec<(i64, DbVal<Post>)>> {
|
||||
let mut stmt = self
|
||||
.connection
|
||||
.prepare(
|
||||
"WITH RECURSIVE cte_replies AS MATERIALIZED
|
||||
(
|
||||
SELECT
|
||||
pk,
|
||||
message_id,
|
||||
REPLACE(
|
||||
TRIM(
|
||||
SUBSTR(
|
||||
CAST(message AS TEXT),
|
||||
INSTR(
|
||||
CAST(message AS TEXT),
|
||||
'In-Reply-To: '
|
||||
)
|
||||
+
|
||||
LENGTH('in-reply-to: '),
|
||||
INSTR(
|
||||
SUBSTR(
|
||||
CAST(message AS TEXT),
|
||||
INSTR(
|
||||
CAST(message AS TEXT),
|
||||
'In-Reply-To: ')
|
||||
+
|
||||
LENGTH('in-reply-to: ')
|
||||
),
|
||||
'>'
|
||||
)
|
||||
)
|
||||
),
|
||||
' ',
|
||||
''
|
||||
) AS in_reply_to,
|
||||
INSTR(
|
||||
CAST(message AS TEXT),
|
||||
'In-Reply-To: '
|
||||
) AS offset
|
||||
FROM post
|
||||
WHERE
|
||||
offset > 0
|
||||
UNION
|
||||
SELECT
|
||||
pk,
|
||||
message_id,
|
||||
NULL AS in_reply_to,
|
||||
INSTR(
|
||||
CAST(message AS TEXT),
|
||||
'In-Reply-To: '
|
||||
) AS offset
|
||||
FROM post
|
||||
WHERE
|
||||
offset = 0
|
||||
),
|
||||
cte_thread(parent, root, depth) AS (
|
||||
SELECT DISTINCT
|
||||
message_id AS parent,
|
||||
message_id AS root,
|
||||
0 AS depth
|
||||
FROM cte_replies
|
||||
WHERE
|
||||
in_reply_to IS NULL
|
||||
UNION ALL
|
||||
SELECT
|
||||
t.message_id AS parent,
|
||||
cte_thread.root AS root,
|
||||
(cte_thread.depth + 1) AS depth
|
||||
FROM cte_replies
|
||||
AS t
|
||||
JOIN
|
||||
cte_thread
|
||||
ON cte_thread.parent = t.in_reply_to
|
||||
WHERE t.in_reply_to IS NOT NULL
|
||||
)
|
||||
SELECT * FROM cte_thread WHERE root = ? ORDER BY root, depth;",
|
||||
)
|
||||
.unwrap();
|
||||
let iter = stmt.query_map(rusqlite::params![root], |row| {
|
||||
let parent: String = row.get("parent")?;
|
||||
let root: String = row.get("root")?;
|
||||
let depth: i64 = row.get("depth")?;
|
||||
Ok((parent, root, depth))
|
||||
})?;
|
||||
let mut ret = vec![];
|
||||
for post in iter {
|
||||
ret.push(post?);
|
||||
}
|
||||
let posts = self.list_posts(list_pk, None)?;
|
||||
let ret = ret
|
||||
.into_iter()
|
||||
.filter_map(|(m, _, depth)| {
|
||||
posts
|
||||
.iter()
|
||||
.find(|p| m.as_str().strip_carets() == p.message_id.as_str().strip_carets())
|
||||
.map(|p| (depth, p.clone()))
|
||||
})
|
||||
.skip(1)
|
||||
.collect();
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Export a list, message, or thread in mbox format
|
||||
pub fn export_mbox(
|
||||
&self,
|
||||
pk: i64,
|
||||
message_id: Option<&str>,
|
||||
as_thread: bool,
|
||||
) -> Result<Vec<u8>> {
|
||||
let posts: Result<Vec<DbVal<Post>>> = {
|
||||
if let Some(message_id) = message_id {
|
||||
if as_thread {
|
||||
// export a thread
|
||||
let thread = self.list_thread(pk, message_id)?;
|
||||
Ok(thread.iter().map(|item| item.1.clone()).collect())
|
||||
} else {
|
||||
// export a single message
|
||||
let message =
|
||||
self.list_post_by_message_id(pk, message_id)?
|
||||
.ok_or_else(|| {
|
||||
Error::from(format!("no message with id: {}", message_id))
|
||||
})?;
|
||||
Ok(vec![message])
|
||||
}
|
||||
} else {
|
||||
// export the entire mailing list
|
||||
let posts = self.list_posts(pk, None)?;
|
||||
Ok(posts)
|
||||
}
|
||||
};
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
let mailbox = melib::mbox::MboxFormat::default();
|
||||
for post in posts? {
|
||||
let envelope_from = if let Some(address) = post.0.envelope_from {
|
||||
let address = melib::Address::try_from(address.as_str())?;
|
||||
Some(address)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let envelope = melib::Envelope::from_bytes(&post.0.message, None)?;
|
||||
mailbox.append(
|
||||
&mut buf,
|
||||
&post.0.message.to_vec(),
|
||||
envelope_from.as_ref(),
|
||||
Some(envelope.timestamp),
|
||||
(melib::Flag::PASSED, vec![]),
|
||||
melib::mbox::MboxMetadata::None,
|
||||
false,
|
||||
false,
|
||||
)?;
|
||||
}
|
||||
buf.flush()?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Fetch the owners of a mailing list.
|
||||
pub fn list_owners(&self, pk: i64) -> Result<Vec<DbVal<ListOwner>>> {
|
||||
let mut stmt = self
|
||||
|
@ -1156,4 +1315,67 @@ mod tests {
|
|||
tx.commit().unwrap();
|
||||
assert_eq!(&db.lists().unwrap(), &[new, new2, new3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mbox_export() {
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::SendMail;
|
||||
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
let db_path = tmp_dir.path().join("mpot.db");
|
||||
let data_path = tmp_dir.path().to_path_buf();
|
||||
let config = Configuration {
|
||||
send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
|
||||
db_path,
|
||||
data_path,
|
||||
administrators: vec![],
|
||||
};
|
||||
let list = MailingList {
|
||||
pk: 0,
|
||||
name: "test".into(),
|
||||
id: "test".into(),
|
||||
description: None,
|
||||
topics: vec![],
|
||||
address: "test@example.com".into(),
|
||||
archive_url: None,
|
||||
};
|
||||
|
||||
let test_emails = vec![
|
||||
r#"From: "User Name" <user@example.com>
|
||||
To: "test" <test@example.com>
|
||||
Subject: Hello World
|
||||
|
||||
Hello, this is a message.
|
||||
|
||||
Goodbye!
|
||||
|
||||
"#,
|
||||
r#"From: "User Name" <user@example.com>
|
||||
To: "test" <test@example.com>
|
||||
Subject: Fuu Bar
|
||||
|
||||
Baz,
|
||||
|
||||
Qux!
|
||||
|
||||
"#,
|
||||
];
|
||||
let db = Connection::open_or_create_db(config).unwrap().trusted();
|
||||
db.create_list(list).unwrap();
|
||||
for email in test_emails {
|
||||
let envelope = melib::Envelope::from_bytes(email.as_bytes(), None).unwrap();
|
||||
db.post(&envelope, email.as_bytes(), false).unwrap();
|
||||
}
|
||||
let mbox = String::from_utf8(db.export_mbox(1, None, false).unwrap()).unwrap();
|
||||
assert!(
|
||||
mbox.split('\n').fold(0, |accm, line| {
|
||||
if line.starts_with("From MAILER-DAEMON") {
|
||||
accm + 1
|
||||
} else {
|
||||
accm
|
||||
}
|
||||
}) == 2
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,9 +40,8 @@
|
|||
|
||||
mod settings;
|
||||
use log::trace;
|
||||
use melib::Address;
|
||||
use melib::{Address, HeaderName};
|
||||
use percent_encoding::utf8_percent_encode;
|
||||
pub use settings::*;
|
||||
|
||||
use crate::{
|
||||
mail::{ListContext, MailJob, PostAction, PostEntry},
|
||||
|
@ -168,7 +167,7 @@ impl PostFilter for AddListHeaders {
|
|||
trace!("Running AddListHeaders filter");
|
||||
let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap();
|
||||
let sender = format!("<{}>", ctx.list.address);
|
||||
headers.push((&b"Sender"[..], sender.as_bytes()));
|
||||
headers.push((HeaderName::SENDER, sender.as_bytes()));
|
||||
|
||||
let list_id = Some(ctx.list.id_header());
|
||||
let list_help = ctx.list.help_header();
|
||||
|
@ -182,12 +181,12 @@ impl PostFilter for AddListHeaders {
|
|||
let list_archive = ctx.list.archive_header();
|
||||
|
||||
for (hdr, val) in [
|
||||
(b"List-Id".as_slice(), &list_id),
|
||||
(b"List-Help".as_slice(), &list_help),
|
||||
(b"List-Post".as_slice(), &list_post),
|
||||
(b"List-Unsubscribe".as_slice(), &list_unsubscribe),
|
||||
(b"List-Subscribe".as_slice(), &list_subscribe),
|
||||
(b"List-Archive".as_slice(), &list_archive),
|
||||
(HeaderName::LIST_ID, &list_id),
|
||||
(HeaderName::LIST_HELP, &list_help),
|
||||
(HeaderName::LIST_POST, &list_post),
|
||||
(HeaderName::LIST_UNSUBSCRIBE, &list_unsubscribe),
|
||||
(HeaderName::LIST_SUBSCRIBE, &list_subscribe),
|
||||
(HeaderName::LIST_ARCHIVE, &list_archive),
|
||||
] {
|
||||
if let Some(val) = val {
|
||||
headers.push((hdr, val.as_bytes()));
|
||||
|
@ -197,13 +196,13 @@ impl PostFilter for AddListHeaders {
|
|||
let mut new_vec = Vec::with_capacity(
|
||||
headers
|
||||
.iter()
|
||||
.map(|(h, v)| h.len() + v.len() + ": \r\n".len())
|
||||
.map(|(h, v)| h.as_str().as_bytes().len() + v.len() + ": \r\n".len())
|
||||
.sum::<usize>()
|
||||
+ "\r\n\r\n".len()
|
||||
+ body.len(),
|
||||
);
|
||||
for (h, v) in headers {
|
||||
new_vec.extend_from_slice(h);
|
||||
new_vec.extend_from_slice(h.as_str().as_bytes());
|
||||
new_vec.extend_from_slice(b": ");
|
||||
new_vec.extend_from_slice(v);
|
||||
new_vec.extend_from_slice(b"\r\n");
|
||||
|
@ -239,28 +238,25 @@ impl PostFilter for AddSubjectTagPrefix {
|
|||
trace!("Running AddSubjectTagPrefix filter");
|
||||
let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap();
|
||||
let mut subject;
|
||||
if let Some((_, subj_val)) = headers
|
||||
.iter_mut()
|
||||
.find(|(k, _)| k.eq_ignore_ascii_case(b"Subject"))
|
||||
{
|
||||
if let Some((_, subj_val)) = headers.iter_mut().find(|(k, _)| k == HeaderName::SUBJECT) {
|
||||
subject = format!("[{}] ", ctx.list.id).into_bytes();
|
||||
subject.extend(subj_val.iter().cloned());
|
||||
*subj_val = subject.as_slice();
|
||||
} else {
|
||||
subject = format!("[{}] (no subject)", ctx.list.id).into_bytes();
|
||||
headers.push((&b"Subject"[..], subject.as_slice()));
|
||||
headers.push((HeaderName::SUBJECT, subject.as_slice()));
|
||||
}
|
||||
|
||||
let mut new_vec = Vec::with_capacity(
|
||||
headers
|
||||
.iter()
|
||||
.map(|(h, v)| h.len() + v.len() + ": \r\n".len())
|
||||
.map(|(h, v)| h.as_str().as_bytes().len() + v.len() + ": \r\n".len())
|
||||
.sum::<usize>()
|
||||
+ "\r\n\r\n".len()
|
||||
+ body.len(),
|
||||
);
|
||||
for (h, v) in headers {
|
||||
new_vec.extend_from_slice(h);
|
||||
new_vec.extend_from_slice(h.as_str().as_bytes());
|
||||
new_vec.extend_from_slice(b": ");
|
||||
new_vec.extend_from_slice(v);
|
||||
new_vec.extend_from_slice(b"\r\n");
|
||||
|
@ -315,18 +311,18 @@ impl PostFilter for ArchivedAtLink {
|
|||
log::error!("ArchivedAtLink: {}", err);
|
||||
})?;
|
||||
let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap();
|
||||
headers.push((&b"Archived-At"[..], header_val.as_bytes()));
|
||||
headers.push((HeaderName::ARCHIVED_AT, header_val.as_bytes()));
|
||||
|
||||
let mut new_vec = Vec::with_capacity(
|
||||
headers
|
||||
.iter()
|
||||
.map(|(h, v)| h.len() + v.len() + ": \r\n".len())
|
||||
.map(|(h, v)| h.as_str().as_bytes().len() + v.len() + ": \r\n".len())
|
||||
.sum::<usize>()
|
||||
+ "\r\n\r\n".len()
|
||||
+ body.len(),
|
||||
);
|
||||
for (h, v) in headers {
|
||||
new_vec.extend_from_slice(h);
|
||||
new_vec.extend_from_slice(h.as_str().as_bytes());
|
||||
new_vec.extend_from_slice(b": ");
|
||||
new_vec.extend_from_slice(v);
|
||||
new_vec.extend_from_slice(b"\r\n");
|
||||
|
|
|
@ -320,7 +320,7 @@ impl MailingList {
|
|||
Address::new(Some(self.name.clone()), self.address.clone())
|
||||
}
|
||||
|
||||
/// List unsubscribe action as a [`MailtoAddress`](super::MailtoAddress).
|
||||
/// List unsubscribe action as a [`MailtoAddress`].
|
||||
pub fn unsubscription_mailto(&self) -> MailtoAddress {
|
||||
MailtoAddress {
|
||||
address: self.request_subaddr(),
|
||||
|
@ -328,7 +328,7 @@ impl MailingList {
|
|||
}
|
||||
}
|
||||
|
||||
/// List subscribe action as a [`MailtoAddress`](super::MailtoAddress).
|
||||
/// List subscribe action as a [`MailtoAddress`].
|
||||
pub fn subscription_mailto(&self) -> MailtoAddress {
|
||||
MailtoAddress {
|
||||
address: self.request_subaddr(),
|
||||
|
@ -336,7 +336,7 @@ impl MailingList {
|
|||
}
|
||||
}
|
||||
|
||||
/// List owner as a [`MailtoAddress`](super::MailtoAddress).
|
||||
/// List owner as a [`MailtoAddress`].
|
||||
pub fn owner_mailto(&self) -> MailtoAddress {
|
||||
let p = self.address.split('@').collect::<Vec<&str>>();
|
||||
MailtoAddress {
|
||||
|
@ -371,7 +371,7 @@ impl MailingList {
|
|||
if let Some(val) = val {
|
||||
draft
|
||||
.headers
|
||||
.insert(melib::HeaderName::new_unchecked(hdr), val);
|
||||
.insert(melib::HeaderName::try_from(hdr).unwrap(), val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,9 +19,6 @@
|
|||
|
||||
//! How each list handles new posts and new subscriptions.
|
||||
|
||||
pub use post_policy::*;
|
||||
pub use subscription_policy::*;
|
||||
|
||||
mod post_policy {
|
||||
use log::trace;
|
||||
use rusqlite::OptionalExtension;
|
||||
|
|
|
@ -67,7 +67,7 @@ pub struct PostfixConfiguration {
|
|||
#[serde(default)]
|
||||
pub process_limit: Option<u64>,
|
||||
/// The directory in which the map files are saved.
|
||||
/// Default is `data_path` from [`Configuration`](crate::Configuration).
|
||||
/// Default is `data_path` from [`Configuration`].
|
||||
#[serde(default)]
|
||||
pub map_output_path: Option<PathBuf>,
|
||||
/// The name of the Postfix service name to use.
|
||||
|
|
|
@ -46,9 +46,9 @@ impl Connection {
|
|||
let datetime: std::borrow::Cow<'_, str> = if !env.date.is_empty() {
|
||||
env.date.as_str().into()
|
||||
} else {
|
||||
melib::datetime::timestamp_to_string(
|
||||
melib::utils::datetime::timestamp_to_string(
|
||||
env.timestamp,
|
||||
Some(melib::datetime::RFC822_DATE),
|
||||
Some(melib::utils::datetime::formats::RFC822_DATE),
|
||||
true,
|
||||
)
|
||||
.into()
|
||||
|
@ -710,15 +710,14 @@ impl Connection {
|
|||
})?;
|
||||
|
||||
let mut draft = templ.render(context)?;
|
||||
draft.headers.insert(
|
||||
melib::HeaderName::new_unchecked("From"),
|
||||
list.request_subaddr(),
|
||||
);
|
||||
draft
|
||||
.headers
|
||||
.insert(melib::HeaderName::FROM, list.request_subaddr());
|
||||
for addr in recipients {
|
||||
let mut draft = draft.clone();
|
||||
draft
|
||||
.headers
|
||||
.insert(melib::HeaderName::new_unchecked("To"), addr.to_string());
|
||||
.insert(melib::HeaderName::TO, addr.to_string());
|
||||
list.insert_headers(
|
||||
&mut draft,
|
||||
post_policy.as_deref(),
|
||||
|
|
|
@ -83,7 +83,7 @@ impl Template {
|
|||
};
|
||||
if let Some(ref subject) = self.subject {
|
||||
draft.headers.insert(
|
||||
HeaderName::new_unchecked("Subject"),
|
||||
HeaderName::SUBJECT,
|
||||
env.render_named_str("subject", subject, &context)?,
|
||||
);
|
||||
}
|
||||
|
|
121
web/src/lists.rs
121
web/src/lists.rs
|
@ -19,6 +19,7 @@
|
|||
|
||||
use chrono::TimeZone;
|
||||
use indexmap::IndexMap;
|
||||
use mailpot::models::Post;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -59,20 +60,30 @@ pub async fn list(
|
|||
.collect::<HashMap<String, [usize; 31]>>();
|
||||
let envelopes: Arc<std::sync::RwLock<HashMap<melib::EnvelopeHash, melib::Envelope>>> =
|
||||
Default::default();
|
||||
let mut env_lock = envelopes.write().unwrap();
|
||||
{
|
||||
let mut env_lock = envelopes.write().unwrap();
|
||||
|
||||
for post in &posts {
|
||||
let envelope = melib::Envelope::from_bytes(post.message.as_slice(), None)
|
||||
.expect("Could not parse mail");
|
||||
env_lock.insert(envelope.hash(), envelope);
|
||||
for post in &posts {
|
||||
let Ok(mut envelope) = melib::Envelope::from_bytes(post.message.as_slice(), None)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if envelope.message_id != post.message_id.as_str() {
|
||||
// If they don't match, the raw envelope doesn't contain a Message-ID and it was
|
||||
// randomly generated. So set the envelope's Message-ID to match the
|
||||
// post's, which is the "permanent" one since our source of truth is
|
||||
// the database.
|
||||
envelope.set_message_id(post.message_id.as_bytes());
|
||||
}
|
||||
env_lock.insert(envelope.hash(), envelope);
|
||||
}
|
||||
}
|
||||
let mut threads: melib::Threads = melib::Threads::new(posts.len());
|
||||
drop(env_lock);
|
||||
threads.amend(&envelopes);
|
||||
let roots = thread_roots(&envelopes, &threads);
|
||||
let posts_ctx = roots
|
||||
.into_iter()
|
||||
.map(|(thread, length, _timestamp)| {
|
||||
.filter_map(|(thread, length, _timestamp)| {
|
||||
let post = &post_map[&thread.message_id.as_str()];
|
||||
//2019-07-14T14:21:02
|
||||
if let Some(day) =
|
||||
|
@ -82,8 +93,7 @@ pub async fn list(
|
|||
{
|
||||
hists.get_mut(&post.month_year).unwrap()[day.saturating_sub(1) as usize] += 1;
|
||||
}
|
||||
let envelope = melib::Envelope::from_bytes(post.message.as_slice(), None)
|
||||
.expect("Could not parse mail");
|
||||
let envelope = melib::Envelope::from_bytes(post.message.as_slice(), None).ok()?;
|
||||
let mut msg_id = &post.message_id[1..];
|
||||
msg_id = &msg_id[..msg_id.len().saturating_sub(1)];
|
||||
let subject = envelope.subject();
|
||||
|
@ -106,7 +116,7 @@ pub async fn list(
|
|||
replies => length.saturating_sub(1),
|
||||
last_active => thread.datetime,
|
||||
};
|
||||
ret
|
||||
Some(ret)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
@ -176,7 +186,20 @@ pub async fn list_post(
|
|||
StatusCode::NOT_FOUND,
|
||||
));
|
||||
};
|
||||
let thread = super::utils::thread_db(&db, list.pk, &post.message_id);
|
||||
let thread: Vec<(i64, DbVal<Post>, String, String)> = {
|
||||
let thread: Vec<(i64, DbVal<Post>)> = db.list_thread(list.pk, &post.message_id)?;
|
||||
|
||||
thread
|
||||
.into_iter()
|
||||
.map(|(depth, p)| {
|
||||
let envelope = melib::Envelope::from_bytes(p.message.as_slice(), None).unwrap();
|
||||
let body = envelope.body_bytes(p.message.as_slice());
|
||||
let body_text = body.text();
|
||||
let date = envelope.date_as_str().to_string();
|
||||
(depth, p, body_text, date)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let envelope = melib::Envelope::from_bytes(post.message.as_slice(), None)
|
||||
.with_status(StatusCode::BAD_REQUEST)?;
|
||||
let body = envelope.body_bytes(post.message.as_slice());
|
||||
|
@ -789,13 +812,13 @@ pub async fn list_candidates(
|
|||
#[allow(non_snake_case)]
|
||||
pub async fn list_search_query_GET(
|
||||
ListSearchPath(id): ListSearchPath,
|
||||
//mut session: WritableSession,
|
||||
mut session: WritableSession,
|
||||
Query(query): Query<DateQueryParameter>,
|
||||
//auth: AuthContext,
|
||||
auth: AuthContext,
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Html<String>, ResponseError> {
|
||||
let db = Connection::open_db(state.conf.clone())?.trusted();
|
||||
let Some(_list) = (match id {
|
||||
let Some(list) = (match id {
|
||||
ListPathIdentifier::Pk(id) => db.list(id)?,
|
||||
ListPathIdentifier::Id(id) => db.list_by_id(id)?,
|
||||
}) else {
|
||||
|
@ -804,8 +827,70 @@ pub async fn list_search_query_GET(
|
|||
StatusCode::NOT_FOUND,
|
||||
));
|
||||
};
|
||||
Err(ResponseError::new(
|
||||
format!("{:#?}", query),
|
||||
StatusCode::NOT_IMPLEMENTED,
|
||||
))
|
||||
match query {
|
||||
DateQueryParameter::Month { ref month } => {
|
||||
let mut stmt = db.connection.prepare(
|
||||
"SELECT *, strftime('%Y-%m', CAST(timestamp AS INTEGER), 'unixepoch') AS \
|
||||
month_year FROM post WHERE list = ? AND month_year = ? ORDER BY timestamp DESC;",
|
||||
)?;
|
||||
let iter = stmt.query_map(rusqlite::params![&list.pk, &month], |row| {
|
||||
let pk = row.get("pk")?;
|
||||
Ok(DbVal(
|
||||
Post {
|
||||
pk,
|
||||
list: row.get("list")?,
|
||||
envelope_from: row.get("envelope_from")?,
|
||||
address: row.get("address")?,
|
||||
message_id: row.get("message_id")?,
|
||||
message: row.get("message")?,
|
||||
timestamp: row.get("timestamp")?,
|
||||
datetime: row.get("datetime")?,
|
||||
month_year: row.get("month_year")?,
|
||||
},
|
||||
pk,
|
||||
))
|
||||
})?;
|
||||
let mut ret = vec![];
|
||||
for post in iter {
|
||||
let post = post?;
|
||||
ret.push(post);
|
||||
}
|
||||
|
||||
let crumbs = crumbs![
|
||||
Crumb {
|
||||
label: list.name.clone().into(),
|
||||
url: ListPath(list.id.to_string().into()).to_crumb(),
|
||||
},
|
||||
Crumb {
|
||||
label: query.to_string().into(),
|
||||
url: ListSearchPath(ListPathIdentifier::Id(list.id.to_string())).to_crumb(),
|
||||
}
|
||||
];
|
||||
let list_owners = db.list_owners(list.pk)?;
|
||||
let mut list_obj = MailingList::from(list.clone());
|
||||
list_obj.set_safety(list_owners.as_slice(), &state.conf.administrators);
|
||||
let user_context = auth
|
||||
.current_user
|
||||
.as_ref()
|
||||
.map(|user| db.list_subscription_by_address(list.pk, &user.address).ok());
|
||||
let context = minijinja::context! {
|
||||
canonical_url => ListSearchPath(ListPathIdentifier::Id(list.id.to_string())),
|
||||
search_term => query,
|
||||
page_title => &list.name,
|
||||
description => &list.description,
|
||||
preamble => false,
|
||||
posts => ret,
|
||||
list => Value::from_object(list_obj),
|
||||
current_user => auth.current_user,
|
||||
user_context,
|
||||
messages => session.drain_messages(),
|
||||
crumbs,
|
||||
};
|
||||
Ok(Html(
|
||||
TEMPLATES.get_template("lists/list.html")?.render(context)?,
|
||||
))
|
||||
}
|
||||
DateQueryParameter::Date { date } => todo!(),
|
||||
DateQueryParameter::Range { start, end } => todo!(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -276,7 +276,7 @@ pub async fn user_list_subscription(
|
|||
.list_subscription(
|
||||
list.pk(),
|
||||
subscriptions
|
||||
.get(0)
|
||||
.first()
|
||||
.ok_or_else(|| {
|
||||
ResponseError::new(
|
||||
"Subscription not found".to_string(),
|
||||
|
|
|
@ -4,110 +4,118 @@
|
|||
<br aria-hidden="true">
|
||||
<br aria-hidden="true">
|
||||
{% endif %}
|
||||
{% if list.description %}
|
||||
<p title="mailing list description">{{ list.description }}</p>
|
||||
{% else %}
|
||||
<p title="mailing list description">No list description.</p>
|
||||
{% endif %}
|
||||
<br aria-hidden="true">
|
||||
{% if current_user and subscription_policy and subscription_policy.open %}
|
||||
{% if user_context %}
|
||||
<form method="post" action="{{ settings_path() }}" class="settings-form">
|
||||
<input type="hidden" name="type", value="unsubscribe">
|
||||
<input type="hidden" name="list_pk", value="{{ list.pk }}">
|
||||
<input type="submit" name="unsubscribe" value="Unsubscribe as {{ current_user.address }}">
|
||||
</form>
|
||||
<br />
|
||||
{% if not search_term %}
|
||||
{% if list.description %}
|
||||
<p title="mailing list description">{{ list.description }}</p>
|
||||
{% else %}
|
||||
<form method="post" action="{{ settings_path() }}" class="settings-form">
|
||||
<input type="hidden" name="type", value="subscribe">
|
||||
<input type="hidden" name="list_pk", value="{{ list.pk }}">
|
||||
<input type="submit" name="subscribe" value="Subscribe as {{ current_user.address }}">
|
||||
</form>
|
||||
<br />
|
||||
<p title="mailing list description">No list description.</p>
|
||||
{% endif %}
|
||||
<br aria-hidden="true">
|
||||
{% if current_user and subscription_policy and subscription_policy.open %}
|
||||
{% if user_context %}
|
||||
<form method="post" action="{{ settings_path() }}" class="settings-form">
|
||||
<input type="hidden" name="type", value="unsubscribe">
|
||||
<input type="hidden" name="list_pk", value="{{ list.pk }}">
|
||||
<input type="submit" name="unsubscribe" value="Unsubscribe as {{ current_user.address }}">
|
||||
</form>
|
||||
<br />
|
||||
{% else %}
|
||||
<form method="post" action="{{ settings_path() }}" class="settings-form">
|
||||
<input type="hidden" name="type", value="subscribe">
|
||||
<input type="hidden" name="list_pk", value="{{ list.pk }}">
|
||||
<input type="submit" name="subscribe" value="Subscribe as {{ current_user.address }}">
|
||||
</form>
|
||||
<br />
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if preamble %}
|
||||
<section id="preamble" class="preamble" aria-label="mailing list instructions">
|
||||
{% if preamble.custom %}
|
||||
{{ preamble.custom|safe }}
|
||||
{% else %}
|
||||
{% if subscription_policy %}
|
||||
{% if subscription_policy.open or subscription_policy.request %}
|
||||
{{ heading(3, "Subscribe") }}
|
||||
{% set subscription_mailto=list.subscription_mailto() %}
|
||||
{% if subscription_mailto %}
|
||||
{% if subscription_mailto.subject %}
|
||||
<p>
|
||||
<a href="mailto:{{ subscription_mailto.address|safe }}?subject={{ subscription_mailto.subject|safe }}"><code>{{ subscription_mailto.address }}</code></a> with the following subject: <code>{{ subscription_mailto.subject}}</code>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
<a href="mailto:{{ subscription_mailto.address|safe }}"><code>{{ subscription_mailto.address }}</code></a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>List is not open for subscriptions.</p>
|
||||
{% endif %}
|
||||
|
||||
{% set unsubscription_mailto=list.unsubscription_mailto() %}
|
||||
{% if unsubscription_mailto %}
|
||||
{{ heading(3, "Unsubscribe") }}
|
||||
{% if unsubscription_mailto.subject %}
|
||||
<p>
|
||||
<a href="mailto:{{ unsubscription_mailto.address|safe }}?subject={{ unsubscription_mailto.subject|safe }}"><code>{{ unsubscription_mailto.address }}</code></a> with the following subject: <code>{{unsubscription_mailto.subject}}</code>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
<a href="mailto:{{ unsubscription_mailto.address|safe }}"><code>{{ unsubscription_mailto.address }}</code></a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if post_policy %}
|
||||
{{ heading(3, "Post") }}
|
||||
{% if post_policy.announce_only %}
|
||||
<p>List is <em>announce-only</em>, i.e. you can only subscribe to receive announcements.</p>
|
||||
{% elif post_policy.subscription_only %}
|
||||
<p>List is <em>subscription-only</em>, i.e. you can only post if you are subscribed.</p>
|
||||
<p>If you are subscribed, you can send new posts to:
|
||||
<a href="mailto:{{ list.address| safe }}"><code>{{ list.address }}</code></a>
|
||||
</p>
|
||||
{% elif post_policy.approval_needed or post_policy.no_subscriptions %}
|
||||
<p>List is open to all posts <em>after approval</em> by the list owners.</p>
|
||||
<p>You can send new posts to:
|
||||
<a href="mailto:{{ list.address| safe }}"><code>{{ list.address }}</code></a>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>List is not open for submissions.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% if months %}
|
||||
<section class="list" aria-hidden="true">
|
||||
{{ heading(3, "Calendar") }}
|
||||
<div class="calendar">
|
||||
{%- from "calendar.html" import cal %}
|
||||
{% if has_more_months %}
|
||||
<a href="{{ list_calendar_path(list.id) }}">See all history..…</a>
|
||||
{% endif %}
|
||||
{% for date in months %}
|
||||
{{ cal(date, hists) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if preamble %}
|
||||
<section id="preamble" class="preamble" aria-label="mailing list instructions">
|
||||
{% if preamble.custom %}
|
||||
{{ preamble.custom|safe }}
|
||||
{% else %}
|
||||
{% if subscription_policy %}
|
||||
{% if subscription_policy.open or subscription_policy.request %}
|
||||
{{ heading(3, "Subscribe") }}
|
||||
{% set subscription_mailto=list.subscription_mailto() %}
|
||||
{% if subscription_mailto %}
|
||||
{% if subscription_mailto.subject %}
|
||||
<p>
|
||||
<a href="mailto:{{ subscription_mailto.address|safe }}?subject={{ subscription_mailto.subject|safe }}"><code>{{ subscription_mailto.address }}</code></a> with the following subject: <code>{{ subscription_mailto.subject}}</code>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
<a href="mailto:{{ subscription_mailto.address|safe }}"><code>{{ subscription_mailto.address }}</code></a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>List is not open for subscriptions.</p>
|
||||
{% endif %}
|
||||
|
||||
{% set unsubscription_mailto=list.unsubscription_mailto() %}
|
||||
{% if unsubscription_mailto %}
|
||||
{{ heading(3, "Unsubscribe") }}
|
||||
{% if unsubscription_mailto.subject %}
|
||||
<p>
|
||||
<a href="mailto:{{ unsubscription_mailto.address|safe }}?subject={{ unsubscription_mailto.subject|safe }}"><code>{{ unsubscription_mailto.address }}</code></a> with the following subject: <code>{{unsubscription_mailto.subject}}</code>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
<a href="mailto:{{ unsubscription_mailto.address|safe }}"><code>{{ unsubscription_mailto.address }}</code></a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if post_policy %}
|
||||
{{ heading(3, "Post") }}
|
||||
{% if post_policy.announce_only %}
|
||||
<p>List is <em>announce-only</em>, i.e. you can only subscribe to receive announcements.</p>
|
||||
{% elif post_policy.subscription_only %}
|
||||
<p>List is <em>subscription-only</em>, i.e. you can only post if you are subscribed.</p>
|
||||
<p>If you are subscribed, you can send new posts to:
|
||||
<a href="mailto:{{ list.address| safe }}"><code>{{ list.address }}</code></a>
|
||||
</p>
|
||||
{% elif post_policy.approval_needed or post_policy.no_subscriptions %}
|
||||
<p>List is open to all posts <em>after approval</em> by the list owners.</p>
|
||||
<p>You can send new posts to:
|
||||
<a href="mailto:{{ list.address| safe }}"><code>{{ list.address }}</code></a>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>List is not open for submissions.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
<section class="list" aria-hidden="true">
|
||||
{{ heading(3, "Calendar") }}
|
||||
<div class="calendar">
|
||||
{%- from "calendar.html" import cal %}
|
||||
{% if has_more_months %}
|
||||
<a href="{{ list_calendar_path(list.id) }}">See all history..…</a>
|
||||
{% endif %}
|
||||
{% for date in months %}
|
||||
{{ cal(date, hists) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
<section aria-label="mailing list posts">
|
||||
{{ heading(3, "Posts") }}
|
||||
{% if search_term %}
|
||||
{{ heading(3, "Results for " ~ search_term) }}
|
||||
{% else %}
|
||||
{{ heading(3, "Posts") }}
|
||||
{% endif %}
|
||||
<div class="posts entries" role="list" aria-label="list of mailing list posts">
|
||||
<p>{{ posts | length }} post{{ posts|length|pluralize }}</p>
|
||||
{% for post in posts %}
|
||||
<div class="entry" role="listitem" aria-labelledby="post_link_{{ loop.index }}">
|
||||
<span class="subject"><a id="post_link_{{ loop.index }}" href="{{ list_post_path(list.id, post.message_id) }}">{{ post.subject }}</a> <span class="metadata replies" title="reply count">{{ post.replies }} repl{{ post.replies|pluralize("y","ies") }}</span></span>
|
||||
<span class="subject"><a id="post_link_{{ loop.index }}" href="{{ list_post_path(list.id, post.message_id) }}">{{ post.subject }}</a>{% if post.replies %} <span class="metadata replies" title="reply count">{{ post.replies }} repl{{ post.replies|pluralize("y","ies") }}</span>{% endif %}</span>
|
||||
<span class="metadata"><span aria-hidden="true">👤 </span><span class="from" title="post author">{{ post.address }}</span><span aria-hidden="true"> 📆 </span><span class="date" title="post date">{{ post.datetime }}</span></span>
|
||||
{% if post.replies > 0 %}<span class="metadata"><span aria-hidden="true">💓 </span><span class="last-active" title="latest thread activity">{{ post.last_active }}</span></span>{% endif %}
|
||||
{% if post.replies and post.replies > 0 %}<span class="metadata"><span aria-hidden="true">💓 </span><span class="last-active" title="latest thread activity">{{ post.last_active }}</span></span>{% endif %}
|
||||
<span class="metadata"><span aria-hidden="true">🪪 </span><span class="message-id" title="e-mail Message-ID">{{ post.message_id }}</span></span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
159
web/src/utils.rs
159
web/src/utils.rs
|
@ -283,6 +283,21 @@ pub enum DateQueryParameter {
|
|||
// },
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DateQueryParameter {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Month { month: v } | Self::Date { date: v } => write!(fmt, "{}", v),
|
||||
Self::Range { start, end } => write!(fmt, "{}..{}", start, end),
|
||||
//Self::RangeFrom { start } => serializer.serialize_str(&format!("{start}..")),
|
||||
//Self::RangeInclusive { start, end } => {
|
||||
// serializer.serialize_str(&format!("{start}..={end}"))
|
||||
//}
|
||||
//Self::RangeTo { end } => serializer.serialize_str(&format!("..{end}")),
|
||||
//Self::RangeToInclusive { end } => serializer.serialize_str(&format!("..={end}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for DateQueryParameter {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
|
@ -306,6 +321,7 @@ impl<'de> serde::Deserialize<'de> for DateQueryParameter {
|
|||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Clone, Copy)]
|
||||
struct DateVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for DateVisitor {
|
||||
|
@ -348,6 +364,33 @@ impl<'de> serde::Deserialize<'de> for DateQueryParameter {
|
|||
|
||||
Err(serde::de::Error::custom("invalid"))
|
||||
}
|
||||
|
||||
fn visit_map<A>(
|
||||
self,
|
||||
mut map: A,
|
||||
) -> Result<Self::Value, <A as serde::de::MapAccess<'de>>::Error>
|
||||
where
|
||||
A: serde::de::MapAccess<'de>,
|
||||
{
|
||||
let mut params = vec![];
|
||||
while let Some((key, value)) = map.next_entry()? {
|
||||
match key {
|
||||
"datetime" | "date" | "month" => params.push(self.visit_str(value)?),
|
||||
_ => {
|
||||
return Err(serde::de::Error::invalid_type(
|
||||
serde::de::Unexpected::Map,
|
||||
&self,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if params.len() > 1 {
|
||||
return Err(serde::de::Error::invalid_length(params.len(), &self));
|
||||
}
|
||||
|
||||
Ok(params.pop().unwrap())
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_any(DateVisitor)
|
||||
}
|
||||
|
@ -419,6 +462,7 @@ where
|
|||
.serialize(ser)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ThreadEntry {
|
||||
pub hash: melib::EnvelopeHash,
|
||||
pub depth: usize,
|
||||
|
@ -430,119 +474,6 @@ pub struct ThreadEntry {
|
|||
pub datetime: String,
|
||||
}
|
||||
|
||||
pub fn thread_db(
|
||||
db: &mailpot::Connection,
|
||||
list: i64,
|
||||
root: &str,
|
||||
) -> Vec<(i64, DbVal<mailpot::models::Post>, String, String)> {
|
||||
let mut stmt = db
|
||||
.connection
|
||||
.prepare(
|
||||
"WITH RECURSIVE cte_replies AS MATERIALIZED
|
||||
(
|
||||
SELECT
|
||||
pk,
|
||||
message_id,
|
||||
REPLACE(
|
||||
TRIM(
|
||||
SUBSTR(
|
||||
CAST(message AS TEXT),
|
||||
INSTR(
|
||||
CAST(message AS TEXT),
|
||||
'In-Reply-To: '
|
||||
)
|
||||
+
|
||||
LENGTH('in-reply-to: '),
|
||||
INSTR(
|
||||
SUBSTR(
|
||||
CAST(message AS TEXT),
|
||||
INSTR(
|
||||
CAST(message AS TEXT),
|
||||
'In-Reply-To: ')
|
||||
+
|
||||
LENGTH('in-reply-to: ')
|
||||
),
|
||||
'>'
|
||||
)
|
||||
)
|
||||
),
|
||||
' ',
|
||||
''
|
||||
) AS in_reply_to,
|
||||
INSTR(
|
||||
CAST(message AS TEXT),
|
||||
'In-Reply-To: '
|
||||
) AS offset
|
||||
FROM post
|
||||
WHERE
|
||||
offset > 0
|
||||
UNION
|
||||
SELECT
|
||||
pk,
|
||||
message_id,
|
||||
NULL AS in_reply_to,
|
||||
INSTR(
|
||||
CAST(message AS TEXT),
|
||||
'In-Reply-To: '
|
||||
) AS offset
|
||||
FROM post
|
||||
WHERE
|
||||
offset = 0
|
||||
),
|
||||
cte_thread(parent, root, depth) AS (
|
||||
SELECT DISTINCT
|
||||
message_id AS parent,
|
||||
message_id AS root,
|
||||
0 AS depth
|
||||
FROM cte_replies
|
||||
WHERE
|
||||
in_reply_to IS NULL
|
||||
UNION ALL
|
||||
SELECT
|
||||
t.message_id AS parent,
|
||||
cte_thread.root AS root,
|
||||
(cte_thread.depth + 1) AS depth
|
||||
FROM cte_replies
|
||||
AS t
|
||||
JOIN
|
||||
cte_thread
|
||||
ON cte_thread.parent = t.in_reply_to
|
||||
WHERE t.in_reply_to IS NOT NULL
|
||||
)
|
||||
SELECT * FROM cte_thread WHERE root = ? ORDER BY root, depth;",
|
||||
)
|
||||
.unwrap();
|
||||
let iter = stmt
|
||||
.query_map(rusqlite::params![root], |row| {
|
||||
let parent: String = row.get("parent")?;
|
||||
let root: String = row.get("root")?;
|
||||
let depth: i64 = row.get("depth")?;
|
||||
Ok((parent, root, depth))
|
||||
})
|
||||
.unwrap();
|
||||
let mut ret = vec![];
|
||||
for post in iter {
|
||||
let post = post.unwrap();
|
||||
ret.push(post);
|
||||
}
|
||||
let posts = db.list_posts(list, None).unwrap();
|
||||
ret.into_iter()
|
||||
.filter_map(|(m, _, depth)| {
|
||||
posts
|
||||
.iter()
|
||||
.find(|p| m.as_str().strip_carets() == p.message_id.as_str().strip_carets())
|
||||
.map(|p| {
|
||||
let envelope = melib::Envelope::from_bytes(p.message.as_slice(), None).unwrap();
|
||||
let body = envelope.body_bytes(p.message.as_slice());
|
||||
let body_text = body.text();
|
||||
let date = envelope.date_as_str().to_string();
|
||||
(depth, p.clone(), body_text, date)
|
||||
})
|
||||
})
|
||||
.skip(1)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn thread(
|
||||
envelopes: &Arc<std::sync::RwLock<HashMap<melib::EnvelopeHash, melib::Envelope>>>,
|
||||
threads: &melib::Threads,
|
||||
|
@ -551,7 +482,7 @@ pub fn thread(
|
|||
let env_lock = envelopes.read().unwrap();
|
||||
let thread = threads.envelope_to_thread[&root_env_hash];
|
||||
let mut ret = vec![];
|
||||
for (depth, t) in threads.thread_group_iter(thread) {
|
||||
for (depth, t) in threads.thread_iter(thread) {
|
||||
let hash = threads.thread_nodes[&t].message.unwrap();
|
||||
ret.push(ThreadEntry {
|
||||
hash,
|
||||
|
|
Loading…
Reference in New Issue