977 lines
37 KiB
Rust
977 lines
37 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},
|
|
process::Stdio,
|
|
};
|
|
|
|
mod lints;
|
|
use lints::*;
|
|
use mailpot::{
|
|
melib::{backends::maildir::MaildirPathTrait, smol, smtp::*, Envelope, EnvelopeHash},
|
|
models::{changesets::*, *},
|
|
queue::QueueEntry,
|
|
transaction::TransactionBehavior,
|
|
Configuration, Connection, Error, ErrorKind, Result, *,
|
|
};
|
|
use mailpot_cli::*;
|
|
|
|
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)
|
|
}
|
|
};
|
|
}
|
|
|
|
fn run_app(opt: Opt) -> Result<()> {
|
|
if opt.debug {
|
|
println!("DEBUG: {:?}", &opt);
|
|
}
|
|
if let Command::SampleConfig { with_smtp } = opt.cmd {
|
|
let mut new = Configuration::new("/path/to/sqlite.db");
|
|
new.administrators.push("admin@example.com".to_string());
|
|
if with_smtp {
|
|
new.send_mail = mailpot::SendMail::Smtp(SmtpServerConf {
|
|
hostname: "mail.example.com".to_string(),
|
|
port: 587,
|
|
envelope_from: "".to_string(),
|
|
auth: SmtpAuth::Auto {
|
|
username: "user".to_string(),
|
|
password: Password::Raw("hunter2".to_string()),
|
|
auth_type: SmtpAuthType::default(),
|
|
require_auth: true,
|
|
},
|
|
security: SmtpSecurity::StartTLS {
|
|
danger_accept_invalid_certs: false,
|
|
},
|
|
extensions: Default::default(),
|
|
});
|
|
}
|
|
println!("{}", new.to_toml());
|
|
return Ok(());
|
|
};
|
|
let config_path = if let Some(path) = opt.config.as_ref() {
|
|
path.as_path()
|
|
} else {
|
|
let mut opt = Opt::command();
|
|
opt.error(
|
|
clap::error::ErrorKind::MissingRequiredArgument,
|
|
"--config is required for mailing list operations",
|
|
)
|
|
.exit();
|
|
};
|
|
|
|
let config = Configuration::from_file(config_path)?;
|
|
|
|
use Command::*;
|
|
let mut db = Connection::open_or_create_db(config)?.trusted();
|
|
match opt.cmd {
|
|
SampleConfig { .. } => {}
|
|
DumpDatabase => {
|
|
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)?)?;
|
|
}
|
|
}
|
|
ListLists => {
|
|
let lists = db.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)?;
|
|
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)? {
|
|
println!("\tPost policy: {}", s);
|
|
} else {
|
|
println!("\tPost policy: None");
|
|
}
|
|
if let Some(s) = db.list_subscription_policy(l.pk)? {
|
|
println!("\tSubscription policy: {}", s);
|
|
} else {
|
|
println!("\tSubscription policy: None");
|
|
}
|
|
println!();
|
|
}
|
|
}
|
|
}
|
|
List { list_id, cmd } => {
|
|
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() {
|
|
println!("No subscriptions found.");
|
|
} else {
|
|
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 => {
|
|
println!("{} health:", list);
|
|
let list_owners = db.list_owners(list.pk)?;
|
|
let post_policy = db.list_post_policy(list.pk)?;
|
|
let subscription_policy = db.list_subscription_policy(list.pk)?;
|
|
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)?;
|
|
let post_policy = db.list_post_policy(list.pk)?;
|
|
let subscription_policy = db.list_subscription_policy(list.pk)?;
|
|
let subscriptions = db.list_subscriptions(list.pk)?;
|
|
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()?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
CreateList {
|
|
name,
|
|
id,
|
|
address,
|
|
description,
|
|
archive_url,
|
|
} => {
|
|
let new = db.create_list(MailingList {
|
|
pk: 0,
|
|
name,
|
|
id,
|
|
description,
|
|
topics: vec![],
|
|
address,
|
|
archive_url,
|
|
})?;
|
|
log::trace!("created new list {:#?}", new);
|
|
if !opt.quiet {
|
|
println!(
|
|
"Created new list {:?} with primary key {}",
|
|
new.id,
|
|
new.pk()
|
|
);
|
|
}
|
|
}
|
|
Post { dry_run } => {
|
|
if opt.debug {
|
|
println!("post dry_run{:?}", dry_run);
|
|
}
|
|
|
|
let tx = db.transaction(TransactionBehavior::Exclusive).unwrap();
|
|
let mut input = String::new();
|
|
std::io::stdin().read_to_string(&mut input)?;
|
|
match Envelope::from_bytes(input.as_bytes(), None) {
|
|
Ok(env) => {
|
|
if opt.debug {
|
|
eprintln!("{:?}", &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()?;
|
|
}
|
|
FlushQueue { dry_run } => {
|
|
let tx = db.transaction(TransactionBehavior::Exclusive).unwrap();
|
|
let messages = if opt.debug {
|
|
println!("flush-queue dry_run {:?}", dry_run);
|
|
tx.queue(mailpot::queue::Queue::Out)?
|
|
.into_iter()
|
|
.map(DbVal::into_inner)
|
|
.chain(
|
|
tx.queue(mailpot::queue::Queue::Deferred)?
|
|
.into_iter()
|
|
.map(DbVal::into_inner),
|
|
)
|
|
.collect()
|
|
} else {
|
|
tx.delete_from_queue(mailpot::queue::Queue::Out, vec![])?
|
|
};
|
|
if opt.verbose > 0 || opt.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) -> Result<()> {
|
|
let mut child = std::process::Command::new("sh")
|
|
.arg("-c")
|
|
.arg(cmd)
|
|
.stdout(Stdio::piped())
|
|
.stdin(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.context("sh command failed to start")?;
|
|
let mut stdin = child.stdin.take().context("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"
|
|
))
|
|
})?;
|
|
Ok::<(), Error>(())
|
|
})?;
|
|
Ok(())
|
|
}
|
|
for msg in messages {
|
|
if let Err(err) = submit(&cmd, &msg) {
|
|
failures.push((err, msg));
|
|
}
|
|
}
|
|
}
|
|
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)?;
|
|
}
|
|
|
|
tx.commit()?;
|
|
}
|
|
ErrorQueue { cmd } => match cmd {
|
|
QueueCommand::List => {
|
|
let errors = db.queue(mailpot::queue::Queue::Error)?;
|
|
if errors.is_empty() {
|
|
println!("Error queue is empty.");
|
|
} else {
|
|
for e in errors {
|
|
println!(
|
|
"- {} {} {} {} {}",
|
|
e.pk, e.datetime, e.from_address, e.to_addresses, e.subject
|
|
);
|
|
}
|
|
}
|
|
}
|
|
QueueCommand::Print { index } => {
|
|
let mut errors = db.queue(mailpot::queue::Queue::Error)?;
|
|
if !index.is_empty() {
|
|
errors.retain(|el| index.contains(&el.pk()));
|
|
}
|
|
if errors.is_empty() {
|
|
println!("Error queue is empty.");
|
|
} else {
|
|
for e in errors {
|
|
println!("{e:?}");
|
|
}
|
|
}
|
|
}
|
|
QueueCommand::Delete { index, quiet } => {
|
|
let mut errors = db.queue(mailpot::queue::Queue::Error)?;
|
|
if !index.is_empty() {
|
|
errors.retain(|el| index.contains(&el.pk()));
|
|
}
|
|
if errors.is_empty() {
|
|
if !quiet {
|
|
println!("Error queue is empty.");
|
|
}
|
|
} else {
|
|
if !quiet {
|
|
println!("Deleting error queue elements {:?}", &index);
|
|
}
|
|
db.delete_from_queue(mailpot::queue::Queue::Error, index)?;
|
|
if !quiet {
|
|
for e in errors {
|
|
println!("{e:?}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
Queue { queue, cmd } => match cmd {
|
|
QueueCommand::List => {
|
|
let entries = db.queue(queue)?;
|
|
if entries.is_empty() {
|
|
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() {
|
|
println!("Queue {queue} is empty.");
|
|
} else {
|
|
for e in entries {
|
|
println!("{e:?}");
|
|
}
|
|
}
|
|
}
|
|
QueueCommand::Delete { index, quiet } => {
|
|
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:?}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
ImportMaildir {
|
|
list_id,
|
|
mut maildir_path,
|
|
} => {
|
|
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()
|
|
.expect("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::backends::maildir::MaildirType::list_mail_in_maildir_fs(maildir_path, true)?;
|
|
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)?);
|
|
buf.clear();
|
|
reader.read_to_end(&mut buf)?;
|
|
if let Ok(mut env) = Envelope::from_bytes(buf.as_slice(), Some(file.flags())) {
|
|
env.set_hash(hash);
|
|
db.insert_post(list.pk, &buf, &env)?;
|
|
ctr += 1;
|
|
}
|
|
}
|
|
println!("Inserted {} posts to {}.", ctr, list_id);
|
|
}
|
|
UpdatePostfixConfig {
|
|
master_cf,
|
|
config:
|
|
PostfixConfig {
|
|
user,
|
|
group,
|
|
binary_path,
|
|
process_limit,
|
|
map_output_path,
|
|
transport_name,
|
|
},
|
|
} => {
|
|
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())?;
|
|
pfconf.save_master_cf_entry(db.conf(), config_path, master_cf.as_deref())?;
|
|
}
|
|
PrintPostfixConfig {
|
|
config:
|
|
PostfixConfig {
|
|
user,
|
|
group,
|
|
binary_path,
|
|
process_limit,
|
|
map_output_path,
|
|
transport_name,
|
|
},
|
|
} => {
|
|
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()?;
|
|
let lists_post_policies = lists
|
|
.into_iter()
|
|
.map(|l| {
|
|
let pk = l.pk;
|
|
Ok((l, db.list_post_policy(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");
|
|
}
|
|
Accounts => {
|
|
let accounts = db.accounts()?;
|
|
if accounts.is_empty() {
|
|
println!("No accounts found.");
|
|
} else {
|
|
for a in accounts {
|
|
println!("- {:?}", a);
|
|
}
|
|
}
|
|
}
|
|
AccountInfo { address } => {
|
|
if let Some(acc) = db.account_by_address(&address)? {
|
|
let subs = db.account_subscriptions(acc.pk())?;
|
|
if subs.is_empty() {
|
|
println!("No subscriptions found.");
|
|
} else {
|
|
for s in subs {
|
|
let list = db
|
|
.list(s.list)
|
|
.unwrap_or_else(|err| {
|
|
panic!(
|
|
"Found subscription with list_pk = {} but no such list \
|
|
exists.\nListSubscription = {:?}\n\n{err}",
|
|
s.list, s
|
|
)
|
|
})
|
|
.unwrap_or_else(|| {
|
|
panic!(
|
|
"Found subscription with list_pk = {} but no such list \
|
|
exists.\nListSubscription = {:?}",
|
|
s.list, s
|
|
)
|
|
});
|
|
println!("- {:?} {}", s, list);
|
|
}
|
|
}
|
|
} else {
|
|
println!("account with this address not found!");
|
|
};
|
|
}
|
|
AddAccount {
|
|
address,
|
|
password,
|
|
name,
|
|
public_key,
|
|
enabled,
|
|
} => {
|
|
db.add_account(Account {
|
|
pk: 0,
|
|
name,
|
|
address,
|
|
public_key,
|
|
password,
|
|
enabled: enabled.unwrap_or(true),
|
|
})?;
|
|
}
|
|
RemoveAccount { address } => {
|
|
let mut input = String::new();
|
|
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)?;
|
|
}
|
|
UpdateAccount {
|
|
address,
|
|
password,
|
|
name,
|
|
public_key,
|
|
enabled,
|
|
} => {
|
|
let changeset = AccountChangeset {
|
|
address,
|
|
name,
|
|
public_key,
|
|
password,
|
|
enabled,
|
|
};
|
|
db.update_account(changeset)?;
|
|
}
|
|
Repair {
|
|
fix,
|
|
all,
|
|
mut datetime_header_value,
|
|
mut remove_empty_accounts,
|
|
mut remove_accepted_subscription_requests,
|
|
mut warn_list_no_owner,
|
|
} => {
|
|
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 (flag, lint_fn) in [
|
|
(datetime_header_value, datetime_header_value_lint as LintFn),
|
|
(remove_empty_accounts, remove_empty_accounts_lint as _),
|
|
(
|
|
remove_accepted_subscription_requests,
|
|
remove_accepted_subscription_requests_lint as _,
|
|
),
|
|
(warn_list_no_owner, warn_list_no_owner_lint as _),
|
|
] {
|
|
if flag {
|
|
lint_fn(&mut db, dry_run)?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn main() -> std::result::Result<(), i32> {
|
|
let opt = Opt::parse();
|
|
stderrlog::new()
|
|
.module(module_path!())
|
|
.module("mailpot")
|
|
.quiet(opt.quiet)
|
|
.verbosity(opt.verbose as usize)
|
|
.timestamp(opt.ts.unwrap_or(stderrlog::Timestamp::Off))
|
|
.init()
|
|
.unwrap();
|
|
if let Err(err) = run_app(opt) {
|
|
print!("{}", err.display_chain());
|
|
std::process::exit(-1);
|
|
}
|
|
Ok(())
|
|
}
|