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(),
|
id: "foo-chat".into(),
|
||||||
address: "foo-chat@example.com".into(),
|
address: "foo-chat@example.com".into(),
|
||||||
description: None,
|
description: None,
|
||||||
|
topics: vec![],
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
})?.pk;
|
})?.pk;
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,7 @@ pub struct MailingList {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub address: String,
|
pub address: String,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
pub topics: Vec<String>,
|
||||||
pub archive_url: Option<String>,
|
pub archive_url: Option<String>,
|
||||||
pub inner: DbVal<mailpot::models::MailingList>,
|
pub inner: DbVal<mailpot::models::MailingList>,
|
||||||
}
|
}
|
||||||
|
@ -70,6 +71,7 @@ impl From<DbVal<mailpot::models::MailingList>> for MailingList {
|
||||||
id,
|
id,
|
||||||
address,
|
address,
|
||||||
description,
|
description,
|
||||||
|
topics,
|
||||||
archive_url,
|
archive_url,
|
||||||
},
|
},
|
||||||
_,
|
_,
|
||||||
|
@ -81,6 +83,7 @@ impl From<DbVal<mailpot::models::MailingList>> for MailingList {
|
||||||
id,
|
id,
|
||||||
address,
|
address,
|
||||||
description,
|
description,
|
||||||
|
topics,
|
||||||
archive_url,
|
archive_url,
|
||||||
inner: val,
|
inner: val,
|
||||||
}
|
}
|
||||||
|
@ -127,13 +130,24 @@ impl minijinja::value::StructObject for MailingList {
|
||||||
"id" => Some(Value::from_serializable(&self.id)),
|
"id" => Some(Value::from_serializable(&self.id)),
|
||||||
"address" => Some(Value::from_serializable(&self.address)),
|
"address" => Some(Value::from_serializable(&self.address)),
|
||||||
"description" => Some(Value::from_serializable(&self.description)),
|
"description" => Some(Value::from_serializable(&self.description)),
|
||||||
|
"topics" => Some(Value::from_serializable(&self.topics)),
|
||||||
"archive_url" => Some(Value::from_serializable(&self.archive_url)),
|
"archive_url" => Some(Value::from_serializable(&self.archive_url)),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn static_fields(&self) -> Option<&'static [&'static str]> {
|
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]
|
[build-dependencies]
|
||||||
clap = { version = "^4.2", default-features = false, features = ["derive", "cargo", "unicode", "wrap_help", "help", "usage", "error-context", "suggestions"] }
|
clap = { version = "^4.2", default-features = false, features = ["derive", "cargo", "unicode", "wrap_help", "help", "usage", "error-context", "suggestions"] }
|
||||||
clap_mangen = "0.2.10"
|
clap_mangen = "0.2.10"
|
||||||
|
mailpot = { version = "^0.1", path = "../core" }
|
||||||
stderrlog = "^0.5"
|
stderrlog = "^0.5"
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
pub use std::path::PathBuf;
|
pub use std::path::PathBuf;
|
||||||
|
|
||||||
pub use clap::{Args, CommandFactory, Parser, Subcommand};
|
pub use clap::{builder::TypedValueParser, Args, CommandFactory, Parser, Subcommand};
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
#[command(
|
#[command(
|
||||||
|
@ -105,7 +105,14 @@ pub enum Command {
|
||||||
/// Mail that has not been handled properly end up in the error queue.
|
/// Mail that has not been handled properly end up in the error queue.
|
||||||
ErrorQueue {
|
ErrorQueue {
|
||||||
#[command(subcommand)]
|
#[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.
|
/// Import a maildir folder into an existing list.
|
||||||
ImportMaildir {
|
ImportMaildir {
|
||||||
|
@ -254,7 +261,7 @@ pub struct PostfixConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub enum ErrorQueueCommand {
|
pub enum QueueCommand {
|
||||||
/// List.
|
/// List.
|
||||||
List,
|
List,
|
||||||
/// Print entry in RFC5322 or JSON format.
|
/// Print entry in RFC5322 or JSON format.
|
||||||
|
@ -344,7 +351,7 @@ pub enum ListCommand {
|
||||||
subscription_options: SubscriptionOptions,
|
subscription_options: SubscriptionOptions,
|
||||||
},
|
},
|
||||||
/// Add a new post policy.
|
/// Add a new post policy.
|
||||||
AddPolicy {
|
AddPostPolicy {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
/// Only list owners can post.
|
/// Only list owners can post.
|
||||||
announce_only: bool,
|
announce_only: bool,
|
||||||
|
@ -363,13 +370,13 @@ pub enum ListCommand {
|
||||||
custom: bool,
|
custom: bool,
|
||||||
},
|
},
|
||||||
// Remove post policy.
|
// Remove post policy.
|
||||||
RemovePolicy {
|
RemovePostPolicy {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
/// Post policy primary key.
|
/// Post policy primary key.
|
||||||
pk: i64,
|
pk: i64,
|
||||||
},
|
},
|
||||||
/// Add subscription policy to list.
|
/// Add subscription policy to list.
|
||||||
AddSubscribePolicy {
|
AddSubscriptionPolicy {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
/// Send confirmation e-mail when subscription is finalized.
|
/// Send confirmation e-mail when subscription is finalized.
|
||||||
send_confirmation: bool,
|
send_confirmation: bool,
|
||||||
|
@ -386,9 +393,9 @@ pub enum ListCommand {
|
||||||
/// Allow subscriptions, but handle it manually.
|
/// Allow subscriptions, but handle it manually.
|
||||||
custom: bool,
|
custom: bool,
|
||||||
},
|
},
|
||||||
RemoveSubscribePolicy {
|
RemoveSubscriptionPolicy {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
/// Subscribe policy primary key.
|
/// Subscription policy primary key.
|
||||||
pk: i64,
|
pk: i64,
|
||||||
},
|
},
|
||||||
/// Add list owner to list.
|
/// Add list owner to list.
|
||||||
|
@ -494,3 +501,57 @@ pub enum ListCommand {
|
||||||
skip_owners: bool,
|
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")?,
|
id: row.get("id")?,
|
||||||
address: row.get("address")?,
|
address: row.get("address")?,
|
||||||
description: row.get("description")?,
|
description: row.get("description")?,
|
||||||
|
topics: vec![],
|
||||||
archive_url: row.get("archive_url")?,
|
archive_url: row.get("archive_url")?,
|
||||||
},
|
},
|
||||||
pk,
|
pk,
|
||||||
|
|
|
@ -29,7 +29,7 @@ use lints::*;
|
||||||
use mailpot::{
|
use mailpot::{
|
||||||
melib::{backends::maildir::MaildirPathTrait, smol, smtp::*, Envelope, EnvelopeHash},
|
melib::{backends::maildir::MaildirPathTrait, smol, smtp::*, Envelope, EnvelopeHash},
|
||||||
models::{changesets::*, *},
|
models::{changesets::*, *},
|
||||||
queue::{Queue, QueueEntry},
|
queue::QueueEntry,
|
||||||
transaction::TransactionBehavior,
|
transaction::TransactionBehavior,
|
||||||
Configuration, Connection, Error, ErrorKind, Result, *,
|
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)? {
|
if let Some(s) = db.list_post_policy(l.pk)? {
|
||||||
println!("\tList policy: {}", s);
|
println!("\tPost policy: {}", s);
|
||||||
} else {
|
} 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!();
|
println!();
|
||||||
}
|
}
|
||||||
|
@ -299,7 +304,7 @@ fn run_app(opt: Opt) -> Result<()> {
|
||||||
};
|
};
|
||||||
db.update_subscription(changeset)?;
|
db.update_subscription(changeset)?;
|
||||||
}
|
}
|
||||||
AddPolicy {
|
AddPostPolicy {
|
||||||
announce_only,
|
announce_only,
|
||||||
subscription_only,
|
subscription_only,
|
||||||
approval_needed,
|
approval_needed,
|
||||||
|
@ -318,11 +323,11 @@ fn run_app(opt: Opt) -> Result<()> {
|
||||||
let new_val = db.set_list_post_policy(policy)?;
|
let new_val = db.set_list_post_policy(policy)?;
|
||||||
println!("Added new policy with pk = {}", new_val.pk());
|
println!("Added new policy with pk = {}", new_val.pk());
|
||||||
}
|
}
|
||||||
RemovePolicy { pk } => {
|
RemovePostPolicy { pk } => {
|
||||||
db.remove_list_post_policy(list.pk, pk)?;
|
db.remove_list_post_policy(list.pk, pk)?;
|
||||||
println!("Removed policy with pk = {}", pk);
|
println!("Removed policy with pk = {}", pk);
|
||||||
}
|
}
|
||||||
AddSubscribePolicy {
|
AddSubscriptionPolicy {
|
||||||
send_confirmation,
|
send_confirmation,
|
||||||
open,
|
open,
|
||||||
manual,
|
manual,
|
||||||
|
@ -341,7 +346,7 @@ fn run_app(opt: Opt) -> Result<()> {
|
||||||
let new_val = db.set_list_subscription_policy(policy)?;
|
let new_val = db.set_list_subscription_policy(policy)?;
|
||||||
println!("Added new subscribe policy with pk = {}", new_val.pk());
|
println!("Added new subscribe policy with pk = {}", new_val.pk());
|
||||||
}
|
}
|
||||||
RemoveSubscribePolicy { pk } => {
|
RemoveSubscriptionPolicy { pk } => {
|
||||||
db.remove_list_subscription_policy(list.pk, pk)?;
|
db.remove_list_subscription_policy(list.pk, pk)?;
|
||||||
println!("Removed subscribe policy with pk = {}", pk);
|
println!("Removed subscribe policy with pk = {}", pk);
|
||||||
}
|
}
|
||||||
|
@ -491,6 +496,7 @@ fn run_app(opt: Opt) -> Result<()> {
|
||||||
name,
|
name,
|
||||||
id,
|
id,
|
||||||
description,
|
description,
|
||||||
|
topics: vec![],
|
||||||
address,
|
address,
|
||||||
archive_url,
|
archive_url,
|
||||||
})?;
|
})?;
|
||||||
|
@ -535,17 +541,17 @@ fn run_app(opt: Opt) -> Result<()> {
|
||||||
let tx = db.transaction(TransactionBehavior::Exclusive).unwrap();
|
let tx = db.transaction(TransactionBehavior::Exclusive).unwrap();
|
||||||
let messages = if opt.debug {
|
let messages = if opt.debug {
|
||||||
println!("flush-queue dry_run {:?}", dry_run);
|
println!("flush-queue dry_run {:?}", dry_run);
|
||||||
tx.queue(Queue::Out)?
|
tx.queue(mailpot::queue::Queue::Out)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(DbVal::into_inner)
|
.map(DbVal::into_inner)
|
||||||
.chain(
|
.chain(
|
||||||
tx.queue(Queue::Deferred)?
|
tx.queue(mailpot::queue::Queue::Deferred)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(DbVal::into_inner),
|
.map(DbVal::into_inner),
|
||||||
)
|
)
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
tx.delete_from_queue(Queue::Out, vec![])?
|
tx.delete_from_queue(mailpot::queue::Queue::Out, vec![])?
|
||||||
};
|
};
|
||||||
if opt.verbose > 0 || opt.debug {
|
if opt.verbose > 0 || opt.debug {
|
||||||
println!("Queue out has {} messages.", messages.len());
|
println!("Queue out has {} messages.", messages.len());
|
||||||
|
@ -614,15 +620,15 @@ fn run_app(opt: Opt) -> Result<()> {
|
||||||
for (err, mut msg) in failures {
|
for (err, mut msg) in failures {
|
||||||
log::error!("Message {msg:?} failed with: {err}. Inserting to Deferred queue.");
|
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.insert_to_queue(msg)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.commit()?;
|
tx.commit()?;
|
||||||
}
|
}
|
||||||
ErrorQueue { cmd } => match cmd {
|
ErrorQueue { cmd } => match cmd {
|
||||||
ErrorQueueCommand::List => {
|
QueueCommand::List => {
|
||||||
let errors = db.queue(Queue::Error)?;
|
let errors = db.queue(mailpot::queue::Queue::Error)?;
|
||||||
if errors.is_empty() {
|
if errors.is_empty() {
|
||||||
println!("Error queue is empty.");
|
println!("Error queue is empty.");
|
||||||
} else {
|
} else {
|
||||||
|
@ -634,8 +640,8 @@ fn run_app(opt: Opt) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ErrorQueueCommand::Print { index } => {
|
QueueCommand::Print { index } => {
|
||||||
let mut errors = db.queue(Queue::Error)?;
|
let mut errors = db.queue(mailpot::queue::Queue::Error)?;
|
||||||
if !index.is_empty() {
|
if !index.is_empty() {
|
||||||
errors.retain(|el| index.contains(&el.pk()));
|
errors.retain(|el| index.contains(&el.pk()));
|
||||||
}
|
}
|
||||||
|
@ -647,8 +653,8 @@ fn run_app(opt: Opt) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ErrorQueueCommand::Delete { index, quiet } => {
|
QueueCommand::Delete { index, quiet } => {
|
||||||
let mut errors = db.queue(Queue::Error)?;
|
let mut errors = db.queue(mailpot::queue::Queue::Error)?;
|
||||||
if !index.is_empty() {
|
if !index.is_empty() {
|
||||||
errors.retain(|el| index.contains(&el.pk()));
|
errors.retain(|el| index.contains(&el.pk()));
|
||||||
}
|
}
|
||||||
|
@ -660,7 +666,7 @@ fn run_app(opt: Opt) -> Result<()> {
|
||||||
if !quiet {
|
if !quiet {
|
||||||
println!("Deleting error queue elements {:?}", &index);
|
println!("Deleting error queue elements {:?}", &index);
|
||||||
}
|
}
|
||||||
db.delete_from_queue(Queue::Error, index)?;
|
db.delete_from_queue(mailpot::queue::Queue::Error, index)?;
|
||||||
if !quiet {
|
if !quiet {
|
||||||
for e in errors {
|
for e in errors {
|
||||||
println!("{e:?}");
|
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 {
|
ImportMaildir {
|
||||||
list_id,
|
list_id,
|
||||||
mut maildir_path,
|
mut maildir_path,
|
||||||
|
|
|
@ -132,6 +132,7 @@ For more information, try '--help'."#,
|
||||||
name: "foobar chat".into(),
|
name: "foobar chat".into(),
|
||||||
id: "foo-chat".into(),
|
id: "foo-chat".into(),
|
||||||
address: "foo-chat@example.com".into(),
|
address: "foo-chat@example.com".into(),
|
||||||
|
topics: vec![],
|
||||||
description: None,
|
description: None,
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
})
|
})
|
||||||
|
@ -142,8 +143,8 @@ For more information, try '--help'."#,
|
||||||
list_lists(
|
list_lists(
|
||||||
&conf_path,
|
&conf_path,
|
||||||
"- foo-chat DbVal(MailingList { pk: 1, name: \"foobar chat\", id: \"foo-chat\", address: \
|
"- 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: \
|
\"foo-chat@example.com\", topics: [], description: None, archive_url: None }, 1)\n\tList \
|
||||||
None\n\tList policy: None",
|
owners: None\n\tPost policy: None\n\tSubscription policy: None",
|
||||||
);
|
);
|
||||||
|
|
||||||
fn create_list(conf: &Path) {
|
fn create_list(conf: &Path) {
|
||||||
|
@ -171,9 +172,10 @@ For more information, try '--help'."#,
|
||||||
list_lists(
|
list_lists(
|
||||||
&conf_path,
|
&conf_path,
|
||||||
"- foo-chat DbVal(MailingList { pk: 1, name: \"foobar chat\", id: \"foo-chat\", address: \
|
"- 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: \
|
\"foo-chat@example.com\", topics: [], description: None, archive_url: None }, 1)\n\tList \
|
||||||
None\n\tList policy: None\n\n- twobar-chat DbVal(MailingList { pk: 2, name: \"twobar\", \
|
owners: None\n\tPost policy: None\n\tSubscription policy: None\n\n- twobar-chat \
|
||||||
id: \"twobar-chat\", address: \"twobar-chat@example.com\", description: None, \
|
DbVal(MailingList { pk: 2, name: \"twobar\", id: \"twobar-chat\", address: \
|
||||||
archive_url: None }, 2)\n\tList owners: None\n\tList policy: None",
|
\"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(),
|
id: "foo-chat".into(),
|
||||||
address: "foo-chat@example.com".into(),
|
address: "foo-chat@example.com".into(),
|
||||||
description: None,
|
description: None,
|
||||||
|
topics: vec![],
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -282,6 +283,7 @@ fn test_list_requests_submission() {
|
||||||
id: "foo-chat".into(),
|
id: "foo-chat".into(),
|
||||||
address: "foo-chat@example.com".into(),
|
address: "foo-chat@example.com".into(),
|
||||||
description: None,
|
description: None,
|
||||||
|
topics: vec![],
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -1,4 +1,2 @@
|
||||||
PRAGMA foreign_keys=ON;
|
PRAGMA foreign_keys=ON;
|
||||||
BEGIN;
|
|
||||||
ALTER TABLE templates RENAME TO template;
|
ALTER TABLE templates RENAME TO template;
|
||||||
COMMIT;
|
|
||||||
|
|
|
@ -1,4 +1,2 @@
|
||||||
PRAGMA foreign_keys=ON;
|
PRAGMA foreign_keys=ON;
|
||||||
BEGIN;
|
|
||||||
ALTER TABLE template RENAME TO templates;
|
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_FKEY, true)?;
|
||||||
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_ENABLE_TRIGGER, 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_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_timeout(core::time::Duration::from_millis(500))?;
|
||||||
conn.busy_handler(Some(|times: i32| -> bool { times < 5 }))?;
|
conn.busy_handler(Some(|times: i32| -> bool { times < 5 }))?;
|
||||||
|
|
||||||
|
@ -246,14 +246,14 @@ impl Connection {
|
||||||
if undo { "un " } else { "re" }
|
if undo { "un " } else { "re" }
|
||||||
);
|
);
|
||||||
if undo {
|
if undo {
|
||||||
trace!("{}", Self::MIGRATIONS[from as usize].2);
|
trace!("{}", Self::MIGRATIONS[from as usize - 1].2);
|
||||||
tx.connection
|
tx.connection
|
||||||
.execute(Self::MIGRATIONS[from as usize].2, [])?;
|
.execute_batch(Self::MIGRATIONS[from as usize - 1].2)?;
|
||||||
from -= 1;
|
from -= 1;
|
||||||
} else {
|
} else {
|
||||||
trace!("{}", Self::MIGRATIONS[from as usize].1);
|
trace!("{}", Self::MIGRATIONS[from as usize].1);
|
||||||
tx.connection
|
tx.connection
|
||||||
.execute(Self::MIGRATIONS[from as usize].1, [])?;
|
.execute_batch(Self::MIGRATIONS[from as usize].1)?;
|
||||||
from += 1;
|
from += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -384,6 +384,8 @@ impl Connection {
|
||||||
let mut stmt = self.connection.prepare("SELECT * FROM list;")?;
|
let mut stmt = self.connection.prepare("SELECT * FROM list;")?;
|
||||||
let list_iter = stmt.query_map([], |row| {
|
let list_iter = stmt.query_map([], |row| {
|
||||||
let pk = row.get("pk")?;
|
let pk = row.get("pk")?;
|
||||||
|
let topics: serde_json::Value = row.get("topics")?;
|
||||||
|
let topics = MailingList::topics_from_json_value(topics)?;
|
||||||
Ok(DbVal(
|
Ok(DbVal(
|
||||||
MailingList {
|
MailingList {
|
||||||
pk,
|
pk,
|
||||||
|
@ -391,6 +393,7 @@ impl Connection {
|
||||||
id: row.get("id")?,
|
id: row.get("id")?,
|
||||||
address: row.get("address")?,
|
address: row.get("address")?,
|
||||||
description: row.get("description")?,
|
description: row.get("description")?,
|
||||||
|
topics,
|
||||||
archive_url: row.get("archive_url")?,
|
archive_url: row.get("archive_url")?,
|
||||||
},
|
},
|
||||||
pk,
|
pk,
|
||||||
|
@ -413,6 +416,8 @@ impl Connection {
|
||||||
let ret = stmt
|
let ret = stmt
|
||||||
.query_row([&pk], |row| {
|
.query_row([&pk], |row| {
|
||||||
let pk = row.get("pk")?;
|
let pk = row.get("pk")?;
|
||||||
|
let topics: serde_json::Value = row.get("topics")?;
|
||||||
|
let topics = MailingList::topics_from_json_value(topics)?;
|
||||||
Ok(DbVal(
|
Ok(DbVal(
|
||||||
MailingList {
|
MailingList {
|
||||||
pk,
|
pk,
|
||||||
|
@ -420,6 +425,7 @@ impl Connection {
|
||||||
id: row.get("id")?,
|
id: row.get("id")?,
|
||||||
address: row.get("address")?,
|
address: row.get("address")?,
|
||||||
description: row.get("description")?,
|
description: row.get("description")?,
|
||||||
|
topics,
|
||||||
archive_url: row.get("archive_url")?,
|
archive_url: row.get("archive_url")?,
|
||||||
},
|
},
|
||||||
pk,
|
pk,
|
||||||
|
@ -438,6 +444,8 @@ impl Connection {
|
||||||
let ret = stmt
|
let ret = stmt
|
||||||
.query_row([&id], |row| {
|
.query_row([&id], |row| {
|
||||||
let pk = row.get("pk")?;
|
let pk = row.get("pk")?;
|
||||||
|
let topics: serde_json::Value = row.get("topics")?;
|
||||||
|
let topics = MailingList::topics_from_json_value(topics)?;
|
||||||
Ok(DbVal(
|
Ok(DbVal(
|
||||||
MailingList {
|
MailingList {
|
||||||
pk,
|
pk,
|
||||||
|
@ -445,6 +453,7 @@ impl Connection {
|
||||||
id: row.get("id")?,
|
id: row.get("id")?,
|
||||||
address: row.get("address")?,
|
address: row.get("address")?,
|
||||||
description: row.get("description")?,
|
description: row.get("description")?,
|
||||||
|
topics,
|
||||||
archive_url: row.get("archive_url")?,
|
archive_url: row.get("archive_url")?,
|
||||||
},
|
},
|
||||||
pk,
|
pk,
|
||||||
|
@ -458,8 +467,8 @@ impl Connection {
|
||||||
/// Create a new list.
|
/// Create a new list.
|
||||||
pub fn create_list(&self, new_val: MailingList) -> Result<DbVal<MailingList>> {
|
pub fn create_list(&self, new_val: MailingList) -> Result<DbVal<MailingList>> {
|
||||||
let mut stmt = self.connection.prepare(
|
let mut stmt = self.connection.prepare(
|
||||||
"INSERT INTO list(name, id, address, description, archive_url) VALUES(?, ?, ?, ?, ?) \
|
"INSERT INTO list(name, id, address, description, archive_url, topics) VALUES(?, ?, \
|
||||||
RETURNING *;",
|
?, ?, ?, ?) RETURNING *;",
|
||||||
)?;
|
)?;
|
||||||
let ret = stmt.query_row(
|
let ret = stmt.query_row(
|
||||||
rusqlite::params![
|
rusqlite::params![
|
||||||
|
@ -468,9 +477,12 @@ impl Connection {
|
||||||
&new_val.address,
|
&new_val.address,
|
||||||
new_val.description.as_ref(),
|
new_val.description.as_ref(),
|
||||||
new_val.archive_url.as_ref(),
|
new_val.archive_url.as_ref(),
|
||||||
|
serde_json::json!(new_val.topics.as_slice()),
|
||||||
],
|
],
|
||||||
|row| {
|
|row| {
|
||||||
let pk = row.get("pk")?;
|
let pk = row.get("pk")?;
|
||||||
|
let topics: serde_json::Value = row.get("topics")?;
|
||||||
|
let topics = MailingList::topics_from_json_value(topics)?;
|
||||||
Ok(DbVal(
|
Ok(DbVal(
|
||||||
MailingList {
|
MailingList {
|
||||||
pk,
|
pk,
|
||||||
|
@ -478,6 +490,7 @@ impl Connection {
|
||||||
id: row.get("id")?,
|
id: row.get("id")?,
|
||||||
address: row.get("address")?,
|
address: row.get("address")?,
|
||||||
description: row.get("description")?,
|
description: row.get("description")?,
|
||||||
|
topics,
|
||||||
archive_url: row.get("archive_url")?,
|
archive_url: row.get("archive_url")?,
|
||||||
},
|
},
|
||||||
pk,
|
pk,
|
||||||
|
@ -999,6 +1012,7 @@ mod tests {
|
||||||
name: "".into(),
|
name: "".into(),
|
||||||
id: "".into(),
|
id: "".into(),
|
||||||
description: None,
|
description: None,
|
||||||
|
topics: vec![],
|
||||||
address: "".into(),
|
address: "".into(),
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
# id: "foo-chat".into(),
|
# id: "foo-chat".into(),
|
||||||
# address: "foo-chat@example.com".into(),
|
# address: "foo-chat@example.com".into(),
|
||||||
# description: Some("Hello world, from foo-chat list".into()),
|
# description: Some("Hello world, from foo-chat list".into()),
|
||||||
|
# topics: vec![],
|
||||||
# archive_url: Some("https://lists.example.com".into()),
|
# archive_url: Some("https://lists.example.com".into()),
|
||||||
# })
|
# })
|
||||||
# .unwrap();
|
# .unwrap();
|
||||||
|
|
|
@ -66,3 +66,11 @@ error_chain! {
|
||||||
Template(minijinja::Error) #[doc="Error returned from minijinja template engine."];
|
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(),
|
//! name: "foobar chat".into(),
|
||||||
//! id: "foo-chat".into(),
|
//! id: "foo-chat".into(),
|
||||||
//! address: "foo-chat@example.com".into(),
|
//! address: "foo-chat@example.com".into(),
|
||||||
|
//! topics: vec![],
|
||||||
//! description: None,
|
//! description: None,
|
||||||
//! archive_url: None,
|
//! archive_url: None,
|
||||||
//! })?
|
//! })?
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
|
|
||||||
//(user_version, redo sql, undo sql
|
//(user_version, redo sql, undo sql
|
||||||
&[(1,"PRAGMA foreign_keys=ON;
|
&[(1,"PRAGMA foreign_keys=ON;
|
||||||
BEGIN;
|
|
||||||
ALTER TABLE templates RENAME TO template;
|
ALTER TABLE templates RENAME TO template;
|
||||||
COMMIT;
|
|
||||||
","PRAGMA foreign_keys=ON;
|
","PRAGMA foreign_keys=ON;
|
||||||
BEGIN;
|
|
||||||
ALTER TABLE template RENAME TO templates;
|
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,
|
pub id: String,
|
||||||
/// Mailing list e-mail address.
|
/// Mailing list e-mail address.
|
||||||
pub address: String,
|
pub address: String,
|
||||||
|
/// Discussion topics.
|
||||||
|
pub topics: Vec<String>,
|
||||||
/// Mailing list description.
|
/// Mailing list description.
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
/// Mailing list archive URL.
|
/// Mailing list archive URL.
|
||||||
|
@ -417,6 +419,52 @@ impl MailingList {
|
||||||
contact = self.owner_mailto().address,
|
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.
|
/// A mailing list subscription entry.
|
||||||
|
|
|
@ -84,6 +84,7 @@ mod post_policy {
|
||||||
/// id: "foo-chat".into(),
|
/// id: "foo-chat".into(),
|
||||||
/// address: "foo-chat@example.com".into(),
|
/// address: "foo-chat@example.com".into(),
|
||||||
/// description: None,
|
/// description: None,
|
||||||
|
/// topics: vec![],
|
||||||
/// archive_url: None,
|
/// archive_url: None,
|
||||||
/// })
|
/// })
|
||||||
/// .unwrap();
|
/// .unwrap();
|
||||||
|
@ -278,6 +279,7 @@ mod subscription_policy {
|
||||||
/// id: "foo-chat".into(),
|
/// id: "foo-chat".into(),
|
||||||
/// address: "foo-chat@example.com".into(),
|
/// address: "foo-chat@example.com".into(),
|
||||||
/// description: None,
|
/// description: None,
|
||||||
|
/// topics: vec![],
|
||||||
/// archive_url: None,
|
/// archive_url: None,
|
||||||
/// })
|
/// })
|
||||||
/// .unwrap();
|
/// .unwrap();
|
||||||
|
|
|
@ -430,6 +430,7 @@ fn test_postfix_generation() -> Result<()> {
|
||||||
id: "first".into(),
|
id: "first".into(),
|
||||||
address: "first@example.com".into(),
|
address: "first@example.com".into(),
|
||||||
description: None,
|
description: None,
|
||||||
|
topics: vec![],
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
})?;
|
})?;
|
||||||
assert_eq!(first.pk(), 1);
|
assert_eq!(first.pk(), 1);
|
||||||
|
@ -439,6 +440,7 @@ fn test_postfix_generation() -> Result<()> {
|
||||||
id: "second".into(),
|
id: "second".into(),
|
||||||
address: "second@example.com".into(),
|
address: "second@example.com".into(),
|
||||||
description: None,
|
description: None,
|
||||||
|
topics: vec![],
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
})?;
|
})?;
|
||||||
assert_eq!(second.pk(), 2);
|
assert_eq!(second.pk(), 2);
|
||||||
|
@ -459,6 +461,7 @@ fn test_postfix_generation() -> Result<()> {
|
||||||
id: "third".into(),
|
id: "third".into(),
|
||||||
address: "third@example.com".into(),
|
address: "third@example.com".into(),
|
||||||
description: None,
|
description: None,
|
||||||
|
topics: vec![],
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
})?;
|
})?;
|
||||||
assert_eq!(third.pk(), 3);
|
assert_eq!(third.pk(), 3);
|
||||||
|
|
|
@ -51,6 +51,22 @@ pub enum Queue {
|
||||||
Error,
|
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 {
|
impl Queue {
|
||||||
/// Returns the name of the queue used in the database schema.
|
/// Returns the name of the queue used in the database schema.
|
||||||
pub fn as_str(&self) -> &'static str {
|
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.
|
/// A queue entry.
|
||||||
#[derive(Clone, Deserialize, Serialize, PartialEq, Eq)]
|
#[derive(Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
pub struct QueueEntry {
|
pub struct QueueEntry {
|
||||||
|
|
|
@ -10,6 +10,7 @@ CREATE TABLE IF NOT EXISTS list (
|
||||||
request_local_part TEXT,
|
request_local_part TEXT,
|
||||||
archive_url TEXT,
|
archive_url TEXT,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
|
topics JSON NOT NULL CHECK (json_type(topics) = 'array') DEFAULT '[]',
|
||||||
created INTEGER NOT NULL DEFAULT (unixepoch()),
|
created INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||||
last_modified INTEGER NOT NULL DEFAULT (unixepoch()),
|
last_modified INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||||
verify BOOLEAN CHECK (verify IN (0, 1)) NOT NULL DEFAULT 1,
|
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,
|
request_local_part TEXT,
|
||||||
archive_url TEXT,
|
archive_url TEXT,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
|
topics JSON NOT NULL CHECK (json_type(topics) = 'array') DEFAULT '[]',
|
||||||
created INTEGER NOT NULL DEFAULT (unixepoch()),
|
created INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||||
last_modified INTEGER NOT NULL DEFAULT (unixepoch()),
|
last_modified INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||||
verify BOOLEAN_TYPE(verify) DEFAULT BOOLEAN_TRUE(),
|
verify BOOLEAN_TYPE(verify) DEFAULT BOOLEAN_TRUE(),
|
||||||
|
|
|
@ -623,6 +623,7 @@ mod tests {
|
||||||
name: "foobar chat".into(),
|
name: "foobar chat".into(),
|
||||||
id: "foo-chat".into(),
|
id: "foo-chat".into(),
|
||||||
address: "foo-chat@example.com".into(),
|
address: "foo-chat@example.com".into(),
|
||||||
|
topics: vec![],
|
||||||
description: None,
|
description: None,
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
})
|
})
|
||||||
|
@ -633,6 +634,7 @@ mod tests {
|
||||||
name: "foobar chat2".into(),
|
name: "foobar chat2".into(),
|
||||||
id: "foo-chat2".into(),
|
id: "foo-chat2".into(),
|
||||||
address: "foo-chat2@example.com".into(),
|
address: "foo-chat2@example.com".into(),
|
||||||
|
topics: vec![],
|
||||||
description: None,
|
description: None,
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
})
|
})
|
||||||
|
|
|
@ -46,6 +46,7 @@ fn test_accounts() {
|
||||||
id: "foo-chat".into(),
|
id: "foo-chat".into(),
|
||||||
address: "foo-chat@example.com".into(),
|
address: "foo-chat@example.com".into(),
|
||||||
description: None,
|
description: None,
|
||||||
|
topics: vec![],
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -46,6 +46,7 @@ fn test_authorizer() {
|
||||||
id: "foo-chat".into(),
|
id: "foo-chat".into(),
|
||||||
address: "foo-chat@example.com".into(),
|
address: "foo-chat@example.com".into(),
|
||||||
description: None,
|
description: None,
|
||||||
|
topics: vec![],
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
})
|
})
|
||||||
.unwrap_err(),
|
.unwrap_err(),
|
||||||
|
@ -84,6 +85,7 @@ fn test_authorizer() {
|
||||||
id: "foo-chat".into(),
|
id: "foo-chat".into(),
|
||||||
address: "foo-chat@example.com".into(),
|
address: "foo-chat@example.com".into(),
|
||||||
description: None,
|
description: None,
|
||||||
|
topics: vec![],
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
})
|
})
|
||||||
.map(|_| ()),
|
.map(|_| ()),
|
||||||
|
|
|
@ -61,6 +61,7 @@ fn test_list_creation() {
|
||||||
id: "foo-chat".into(),
|
id: "foo-chat".into(),
|
||||||
address: "foo-chat@example.com".into(),
|
address: "foo-chat@example.com".into(),
|
||||||
description: None,
|
description: None,
|
||||||
|
topics: vec![],
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -55,6 +55,7 @@ fn test_error_queue() {
|
||||||
id: "foo-chat".into(),
|
id: "foo-chat".into(),
|
||||||
address: "foo-chat@example.com".into(),
|
address: "foo-chat@example.com".into(),
|
||||||
description: None,
|
description: None,
|
||||||
|
topics: vec![],
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -48,6 +48,7 @@ fn test_smtp() {
|
||||||
id: "foo-chat".into(),
|
id: "foo-chat".into(),
|
||||||
address: "foo-chat@example.com".into(),
|
address: "foo-chat@example.com".into(),
|
||||||
description: None,
|
description: None,
|
||||||
|
topics: vec![],
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -202,6 +203,7 @@ fn test_smtp_mailcrab() {
|
||||||
id: "foo-chat".into(),
|
id: "foo-chat".into(),
|
||||||
address: "foo-chat@example.com".into(),
|
address: "foo-chat@example.com".into(),
|
||||||
description: None,
|
description: None,
|
||||||
|
topics: vec![],
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -44,6 +44,7 @@ fn test_list_subscription() {
|
||||||
id: "foo-chat".into(),
|
id: "foo-chat".into(),
|
||||||
address: "foo-chat@example.com".into(),
|
address: "foo-chat@example.com".into(),
|
||||||
description: None,
|
description: None,
|
||||||
|
topics: vec![],
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -178,6 +179,7 @@ fn test_post_rejection() {
|
||||||
id: "foo-chat".into(),
|
id: "foo-chat".into(),
|
||||||
address: "foo-chat@example.com".into(),
|
address: "foo-chat@example.com".into(),
|
||||||
description: None,
|
description: None,
|
||||||
|
topics: vec![],
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -67,6 +67,7 @@ MIME-Version: 1.0
|
||||||
id: "foo-chat".into(),
|
id: "foo-chat".into(),
|
||||||
address: "foo-chat@example.com".into(),
|
address: "foo-chat@example.com".into(),
|
||||||
description: None,
|
description: None,
|
||||||
|
topics: vec![],
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
79
docs/mpot.1
79
docs/mpot.1
|
@ -330,13 +330,13 @@ Is subscription enabled.
|
||||||
.ie \n(.g .ds Aq \(aq
|
.ie \n(.g .ds Aq \(aq
|
||||||
.el .ds Aq '
|
.el .ds Aq '
|
||||||
.\fB
|
.\fB
|
||||||
.SS mpot list add-policy
|
.SS mpot list add-post-policy
|
||||||
.\fR
|
.\fR
|
||||||
.br
|
.br
|
||||||
|
|
||||||
.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
|
.br
|
||||||
|
|
||||||
Add a new post policy.
|
Add a new post policy.
|
||||||
|
@ -358,13 +358,13 @@ Allow posts, but handle it manually.
|
||||||
.ie \n(.g .ds Aq \(aq
|
.ie \n(.g .ds Aq \(aq
|
||||||
.el .ds Aq '
|
.el .ds Aq '
|
||||||
.\fB
|
.\fB
|
||||||
.SS mpot list remove-policy
|
.SS mpot list remove-post-policy
|
||||||
.\fR
|
.\fR
|
||||||
.br
|
.br
|
||||||
|
|
||||||
.br
|
.br
|
||||||
|
|
||||||
mpot list remove\-policy \-\-pk \fIPK\fR
|
mpot list remove\-post\-policy \-\-pk \fIPK\fR
|
||||||
.br
|
.br
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
|
@ -373,13 +373,13 @@ Post policy primary key.
|
||||||
.ie \n(.g .ds Aq \(aq
|
.ie \n(.g .ds Aq \(aq
|
||||||
.el .ds Aq '
|
.el .ds Aq '
|
||||||
.\fB
|
.\fB
|
||||||
.SS mpot list add-subscribe-policy
|
.SS mpot list add-subscription-policy
|
||||||
.\fR
|
.\fR
|
||||||
.br
|
.br
|
||||||
|
|
||||||
.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
|
.br
|
||||||
|
|
||||||
Add subscription policy to list.
|
Add subscription policy to list.
|
||||||
|
@ -401,18 +401,18 @@ Allow subscriptions, but handle it manually.
|
||||||
.ie \n(.g .ds Aq \(aq
|
.ie \n(.g .ds Aq \(aq
|
||||||
.el .ds Aq '
|
.el .ds Aq '
|
||||||
.\fB
|
.\fB
|
||||||
.SS mpot list remove-subscribe-policy
|
.SS mpot list remove-subscription-policy
|
||||||
.\fR
|
.\fR
|
||||||
.br
|
.br
|
||||||
|
|
||||||
.br
|
.br
|
||||||
|
|
||||||
mpot list remove\-subscribe\-policy \-\-pk \fIPK\fR
|
mpot list remove\-subscription\-policy \-\-pk \fIPK\fR
|
||||||
.br
|
.br
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
\-\-pk \fIPK\fR
|
\-\-pk \fIPK\fR
|
||||||
Subscribe policy primary key.
|
Subscription policy primary key.
|
||||||
.ie \n(.g .ds Aq \(aq
|
.ie \n(.g .ds Aq \(aq
|
||||||
.el .ds Aq '
|
.el .ds Aq '
|
||||||
.\fB
|
.\fB
|
||||||
|
@ -705,6 +705,67 @@ index of entry.
|
||||||
mpot error\-queue delete [\-\-index \fIINDEX\fR] [\-\-quiet \fIQUIET\fR]
|
mpot error\-queue delete [\-\-index \fIINDEX\fR] [\-\-quiet \fIQUIET\fR]
|
||||||
.br
|
.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.
|
Delete entry and print it in stdout.
|
||||||
.TP
|
.TP
|
||||||
\-\-index \fIINDEX\fR
|
\-\-index \fIINDEX\fR
|
||||||
|
|
|
@ -217,6 +217,10 @@ mod tests {
|
||||||
|
|
||||||
let db_path = tmp_dir.path().join("mpot.db");
|
let db_path = tmp_dir.path().join("mpot.db");
|
||||||
std::fs::copy("../mailpot-tests/for_testing.db", &db_path).unwrap();
|
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 {
|
let config = Configuration {
|
||||||
send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
|
send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()),
|
||||||
db_path,
|
db_path,
|
||||||
|
@ -231,9 +235,11 @@ mod tests {
|
||||||
name: "foobar chat".into(),
|
name: "foobar chat".into(),
|
||||||
id: "foo-chat".into(),
|
id: "foo-chat".into(),
|
||||||
address: "foo-chat@example.com".into(),
|
address: "foo-chat@example.com".into(),
|
||||||
|
topics: vec![],
|
||||||
description: None,
|
description: None,
|
||||||
archive_url: None,
|
archive_url: None,
|
||||||
};
|
};
|
||||||
|
assert_eq!(&db.lists().unwrap().remove(0).into_inner(), &foo_chat);
|
||||||
drop(db);
|
drop(db);
|
||||||
|
|
||||||
let config = Arc::new(config);
|
let config = Arc::new(config);
|
||||||
|
|
|
@ -92,6 +92,7 @@ pub struct MailingList {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub address: String,
|
pub address: String,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
pub topics: Vec<String>,
|
||||||
#[serde(serialize_with = "super::utils::to_safe_string_opt")]
|
#[serde(serialize_with = "super::utils::to_safe_string_opt")]
|
||||||
pub archive_url: Option<String>,
|
pub archive_url: Option<String>,
|
||||||
pub inner: DbVal<mailpot::models::MailingList>,
|
pub inner: DbVal<mailpot::models::MailingList>,
|
||||||
|
@ -106,6 +107,7 @@ impl From<DbVal<mailpot::models::MailingList>> for MailingList {
|
||||||
id,
|
id,
|
||||||
address,
|
address,
|
||||||
description,
|
description,
|
||||||
|
topics,
|
||||||
archive_url,
|
archive_url,
|
||||||
},
|
},
|
||||||
_,
|
_,
|
||||||
|
@ -117,6 +119,7 @@ impl From<DbVal<mailpot::models::MailingList>> for MailingList {
|
||||||
id,
|
id,
|
||||||
address,
|
address,
|
||||||
description,
|
description,
|
||||||
|
topics,
|
||||||
archive_url,
|
archive_url,
|
||||||
inner: val,
|
inner: val,
|
||||||
}
|
}
|
||||||
|
@ -163,13 +166,24 @@ impl minijinja::value::StructObject for MailingList {
|
||||||
"id" => Some(Value::from_serializable(&self.id)),
|
"id" => Some(Value::from_serializable(&self.id)),
|
||||||
"address" => Some(Value::from_serializable(&self.address)),
|
"address" => Some(Value::from_serializable(&self.address)),
|
||||||
"description" => Some(Value::from_serializable(&self.description)),
|
"description" => Some(Value::from_serializable(&self.description)),
|
||||||
|
"topics" => Some(Value::from_serializable(&self.topics)),
|
||||||
"archive_url" => Some(Value::from_serializable(&self.archive_url)),
|
"archive_url" => Some(Value::from_serializable(&self.archive_url)),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn static_fields(&self) -> Option<&'static [&'static str]> {
|
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