Author: Manos Pitsidianakis [manos@pitsidianak.is]
Hash: 6f13cd1e31216c4faae11719689f72b791f12081
Timestamp: Mon, 30 Oct 2023 22:36:42 +0000 (10 months ago)

+1358 -1026 +/-10 browse
core: split commands in their own module
core: split commands in their own module

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
1diff --git a/cli/src/args.rs b/cli/src/args.rs
2index e1da086..57211f8 100644
3--- a/cli/src/args.rs
4+++ b/cli/src/args.rs
5 @@ -102,12 +102,7 @@ pub enum Command {
6 #[arg(long)]
7 dry_run: bool,
8 },
9- /// Mail that has not been handled properly end up in the error queue.
10- ErrorQueue {
11- #[command(subcommand)]
12- cmd: QueueCommand,
13- },
14- /// Mail that has not been handled properly end up in the error queue.
15+ /// Processed mail is stored in queues.
16 Queue {
17 #[arg(long, value_parser = QueueValueParser)]
18 queue: mailpot::queue::Queue,
19 @@ -275,9 +270,6 @@ pub enum QueueCommand {
20 /// index of entry.
21 #[arg(long)]
22 index: Vec<i64>,
23- /// Do not print in stdout.
24- #[arg(long)]
25- quiet: bool,
26 },
27 }
28
29 diff --git a/cli/src/commands.rs b/cli/src/commands.rs
30new file mode 100644
31index 0000000..8aebbf3
32--- /dev/null
33+++ b/cli/src/commands.rs
34 @@ -0,0 +1,1083 @@
35+ /*
36+ * This file is part of mailpot
37+ *
38+ * Copyright 2020 - Manos Pitsidianakis
39+ *
40+ * This program is free software: you can redistribute it and/or modify
41+ * it under the terms of the GNU Affero General Public License as
42+ * published by the Free Software Foundation, either version 3 of the
43+ * License, or (at your option) any later version.
44+ *
45+ * This program is distributed in the hope that it will be useful,
46+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
47+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
48+ * GNU Affero General Public License for more details.
49+ *
50+ * You should have received a copy of the GNU Affero General Public License
51+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
52+ */
53+
54+ use std::{
55+ collections::hash_map::DefaultHasher,
56+ hash::{Hash, Hasher},
57+ io::{Read, Write},
58+ path::{Path, PathBuf},
59+ process::Stdio,
60+ };
61+
62+ use mailpot::{
63+ melib,
64+ melib::{backends::maildir::MaildirPathTrait, smol, Envelope, EnvelopeHash},
65+ models::{changesets::*, *},
66+ queue::{Queue, QueueEntry},
67+ transaction::TransactionBehavior,
68+ Connection, Context, Error, ErrorKind, Result,
69+ };
70+
71+ use crate::{lints::*, *};
72+
73+ macro_rules! list {
74+ ($db:ident, $list_id:expr) => {{
75+ $db.list_by_id(&$list_id)?.or_else(|| {
76+ $list_id
77+ .parse::<i64>()
78+ .ok()
79+ .map(|pk| $db.list(pk).ok())
80+ .flatten()
81+ .flatten()
82+ })
83+ }};
84+ }
85+
86+ macro_rules! string_opts {
87+ ($field:ident) => {
88+ if $field.as_deref().map(str::is_empty).unwrap_or(false) {
89+ None
90+ } else {
91+ Some($field)
92+ }
93+ };
94+ }
95+
96+ pub fn dump_database(db: &mut Connection) -> Result<()> {
97+ let lists = db.lists()?;
98+ let mut stdout = std::io::stdout();
99+ serde_json::to_writer_pretty(&mut stdout, &lists)?;
100+ for l in &lists {
101+ serde_json::to_writer_pretty(
102+ &mut stdout,
103+ &db.list_subscriptions(l.pk)
104+ .context("Could not retrieve list subscriptions.")?,
105+ )?;
106+ }
107+ Ok(())
108+ }
109+
110+ pub fn list_lists(db: &mut Connection) -> Result<()> {
111+ let lists = db.lists().context("Could not retrieve lists.")?;
112+ if lists.is_empty() {
113+ println!("No lists found.");
114+ } else {
115+ for l in lists {
116+ println!("- {} {:?}", l.id, l);
117+ let list_owners = db
118+ .list_owners(l.pk)
119+ .context("Could not retrieve list owners.")?;
120+ if list_owners.is_empty() {
121+ println!("\tList owners: None");
122+ } else {
123+ println!("\tList owners:");
124+ for o in list_owners {
125+ println!("\t- {}", o);
126+ }
127+ }
128+ if let Some(s) = db
129+ .list_post_policy(l.pk)
130+ .context("Could not retrieve list post policy.")?
131+ {
132+ println!("\tPost policy: {}", s);
133+ } else {
134+ println!("\tPost policy: None");
135+ }
136+ if let Some(s) = db
137+ .list_subscription_policy(l.pk)
138+ .context("Could not retrieve list subscription policy.")?
139+ {
140+ println!("\tSubscription policy: {}", s);
141+ } else {
142+ println!("\tSubscription policy: None");
143+ }
144+ println!();
145+ }
146+ }
147+ Ok(())
148+ }
149+
150+ pub fn list(db: &mut Connection, list_id: &str, cmd: ListCommand, quiet: bool) -> Result<()> {
151+ let list = match list!(db, list_id) {
152+ Some(v) => v,
153+ None => {
154+ return Err(format!("No list with id or pk {} was found", list_id).into());
155+ }
156+ };
157+ use ListCommand::*;
158+ match cmd {
159+ Subscriptions => {
160+ let subscriptions = db.list_subscriptions(list.pk)?;
161+ if subscriptions.is_empty() {
162+ if !quiet {
163+ println!("No subscriptions found.");
164+ }
165+ } else {
166+ if !quiet {
167+ println!("Subscriptions of list {}", list.id);
168+ }
169+ for l in subscriptions {
170+ println!("- {}", &l);
171+ }
172+ }
173+ }
174+ AddSubscription {
175+ address,
176+ subscription_options:
177+ SubscriptionOptions {
178+ name,
179+ digest,
180+ hide_address,
181+ receive_duplicates,
182+ receive_own_posts,
183+ receive_confirmation,
184+ enabled,
185+ verified,
186+ },
187+ } => {
188+ db.add_subscription(
189+ list.pk,
190+ ListSubscription {
191+ pk: 0,
192+ list: list.pk,
193+ address,
194+ account: None,
195+ name,
196+ digest: digest.unwrap_or(false),
197+ hide_address: hide_address.unwrap_or(false),
198+ receive_confirmation: receive_confirmation.unwrap_or(true),
199+ receive_duplicates: receive_duplicates.unwrap_or(true),
200+ receive_own_posts: receive_own_posts.unwrap_or(false),
201+ enabled: enabled.unwrap_or(true),
202+ verified: verified.unwrap_or(false),
203+ },
204+ )?;
205+ }
206+ RemoveSubscription { address } => {
207+ let mut input = String::new();
208+ loop {
209+ println!(
210+ "Are you sure you want to remove subscription of {} from list {}? [Yy/n]",
211+ address, list
212+ );
213+ input.clear();
214+ std::io::stdin().read_line(&mut input)?;
215+ if input.trim() == "Y" || input.trim() == "y" || input.trim() == "" {
216+ break;
217+ } else if input.trim() == "n" {
218+ return Ok(());
219+ }
220+ }
221+
222+ db.remove_subscription(list.pk, &address)?;
223+ }
224+ Health => {
225+ if !quiet {
226+ println!("{} health:", list);
227+ }
228+ let list_owners = db
229+ .list_owners(list.pk)
230+ .context("Could not retrieve list owners.")?;
231+ let post_policy = db
232+ .list_post_policy(list.pk)
233+ .context("Could not retrieve list post policy.")?;
234+ let subscription_policy = db
235+ .list_subscription_policy(list.pk)
236+ .context("Could not retrieve list subscription policy.")?;
237+ if list_owners.is_empty() {
238+ println!("\tList has no owners: you should add at least one.");
239+ } else {
240+ for owner in list_owners {
241+ println!("\tList owner: {}.", owner);
242+ }
243+ }
244+ if let Some(p) = post_policy {
245+ println!("\tList has post policy: {p}.");
246+ } else {
247+ println!("\tList has no post policy: you should add one.");
248+ }
249+ if let Some(p) = subscription_policy {
250+ println!("\tList has subscription policy: {p}.");
251+ } else {
252+ println!("\tList has no subscription policy: you should add one.");
253+ }
254+ }
255+ Info => {
256+ println!("{} info:", list);
257+ let list_owners = db
258+ .list_owners(list.pk)
259+ .context("Could not retrieve list owners.")?;
260+ let post_policy = db
261+ .list_post_policy(list.pk)
262+ .context("Could not retrieve list post policy.")?;
263+ let subscription_policy = db
264+ .list_subscription_policy(list.pk)
265+ .context("Could not retrieve list subscription policy.")?;
266+ let subscriptions = db
267+ .list_subscriptions(list.pk)
268+ .context("Could not retrieve list subscriptions.")?;
269+ if subscriptions.is_empty() {
270+ println!("No subscriptions.");
271+ } else if subscriptions.len() == 1 {
272+ println!("1 subscription.");
273+ } else {
274+ println!("{} subscriptions.", subscriptions.len());
275+ }
276+ if list_owners.is_empty() {
277+ println!("List owners: None");
278+ } else {
279+ println!("List owners:");
280+ for o in list_owners {
281+ println!("\t- {}", o);
282+ }
283+ }
284+ if let Some(s) = post_policy {
285+ println!("Post policy: {s}");
286+ } else {
287+ println!("Post policy: None");
288+ }
289+ if let Some(s) = subscription_policy {
290+ println!("Subscription policy: {s}");
291+ } else {
292+ println!("Subscription policy: None");
293+ }
294+ }
295+ UpdateSubscription {
296+ address,
297+ subscription_options:
298+ SubscriptionOptions {
299+ name,
300+ digest,
301+ hide_address,
302+ receive_duplicates,
303+ receive_own_posts,
304+ receive_confirmation,
305+ enabled,
306+ verified,
307+ },
308+ } => {
309+ let name = if name
310+ .as_ref()
311+ .map(|s: &String| s.is_empty())
312+ .unwrap_or(false)
313+ {
314+ None
315+ } else {
316+ Some(name)
317+ };
318+ let changeset = ListSubscriptionChangeset {
319+ list: list.pk,
320+ address,
321+ account: None,
322+ name,
323+ digest,
324+ verified,
325+ hide_address,
326+ receive_duplicates,
327+ receive_own_posts,
328+ receive_confirmation,
329+ enabled,
330+ };
331+ db.update_subscription(changeset)?;
332+ }
333+ AddPostPolicy {
334+ announce_only,
335+ subscription_only,
336+ approval_needed,
337+ open,
338+ custom,
339+ } => {
340+ let policy = PostPolicy {
341+ pk: 0,
342+ list: list.pk,
343+ announce_only,
344+ subscription_only,
345+ approval_needed,
346+ open,
347+ custom,
348+ };
349+ let new_val = db.set_list_post_policy(policy)?;
350+ println!("Added new policy with pk = {}", new_val.pk());
351+ }
352+ RemovePostPolicy { pk } => {
353+ db.remove_list_post_policy(list.pk, pk)?;
354+ println!("Removed policy with pk = {}", pk);
355+ }
356+ AddSubscriptionPolicy {
357+ send_confirmation,
358+ open,
359+ manual,
360+ request,
361+ custom,
362+ } => {
363+ let policy = SubscriptionPolicy {
364+ pk: 0,
365+ list: list.pk,
366+ send_confirmation,
367+ open,
368+ manual,
369+ request,
370+ custom,
371+ };
372+ let new_val = db.set_list_subscription_policy(policy)?;
373+ println!("Added new subscribe policy with pk = {}", new_val.pk());
374+ }
375+ RemoveSubscriptionPolicy { pk } => {
376+ db.remove_list_subscription_policy(list.pk, pk)?;
377+ println!("Removed subscribe policy with pk = {}", pk);
378+ }
379+ AddListOwner { address, name } => {
380+ let list_owner = ListOwner {
381+ pk: 0,
382+ list: list.pk,
383+ address,
384+ name,
385+ };
386+ let new_val = db.add_list_owner(list_owner)?;
387+ println!("Added new list owner {}", new_val);
388+ }
389+ RemoveListOwner { pk } => {
390+ db.remove_list_owner(list.pk, pk)?;
391+ println!("Removed list owner with pk = {}", pk);
392+ }
393+ EnableSubscription { address } => {
394+ let changeset = ListSubscriptionChangeset {
395+ list: list.pk,
396+ address,
397+ account: None,
398+ name: None,
399+ digest: None,
400+ verified: None,
401+ enabled: Some(true),
402+ hide_address: None,
403+ receive_duplicates: None,
404+ receive_own_posts: None,
405+ receive_confirmation: None,
406+ };
407+ db.update_subscription(changeset)?;
408+ }
409+ DisableSubscription { address } => {
410+ let changeset = ListSubscriptionChangeset {
411+ list: list.pk,
412+ address,
413+ account: None,
414+ name: None,
415+ digest: None,
416+ enabled: Some(false),
417+ verified: None,
418+ hide_address: None,
419+ receive_duplicates: None,
420+ receive_own_posts: None,
421+ receive_confirmation: None,
422+ };
423+ db.update_subscription(changeset)?;
424+ }
425+ Update {
426+ name,
427+ id,
428+ address,
429+ description,
430+ archive_url,
431+ owner_local_part,
432+ request_local_part,
433+ verify,
434+ hidden,
435+ enabled,
436+ } => {
437+ let description = string_opts!(description);
438+ let archive_url = string_opts!(archive_url);
439+ let owner_local_part = string_opts!(owner_local_part);
440+ let request_local_part = string_opts!(request_local_part);
441+ let changeset = MailingListChangeset {
442+ pk: list.pk,
443+ name,
444+ id,
445+ address,
446+ description,
447+ archive_url,
448+ owner_local_part,
449+ request_local_part,
450+ verify,
451+ hidden,
452+ enabled,
453+ };
454+ db.update_list(changeset)?;
455+ }
456+ ImportMembers {
457+ url,
458+ username,
459+ password,
460+ list_id,
461+ dry_run,
462+ skip_owners,
463+ } => {
464+ let conn = import::Mailman3Connection::new(&url, &username, &password).unwrap();
465+ if dry_run {
466+ let entries = conn.users(&list_id).unwrap();
467+ println!("{} result(s)", entries.len());
468+ for e in entries {
469+ println!(
470+ "{}{}<{}>",
471+ if let Some(n) = e.display_name() {
472+ n
473+ } else {
474+ ""
475+ },
476+ if e.display_name().is_none() { "" } else { " " },
477+ e.email()
478+ );
479+ }
480+ if !skip_owners {
481+ let entries = conn.owners(&list_id).unwrap();
482+ println!("\nOwners: {} result(s)", entries.len());
483+ for e in entries {
484+ println!(
485+ "{}{}<{}>",
486+ if let Some(n) = e.display_name() {
487+ n
488+ } else {
489+ ""
490+ },
491+ if e.display_name().is_none() { "" } else { " " },
492+ e.email()
493+ );
494+ }
495+ }
496+ } else {
497+ let entries = conn.users(&list_id).unwrap();
498+ let tx = db.transaction(Default::default()).unwrap();
499+ for sub in entries.into_iter().map(|e| e.into_subscription(list.pk)) {
500+ tx.add_subscription(list.pk, sub)?;
501+ }
502+ if !skip_owners {
503+ let entries = conn.owners(&list_id).unwrap();
504+ for sub in entries.into_iter().map(|e| e.into_owner(list.pk)) {
505+ tx.add_list_owner(sub)?;
506+ }
507+ }
508+ tx.commit()?;
509+ }
510+ }
511+ SubscriptionRequests => {
512+ let subscriptions = db.list_subscription_requests(list.pk)?;
513+ if subscriptions.is_empty() {
514+ println!("No subscription requests found.");
515+ } else {
516+ println!("Subscription requests of list {}", list.id);
517+ for l in subscriptions {
518+ println!("- {}", &l);
519+ }
520+ }
521+ }
522+ AcceptSubscriptionRequest {
523+ pk,
524+ do_not_send_confirmation,
525+ } => match db.accept_candidate_subscription(pk) {
526+ Ok(subscription) => {
527+ println!("Added: {subscription:#?}");
528+ if !do_not_send_confirmation {
529+ if let Err(err) = db
530+ .list(subscription.list)
531+ .and_then(|v| match v {
532+ Some(v) => Ok(v),
533+ None => Err(format!(
534+ "No list with id or pk {} was found",
535+ subscription.list
536+ )
537+ .into()),
538+ })
539+ .and_then(|list| {
540+ db.send_subscription_confirmation(&list, &subscription.address())
541+ })
542+ {
543+ eprintln!("Could not send subscription confirmation!");
544+ return Err(err);
545+ }
546+ println!("Sent confirmation e-mail to {}", subscription.address());
547+ } else {
548+ println!(
549+ "Did not sent confirmation e-mail to {}. You can do it manually with the \
550+ appropriate command.",
551+ subscription.address()
552+ );
553+ }
554+ }
555+ Err(err) => {
556+ eprintln!("Could not accept subscription request!");
557+ return Err(err);
558+ }
559+ },
560+ SendConfirmationForSubscription { pk } => {
561+ let req = match db.candidate_subscription(pk) {
562+ Ok(req) => req,
563+ Err(err) => {
564+ eprintln!("Could not find subscription request by that pk!");
565+
566+ return Err(err);
567+ }
568+ };
569+ log::info!("Found {:#?}", req);
570+ if req.accepted.is_none() {
571+ return Err("Request has not been accepted!".into());
572+ }
573+ if let Err(err) = db
574+ .list(req.list)
575+ .and_then(|v| match v {
576+ Some(v) => Ok(v),
577+ None => Err(format!("No list with id or pk {} was found", req.list).into()),
578+ })
579+ .and_then(|list| db.send_subscription_confirmation(&list, &req.address()))
580+ {
581+ eprintln!("Could not send subscription request confirmation!");
582+ return Err(err);
583+ }
584+
585+ println!("Sent confirmation e-mail to {}", req.address());
586+ }
587+ }
588+ Ok(())
589+ }
590+
591+ pub fn create_list(
592+ db: &mut Connection,
593+ name: String,
594+ id: String,
595+ address: String,
596+ description: Option<String>,
597+ archive_url: Option<String>,
598+ quiet: bool,
599+ ) -> Result<()> {
600+ let new = db.create_list(MailingList {
601+ pk: 0,
602+ name,
603+ id,
604+ description,
605+ topics: vec![],
606+ address,
607+ archive_url,
608+ })?;
609+ log::trace!("created new list {:#?}", new);
610+ if !quiet {
611+ println!(
612+ "Created new list {:?} with primary key {}",
613+ new.id,
614+ new.pk()
615+ );
616+ }
617+ Ok(())
618+ }
619+
620+ pub fn post(db: &mut Connection, dry_run: bool, debug: bool) -> Result<()> {
621+ if debug {
622+ println!("Post dry_run = {:?}", dry_run);
623+ }
624+
625+ let tx = db
626+ .transaction(TransactionBehavior::Exclusive)
627+ .context("Could not open Exclusive transaction in database.")?;
628+ let mut input = String::new();
629+ std::io::stdin()
630+ .read_to_string(&mut input)
631+ .context("Could not read from stdin")?;
632+ match Envelope::from_bytes(input.as_bytes(), None) {
633+ Ok(env) => {
634+ if debug {
635+ eprintln!("Parsed envelope is:\n{:?}", &env);
636+ }
637+ tx.post(&env, input.as_bytes(), dry_run)?;
638+ }
639+ Err(err) if input.trim().is_empty() => {
640+ eprintln!("Empty input, abort.");
641+ return Err(err.into());
642+ }
643+ Err(err) => {
644+ eprintln!("Could not parse message: {}", err);
645+ let p = tx.conf().save_message(input)?;
646+ eprintln!("Message saved at {}", p.display());
647+ return Err(err.into());
648+ }
649+ }
650+ tx.commit()
651+ }
652+
653+ pub fn flush_queue(db: &mut Connection, dry_run: bool, verbose: u8, debug: bool) -> Result<()> {
654+ let tx = db
655+ .transaction(TransactionBehavior::Exclusive)
656+ .context("Could not open Exclusive transaction in database.")?;
657+ let messages = tx.delete_from_queue(mailpot::queue::Queue::Out, vec![])?;
658+ if verbose > 0 || debug {
659+ println!("Queue out has {} messages.", messages.len());
660+ }
661+
662+ let mut failures = Vec::with_capacity(messages.len());
663+
664+ let send_mail = tx.conf().send_mail.clone();
665+ match send_mail {
666+ mailpot::SendMail::ShellCommand(cmd) => {
667+ fn submit(cmd: &str, msg: &QueueEntry, dry_run: bool) -> Result<()> {
668+ if dry_run {
669+ return Ok(());
670+ }
671+ let mut child = std::process::Command::new("sh")
672+ .arg("-c")
673+ .arg(cmd)
674+ .stdout(Stdio::piped())
675+ .stdin(Stdio::piped())
676+ .stderr(Stdio::piped())
677+ .spawn()
678+ .context("sh command failed to start")?;
679+ let mut stdin = child
680+ .stdin
681+ .take()
682+ .ok_or_else(|| Error::from("Failed to open stdin"))?;
683+
684+ let builder = std::thread::Builder::new();
685+
686+ std::thread::scope(|s| {
687+ let handler = builder
688+ .spawn_scoped(s, move || {
689+ stdin
690+ .write_all(&msg.message)
691+ .expect("Failed to write to stdin");
692+ })
693+ .context(
694+ "Could not spawn IPC communication thread for SMTP ShellCommand \
695+ process",
696+ )?;
697+
698+ handler.join().map_err(|_| {
699+ ErrorKind::External(mailpot::anyhow::anyhow!(
700+ "Could not join with IPC communication thread for SMTP ShellCommand \
701+ process"
702+ ))
703+ })?;
704+ Ok::<(), Error>(())
705+ })?;
706+ Ok(())
707+ }
708+ for msg in messages {
709+ if let Err(err) = submit(&cmd, &msg, dry_run) {
710+ if verbose > 0 || debug {
711+ eprintln!("Message {msg:?} failed with: {err}.");
712+ }
713+ failures.push((err, msg));
714+ } else if verbose > 0 || debug {
715+ eprintln!("Submitted message {}", msg.message_id);
716+ }
717+ }
718+ }
719+ mailpot::SendMail::Smtp(_) => {
720+ let conn_future = tx.new_smtp_connection()?;
721+ failures = smol::future::block_on(smol::spawn(async move {
722+ let mut conn = conn_future.await?;
723+ for msg in messages {
724+ if let Err(err) = Connection::submit(&mut conn, &msg, dry_run).await {
725+ failures.push((err, msg));
726+ }
727+ }
728+ Ok::<_, Error>(failures)
729+ }))?;
730+ }
731+ }
732+
733+ for (err, mut msg) in failures {
734+ log::error!("Message {msg:?} failed with: {err}. Inserting to Deferred queue.");
735+
736+ msg.queue = mailpot::queue::Queue::Deferred;
737+ tx.insert_to_queue(msg)?;
738+ }
739+
740+ if !dry_run {
741+ tx.commit()?;
742+ }
743+ Ok(())
744+ }
745+
746+ pub fn queue_(db: &mut Connection, queue: Queue, cmd: QueueCommand, quiet: bool) -> Result<()> {
747+ match cmd {
748+ QueueCommand::List => {
749+ let entries = db.queue(queue)?;
750+ if entries.is_empty() {
751+ if !quiet {
752+ println!("Queue {queue} is empty.");
753+ }
754+ } else {
755+ for e in entries {
756+ println!(
757+ "- {} {} {} {} {}",
758+ e.pk, e.datetime, e.from_address, e.to_addresses, e.subject
759+ );
760+ }
761+ }
762+ }
763+ QueueCommand::Print { index } => {
764+ let mut entries = db.queue(queue)?;
765+ if !index.is_empty() {
766+ entries.retain(|el| index.contains(&el.pk()));
767+ }
768+ if entries.is_empty() {
769+ if !quiet {
770+ println!("Queue {queue} is empty.");
771+ }
772+ } else {
773+ for e in entries {
774+ println!("{e:?}");
775+ }
776+ }
777+ }
778+ QueueCommand::Delete { index } => {
779+ let mut entries = db.queue(queue)?;
780+ if !index.is_empty() {
781+ entries.retain(|el| index.contains(&el.pk()));
782+ }
783+ if entries.is_empty() {
784+ if !quiet {
785+ println!("Queue {queue} is empty.");
786+ }
787+ } else {
788+ if !quiet {
789+ println!("Deleting queue {queue} elements {:?}", &index);
790+ }
791+ db.delete_from_queue(queue, index)?;
792+ if !quiet {
793+ for e in entries {
794+ println!("{e:?}");
795+ }
796+ }
797+ }
798+ }
799+ }
800+ Ok(())
801+ }
802+
803+ pub fn import_maildir(
804+ db: &mut Connection,
805+ list_id: &str,
806+ mut maildir_path: PathBuf,
807+ quiet: bool,
808+ debug: bool,
809+ verbose: u8,
810+ ) -> Result<()> {
811+ let list = match list!(db, list_id) {
812+ Some(v) => v,
813+ None => {
814+ return Err(format!("No list with id or pk {} was found", list_id).into());
815+ }
816+ };
817+ if !maildir_path.is_absolute() {
818+ maildir_path = std::env::current_dir()
819+ .context("could not detect current directory")?
820+ .join(&maildir_path);
821+ }
822+
823+ fn get_file_hash(file: &std::path::Path) -> EnvelopeHash {
824+ let mut hasher = DefaultHasher::default();
825+ file.hash(&mut hasher);
826+ EnvelopeHash(hasher.finish())
827+ }
828+ let mut buf = Vec::with_capacity(4096);
829+ let files = melib::backends::maildir::MaildirType::list_mail_in_maildir_fs(maildir_path, true)
830+ .context("Could not parse files in maildir path")?;
831+ let mut ctr = 0;
832+ for file in files {
833+ let hash = get_file_hash(&file);
834+ let mut reader = std::io::BufReader::new(
835+ std::fs::File::open(&file)
836+ .with_context(|| format!("Could not open {}.", file.display()))?,
837+ );
838+ buf.clear();
839+ reader
840+ .read_to_end(&mut buf)
841+ .with_context(|| format!("Could not read from {}.", file.display()))?;
842+ match Envelope::from_bytes(buf.as_slice(), Some(file.flags())) {
843+ Ok(mut env) => {
844+ env.set_hash(hash);
845+ if verbose > 1 {
846+ println!(
847+ "Inserting post from {:?} with subject `{}` and Message-ID `{}`.",
848+ env.from(),
849+ env.subject(),
850+ env.message_id()
851+ );
852+ }
853+ db.insert_post(list.pk, &buf, &env).with_context(|| {
854+ format!(
855+ "Could not insert post `{}` from path `{}`",
856+ env.message_id(),
857+ file.display()
858+ )
859+ })?;
860+ ctr += 1;
861+ }
862+ Err(err) => {
863+ if verbose > 0 || debug {
864+ log::error!(
865+ "Could not parse Envelope from file {}: {err}",
866+ file.display()
867+ );
868+ }
869+ }
870+ }
871+ }
872+ if !quiet {
873+ println!("Inserted {} posts to {}.", ctr, list_id);
874+ }
875+ Ok(())
876+ }
877+
878+ pub fn update_postfix_config(
879+ config_path: &Path,
880+ db: &mut Connection,
881+ master_cf: Option<PathBuf>,
882+ PostfixConfig {
883+ user,
884+ group,
885+ binary_path,
886+ process_limit,
887+ map_output_path,
888+ transport_name,
889+ }: PostfixConfig,
890+ ) -> Result<()> {
891+ let pfconf = mailpot::postfix::PostfixConfiguration {
892+ user: user.into(),
893+ group: group.map(Into::into),
894+ binary_path,
895+ process_limit,
896+ map_output_path,
897+ transport_name: transport_name.map(std::borrow::Cow::from),
898+ };
899+ pfconf
900+ .save_maps(db.conf())
901+ .context("Could not save maps.")?;
902+ pfconf
903+ .save_master_cf_entry(db.conf(), config_path, master_cf.as_deref())
904+ .context("Could not save master.cf file.")?;
905+
906+ Ok(())
907+ }
908+
909+ pub fn print_postfix_config(
910+ config_path: &Path,
911+ db: &mut Connection,
912+ PostfixConfig {
913+ user,
914+ group,
915+ binary_path,
916+ process_limit,
917+ map_output_path,
918+ transport_name,
919+ }: PostfixConfig,
920+ ) -> Result<()> {
921+ let pfconf = mailpot::postfix::PostfixConfiguration {
922+ user: user.into(),
923+ group: group.map(Into::into),
924+ binary_path,
925+ process_limit,
926+ map_output_path,
927+ transport_name: transport_name.map(std::borrow::Cow::from),
928+ };
929+ let lists = db.lists().context("Could not retrieve lists.")?;
930+ let lists_post_policies = lists
931+ .into_iter()
932+ .map(|l| {
933+ let pk = l.pk;
934+ Ok((
935+ l,
936+ db.list_post_policy(pk).with_context(|| {
937+ format!("Could not retrieve list post policy for list_pk = {pk}.")
938+ })?,
939+ ))
940+ })
941+ .collect::<Result<Vec<(DbVal<MailingList>, Option<DbVal<PostPolicy>>)>>>()?;
942+ let maps = pfconf.generate_maps(&lists_post_policies);
943+ let mastercf = pfconf.generate_master_cf_entry(db.conf(), config_path);
944+
945+ println!("{maps}\n\n{mastercf}\n");
946+ Ok(())
947+ }
948+
949+ pub fn accounts(db: &mut Connection, quiet: bool) -> Result<()> {
950+ let accounts = db.accounts()?;
951+ if accounts.is_empty() {
952+ if !quiet {
953+ println!("No accounts found.");
954+ }
955+ } else {
956+ for a in accounts {
957+ println!("- {:?}", a);
958+ }
959+ }
960+ Ok(())
961+ }
962+
963+ pub fn account_info(db: &mut Connection, address: &str, quiet: bool) -> Result<()> {
964+ if let Some(acc) = db.account_by_address(address)? {
965+ let subs = db
966+ .account_subscriptions(acc.pk())
967+ .context("Could not retrieve account subscriptions for this account.")?;
968+ if subs.is_empty() {
969+ if !quiet {
970+ println!("No subscriptions found.");
971+ }
972+ } else {
973+ for s in subs {
974+ let list = db
975+ .list(s.list)
976+ .with_context(|| {
977+ format!(
978+ "Found subscription with list_pk = {} but could not retrieve the \
979+ list.\nListSubscription = {:?}",
980+ s.list, s
981+ )
982+ })?
983+ .ok_or_else(|| {
984+ format!(
985+ "Found subscription with list_pk = {} but no such list \
986+ exists.\nListSubscription = {:?}",
987+ s.list, s
988+ )
989+ })?;
990+ println!("- {:?} {}", s, list);
991+ }
992+ }
993+ } else {
994+ return Err(format!("Account with address {address} not found!").into());
995+ }
996+ Ok(())
997+ }
998+
999+ pub fn add_account(
1000+ db: &mut Connection,
1001+ address: String,
1002+ password: String,
1003+ name: Option<String>,
1004+ public_key: Option<String>,
1005+ enabled: Option<bool>,
1006+ ) -> Result<()> {
1007+ db.add_account(Account {
1008+ pk: 0,
1009+ name,
1010+ address,
1011+ public_key,
1012+ password,
1013+ enabled: enabled.unwrap_or(true),
1014+ })?;
1015+ Ok(())
1016+ }
1017+
1018+ pub fn remove_account(db: &mut Connection, address: &str, quiet: bool) -> Result<()> {
1019+ let mut input = String::new();
1020+ if !quiet {
1021+ loop {
1022+ println!(
1023+ "Are you sure you want to remove account with address {}? [Yy/n]",
1024+ address
1025+ );
1026+ input.clear();
1027+ std::io::stdin().read_line(&mut input)?;
1028+ if input.trim() == "Y" || input.trim() == "y" || input.trim() == "" {
1029+ break;
1030+ } else if input.trim() == "n" {
1031+ return Ok(());
1032+ }
1033+ }
1034+ }
1035+
1036+ db.remove_account(address)?;
1037+
1038+ Ok(())
1039+ }
1040+
1041+ pub fn update_account(
1042+ db: &mut Connection,
1043+ address: String,
1044+ password: Option<String>,
1045+ name: Option<Option<String>>,
1046+ public_key: Option<Option<String>>,
1047+ enabled: Option<Option<bool>>,
1048+ ) -> Result<()> {
1049+ let changeset = AccountChangeset {
1050+ address,
1051+ name,
1052+ public_key,
1053+ password,
1054+ enabled,
1055+ };
1056+ db.update_account(changeset)?;
1057+ Ok(())
1058+ }
1059+
1060+ pub fn repair(
1061+ db: &mut Connection,
1062+ fix: bool,
1063+ all: bool,
1064+ mut datetime_header_value: bool,
1065+ mut remove_empty_accounts: bool,
1066+ mut remove_accepted_subscription_requests: bool,
1067+ mut warn_list_no_owner: bool,
1068+ ) -> Result<()> {
1069+ type LintFn = fn(&'_ mut mailpot::Connection, bool) -> std::result::Result<(), mailpot::Error>;
1070+ let dry_run = !fix;
1071+ if all {
1072+ datetime_header_value = true;
1073+ remove_empty_accounts = true;
1074+ remove_accepted_subscription_requests = true;
1075+ warn_list_no_owner = true;
1076+ }
1077+
1078+ if !(datetime_header_value
1079+ | remove_empty_accounts
1080+ | remove_accepted_subscription_requests
1081+ | warn_list_no_owner)
1082+ {
1083+ return Err("No lints selected: specify them with flag arguments. See --help".into());
1084+ }
1085+
1086+ if dry_run {
1087+ println!("running without making modifications (dry run)");
1088+ }
1089+
1090+ for (name, flag, lint_fn) in [
1091+ (
1092+ stringify!(datetime_header_value),
1093+ datetime_header_value,
1094+ datetime_header_value_lint as LintFn,
1095+ ),
1096+ (
1097+ stringify!(remove_empty_accounts),
1098+ remove_empty_accounts,
1099+ remove_empty_accounts_lint as _,
1100+ ),
1101+ (
1102+ stringify!(remove_accepted_subscription_requests),
1103+ remove_accepted_subscription_requests,
1104+ remove_accepted_subscription_requests_lint as _,
1105+ ),
1106+ (
1107+ stringify!(warn_list_no_owner),
1108+ warn_list_no_owner,
1109+ warn_list_no_owner_lint as _,
1110+ ),
1111+ ] {
1112+ if flag {
1113+ lint_fn(db, dry_run).with_context(|| format!("Lint {name} failed."))?;
1114+ }
1115+ }
1116+ Ok(())
1117+ }
1118 diff --git a/cli/src/lib.rs b/cli/src/lib.rs
1119index 67aad61..597fcbd 100644
1120--- a/cli/src/lib.rs
1121+++ b/cli/src/lib.rs
1122 @@ -22,6 +22,8 @@ extern crate ureq;
1123 pub use std::path::PathBuf;
1124
1125 mod args;
1126+ pub mod commands;
1127 pub mod import;
1128+ pub mod lints;
1129 pub use args::*;
1130 pub use clap::{Args, CommandFactory, Parser, Subcommand};
1131 diff --git a/cli/src/lints.rs b/cli/src/lints.rs
1132index f4771ba..821f842 100644
1133--- a/cli/src/lints.rs
1134+++ b/cli/src/lints.rs
1135 @@ -17,7 +17,12 @@
1136 * along with this program. If not, see <https://www.gnu.org/licenses/>.
1137 */
1138
1139- use super::*;
1140+ use mailpot::{
1141+ chrono,
1142+ melib::{self, Envelope},
1143+ models::{Account, DbVal, ListSubscription, MailingList},
1144+ rusqlite, Connection, Result,
1145+ };
1146
1147 pub fn datetime_header_value_lint(db: &mut Connection, dry_run: bool) -> Result<()> {
1148 let mut col = vec![];
1149 diff --git a/cli/src/main.rs b/cli/src/main.rs
1150index 6e10a05..3b23746 100644
1151--- a/cli/src/main.rs
1152+++ b/cli/src/main.rs
1153 @@ -17,66 +17,31 @@
1154 * along with this program. If not, see <https://www.gnu.org/licenses/>.
1155 */
1156
1157- use std::{
1158- collections::hash_map::DefaultHasher,
1159- hash::{Hash, Hasher},
1160- io::{Read, Write},
1161- process::Stdio,
1162- };
1163-
1164- mod lints;
1165- use lints::*;
1166- use mailpot::{
1167- melib::{backends::maildir::MaildirPathTrait, smol, smtp::*, Envelope, EnvelopeHash},
1168- models::{changesets::*, *},
1169- queue::QueueEntry,
1170- transaction::TransactionBehavior,
1171- Configuration, Connection, Error, ErrorKind, Result, *,
1172- };
1173- use mailpot_cli::*;
1174-
1175- macro_rules! list {
1176- ($db:ident, $list_id:expr) => {{
1177- $db.list_by_id(&$list_id)?.or_else(|| {
1178- $list_id
1179- .parse::<i64>()
1180- .ok()
1181- .map(|pk| $db.list(pk).ok())
1182- .flatten()
1183- .flatten()
1184- })
1185- }};
1186- }
1187-
1188- macro_rules! string_opts {
1189- ($field:ident) => {
1190- if $field.as_deref().map(str::is_empty).unwrap_or(false) {
1191- None
1192- } else {
1193- Some($field)
1194- }
1195- };
1196- }
1197-
1198- fn run_app(opt: Opt) -> Result<()> {
1199- if opt.debug {
1200- println!("DEBUG: {:?}", &opt);
1201- }
1202- if let Command::SampleConfig { with_smtp } = opt.cmd {
1203+ use mailpot::{melib::smtp, Configuration, Connection, Context, Result};
1204+ use mailpot_cli::{commands::*, *};
1205+
1206+ fn run_app(
1207+ config: Option<PathBuf>,
1208+ cmd: Command,
1209+ debug: bool,
1210+ quiet: bool,
1211+ verbose: u8,
1212+ ) -> Result<()> {
1213+ if let Command::SampleConfig { with_smtp } = cmd {
1214 let mut new = Configuration::new("/path/to/sqlite.db");
1215 new.administrators.push("admin@example.com".to_string());
1216 if with_smtp {
1217- new.send_mail = mailpot::SendMail::Smtp(SmtpServerConf {
1218+ new.send_mail = mailpot::SendMail::Smtp(smtp::SmtpServerConf {
1219 hostname: "mail.example.com".to_string(),
1220 port: 587,
1221 envelope_from: "".to_string(),
1222- auth: SmtpAuth::Auto {
1223+ auth: smtp::SmtpAuth::Auto {
1224 username: "user".to_string(),
1225- password: Password::Raw("hunter2".to_string()),
1226- auth_type: SmtpAuthType::default(),
1227+ password: smtp::Password::Raw("hunter2".to_string()),
1228+ auth_type: smtp::SmtpAuthType::default(),
1229 require_auth: true,
1230 },
1231- security: SmtpSecurity::StartTLS {
1232+ security: smtp::SmtpSecurity::StartTLS {
1233 danger_accept_invalid_certs: false,
1234 },
1235 extensions: Default::default(),
1236 @@ -85,8 +50,8 @@ fn run_app(opt: Opt) -> Result<()> {
1237 println!("{}", new.to_toml());
1238 return Ok(());
1239 };
1240- let config_path = if let Some(path) = opt.config.as_ref() {
1241- path.as_path()
1242+ let config_path = if let Some(path) = config.as_deref() {
1243+ path
1244 } else {
1245 let mut opt = Opt::command();
1246 opt.error(
1247 @@ -96,474 +61,31 @@ fn run_app(opt: Opt) -> Result<()> {
1248 .exit();
1249 };
1250
1251- let config = Configuration::from_file(config_path)?;
1252+ let config = Configuration::from_file(config_path).with_context(|| {
1253+ format!(
1254+ "Could not read configuration file from path: {}",
1255+ config_path.display()
1256+ )
1257+ })?;
1258
1259 use Command::*;
1260- let mut db = Connection::open_or_create_db(config)?.trusted();
1261- match opt.cmd {
1262+ let mut db = Connection::open_or_create_db(config)
1263+ .context("Could not open database connection with this configuration")?
1264+ .trusted();
1265+ match cmd {
1266 SampleConfig { .. } => {}
1267 DumpDatabase => {
1268- let lists = db.lists()?;
1269- let mut stdout = std::io::stdout();
1270- serde_json::to_writer_pretty(&mut stdout, &lists)?;
1271- for l in &lists {
1272- serde_json::to_writer_pretty(&mut stdout, &db.list_subscriptions(l.pk)?)?;
1273- }
1274+ dump_database(&mut db).context("Could not dump database.")?;
1275 }
1276 ListLists => {
1277- let lists = db.lists()?;
1278- if lists.is_empty() {
1279- println!("No lists found.");
1280- } else {
1281- for l in lists {
1282- println!("- {} {:?}", l.id, l);
1283- let list_owners = db.list_owners(l.pk)?;
1284- if list_owners.is_empty() {
1285- println!("\tList owners: None");
1286- } else {
1287- println!("\tList owners:");
1288- for o in list_owners {
1289- println!("\t- {}", o);
1290- }
1291- }
1292- if let Some(s) = db.list_post_policy(l.pk)? {
1293- println!("\tPost policy: {}", s);
1294- } else {
1295- println!("\tPost policy: None");
1296- }
1297- if let Some(s) = db.list_subscription_policy(l.pk)? {
1298- println!("\tSubscription policy: {}", s);
1299- } else {
1300- println!("\tSubscription policy: None");
1301- }
1302- println!();
1303- }
1304- }
1305+ list_lists(&mut db).context("Could not retrieve mailing lists.")?;
1306 }
1307 List { list_id, cmd } => {
1308- let list = match list!(db, list_id) {
1309- Some(v) => v,
1310- None => {
1311- return Err(format!("No list with id or pk {} was found", list_id).into());
1312- }
1313- };
1314- use ListCommand::*;
1315- match cmd {
1316- Subscriptions => {
1317- let subscriptions = db.list_subscriptions(list.pk)?;
1318- if subscriptions.is_empty() {
1319- println!("No subscriptions found.");
1320- } else {
1321- println!("Subscriptions of list {}", list.id);
1322- for l in subscriptions {
1323- println!("- {}", &l);
1324- }
1325- }
1326- }
1327- AddSubscription {
1328- address,
1329- subscription_options:
1330- SubscriptionOptions {
1331- name,
1332- digest,
1333- hide_address,
1334- receive_duplicates,
1335- receive_own_posts,
1336- receive_confirmation,
1337- enabled,
1338- verified,
1339- },
1340- } => {
1341- db.add_subscription(
1342- list.pk,
1343- ListSubscription {
1344- pk: 0,
1345- list: list.pk,
1346- address,
1347- account: None,
1348- name,
1349- digest: digest.unwrap_or(false),
1350- hide_address: hide_address.unwrap_or(false),
1351- receive_confirmation: receive_confirmation.unwrap_or(true),
1352- receive_duplicates: receive_duplicates.unwrap_or(true),
1353- receive_own_posts: receive_own_posts.unwrap_or(false),
1354- enabled: enabled.unwrap_or(true),
1355- verified: verified.unwrap_or(false),
1356- },
1357- )?;
1358- }
1359- RemoveSubscription { address } => {
1360- let mut input = String::new();
1361- loop {
1362- println!(
1363- "Are you sure you want to remove subscription of {} from list {}? \
1364- [Yy/n]",
1365- address, list
1366- );
1367- input.clear();
1368- std::io::stdin().read_line(&mut input)?;
1369- if input.trim() == "Y" || input.trim() == "y" || input.trim() == "" {
1370- break;
1371- } else if input.trim() == "n" {
1372- return Ok(());
1373- }
1374- }
1375-
1376- db.remove_subscription(list.pk, &address)?;
1377- }
1378- Health => {
1379- println!("{} health:", list);
1380- let list_owners = db.list_owners(list.pk)?;
1381- let post_policy = db.list_post_policy(list.pk)?;
1382- let subscription_policy = db.list_subscription_policy(list.pk)?;
1383- if list_owners.is_empty() {
1384- println!("\tList has no owners: you should add at least one.");
1385- } else {
1386- for owner in list_owners {
1387- println!("\tList owner: {}.", owner);
1388- }
1389- }
1390- if let Some(p) = post_policy {
1391- println!("\tList has post policy: {p}.");
1392- } else {
1393- println!("\tList has no post policy: you should add one.");
1394- }
1395- if let Some(p) = subscription_policy {
1396- println!("\tList has subscription policy: {p}.");
1397- } else {
1398- println!("\tList has no subscription policy: you should add one.");
1399- }
1400- }
1401- Info => {
1402- println!("{} info:", list);
1403- let list_owners = db.list_owners(list.pk)?;
1404- let post_policy = db.list_post_policy(list.pk)?;
1405- let subscription_policy = db.list_subscription_policy(list.pk)?;
1406- let subscriptions = db.list_subscriptions(list.pk)?;
1407- if subscriptions.is_empty() {
1408- println!("No subscriptions.");
1409- } else if subscriptions.len() == 1 {
1410- println!("1 subscription.");
1411- } else {
1412- println!("{} subscriptions.", subscriptions.len());
1413- }
1414- if list_owners.is_empty() {
1415- println!("List owners: None");
1416- } else {
1417- println!("List owners:");
1418- for o in list_owners {
1419- println!("\t- {}", o);
1420- }
1421- }
1422- if let Some(s) = post_policy {
1423- println!("Post policy: {s}");
1424- } else {
1425- println!("Post policy: None");
1426- }
1427- if let Some(s) = subscription_policy {
1428- println!("Subscription policy: {s}");
1429- } else {
1430- println!("Subscription policy: None");
1431- }
1432- }
1433- UpdateSubscription {
1434- address,
1435- subscription_options:
1436- SubscriptionOptions {
1437- name,
1438- digest,
1439- hide_address,
1440- receive_duplicates,
1441- receive_own_posts,
1442- receive_confirmation,
1443- enabled,
1444- verified,
1445- },
1446- } => {
1447- let name = if name
1448- .as_ref()
1449- .map(|s: &String| s.is_empty())
1450- .unwrap_or(false)
1451- {
1452- None
1453- } else {
1454- Some(name)
1455- };
1456- let changeset = ListSubscriptionChangeset {
1457- list: list.pk,
1458- address,
1459- account: None,
1460- name,
1461- digest,
1462- verified,
1463- hide_address,
1464- receive_duplicates,
1465- receive_own_posts,
1466- receive_confirmation,
1467- enabled,
1468- };
1469- db.update_subscription(changeset)?;
1470- }
1471- AddPostPolicy {
1472- announce_only,
1473- subscription_only,
1474- approval_needed,
1475- open,
1476- custom,
1477- } => {
1478- let policy = PostPolicy {
1479- pk: 0,
1480- list: list.pk,
1481- announce_only,
1482- subscription_only,
1483- approval_needed,
1484- open,
1485- custom,
1486- };
1487- let new_val = db.set_list_post_policy(policy)?;
1488- println!("Added new policy with pk = {}", new_val.pk());
1489- }
1490- RemovePostPolicy { pk } => {
1491- db.remove_list_post_policy(list.pk, pk)?;
1492- println!("Removed policy with pk = {}", pk);
1493- }
1494- AddSubscriptionPolicy {
1495- send_confirmation,
1496- open,
1497- manual,
1498- request,
1499- custom,
1500- } => {
1501- let policy = SubscriptionPolicy {
1502- pk: 0,
1503- list: list.pk,
1504- send_confirmation,
1505- open,
1506- manual,
1507- request,
1508- custom,
1509- };
1510- let new_val = db.set_list_subscription_policy(policy)?;
1511- println!("Added new subscribe policy with pk = {}", new_val.pk());
1512- }
1513- RemoveSubscriptionPolicy { pk } => {
1514- db.remove_list_subscription_policy(list.pk, pk)?;
1515- println!("Removed subscribe policy with pk = {}", pk);
1516- }
1517- AddListOwner { address, name } => {
1518- let list_owner = ListOwner {
1519- pk: 0,
1520- list: list.pk,
1521- address,
1522- name,
1523- };
1524- let new_val = db.add_list_owner(list_owner)?;
1525- println!("Added new list owner {}", new_val);
1526- }
1527- RemoveListOwner { pk } => {
1528- db.remove_list_owner(list.pk, pk)?;
1529- println!("Removed list owner with pk = {}", pk);
1530- }
1531- EnableSubscription { address } => {
1532- let changeset = ListSubscriptionChangeset {
1533- list: list.pk,
1534- address,
1535- account: None,
1536- name: None,
1537- digest: None,
1538- verified: None,
1539- enabled: Some(true),
1540- hide_address: None,
1541- receive_duplicates: None,
1542- receive_own_posts: None,
1543- receive_confirmation: None,
1544- };
1545- db.update_subscription(changeset)?;
1546- }
1547- DisableSubscription { address } => {
1548- let changeset = ListSubscriptionChangeset {
1549- list: list.pk,
1550- address,
1551- account: None,
1552- name: None,
1553- digest: None,
1554- enabled: Some(false),
1555- verified: None,
1556- hide_address: None,
1557- receive_duplicates: None,
1558- receive_own_posts: None,
1559- receive_confirmation: None,
1560- };
1561- db.update_subscription(changeset)?;
1562- }
1563- Update {
1564- name,
1565- id,
1566- address,
1567- description,
1568- archive_url,
1569- owner_local_part,
1570- request_local_part,
1571- verify,
1572- hidden,
1573- enabled,
1574- } => {
1575- let description = string_opts!(description);
1576- let archive_url = string_opts!(archive_url);
1577- let owner_local_part = string_opts!(owner_local_part);
1578- let request_local_part = string_opts!(request_local_part);
1579- let changeset = MailingListChangeset {
1580- pk: list.pk,
1581- name,
1582- id,
1583- address,
1584- description,
1585- archive_url,
1586- owner_local_part,
1587- request_local_part,
1588- verify,
1589- hidden,
1590- enabled,
1591- };
1592- db.update_list(changeset)?;
1593- }
1594- ImportMembers {
1595- url,
1596- username,
1597- password,
1598- list_id,
1599- dry_run,
1600- skip_owners,
1601- } => {
1602- let conn = import::Mailman3Connection::new(&url, &username, &password).unwrap();
1603- if dry_run {
1604- let entries = conn.users(&list_id).unwrap();
1605- println!("{} result(s)", entries.len());
1606- for e in entries {
1607- println!(
1608- "{}{}<{}>",
1609- if let Some(n) = e.display_name() {
1610- n
1611- } else {
1612- ""
1613- },
1614- if e.display_name().is_none() { "" } else { " " },
1615- e.email()
1616- );
1617- }
1618- if !skip_owners {
1619- let entries = conn.owners(&list_id).unwrap();
1620- println!("\nOwners: {} result(s)", entries.len());
1621- for e in entries {
1622- println!(
1623- "{}{}<{}>",
1624- if let Some(n) = e.display_name() {
1625- n
1626- } else {
1627- ""
1628- },
1629- if e.display_name().is_none() { "" } else { " " },
1630- e.email()
1631- );
1632- }
1633- }
1634- } else {
1635- let entries = conn.users(&list_id).unwrap();
1636- let tx = db.transaction(Default::default()).unwrap();
1637- for sub in entries.into_iter().map(|e| e.into_subscription(list.pk)) {
1638- tx.add_subscription(list.pk, sub)?;
1639- }
1640- if !skip_owners {
1641- let entries = conn.owners(&list_id).unwrap();
1642- for sub in entries.into_iter().map(|e| e.into_owner(list.pk)) {
1643- tx.add_list_owner(sub)?;
1644- }
1645- }
1646- tx.commit()?;
1647- }
1648- }
1649- SubscriptionRequests => {
1650- let subscriptions = db.list_subscription_requests(list.pk)?;
1651- if subscriptions.is_empty() {
1652- println!("No subscription requests found.");
1653- } else {
1654- println!("Subscription requests of list {}", list.id);
1655- for l in subscriptions {
1656- println!("- {}", &l);
1657- }
1658- }
1659- }
1660- AcceptSubscriptionRequest {
1661- pk,
1662- do_not_send_confirmation,
1663- } => match db.accept_candidate_subscription(pk) {
1664- Ok(subscription) => {
1665- println!("Added: {subscription:#?}");
1666- if !do_not_send_confirmation {
1667- if let Err(err) = db
1668- .list(subscription.list)
1669- .and_then(|v| match v {
1670- Some(v) => Ok(v),
1671- None => Err(format!(
1672- "No list with id or pk {} was found",
1673- subscription.list
1674- )
1675- .into()),
1676- })
1677- .and_then(|list| {
1678- db.send_subscription_confirmation(
1679- &list,
1680- &subscription.address(),
1681- )
1682- })
1683- {
1684- eprintln!("Could not send subscription confirmation!");
1685- return Err(err);
1686- }
1687- println!("Sent confirmation e-mail to {}", subscription.address());
1688- } else {
1689- println!(
1690- "Did not sent confirmation e-mail to {}. You can do it manually \
1691- with the appropriate command.",
1692- subscription.address()
1693- );
1694- }
1695- }
1696- Err(err) => {
1697- eprintln!("Could not accept subscription request!");
1698- return Err(err);
1699- }
1700- },
1701- SendConfirmationForSubscription { pk } => {
1702- let req = match db.candidate_subscription(pk) {
1703- Ok(req) => req,
1704- Err(err) => {
1705- eprintln!("Could not find subscription request by that pk!");
1706-
1707- return Err(err);
1708- }
1709- };
1710- log::info!("Found {:#?}", req);
1711- if req.accepted.is_none() {
1712- return Err("Request has not been accepted!".into());
1713- }
1714- if let Err(err) = db
1715- .list(req.list)
1716- .and_then(|v| match v {
1717- Some(v) => Ok(v),
1718- None => {
1719- Err(format!("No list with id or pk {} was found", req.list).into())
1720- }
1721- })
1722- .and_then(|list| db.send_subscription_confirmation(&list, &req.address()))
1723- {
1724- eprintln!("Could not send subscription request confirmation!");
1725- return Err(err);
1726- }
1727-
1728- println!("Sent confirmation e-mail to {}", req.address());
1729- }
1730- }
1731+ list(&mut db, &list_id, cmd, quiet).map_err(|err| {
1732+ err.chain_err(|| {
1733+ mailpot::Error::from(format!("Could not perform list command for {list_id}."))
1734+ })
1735+ })?;
1736 }
1737 CreateList {
1738 name,
1739 @@ -572,371 +94,55 @@ fn run_app(opt: Opt) -> Result<()> {
1740 description,
1741 archive_url,
1742 } => {
1743- let new = db.create_list(MailingList {
1744- pk: 0,
1745- name,
1746- id,
1747- description,
1748- topics: vec![],
1749- address,
1750- archive_url,
1751- })?;
1752- log::trace!("created new list {:#?}", new);
1753- if !opt.quiet {
1754- println!(
1755- "Created new list {:?} with primary key {}",
1756- new.id,
1757- new.pk()
1758- );
1759- }
1760+ create_list(&mut db, name, id, address, description, archive_url, quiet)
1761+ .context("Could not create list.")?;
1762 }
1763 Post { dry_run } => {
1764- if opt.debug {
1765- println!("post dry_run{:?}", dry_run);
1766- }
1767-
1768- let tx = db.transaction(TransactionBehavior::Exclusive).unwrap();
1769- let mut input = String::new();
1770- std::io::stdin().read_to_string(&mut input)?;
1771- match Envelope::from_bytes(input.as_bytes(), None) {
1772- Ok(env) => {
1773- if opt.debug {
1774- eprintln!("{:?}", &env);
1775- }
1776- tx.post(&env, input.as_bytes(), dry_run)?;
1777- }
1778- Err(err) if input.trim().is_empty() => {
1779- eprintln!("Empty input, abort.");
1780- return Err(err.into());
1781- }
1782- Err(err) => {
1783- eprintln!("Could not parse message: {}", err);
1784- let p = tx.conf().save_message(input)?;
1785- eprintln!("Message saved at {}", p.display());
1786- return Err(err.into());
1787- }
1788- }
1789- tx.commit()?;
1790+ post(&mut db, dry_run, debug).context("Could not process post.")?;
1791 }
1792 FlushQueue { dry_run } => {
1793- let tx = db.transaction(TransactionBehavior::Exclusive).unwrap();
1794- let messages = if opt.debug {
1795- println!("flush-queue dry_run {:?}", dry_run);
1796- tx.queue(mailpot::queue::Queue::Out)?
1797- .into_iter()
1798- .map(DbVal::into_inner)
1799- .chain(
1800- tx.queue(mailpot::queue::Queue::Deferred)?
1801- .into_iter()
1802- .map(DbVal::into_inner),
1803- )
1804- .collect()
1805- } else {
1806- tx.delete_from_queue(mailpot::queue::Queue::Out, vec![])?
1807- };
1808- if opt.verbose > 0 || opt.debug {
1809- println!("Queue out has {} messages.", messages.len());
1810- }
1811-
1812- let mut failures = Vec::with_capacity(messages.len());
1813-
1814- let send_mail = tx.conf().send_mail.clone();
1815- match send_mail {
1816- mailpot::SendMail::ShellCommand(cmd) => {
1817- fn submit(cmd: &str, msg: &QueueEntry) -> Result<()> {
1818- let mut child = std::process::Command::new("sh")
1819- .arg("-c")
1820- .arg(cmd)
1821- .stdout(Stdio::piped())
1822- .stdin(Stdio::piped())
1823- .stderr(Stdio::piped())
1824- .spawn()
1825- .context("sh command failed to start")?;
1826- let mut stdin = child.stdin.take().context("Failed to open stdin")?;
1827-
1828- let builder = std::thread::Builder::new();
1829-
1830- std::thread::scope(|s| {
1831- let handler = builder
1832- .spawn_scoped(s, move || {
1833- stdin
1834- .write_all(&msg.message)
1835- .expect("Failed to write to stdin");
1836- })
1837- .context(
1838- "Could not spawn IPC communication thread for SMTP \
1839- ShellCommand process",
1840- )?;
1841-
1842- handler.join().map_err(|_| {
1843- ErrorKind::External(mailpot::anyhow::anyhow!(
1844- "Could not join with IPC communication thread for SMTP \
1845- ShellCommand process"
1846- ))
1847- })?;
1848- Ok::<(), Error>(())
1849- })?;
1850- Ok(())
1851- }
1852- for msg in messages {
1853- if let Err(err) = submit(&cmd, &msg) {
1854- failures.push((err, msg));
1855- }
1856- }
1857- }
1858- mailpot::SendMail::Smtp(_) => {
1859- let conn_future = tx.new_smtp_connection()?;
1860- failures = smol::future::block_on(smol::spawn(async move {
1861- let mut conn = conn_future.await?;
1862- for msg in messages {
1863- if let Err(err) = Connection::submit(&mut conn, &msg, dry_run).await {
1864- failures.push((err, msg));
1865- }
1866- }
1867- Ok::<_, Error>(failures)
1868- }))?;
1869- }
1870- }
1871-
1872- for (err, mut msg) in failures {
1873- log::error!("Message {msg:?} failed with: {err}. Inserting to Deferred queue.");
1874-
1875- msg.queue = mailpot::queue::Queue::Deferred;
1876- tx.insert_to_queue(msg)?;
1877- }
1878-
1879- tx.commit()?;
1880+ flush_queue(&mut db, dry_run, verbose, debug).with_context(|| {
1881+ format!("Could not flush queue {}.", mailpot::queue::Queue::Out)
1882+ })?;
1883+ }
1884+ Queue { queue, cmd } => {
1885+ queue_(&mut db, queue, cmd, quiet)
1886+ .with_context(|| format!("Could not perform queue command for queue `{queue}`."))?;
1887 }
1888- ErrorQueue { cmd } => match cmd {
1889- QueueCommand::List => {
1890- let errors = db.queue(mailpot::queue::Queue::Error)?;
1891- if errors.is_empty() {
1892- println!("Error queue is empty.");
1893- } else {
1894- for e in errors {
1895- println!(
1896- "- {} {} {} {} {}",
1897- e.pk, e.datetime, e.from_address, e.to_addresses, e.subject
1898- );
1899- }
1900- }
1901- }
1902- QueueCommand::Print { index } => {
1903- let mut errors = db.queue(mailpot::queue::Queue::Error)?;
1904- if !index.is_empty() {
1905- errors.retain(|el| index.contains(&el.pk()));
1906- }
1907- if errors.is_empty() {
1908- println!("Error queue is empty.");
1909- } else {
1910- for e in errors {
1911- println!("{e:?}");
1912- }
1913- }
1914- }
1915- QueueCommand::Delete { index, quiet } => {
1916- let mut errors = db.queue(mailpot::queue::Queue::Error)?;
1917- if !index.is_empty() {
1918- errors.retain(|el| index.contains(&el.pk()));
1919- }
1920- if errors.is_empty() {
1921- if !quiet {
1922- println!("Error queue is empty.");
1923- }
1924- } else {
1925- if !quiet {
1926- println!("Deleting error queue elements {:?}", &index);
1927- }
1928- db.delete_from_queue(mailpot::queue::Queue::Error, index)?;
1929- if !quiet {
1930- for e in errors {
1931- println!("{e:?}");
1932- }
1933- }
1934- }
1935- }
1936- },
1937- Queue { queue, cmd } => match cmd {
1938- QueueCommand::List => {
1939- let entries = db.queue(queue)?;
1940- if entries.is_empty() {
1941- println!("Queue {queue} is empty.");
1942- } else {
1943- for e in entries {
1944- println!(
1945- "- {} {} {} {} {}",
1946- e.pk, e.datetime, e.from_address, e.to_addresses, e.subject
1947- );
1948- }
1949- }
1950- }
1951- QueueCommand::Print { index } => {
1952- let mut entries = db.queue(queue)?;
1953- if !index.is_empty() {
1954- entries.retain(|el| index.contains(&el.pk()));
1955- }
1956- if entries.is_empty() {
1957- println!("Queue {queue} is empty.");
1958- } else {
1959- for e in entries {
1960- println!("{e:?}");
1961- }
1962- }
1963- }
1964- QueueCommand::Delete { index, quiet } => {
1965- let mut entries = db.queue(queue)?;
1966- if !index.is_empty() {
1967- entries.retain(|el| index.contains(&el.pk()));
1968- }
1969- if entries.is_empty() {
1970- if !quiet {
1971- println!("Queue {queue} is empty.");
1972- }
1973- } else {
1974- if !quiet {
1975- println!("Deleting queue {queue} elements {:?}", &index);
1976- }
1977- db.delete_from_queue(queue, index)?;
1978- if !quiet {
1979- for e in entries {
1980- println!("{e:?}");
1981- }
1982- }
1983- }
1984- }
1985- },
1986 ImportMaildir {
1987 list_id,
1988- mut maildir_path,
1989+ maildir_path,
1990 } => {
1991- let list = match list!(db, list_id) {
1992- Some(v) => v,
1993- None => {
1994- return Err(format!("No list with id or pk {} was found", list_id).into());
1995- }
1996- };
1997- if !maildir_path.is_absolute() {
1998- maildir_path = std::env::current_dir()
1999- .expect("could not detect current directory")
2000- .join(&maildir_path);
2001- }
2002-
2003- fn get_file_hash(file: &std::path::Path) -> EnvelopeHash {
2004- let mut hasher = DefaultHasher::default();
2005- file.hash(&mut hasher);
2006- EnvelopeHash(hasher.finish())
2007- }
2008- let mut buf = Vec::with_capacity(4096);
2009- let files =
2010- melib::backends::maildir::MaildirType::list_mail_in_maildir_fs(maildir_path, true)?;
2011- let mut ctr = 0;
2012- for file in files {
2013- let hash = get_file_hash(&file);
2014- let mut reader = std::io::BufReader::new(std::fs::File::open(&file)?);
2015- buf.clear();
2016- reader.read_to_end(&mut buf)?;
2017- if let Ok(mut env) = Envelope::from_bytes(buf.as_slice(), Some(file.flags())) {
2018- env.set_hash(hash);
2019- db.insert_post(list.pk, &buf, &env)?;
2020- ctr += 1;
2021- }
2022- }
2023- println!("Inserted {} posts to {}.", ctr, list_id);
2024+ import_maildir(
2025+ &mut db,
2026+ &list_id,
2027+ maildir_path.clone(),
2028+ quiet,
2029+ debug,
2030+ verbose,
2031+ )
2032+ .with_context(|| {
2033+ format!(
2034+ "Could not import maildir path {} to list `{list_id}`.",
2035+ maildir_path.display(),
2036+ )
2037+ })?;
2038 }
2039- UpdatePostfixConfig {
2040- master_cf,
2041- config:
2042- PostfixConfig {
2043- user,
2044- group,
2045- binary_path,
2046- process_limit,
2047- map_output_path,
2048- transport_name,
2049- },
2050- } => {
2051- let pfconf = mailpot::postfix::PostfixConfiguration {
2052- user: user.into(),
2053- group: group.map(Into::into),
2054- binary_path,
2055- process_limit,
2056- map_output_path,
2057- transport_name: transport_name.map(std::borrow::Cow::from),
2058- };
2059- pfconf.save_maps(db.conf())?;
2060- pfconf.save_master_cf_entry(db.conf(), config_path, master_cf.as_deref())?;
2061+ UpdatePostfixConfig { master_cf, config } => {
2062+ update_postfix_config(config_path, &mut db, master_cf, config)
2063+ .context("Could not update postfix configuration.")?;
2064 }
2065- PrintPostfixConfig {
2066- config:
2067- PostfixConfig {
2068- user,
2069- group,
2070- binary_path,
2071- process_limit,
2072- map_output_path,
2073- transport_name,
2074- },
2075- } => {
2076- let pfconf = mailpot::postfix::PostfixConfiguration {
2077- user: user.into(),
2078- group: group.map(Into::into),
2079- binary_path,
2080- process_limit,
2081- map_output_path,
2082- transport_name: transport_name.map(std::borrow::Cow::from),
2083- };
2084- let lists = db.lists()?;
2085- let lists_post_policies = lists
2086- .into_iter()
2087- .map(|l| {
2088- let pk = l.pk;
2089- Ok((l, db.list_post_policy(pk)?))
2090- })
2091- .collect::<Result<Vec<(DbVal<MailingList>, Option<DbVal<PostPolicy>>)>>>()?;
2092- let maps = pfconf.generate_maps(&lists_post_policies);
2093- let mastercf = pfconf.generate_master_cf_entry(db.conf(), config_path);
2094-
2095- println!("{maps}\n\n{mastercf}\n");
2096+ PrintPostfixConfig { config } => {
2097+ print_postfix_config(config_path, &mut db, config)
2098+ .context("Could not print postfix configuration.")?;
2099 }
2100 Accounts => {
2101- let accounts = db.accounts()?;
2102- if accounts.is_empty() {
2103- println!("No accounts found.");
2104- } else {
2105- for a in accounts {
2106- println!("- {:?}", a);
2107- }
2108- }
2109+ accounts(&mut db, quiet).context("Could not retrieve accounts.")?;
2110 }
2111 AccountInfo { address } => {
2112- if let Some(acc) = db.account_by_address(&address)? {
2113- let subs = db.account_subscriptions(acc.pk())?;
2114- if subs.is_empty() {
2115- println!("No subscriptions found.");
2116- } else {
2117- for s in subs {
2118- let list = db
2119- .list(s.list)
2120- .unwrap_or_else(|err| {
2121- panic!(
2122- "Found subscription with list_pk = {} but no such list \
2123- exists.\nListSubscription = {:?}\n\n{err}",
2124- s.list, s
2125- )
2126- })
2127- .unwrap_or_else(|| {
2128- panic!(
2129- "Found subscription with list_pk = {} but no such list \
2130- exists.\nListSubscription = {:?}",
2131- s.list, s
2132- )
2133- });
2134- println!("- {:?} {}", s, list);
2135- }
2136- }
2137- } else {
2138- println!("account with this address not found!");
2139- };
2140+ account_info(&mut db, &address, quiet).with_context(|| {
2141+ format!("Could not retrieve account info for address {address}.")
2142+ })?;
2143 }
2144 AddAccount {
2145 address,
2146 @@ -945,32 +151,12 @@ fn run_app(opt: Opt) -> Result<()> {
2147 public_key,
2148 enabled,
2149 } => {
2150- db.add_account(Account {
2151- pk: 0,
2152- name,
2153- address,
2154- public_key,
2155- password,
2156- enabled: enabled.unwrap_or(true),
2157- })?;
2158+ add_account(&mut db, address, password, name, public_key, enabled)
2159+ .context("Could not add account.")?;
2160 }
2161 RemoveAccount { address } => {
2162- let mut input = String::new();
2163- loop {
2164- println!(
2165- "Are you sure you want to remove account with address {}? [Yy/n]",
2166- address
2167- );
2168- input.clear();
2169- std::io::stdin().read_line(&mut input)?;
2170- if input.trim() == "Y" || input.trim() == "y" || input.trim() == "" {
2171- break;
2172- } else if input.trim() == "n" {
2173- return Ok(());
2174- }
2175- }
2176-
2177- db.remove_account(&address)?;
2178+ remove_account(&mut db, &address, quiet)
2179+ .with_context(|| format!("Could not remove account with address {address}."))?;
2180 }
2181 UpdateAccount {
2182 address,
2183 @@ -979,60 +165,27 @@ fn run_app(opt: Opt) -> Result<()> {
2184 public_key,
2185 enabled,
2186 } => {
2187- let changeset = AccountChangeset {
2188- address,
2189- name,
2190- public_key,
2191- password,
2192- enabled,
2193- };
2194- db.update_account(changeset)?;
2195+ update_account(&mut db, address, password, name, public_key, enabled)
2196+ .context("Could not update account.")?;
2197 }
2198 Repair {
2199 fix,
2200 all,
2201- mut datetime_header_value,
2202- mut remove_empty_accounts,
2203- mut remove_accepted_subscription_requests,
2204- mut warn_list_no_owner,
2205+ datetime_header_value,
2206+ remove_empty_accounts,
2207+ remove_accepted_subscription_requests,
2208+ warn_list_no_owner,
2209 } => {
2210- type LintFn =
2211- fn(&'_ mut mailpot::Connection, bool) -> std::result::Result<(), mailpot::Error>;
2212- let dry_run = !fix;
2213- if all {
2214- datetime_header_value = true;
2215- remove_empty_accounts = true;
2216- remove_accepted_subscription_requests = true;
2217- warn_list_no_owner = true;
2218- }
2219-
2220- if !(datetime_header_value
2221- | remove_empty_accounts
2222- | remove_accepted_subscription_requests
2223- | warn_list_no_owner)
2224- {
2225- return Err(
2226- "No lints selected: specify them with flag arguments. See --help".into(),
2227- );
2228- }
2229-
2230- if dry_run {
2231- println!("running without making modifications (dry run)");
2232- }
2233-
2234- for (flag, lint_fn) in [
2235- (datetime_header_value, datetime_header_value_lint as LintFn),
2236- (remove_empty_accounts, remove_empty_accounts_lint as _),
2237- (
2238- remove_accepted_subscription_requests,
2239- remove_accepted_subscription_requests_lint as _,
2240- ),
2241- (warn_list_no_owner, warn_list_no_owner_lint as _),
2242- ] {
2243- if flag {
2244- lint_fn(&mut db, dry_run)?;
2245- }
2246- }
2247+ repair(
2248+ &mut db,
2249+ fix,
2250+ all,
2251+ datetime_header_value,
2252+ remove_empty_accounts,
2253+ remove_accepted_subscription_requests,
2254+ warn_list_no_owner,
2255+ )
2256+ .context("Could not perform database repair.")?;
2257 }
2258 }
2259
2260 @@ -1049,7 +202,18 @@ fn main() -> std::result::Result<(), i32> {
2261 .timestamp(opt.ts.unwrap_or(stderrlog::Timestamp::Off))
2262 .init()
2263 .unwrap();
2264- if let Err(err) = run_app(opt) {
2265+ if opt.debug {
2266+ println!("DEBUG: {:?}", &opt);
2267+ }
2268+ let Opt {
2269+ config,
2270+ cmd,
2271+ debug,
2272+ quiet,
2273+ verbose,
2274+ ..
2275+ } = opt;
2276+ if let Err(err) = run_app(config, cmd, debug, quiet, verbose) {
2277 print!("{}", err.display_chain());
2278 std::process::exit(-1);
2279 }
2280 diff --git a/cli/tests/basic_interfaces.rs b/cli/tests/basic_interfaces.rs
2281index 903d502..8fcdcdf 100644
2282--- a/cli/tests/basic_interfaces.rs
2283+++ b/cli/tests/basic_interfaces.rs
2284 @@ -104,6 +104,31 @@ For more information, try '--help'."#,
2285
2286 let config_str = config.to_toml();
2287
2288+ fn config_not_exists(conf: &Path) {
2289+ let mut cmd = Command::cargo_bin("mpot").unwrap();
2290+ let output = cmd
2291+ .arg("-c")
2292+ .arg(conf)
2293+ .arg("list-lists")
2294+ .output()
2295+ .unwrap()
2296+ .assert();
2297+ output.code(255).stderr(predicates::str::is_empty()).stdout(
2298+ predicate::eq(
2299+ format!(
2300+ "[1] Could not read configuration file from path: {} Caused by:\n[2] Error \
2301+ returned from internal I/O operation: No such file or directory (os error 2)",
2302+ conf.display()
2303+ )
2304+ .as_str(),
2305+ )
2306+ .trim()
2307+ .normalize(),
2308+ );
2309+ }
2310+
2311+ config_not_exists(&conf_path);
2312+
2313 std::fs::write(&conf_path, config_str.as_bytes()).unwrap();
2314
2315 fn list_lists(conf: &Path, eq: &str) {
2316 @@ -178,4 +203,65 @@ For more information, try '--help'."#,
2317 \"twobar-chat@example.com\", topics: [], description: None, archive_url: None }, \
2318 2)\n\tList owners: None\n\tPost policy: None\n\tSubscription policy: None",
2319 );
2320+
2321+ fn add_list_owner(conf: &Path) {
2322+ let mut cmd = Command::cargo_bin("mpot").unwrap();
2323+ let output = cmd
2324+ .arg("-c")
2325+ .arg(conf)
2326+ .arg("list")
2327+ .arg("twobar-chat")
2328+ .arg("add-list-owner")
2329+ .arg("--address")
2330+ .arg("list-owner@example.com")
2331+ .output()
2332+ .unwrap()
2333+ .assert();
2334+ output.code(0).stderr(predicates::str::is_empty()).stdout(
2335+ predicate::eq("Added new list owner [#1 2] list-owner@example.com")
2336+ .trim()
2337+ .normalize(),
2338+ );
2339+ }
2340+ add_list_owner(&conf_path);
2341+ list_lists(
2342+ &conf_path,
2343+ "- foo-chat DbVal(MailingList { pk: 1, name: \"foobar chat\", id: \"foo-chat\", address: \
2344+ \"foo-chat@example.com\", topics: [], description: None, archive_url: None }, 1)\n\tList \
2345+ owners: None\n\tPost policy: None\n\tSubscription policy: None\n\n- twobar-chat \
2346+ DbVal(MailingList { pk: 2, name: \"twobar\", id: \"twobar-chat\", address: \
2347+ \"twobar-chat@example.com\", topics: [], description: None, archive_url: None }, \
2348+ 2)\n\tList owners:\n\t- [#1 2] list-owner@example.com\n\tPost policy: \
2349+ None\n\tSubscription policy: None",
2350+ );
2351+
2352+ fn remove_list_owner(conf: &Path) {
2353+ let mut cmd = Command::cargo_bin("mpot").unwrap();
2354+ let output = cmd
2355+ .arg("-c")
2356+ .arg(conf)
2357+ .arg("list")
2358+ .arg("twobar-chat")
2359+ .arg("remove-list-owner")
2360+ .arg("--pk")
2361+ .arg("1")
2362+ .output()
2363+ .unwrap()
2364+ .assert();
2365+ output.code(0).stderr(predicates::str::is_empty()).stdout(
2366+ predicate::eq("Removed list owner with pk = 1")
2367+ .trim()
2368+ .normalize(),
2369+ );
2370+ }
2371+ remove_list_owner(&conf_path);
2372+ list_lists(
2373+ &conf_path,
2374+ "- foo-chat DbVal(MailingList { pk: 1, name: \"foobar chat\", id: \"foo-chat\", address: \
2375+ \"foo-chat@example.com\", topics: [], description: None, archive_url: None }, 1)\n\tList \
2376+ owners: None\n\tPost policy: None\n\tSubscription policy: None\n\n- twobar-chat \
2377+ DbVal(MailingList { pk: 2, name: \"twobar\", id: \"twobar-chat\", address: \
2378+ \"twobar-chat@example.com\", topics: [], description: None, archive_url: None }, \
2379+ 2)\n\tList owners: None\n\tPost policy: None\n\tSubscription policy: None",
2380+ );
2381 }
2382 diff --git a/core/src/config.rs b/core/src/config.rs
2383index 0c304a9..d99248f 100644
2384--- a/core/src/config.rs
2385+++ b/core/src/config.rs
2386 @@ -78,10 +78,12 @@ impl Configuration {
2387 let mut s = String::new();
2388 let mut file = std::fs::File::open(path)?;
2389 file.read_to_string(&mut s)?;
2390- let config: Self = toml::from_str(&s).context(format!(
2391- "Could not parse configuration file `{}` succesfully: ",
2392- path.display()
2393- ))?;
2394+ let config: Self = toml::from_str(&s)
2395+ .map_err(anyhow::Error::from)
2396+ .context(format!(
2397+ "Could not parse configuration file `{}` successfully: ",
2398+ path.display()
2399+ ))?;
2400
2401 Ok(config)
2402 }
2403 @@ -127,3 +129,29 @@ impl Configuration {
2404 .to_string()
2405 }
2406 }
2407+
2408+ #[cfg(test)]
2409+ mod tests {
2410+ use tempfile::TempDir;
2411+
2412+ use super::*;
2413+
2414+ #[test]
2415+ fn test_config_parse_error() {
2416+ let tmp_dir = TempDir::new().unwrap();
2417+ let conf_path = tmp_dir.path().join("conf.toml");
2418+ std::fs::write(&conf_path, b"afjsad skas as a as\n\n\n\n\t\x11\n").unwrap();
2419+
2420+ assert_eq!(
2421+ Configuration::from_file(&conf_path)
2422+ .unwrap_err()
2423+ .display_chain()
2424+ .to_string(),
2425+ format!(
2426+ "[1] Could not parse configuration file `{}` successfully: Caused by:\n[2] \
2427+ Error: expected an equals, found an identifier at line 1 column 8\n",
2428+ conf_path.display()
2429+ ),
2430+ );
2431+ }
2432+ }
2433 diff --git a/core/src/connection.rs b/core/src/connection.rs
2434index 551b49a..df1b7d8 100644
2435--- a/core/src/connection.rs
2436+++ b/core/src/connection.rs
2437 @@ -617,7 +617,7 @@ impl Connection {
2438 if matches!(err, rusqlite::Error::QueryReturnedNoRows) {
2439 Error::from(err).chain_err(|| NotFound("list or list owner not found!"))
2440 } else {
2441- err.into()
2442+ Error::from(err)
2443 }
2444 })?;
2445 Ok(())
2446 diff --git a/core/src/errors.rs b/core/src/errors.rs
2447index ef37006..da07e70 100644
2448--- a/core/src/errors.rs
2449+++ b/core/src/errors.rs
2450 @@ -23,8 +23,6 @@ use std::sync::Arc;
2451
2452 use thiserror::Error;
2453
2454- pub use crate::anyhow::Context;
2455-
2456 /// Mailpot library error.
2457 #[derive(Error, Debug)]
2458 pub struct Error {
2459 @@ -53,39 +51,36 @@ pub enum ErrorKind {
2460
2461 /// Error returned from an external user initiated operation such as
2462 /// deserialization or I/O.
2463- #[error(
2464- "Error returned from an external user initiated operation such as deserialization or I/O. \
2465- {0}"
2466- )]
2467+ #[error("Error: {0}")]
2468 External(#[from] anyhow::Error),
2469 /// Generic
2470 #[error("{0}")]
2471 Generic(anyhow::Error),
2472 /// Error returned from sqlite3.
2473- #[error("Error returned from sqlite3 {0}.")]
2474+ #[error("Error returned from sqlite3: {0}.")]
2475 Sql(
2476 #[from]
2477 #[source]
2478 rusqlite::Error,
2479 ),
2480 /// Error returned from sqlite3.
2481- #[error("Error returned from sqlite3. {0}")]
2482+ #[error("Error returned from sqlite3: {0}")]
2483 SqlLib(
2484 #[from]
2485 #[source]
2486 rusqlite::ffi::Error,
2487 ),
2488 /// Error returned from internal I/O operations.
2489- #[error("Error returned from internal I/O operations. {0}")]
2490+ #[error("Error returned from internal I/O operation: {0}")]
2491 Io(#[from] ::std::io::Error),
2492 /// Error returned from e-mail protocol operations from `melib` crate.
2493- #[error("Error returned from e-mail protocol operations from `melib` crate. {0}")]
2494+ #[error("Error returned from e-mail protocol operations from `melib` crate: {0}")]
2495 Melib(#[from] melib::error::Error),
2496 /// Error from deserializing JSON values.
2497- #[error("Error from deserializing JSON values. {0}")]
2498+ #[error("Error from deserializing JSON values: {0}")]
2499 SerdeJson(#[from] serde_json::Error),
2500 /// Error returned from minijinja template engine.
2501- #[error("Error returned from minijinja template engine. {0}")]
2502+ #[error("Error returned from minijinja template engine: {0}")]
2503 Template(#[from] minijinja::Error),
2504 }
2505
2506 @@ -188,7 +183,7 @@ struct ErrorChainDisplay<'e> {
2507 impl std::fmt::Display for ErrorChainDisplay<'_> {
2508 fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
2509 if let Some(ref source) = self.current.source {
2510- writeln!(fmt, "[{}] {}, caused by:", self.counter, self.current.kind)?;
2511+ writeln!(fmt, "[{}] {} Caused by:", self.counter, self.current.kind)?;
2512 Self {
2513 current: source,
2514 counter: self.counter + 1,
2515 @@ -200,3 +195,38 @@ impl std::fmt::Display for ErrorChainDisplay<'_> {
2516 }
2517 }
2518 }
2519+
2520+ /// adfsa
2521+ pub trait Context<T> {
2522+ /// Wrap the error value with additional context.
2523+ fn context<C>(self, context: C) -> Result<T>
2524+ where
2525+ C: Into<Error>;
2526+
2527+ /// Wrap the error value with additional context that is evaluated lazily
2528+ /// only once an error does occur.
2529+ fn with_context<C, F>(self, f: F) -> Result<T>
2530+ where
2531+ C: Into<Error>,
2532+ F: FnOnce() -> C;
2533+ }
2534+
2535+ impl<T, E> Context<T> for std::result::Result<T, E>
2536+ where
2537+ Error: From<E>,
2538+ {
2539+ fn context<C>(self, context: C) -> Result<T>
2540+ where
2541+ C: Into<Error>,
2542+ {
2543+ self.map_err(|err| Error::from(err).chain_err(|| context.into()))
2544+ }
2545+
2546+ fn with_context<C, F>(self, f: F) -> Result<T>
2547+ where
2548+ C: Into<Error>,
2549+ F: FnOnce() -> C,
2550+ {
2551+ self.map_err(|err| Error::from(err).chain_err(|| f().into()))
2552+ }
2553+ }
2554 diff --git a/docs/mpot.1 b/docs/mpot.1
2555index d82f5a8..85a2645 100644
2556--- a/docs/mpot.1
2557+++ b/docs/mpot.1
2558 @@ -705,61 +705,6 @@ Show e\-mail processing result without actually consuming it.
2559 .ie \n(.g .ds Aq \(aq
2560 .el .ds Aq '
2561 .\fB
2562- .SS mpot error-queue
2563- .\fR
2564- .br
2565-
2566- .br
2567-
2568- Mail that has not been handled properly end up in the error queue.
2569- .ie \n(.g .ds Aq \(aq
2570- .el .ds Aq '
2571- .\fB
2572- .SS mpot error-queue list
2573- .\fR
2574- .br
2575-
2576- .br
2577-
2578- List.
2579- .ie \n(.g .ds Aq \(aq
2580- .el .ds Aq '
2581- .\fB
2582- .SS mpot error-queue print
2583- .\fR
2584- .br
2585-
2586- .br
2587-
2588- mpot error\-queue print [\-\-index \fIINDEX\fR]
2589- .br
2590-
2591- Print entry in RFC5322 or JSON format.
2592- .TP
2593- \-\-index \fIINDEX\fR
2594- index of entry.
2595- .ie \n(.g .ds Aq \(aq
2596- .el .ds Aq '
2597- .\fB
2598- .SS mpot error-queue delete
2599- .\fR
2600- .br
2601-
2602- .br
2603-
2604- mpot error\-queue delete [\-\-index \fIINDEX\fR] [\-\-quiet \fIQUIET\fR]
2605- .br
2606-
2607- Delete entry and print it in stdout.
2608- .TP
2609- \-\-index \fIINDEX\fR
2610- index of entry.
2611- .TP
2612- \-\-quiet
2613- Do not print in stdout.
2614- .ie \n(.g .ds Aq \(aq
2615- .el .ds Aq '
2616- .\fB
2617 .SS mpot queue
2618 .\fR
2619 .br
2620 @@ -769,7 +714,7 @@ Do not print in stdout.
2621 mpot queue \-\-queue \fIQUEUE\fR
2622 .br
2623
2624- Mail that has not been handled properly end up in the error queue.
2625+ Processed mail is stored in queues.
2626 .TP
2627 \-\-queue \fIQUEUE\fR
2628
2629 @@ -810,16 +755,13 @@ index of entry.
2630
2631 .br
2632
2633- mpot queue delete [\-\-index \fIINDEX\fR] [\-\-quiet \fIQUIET\fR]
2634+ mpot queue delete [\-\-index \fIINDEX\fR]
2635 .br
2636
2637 Delete entry and print it in stdout.
2638 .TP
2639 \-\-index \fIINDEX\fR
2640 index of entry.
2641- .TP
2642- \-\-quiet
2643- Do not print in stdout.
2644 .ie \n(.g .ds Aq \(aq
2645 .el .ds Aq '
2646 .\fB