mailpot/cli/src/commands.rs

1094 lines
35 KiB
Rust

/*
* 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/>.
*/
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
io::{Read, Write},
path::{Path, PathBuf},
process::Stdio,
};
use mailpot::{
melib,
melib::{maildir::MaildirPathTrait, smol, Envelope, EnvelopeHash},
models::{changesets::*, *},
queue::{Queue, QueueEntry},
transaction::TransactionBehavior,
Connection, Context, Error, ErrorKind, Result,
};
use crate::{lints::*, *};
macro_rules! list {
($db:ident, $list_id:expr) => {{
$db.list_by_id(&$list_id)?.or_else(|| {
$list_id
.parse::<i64>()
.ok()
.map(|pk| $db.list(pk).ok())
.flatten()
.flatten()
})
}};
}
macro_rules! string_opts {
($field:ident) => {
if $field.as_deref().map(str::is_empty).unwrap_or(false) {
None
} else {
Some($field)
}
};
}
pub fn dump_database(db: &mut Connection) -> Result<()> {
let lists = db.lists()?;
let mut stdout = std::io::stdout();
serde_json::to_writer_pretty(&mut stdout, &lists)?;
for l in &lists {
serde_json::to_writer_pretty(
&mut stdout,
&db.list_subscriptions(l.pk)
.context("Could not retrieve list subscriptions.")?,
)?;
}
Ok(())
}
pub fn list_lists(db: &mut Connection) -> Result<()> {
let lists = db.lists().context("Could not retrieve lists.")?;
if lists.is_empty() {
println!("No lists found.");
} else {
for l in lists {
println!("- {} {:?}", l.id, l);
let list_owners = db
.list_owners(l.pk)
.context("Could not retrieve list owners.")?;
if list_owners.is_empty() {
println!("\tList owners: None");
} else {
println!("\tList owners:");
for o in list_owners {
println!("\t- {}", o);
}
}
if let Some(s) = db
.list_post_policy(l.pk)
.context("Could not retrieve list post policy.")?
{
println!("\tPost policy: {}", s);
} else {
println!("\tPost policy: None");
}
if let Some(s) = db
.list_subscription_policy(l.pk)
.context("Could not retrieve list subscription policy.")?
{
println!("\tSubscription policy: {}", s);
} else {
println!("\tSubscription policy: None");
}
println!();
}
}
Ok(())
}
pub fn list(db: &mut Connection, list_id: &str, cmd: ListCommand, quiet: bool) -> Result<()> {
let list = match list!(db, list_id) {
Some(v) => v,
None => {
return Err(format!("No list with id or pk {} was found", list_id).into());
}
};
use ListCommand::*;
match cmd {
Subscriptions => {
let subscriptions = db.list_subscriptions(list.pk)?;
if subscriptions.is_empty() {
if !quiet {
println!("No subscriptions found.");
}
} else {
if !quiet {
println!("Subscriptions of list {}", list.id);
}
for l in subscriptions {
println!("- {}", &l);
}
}
}
AddSubscription {
address,
subscription_options:
SubscriptionOptions {
name,
digest,
hide_address,
receive_duplicates,
receive_own_posts,
receive_confirmation,
enabled,
verified,
},
} => {
db.add_subscription(
list.pk,
ListSubscription {
pk: 0,
list: list.pk,
address,
account: None,
name,
digest: digest.unwrap_or(false),
hide_address: hide_address.unwrap_or(false),
receive_confirmation: receive_confirmation.unwrap_or(true),
receive_duplicates: receive_duplicates.unwrap_or(true),
receive_own_posts: receive_own_posts.unwrap_or(false),
enabled: enabled.unwrap_or(true),
verified: verified.unwrap_or(false),
},
)?;
}
RemoveSubscription { address } => {
let mut input = String::new();
loop {
println!(
"Are you sure you want to remove subscription of {} from list {}? [Yy/n]",
address, list
);
input.clear();
std::io::stdin().read_line(&mut input)?;
if input.trim() == "Y" || input.trim() == "y" || input.trim() == "" {
break;
} else if input.trim() == "n" {
return Ok(());
}
}
db.remove_subscription(list.pk, &address)?;
}
Health => {
if !quiet {
println!("{} health:", list);
}
let list_owners = db
.list_owners(list.pk)
.context("Could not retrieve list owners.")?;
let post_policy = db
.list_post_policy(list.pk)
.context("Could not retrieve list post policy.")?;
let subscription_policy = db
.list_subscription_policy(list.pk)
.context("Could not retrieve list subscription policy.")?;
if list_owners.is_empty() {
println!("\tList has no owners: you should add at least one.");
} else {
for owner in list_owners {
println!("\tList owner: {}.", owner);
}
}
if let Some(p) = post_policy {
println!("\tList has post policy: {p}.");
} else {
println!("\tList has no post policy: you should add one.");
}
if let Some(p) = subscription_policy {
println!("\tList has subscription policy: {p}.");
} else {
println!("\tList has no subscription policy: you should add one.");
}
}
Info => {
println!("{} info:", list);
let list_owners = db
.list_owners(list.pk)
.context("Could not retrieve list owners.")?;
let post_policy = db
.list_post_policy(list.pk)
.context("Could not retrieve list post policy.")?;
let subscription_policy = db
.list_subscription_policy(list.pk)
.context("Could not retrieve list subscription policy.")?;
let subscriptions = db
.list_subscriptions(list.pk)
.context("Could not retrieve list subscriptions.")?;
if subscriptions.is_empty() {
println!("No subscriptions.");
} else if subscriptions.len() == 1 {
println!("1 subscription.");
} else {
println!("{} subscriptions.", subscriptions.len());
}
if list_owners.is_empty() {
println!("List owners: None");
} else {
println!("List owners:");
for o in list_owners {
println!("\t- {}", o);
}
}
if let Some(s) = post_policy {
println!("Post policy: {s}");
} else {
println!("Post policy: None");
}
if let Some(s) = subscription_policy {
println!("Subscription policy: {s}");
} else {
println!("Subscription policy: None");
}
}
UpdateSubscription {
address,
subscription_options:
SubscriptionOptions {
name,
digest,
hide_address,
receive_duplicates,
receive_own_posts,
receive_confirmation,
enabled,
verified,
},
} => {
let name = if name
.as_ref()
.map(|s: &String| s.is_empty())
.unwrap_or(false)
{
None
} else {
Some(name)
};
let changeset = ListSubscriptionChangeset {
list: list.pk,
address,
account: None,
name,
digest,
verified,
hide_address,
receive_duplicates,
receive_own_posts,
receive_confirmation,
enabled,
};
db.update_subscription(changeset)?;
}
AddPostPolicy {
announce_only,
subscription_only,
approval_needed,
open,
custom,
} => {
let policy = PostPolicy {
pk: 0,
list: list.pk,
announce_only,
subscription_only,
approval_needed,
open,
custom,
};
let new_val = db.set_list_post_policy(policy)?;
println!("Added new policy with pk = {}", new_val.pk());
}
RemovePostPolicy { pk } => {
db.remove_list_post_policy(list.pk, pk)?;
println!("Removed policy with pk = {}", pk);
}
AddSubscriptionPolicy {
send_confirmation,
open,
manual,
request,
custom,
} => {
let policy = SubscriptionPolicy {
pk: 0,
list: list.pk,
send_confirmation,
open,
manual,
request,
custom,
};
let new_val = db.set_list_subscription_policy(policy)?;
println!("Added new subscribe policy with pk = {}", new_val.pk());
}
RemoveSubscriptionPolicy { pk } => {
db.remove_list_subscription_policy(list.pk, pk)?;
println!("Removed subscribe policy with pk = {}", pk);
}
AddListOwner { address, name } => {
let list_owner = ListOwner {
pk: 0,
list: list.pk,
address,
name,
};
let new_val = db.add_list_owner(list_owner)?;
println!("Added new list owner {}", new_val);
}
RemoveListOwner { pk } => {
db.remove_list_owner(list.pk, pk)?;
println!("Removed list owner with pk = {}", pk);
}
EnableSubscription { address } => {
let changeset = ListSubscriptionChangeset {
list: list.pk,
address,
account: None,
name: None,
digest: None,
verified: None,
enabled: Some(true),
hide_address: None,
receive_duplicates: None,
receive_own_posts: None,
receive_confirmation: None,
};
db.update_subscription(changeset)?;
}
DisableSubscription { address } => {
let changeset = ListSubscriptionChangeset {
list: list.pk,
address,
account: None,
name: None,
digest: None,
enabled: Some(false),
verified: None,
hide_address: None,
receive_duplicates: None,
receive_own_posts: None,
receive_confirmation: None,
};
db.update_subscription(changeset)?;
}
Update {
name,
id,
address,
description,
archive_url,
owner_local_part,
request_local_part,
verify,
hidden,
enabled,
} => {
let description = string_opts!(description);
let archive_url = string_opts!(archive_url);
let owner_local_part = string_opts!(owner_local_part);
let request_local_part = string_opts!(request_local_part);
let changeset = MailingListChangeset {
pk: list.pk,
name,
id,
address,
description,
archive_url,
owner_local_part,
request_local_part,
verify,
hidden,
enabled,
};
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()?;
}
}
SubscriptionRequests => {
let subscriptions = db.list_subscription_requests(list.pk)?;
if subscriptions.is_empty() {
println!("No subscription requests found.");
} else {
println!("Subscription requests of list {}", list.id);
for l in subscriptions {
println!("- {}", &l);
}
}
}
AcceptSubscriptionRequest {
pk,
do_not_send_confirmation,
} => match db.accept_candidate_subscription(pk) {
Ok(subscription) => {
println!("Added: {subscription:#?}");
if !do_not_send_confirmation {
if let Err(err) = db
.list(subscription.list)
.and_then(|v| match v {
Some(v) => Ok(v),
None => Err(format!(
"No list with id or pk {} was found",
subscription.list
)
.into()),
})
.and_then(|list| {
db.send_subscription_confirmation(&list, &subscription.address())
})
{
eprintln!("Could not send subscription confirmation!");
return Err(err);
}
println!("Sent confirmation e-mail to {}", subscription.address());
} else {
println!(
"Did not sent confirmation e-mail to {}. You can do it manually with the \
appropriate command.",
subscription.address()
);
}
}
Err(err) => {
eprintln!("Could not accept subscription request!");
return Err(err);
}
},
SendConfirmationForSubscription { pk } => {
let req = match db.candidate_subscription(pk) {
Ok(req) => req,
Err(err) => {
eprintln!("Could not find subscription request by that pk!");
return Err(err);
}
};
log::info!("Found {:#?}", req);
if req.accepted.is_none() {
return Err("Request has not been accepted!".into());
}
if let Err(err) = db
.list(req.list)
.and_then(|v| match v {
Some(v) => Ok(v),
None => Err(format!("No list with id or pk {} was found", req.list).into()),
})
.and_then(|list| db.send_subscription_confirmation(&list, &req.address()))
{
eprintln!("Could not send subscription request confirmation!");
return Err(err);
}
println!("Sent confirmation e-mail to {}", req.address());
}
}
Ok(())
}
pub fn create_list(
db: &mut Connection,
name: String,
id: String,
address: String,
description: Option<String>,
archive_url: Option<String>,
quiet: bool,
) -> Result<()> {
let new = db.create_list(MailingList {
pk: 0,
name,
id,
description,
topics: vec![],
address,
archive_url,
})?;
log::trace!("created new list {:#?}", new);
if !quiet {
println!(
"Created new list {:?} with primary key {}",
new.id,
new.pk()
);
}
Ok(())
}
pub fn post(db: &mut Connection, dry_run: bool, debug: bool) -> Result<()> {
if debug {
println!("Post dry_run = {:?}", dry_run);
}
let tx = db
.transaction(TransactionBehavior::Exclusive)
.context("Could not open Exclusive transaction in database.")?;
let mut input = String::new();
std::io::stdin()
.read_to_string(&mut input)
.context("Could not read from stdin")?;
match Envelope::from_bytes(input.as_bytes(), None) {
Ok(env) => {
if debug {
eprintln!("Parsed envelope is:\n{:?}", &env);
}
tx.post(&env, input.as_bytes(), dry_run)?;
}
Err(err) if input.trim().is_empty() => {
eprintln!("Empty input, abort.");
return Err(err.into());
}
Err(err) => {
eprintln!("Could not parse message: {}", err);
let p = tx.conf().save_message(input)?;
eprintln!("Message saved at {}", p.display());
return Err(err.into());
}
}
tx.commit()
}
pub fn flush_queue(db: &mut Connection, dry_run: bool, verbose: u8, debug: bool) -> Result<()> {
let tx = db
.transaction(TransactionBehavior::Exclusive)
.context("Could not open Exclusive transaction in database.")?;
let messages = tx.delete_from_queue(mailpot::queue::Queue::Out, vec![])?;
if verbose > 0 || debug {
println!("Queue out has {} messages.", messages.len());
}
let mut failures = Vec::with_capacity(messages.len());
let send_mail = tx.conf().send_mail.clone();
match send_mail {
mailpot::SendMail::ShellCommand(cmd) => {
fn submit(cmd: &str, msg: &QueueEntry, dry_run: bool) -> Result<()> {
if dry_run {
return Ok(());
}
let mut child = std::process::Command::new("sh")
.arg("-c")
.arg(cmd)
.env("TO_ADDRESS", msg.to_addresses.clone())
.stdout(Stdio::piped())
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("sh command failed to start")?;
let mut stdin = child
.stdin
.take()
.ok_or_else(|| Error::from("Failed to open stdin"))?;
let builder = std::thread::Builder::new();
std::thread::scope(|s| {
let handler = builder
.spawn_scoped(s, move || {
stdin
.write_all(&msg.message)
.expect("Failed to write to stdin");
})
.context(
"Could not spawn IPC communication thread for SMTP ShellCommand \
process",
)?;
handler.join().map_err(|_| {
ErrorKind::External(mailpot::anyhow::anyhow!(
"Could not join with IPC communication thread for SMTP ShellCommand \
process"
))
})?;
let result = child.wait_with_output()?;
if !result.status.success() {
return Err(Error::new_external(format!(
"{} proccess failed with exit code: {:?}\n{}",
cmd,
result.status.code(),
String::from_utf8(result.stderr).unwrap()
)));
}
Ok::<(), Error>(())
})?;
Ok(())
}
for msg in messages {
if let Err(err) = submit(&cmd, &msg, dry_run) {
if verbose > 0 || debug {
eprintln!("Message {msg:?} failed with: {err}.");
}
failures.push((err, msg));
} else if verbose > 0 || debug {
eprintln!("Submitted message {}", msg.message_id);
}
}
}
mailpot::SendMail::Smtp(_) => {
let conn_future = tx.new_smtp_connection()?;
failures = smol::future::block_on(smol::spawn(async move {
let mut conn = conn_future.await?;
for msg in messages {
if let Err(err) = Connection::submit(&mut conn, &msg, dry_run).await {
failures.push((err, msg));
}
}
Ok::<_, Error>(failures)
}))?;
}
}
for (err, mut msg) in failures {
log::error!("Message {msg:?} failed with: {err}. Inserting to Deferred queue.");
msg.queue = mailpot::queue::Queue::Deferred;
tx.insert_to_queue(msg)?;
}
if !dry_run {
tx.commit()?;
}
Ok(())
}
pub fn queue_(db: &mut Connection, queue: Queue, cmd: QueueCommand, quiet: bool) -> Result<()> {
match cmd {
QueueCommand::List => {
let entries = db.queue(queue)?;
if entries.is_empty() {
if !quiet {
println!("Queue {queue} is empty.");
}
} else {
for e in entries {
println!(
"- {} {} {} {} {}",
e.pk, e.datetime, e.from_address, e.to_addresses, e.subject
);
}
}
}
QueueCommand::Print { index } => {
let mut entries = db.queue(queue)?;
if !index.is_empty() {
entries.retain(|el| index.contains(&el.pk()));
}
if entries.is_empty() {
if !quiet {
println!("Queue {queue} is empty.");
}
} else {
for e in entries {
println!("{e:?}");
}
}
}
QueueCommand::Delete { index } => {
let mut entries = db.queue(queue)?;
if !index.is_empty() {
entries.retain(|el| index.contains(&el.pk()));
}
if entries.is_empty() {
if !quiet {
println!("Queue {queue} is empty.");
}
} else {
if !quiet {
println!("Deleting queue {queue} elements {:?}", &index);
}
db.delete_from_queue(queue, index)?;
if !quiet {
for e in entries {
println!("{e:?}");
}
}
}
}
}
Ok(())
}
pub fn import_maildir(
db: &mut Connection,
list_id: &str,
mut maildir_path: PathBuf,
quiet: bool,
debug: bool,
verbose: u8,
) -> Result<()> {
let list = match list!(db, list_id) {
Some(v) => v,
None => {
return Err(format!("No list with id or pk {} was found", list_id).into());
}
};
if !maildir_path.is_absolute() {
maildir_path = std::env::current_dir()
.context("could not detect current directory")?
.join(&maildir_path);
}
fn get_file_hash(file: &std::path::Path) -> EnvelopeHash {
let mut hasher = DefaultHasher::default();
file.hash(&mut hasher);
EnvelopeHash(hasher.finish())
}
let mut buf = Vec::with_capacity(4096);
let files = melib::maildir::MaildirType::list_mail_in_maildir_fs(maildir_path, true)
.context("Could not parse files in maildir path")?;
let mut ctr = 0;
for file in files {
let hash = get_file_hash(&file);
let mut reader = std::io::BufReader::new(
std::fs::File::open(&file)
.with_context(|| format!("Could not open {}.", file.display()))?,
);
buf.clear();
reader
.read_to_end(&mut buf)
.with_context(|| format!("Could not read from {}.", file.display()))?;
match Envelope::from_bytes(buf.as_slice(), Some(file.flags())) {
Ok(mut env) => {
env.set_hash(hash);
if verbose > 1 {
println!(
"Inserting post from {:?} with subject `{}` and Message-ID `{}`.",
env.from(),
env.subject(),
env.message_id()
);
}
db.insert_post(list.pk, &buf, &env).with_context(|| {
format!(
"Could not insert post `{}` from path `{}`",
env.message_id(),
file.display()
)
})?;
ctr += 1;
}
Err(err) => {
if verbose > 0 || debug {
log::error!(
"Could not parse Envelope from file {}: {err}",
file.display()
);
}
}
}
}
if !quiet {
println!("Inserted {} posts to {}.", ctr, list_id);
}
Ok(())
}
pub fn update_postfix_config(
config_path: &Path,
db: &mut Connection,
master_cf: Option<PathBuf>,
PostfixConfig {
user,
group,
binary_path,
process_limit,
map_output_path,
transport_name,
}: PostfixConfig,
) -> Result<()> {
let pfconf = mailpot::postfix::PostfixConfiguration {
user: user.into(),
group: group.map(Into::into),
binary_path,
process_limit,
map_output_path,
transport_name: transport_name.map(std::borrow::Cow::from),
};
pfconf
.save_maps(db.conf())
.context("Could not save maps.")?;
pfconf
.save_master_cf_entry(db.conf(), config_path, master_cf.as_deref())
.context("Could not save master.cf file.")?;
Ok(())
}
pub fn print_postfix_config(
config_path: &Path,
db: &mut Connection,
PostfixConfig {
user,
group,
binary_path,
process_limit,
map_output_path,
transport_name,
}: PostfixConfig,
) -> Result<()> {
let pfconf = mailpot::postfix::PostfixConfiguration {
user: user.into(),
group: group.map(Into::into),
binary_path,
process_limit,
map_output_path,
transport_name: transport_name.map(std::borrow::Cow::from),
};
let lists = db.lists().context("Could not retrieve lists.")?;
let lists_post_policies = lists
.into_iter()
.map(|l| {
let pk = l.pk;
Ok((
l,
db.list_post_policy(pk).with_context(|| {
format!("Could not retrieve list post policy for list_pk = {pk}.")
})?,
))
})
.collect::<Result<Vec<(DbVal<MailingList>, Option<DbVal<PostPolicy>>)>>>()?;
let maps = pfconf.generate_maps(&lists_post_policies);
let mastercf = pfconf.generate_master_cf_entry(db.conf(), config_path);
println!("{maps}\n\n{mastercf}\n");
Ok(())
}
pub fn accounts(db: &mut Connection, quiet: bool) -> Result<()> {
let accounts = db.accounts()?;
if accounts.is_empty() {
if !quiet {
println!("No accounts found.");
}
} else {
for a in accounts {
println!("- {:?}", a);
}
}
Ok(())
}
pub fn account_info(db: &mut Connection, address: &str, quiet: bool) -> Result<()> {
if let Some(acc) = db.account_by_address(address)? {
let subs = db
.account_subscriptions(acc.pk())
.context("Could not retrieve account subscriptions for this account.")?;
if subs.is_empty() {
if !quiet {
println!("No subscriptions found.");
}
} else {
for s in subs {
let list = db
.list(s.list)
.with_context(|| {
format!(
"Found subscription with list_pk = {} but could not retrieve the \
list.\nListSubscription = {:?}",
s.list, s
)
})?
.ok_or_else(|| {
format!(
"Found subscription with list_pk = {} but no such list \
exists.\nListSubscription = {:?}",
s.list, s
)
})?;
println!("- {:?} {}", s, list);
}
}
} else {
return Err(format!("Account with address {address} not found!").into());
}
Ok(())
}
pub fn add_account(
db: &mut Connection,
address: String,
password: String,
name: Option<String>,
public_key: Option<String>,
enabled: Option<bool>,
) -> Result<()> {
db.add_account(Account {
pk: 0,
name,
address,
public_key,
password,
enabled: enabled.unwrap_or(true),
})?;
Ok(())
}
pub fn remove_account(db: &mut Connection, address: &str, quiet: bool) -> Result<()> {
let mut input = String::new();
if !quiet {
loop {
println!(
"Are you sure you want to remove account with address {}? [Yy/n]",
address
);
input.clear();
std::io::stdin().read_line(&mut input)?;
if input.trim() == "Y" || input.trim() == "y" || input.trim() == "" {
break;
} else if input.trim() == "n" {
return Ok(());
}
}
}
db.remove_account(address)?;
Ok(())
}
pub fn update_account(
db: &mut Connection,
address: String,
password: Option<String>,
name: Option<Option<String>>,
public_key: Option<Option<String>>,
enabled: Option<Option<bool>>,
) -> Result<()> {
let changeset = AccountChangeset {
address,
name,
public_key,
password,
enabled,
};
db.update_account(changeset)?;
Ok(())
}
pub fn repair(
db: &mut Connection,
fix: bool,
all: bool,
mut datetime_header_value: bool,
mut remove_empty_accounts: bool,
mut remove_accepted_subscription_requests: bool,
mut warn_list_no_owner: bool,
) -> Result<()> {
type LintFn = fn(&'_ mut mailpot::Connection, bool) -> std::result::Result<(), mailpot::Error>;
let dry_run = !fix;
if all {
datetime_header_value = true;
remove_empty_accounts = true;
remove_accepted_subscription_requests = true;
warn_list_no_owner = true;
}
if !(datetime_header_value
| remove_empty_accounts
| remove_accepted_subscription_requests
| warn_list_no_owner)
{
return Err("No lints selected: specify them with flag arguments. See --help".into());
}
if dry_run {
println!("running without making modifications (dry run)");
}
for (name, flag, lint_fn) in [
(
stringify!(datetime_header_value),
datetime_header_value,
datetime_header_value_lint as LintFn,
),
(
stringify!(remove_empty_accounts),
remove_empty_accounts,
remove_empty_accounts_lint as _,
),
(
stringify!(remove_accepted_subscription_requests),
remove_accepted_subscription_requests,
remove_accepted_subscription_requests_lint as _,
),
(
stringify!(warn_list_no_owner),
warn_list_no_owner,
warn_list_no_owner_lint as _,
),
] {
if flag {
lint_fn(db, dry_run).with_context(|| format!("Lint {name} failed."))?;
}
}
Ok(())
}