/* * 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 . */ pub use std::path::PathBuf; pub use clap::{builder::TypedValueParser, 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 ", 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, #[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, } #[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, /// List archive URL. #[arg(long)] archive_url: Option, }, /// 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: QueueCommand, }, /// Mail that has not been handled properly end up in the error queue. Queue { #[arg(long, value_parser = QueueValueParser)] queue: mailpot::queue::Queue, #[command(subcommand)] cmd: QueueCommand, }, /// Import a maildir folder into an existing list. ImportMaildir { /// 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, #[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 . 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, /// Public key. #[arg(long)] public_key: Option, #[arg(long)] /// Is account enabled. enabled: Option, }, /// 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, /// Name. #[arg(long)] name: Option>, /// Public key. #[arg(long)] public_key: Option>, #[arg(long)] /// Is account enabled. enabled: Option>, }, /// 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, /// 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, /// The directory in which the map files are saved. /// /// Default is `data_path` from [`Configuration`](mailpot::Configuration). #[arg(long)] pub map_output_path: Option, /// 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): /// . #[arg(long)] pub transport_name: Option, } #[derive(Debug, Subcommand)] pub enum QueueCommand { /// List. List, /// Print entry in RFC5322 or JSON format. Print { /// index of entry. #[arg(long)] index: Vec, }, /// Delete entry and print it in stdout. Delete { /// index of entry. #[arg(long)] index: Vec, /// Do not print in stdout. #[arg(long)] quiet: bool, }, } /// Subscription options. #[derive(Debug, Args)] pub struct SubscriptionOptions { /// Name. #[arg(long)] pub name: Option, /// Send messages as digest. #[arg(long, default_value = "false")] pub digest: Option, /// Hide message from list when posting. #[arg(long, default_value = "false")] pub hide_address: Option, /// Hide message from list when posting. #[arg(long, default_value = "false")] /// E-mail address verification status. pub verified: Option, #[arg(long, default_value = "true")] /// Receive confirmation email when posting. pub receive_confirmation: Option, #[arg(long, default_value = "true")] /// Receive posts from list even if address exists in To or Cc header. pub receive_duplicates: Option, #[arg(long, default_value = "false")] /// Receive own posts from list. pub receive_own_posts: Option, #[arg(long, default_value = "true")] /// Is subscription enabled. pub enabled: Option, } /// Account options. #[derive(Debug, Args)] pub struct AccountOptions { /// Name. #[arg(long)] pub name: Option, /// Public key. #[arg(long)] pub public_key: Option, #[arg(long)] /// Is account enabled. pub enabled: Option, } #[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. AddPostPolicy { #[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. RemovePostPolicy { #[arg(long)] /// Post policy primary key. pk: i64, }, /// Add subscription policy to list. AddSubscriptionPolicy { #[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, }, RemoveSubscriptionPolicy { #[arg(long)] /// Subscription policy primary key. pk: i64, }, /// Add list owner to list. AddListOwner { #[arg(long)] address: String, #[arg(long)] name: Option, }, 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, /// New List-ID. #[arg(long)] id: Option, /// New list address. #[arg(long)] address: Option, /// New list description. #[arg(long)] description: Option, /// New list archive URL. #[arg(long)] archive_url: Option, /// New owner address local part. /// If empty, it defaults to '+owner'. #[arg(long)] owner_local_part: Option, /// New request address local part. /// If empty, it defaults to '+request'. #[arg(long)] request_local_part: Option, /// Require verification of e-mails for new subscriptions. /// /// Subscriptions that are initiated from the subscription's address are /// verified automatically. #[arg(long)] verify: Option, /// 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, /// 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, }, /// 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, }, } #[derive(Clone, Copy, Debug)] pub struct QueueValueParser; impl QueueValueParser { /// Implementation for [`ValueParser::path_buf`] pub fn new() -> Self { Self } } impl TypedValueParser for QueueValueParser { type Value = mailpot::queue::Queue; fn parse_ref( &self, cmd: &clap::Command, arg: Option<&clap::Arg>, value: &std::ffi::OsStr, ) -> std::result::Result { TypedValueParser::parse(self, cmd, arg, value.to_owned()) } fn parse( &self, cmd: &clap::Command, _arg: Option<&clap::Arg>, value: std::ffi::OsString, ) -> std::result::Result { use std::str::FromStr; use clap::error::ErrorKind; if value.is_empty() { return Err(cmd.clone().error( ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand, "queue value required", )); } Self::Value::from_str(value.to_str().ok_or_else(|| { cmd.clone().error( ErrorKind::InvalidValue, "Queue value is not an UTF-8 string", ) })?) .map_err(|err| cmd.clone().error(ErrorKind::InvalidValue, err)) } } impl Default for QueueValueParser { fn default() -> Self { Self::new() } }