Author: Manos Pitsidianakis [manos@pitsidianak.is]
Hash: e8120c75dbf6e80594c7081c2e40303d9dd06efe
Timestamp: Thu, 18 May 2023 07:34:00 +0000 (1 year ago)

+388 -57 +/-34 browse
core: Add topics field to MailingList
1diff --git a/README.md b/README.md
2index 4878d78..e0f7fca 100644
3--- a/README.md
4+++ b/README.md
5 @@ -228,6 +228,7 @@ let list_pk = db.create_list(MailingList {
6 id: "foo-chat".into(),
7 address: "foo-chat@example.com".into(),
8 description: None,
9+ topics: vec![],
10 archive_url: None,
11 })?.pk;
12
13 diff --git a/archive-http/src/utils.rs b/archive-http/src/utils.rs
14index 7e2597b..71905b5 100644
15--- a/archive-http/src/utils.rs
16+++ b/archive-http/src/utils.rs
17 @@ -57,6 +57,7 @@ pub struct MailingList {
18 pub id: String,
19 pub address: String,
20 pub description: Option<String>,
21+ pub topics: Vec<String>,
22 pub archive_url: Option<String>,
23 pub inner: DbVal<mailpot::models::MailingList>,
24 }
25 @@ -70,6 +71,7 @@ impl From<DbVal<mailpot::models::MailingList>> for MailingList {
26 id,
27 address,
28 description,
29+ topics,
30 archive_url,
31 },
32 _,
33 @@ -81,6 +83,7 @@ impl From<DbVal<mailpot::models::MailingList>> for MailingList {
34 id,
35 address,
36 description,
37+ topics,
38 archive_url,
39 inner: val,
40 }
41 @@ -127,13 +130,24 @@ impl minijinja::value::StructObject for MailingList {
42 "id" => Some(Value::from_serializable(&self.id)),
43 "address" => Some(Value::from_serializable(&self.address)),
44 "description" => Some(Value::from_serializable(&self.description)),
45+ "topics" => Some(Value::from_serializable(&self.topics)),
46 "archive_url" => Some(Value::from_serializable(&self.archive_url)),
47 _ => None,
48 }
49 }
50
51 fn static_fields(&self) -> Option<&'static [&'static str]> {
52- Some(&["pk", "name", "id", "address", "description", "archive_url"][..])
53+ Some(
54+ &[
55+ "pk",
56+ "name",
57+ "id",
58+ "address",
59+ "description",
60+ "topics",
61+ "archive_url",
62+ ][..],
63+ )
64 }
65 }
66
67 diff --git a/cli/Cargo.toml b/cli/Cargo.toml
68index 54a3638..49224df 100644
69--- a/cli/Cargo.toml
70+++ b/cli/Cargo.toml
71 @@ -34,4 +34,5 @@ tempfile = "3.3"
72 [build-dependencies]
73 clap = { version = "^4.2", default-features = false, features = ["derive", "cargo", "unicode", "wrap_help", "help", "usage", "error-context", "suggestions"] }
74 clap_mangen = "0.2.10"
75+ mailpot = { version = "^0.1", path = "../core" }
76 stderrlog = "^0.5"
77 diff --git a/cli/src/args.rs b/cli/src/args.rs
78index d3f79d9..5cc26e8 100644
79--- a/cli/src/args.rs
80+++ b/cli/src/args.rs
81 @@ -19,7 +19,7 @@
82
83 pub use std::path::PathBuf;
84
85- pub use clap::{Args, CommandFactory, Parser, Subcommand};
86+ pub use clap::{builder::TypedValueParser, Args, CommandFactory, Parser, Subcommand};
87
88 #[derive(Debug, Parser)]
89 #[command(
90 @@ -105,7 +105,14 @@ pub enum Command {
91 /// Mail that has not been handled properly end up in the error queue.
92 ErrorQueue {
93 #[command(subcommand)]
94- cmd: ErrorQueueCommand,
95+ cmd: QueueCommand,
96+ },
97+ /// Mail that has not been handled properly end up in the error queue.
98+ Queue {
99+ #[arg(long, value_parser = QueueValueParser)]
100+ queue: mailpot::queue::Queue,
101+ #[command(subcommand)]
102+ cmd: QueueCommand,
103 },
104 /// Import a maildir folder into an existing list.
105 ImportMaildir {
106 @@ -254,7 +261,7 @@ pub struct PostfixConfig {
107 }
108
109 #[derive(Debug, Subcommand)]
110- pub enum ErrorQueueCommand {
111+ pub enum QueueCommand {
112 /// List.
113 List,
114 /// Print entry in RFC5322 or JSON format.
115 @@ -344,7 +351,7 @@ pub enum ListCommand {
116 subscription_options: SubscriptionOptions,
117 },
118 /// Add a new post policy.
119- AddPolicy {
120+ AddPostPolicy {
121 #[arg(long)]
122 /// Only list owners can post.
123 announce_only: bool,
124 @@ -363,13 +370,13 @@ pub enum ListCommand {
125 custom: bool,
126 },
127 // Remove post policy.
128- RemovePolicy {
129+ RemovePostPolicy {
130 #[arg(long)]
131 /// Post policy primary key.
132 pk: i64,
133 },
134 /// Add subscription policy to list.
135- AddSubscribePolicy {
136+ AddSubscriptionPolicy {
137 #[arg(long)]
138 /// Send confirmation e-mail when subscription is finalized.
139 send_confirmation: bool,
140 @@ -386,9 +393,9 @@ pub enum ListCommand {
141 /// Allow subscriptions, but handle it manually.
142 custom: bool,
143 },
144- RemoveSubscribePolicy {
145+ RemoveSubscriptionPolicy {
146 #[arg(long)]
147- /// Subscribe policy primary key.
148+ /// Subscription policy primary key.
149 pk: i64,
150 },
151 /// Add list owner to list.
152 @@ -494,3 +501,57 @@ pub enum ListCommand {
153 skip_owners: bool,
154 },
155 }
156+
157+ #[derive(Clone, Copy, Debug)]
158+ pub struct QueueValueParser;
159+
160+ impl QueueValueParser {
161+ /// Implementation for [`ValueParser::path_buf`]
162+ pub fn new() -> Self {
163+ Self
164+ }
165+ }
166+
167+ impl TypedValueParser for QueueValueParser {
168+ type Value = mailpot::queue::Queue;
169+
170+ fn parse_ref(
171+ &self,
172+ cmd: &clap::Command,
173+ arg: Option<&clap::Arg>,
174+ value: &std::ffi::OsStr,
175+ ) -> std::result::Result<Self::Value, clap::Error> {
176+ TypedValueParser::parse(self, cmd, arg, value.to_owned())
177+ }
178+
179+ fn parse(
180+ &self,
181+ cmd: &clap::Command,
182+ _arg: Option<&clap::Arg>,
183+ value: std::ffi::OsString,
184+ ) -> std::result::Result<Self::Value, clap::Error> {
185+ use std::str::FromStr;
186+
187+ use clap::error::ErrorKind;
188+
189+ if value.is_empty() {
190+ return Err(cmd.clone().error(
191+ ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand,
192+ "queue value required",
193+ ));
194+ }
195+ Self::Value::from_str(value.to_str().ok_or_else(|| {
196+ cmd.clone().error(
197+ ErrorKind::InvalidValue,
198+ "Queue value is not an UTF-8 string",
199+ )
200+ })?)
201+ .map_err(|err| cmd.clone().error(ErrorKind::InvalidValue, err))
202+ }
203+ }
204+
205+ impl Default for QueueValueParser {
206+ fn default() -> Self {
207+ Self::new()
208+ }
209+ }
210 diff --git a/cli/src/lints.rs b/cli/src/lints.rs
211index 68b118f..f4771ba 100644
212--- a/cli/src/lints.rs
213+++ b/cli/src/lints.rs
214 @@ -228,6 +228,7 @@ pub fn warn_list_no_owner_lint(db: &mut Connection, _: bool) -> Result<()> {
215 id: row.get("id")?,
216 address: row.get("address")?,
217 description: row.get("description")?,
218+ topics: vec![],
219 archive_url: row.get("archive_url")?,
220 },
221 pk,
222 diff --git a/cli/src/main.rs b/cli/src/main.rs
223index 26d0c3a..e3e71fe 100644
224--- a/cli/src/main.rs
225+++ b/cli/src/main.rs
226 @@ -29,7 +29,7 @@ use lints::*;
227 use mailpot::{
228 melib::{backends::maildir::MaildirPathTrait, smol, smtp::*, Envelope, EnvelopeHash},
229 models::{changesets::*, *},
230- queue::{Queue, QueueEntry},
231+ queue::QueueEntry,
232 transaction::TransactionBehavior,
233 Configuration, Connection, Error, ErrorKind, Result, *,
234 };
235 @@ -127,9 +127,14 @@ fn run_app(opt: Opt) -> Result<()> {
236 }
237 }
238 if let Some(s) = db.list_post_policy(l.pk)? {
239- println!("\tList policy: {}", s);
240+ println!("\tPost policy: {}", s);
241 } else {
242- println!("\tList policy: None");
243+ println!("\tPost policy: None");
244+ }
245+ if let Some(s) = db.list_subscription_policy(l.pk)? {
246+ println!("\tSubscription policy: {}", s);
247+ } else {
248+ println!("\tSubscription policy: None");
249 }
250 println!();
251 }
252 @@ -299,7 +304,7 @@ fn run_app(opt: Opt) -> Result<()> {
253 };
254 db.update_subscription(changeset)?;
255 }
256- AddPolicy {
257+ AddPostPolicy {
258 announce_only,
259 subscription_only,
260 approval_needed,
261 @@ -318,11 +323,11 @@ fn run_app(opt: Opt) -> Result<()> {
262 let new_val = db.set_list_post_policy(policy)?;
263 println!("Added new policy with pk = {}", new_val.pk());
264 }
265- RemovePolicy { pk } => {
266+ RemovePostPolicy { pk } => {
267 db.remove_list_post_policy(list.pk, pk)?;
268 println!("Removed policy with pk = {}", pk);
269 }
270- AddSubscribePolicy {
271+ AddSubscriptionPolicy {
272 send_confirmation,
273 open,
274 manual,
275 @@ -341,7 +346,7 @@ fn run_app(opt: Opt) -> Result<()> {
276 let new_val = db.set_list_subscription_policy(policy)?;
277 println!("Added new subscribe policy with pk = {}", new_val.pk());
278 }
279- RemoveSubscribePolicy { pk } => {
280+ RemoveSubscriptionPolicy { pk } => {
281 db.remove_list_subscription_policy(list.pk, pk)?;
282 println!("Removed subscribe policy with pk = {}", pk);
283 }
284 @@ -491,6 +496,7 @@ fn run_app(opt: Opt) -> Result<()> {
285 name,
286 id,
287 description,
288+ topics: vec![],
289 address,
290 archive_url,
291 })?;
292 @@ -535,17 +541,17 @@ fn run_app(opt: Opt) -> Result<()> {
293 let tx = db.transaction(TransactionBehavior::Exclusive).unwrap();
294 let messages = if opt.debug {
295 println!("flush-queue dry_run {:?}", dry_run);
296- tx.queue(Queue::Out)?
297+ tx.queue(mailpot::queue::Queue::Out)?
298 .into_iter()
299 .map(DbVal::into_inner)
300 .chain(
301- tx.queue(Queue::Deferred)?
302+ tx.queue(mailpot::queue::Queue::Deferred)?
303 .into_iter()
304 .map(DbVal::into_inner),
305 )
306 .collect()
307 } else {
308- tx.delete_from_queue(Queue::Out, vec![])?
309+ tx.delete_from_queue(mailpot::queue::Queue::Out, vec![])?
310 };
311 if opt.verbose > 0 || opt.debug {
312 println!("Queue out has {} messages.", messages.len());
313 @@ -614,15 +620,15 @@ fn run_app(opt: Opt) -> Result<()> {
314 for (err, mut msg) in failures {
315 log::error!("Message {msg:?} failed with: {err}. Inserting to Deferred queue.");
316
317- msg.queue = Queue::Deferred;
318+ msg.queue = mailpot::queue::Queue::Deferred;
319 tx.insert_to_queue(msg)?;
320 }
321
322 tx.commit()?;
323 }
324 ErrorQueue { cmd } => match cmd {
325- ErrorQueueCommand::List => {
326- let errors = db.queue(Queue::Error)?;
327+ QueueCommand::List => {
328+ let errors = db.queue(mailpot::queue::Queue::Error)?;
329 if errors.is_empty() {
330 println!("Error queue is empty.");
331 } else {
332 @@ -634,8 +640,8 @@ fn run_app(opt: Opt) -> Result<()> {
333 }
334 }
335 }
336- ErrorQueueCommand::Print { index } => {
337- let mut errors = db.queue(Queue::Error)?;
338+ QueueCommand::Print { index } => {
339+ let mut errors = db.queue(mailpot::queue::Queue::Error)?;
340 if !index.is_empty() {
341 errors.retain(|el| index.contains(&el.pk()));
342 }
343 @@ -647,8 +653,8 @@ fn run_app(opt: Opt) -> Result<()> {
344 }
345 }
346 }
347- ErrorQueueCommand::Delete { index, quiet } => {
348- let mut errors = db.queue(Queue::Error)?;
349+ QueueCommand::Delete { index, quiet } => {
350+ let mut errors = db.queue(mailpot::queue::Queue::Error)?;
351 if !index.is_empty() {
352 errors.retain(|el| index.contains(&el.pk()));
353 }
354 @@ -660,7 +666,7 @@ fn run_app(opt: Opt) -> Result<()> {
355 if !quiet {
356 println!("Deleting error queue elements {:?}", &index);
357 }
358- db.delete_from_queue(Queue::Error, index)?;
359+ db.delete_from_queue(mailpot::queue::Queue::Error, index)?;
360 if !quiet {
361 for e in errors {
362 println!("{e:?}");
363 @@ -669,6 +675,55 @@ fn run_app(opt: Opt) -> Result<()> {
364 }
365 }
366 },
367+ Queue { queue, cmd } => match cmd {
368+ QueueCommand::List => {
369+ let entries = db.queue(queue)?;
370+ if entries.is_empty() {
371+ println!("Queue {queue} is empty.");
372+ } else {
373+ for e in entries {
374+ println!(
375+ "- {} {} {} {} {}",
376+ e.pk, e.datetime, e.from_address, e.to_addresses, e.subject
377+ );
378+ }
379+ }
380+ }
381+ QueueCommand::Print { index } => {
382+ let mut entries = db.queue(queue)?;
383+ if !index.is_empty() {
384+ entries.retain(|el| index.contains(&el.pk()));
385+ }
386+ if entries.is_empty() {
387+ println!("Queue {queue} is empty.");
388+ } else {
389+ for e in entries {
390+ println!("{e:?}");
391+ }
392+ }
393+ }
394+ QueueCommand::Delete { index, quiet } => {
395+ let mut entries = db.queue(queue)?;
396+ if !index.is_empty() {
397+ entries.retain(|el| index.contains(&el.pk()));
398+ }
399+ if entries.is_empty() {
400+ if !quiet {
401+ println!("Queue {queue} is empty.");
402+ }
403+ } else {
404+ if !quiet {
405+ println!("Deleting queue {queue} elements {:?}", &index);
406+ }
407+ db.delete_from_queue(queue, index)?;
408+ if !quiet {
409+ for e in entries {
410+ println!("{e:?}");
411+ }
412+ }
413+ }
414+ }
415+ },
416 ImportMaildir {
417 list_id,
418 mut maildir_path,
419 diff --git a/cli/tests/basic_interfaces.rs b/cli/tests/basic_interfaces.rs
420index 16a83c0..903d502 100644
421--- a/cli/tests/basic_interfaces.rs
422+++ b/cli/tests/basic_interfaces.rs
423 @@ -132,6 +132,7 @@ For more information, try '--help'."#,
424 name: "foobar chat".into(),
425 id: "foo-chat".into(),
426 address: "foo-chat@example.com".into(),
427+ topics: vec![],
428 description: None,
429 archive_url: None,
430 })
431 @@ -142,8 +143,8 @@ For more information, try '--help'."#,
432 list_lists(
433 &conf_path,
434 "- foo-chat DbVal(MailingList { pk: 1, name: \"foobar chat\", id: \"foo-chat\", address: \
435- \"foo-chat@example.com\", description: None, archive_url: None }, 1)\n\tList owners: \
436- None\n\tList policy: None",
437+ \"foo-chat@example.com\", topics: [], description: None, archive_url: None }, 1)\n\tList \
438+ owners: None\n\tPost policy: None\n\tSubscription policy: None",
439 );
440
441 fn create_list(conf: &Path) {
442 @@ -171,9 +172,10 @@ For more information, try '--help'."#,
443 list_lists(
444 &conf_path,
445 "- foo-chat DbVal(MailingList { pk: 1, name: \"foobar chat\", id: \"foo-chat\", address: \
446- \"foo-chat@example.com\", description: None, archive_url: None }, 1)\n\tList owners: \
447- None\n\tList policy: None\n\n- twobar-chat DbVal(MailingList { pk: 2, name: \"twobar\", \
448- id: \"twobar-chat\", address: \"twobar-chat@example.com\", description: None, \
449- archive_url: None }, 2)\n\tList owners: None\n\tList policy: None",
450+ \"foo-chat@example.com\", topics: [], description: None, archive_url: None }, 1)\n\tList \
451+ owners: None\n\tPost policy: None\n\tSubscription policy: None\n\n- twobar-chat \
452+ DbVal(MailingList { pk: 2, name: \"twobar\", id: \"twobar-chat\", address: \
453+ \"twobar-chat@example.com\", topics: [], description: None, archive_url: None }, \
454+ 2)\n\tList owners: None\n\tPost policy: None\n\tSubscription policy: None",
455 );
456 }
457 diff --git a/cli/tests/out_queue_flush.rs b/cli/tests/out_queue_flush.rs
458index e962fd2..87a381b 100644
459--- a/cli/tests/out_queue_flush.rs
460+++ b/cli/tests/out_queue_flush.rs
461 @@ -85,6 +85,7 @@ fn test_out_queue_flush() {
462 id: "foo-chat".into(),
463 address: "foo-chat@example.com".into(),
464 description: None,
465+ topics: vec![],
466 archive_url: None,
467 })
468 .unwrap();
469 @@ -282,6 +283,7 @@ fn test_list_requests_submission() {
470 id: "foo-chat".into(),
471 address: "foo-chat@example.com".into(),
472 description: None,
473+ topics: vec![],
474 archive_url: None,
475 })
476 .unwrap();
477 diff --git a/core/migrations/001.sql b/core/migrations/001.sql
478index a62617c..345a376 100644
479--- a/core/migrations/001.sql
480+++ b/core/migrations/001.sql
481 @@ -1,4 +1,2 @@
482 PRAGMA foreign_keys=ON;
483- BEGIN;
484 ALTER TABLE templates RENAME TO template;
485- COMMIT;
486 diff --git a/core/migrations/001.undo.sql b/core/migrations/001.undo.sql
487index 86fe8ac..e0e03fb 100644
488--- a/core/migrations/001.undo.sql
489+++ b/core/migrations/001.undo.sql
490 @@ -1,4 +1,2 @@
491 PRAGMA foreign_keys=ON;
492- BEGIN;
493 ALTER TABLE template RENAME TO templates;
494- COMMIT;
495 diff --git a/core/migrations/002.sql b/core/migrations/002.sql
496new file mode 100644
497index 0000000..7dbb83a
498--- /dev/null
499+++ b/core/migrations/002.sql
500 @@ -0,0 +1,2 @@
501+ PRAGMA foreign_keys=ON;
502+ ALTER TABLE list ADD COLUMN topics JSON NOT NULL CHECK (json_type(topics) == 'array') DEFAULT '[]';
503 diff --git a/core/migrations/002.undo.sql b/core/migrations/002.undo.sql
504new file mode 100644
505index 0000000..9a18755
506--- /dev/null
507+++ b/core/migrations/002.undo.sql
508 @@ -0,0 +1,2 @@
509+ PRAGMA foreign_keys=ON;
510+ ALTER TABLE list DROP COLUMN topics;
511 diff --git a/core/src/connection.rs b/core/src/connection.rs
512index aa1866e..05ad84c 100644
513--- a/core/src/connection.rs
514+++ b/core/src/connection.rs
515 @@ -195,7 +195,7 @@ impl Connection {
516 conn.set_db_config(DbConfig::SQLITE_DBCONFIG_ENABLE_FKEY, true)?;
517 conn.set_db_config(DbConfig::SQLITE_DBCONFIG_ENABLE_TRIGGER, true)?;
518 conn.set_db_config(DbConfig::SQLITE_DBCONFIG_DEFENSIVE, true)?;
519- conn.set_db_config(DbConfig::SQLITE_DBCONFIG_TRUSTED_SCHEMA, false)?;
520+ conn.set_db_config(DbConfig::SQLITE_DBCONFIG_TRUSTED_SCHEMA, true)?;
521 conn.busy_timeout(core::time::Duration::from_millis(500))?;
522 conn.busy_handler(Some(|times: i32| -> bool { times < 5 }))?;
523
524 @@ -246,14 +246,14 @@ impl Connection {
525 if undo { "un " } else { "re" }
526 );
527 if undo {
528- trace!("{}", Self::MIGRATIONS[from as usize].2);
529+ trace!("{}", Self::MIGRATIONS[from as usize - 1].2);
530 tx.connection
531- .execute(Self::MIGRATIONS[from as usize].2, [])?;
532+ .execute_batch(Self::MIGRATIONS[from as usize - 1].2)?;
533 from -= 1;
534 } else {
535 trace!("{}", Self::MIGRATIONS[from as usize].1);
536 tx.connection
537- .execute(Self::MIGRATIONS[from as usize].1, [])?;
538+ .execute_batch(Self::MIGRATIONS[from as usize].1)?;
539 from += 1;
540 }
541 }
542 @@ -384,6 +384,8 @@ impl Connection {
543 let mut stmt = self.connection.prepare("SELECT * FROM list;")?;
544 let list_iter = stmt.query_map([], |row| {
545 let pk = row.get("pk")?;
546+ let topics: serde_json::Value = row.get("topics")?;
547+ let topics = MailingList::topics_from_json_value(topics)?;
548 Ok(DbVal(
549 MailingList {
550 pk,
551 @@ -391,6 +393,7 @@ impl Connection {
552 id: row.get("id")?,
553 address: row.get("address")?,
554 description: row.get("description")?,
555+ topics,
556 archive_url: row.get("archive_url")?,
557 },
558 pk,
559 @@ -413,6 +416,8 @@ impl Connection {
560 let ret = stmt
561 .query_row([&pk], |row| {
562 let pk = row.get("pk")?;
563+ let topics: serde_json::Value = row.get("topics")?;
564+ let topics = MailingList::topics_from_json_value(topics)?;
565 Ok(DbVal(
566 MailingList {
567 pk,
568 @@ -420,6 +425,7 @@ impl Connection {
569 id: row.get("id")?,
570 address: row.get("address")?,
571 description: row.get("description")?,
572+ topics,
573 archive_url: row.get("archive_url")?,
574 },
575 pk,
576 @@ -438,6 +444,8 @@ impl Connection {
577 let ret = stmt
578 .query_row([&id], |row| {
579 let pk = row.get("pk")?;
580+ let topics: serde_json::Value = row.get("topics")?;
581+ let topics = MailingList::topics_from_json_value(topics)?;
582 Ok(DbVal(
583 MailingList {
584 pk,
585 @@ -445,6 +453,7 @@ impl Connection {
586 id: row.get("id")?,
587 address: row.get("address")?,
588 description: row.get("description")?,
589+ topics,
590 archive_url: row.get("archive_url")?,
591 },
592 pk,
593 @@ -458,8 +467,8 @@ impl Connection {
594 /// Create a new list.
595 pub fn create_list(&self, new_val: MailingList) -> Result<DbVal<MailingList>> {
596 let mut stmt = self.connection.prepare(
597- "INSERT INTO list(name, id, address, description, archive_url) VALUES(?, ?, ?, ?, ?) \
598- RETURNING *;",
599+ "INSERT INTO list(name, id, address, description, archive_url, topics) VALUES(?, ?, \
600+ ?, ?, ?, ?) RETURNING *;",
601 )?;
602 let ret = stmt.query_row(
603 rusqlite::params![
604 @@ -468,9 +477,12 @@ impl Connection {
605 &new_val.address,
606 new_val.description.as_ref(),
607 new_val.archive_url.as_ref(),
608+ serde_json::json!(new_val.topics.as_slice()),
609 ],
610 |row| {
611 let pk = row.get("pk")?;
612+ let topics: serde_json::Value = row.get("topics")?;
613+ let topics = MailingList::topics_from_json_value(topics)?;
614 Ok(DbVal(
615 MailingList {
616 pk,
617 @@ -478,6 +490,7 @@ impl Connection {
618 id: row.get("id")?,
619 address: row.get("address")?,
620 description: row.get("description")?,
621+ topics,
622 archive_url: row.get("archive_url")?,
623 },
624 pk,
625 @@ -999,6 +1012,7 @@ mod tests {
626 name: "".into(),
627 id: "".into(),
628 description: None,
629+ topics: vec![],
630 address: "".into(),
631 archive_url: None,
632 };
633 diff --git a/core/src/doctests/db_setup.rs.inc b/core/src/doctests/db_setup.rs.inc
634index bb38811..46b82ca 100644
635--- a/core/src/doctests/db_setup.rs.inc
636+++ b/core/src/doctests/db_setup.rs.inc
637 @@ -29,6 +29,7 @@
638 # id: "foo-chat".into(),
639 # address: "foo-chat@example.com".into(),
640 # description: Some("Hello world, from foo-chat list".into()),
641+ # topics: vec![],
642 # archive_url: Some("https://lists.example.com".into()),
643 # })
644 # .unwrap();
645 diff --git a/core/src/errors.rs b/core/src/errors.rs
646index 113bb70..8aebb2a 100644
647--- a/core/src/errors.rs
648+++ b/core/src/errors.rs
649 @@ -66,3 +66,11 @@ error_chain! {
650 Template(minijinja::Error) #[doc="Error returned from minijinja template engine."];
651 }
652 }
653+
654+ impl Error {
655+ /// Helper function to create a new generic error message.
656+ pub fn new_external<S: Into<String>>(msg: S) -> Self {
657+ let msg = msg.into();
658+ Self::from(ErrorKind::External(anyhow::Error::msg(msg)))
659+ }
660+ }
661 diff --git a/core/src/lib.rs b/core/src/lib.rs
662index 9c3fdbb..f6520c3 100644
663--- a/core/src/lib.rs
664+++ b/core/src/lib.rs
665 @@ -83,6 +83,7 @@
666 //! name: "foobar chat".into(),
667 //! id: "foo-chat".into(),
668 //! address: "foo-chat@example.com".into(),
669+ //! topics: vec![],
670 //! description: None,
671 //! archive_url: None,
672 //! })?
673 diff --git a/core/src/migrations.rs.inc b/core/src/migrations.rs.inc
674index b6ad33e..ffdaa44 100644
675--- a/core/src/migrations.rs.inc
676+++ b/core/src/migrations.rs.inc
677 @@ -1,11 +1,11 @@
678
679 //(user_version, redo sql, undo sql
680 &[(1,"PRAGMA foreign_keys=ON;
681- BEGIN;
682 ALTER TABLE templates RENAME TO template;
683- COMMIT;
684 ","PRAGMA foreign_keys=ON;
685- BEGIN;
686 ALTER TABLE template RENAME TO templates;
687- COMMIT;
688+ "),(2,"PRAGMA foreign_keys=ON;
689+ ALTER TABLE list ADD COLUMN topics JSON NOT NULL CHECK (json_type(topics) == 'array') DEFAULT '[]';
690+ ","PRAGMA foreign_keys=ON;
691+ ALTER TABLE list DROP COLUMN topics;
692 "),]
693\ No newline at end of file
694 diff --git a/core/src/models.rs b/core/src/models.rs
695index fbac235..e4fe69a 100644
696--- a/core/src/models.rs
697+++ b/core/src/models.rs
698 @@ -92,6 +92,8 @@ pub struct MailingList {
699 pub id: String,
700 /// Mailing list e-mail address.
701 pub address: String,
702+ /// Discussion topics.
703+ pub topics: Vec<String>,
704 /// Mailing list description.
705 pub description: Option<String>,
706 /// Mailing list archive URL.
707 @@ -417,6 +419,52 @@ impl MailingList {
708 contact = self.owner_mailto().address,
709 )
710 }
711+
712+ /// Utility function to get a `Vec<String>` -which is the expected type of
713+ /// the `topics` field- from a `serde_json::Value`, which is the value
714+ /// stored in the `topics` column in `sqlite3`.
715+ ///
716+ /// # Example
717+ ///
718+ /// ```rust
719+ /// # use mailpot::models::MailingList;
720+ /// use serde_json::Value;
721+ ///
722+ /// # fn main() -> Result<(), serde_json::Error> {
723+ /// let value: Value = serde_json::from_str(r#"["fruits","vegetables"]"#)?;
724+ /// assert_eq!(
725+ /// MailingList::topics_from_json_value(value),
726+ /// Ok(vec!["fruits".to_string(), "vegetables".to_string()])
727+ /// );
728+ ///
729+ /// let value: Value = serde_json::from_str(r#"{"invalid":"value"}"#)?;
730+ /// assert!(MailingList::topics_from_json_value(value).is_err());
731+ /// # Ok(())
732+ /// # }
733+ /// ```
734+ pub fn topics_from_json_value(
735+ v: serde_json::Value,
736+ ) -> std::result::Result<Vec<String>, rusqlite::Error> {
737+ let err_fn = || {
738+ rusqlite::Error::FromSqlConversionFailure(
739+ 8,
740+ rusqlite::types::Type::Text,
741+ anyhow::Error::msg(
742+ "topics column must be a json array of strings serialized as a string, e.g. \
743+ \"[]\" or \"['topicA', 'topicB']\"",
744+ )
745+ .into(),
746+ )
747+ };
748+ v.as_array()
749+ .map(|arr| {
750+ arr.iter()
751+ .map(|v| v.as_str().map(str::to_string))
752+ .collect::<Option<Vec<String>>>()
753+ })
754+ .ok_or_else(err_fn)?
755+ .ok_or_else(err_fn)
756+ }
757 }
758
759 /// A mailing list subscription entry.
760 diff --git a/core/src/policies.rs b/core/src/policies.rs
761index 902404a..12e2df3 100644
762--- a/core/src/policies.rs
763+++ b/core/src/policies.rs
764 @@ -84,6 +84,7 @@ mod post_policy {
765 /// id: "foo-chat".into(),
766 /// address: "foo-chat@example.com".into(),
767 /// description: None,
768+ /// topics: vec![],
769 /// archive_url: None,
770 /// })
771 /// .unwrap();
772 @@ -278,6 +279,7 @@ mod subscription_policy {
773 /// id: "foo-chat".into(),
774 /// address: "foo-chat@example.com".into(),
775 /// description: None,
776+ /// topics: vec![],
777 /// archive_url: None,
778 /// })
779 /// .unwrap();
780 diff --git a/core/src/postfix.rs b/core/src/postfix.rs
781index f3446c1..4673d74 100644
782--- a/core/src/postfix.rs
783+++ b/core/src/postfix.rs
784 @@ -430,6 +430,7 @@ fn test_postfix_generation() -> Result<()> {
785 id: "first".into(),
786 address: "first@example.com".into(),
787 description: None,
788+ topics: vec![],
789 archive_url: None,
790 })?;
791 assert_eq!(first.pk(), 1);
792 @@ -439,6 +440,7 @@ fn test_postfix_generation() -> Result<()> {
793 id: "second".into(),
794 address: "second@example.com".into(),
795 description: None,
796+ topics: vec![],
797 archive_url: None,
798 })?;
799 assert_eq!(second.pk(), 2);
800 @@ -459,6 +461,7 @@ fn test_postfix_generation() -> Result<()> {
801 id: "third".into(),
802 address: "third@example.com".into(),
803 description: None,
804+ topics: vec![],
805 archive_url: None,
806 })?;
807 assert_eq!(third.pk(), 3);
808 diff --git a/core/src/queue.rs b/core/src/queue.rs
809index 8cb311e..45761ad 100644
810--- a/core/src/queue.rs
811+++ b/core/src/queue.rs
812 @@ -51,6 +51,22 @@ pub enum Queue {
813 Error,
814 }
815
816+ impl std::str::FromStr for Queue {
817+ type Err = Error;
818+
819+ fn from_str(s: &str) -> Result<Self> {
820+ Ok(match s.trim() {
821+ s if s.eq_ignore_ascii_case(stringify!(Maildrop)) => Self::Maildrop,
822+ s if s.eq_ignore_ascii_case(stringify!(Hold)) => Self::Hold,
823+ s if s.eq_ignore_ascii_case(stringify!(Deferred)) => Self::Deferred,
824+ s if s.eq_ignore_ascii_case(stringify!(Corrupt)) => Self::Corrupt,
825+ s if s.eq_ignore_ascii_case(stringify!(Out)) => Self::Out,
826+ s if s.eq_ignore_ascii_case(stringify!(Error)) => Self::Error,
827+ other => return Err(Error::new_external(format!("Invalid Queue name: {other}."))),
828+ })
829+ }
830+ }
831+
832 impl Queue {
833 /// Returns the name of the queue used in the database schema.
834 pub fn as_str(&self) -> &'static str {
835 @@ -65,6 +81,12 @@ impl Queue {
836 }
837 }
838
839+ impl std::fmt::Display for Queue {
840+ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
841+ write!(fmt, "{}", self.as_str())
842+ }
843+ }
844+
845 /// A queue entry.
846 #[derive(Clone, Deserialize, Serialize, PartialEq, Eq)]
847 pub struct QueueEntry {
848 diff --git a/core/src/schema.sql b/core/src/schema.sql
849index 30654a6..aba82bb 100644
850--- a/core/src/schema.sql
851+++ b/core/src/schema.sql
852 @@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS list (
853 request_local_part TEXT,
854 archive_url TEXT,
855 description TEXT,
856+ topics JSON NOT NULL CHECK (json_type(topics) = 'array') DEFAULT '[]',
857 created INTEGER NOT NULL DEFAULT (unixepoch()),
858 last_modified INTEGER NOT NULL DEFAULT (unixepoch()),
859 verify BOOLEAN CHECK (verify IN (0, 1)) NOT NULL DEFAULT 1,
860 diff --git a/core/src/schema.sql.m4 b/core/src/schema.sql.m4
861index 0f1cee6..93324d6 100644
862--- a/core/src/schema.sql.m4
863+++ b/core/src/schema.sql.m4
864 @@ -44,6 +44,7 @@ CREATE TABLE IF NOT EXISTS list (
865 request_local_part TEXT,
866 archive_url TEXT,
867 description TEXT,
868+ topics JSON NOT NULL CHECK (json_type(topics) = 'array') DEFAULT '[]',
869 created INTEGER NOT NULL DEFAULT (unixepoch()),
870 last_modified INTEGER NOT NULL DEFAULT (unixepoch()),
871 verify BOOLEAN_TYPE(verify) DEFAULT BOOLEAN_TRUE(),
872 diff --git a/core/src/subscriptions.rs b/core/src/subscriptions.rs
873index fce5678..f25ffb9 100644
874--- a/core/src/subscriptions.rs
875+++ b/core/src/subscriptions.rs
876 @@ -623,6 +623,7 @@ mod tests {
877 name: "foobar chat".into(),
878 id: "foo-chat".into(),
879 address: "foo-chat@example.com".into(),
880+ topics: vec![],
881 description: None,
882 archive_url: None,
883 })
884 @@ -633,6 +634,7 @@ mod tests {
885 name: "foobar chat2".into(),
886 id: "foo-chat2".into(),
887 address: "foo-chat2@example.com".into(),
888+ topics: vec![],
889 description: None,
890 archive_url: None,
891 })
892 diff --git a/core/tests/account.rs b/core/tests/account.rs
893index 0a97f20..f02a05f 100644
894--- a/core/tests/account.rs
895+++ b/core/tests/account.rs
896 @@ -46,6 +46,7 @@ fn test_accounts() {
897 id: "foo-chat".into(),
898 address: "foo-chat@example.com".into(),
899 description: None,
900+ topics: vec![],
901 archive_url: None,
902 })
903 .unwrap();
904 diff --git a/core/tests/authorizer.rs b/core/tests/authorizer.rs
905index 12e2349..b5fa1ca 100644
906--- a/core/tests/authorizer.rs
907+++ b/core/tests/authorizer.rs
908 @@ -46,6 +46,7 @@ fn test_authorizer() {
909 id: "foo-chat".into(),
910 address: "foo-chat@example.com".into(),
911 description: None,
912+ topics: vec![],
913 archive_url: None,
914 })
915 .unwrap_err(),
916 @@ -84,6 +85,7 @@ fn test_authorizer() {
917 id: "foo-chat".into(),
918 address: "foo-chat@example.com".into(),
919 description: None,
920+ topics: vec![],
921 archive_url: None,
922 })
923 .map(|_| ()),
924 diff --git a/core/tests/creation.rs b/core/tests/creation.rs
925index c244181..31aa0cc 100644
926--- a/core/tests/creation.rs
927+++ b/core/tests/creation.rs
928 @@ -61,6 +61,7 @@ fn test_list_creation() {
929 id: "foo-chat".into(),
930 address: "foo-chat@example.com".into(),
931 description: None,
932+ topics: vec![],
933 archive_url: None,
934 })
935 .unwrap();
936 diff --git a/core/tests/error_queue.rs b/core/tests/error_queue.rs
937index fa33b83..ed8a117 100644
938--- a/core/tests/error_queue.rs
939+++ b/core/tests/error_queue.rs
940 @@ -55,6 +55,7 @@ fn test_error_queue() {
941 id: "foo-chat".into(),
942 address: "foo-chat@example.com".into(),
943 description: None,
944+ topics: vec![],
945 archive_url: None,
946 })
947 .unwrap();
948 diff --git a/core/tests/smtp.rs b/core/tests/smtp.rs
949index 63160a9..6fc84d9 100644
950--- a/core/tests/smtp.rs
951+++ b/core/tests/smtp.rs
952 @@ -48,6 +48,7 @@ fn test_smtp() {
953 id: "foo-chat".into(),
954 address: "foo-chat@example.com".into(),
955 description: None,
956+ topics: vec![],
957 archive_url: None,
958 })
959 .unwrap();
960 @@ -202,6 +203,7 @@ fn test_smtp_mailcrab() {
961 id: "foo-chat".into(),
962 address: "foo-chat@example.com".into(),
963 description: None,
964+ topics: vec![],
965 archive_url: None,
966 })
967 .unwrap();
968 diff --git a/core/tests/subscription.rs b/core/tests/subscription.rs
969index c83f201..c92081a 100644
970--- a/core/tests/subscription.rs
971+++ b/core/tests/subscription.rs
972 @@ -44,6 +44,7 @@ fn test_list_subscription() {
973 id: "foo-chat".into(),
974 address: "foo-chat@example.com".into(),
975 description: None,
976+ topics: vec![],
977 archive_url: None,
978 })
979 .unwrap();
980 @@ -178,6 +179,7 @@ fn test_post_rejection() {
981 id: "foo-chat".into(),
982 address: "foo-chat@example.com".into(),
983 description: None,
984+ topics: vec![],
985 archive_url: None,
986 })
987 .unwrap();
988 diff --git a/core/tests/template_replies.rs b/core/tests/template_replies.rs
989index 438c6c2..8648b2e 100644
990--- a/core/tests/template_replies.rs
991+++ b/core/tests/template_replies.rs
992 @@ -67,6 +67,7 @@ MIME-Version: 1.0
993 id: "foo-chat".into(),
994 address: "foo-chat@example.com".into(),
995 description: None,
996+ topics: vec![],
997 archive_url: None,
998 })
999 .unwrap();
1000 diff --git a/docs/mpot.1 b/docs/mpot.1
1001index 02a0504..f950b67 100644
1002--- a/docs/mpot.1
1003+++ b/docs/mpot.1
1004 @@ -330,13 +330,13 @@ Is subscription enabled.
1005 .ie \n(.g .ds Aq \(aq
1006 .el .ds Aq '
1007 .\fB
1008- .SS mpot list add-policy
1009+ .SS mpot list add-post-policy
1010 .\fR
1011 .br
1012
1013 .br
1014
1015- mpot list add\-policy [\-\-announce\-only \fIANNOUNCE_ONLY\fR] [\-\-subscription\-only \fISUBSCRIPTION_ONLY\fR] [\-\-approval\-needed \fIAPPROVAL_NEEDED\fR] [\-\-open \fIOPEN\fR] [\-\-custom \fICUSTOM\fR]
1016+ mpot list add\-post\-policy [\-\-announce\-only \fIANNOUNCE_ONLY\fR] [\-\-subscription\-only \fISUBSCRIPTION_ONLY\fR] [\-\-approval\-needed \fIAPPROVAL_NEEDED\fR] [\-\-open \fIOPEN\fR] [\-\-custom \fICUSTOM\fR]
1017 .br
1018
1019 Add a new post policy.
1020 @@ -358,13 +358,13 @@ Allow posts, but handle it manually.
1021 .ie \n(.g .ds Aq \(aq
1022 .el .ds Aq '
1023 .\fB
1024- .SS mpot list remove-policy
1025+ .SS mpot list remove-post-policy
1026 .\fR
1027 .br
1028
1029 .br
1030
1031- mpot list remove\-policy \-\-pk \fIPK\fR
1032+ mpot list remove\-post\-policy \-\-pk \fIPK\fR
1033 .br
1034
1035 .TP
1036 @@ -373,13 +373,13 @@ Post policy primary key.
1037 .ie \n(.g .ds Aq \(aq
1038 .el .ds Aq '
1039 .\fB
1040- .SS mpot list add-subscribe-policy
1041+ .SS mpot list add-subscription-policy
1042 .\fR
1043 .br
1044
1045 .br
1046
1047- mpot list add\-subscribe\-policy [\-\-send\-confirmation \fISEND_CONFIRMATION\fR] [\-\-open \fIOPEN\fR] [\-\-manual \fIMANUAL\fR] [\-\-request \fIREQUEST\fR] [\-\-custom \fICUSTOM\fR]
1048+ mpot list add\-subscription\-policy [\-\-send\-confirmation \fISEND_CONFIRMATION\fR] [\-\-open \fIOPEN\fR] [\-\-manual \fIMANUAL\fR] [\-\-request \fIREQUEST\fR] [\-\-custom \fICUSTOM\fR]
1049 .br
1050
1051 Add subscription policy to list.
1052 @@ -401,18 +401,18 @@ Allow subscriptions, but handle it manually.
1053 .ie \n(.g .ds Aq \(aq
1054 .el .ds Aq '
1055 .\fB
1056- .SS mpot list remove-subscribe-policy
1057+ .SS mpot list remove-subscription-policy
1058 .\fR
1059 .br
1060
1061 .br
1062
1063- mpot list remove\-subscribe\-policy \-\-pk \fIPK\fR
1064+ mpot list remove\-subscription\-policy \-\-pk \fIPK\fR
1065 .br
1066
1067 .TP
1068 \-\-pk \fIPK\fR
1069- Subscribe policy primary key.
1070+ Subscription policy primary key.
1071 .ie \n(.g .ds Aq \(aq
1072 .el .ds Aq '
1073 .\fB
1074 @@ -715,6 +715,67 @@ Do not print in stdout.
1075 .ie \n(.g .ds Aq \(aq
1076 .el .ds Aq '
1077 .\fB
1078+ .SS mpot queue
1079+ .\fR
1080+ .br
1081+
1082+ .br
1083+
1084+ mpot queue \-\-queue \fIQUEUE\fR
1085+ .br
1086+
1087+ Mail that has not been handled properly end up in the error queue.
1088+ .TP
1089+ \-\-queue \fIQUEUE\fR
1090+
1091+ .ie \n(.g .ds Aq \(aq
1092+ .el .ds Aq '
1093+ .\fB
1094+ .SS mpot queue list
1095+ .\fR
1096+ .br
1097+
1098+ .br
1099+
1100+ List.
1101+ .ie \n(.g .ds Aq \(aq
1102+ .el .ds Aq '
1103+ .\fB
1104+ .SS mpot queue print
1105+ .\fR
1106+ .br
1107+
1108+ .br
1109+
1110+ mpot queue print [\-\-index \fIINDEX\fR]
1111+ .br
1112+
1113+ Print entry in RFC5322 or JSON format.
1114+ .TP
1115+ \-\-index \fIINDEX\fR
1116+ index of entry.
1117+ .ie \n(.g .ds Aq \(aq
1118+ .el .ds Aq '
1119+ .\fB
1120+ .SS mpot queue delete
1121+ .\fR
1122+ .br
1123+
1124+ .br
1125+
1126+ mpot queue delete [\-\-index \fIINDEX\fR] [\-\-quiet \fIQUIET\fR]
1127+ .br
1128+
1129+ Delete entry and print it in stdout.
1130+ .TP
1131+ \-\-index \fIINDEX\fR
1132+ index of entry.
1133+ .TP
1134+ \-\-quiet
1135+ Do not print in stdout.
1136+ .ie \n(.g .ds Aq \(aq
1137+ .el .ds Aq '
1138+ .\fB
1139 .SS mpot import-maildir
1140 .\fR
1141 .br
1142 diff --git a/rest-http/src/routes/list.rs b/rest-http/src/routes/list.rs
1143index ebe9910..e7f4211 100644
1144--- a/rest-http/src/routes/list.rs
1145+++ b/rest-http/src/routes/list.rs
1146 @@ -217,6 +217,10 @@ mod tests {
1147
1148 let db_path = tmp_dir.path().join("mpot.db");
1149 std::fs::copy("../mailpot-tests/for_testing.db", &db_path).unwrap();
1150+ let mut perms = std::fs::metadata(&db_path).unwrap().permissions();
1151+ #[allow(clippy::permissions_set_readonly_false)]
1152+ perms.set_readonly(false);
1153+ std::fs::set_permissions(&db_path, perms).unwrap();
1154 let config = Configuration {
1155 send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
1156 db_path,
1157 @@ -231,9 +235,11 @@ mod tests {
1158 name: "foobar chat".into(),
1159 id: "foo-chat".into(),
1160 address: "foo-chat@example.com".into(),
1161+ topics: vec![],
1162 description: None,
1163 archive_url: None,
1164 };
1165+ assert_eq!(&db.lists().unwrap().remove(0).into_inner(), &foo_chat);
1166 drop(db);
1167
1168 let config = Arc::new(config);
1169 diff --git a/web/src/minijinja_utils.rs b/web/src/minijinja_utils.rs
1170index 04da2d1..b7b0c04 100644
1171--- a/web/src/minijinja_utils.rs
1172+++ b/web/src/minijinja_utils.rs
1173 @@ -92,6 +92,7 @@ pub struct MailingList {
1174 pub id: String,
1175 pub address: String,
1176 pub description: Option<String>,
1177+ pub topics: Vec<String>,
1178 #[serde(serialize_with = "super::utils::to_safe_string_opt")]
1179 pub archive_url: Option<String>,
1180 pub inner: DbVal<mailpot::models::MailingList>,
1181 @@ -106,6 +107,7 @@ impl From<DbVal<mailpot::models::MailingList>> for MailingList {
1182 id,
1183 address,
1184 description,
1185+ topics,
1186 archive_url,
1187 },
1188 _,
1189 @@ -117,6 +119,7 @@ impl From<DbVal<mailpot::models::MailingList>> for MailingList {
1190 id,
1191 address,
1192 description,
1193+ topics,
1194 archive_url,
1195 inner: val,
1196 }
1197 @@ -163,13 +166,24 @@ impl minijinja::value::StructObject for MailingList {
1198 "id" => Some(Value::from_serializable(&self.id)),
1199 "address" => Some(Value::from_serializable(&self.address)),
1200 "description" => Some(Value::from_serializable(&self.description)),
1201+ "topics" => Some(Value::from_serializable(&self.topics)),
1202 "archive_url" => Some(Value::from_serializable(&self.archive_url)),
1203 _ => None,
1204 }
1205 }
1206
1207 fn static_fields(&self) -> Option<&'static [&'static str]> {
1208- Some(&["pk", "name", "id", "address", "description", "archive_url"][..])
1209+ Some(
1210+ &[
1211+ "pk",
1212+ "name",
1213+ "id",
1214+ "address",
1215+ "description",
1216+ "topics",
1217+ "archive_url",
1218+ ][..],
1219+ )
1220 }
1221 }
1222