cli: add import from mailman3 rest api
parent
6cae75e5ae
commit
f0bf147a0d
|
@ -1799,14 +1799,18 @@ name = "mailpot-cli"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"assert_cmd",
|
"assert_cmd",
|
||||||
|
"base64 0.21.0",
|
||||||
"clap",
|
"clap",
|
||||||
"clap_mangen",
|
"clap_mangen",
|
||||||
"log",
|
"log",
|
||||||
"mailpot",
|
"mailpot",
|
||||||
"mailpot-tests",
|
"mailpot-tests",
|
||||||
"predicates",
|
"predicates",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"stderrlog",
|
"stderrlog",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"ureq",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3222,6 +3226,18 @@ version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ureq"
|
||||||
|
version = "2.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.13.1",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
|
|
|
@ -16,10 +16,14 @@ name = "mpot"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
base64 = { version = "0.21" }
|
||||||
clap = { version = "^4.2", default-features = false, features = ["derive", "cargo", "unicode", "help", "usage", "error-context", "suggestions"] }
|
clap = { version = "^4.2", default-features = false, features = ["derive", "cargo", "unicode", "help", "usage", "error-context", "suggestions"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mailpot = { version = "^0.1", path = "../core" }
|
mailpot = { version = "^0.1", path = "../core" }
|
||||||
|
serde = { version = "^1", features = ["derive", ] }
|
||||||
|
serde_json = "^1"
|
||||||
stderrlog = "^0.5"
|
stderrlog = "^0.5"
|
||||||
|
ureq = { version = "2.6", default-features = false }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = "2"
|
assert_cmd = "2"
|
||||||
|
|
|
@ -27,7 +27,7 @@ use clap::ArgAction;
|
||||||
use clap_mangen::{roff, Man};
|
use clap_mangen::{roff, Man};
|
||||||
use roff::{bold, italic, roman, Inline, Roff};
|
use roff::{bold, italic, roman, Inline, Roff};
|
||||||
|
|
||||||
include!("src/lib.rs");
|
include!("src/args.rs");
|
||||||
|
|
||||||
fn main() -> std::io::Result<()> {
|
fn main() -> std::io::Result<()> {
|
||||||
println!("cargo:rerun-if-changed=./src/lib.rs");
|
println!("cargo:rerun-if-changed=./src/lib.rs");
|
||||||
|
|
|
@ -0,0 +1,496 @@
|
||||||
|
/*
|
||||||
|
* This file is part of mailpot
|
||||||
|
*
|
||||||
|
* Copyright 2020 - Manos Pitsidianakis
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub use clap::{Args, CommandFactory, Parser, Subcommand};
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(
|
||||||
|
name = "mpot",
|
||||||
|
about = "mailing list manager",
|
||||||
|
long_about = "Tool for mailpot mailing list management.",
|
||||||
|
before_long_help = "GNU Affero version 3 or later <https://www.gnu.org/licenses/>",
|
||||||
|
author,
|
||||||
|
version
|
||||||
|
)]
|
||||||
|
pub struct Opt {
|
||||||
|
/// Print logs.
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub debug: bool,
|
||||||
|
/// Configuration file to use.
|
||||||
|
#[arg(short, long, value_parser)]
|
||||||
|
pub config: Option<PathBuf>,
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub cmd: Command,
|
||||||
|
/// Silence all output.
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub quiet: bool,
|
||||||
|
/// Verbose mode (-v, -vv, -vvv, etc).
|
||||||
|
#[arg(short, long, action = clap::ArgAction::Count)]
|
||||||
|
pub verbose: u8,
|
||||||
|
/// Debug log timestamp (sec, ms, ns, none).
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub ts: Option<stderrlog::Timestamp>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum Command {
|
||||||
|
/// Prints a sample config file to STDOUT.
|
||||||
|
///
|
||||||
|
/// You can generate a new configuration file by writing the output to a
|
||||||
|
/// file, e.g: mpot sample-config --with-smtp > config.toml
|
||||||
|
SampleConfig {
|
||||||
|
/// Use an SMTP connection instead of a shell process.
|
||||||
|
#[arg(long)]
|
||||||
|
with_smtp: bool,
|
||||||
|
},
|
||||||
|
/// Dumps database data to STDOUT.
|
||||||
|
DumpDatabase,
|
||||||
|
/// Lists all registered mailing lists.
|
||||||
|
ListLists,
|
||||||
|
/// Mailing list management.
|
||||||
|
List {
|
||||||
|
/// Selects mailing list to operate on.
|
||||||
|
list_id: String,
|
||||||
|
#[command(subcommand)]
|
||||||
|
cmd: ListCommand,
|
||||||
|
},
|
||||||
|
/// Create new list.
|
||||||
|
CreateList {
|
||||||
|
/// List name.
|
||||||
|
#[arg(long)]
|
||||||
|
name: String,
|
||||||
|
/// List ID.
|
||||||
|
#[arg(long)]
|
||||||
|
id: String,
|
||||||
|
/// List e-mail address.
|
||||||
|
#[arg(long)]
|
||||||
|
address: String,
|
||||||
|
/// List description.
|
||||||
|
#[arg(long)]
|
||||||
|
description: Option<String>,
|
||||||
|
/// List archive URL.
|
||||||
|
#[arg(long)]
|
||||||
|
archive_url: Option<String>,
|
||||||
|
},
|
||||||
|
/// Post message from STDIN to list.
|
||||||
|
Post {
|
||||||
|
/// Show e-mail processing result without actually consuming it.
|
||||||
|
#[arg(long)]
|
||||||
|
dry_run: bool,
|
||||||
|
},
|
||||||
|
/// Flush outgoing e-mail queue.
|
||||||
|
FlushQueue {
|
||||||
|
/// Show e-mail processing result without actually consuming it.
|
||||||
|
#[arg(long)]
|
||||||
|
dry_run: bool,
|
||||||
|
},
|
||||||
|
/// Mail that has not been handled properly end up in the error queue.
|
||||||
|
ErrorQueue {
|
||||||
|
#[command(subcommand)]
|
||||||
|
cmd: ErrorQueueCommand,
|
||||||
|
},
|
||||||
|
/// Import a maildir folder into an existing list.
|
||||||
|
ImportMaildir {
|
||||||
|
/// List-ID or primary key value.
|
||||||
|
list_id: String,
|
||||||
|
/// Path to a maildir mailbox.
|
||||||
|
/// Must contain {cur, tmp, new} folders.
|
||||||
|
#[arg(long, value_parser)]
|
||||||
|
maildir_path: PathBuf,
|
||||||
|
},
|
||||||
|
/// Update postfix maps and master.cf (probably needs root permissions).
|
||||||
|
UpdatePostfixConfig {
|
||||||
|
#[arg(short = 'p', long)]
|
||||||
|
/// Override location of master.cf file (default:
|
||||||
|
/// /etc/postfix/master.cf)
|
||||||
|
master_cf: Option<PathBuf>,
|
||||||
|
#[clap(flatten)]
|
||||||
|
config: PostfixConfig,
|
||||||
|
},
|
||||||
|
/// Print postfix maps and master.cf entry to STDOUT.
|
||||||
|
///
|
||||||
|
/// Map output should be added to transport_maps and local_recipient_maps
|
||||||
|
/// parameters in postfix's main.cf. It must be saved in a plain text
|
||||||
|
/// file. To make postfix be able to read them, the postmap application
|
||||||
|
/// must be executed with the path to the map file as its sole argument.
|
||||||
|
///
|
||||||
|
/// postmap /path/to/mylist_maps
|
||||||
|
///
|
||||||
|
/// postmap is usually distributed along with the other postfix binaries.
|
||||||
|
///
|
||||||
|
/// The master.cf entry must be manually appended to the master.cf file. See <https://www.postfix.org/master.5.html>.
|
||||||
|
PrintPostfixConfig {
|
||||||
|
#[clap(flatten)]
|
||||||
|
config: PostfixConfig,
|
||||||
|
},
|
||||||
|
/// All Accounts.
|
||||||
|
Accounts,
|
||||||
|
/// Account info.
|
||||||
|
AccountInfo {
|
||||||
|
/// Account address.
|
||||||
|
address: String,
|
||||||
|
},
|
||||||
|
/// Add account.
|
||||||
|
AddAccount {
|
||||||
|
/// E-mail address.
|
||||||
|
#[arg(long)]
|
||||||
|
address: String,
|
||||||
|
/// SSH public key for authentication.
|
||||||
|
#[arg(long)]
|
||||||
|
password: String,
|
||||||
|
/// Name.
|
||||||
|
#[arg(long)]
|
||||||
|
name: Option<String>,
|
||||||
|
/// Public key.
|
||||||
|
#[arg(long)]
|
||||||
|
public_key: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
/// Is account enabled.
|
||||||
|
enabled: Option<bool>,
|
||||||
|
},
|
||||||
|
/// Remove account.
|
||||||
|
RemoveAccount {
|
||||||
|
#[arg(long)]
|
||||||
|
/// E-mail address.
|
||||||
|
address: String,
|
||||||
|
},
|
||||||
|
/// Update account info.
|
||||||
|
UpdateAccount {
|
||||||
|
/// Address to edit.
|
||||||
|
address: String,
|
||||||
|
/// Public key for authentication.
|
||||||
|
#[arg(long)]
|
||||||
|
password: Option<String>,
|
||||||
|
/// Name.
|
||||||
|
#[arg(long)]
|
||||||
|
name: Option<Option<String>>,
|
||||||
|
/// Public key.
|
||||||
|
#[arg(long)]
|
||||||
|
public_key: Option<Option<String>>,
|
||||||
|
#[arg(long)]
|
||||||
|
/// Is account enabled.
|
||||||
|
enabled: Option<Option<bool>>,
|
||||||
|
},
|
||||||
|
/// Show and fix possible data mistakes or inconsistencies.
|
||||||
|
Repair {
|
||||||
|
/// Fix errors (default: false)
|
||||||
|
#[arg(long, default_value = "false")]
|
||||||
|
fix: bool,
|
||||||
|
/// Select all tests (default: false)
|
||||||
|
#[arg(long, default_value = "false")]
|
||||||
|
all: bool,
|
||||||
|
/// Post `datetime` column must have the Date: header value, in RFC2822
|
||||||
|
/// format.
|
||||||
|
#[arg(long, default_value = "false")]
|
||||||
|
datetime_header_value: bool,
|
||||||
|
/// Remove accounts that have no matching subscriptions.
|
||||||
|
#[arg(long, default_value = "false")]
|
||||||
|
remove_empty_accounts: bool,
|
||||||
|
/// Remove subscription requests that have been accepted.
|
||||||
|
#[arg(long, default_value = "false")]
|
||||||
|
remove_accepted_subscription_requests: bool,
|
||||||
|
/// Warn if a list has no owners.
|
||||||
|
#[arg(long, default_value = "false")]
|
||||||
|
warn_list_no_owner: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Postfix config values.
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct PostfixConfig {
|
||||||
|
/// User that runs mailpot when postfix relays a message.
|
||||||
|
///
|
||||||
|
/// Must not be the `postfix` user.
|
||||||
|
/// Must have permissions to access the database file and the data
|
||||||
|
/// directory.
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub user: String,
|
||||||
|
/// Group that runs mailpot when postfix relays a message.
|
||||||
|
/// Optional.
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub group: Option<String>,
|
||||||
|
/// The path to the mailpot binary postfix will execute.
|
||||||
|
#[arg(long)]
|
||||||
|
pub binary_path: PathBuf,
|
||||||
|
/// Limit the number of mailpot instances that can exist at the same time.
|
||||||
|
///
|
||||||
|
/// Default is 1.
|
||||||
|
#[arg(long, default_value = "1")]
|
||||||
|
pub process_limit: Option<u64>,
|
||||||
|
/// The directory in which the map files are saved.
|
||||||
|
///
|
||||||
|
/// Default is `data_path` from [`Configuration`](mailpot::Configuration).
|
||||||
|
#[arg(long)]
|
||||||
|
pub map_output_path: Option<PathBuf>,
|
||||||
|
/// The name of the postfix service name to use.
|
||||||
|
/// Default is `mailpot`.
|
||||||
|
///
|
||||||
|
/// A postfix service is a daemon managed by the postfix process.
|
||||||
|
/// Each entry in the `master.cf` configuration file defines a single
|
||||||
|
/// service.
|
||||||
|
///
|
||||||
|
/// The `master.cf` file is documented in [`master(5)`](https://www.postfix.org/master.5.html):
|
||||||
|
/// <https://www.postfix.org/master.5.html>.
|
||||||
|
#[arg(long)]
|
||||||
|
pub transport_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum ErrorQueueCommand {
|
||||||
|
/// List.
|
||||||
|
List,
|
||||||
|
/// Print entry in RFC5322 or JSON format.
|
||||||
|
Print {
|
||||||
|
/// index of entry.
|
||||||
|
#[arg(long)]
|
||||||
|
index: Vec<i64>,
|
||||||
|
},
|
||||||
|
/// Delete entry and print it in stdout.
|
||||||
|
Delete {
|
||||||
|
/// index of entry.
|
||||||
|
#[arg(long)]
|
||||||
|
index: Vec<i64>,
|
||||||
|
/// Do not print in stdout.
|
||||||
|
#[arg(long)]
|
||||||
|
quiet: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscription options.
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct SubscriptionOptions {
|
||||||
|
/// Name.
|
||||||
|
#[arg(long)]
|
||||||
|
pub name: Option<String>,
|
||||||
|
/// Send messages as digest.
|
||||||
|
#[arg(long, default_value = "false")]
|
||||||
|
pub digest: Option<bool>,
|
||||||
|
/// Hide message from list when posting.
|
||||||
|
#[arg(long, default_value = "false")]
|
||||||
|
pub hide_address: Option<bool>,
|
||||||
|
/// Hide message from list when posting.
|
||||||
|
#[arg(long, default_value = "false")]
|
||||||
|
/// E-mail address verification status.
|
||||||
|
pub verified: Option<bool>,
|
||||||
|
#[arg(long, default_value = "true")]
|
||||||
|
/// Receive confirmation email when posting.
|
||||||
|
pub receive_confirmation: Option<bool>,
|
||||||
|
#[arg(long, default_value = "true")]
|
||||||
|
/// Receive posts from list even if address exists in To or Cc header.
|
||||||
|
pub receive_duplicates: Option<bool>,
|
||||||
|
#[arg(long, default_value = "false")]
|
||||||
|
/// Receive own posts from list.
|
||||||
|
pub receive_own_posts: Option<bool>,
|
||||||
|
#[arg(long, default_value = "true")]
|
||||||
|
/// Is subscription enabled.
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Account options.
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
pub struct AccountOptions {
|
||||||
|
/// Name.
|
||||||
|
#[arg(long)]
|
||||||
|
pub name: Option<String>,
|
||||||
|
/// Public key.
|
||||||
|
#[arg(long)]
|
||||||
|
pub public_key: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
/// Is account enabled.
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum ListCommand {
|
||||||
|
/// List subscriptions of list.
|
||||||
|
Subscriptions,
|
||||||
|
/// Add subscription to list.
|
||||||
|
AddSubscription {
|
||||||
|
/// E-mail address.
|
||||||
|
#[arg(long)]
|
||||||
|
address: String,
|
||||||
|
#[clap(flatten)]
|
||||||
|
subscription_options: SubscriptionOptions,
|
||||||
|
},
|
||||||
|
/// Remove subscription from list.
|
||||||
|
RemoveSubscription {
|
||||||
|
#[arg(long)]
|
||||||
|
/// E-mail address.
|
||||||
|
address: String,
|
||||||
|
},
|
||||||
|
/// Update subscription info.
|
||||||
|
UpdateSubscription {
|
||||||
|
/// Address to edit.
|
||||||
|
address: String,
|
||||||
|
#[clap(flatten)]
|
||||||
|
subscription_options: SubscriptionOptions,
|
||||||
|
},
|
||||||
|
/// Add a new post policy.
|
||||||
|
AddPolicy {
|
||||||
|
#[arg(long)]
|
||||||
|
/// Only list owners can post.
|
||||||
|
announce_only: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
/// Only subscriptions can post.
|
||||||
|
subscription_only: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
/// Subscriptions can post.
|
||||||
|
/// Other posts must be approved by list owners.
|
||||||
|
approval_needed: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
/// Anyone can post without restrictions.
|
||||||
|
open: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
/// Allow posts, but handle it manually.
|
||||||
|
custom: bool,
|
||||||
|
},
|
||||||
|
// Remove post policy.
|
||||||
|
RemovePolicy {
|
||||||
|
#[arg(long)]
|
||||||
|
/// Post policy primary key.
|
||||||
|
pk: i64,
|
||||||
|
},
|
||||||
|
/// Add subscription policy to list.
|
||||||
|
AddSubscribePolicy {
|
||||||
|
#[arg(long)]
|
||||||
|
/// Send confirmation e-mail when subscription is finalized.
|
||||||
|
send_confirmation: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
/// Anyone can subscribe without restrictions.
|
||||||
|
open: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
/// Only list owners can manually add subscriptions.
|
||||||
|
manual: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
/// Anyone can request to subscribe.
|
||||||
|
request: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
/// Allow subscriptions, but handle it manually.
|
||||||
|
custom: bool,
|
||||||
|
},
|
||||||
|
RemoveSubscribePolicy {
|
||||||
|
#[arg(long)]
|
||||||
|
/// Subscribe policy primary key.
|
||||||
|
pk: i64,
|
||||||
|
},
|
||||||
|
/// Add list owner to list.
|
||||||
|
AddListOwner {
|
||||||
|
#[arg(long)]
|
||||||
|
address: String,
|
||||||
|
#[arg(long)]
|
||||||
|
name: Option<String>,
|
||||||
|
},
|
||||||
|
RemoveListOwner {
|
||||||
|
#[arg(long)]
|
||||||
|
/// List owner primary key.
|
||||||
|
pk: i64,
|
||||||
|
},
|
||||||
|
/// Alias for update-subscription --enabled true.
|
||||||
|
EnableSubscription {
|
||||||
|
/// Subscription address.
|
||||||
|
address: String,
|
||||||
|
},
|
||||||
|
/// Alias for update-subscription --enabled false.
|
||||||
|
DisableSubscription {
|
||||||
|
/// Subscription address.
|
||||||
|
address: String,
|
||||||
|
},
|
||||||
|
/// Update mailing list details.
|
||||||
|
Update {
|
||||||
|
/// New list name.
|
||||||
|
#[arg(long)]
|
||||||
|
name: Option<String>,
|
||||||
|
/// New List-ID.
|
||||||
|
#[arg(long)]
|
||||||
|
id: Option<String>,
|
||||||
|
/// New list address.
|
||||||
|
#[arg(long)]
|
||||||
|
address: Option<String>,
|
||||||
|
/// New list description.
|
||||||
|
#[arg(long)]
|
||||||
|
description: Option<String>,
|
||||||
|
/// New list archive URL.
|
||||||
|
#[arg(long)]
|
||||||
|
archive_url: Option<String>,
|
||||||
|
/// New owner address local part.
|
||||||
|
/// If empty, it defaults to '+owner'.
|
||||||
|
#[arg(long)]
|
||||||
|
owner_local_part: Option<String>,
|
||||||
|
/// New request address local part.
|
||||||
|
/// If empty, it defaults to '+request'.
|
||||||
|
#[arg(long)]
|
||||||
|
request_local_part: Option<String>,
|
||||||
|
/// Require verification of e-mails for new subscriptions.
|
||||||
|
///
|
||||||
|
/// Subscriptions that are initiated from the subscription's address are
|
||||||
|
/// verified automatically.
|
||||||
|
#[arg(long)]
|
||||||
|
verify: Option<bool>,
|
||||||
|
/// Public visibility of list.
|
||||||
|
///
|
||||||
|
/// If hidden, the list will not show up in public APIs unless
|
||||||
|
/// requests to it won't work.
|
||||||
|
#[arg(long)]
|
||||||
|
hidden: Option<bool>,
|
||||||
|
/// Enable or disable the list's functionality.
|
||||||
|
///
|
||||||
|
/// If not enabled, the list will continue to show up in the database
|
||||||
|
/// but e-mails and requests to it won't work.
|
||||||
|
#[arg(long)]
|
||||||
|
enabled: Option<bool>,
|
||||||
|
},
|
||||||
|
/// Show mailing list health status.
|
||||||
|
Health,
|
||||||
|
/// Show mailing list info.
|
||||||
|
Info,
|
||||||
|
/// Import members in a local list from a remote mailman3 REST API instance.
|
||||||
|
///
|
||||||
|
/// To find the id of the remote list, you can check URL/lists.
|
||||||
|
/// Example with curl:
|
||||||
|
///
|
||||||
|
/// curl --anyauth -u admin:pass "http://localhost:9001/3.0/lists"
|
||||||
|
///
|
||||||
|
/// If you're trying to import an entire list, create it first and then
|
||||||
|
/// import its users with this command.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// mpot -c conf.toml list list-general import-members --url "http://localhost:9001/3.0/" --username admin --password password --list-id list-general.example.com --skip-owners --dry-run
|
||||||
|
ImportMembers {
|
||||||
|
#[arg(long)]
|
||||||
|
/// REST HTTP endpoint e.g. http://localhost:9001/3.0/
|
||||||
|
url: String,
|
||||||
|
#[arg(long)]
|
||||||
|
/// REST HTTP Basic Authentication username.
|
||||||
|
username: String,
|
||||||
|
#[arg(long)]
|
||||||
|
/// REST HTTP Basic Authentication password.
|
||||||
|
password: String,
|
||||||
|
#[arg(long)]
|
||||||
|
/// List ID of remote list to query.
|
||||||
|
list_id: String,
|
||||||
|
/// Show what would be inserted without performing any changes.
|
||||||
|
#[arg(long)]
|
||||||
|
dry_run: bool,
|
||||||
|
/// Don't import list owners.
|
||||||
|
#[arg(long)]
|
||||||
|
skip_owners: bool,
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,149 @@
|
||||||
|
/*
|
||||||
|
* This file is part of mailpot
|
||||||
|
*
|
||||||
|
* Copyright 2023 - Manos Pitsidianakis
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::{borrow::Cow, time::Duration};
|
||||||
|
|
||||||
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
|
use mailpot::models::{ListOwner, ListSubscription};
|
||||||
|
use ureq::Agent;
|
||||||
|
|
||||||
|
pub struct Mailman3Connection {
|
||||||
|
agent: Agent,
|
||||||
|
url: Cow<'static, str>,
|
||||||
|
auth: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mailman3Connection {
|
||||||
|
pub fn new(
|
||||||
|
url: &str,
|
||||||
|
username: &str,
|
||||||
|
password: &str,
|
||||||
|
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
|
let agent: Agent = ureq::AgentBuilder::new()
|
||||||
|
.timeout_read(Duration::from_secs(5))
|
||||||
|
.timeout_write(Duration::from_secs(5))
|
||||||
|
.build();
|
||||||
|
let mut buf = String::new();
|
||||||
|
general_purpose::STANDARD
|
||||||
|
.encode_string(format!("{username}:{password}").as_bytes(), &mut buf);
|
||||||
|
|
||||||
|
let auth: String = format!("Basic {buf}");
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
agent,
|
||||||
|
url: url.trim_end_matches('/').to_string().into(),
|
||||||
|
auth,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn users(&self, list_address: &str) -> Result<Vec<Entry>, Box<dyn std::error::Error>> {
|
||||||
|
let response: String = self
|
||||||
|
.agent
|
||||||
|
.get(&format!(
|
||||||
|
"{}/lists/{list_address}/roster/member?fields=email&fields=display_name",
|
||||||
|
self.url
|
||||||
|
))
|
||||||
|
.set("Authorization", &self.auth)
|
||||||
|
.call()?
|
||||||
|
.into_string()?;
|
||||||
|
Ok(serde_json::from_str::<Roster>(&response)?.entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn owners(&self, list_address: &str) -> Result<Vec<Entry>, Box<dyn std::error::Error>> {
|
||||||
|
let response: String = self
|
||||||
|
.agent
|
||||||
|
.get(&format!(
|
||||||
|
"{}/lists/{list_address}/roster/owner?fields=email&fields=display_name",
|
||||||
|
self.url
|
||||||
|
))
|
||||||
|
.set("Authorization", &self.auth)
|
||||||
|
.call()?
|
||||||
|
.into_string()?;
|
||||||
|
Ok(serde_json::from_str::<Roster>(&response)?.entries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug)]
|
||||||
|
pub struct Roster {
|
||||||
|
pub entries: Vec<Entry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize, Debug)]
|
||||||
|
pub struct Entry {
|
||||||
|
display_name: String,
|
||||||
|
email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entry {
|
||||||
|
pub fn display_name(&self) -> Option<&str> {
|
||||||
|
if !self.display_name.trim().is_empty() && &self.display_name != "None" {
|
||||||
|
Some(&self.display_name)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn email(&self) -> &str {
|
||||||
|
&self.email
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_subscription(self, list: i64) -> ListSubscription {
|
||||||
|
let Self {
|
||||||
|
display_name,
|
||||||
|
email,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
ListSubscription {
|
||||||
|
pk: -1,
|
||||||
|
list,
|
||||||
|
address: email,
|
||||||
|
name: if !display_name.trim().is_empty() && &display_name != "None" {
|
||||||
|
Some(display_name)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
account: None,
|
||||||
|
enabled: true,
|
||||||
|
verified: true,
|
||||||
|
digest: false,
|
||||||
|
hide_address: false,
|
||||||
|
receive_duplicates: false,
|
||||||
|
receive_own_posts: false,
|
||||||
|
receive_confirmation: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_owner(self, list: i64) -> ListOwner {
|
||||||
|
let Self {
|
||||||
|
display_name,
|
||||||
|
email,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
ListOwner {
|
||||||
|
pk: -1,
|
||||||
|
list,
|
||||||
|
address: email,
|
||||||
|
name: if !display_name.trim().is_empty() && &display_name != "None" {
|
||||||
|
Some(display_name)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
447
cli/src/lib.rs
447
cli/src/lib.rs
|
@ -17,448 +17,11 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
extern crate base64;
|
||||||
|
extern crate ureq;
|
||||||
pub use std::path::PathBuf;
|
pub use std::path::PathBuf;
|
||||||
|
|
||||||
|
mod args;
|
||||||
|
pub mod import;
|
||||||
|
pub use args::*;
|
||||||
pub use clap::{Args, CommandFactory, Parser, Subcommand};
|
pub use clap::{Args, CommandFactory, Parser, Subcommand};
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
|
||||||
#[command(
|
|
||||||
name = "mpot",
|
|
||||||
about = "mailing list manager",
|
|
||||||
long_about = "Tool for mailpot mailing list management.",
|
|
||||||
before_long_help = "GNU Affero version 3 or later <https://www.gnu.org/licenses/>",
|
|
||||||
author,
|
|
||||||
version
|
|
||||||
)]
|
|
||||||
pub struct Opt {
|
|
||||||
/// Print logs.
|
|
||||||
#[arg(short, long)]
|
|
||||||
pub debug: bool,
|
|
||||||
/// Configuration file to use.
|
|
||||||
#[arg(short, long, value_parser)]
|
|
||||||
pub config: Option<PathBuf>,
|
|
||||||
#[command(subcommand)]
|
|
||||||
pub cmd: Command,
|
|
||||||
/// Silence all output.
|
|
||||||
#[arg(short, long)]
|
|
||||||
pub quiet: bool,
|
|
||||||
/// Verbose mode (-v, -vv, -vvv, etc).
|
|
||||||
#[arg(short, long, action = clap::ArgAction::Count)]
|
|
||||||
pub verbose: u8,
|
|
||||||
/// Debug log timestamp (sec, ms, ns, none).
|
|
||||||
#[arg(short, long)]
|
|
||||||
pub ts: Option<stderrlog::Timestamp>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
|
||||||
pub enum Command {
|
|
||||||
/// Prints a sample config file to STDOUT.
|
|
||||||
///
|
|
||||||
/// You can generate a new configuration file by writing the output to a
|
|
||||||
/// file, e.g: mpot sample-config --with-smtp > config.toml
|
|
||||||
SampleConfig {
|
|
||||||
/// Use an SMTP connection instead of a shell process.
|
|
||||||
#[arg(long)]
|
|
||||||
with_smtp: bool,
|
|
||||||
},
|
|
||||||
/// Dumps database data to STDOUT.
|
|
||||||
DumpDatabase,
|
|
||||||
/// Lists all registered mailing lists.
|
|
||||||
ListLists,
|
|
||||||
/// Mailing list management.
|
|
||||||
List {
|
|
||||||
/// Selects mailing list to operate on.
|
|
||||||
list_id: String,
|
|
||||||
#[command(subcommand)]
|
|
||||||
cmd: ListCommand,
|
|
||||||
},
|
|
||||||
/// Create new list.
|
|
||||||
CreateList {
|
|
||||||
/// List name.
|
|
||||||
#[arg(long)]
|
|
||||||
name: String,
|
|
||||||
/// List ID.
|
|
||||||
#[arg(long)]
|
|
||||||
id: String,
|
|
||||||
/// List e-mail address.
|
|
||||||
#[arg(long)]
|
|
||||||
address: String,
|
|
||||||
/// List description.
|
|
||||||
#[arg(long)]
|
|
||||||
description: Option<String>,
|
|
||||||
/// List archive URL.
|
|
||||||
#[arg(long)]
|
|
||||||
archive_url: Option<String>,
|
|
||||||
},
|
|
||||||
/// Post message from STDIN to list.
|
|
||||||
Post {
|
|
||||||
/// Show e-mail processing result without actually consuming it.
|
|
||||||
#[arg(long)]
|
|
||||||
dry_run: bool,
|
|
||||||
},
|
|
||||||
/// Flush outgoing e-mail queue.
|
|
||||||
FlushQueue {
|
|
||||||
/// Show e-mail processing result without actually consuming it.
|
|
||||||
#[arg(long)]
|
|
||||||
dry_run: bool,
|
|
||||||
},
|
|
||||||
/// Mail that has not been handled properly end up in the error queue.
|
|
||||||
ErrorQueue {
|
|
||||||
#[command(subcommand)]
|
|
||||||
cmd: ErrorQueueCommand,
|
|
||||||
},
|
|
||||||
/// Import a maildir folder into an existing list.
|
|
||||||
ImportMaildir {
|
|
||||||
/// List-ID or primary key value.
|
|
||||||
list_id: String,
|
|
||||||
/// Path to a maildir mailbox.
|
|
||||||
/// Must contain {cur, tmp, new} folders.
|
|
||||||
#[arg(long, value_parser)]
|
|
||||||
maildir_path: PathBuf,
|
|
||||||
},
|
|
||||||
/// Update postfix maps and master.cf (probably needs root permissions).
|
|
||||||
UpdatePostfixConfig {
|
|
||||||
#[arg(short = 'p', long)]
|
|
||||||
/// Override location of master.cf file (default:
|
|
||||||
/// /etc/postfix/master.cf)
|
|
||||||
master_cf: Option<PathBuf>,
|
|
||||||
#[clap(flatten)]
|
|
||||||
config: PostfixConfig,
|
|
||||||
},
|
|
||||||
/// Print postfix maps and master.cf entry to STDOUT.
|
|
||||||
///
|
|
||||||
/// Map output should be added to transport_maps and local_recipient_maps
|
|
||||||
/// parameters in postfix's main.cf. It must be saved in a plain text
|
|
||||||
/// file. To make postfix be able to read them, the postmap application
|
|
||||||
/// must be executed with the path to the map file as its sole argument.
|
|
||||||
///
|
|
||||||
/// postmap /path/to/mylist_maps
|
|
||||||
///
|
|
||||||
/// postmap is usually distributed along with the other postfix binaries.
|
|
||||||
///
|
|
||||||
/// The master.cf entry must be manually appended to the master.cf file. See <https://www.postfix.org/master.5.html>.
|
|
||||||
PrintPostfixConfig {
|
|
||||||
#[clap(flatten)]
|
|
||||||
config: PostfixConfig,
|
|
||||||
},
|
|
||||||
/// All Accounts.
|
|
||||||
Accounts,
|
|
||||||
/// Account info.
|
|
||||||
AccountInfo {
|
|
||||||
/// Account address.
|
|
||||||
address: String,
|
|
||||||
},
|
|
||||||
/// Add account.
|
|
||||||
AddAccount {
|
|
||||||
/// E-mail address.
|
|
||||||
#[arg(long)]
|
|
||||||
address: String,
|
|
||||||
/// SSH public key for authentication.
|
|
||||||
#[arg(long)]
|
|
||||||
password: String,
|
|
||||||
/// Name.
|
|
||||||
#[arg(long)]
|
|
||||||
name: Option<String>,
|
|
||||||
/// Public key.
|
|
||||||
#[arg(long)]
|
|
||||||
public_key: Option<String>,
|
|
||||||
#[arg(long)]
|
|
||||||
/// Is account enabled.
|
|
||||||
enabled: Option<bool>,
|
|
||||||
},
|
|
||||||
/// Remove account.
|
|
||||||
RemoveAccount {
|
|
||||||
#[arg(long)]
|
|
||||||
/// E-mail address.
|
|
||||||
address: String,
|
|
||||||
},
|
|
||||||
/// Update account info.
|
|
||||||
UpdateAccount {
|
|
||||||
/// Address to edit.
|
|
||||||
address: String,
|
|
||||||
/// Public key for authentication.
|
|
||||||
#[arg(long)]
|
|
||||||
password: Option<String>,
|
|
||||||
/// Name.
|
|
||||||
#[arg(long)]
|
|
||||||
name: Option<Option<String>>,
|
|
||||||
/// Public key.
|
|
||||||
#[arg(long)]
|
|
||||||
public_key: Option<Option<String>>,
|
|
||||||
#[arg(long)]
|
|
||||||
/// Is account enabled.
|
|
||||||
enabled: Option<Option<bool>>,
|
|
||||||
},
|
|
||||||
/// Show and fix possible data mistakes or inconsistencies.
|
|
||||||
Repair {
|
|
||||||
/// Fix errors (default: false)
|
|
||||||
#[arg(long, default_value = "false")]
|
|
||||||
fix: bool,
|
|
||||||
/// Select all tests (default: false)
|
|
||||||
#[arg(long, default_value = "false")]
|
|
||||||
all: bool,
|
|
||||||
/// Post `datetime` column must have the Date: header value, in RFC2822
|
|
||||||
/// format.
|
|
||||||
#[arg(long, default_value = "false")]
|
|
||||||
datetime_header_value: bool,
|
|
||||||
/// Remove accounts that have no matching subscriptions.
|
|
||||||
#[arg(long, default_value = "false")]
|
|
||||||
remove_empty_accounts: bool,
|
|
||||||
/// Remove subscription requests that have been accepted.
|
|
||||||
#[arg(long, default_value = "false")]
|
|
||||||
remove_accepted_subscription_requests: bool,
|
|
||||||
/// Warn if a list has no owners.
|
|
||||||
#[arg(long, default_value = "false")]
|
|
||||||
warn_list_no_owner: bool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Postfix config values.
|
|
||||||
#[derive(Debug, Args)]
|
|
||||||
pub struct PostfixConfig {
|
|
||||||
/// User that runs mailpot when postfix relays a message.
|
|
||||||
///
|
|
||||||
/// Must not be the `postfix` user.
|
|
||||||
/// Must have permissions to access the database file and the data
|
|
||||||
/// directory.
|
|
||||||
#[arg(short, long)]
|
|
||||||
pub user: String,
|
|
||||||
/// Group that runs mailpot when postfix relays a message.
|
|
||||||
/// Optional.
|
|
||||||
#[arg(short, long)]
|
|
||||||
pub group: Option<String>,
|
|
||||||
/// The path to the mailpot binary postfix will execute.
|
|
||||||
#[arg(long)]
|
|
||||||
pub binary_path: PathBuf,
|
|
||||||
/// Limit the number of mailpot instances that can exist at the same time.
|
|
||||||
///
|
|
||||||
/// Default is 1.
|
|
||||||
#[arg(long, default_value = "1")]
|
|
||||||
pub process_limit: Option<u64>,
|
|
||||||
/// The directory in which the map files are saved.
|
|
||||||
///
|
|
||||||
/// Default is `data_path` from [`Configuration`](mailpot::Configuration).
|
|
||||||
#[arg(long)]
|
|
||||||
pub map_output_path: Option<PathBuf>,
|
|
||||||
/// The name of the postfix service name to use.
|
|
||||||
/// Default is `mailpot`.
|
|
||||||
///
|
|
||||||
/// A postfix service is a daemon managed by the postfix process.
|
|
||||||
/// Each entry in the `master.cf` configuration file defines a single
|
|
||||||
/// service.
|
|
||||||
///
|
|
||||||
/// The `master.cf` file is documented in [`master(5)`](https://www.postfix.org/master.5.html):
|
|
||||||
/// <https://www.postfix.org/master.5.html>.
|
|
||||||
#[arg(long)]
|
|
||||||
pub transport_name: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
|
||||||
pub enum ErrorQueueCommand {
|
|
||||||
/// List.
|
|
||||||
List,
|
|
||||||
/// Print entry in RFC5322 or JSON format.
|
|
||||||
Print {
|
|
||||||
/// index of entry.
|
|
||||||
#[arg(long)]
|
|
||||||
index: Vec<i64>,
|
|
||||||
},
|
|
||||||
/// Delete entry and print it in stdout.
|
|
||||||
Delete {
|
|
||||||
/// index of entry.
|
|
||||||
#[arg(long)]
|
|
||||||
index: Vec<i64>,
|
|
||||||
/// Do not print in stdout.
|
|
||||||
#[arg(long)]
|
|
||||||
quiet: bool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Subscription options.
|
|
||||||
#[derive(Debug, Args)]
|
|
||||||
pub struct SubscriptionOptions {
|
|
||||||
/// Name.
|
|
||||||
#[arg(long)]
|
|
||||||
pub name: Option<String>,
|
|
||||||
/// Send messages as digest.
|
|
||||||
#[arg(long, default_value = "false")]
|
|
||||||
pub digest: Option<bool>,
|
|
||||||
/// Hide message from list when posting.
|
|
||||||
#[arg(long, default_value = "false")]
|
|
||||||
pub hide_address: Option<bool>,
|
|
||||||
/// Hide message from list when posting.
|
|
||||||
#[arg(long, default_value = "false")]
|
|
||||||
/// E-mail address verification status.
|
|
||||||
pub verified: Option<bool>,
|
|
||||||
#[arg(long, default_value = "true")]
|
|
||||||
/// Receive confirmation email when posting.
|
|
||||||
pub receive_confirmation: Option<bool>,
|
|
||||||
#[arg(long, default_value = "true")]
|
|
||||||
/// Receive posts from list even if address exists in To or Cc header.
|
|
||||||
pub receive_duplicates: Option<bool>,
|
|
||||||
#[arg(long, default_value = "false")]
|
|
||||||
/// Receive own posts from list.
|
|
||||||
pub receive_own_posts: Option<bool>,
|
|
||||||
#[arg(long, default_value = "true")]
|
|
||||||
/// Is subscription enabled.
|
|
||||||
pub enabled: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Account options.
|
|
||||||
#[derive(Debug, Args)]
|
|
||||||
pub struct AccountOptions {
|
|
||||||
/// Name.
|
|
||||||
#[arg(long)]
|
|
||||||
pub name: Option<String>,
|
|
||||||
/// Public key.
|
|
||||||
#[arg(long)]
|
|
||||||
pub public_key: Option<String>,
|
|
||||||
#[arg(long)]
|
|
||||||
/// Is account enabled.
|
|
||||||
pub enabled: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
|
||||||
pub enum ListCommand {
|
|
||||||
/// List subscriptions of list.
|
|
||||||
Subscriptions,
|
|
||||||
/// Add subscription to list.
|
|
||||||
AddSubscription {
|
|
||||||
/// E-mail address.
|
|
||||||
#[arg(long)]
|
|
||||||
address: String,
|
|
||||||
#[clap(flatten)]
|
|
||||||
subscription_options: SubscriptionOptions,
|
|
||||||
},
|
|
||||||
/// Remove subscription from list.
|
|
||||||
RemoveSubscription {
|
|
||||||
#[arg(long)]
|
|
||||||
/// E-mail address.
|
|
||||||
address: String,
|
|
||||||
},
|
|
||||||
/// Update subscription info.
|
|
||||||
UpdateSubscription {
|
|
||||||
/// Address to edit.
|
|
||||||
address: String,
|
|
||||||
#[clap(flatten)]
|
|
||||||
subscription_options: SubscriptionOptions,
|
|
||||||
},
|
|
||||||
/// Add a new post policy.
|
|
||||||
AddPolicy {
|
|
||||||
#[arg(long)]
|
|
||||||
/// Only list owners can post.
|
|
||||||
announce_only: bool,
|
|
||||||
#[arg(long)]
|
|
||||||
/// Only subscriptions can post.
|
|
||||||
subscription_only: bool,
|
|
||||||
#[arg(long)]
|
|
||||||
/// Subscriptions can post.
|
|
||||||
/// Other posts must be approved by list owners.
|
|
||||||
approval_needed: bool,
|
|
||||||
#[arg(long)]
|
|
||||||
/// Anyone can post without restrictions.
|
|
||||||
open: bool,
|
|
||||||
#[arg(long)]
|
|
||||||
/// Allow posts, but handle it manually.
|
|
||||||
custom: bool,
|
|
||||||
},
|
|
||||||
// Remove post policy.
|
|
||||||
RemovePolicy {
|
|
||||||
#[arg(long)]
|
|
||||||
/// Post policy primary key.
|
|
||||||
pk: i64,
|
|
||||||
},
|
|
||||||
/// Add subscription policy to list.
|
|
||||||
AddSubscribePolicy {
|
|
||||||
#[arg(long)]
|
|
||||||
/// Send confirmation e-mail when subscription is finalized.
|
|
||||||
send_confirmation: bool,
|
|
||||||
#[arg(long)]
|
|
||||||
/// Anyone can subscribe without restrictions.
|
|
||||||
open: bool,
|
|
||||||
#[arg(long)]
|
|
||||||
/// Only list owners can manually add subscriptions.
|
|
||||||
manual: bool,
|
|
||||||
#[arg(long)]
|
|
||||||
/// Anyone can request to subscribe.
|
|
||||||
request: bool,
|
|
||||||
#[arg(long)]
|
|
||||||
/// Allow subscriptions, but handle it manually.
|
|
||||||
custom: bool,
|
|
||||||
},
|
|
||||||
RemoveSubscribePolicy {
|
|
||||||
#[arg(long)]
|
|
||||||
/// Subscribe policy primary key.
|
|
||||||
pk: i64,
|
|
||||||
},
|
|
||||||
/// Add list owner to list.
|
|
||||||
AddListOwner {
|
|
||||||
#[arg(long)]
|
|
||||||
address: String,
|
|
||||||
#[arg(long)]
|
|
||||||
name: Option<String>,
|
|
||||||
},
|
|
||||||
RemoveListOwner {
|
|
||||||
#[arg(long)]
|
|
||||||
/// List owner primary key.
|
|
||||||
pk: i64,
|
|
||||||
},
|
|
||||||
/// Alias for update-subscription --enabled true.
|
|
||||||
EnableSubscription {
|
|
||||||
/// Subscription address.
|
|
||||||
address: String,
|
|
||||||
},
|
|
||||||
/// Alias for update-subscription --enabled false.
|
|
||||||
DisableSubscription {
|
|
||||||
/// Subscription address.
|
|
||||||
address: String,
|
|
||||||
},
|
|
||||||
/// Update mailing list details.
|
|
||||||
Update {
|
|
||||||
/// New list name.
|
|
||||||
#[arg(long)]
|
|
||||||
name: Option<String>,
|
|
||||||
/// New List-ID.
|
|
||||||
#[arg(long)]
|
|
||||||
id: Option<String>,
|
|
||||||
/// New list address.
|
|
||||||
#[arg(long)]
|
|
||||||
address: Option<String>,
|
|
||||||
/// New list description.
|
|
||||||
#[arg(long)]
|
|
||||||
description: Option<String>,
|
|
||||||
/// New list archive URL.
|
|
||||||
#[arg(long)]
|
|
||||||
archive_url: Option<String>,
|
|
||||||
/// New owner address local part.
|
|
||||||
/// If empty, it defaults to '+owner'.
|
|
||||||
#[arg(long)]
|
|
||||||
owner_local_part: Option<String>,
|
|
||||||
/// New request address local part.
|
|
||||||
/// If empty, it defaults to '+request'.
|
|
||||||
#[arg(long)]
|
|
||||||
request_local_part: Option<String>,
|
|
||||||
/// Require verification of e-mails for new subscriptions.
|
|
||||||
///
|
|
||||||
/// Subscriptions that are initiated from the subscription's address are
|
|
||||||
/// verified automatically.
|
|
||||||
#[arg(long)]
|
|
||||||
verify: Option<bool>,
|
|
||||||
/// Public visibility of list.
|
|
||||||
///
|
|
||||||
/// If hidden, the list will not show up in public APIs unless
|
|
||||||
/// requests to it won't work.
|
|
||||||
#[arg(long)]
|
|
||||||
hidden: Option<bool>,
|
|
||||||
/// Enable or disable the list's functionality.
|
|
||||||
///
|
|
||||||
/// If not enabled, the list will continue to show up in the database
|
|
||||||
/// but e-mails and requests to it won't work.
|
|
||||||
#[arg(long)]
|
|
||||||
enabled: Option<bool>,
|
|
||||||
},
|
|
||||||
/// Show mailing list health status.
|
|
||||||
Health,
|
|
||||||
/// Show mailing list info.
|
|
||||||
Info,
|
|
||||||
}
|
|
||||||
|
|
|
@ -421,6 +421,61 @@ fn run_app(opt: Opt) -> Result<()> {
|
||||||
};
|
};
|
||||||
db.update_list(changeset)?;
|
db.update_list(changeset)?;
|
||||||
}
|
}
|
||||||
|
ImportMembers {
|
||||||
|
url,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
list_id,
|
||||||
|
dry_run,
|
||||||
|
skip_owners,
|
||||||
|
} => {
|
||||||
|
let conn = import::Mailman3Connection::new(&url, &username, &password).unwrap();
|
||||||
|
if dry_run {
|
||||||
|
let entries = conn.users(&list_id).unwrap();
|
||||||
|
println!("{} result(s)", entries.len());
|
||||||
|
for e in entries {
|
||||||
|
println!(
|
||||||
|
"{}{}<{}>",
|
||||||
|
if let Some(n) = e.display_name() {
|
||||||
|
n
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
|
if e.display_name().is_none() { "" } else { " " },
|
||||||
|
e.email()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !skip_owners {
|
||||||
|
let entries = conn.owners(&list_id).unwrap();
|
||||||
|
println!("\nOwners: {} result(s)", entries.len());
|
||||||
|
for e in entries {
|
||||||
|
println!(
|
||||||
|
"{}{}<{}>",
|
||||||
|
if let Some(n) = e.display_name() {
|
||||||
|
n
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
|
if e.display_name().is_none() { "" } else { " " },
|
||||||
|
e.email()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let entries = conn.users(&list_id).unwrap();
|
||||||
|
let tx = db.transaction(Default::default()).unwrap();
|
||||||
|
for sub in entries.into_iter().map(|e| e.into_subscription(list.pk)) {
|
||||||
|
tx.add_subscription(list.pk, sub)?;
|
||||||
|
}
|
||||||
|
if !skip_owners {
|
||||||
|
let entries = conn.owners(&list_id).unwrap();
|
||||||
|
for sub in entries.into_iter().map(|e| e.into_owner(list.pk)) {
|
||||||
|
tx.add_list_owner(sub)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tx.commit()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CreateList {
|
CreateList {
|
||||||
|
|
|
@ -670,4 +670,140 @@ impl Connection {
|
||||||
tx.commit()?;
|
tx.commit()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Execute operations inside an SQL transaction.
|
||||||
|
pub fn transaction(
|
||||||
|
&'_ self,
|
||||||
|
behavior: transaction::TransactionBehavior,
|
||||||
|
) -> Result<transaction::Transaction<'_>> {
|
||||||
|
use transaction::*;
|
||||||
|
|
||||||
|
let query = match behavior {
|
||||||
|
TransactionBehavior::Deferred => "BEGIN DEFERRED",
|
||||||
|
TransactionBehavior::Immediate => "BEGIN IMMEDIATE",
|
||||||
|
TransactionBehavior::Exclusive => "BEGIN EXCLUSIVE",
|
||||||
|
};
|
||||||
|
self.connection.execute_batch(query)?;
|
||||||
|
Ok(Transaction {
|
||||||
|
conn: self,
|
||||||
|
drop_behavior: DropBehavior::Rollback,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute operations inside an SQL transaction.
|
||||||
|
pub mod transaction {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// A transaction handle.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Transaction<'conn> {
|
||||||
|
pub(super) conn: &'conn Connection,
|
||||||
|
pub(super) drop_behavior: DropBehavior,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Transaction<'_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
_ = self.finish_();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Transaction<'_> {
|
||||||
|
/// Commit and consume transaction.
|
||||||
|
pub fn commit(mut self) -> Result<()> {
|
||||||
|
self.commit_()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn commit_(&mut self) -> Result<()> {
|
||||||
|
self.conn.connection.execute_batch("COMMIT")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configure the transaction to perform the specified action when it is
|
||||||
|
/// dropped.
|
||||||
|
#[inline]
|
||||||
|
pub fn set_drop_behavior(&mut self, drop_behavior: DropBehavior) {
|
||||||
|
self.drop_behavior = drop_behavior;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A convenience method which consumes and rolls back a transaction.
|
||||||
|
#[inline]
|
||||||
|
pub fn rollback(mut self) -> Result<()> {
|
||||||
|
self.rollback_()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rollback_(&mut self) -> Result<()> {
|
||||||
|
self.conn.connection.execute_batch("ROLLBACK")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Consumes the transaction, committing or rolling back according to
|
||||||
|
/// the current setting (see `drop_behavior`).
|
||||||
|
///
|
||||||
|
/// Functionally equivalent to the `Drop` implementation, but allows
|
||||||
|
/// callers to see any errors that occur.
|
||||||
|
#[inline]
|
||||||
|
pub fn finish(mut self) -> Result<()> {
|
||||||
|
self.finish_()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn finish_(&mut self) -> Result<()> {
|
||||||
|
if self.conn.connection.is_autocommit() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
match self.drop_behavior {
|
||||||
|
DropBehavior::Commit => self.commit_().or_else(|_| self.rollback_()),
|
||||||
|
DropBehavior::Rollback => self.rollback_(),
|
||||||
|
DropBehavior::Ignore => Ok(()),
|
||||||
|
DropBehavior::Panic => panic!("Transaction dropped unexpectedly."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for Transaction<'_> {
|
||||||
|
type Target = Connection;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deref(&self) -> &Connection {
|
||||||
|
self.conn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Options for transaction behavior. See [BEGIN
|
||||||
|
/// TRANSACTION](http://www.sqlite.org/lang_transaction.html) for details.
|
||||||
|
#[derive(Copy, Clone, Default)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum TransactionBehavior {
|
||||||
|
/// DEFERRED means that the transaction does not actually start until
|
||||||
|
/// the database is first accessed.
|
||||||
|
Deferred,
|
||||||
|
/// IMMEDIATE cause the database connection to start a new write
|
||||||
|
/// immediately, without waiting for a writes statement.
|
||||||
|
Immediate,
|
||||||
|
#[default]
|
||||||
|
/// EXCLUSIVE prevents other database connections from reading the
|
||||||
|
/// database while the transaction is underway.
|
||||||
|
Exclusive,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Options for how a Transaction or Savepoint should behave when it is
|
||||||
|
/// dropped.
|
||||||
|
#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum DropBehavior {
|
||||||
|
#[default]
|
||||||
|
/// Roll back the changes. This is the default.
|
||||||
|
Rollback,
|
||||||
|
|
||||||
|
/// Commit the changes.
|
||||||
|
Commit,
|
||||||
|
|
||||||
|
/// Do not commit or roll back changes - this will leave the transaction
|
||||||
|
/// or savepoint open, so should be used with care.
|
||||||
|
Ignore,
|
||||||
|
|
||||||
|
/// Panic. Used to enforce intentional behavior during development.
|
||||||
|
Panic,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
31
docs/mpot.1
31
docs/mpot.1
|
@ -569,6 +569,37 @@ Show mailing list info.
|
||||||
.ie \n(.g .ds Aq \(aq
|
.ie \n(.g .ds Aq \(aq
|
||||||
.el .ds Aq '
|
.el .ds Aq '
|
||||||
.\fB
|
.\fB
|
||||||
|
.SS mpot list import-members
|
||||||
|
.\fR
|
||||||
|
.br
|
||||||
|
|
||||||
|
.br
|
||||||
|
|
||||||
|
mpot list import\-members \-\-url \fIURL\fR \-\-username \fIUSERNAME\fR \-\-password \fIPASSWORD\fR \-\-list\-id \fILIST_ID\fR [\-\-dry\-run \fIDRY_RUN\fR] [\-\-skip\-owners \fISKIP_OWNERS\fR]
|
||||||
|
.br
|
||||||
|
|
||||||
|
Import members in a local list from a remote mailman3 REST API instance.
|
||||||
|
.TP
|
||||||
|
\-\-url \fIURL\fR
|
||||||
|
REST HTTP endpoint e.g. http://localhost:9001/3.0/.
|
||||||
|
.TP
|
||||||
|
\-\-username \fIUSERNAME\fR
|
||||||
|
REST HTTP Basic Authentication username.
|
||||||
|
.TP
|
||||||
|
\-\-password \fIPASSWORD\fR
|
||||||
|
REST HTTP Basic Authentication password.
|
||||||
|
.TP
|
||||||
|
\-\-list\-id \fILIST_ID\fR
|
||||||
|
List ID of remote list to query.
|
||||||
|
.TP
|
||||||
|
\-\-dry\-run
|
||||||
|
Show what would be inserted without performing any changes.
|
||||||
|
.TP
|
||||||
|
\-\-skip\-owners
|
||||||
|
Don\*(Aqt import list owners.
|
||||||
|
.ie \n(.g .ds Aq \(aq
|
||||||
|
.el .ds Aq '
|
||||||
|
.\fB
|
||||||
.SS mpot create-list
|
.SS mpot create-list
|
||||||
.\fR
|
.\fR
|
||||||
.br
|
.br
|
||||||
|
|
Loading…
Reference in New Issue