cli: add import from mailman3 rest api
parent
6cae75e5ae
commit
f0bf147a0d
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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/>.
|
||||
*/
|
||||
|
||||
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,
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
31
docs/mpot.1
31
docs/mpot.1
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue