core: Add topics field to MailingList

axum-login-upgrade
Manos Pitsidianakis 2023-05-18 10:34:00 +03:00
parent 52ef646fae
commit e8120c75db
Signed by: Manos Pitsidianakis
GPG Key ID: 7729C7707F7E09D0
34 changed files with 388 additions and 57 deletions

View File

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

View File

@ -57,6 +57,7 @@ pub struct MailingList {
pub id: String,
pub address: String,
pub description: Option<String>,
pub topics: Vec<String>,
pub archive_url: Option<String>,
pub inner: DbVal<mailpot::models::MailingList>,
}
@ -70,6 +71,7 @@ impl From<DbVal<mailpot::models::MailingList>> for MailingList {
id,
address,
description,
topics,
archive_url,
},
_,
@ -81,6 +83,7 @@ impl From<DbVal<mailpot::models::MailingList>> 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",
][..],
)
}
}

View File

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

View File

@ -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<Self::Value, clap::Error> {
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<Self::Value, clap::Error> {
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()
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,2 @@
PRAGMA foreign_keys=ON;
BEGIN;
ALTER TABLE templates RENAME TO template;
COMMIT;

View File

@ -1,4 +1,2 @@
PRAGMA foreign_keys=ON;
BEGIN;
ALTER TABLE template RENAME TO templates;
COMMIT;

View File

@ -0,0 +1,2 @@
PRAGMA foreign_keys=ON;
ALTER TABLE list ADD COLUMN topics JSON NOT NULL CHECK (json_type(topics) == 'array') DEFAULT '[]';

View File

@ -0,0 +1,2 @@
PRAGMA foreign_keys=ON;
ALTER TABLE list DROP COLUMN topics;

View File

@ -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<DbVal<MailingList>> {
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,
};

View File

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

View File

@ -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<S: Into<String>>(msg: S) -> Self {
let msg = msg.into();
Self::from(ErrorKind::External(anyhow::Error::msg(msg)))
}
}

View File

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

View File

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

View File

@ -92,6 +92,8 @@ pub struct MailingList {
pub id: String,
/// Mailing list e-mail address.
pub address: String,
/// Discussion topics.
pub topics: Vec<String>,
/// Mailing list description.
pub description: Option<String>,
/// Mailing list archive URL.
@ -417,6 +419,52 @@ impl MailingList {
contact = self.owner_mailto().address,
)
}
/// Utility function to get a `Vec<String>` -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<Vec<String>, 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::<Option<Vec<String>>>()
})
.ok_or_else(err_fn)?
.ok_or_else(err_fn)
}
}
/// A mailing list subscription entry.

View File

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

View File

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

View File

@ -51,6 +51,22 @@ pub enum Queue {
Error,
}
impl std::str::FromStr for Queue {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -92,6 +92,7 @@ pub struct MailingList {
pub id: String,
pub address: String,
pub description: Option<String>,
pub topics: Vec<String>,
#[serde(serialize_with = "super::utils::to_safe_string_opt")]
pub archive_url: Option<String>,
pub inner: DbVal<mailpot::models::MailingList>,
@ -106,6 +107,7 @@ impl From<DbVal<mailpot::models::MailingList>> for MailingList {
id,
address,
description,
topics,
archive_url,
},
_,
@ -117,6 +119,7 @@ impl From<DbVal<mailpot::models::MailingList>> 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",
][..],
)
}
}