core: Add topics field to MailingList
parent
52ef646fae
commit
e8120c75db
|
@ -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;
|
||||
|
||||
|
|
|
@ -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",
|
||||
][..],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -1,4 +1,2 @@
|
|||
PRAGMA foreign_keys=ON;
|
||||
BEGIN;
|
||||
ALTER TABLE templates RENAME TO template;
|
||||
COMMIT;
|
||||
|
|
|
@ -1,4 +1,2 @@
|
|||
PRAGMA foreign_keys=ON;
|
||||
BEGIN;
|
||||
ALTER TABLE template RENAME TO templates;
|
||||
COMMIT;
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
PRAGMA foreign_keys=ON;
|
||||
ALTER TABLE list ADD COLUMN topics JSON NOT NULL CHECK (json_type(topics) == 'array') DEFAULT '[]';
|
|
@ -0,0 +1,2 @@
|
|||
PRAGMA foreign_keys=ON;
|
||||
ALTER TABLE list DROP COLUMN topics;
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
//! })?
|
||||
|
|
|
@ -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;
|
||||
"),]
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(|_| ()),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
79
docs/mpot.1
79
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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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",
|
||||
][..],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue