+1358 -1026 +/-10 browse
1 | diff --git a/cli/src/args.rs b/cli/src/args.rs |
2 | index 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 |
30 | new file mode 100644 |
31 | index 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 |
1119 | index 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 |
1132 | index 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 |
1150 | index 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 |
2281 | index 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 |
2383 | index 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 |
2434 | index 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 |
2447 | index 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 |
2555 | index 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 |