From e8120c75dbf6e80594c7081c2e40303d9dd06efe Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Thu, 18 May 2023 10:34:00 +0300 Subject: [PATCH] core: Add topics field to MailingList --- README.md | 1 + archive-http/src/utils.rs | 16 +++++- cli/Cargo.toml | 1 + cli/src/args.rs | 77 +++++++++++++++++++++++--- cli/src/lints.rs | 1 + cli/src/main.rs | 91 +++++++++++++++++++++++++------ cli/tests/basic_interfaces.rs | 14 +++-- cli/tests/out_queue_flush.rs | 2 + core/migrations/001.sql | 2 - core/migrations/001.undo.sql | 2 - core/migrations/002.sql | 2 + core/migrations/002.undo.sql | 2 + core/src/connection.rs | 26 +++++++-- core/src/doctests/db_setup.rs.inc | 1 + core/src/errors.rs | 8 +++ core/src/lib.rs | 1 + core/src/migrations.rs.inc | 8 +-- core/src/models.rs | 48 ++++++++++++++++ core/src/policies.rs | 2 + core/src/postfix.rs | 3 + core/src/queue.rs | 22 ++++++++ core/src/schema.sql | 1 + core/src/schema.sql.m4 | 1 + core/src/subscriptions.rs | 2 + core/tests/account.rs | 1 + core/tests/authorizer.rs | 2 + core/tests/creation.rs | 1 + core/tests/error_queue.rs | 1 + core/tests/smtp.rs | 2 + core/tests/subscription.rs | 2 + core/tests/template_replies.rs | 1 + docs/mpot.1 | 79 ++++++++++++++++++++++++--- rest-http/src/routes/list.rs | 6 ++ web/src/minijinja_utils.rs | 16 +++++- 34 files changed, 388 insertions(+), 57 deletions(-) create mode 100644 core/migrations/002.sql create mode 100644 core/migrations/002.undo.sql diff --git a/README.md b/README.md index 4878d78..e0f7fca 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,7 @@ let list_pk = db.create_list(MailingList { id: "foo-chat".into(), address: "foo-chat@example.com".into(), description: None, + topics: vec![], archive_url: None, })?.pk; diff --git a/archive-http/src/utils.rs b/archive-http/src/utils.rs index 7e2597b..71905b5 100644 --- a/archive-http/src/utils.rs +++ b/archive-http/src/utils.rs @@ -57,6 +57,7 @@ pub struct MailingList { pub id: String, pub address: String, pub description: Option, + pub topics: Vec, pub archive_url: Option, pub inner: DbVal, } @@ -70,6 +71,7 @@ impl From> for MailingList { id, address, description, + topics, archive_url, }, _, @@ -81,6 +83,7 @@ impl From> for MailingList { id, address, description, + topics, archive_url, inner: val, } @@ -127,13 +130,24 @@ impl minijinja::value::StructObject for MailingList { "id" => Some(Value::from_serializable(&self.id)), "address" => Some(Value::from_serializable(&self.address)), "description" => Some(Value::from_serializable(&self.description)), + "topics" => Some(Value::from_serializable(&self.topics)), "archive_url" => Some(Value::from_serializable(&self.archive_url)), _ => None, } } fn static_fields(&self) -> Option<&'static [&'static str]> { - Some(&["pk", "name", "id", "address", "description", "archive_url"][..]) + Some( + &[ + "pk", + "name", + "id", + "address", + "description", + "topics", + "archive_url", + ][..], + ) } } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 54a3638..49224df 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -34,4 +34,5 @@ tempfile = "3.3" [build-dependencies] clap = { version = "^4.2", default-features = false, features = ["derive", "cargo", "unicode", "wrap_help", "help", "usage", "error-context", "suggestions"] } clap_mangen = "0.2.10" +mailpot = { version = "^0.1", path = "../core" } stderrlog = "^0.5" diff --git a/cli/src/args.rs b/cli/src/args.rs index d3f79d9..5cc26e8 100644 --- a/cli/src/args.rs +++ b/cli/src/args.rs @@ -19,7 +19,7 @@ pub use std::path::PathBuf; -pub use clap::{Args, CommandFactory, Parser, Subcommand}; +pub use clap::{builder::TypedValueParser, Args, CommandFactory, Parser, Subcommand}; #[derive(Debug, Parser)] #[command( @@ -105,7 +105,14 @@ pub enum Command { /// Mail that has not been handled properly end up in the error queue. ErrorQueue { #[command(subcommand)] - cmd: ErrorQueueCommand, + cmd: QueueCommand, + }, + /// Mail that has not been handled properly end up in the error queue. + Queue { + #[arg(long, value_parser = QueueValueParser)] + queue: mailpot::queue::Queue, + #[command(subcommand)] + cmd: QueueCommand, }, /// Import a maildir folder into an existing list. ImportMaildir { @@ -254,7 +261,7 @@ pub struct PostfixConfig { } #[derive(Debug, Subcommand)] -pub enum ErrorQueueCommand { +pub enum QueueCommand { /// List. List, /// Print entry in RFC5322 or JSON format. @@ -344,7 +351,7 @@ pub enum ListCommand { subscription_options: SubscriptionOptions, }, /// Add a new post policy. - AddPolicy { + AddPostPolicy { #[arg(long)] /// Only list owners can post. announce_only: bool, @@ -363,13 +370,13 @@ pub enum ListCommand { custom: bool, }, // Remove post policy. - RemovePolicy { + RemovePostPolicy { #[arg(long)] /// Post policy primary key. pk: i64, }, /// Add subscription policy to list. - AddSubscribePolicy { + AddSubscriptionPolicy { #[arg(long)] /// Send confirmation e-mail when subscription is finalized. send_confirmation: bool, @@ -386,9 +393,9 @@ pub enum ListCommand { /// Allow subscriptions, but handle it manually. custom: bool, }, - RemoveSubscribePolicy { + RemoveSubscriptionPolicy { #[arg(long)] - /// Subscribe policy primary key. + /// Subscription policy primary key. pk: i64, }, /// Add list owner to list. @@ -494,3 +501,57 @@ pub enum ListCommand { skip_owners: bool, }, } + +#[derive(Clone, Copy, Debug)] +pub struct QueueValueParser; + +impl QueueValueParser { + /// Implementation for [`ValueParser::path_buf`] + pub fn new() -> Self { + Self + } +} + +impl TypedValueParser for QueueValueParser { + type Value = mailpot::queue::Queue; + + fn parse_ref( + &self, + cmd: &clap::Command, + arg: Option<&clap::Arg>, + value: &std::ffi::OsStr, + ) -> std::result::Result { + TypedValueParser::parse(self, cmd, arg, value.to_owned()) + } + + fn parse( + &self, + cmd: &clap::Command, + _arg: Option<&clap::Arg>, + value: std::ffi::OsString, + ) -> std::result::Result { + use std::str::FromStr; + + use clap::error::ErrorKind; + + if value.is_empty() { + return Err(cmd.clone().error( + ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand, + "queue value required", + )); + } + Self::Value::from_str(value.to_str().ok_or_else(|| { + cmd.clone().error( + ErrorKind::InvalidValue, + "Queue value is not an UTF-8 string", + ) + })?) + .map_err(|err| cmd.clone().error(ErrorKind::InvalidValue, err)) + } +} + +impl Default for QueueValueParser { + fn default() -> Self { + Self::new() + } +} diff --git a/cli/src/lints.rs b/cli/src/lints.rs index 68b118f..f4771ba 100644 --- a/cli/src/lints.rs +++ b/cli/src/lints.rs @@ -228,6 +228,7 @@ pub fn warn_list_no_owner_lint(db: &mut Connection, _: bool) -> Result<()> { id: row.get("id")?, address: row.get("address")?, description: row.get("description")?, + topics: vec![], archive_url: row.get("archive_url")?, }, pk, diff --git a/cli/src/main.rs b/cli/src/main.rs index 26d0c3a..e3e71fe 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -29,7 +29,7 @@ use lints::*; use mailpot::{ melib::{backends::maildir::MaildirPathTrait, smol, smtp::*, Envelope, EnvelopeHash}, models::{changesets::*, *}, - queue::{Queue, QueueEntry}, + queue::QueueEntry, transaction::TransactionBehavior, Configuration, Connection, Error, ErrorKind, Result, *, }; @@ -127,9 +127,14 @@ fn run_app(opt: Opt) -> Result<()> { } } if let Some(s) = db.list_post_policy(l.pk)? { - println!("\tList policy: {}", s); + println!("\tPost policy: {}", s); } else { - println!("\tList policy: None"); + println!("\tPost policy: None"); + } + if let Some(s) = db.list_subscription_policy(l.pk)? { + println!("\tSubscription policy: {}", s); + } else { + println!("\tSubscription policy: None"); } println!(); } @@ -299,7 +304,7 @@ fn run_app(opt: Opt) -> Result<()> { }; db.update_subscription(changeset)?; } - AddPolicy { + AddPostPolicy { announce_only, subscription_only, approval_needed, @@ -318,11 +323,11 @@ fn run_app(opt: Opt) -> Result<()> { let new_val = db.set_list_post_policy(policy)?; println!("Added new policy with pk = {}", new_val.pk()); } - RemovePolicy { pk } => { + RemovePostPolicy { pk } => { db.remove_list_post_policy(list.pk, pk)?; println!("Removed policy with pk = {}", pk); } - AddSubscribePolicy { + AddSubscriptionPolicy { send_confirmation, open, manual, @@ -341,7 +346,7 @@ fn run_app(opt: Opt) -> Result<()> { let new_val = db.set_list_subscription_policy(policy)?; println!("Added new subscribe policy with pk = {}", new_val.pk()); } - RemoveSubscribePolicy { pk } => { + RemoveSubscriptionPolicy { pk } => { db.remove_list_subscription_policy(list.pk, pk)?; println!("Removed subscribe policy with pk = {}", pk); } @@ -491,6 +496,7 @@ fn run_app(opt: Opt) -> Result<()> { name, id, description, + topics: vec![], address, archive_url, })?; @@ -535,17 +541,17 @@ fn run_app(opt: Opt) -> Result<()> { let tx = db.transaction(TransactionBehavior::Exclusive).unwrap(); let messages = if opt.debug { println!("flush-queue dry_run {:?}", dry_run); - tx.queue(Queue::Out)? + tx.queue(mailpot::queue::Queue::Out)? .into_iter() .map(DbVal::into_inner) .chain( - tx.queue(Queue::Deferred)? + tx.queue(mailpot::queue::Queue::Deferred)? .into_iter() .map(DbVal::into_inner), ) .collect() } else { - tx.delete_from_queue(Queue::Out, vec![])? + tx.delete_from_queue(mailpot::queue::Queue::Out, vec![])? }; if opt.verbose > 0 || opt.debug { println!("Queue out has {} messages.", messages.len()); @@ -614,15 +620,15 @@ fn run_app(opt: Opt) -> Result<()> { for (err, mut msg) in failures { log::error!("Message {msg:?} failed with: {err}. Inserting to Deferred queue."); - msg.queue = Queue::Deferred; + msg.queue = mailpot::queue::Queue::Deferred; tx.insert_to_queue(msg)?; } tx.commit()?; } ErrorQueue { cmd } => match cmd { - ErrorQueueCommand::List => { - let errors = db.queue(Queue::Error)?; + QueueCommand::List => { + let errors = db.queue(mailpot::queue::Queue::Error)?; if errors.is_empty() { println!("Error queue is empty."); } else { @@ -634,8 +640,8 @@ fn run_app(opt: Opt) -> Result<()> { } } } - ErrorQueueCommand::Print { index } => { - let mut errors = db.queue(Queue::Error)?; + QueueCommand::Print { index } => { + let mut errors = db.queue(mailpot::queue::Queue::Error)?; if !index.is_empty() { errors.retain(|el| index.contains(&el.pk())); } @@ -647,8 +653,8 @@ fn run_app(opt: Opt) -> Result<()> { } } } - ErrorQueueCommand::Delete { index, quiet } => { - let mut errors = db.queue(Queue::Error)?; + QueueCommand::Delete { index, quiet } => { + let mut errors = db.queue(mailpot::queue::Queue::Error)?; if !index.is_empty() { errors.retain(|el| index.contains(&el.pk())); } @@ -660,7 +666,7 @@ fn run_app(opt: Opt) -> Result<()> { if !quiet { println!("Deleting error queue elements {:?}", &index); } - db.delete_from_queue(Queue::Error, index)?; + db.delete_from_queue(mailpot::queue::Queue::Error, index)?; if !quiet { for e in errors { println!("{e:?}"); @@ -669,6 +675,55 @@ fn run_app(opt: Opt) -> Result<()> { } } }, + Queue { queue, cmd } => match cmd { + QueueCommand::List => { + let entries = db.queue(queue)?; + if entries.is_empty() { + println!("Queue {queue} is empty."); + } else { + for e in entries { + println!( + "- {} {} {} {} {}", + e.pk, e.datetime, e.from_address, e.to_addresses, e.subject + ); + } + } + } + QueueCommand::Print { index } => { + let mut entries = db.queue(queue)?; + if !index.is_empty() { + entries.retain(|el| index.contains(&el.pk())); + } + if entries.is_empty() { + println!("Queue {queue} is empty."); + } else { + for e in entries { + println!("{e:?}"); + } + } + } + QueueCommand::Delete { index, quiet } => { + let mut entries = db.queue(queue)?; + if !index.is_empty() { + entries.retain(|el| index.contains(&el.pk())); + } + if entries.is_empty() { + if !quiet { + println!("Queue {queue} is empty."); + } + } else { + if !quiet { + println!("Deleting queue {queue} elements {:?}", &index); + } + db.delete_from_queue(queue, index)?; + if !quiet { + for e in entries { + println!("{e:?}"); + } + } + } + } + }, ImportMaildir { list_id, mut maildir_path, diff --git a/cli/tests/basic_interfaces.rs b/cli/tests/basic_interfaces.rs index 16a83c0..903d502 100644 --- a/cli/tests/basic_interfaces.rs +++ b/cli/tests/basic_interfaces.rs @@ -132,6 +132,7 @@ For more information, try '--help'."#, name: "foobar chat".into(), id: "foo-chat".into(), address: "foo-chat@example.com".into(), + topics: vec![], description: None, archive_url: None, }) @@ -142,8 +143,8 @@ For more information, try '--help'."#, list_lists( &conf_path, "- foo-chat DbVal(MailingList { pk: 1, name: \"foobar chat\", id: \"foo-chat\", address: \ - \"foo-chat@example.com\", description: None, archive_url: None }, 1)\n\tList owners: \ - None\n\tList policy: None", + \"foo-chat@example.com\", topics: [], description: None, archive_url: None }, 1)\n\tList \ + owners: None\n\tPost policy: None\n\tSubscription policy: None", ); fn create_list(conf: &Path) { @@ -171,9 +172,10 @@ For more information, try '--help'."#, list_lists( &conf_path, "- foo-chat DbVal(MailingList { pk: 1, name: \"foobar chat\", id: \"foo-chat\", address: \ - \"foo-chat@example.com\", description: None, archive_url: None }, 1)\n\tList owners: \ - None\n\tList policy: None\n\n- twobar-chat DbVal(MailingList { pk: 2, name: \"twobar\", \ - id: \"twobar-chat\", address: \"twobar-chat@example.com\", description: None, \ - archive_url: None }, 2)\n\tList owners: None\n\tList policy: None", + \"foo-chat@example.com\", topics: [], description: None, archive_url: None }, 1)\n\tList \ + owners: None\n\tPost policy: None\n\tSubscription policy: None\n\n- twobar-chat \ + DbVal(MailingList { pk: 2, name: \"twobar\", id: \"twobar-chat\", address: \ + \"twobar-chat@example.com\", topics: [], description: None, archive_url: None }, \ + 2)\n\tList owners: None\n\tPost policy: None\n\tSubscription policy: None", ); } diff --git a/cli/tests/out_queue_flush.rs b/cli/tests/out_queue_flush.rs index e962fd2..87a381b 100644 --- a/cli/tests/out_queue_flush.rs +++ b/cli/tests/out_queue_flush.rs @@ -85,6 +85,7 @@ fn test_out_queue_flush() { id: "foo-chat".into(), address: "foo-chat@example.com".into(), description: None, + topics: vec![], archive_url: None, }) .unwrap(); @@ -282,6 +283,7 @@ fn test_list_requests_submission() { id: "foo-chat".into(), address: "foo-chat@example.com".into(), description: None, + topics: vec![], archive_url: None, }) .unwrap(); diff --git a/core/migrations/001.sql b/core/migrations/001.sql index a62617c..345a376 100644 --- a/core/migrations/001.sql +++ b/core/migrations/001.sql @@ -1,4 +1,2 @@ PRAGMA foreign_keys=ON; -BEGIN; ALTER TABLE templates RENAME TO template; -COMMIT; diff --git a/core/migrations/001.undo.sql b/core/migrations/001.undo.sql index 86fe8ac..e0e03fb 100644 --- a/core/migrations/001.undo.sql +++ b/core/migrations/001.undo.sql @@ -1,4 +1,2 @@ PRAGMA foreign_keys=ON; -BEGIN; ALTER TABLE template RENAME TO templates; -COMMIT; diff --git a/core/migrations/002.sql b/core/migrations/002.sql new file mode 100644 index 0000000..7dbb83a --- /dev/null +++ b/core/migrations/002.sql @@ -0,0 +1,2 @@ +PRAGMA foreign_keys=ON; +ALTER TABLE list ADD COLUMN topics JSON NOT NULL CHECK (json_type(topics) == 'array') DEFAULT '[]'; diff --git a/core/migrations/002.undo.sql b/core/migrations/002.undo.sql new file mode 100644 index 0000000..9a18755 --- /dev/null +++ b/core/migrations/002.undo.sql @@ -0,0 +1,2 @@ +PRAGMA foreign_keys=ON; +ALTER TABLE list DROP COLUMN topics; diff --git a/core/src/connection.rs b/core/src/connection.rs index aa1866e..05ad84c 100644 --- a/core/src/connection.rs +++ b/core/src/connection.rs @@ -195,7 +195,7 @@ impl Connection { conn.set_db_config(DbConfig::SQLITE_DBCONFIG_ENABLE_FKEY, true)?; conn.set_db_config(DbConfig::SQLITE_DBCONFIG_ENABLE_TRIGGER, true)?; conn.set_db_config(DbConfig::SQLITE_DBCONFIG_DEFENSIVE, true)?; - conn.set_db_config(DbConfig::SQLITE_DBCONFIG_TRUSTED_SCHEMA, false)?; + conn.set_db_config(DbConfig::SQLITE_DBCONFIG_TRUSTED_SCHEMA, true)?; conn.busy_timeout(core::time::Duration::from_millis(500))?; conn.busy_handler(Some(|times: i32| -> bool { times < 5 }))?; @@ -246,14 +246,14 @@ impl Connection { if undo { "un " } else { "re" } ); if undo { - trace!("{}", Self::MIGRATIONS[from as usize].2); + trace!("{}", Self::MIGRATIONS[from as usize - 1].2); tx.connection - .execute(Self::MIGRATIONS[from as usize].2, [])?; + .execute_batch(Self::MIGRATIONS[from as usize - 1].2)?; from -= 1; } else { trace!("{}", Self::MIGRATIONS[from as usize].1); tx.connection - .execute(Self::MIGRATIONS[from as usize].1, [])?; + .execute_batch(Self::MIGRATIONS[from as usize].1)?; from += 1; } } @@ -384,6 +384,8 @@ impl Connection { let mut stmt = self.connection.prepare("SELECT * FROM list;")?; let list_iter = stmt.query_map([], |row| { let pk = row.get("pk")?; + let topics: serde_json::Value = row.get("topics")?; + let topics = MailingList::topics_from_json_value(topics)?; Ok(DbVal( MailingList { pk, @@ -391,6 +393,7 @@ impl Connection { id: row.get("id")?, address: row.get("address")?, description: row.get("description")?, + topics, archive_url: row.get("archive_url")?, }, pk, @@ -413,6 +416,8 @@ impl Connection { let ret = stmt .query_row([&pk], |row| { let pk = row.get("pk")?; + let topics: serde_json::Value = row.get("topics")?; + let topics = MailingList::topics_from_json_value(topics)?; Ok(DbVal( MailingList { pk, @@ -420,6 +425,7 @@ impl Connection { id: row.get("id")?, address: row.get("address")?, description: row.get("description")?, + topics, archive_url: row.get("archive_url")?, }, pk, @@ -438,6 +444,8 @@ impl Connection { let ret = stmt .query_row([&id], |row| { let pk = row.get("pk")?; + let topics: serde_json::Value = row.get("topics")?; + let topics = MailingList::topics_from_json_value(topics)?; Ok(DbVal( MailingList { pk, @@ -445,6 +453,7 @@ impl Connection { id: row.get("id")?, address: row.get("address")?, description: row.get("description")?, + topics, archive_url: row.get("archive_url")?, }, pk, @@ -458,8 +467,8 @@ impl Connection { /// Create a new list. pub fn create_list(&self, new_val: MailingList) -> Result> { let mut stmt = self.connection.prepare( - "INSERT INTO list(name, id, address, description, archive_url) VALUES(?, ?, ?, ?, ?) \ - RETURNING *;", + "INSERT INTO list(name, id, address, description, archive_url, topics) VALUES(?, ?, \ + ?, ?, ?, ?) RETURNING *;", )?; let ret = stmt.query_row( rusqlite::params![ @@ -468,9 +477,12 @@ impl Connection { &new_val.address, new_val.description.as_ref(), new_val.archive_url.as_ref(), + serde_json::json!(new_val.topics.as_slice()), ], |row| { let pk = row.get("pk")?; + let topics: serde_json::Value = row.get("topics")?; + let topics = MailingList::topics_from_json_value(topics)?; Ok(DbVal( MailingList { pk, @@ -478,6 +490,7 @@ impl Connection { id: row.get("id")?, address: row.get("address")?, description: row.get("description")?, + topics, archive_url: row.get("archive_url")?, }, pk, @@ -999,6 +1012,7 @@ mod tests { name: "".into(), id: "".into(), description: None, + topics: vec![], address: "".into(), archive_url: None, }; diff --git a/core/src/doctests/db_setup.rs.inc b/core/src/doctests/db_setup.rs.inc index bb38811..46b82ca 100644 --- a/core/src/doctests/db_setup.rs.inc +++ b/core/src/doctests/db_setup.rs.inc @@ -29,6 +29,7 @@ # id: "foo-chat".into(), # address: "foo-chat@example.com".into(), # description: Some("Hello world, from foo-chat list".into()), +# topics: vec![], # archive_url: Some("https://lists.example.com".into()), # }) # .unwrap(); diff --git a/core/src/errors.rs b/core/src/errors.rs index 113bb70..8aebb2a 100644 --- a/core/src/errors.rs +++ b/core/src/errors.rs @@ -66,3 +66,11 @@ error_chain! { Template(minijinja::Error) #[doc="Error returned from minijinja template engine."]; } } + +impl Error { + /// Helper function to create a new generic error message. + pub fn new_external>(msg: S) -> Self { + let msg = msg.into(); + Self::from(ErrorKind::External(anyhow::Error::msg(msg))) + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index 9c3fdbb..f6520c3 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -83,6 +83,7 @@ //! name: "foobar chat".into(), //! id: "foo-chat".into(), //! address: "foo-chat@example.com".into(), +//! topics: vec![], //! description: None, //! archive_url: None, //! })? diff --git a/core/src/migrations.rs.inc b/core/src/migrations.rs.inc index b6ad33e..ffdaa44 100644 --- a/core/src/migrations.rs.inc +++ b/core/src/migrations.rs.inc @@ -1,11 +1,11 @@ //(user_version, redo sql, undo sql &[(1,"PRAGMA foreign_keys=ON; -BEGIN; ALTER TABLE templates RENAME TO template; -COMMIT; ","PRAGMA foreign_keys=ON; -BEGIN; ALTER TABLE template RENAME TO templates; -COMMIT; +"),(2,"PRAGMA foreign_keys=ON; +ALTER TABLE list ADD COLUMN topics JSON NOT NULL CHECK (json_type(topics) == 'array') DEFAULT '[]'; +","PRAGMA foreign_keys=ON; +ALTER TABLE list DROP COLUMN topics; "),] \ No newline at end of file diff --git a/core/src/models.rs b/core/src/models.rs index fbac235..e4fe69a 100644 --- a/core/src/models.rs +++ b/core/src/models.rs @@ -92,6 +92,8 @@ pub struct MailingList { pub id: String, /// Mailing list e-mail address. pub address: String, + /// Discussion topics. + pub topics: Vec, /// Mailing list description. pub description: Option, /// Mailing list archive URL. @@ -417,6 +419,52 @@ impl MailingList { contact = self.owner_mailto().address, ) } + + /// Utility function to get a `Vec` -which is the expected type of + /// the `topics` field- from a `serde_json::Value`, which is the value + /// stored in the `topics` column in `sqlite3`. + /// + /// # Example + /// + /// ```rust + /// # use mailpot::models::MailingList; + /// use serde_json::Value; + /// + /// # fn main() -> Result<(), serde_json::Error> { + /// let value: Value = serde_json::from_str(r#"["fruits","vegetables"]"#)?; + /// assert_eq!( + /// MailingList::topics_from_json_value(value), + /// Ok(vec!["fruits".to_string(), "vegetables".to_string()]) + /// ); + /// + /// let value: Value = serde_json::from_str(r#"{"invalid":"value"}"#)?; + /// assert!(MailingList::topics_from_json_value(value).is_err()); + /// # Ok(()) + /// # } + /// ``` + pub fn topics_from_json_value( + v: serde_json::Value, + ) -> std::result::Result, rusqlite::Error> { + let err_fn = || { + rusqlite::Error::FromSqlConversionFailure( + 8, + rusqlite::types::Type::Text, + anyhow::Error::msg( + "topics column must be a json array of strings serialized as a string, e.g. \ + \"[]\" or \"['topicA', 'topicB']\"", + ) + .into(), + ) + }; + v.as_array() + .map(|arr| { + arr.iter() + .map(|v| v.as_str().map(str::to_string)) + .collect::>>() + }) + .ok_or_else(err_fn)? + .ok_or_else(err_fn) + } } /// A mailing list subscription entry. diff --git a/core/src/policies.rs b/core/src/policies.rs index 902404a..12e2df3 100644 --- a/core/src/policies.rs +++ b/core/src/policies.rs @@ -84,6 +84,7 @@ mod post_policy { /// id: "foo-chat".into(), /// address: "foo-chat@example.com".into(), /// description: None, + /// topics: vec![], /// archive_url: None, /// }) /// .unwrap(); @@ -278,6 +279,7 @@ mod subscription_policy { /// id: "foo-chat".into(), /// address: "foo-chat@example.com".into(), /// description: None, + /// topics: vec![], /// archive_url: None, /// }) /// .unwrap(); diff --git a/core/src/postfix.rs b/core/src/postfix.rs index f3446c1..4673d74 100644 --- a/core/src/postfix.rs +++ b/core/src/postfix.rs @@ -430,6 +430,7 @@ fn test_postfix_generation() -> Result<()> { id: "first".into(), address: "first@example.com".into(), description: None, + topics: vec![], archive_url: None, })?; assert_eq!(first.pk(), 1); @@ -439,6 +440,7 @@ fn test_postfix_generation() -> Result<()> { id: "second".into(), address: "second@example.com".into(), description: None, + topics: vec![], archive_url: None, })?; assert_eq!(second.pk(), 2); @@ -459,6 +461,7 @@ fn test_postfix_generation() -> Result<()> { id: "third".into(), address: "third@example.com".into(), description: None, + topics: vec![], archive_url: None, })?; assert_eq!(third.pk(), 3); diff --git a/core/src/queue.rs b/core/src/queue.rs index 8cb311e..45761ad 100644 --- a/core/src/queue.rs +++ b/core/src/queue.rs @@ -51,6 +51,22 @@ pub enum Queue { Error, } +impl std::str::FromStr for Queue { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(match s.trim() { + s if s.eq_ignore_ascii_case(stringify!(Maildrop)) => Self::Maildrop, + s if s.eq_ignore_ascii_case(stringify!(Hold)) => Self::Hold, + s if s.eq_ignore_ascii_case(stringify!(Deferred)) => Self::Deferred, + s if s.eq_ignore_ascii_case(stringify!(Corrupt)) => Self::Corrupt, + s if s.eq_ignore_ascii_case(stringify!(Out)) => Self::Out, + s if s.eq_ignore_ascii_case(stringify!(Error)) => Self::Error, + other => return Err(Error::new_external(format!("Invalid Queue name: {other}."))), + }) + } +} + impl Queue { /// Returns the name of the queue used in the database schema. pub fn as_str(&self) -> &'static str { @@ -65,6 +81,12 @@ impl Queue { } } +impl std::fmt::Display for Queue { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(fmt, "{}", self.as_str()) + } +} + /// A queue entry. #[derive(Clone, Deserialize, Serialize, PartialEq, Eq)] pub struct QueueEntry { diff --git a/core/src/schema.sql b/core/src/schema.sql index 30654a6..aba82bb 100644 --- a/core/src/schema.sql +++ b/core/src/schema.sql @@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS list ( request_local_part TEXT, archive_url TEXT, description TEXT, + topics JSON NOT NULL CHECK (json_type(topics) = 'array') DEFAULT '[]', created INTEGER NOT NULL DEFAULT (unixepoch()), last_modified INTEGER NOT NULL DEFAULT (unixepoch()), verify BOOLEAN CHECK (verify IN (0, 1)) NOT NULL DEFAULT 1, diff --git a/core/src/schema.sql.m4 b/core/src/schema.sql.m4 index 0f1cee6..93324d6 100644 --- a/core/src/schema.sql.m4 +++ b/core/src/schema.sql.m4 @@ -44,6 +44,7 @@ CREATE TABLE IF NOT EXISTS list ( request_local_part TEXT, archive_url TEXT, description TEXT, + topics JSON NOT NULL CHECK (json_type(topics) = 'array') DEFAULT '[]', created INTEGER NOT NULL DEFAULT (unixepoch()), last_modified INTEGER NOT NULL DEFAULT (unixepoch()), verify BOOLEAN_TYPE(verify) DEFAULT BOOLEAN_TRUE(), diff --git a/core/src/subscriptions.rs b/core/src/subscriptions.rs index fce5678..f25ffb9 100644 --- a/core/src/subscriptions.rs +++ b/core/src/subscriptions.rs @@ -623,6 +623,7 @@ mod tests { name: "foobar chat".into(), id: "foo-chat".into(), address: "foo-chat@example.com".into(), + topics: vec![], description: None, archive_url: None, }) @@ -633,6 +634,7 @@ mod tests { name: "foobar chat2".into(), id: "foo-chat2".into(), address: "foo-chat2@example.com".into(), + topics: vec![], description: None, archive_url: None, }) diff --git a/core/tests/account.rs b/core/tests/account.rs index 0a97f20..f02a05f 100644 --- a/core/tests/account.rs +++ b/core/tests/account.rs @@ -46,6 +46,7 @@ fn test_accounts() { id: "foo-chat".into(), address: "foo-chat@example.com".into(), description: None, + topics: vec![], archive_url: None, }) .unwrap(); diff --git a/core/tests/authorizer.rs b/core/tests/authorizer.rs index 12e2349..b5fa1ca 100644 --- a/core/tests/authorizer.rs +++ b/core/tests/authorizer.rs @@ -46,6 +46,7 @@ fn test_authorizer() { id: "foo-chat".into(), address: "foo-chat@example.com".into(), description: None, + topics: vec![], archive_url: None, }) .unwrap_err(), @@ -84,6 +85,7 @@ fn test_authorizer() { id: "foo-chat".into(), address: "foo-chat@example.com".into(), description: None, + topics: vec![], archive_url: None, }) .map(|_| ()), diff --git a/core/tests/creation.rs b/core/tests/creation.rs index c244181..31aa0cc 100644 --- a/core/tests/creation.rs +++ b/core/tests/creation.rs @@ -61,6 +61,7 @@ fn test_list_creation() { id: "foo-chat".into(), address: "foo-chat@example.com".into(), description: None, + topics: vec![], archive_url: None, }) .unwrap(); diff --git a/core/tests/error_queue.rs b/core/tests/error_queue.rs index fa33b83..ed8a117 100644 --- a/core/tests/error_queue.rs +++ b/core/tests/error_queue.rs @@ -55,6 +55,7 @@ fn test_error_queue() { id: "foo-chat".into(), address: "foo-chat@example.com".into(), description: None, + topics: vec![], archive_url: None, }) .unwrap(); diff --git a/core/tests/smtp.rs b/core/tests/smtp.rs index 63160a9..6fc84d9 100644 --- a/core/tests/smtp.rs +++ b/core/tests/smtp.rs @@ -48,6 +48,7 @@ fn test_smtp() { id: "foo-chat".into(), address: "foo-chat@example.com".into(), description: None, + topics: vec![], archive_url: None, }) .unwrap(); @@ -202,6 +203,7 @@ fn test_smtp_mailcrab() { id: "foo-chat".into(), address: "foo-chat@example.com".into(), description: None, + topics: vec![], archive_url: None, }) .unwrap(); diff --git a/core/tests/subscription.rs b/core/tests/subscription.rs index c83f201..c92081a 100644 --- a/core/tests/subscription.rs +++ b/core/tests/subscription.rs @@ -44,6 +44,7 @@ fn test_list_subscription() { id: "foo-chat".into(), address: "foo-chat@example.com".into(), description: None, + topics: vec![], archive_url: None, }) .unwrap(); @@ -178,6 +179,7 @@ fn test_post_rejection() { id: "foo-chat".into(), address: "foo-chat@example.com".into(), description: None, + topics: vec![], archive_url: None, }) .unwrap(); diff --git a/core/tests/template_replies.rs b/core/tests/template_replies.rs index 438c6c2..8648b2e 100644 --- a/core/tests/template_replies.rs +++ b/core/tests/template_replies.rs @@ -67,6 +67,7 @@ MIME-Version: 1.0 id: "foo-chat".into(), address: "foo-chat@example.com".into(), description: None, + topics: vec![], archive_url: None, }) .unwrap(); diff --git a/docs/mpot.1 b/docs/mpot.1 index 02a0504..f950b67 100644 --- a/docs/mpot.1 +++ b/docs/mpot.1 @@ -330,13 +330,13 @@ Is subscription enabled. .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\fB -.SS mpot list add-policy +.SS mpot list add-post-policy .\fR .br .br -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] +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] .br Add a new post policy. @@ -358,13 +358,13 @@ Allow posts, but handle it manually. .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\fB -.SS mpot list remove-policy +.SS mpot list remove-post-policy .\fR .br .br -mpot list remove\-policy \-\-pk \fIPK\fR +mpot list remove\-post\-policy \-\-pk \fIPK\fR .br .TP @@ -373,13 +373,13 @@ Post policy primary key. .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\fB -.SS mpot list add-subscribe-policy +.SS mpot list add-subscription-policy .\fR .br .br -mpot list add\-subscribe\-policy [\-\-send\-confirmation \fISEND_CONFIRMATION\fR] [\-\-open \fIOPEN\fR] [\-\-manual \fIMANUAL\fR] [\-\-request \fIREQUEST\fR] [\-\-custom \fICUSTOM\fR] +mpot list add\-subscription\-policy [\-\-send\-confirmation \fISEND_CONFIRMATION\fR] [\-\-open \fIOPEN\fR] [\-\-manual \fIMANUAL\fR] [\-\-request \fIREQUEST\fR] [\-\-custom \fICUSTOM\fR] .br Add subscription policy to list. @@ -401,18 +401,18 @@ Allow subscriptions, but handle it manually. .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\fB -.SS mpot list remove-subscribe-policy +.SS mpot list remove-subscription-policy .\fR .br .br -mpot list remove\-subscribe\-policy \-\-pk \fIPK\fR +mpot list remove\-subscription\-policy \-\-pk \fIPK\fR .br .TP \-\-pk \fIPK\fR -Subscribe policy primary key. +Subscription policy primary key. .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\fB @@ -705,6 +705,67 @@ index of entry. mpot error\-queue delete [\-\-index \fIINDEX\fR] [\-\-quiet \fIQUIET\fR] .br +Delete entry and print it in stdout. +.TP +\-\-index \fIINDEX\fR +index of entry. +.TP +\-\-quiet +Do not print in stdout. +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.\fB +.SS mpot queue +.\fR +.br + +.br + +mpot queue \-\-queue \fIQUEUE\fR +.br + +Mail that has not been handled properly end up in the error queue. +.TP +\-\-queue \fIQUEUE\fR + +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.\fB +.SS mpot queue list +.\fR +.br + +.br + +List. +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.\fB +.SS mpot queue print +.\fR +.br + +.br + +mpot queue print [\-\-index \fIINDEX\fR] +.br + +Print entry in RFC5322 or JSON format. +.TP +\-\-index \fIINDEX\fR +index of entry. +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.\fB +.SS mpot queue delete +.\fR +.br + +.br + +mpot queue delete [\-\-index \fIINDEX\fR] [\-\-quiet \fIQUIET\fR] +.br + Delete entry and print it in stdout. .TP \-\-index \fIINDEX\fR diff --git a/rest-http/src/routes/list.rs b/rest-http/src/routes/list.rs index ebe9910..e7f4211 100644 --- a/rest-http/src/routes/list.rs +++ b/rest-http/src/routes/list.rs @@ -217,6 +217,10 @@ mod tests { let db_path = tmp_dir.path().join("mpot.db"); std::fs::copy("../mailpot-tests/for_testing.db", &db_path).unwrap(); + let mut perms = std::fs::metadata(&db_path).unwrap().permissions(); + #[allow(clippy::permissions_set_readonly_false)] + perms.set_readonly(false); + std::fs::set_permissions(&db_path, perms).unwrap(); let config = Configuration { send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), db_path, @@ -231,9 +235,11 @@ mod tests { name: "foobar chat".into(), id: "foo-chat".into(), address: "foo-chat@example.com".into(), + topics: vec![], description: None, archive_url: None, }; + assert_eq!(&db.lists().unwrap().remove(0).into_inner(), &foo_chat); drop(db); let config = Arc::new(config); diff --git a/web/src/minijinja_utils.rs b/web/src/minijinja_utils.rs index 04da2d1..b7b0c04 100644 --- a/web/src/minijinja_utils.rs +++ b/web/src/minijinja_utils.rs @@ -92,6 +92,7 @@ pub struct MailingList { pub id: String, pub address: String, pub description: Option, + pub topics: Vec, #[serde(serialize_with = "super::utils::to_safe_string_opt")] pub archive_url: Option, pub inner: DbVal, @@ -106,6 +107,7 @@ impl From> for MailingList { id, address, description, + topics, archive_url, }, _, @@ -117,6 +119,7 @@ impl From> for MailingList { id, address, description, + topics, archive_url, inner: val, } @@ -163,13 +166,24 @@ impl minijinja::value::StructObject for MailingList { "id" => Some(Value::from_serializable(&self.id)), "address" => Some(Value::from_serializable(&self.address)), "description" => Some(Value::from_serializable(&self.description)), + "topics" => Some(Value::from_serializable(&self.topics)), "archive_url" => Some(Value::from_serializable(&self.archive_url)), _ => None, } } fn static_fields(&self) -> Option<&'static [&'static str]> { - Some(&["pk", "name", "id", "address", "description", "archive_url"][..]) + Some( + &[ + "pk", + "name", + "id", + "address", + "description", + "topics", + "archive_url", + ][..], + ) } }