cli: add import from mailman3 rest api

axum-login-upgrade
Manos Pitsidianakis 2023-05-09 10:49:54 +03:00
parent 6cae75e5ae
commit f0bf147a0d
Signed by: Manos Pitsidianakis
GPG Key ID: 7729C7707F7E09D0
9 changed files with 893 additions and 443 deletions

16
Cargo.lock generated
View File

@ -1799,14 +1799,18 @@ name = "mailpot-cli"
version = "0.1.1"
dependencies = [
"assert_cmd",
"base64 0.21.0",
"clap",
"clap_mangen",
"log",
"mailpot",
"mailpot-tests",
"predicates",
"serde",
"serde_json",
"stderrlog",
"tempfile",
"ureq",
]
[[package]]
@ -3222,6 +3226,18 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "url"
version = "2.3.1"

View File

@ -16,10 +16,14 @@ name = "mpot"
path = "src/main.rs"
[dependencies]
base64 = { version = "0.21" }
clap = { version = "^4.2", default-features = false, features = ["derive", "cargo", "unicode", "help", "usage", "error-context", "suggestions"] }
log = "0.4"
mailpot = { version = "^0.1", path = "../core" }
serde = { version = "^1", features = ["derive", ] }
serde_json = "^1"
stderrlog = "^0.5"
ureq = { version = "2.6", default-features = false }
[dev-dependencies]
assert_cmd = "2"

View File

@ -27,7 +27,7 @@ use clap::ArgAction;
use clap_mangen::{roff, Man};
use roff::{bold, italic, roman, Inline, Roff};
include!("src/lib.rs");
include!("src/args.rs");
fn main() -> std::io::Result<()> {
println!("cargo:rerun-if-changed=./src/lib.rs");

496
cli/src/args.rs 100644
View File

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

149
cli/src/import.rs 100644
View File

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

View File

@ -17,448 +17,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
extern crate base64;
extern crate ureq;
pub use std::path::PathBuf;
mod args;
pub mod import;
pub use args::*;
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,
}

View File

@ -421,6 +421,61 @@ fn run_app(opt: Opt) -> Result<()> {
};
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 {

View File

@ -670,4 +670,140 @@ impl Connection {
tx.commit()?;
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,
}
}

View File

@ -569,6 +569,37 @@ Show mailing list info.
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.\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
.\fR
.br