Compare commits

..

8 Commits

Author SHA1 Message Date
Manos Pitsidianakis d07a0487e6
WIP: web: implement search history
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-16 10:45:25 +02:00
Manos Pitsidianakis f7039e1997
web: don't panic when calculating list posts
When going through a list's root messages, use filter_map() instead of
map() to avoid panicking in case the Envelope cannot be parsed or
there's a bug in the thread calculation.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-10 11:30:54 +02:00
Manos Pitsidianakis 05333385a8
Fix new clippy lints.
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-10 11:30:54 +02:00
Kevin Schoon 0007bb30c5
add ability to export lists in mbox format
This adds the ability to export mailing lists, threads, or individual messages
in the mboxcl2 format directly from the database.
2024-01-10 11:30:54 +02:00
Kevin Schoon 0216cc1276
bump melib dependency to a modern version
This updates the melib dependency from an old version it was pinned to and
updates the associated code and tests.
2024-01-10 11:30:53 +02:00
Kevin Schoon a9a50f4659
add TO_ADDRESS as an environment variable for sendmail command
This fixes a bug where when using the sendmail command the server sends mail
to the mailing list address rather than subscribers of the list. Additionally
if the sendmail command exits with a non-zero exit code mpot will now output
stderr for diagnostic purposes.
2024-01-10 11:30:17 +02:00
Kevin Schoon 3a515c2718
move thread listing to core 2024-01-10 11:27:15 +02:00
Kevin Schoon 27dd84e1ff bump rusqlite to v0.30.0 2023-12-29 15:57:07 +00:00
18 changed files with 634 additions and 317 deletions

115
Cargo.lock generated
View File

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

View File

@ -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};

View File

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

View File

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

View File

@ -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) =

View File

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

View File

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

View File

@ -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
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)?,
);
}

View File

@ -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!(),
}
}

View File

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

View File

@ -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..&mldr;</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..&mldr;</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>&nbsp;<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 %}&nbsp;<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">👤&nbsp;</span><span class="from" title="post author">{{ post.address }}</span><span aria-hidden="true"> 📆&nbsp;</span><span class="date" title="post date">{{ post.datetime }}</span></span>
{% if post.replies > 0 %}<span class="metadata"><span aria-hidden="true">&#x1F493;&nbsp;</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">&#x1F493;&nbsp;</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 %}

View File

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