+893 -443 +/-9 browse
1 | diff --git a/Cargo.lock b/Cargo.lock |
2 | index 2efe9e8..8b04332 100644 |
3 | --- a/Cargo.lock |
4 | +++ b/Cargo.lock |
5 | @@ -1799,14 +1799,18 @@ name = "mailpot-cli" |
6 | version = "0.1.1" |
7 | dependencies = [ |
8 | "assert_cmd", |
9 | + "base64 0.21.0", |
10 | "clap", |
11 | "clap_mangen", |
12 | "log", |
13 | "mailpot", |
14 | "mailpot-tests", |
15 | "predicates", |
16 | + "serde", |
17 | + "serde_json", |
18 | "stderrlog", |
19 | "tempfile", |
20 | + "ureq", |
21 | ] |
22 | |
23 | [[package]] |
24 | @@ -3223,6 +3227,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
25 | checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" |
26 | |
27 | [[package]] |
28 | + name = "ureq" |
29 | + version = "2.6.2" |
30 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
31 | + checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d" |
32 | + dependencies = [ |
33 | + "base64 0.13.1", |
34 | + "log", |
35 | + "once_cell", |
36 | + "url", |
37 | + ] |
38 | + |
39 | + [[package]] |
40 | name = "url" |
41 | version = "2.3.1" |
42 | source = "registry+https://github.com/rust-lang/crates.io-index" |
43 | diff --git a/cli/Cargo.toml b/cli/Cargo.toml |
44 | index 9caadea..54a3638 100644 |
45 | --- a/cli/Cargo.toml |
46 | +++ b/cli/Cargo.toml |
47 | @@ -16,10 +16,14 @@ name = "mpot" |
48 | path = "src/main.rs" |
49 | |
50 | [dependencies] |
51 | + base64 = { version = "0.21" } |
52 | clap = { version = "^4.2", default-features = false, features = ["derive", "cargo", "unicode", "help", "usage", "error-context", "suggestions"] } |
53 | log = "0.4" |
54 | mailpot = { version = "^0.1", path = "../core" } |
55 | + serde = { version = "^1", features = ["derive", ] } |
56 | + serde_json = "^1" |
57 | stderrlog = "^0.5" |
58 | + ureq = { version = "2.6", default-features = false } |
59 | |
60 | [dev-dependencies] |
61 | assert_cmd = "2" |
62 | diff --git a/cli/build.rs b/cli/build.rs |
63 | index 0f3e9a4..568d926 100644 |
64 | --- a/cli/build.rs |
65 | +++ b/cli/build.rs |
66 | @@ -27,7 +27,7 @@ use clap::ArgAction; |
67 | use clap_mangen::{roff, Man}; |
68 | use roff::{bold, italic, roman, Inline, Roff}; |
69 | |
70 | - include!("src/lib.rs"); |
71 | + include!("src/args.rs"); |
72 | |
73 | fn main() -> std::io::Result<()> { |
74 | println!("cargo:rerun-if-changed=./src/lib.rs"); |
75 | diff --git a/cli/src/args.rs b/cli/src/args.rs |
76 | new file mode 100644 |
77 | index 0000000..d3f79d9 |
78 | --- /dev/null |
79 | +++ b/cli/src/args.rs |
80 | @@ -0,0 +1,496 @@ |
81 | + /* |
82 | + * This file is part of mailpot |
83 | + * |
84 | + * Copyright 2020 - Manos Pitsidianakis |
85 | + * |
86 | + * This program is free software: you can redistribute it and/or modify |
87 | + * it under the terms of the GNU Affero General Public License as |
88 | + * published by the Free Software Foundation, either version 3 of the |
89 | + * License, or (at your option) any later version. |
90 | + * |
91 | + * This program is distributed in the hope that it will be useful, |
92 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
93 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
94 | + * GNU Affero General Public License for more details. |
95 | + * |
96 | + * You should have received a copy of the GNU Affero General Public License |
97 | + * along with this program. If not, see <https://www.gnu.org/licenses/>. |
98 | + */ |
99 | + |
100 | + pub use std::path::PathBuf; |
101 | + |
102 | + pub use clap::{Args, CommandFactory, Parser, Subcommand}; |
103 | + |
104 | + #[derive(Debug, Parser)] |
105 | + #[command( |
106 | + name = "mpot", |
107 | + about = "mailing list manager", |
108 | + long_about = "Tool for mailpot mailing list management.", |
109 | + before_long_help = "GNU Affero version 3 or later <https://www.gnu.org/licenses/>", |
110 | + author, |
111 | + version |
112 | + )] |
113 | + pub struct Opt { |
114 | + /// Print logs. |
115 | + #[arg(short, long)] |
116 | + pub debug: bool, |
117 | + /// Configuration file to use. |
118 | + #[arg(short, long, value_parser)] |
119 | + pub config: Option<PathBuf>, |
120 | + #[command(subcommand)] |
121 | + pub cmd: Command, |
122 | + /// Silence all output. |
123 | + #[arg(short, long)] |
124 | + pub quiet: bool, |
125 | + /// Verbose mode (-v, -vv, -vvv, etc). |
126 | + #[arg(short, long, action = clap::ArgAction::Count)] |
127 | + pub verbose: u8, |
128 | + /// Debug log timestamp (sec, ms, ns, none). |
129 | + #[arg(short, long)] |
130 | + pub ts: Option<stderrlog::Timestamp>, |
131 | + } |
132 | + |
133 | + #[derive(Debug, Subcommand)] |
134 | + pub enum Command { |
135 | + /// Prints a sample config file to STDOUT. |
136 | + /// |
137 | + /// You can generate a new configuration file by writing the output to a |
138 | + /// file, e.g: mpot sample-config --with-smtp > config.toml |
139 | + SampleConfig { |
140 | + /// Use an SMTP connection instead of a shell process. |
141 | + #[arg(long)] |
142 | + with_smtp: bool, |
143 | + }, |
144 | + /// Dumps database data to STDOUT. |
145 | + DumpDatabase, |
146 | + /// Lists all registered mailing lists. |
147 | + ListLists, |
148 | + /// Mailing list management. |
149 | + List { |
150 | + /// Selects mailing list to operate on. |
151 | + list_id: String, |
152 | + #[command(subcommand)] |
153 | + cmd: ListCommand, |
154 | + }, |
155 | + /// Create new list. |
156 | + CreateList { |
157 | + /// List name. |
158 | + #[arg(long)] |
159 | + name: String, |
160 | + /// List ID. |
161 | + #[arg(long)] |
162 | + id: String, |
163 | + /// List e-mail address. |
164 | + #[arg(long)] |
165 | + address: String, |
166 | + /// List description. |
167 | + #[arg(long)] |
168 | + description: Option<String>, |
169 | + /// List archive URL. |
170 | + #[arg(long)] |
171 | + archive_url: Option<String>, |
172 | + }, |
173 | + /// Post message from STDIN to list. |
174 | + Post { |
175 | + /// Show e-mail processing result without actually consuming it. |
176 | + #[arg(long)] |
177 | + dry_run: bool, |
178 | + }, |
179 | + /// Flush outgoing e-mail queue. |
180 | + FlushQueue { |
181 | + /// Show e-mail processing result without actually consuming it. |
182 | + #[arg(long)] |
183 | + dry_run: bool, |
184 | + }, |
185 | + /// Mail that has not been handled properly end up in the error queue. |
186 | + ErrorQueue { |
187 | + #[command(subcommand)] |
188 | + cmd: ErrorQueueCommand, |
189 | + }, |
190 | + /// Import a maildir folder into an existing list. |
191 | + ImportMaildir { |
192 | + /// List-ID or primary key value. |
193 | + list_id: String, |
194 | + /// Path to a maildir mailbox. |
195 | + /// Must contain {cur, tmp, new} folders. |
196 | + #[arg(long, value_parser)] |
197 | + maildir_path: PathBuf, |
198 | + }, |
199 | + /// Update postfix maps and master.cf (probably needs root permissions). |
200 | + UpdatePostfixConfig { |
201 | + #[arg(short = 'p', long)] |
202 | + /// Override location of master.cf file (default: |
203 | + /// /etc/postfix/master.cf) |
204 | + master_cf: Option<PathBuf>, |
205 | + #[clap(flatten)] |
206 | + config: PostfixConfig, |
207 | + }, |
208 | + /// Print postfix maps and master.cf entry to STDOUT. |
209 | + /// |
210 | + /// Map output should be added to transport_maps and local_recipient_maps |
211 | + /// parameters in postfix's main.cf. It must be saved in a plain text |
212 | + /// file. To make postfix be able to read them, the postmap application |
213 | + /// must be executed with the path to the map file as its sole argument. |
214 | + /// |
215 | + /// postmap /path/to/mylist_maps |
216 | + /// |
217 | + /// postmap is usually distributed along with the other postfix binaries. |
218 | + /// |
219 | + /// The master.cf entry must be manually appended to the master.cf file. See <https://www.postfix.org/master.5.html>. |
220 | + PrintPostfixConfig { |
221 | + #[clap(flatten)] |
222 | + config: PostfixConfig, |
223 | + }, |
224 | + /// All Accounts. |
225 | + Accounts, |
226 | + /// Account info. |
227 | + AccountInfo { |
228 | + /// Account address. |
229 | + address: String, |
230 | + }, |
231 | + /// Add account. |
232 | + AddAccount { |
233 | + /// E-mail address. |
234 | + #[arg(long)] |
235 | + address: String, |
236 | + /// SSH public key for authentication. |
237 | + #[arg(long)] |
238 | + password: String, |
239 | + /// Name. |
240 | + #[arg(long)] |
241 | + name: Option<String>, |
242 | + /// Public key. |
243 | + #[arg(long)] |
244 | + public_key: Option<String>, |
245 | + #[arg(long)] |
246 | + /// Is account enabled. |
247 | + enabled: Option<bool>, |
248 | + }, |
249 | + /// Remove account. |
250 | + RemoveAccount { |
251 | + #[arg(long)] |
252 | + /// E-mail address. |
253 | + address: String, |
254 | + }, |
255 | + /// Update account info. |
256 | + UpdateAccount { |
257 | + /// Address to edit. |
258 | + address: String, |
259 | + /// Public key for authentication. |
260 | + #[arg(long)] |
261 | + password: Option<String>, |
262 | + /// Name. |
263 | + #[arg(long)] |
264 | + name: Option<Option<String>>, |
265 | + /// Public key. |
266 | + #[arg(long)] |
267 | + public_key: Option<Option<String>>, |
268 | + #[arg(long)] |
269 | + /// Is account enabled. |
270 | + enabled: Option<Option<bool>>, |
271 | + }, |
272 | + /// Show and fix possible data mistakes or inconsistencies. |
273 | + Repair { |
274 | + /// Fix errors (default: false) |
275 | + #[arg(long, default_value = "false")] |
276 | + fix: bool, |
277 | + /// Select all tests (default: false) |
278 | + #[arg(long, default_value = "false")] |
279 | + all: bool, |
280 | + /// Post `datetime` column must have the Date: header value, in RFC2822 |
281 | + /// format. |
282 | + #[arg(long, default_value = "false")] |
283 | + datetime_header_value: bool, |
284 | + /// Remove accounts that have no matching subscriptions. |
285 | + #[arg(long, default_value = "false")] |
286 | + remove_empty_accounts: bool, |
287 | + /// Remove subscription requests that have been accepted. |
288 | + #[arg(long, default_value = "false")] |
289 | + remove_accepted_subscription_requests: bool, |
290 | + /// Warn if a list has no owners. |
291 | + #[arg(long, default_value = "false")] |
292 | + warn_list_no_owner: bool, |
293 | + }, |
294 | + } |
295 | + |
296 | + /// Postfix config values. |
297 | + #[derive(Debug, Args)] |
298 | + pub struct PostfixConfig { |
299 | + /// User that runs mailpot when postfix relays a message. |
300 | + /// |
301 | + /// Must not be the `postfix` user. |
302 | + /// Must have permissions to access the database file and the data |
303 | + /// directory. |
304 | + #[arg(short, long)] |
305 | + pub user: String, |
306 | + /// Group that runs mailpot when postfix relays a message. |
307 | + /// Optional. |
308 | + #[arg(short, long)] |
309 | + pub group: Option<String>, |
310 | + /// The path to the mailpot binary postfix will execute. |
311 | + #[arg(long)] |
312 | + pub binary_path: PathBuf, |
313 | + /// Limit the number of mailpot instances that can exist at the same time. |
314 | + /// |
315 | + /// Default is 1. |
316 | + #[arg(long, default_value = "1")] |
317 | + pub process_limit: Option<u64>, |
318 | + /// The directory in which the map files are saved. |
319 | + /// |
320 | + /// Default is `data_path` from [`Configuration`](mailpot::Configuration). |
321 | + #[arg(long)] |
322 | + pub map_output_path: Option<PathBuf>, |
323 | + /// The name of the postfix service name to use. |
324 | + /// Default is `mailpot`. |
325 | + /// |
326 | + /// A postfix service is a daemon managed by the postfix process. |
327 | + /// Each entry in the `master.cf` configuration file defines a single |
328 | + /// service. |
329 | + /// |
330 | + /// The `master.cf` file is documented in [`master(5)`](https://www.postfix.org/master.5.html): |
331 | + /// <https://www.postfix.org/master.5.html>. |
332 | + #[arg(long)] |
333 | + pub transport_name: Option<String>, |
334 | + } |
335 | + |
336 | + #[derive(Debug, Subcommand)] |
337 | + pub enum ErrorQueueCommand { |
338 | + /// List. |
339 | + List, |
340 | + /// Print entry in RFC5322 or JSON format. |
341 | + Print { |
342 | + /// index of entry. |
343 | + #[arg(long)] |
344 | + index: Vec<i64>, |
345 | + }, |
346 | + /// Delete entry and print it in stdout. |
347 | + Delete { |
348 | + /// index of entry. |
349 | + #[arg(long)] |
350 | + index: Vec<i64>, |
351 | + /// Do not print in stdout. |
352 | + #[arg(long)] |
353 | + quiet: bool, |
354 | + }, |
355 | + } |
356 | + |
357 | + /// Subscription options. |
358 | + #[derive(Debug, Args)] |
359 | + pub struct SubscriptionOptions { |
360 | + /// Name. |
361 | + #[arg(long)] |
362 | + pub name: Option<String>, |
363 | + /// Send messages as digest. |
364 | + #[arg(long, default_value = "false")] |
365 | + pub digest: Option<bool>, |
366 | + /// Hide message from list when posting. |
367 | + #[arg(long, default_value = "false")] |
368 | + pub hide_address: Option<bool>, |
369 | + /// Hide message from list when posting. |
370 | + #[arg(long, default_value = "false")] |
371 | + /// E-mail address verification status. |
372 | + pub verified: Option<bool>, |
373 | + #[arg(long, default_value = "true")] |
374 | + /// Receive confirmation email when posting. |
375 | + pub receive_confirmation: Option<bool>, |
376 | + #[arg(long, default_value = "true")] |
377 | + /// Receive posts from list even if address exists in To or Cc header. |
378 | + pub receive_duplicates: Option<bool>, |
379 | + #[arg(long, default_value = "false")] |
380 | + /// Receive own posts from list. |
381 | + pub receive_own_posts: Option<bool>, |
382 | + #[arg(long, default_value = "true")] |
383 | + /// Is subscription enabled. |
384 | + pub enabled: Option<bool>, |
385 | + } |
386 | + |
387 | + /// Account options. |
388 | + #[derive(Debug, Args)] |
389 | + pub struct AccountOptions { |
390 | + /// Name. |
391 | + #[arg(long)] |
392 | + pub name: Option<String>, |
393 | + /// Public key. |
394 | + #[arg(long)] |
395 | + pub public_key: Option<String>, |
396 | + #[arg(long)] |
397 | + /// Is account enabled. |
398 | + pub enabled: Option<bool>, |
399 | + } |
400 | + |
401 | + #[derive(Debug, Subcommand)] |
402 | + pub enum ListCommand { |
403 | + /// List subscriptions of list. |
404 | + Subscriptions, |
405 | + /// Add subscription to list. |
406 | + AddSubscription { |
407 | + /// E-mail address. |
408 | + #[arg(long)] |
409 | + address: String, |
410 | + #[clap(flatten)] |
411 | + subscription_options: SubscriptionOptions, |
412 | + }, |
413 | + /// Remove subscription from list. |
414 | + RemoveSubscription { |
415 | + #[arg(long)] |
416 | + /// E-mail address. |
417 | + address: String, |
418 | + }, |
419 | + /// Update subscription info. |
420 | + UpdateSubscription { |
421 | + /// Address to edit. |
422 | + address: String, |
423 | + #[clap(flatten)] |
424 | + subscription_options: SubscriptionOptions, |
425 | + }, |
426 | + /// Add a new post policy. |
427 | + AddPolicy { |
428 | + #[arg(long)] |
429 | + /// Only list owners can post. |
430 | + announce_only: bool, |
431 | + #[arg(long)] |
432 | + /// Only subscriptions can post. |
433 | + subscription_only: bool, |
434 | + #[arg(long)] |
435 | + /// Subscriptions can post. |
436 | + /// Other posts must be approved by list owners. |
437 | + approval_needed: bool, |
438 | + #[arg(long)] |
439 | + /// Anyone can post without restrictions. |
440 | + open: bool, |
441 | + #[arg(long)] |
442 | + /// Allow posts, but handle it manually. |
443 | + custom: bool, |
444 | + }, |
445 | + // Remove post policy. |
446 | + RemovePolicy { |
447 | + #[arg(long)] |
448 | + /// Post policy primary key. |
449 | + pk: i64, |
450 | + }, |
451 | + /// Add subscription policy to list. |
452 | + AddSubscribePolicy { |
453 | + #[arg(long)] |
454 | + /// Send confirmation e-mail when subscription is finalized. |
455 | + send_confirmation: bool, |
456 | + #[arg(long)] |
457 | + /// Anyone can subscribe without restrictions. |
458 | + open: bool, |
459 | + #[arg(long)] |
460 | + /// Only list owners can manually add subscriptions. |
461 | + manual: bool, |
462 | + #[arg(long)] |
463 | + /// Anyone can request to subscribe. |
464 | + request: bool, |
465 | + #[arg(long)] |
466 | + /// Allow subscriptions, but handle it manually. |
467 | + custom: bool, |
468 | + }, |
469 | + RemoveSubscribePolicy { |
470 | + #[arg(long)] |
471 | + /// Subscribe policy primary key. |
472 | + pk: i64, |
473 | + }, |
474 | + /// Add list owner to list. |
475 | + AddListOwner { |
476 | + #[arg(long)] |
477 | + address: String, |
478 | + #[arg(long)] |
479 | + name: Option<String>, |
480 | + }, |
481 | + RemoveListOwner { |
482 | + #[arg(long)] |
483 | + /// List owner primary key. |
484 | + pk: i64, |
485 | + }, |
486 | + /// Alias for update-subscription --enabled true. |
487 | + EnableSubscription { |
488 | + /// Subscription address. |
489 | + address: String, |
490 | + }, |
491 | + /// Alias for update-subscription --enabled false. |
492 | + DisableSubscription { |
493 | + /// Subscription address. |
494 | + address: String, |
495 | + }, |
496 | + /// Update mailing list details. |
497 | + Update { |
498 | + /// New list name. |
499 | + #[arg(long)] |
500 | + name: Option<String>, |
501 | + /// New List-ID. |
502 | + #[arg(long)] |
503 | + id: Option<String>, |
504 | + /// New list address. |
505 | + #[arg(long)] |
506 | + address: Option<String>, |
507 | + /// New list description. |
508 | + #[arg(long)] |
509 | + description: Option<String>, |
510 | + /// New list archive URL. |
511 | + #[arg(long)] |
512 | + archive_url: Option<String>, |
513 | + /// New owner address local part. |
514 | + /// If empty, it defaults to '+owner'. |
515 | + #[arg(long)] |
516 | + owner_local_part: Option<String>, |
517 | + /// New request address local part. |
518 | + /// If empty, it defaults to '+request'. |
519 | + #[arg(long)] |
520 | + request_local_part: Option<String>, |
521 | + /// Require verification of e-mails for new subscriptions. |
522 | + /// |
523 | + /// Subscriptions that are initiated from the subscription's address are |
524 | + /// verified automatically. |
525 | + #[arg(long)] |
526 | + verify: Option<bool>, |
527 | + /// Public visibility of list. |
528 | + /// |
529 | + /// If hidden, the list will not show up in public APIs unless |
530 | + /// requests to it won't work. |
531 | + #[arg(long)] |
532 | + hidden: Option<bool>, |
533 | + /// Enable or disable the list's functionality. |
534 | + /// |
535 | + /// If not enabled, the list will continue to show up in the database |
536 | + /// but e-mails and requests to it won't work. |
537 | + #[arg(long)] |
538 | + enabled: Option<bool>, |
539 | + }, |
540 | + /// Show mailing list health status. |
541 | + Health, |
542 | + /// Show mailing list info. |
543 | + Info, |
544 | + /// Import members in a local list from a remote mailman3 REST API instance. |
545 | + /// |
546 | + /// To find the id of the remote list, you can check URL/lists. |
547 | + /// Example with curl: |
548 | + /// |
549 | + /// curl --anyauth -u admin:pass "http://localhost:9001/3.0/lists" |
550 | + /// |
551 | + /// If you're trying to import an entire list, create it first and then |
552 | + /// import its users with this command. |
553 | + /// |
554 | + /// Example: |
555 | + /// mpot -c conf.toml list list-general import-members --url "http://localhost:9001/3.0/" --username admin --password password --list-id list-general.example.com --skip-owners --dry-run |
556 | + ImportMembers { |
557 | + #[arg(long)] |
558 | + /// REST HTTP endpoint e.g. http://localhost:9001/3.0/ |
559 | + url: String, |
560 | + #[arg(long)] |
561 | + /// REST HTTP Basic Authentication username. |
562 | + username: String, |
563 | + #[arg(long)] |
564 | + /// REST HTTP Basic Authentication password. |
565 | + password: String, |
566 | + #[arg(long)] |
567 | + /// List ID of remote list to query. |
568 | + list_id: String, |
569 | + /// Show what would be inserted without performing any changes. |
570 | + #[arg(long)] |
571 | + dry_run: bool, |
572 | + /// Don't import list owners. |
573 | + #[arg(long)] |
574 | + skip_owners: bool, |
575 | + }, |
576 | + } |
577 | diff --git a/cli/src/import.rs b/cli/src/import.rs |
578 | new file mode 100644 |
579 | index 0000000..f7425dd |
580 | --- /dev/null |
581 | +++ b/cli/src/import.rs |
582 | @@ -0,0 +1,149 @@ |
583 | + /* |
584 | + * This file is part of mailpot |
585 | + * |
586 | + * Copyright 2023 - Manos Pitsidianakis |
587 | + * |
588 | + * This program is free software: you can redistribute it and/or modify |
589 | + * it under the terms of the GNU Affero General Public License as |
590 | + * published by the Free Software Foundation, either version 3 of the |
591 | + * License, or (at your option) any later version. |
592 | + * |
593 | + * This program is distributed in the hope that it will be useful, |
594 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
595 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
596 | + * GNU Affero General Public License for more details. |
597 | + * |
598 | + * You should have received a copy of the GNU Affero General Public License |
599 | + * along with this program. If not, see <https://www.gnu.org/licenses/>. |
600 | + */ |
601 | + |
602 | + use std::{borrow::Cow, time::Duration}; |
603 | + |
604 | + use base64::{engine::general_purpose, Engine as _}; |
605 | + use mailpot::models::{ListOwner, ListSubscription}; |
606 | + use ureq::Agent; |
607 | + |
608 | + pub struct Mailman3Connection { |
609 | + agent: Agent, |
610 | + url: Cow<'static, str>, |
611 | + auth: String, |
612 | + } |
613 | + |
614 | + impl Mailman3Connection { |
615 | + pub fn new( |
616 | + url: &str, |
617 | + username: &str, |
618 | + password: &str, |
619 | + ) -> Result<Self, Box<dyn std::error::Error>> { |
620 | + let agent: Agent = ureq::AgentBuilder::new() |
621 | + .timeout_read(Duration::from_secs(5)) |
622 | + .timeout_write(Duration::from_secs(5)) |
623 | + .build(); |
624 | + let mut buf = String::new(); |
625 | + general_purpose::STANDARD |
626 | + .encode_string(format!("{username}:{password}").as_bytes(), &mut buf); |
627 | + |
628 | + let auth: String = format!("Basic {buf}"); |
629 | + |
630 | + Ok(Self { |
631 | + agent, |
632 | + url: url.trim_end_matches('/').to_string().into(), |
633 | + auth, |
634 | + }) |
635 | + } |
636 | + |
637 | + pub fn users(&self, list_address: &str) -> Result<Vec<Entry>, Box<dyn std::error::Error>> { |
638 | + let response: String = self |
639 | + .agent |
640 | + .get(&format!( |
641 | + "{}/lists/{list_address}/roster/member?fields=email&fields=display_name", |
642 | + self.url |
643 | + )) |
644 | + .set("Authorization", &self.auth) |
645 | + .call()? |
646 | + .into_string()?; |
647 | + Ok(serde_json::from_str::<Roster>(&response)?.entries) |
648 | + } |
649 | + |
650 | + pub fn owners(&self, list_address: &str) -> Result<Vec<Entry>, Box<dyn std::error::Error>> { |
651 | + let response: String = self |
652 | + .agent |
653 | + .get(&format!( |
654 | + "{}/lists/{list_address}/roster/owner?fields=email&fields=display_name", |
655 | + self.url |
656 | + )) |
657 | + .set("Authorization", &self.auth) |
658 | + .call()? |
659 | + .into_string()?; |
660 | + Ok(serde_json::from_str::<Roster>(&response)?.entries) |
661 | + } |
662 | + } |
663 | + |
664 | + #[derive(serde::Deserialize, Debug)] |
665 | + pub struct Roster { |
666 | + pub entries: Vec<Entry>, |
667 | + } |
668 | + |
669 | + #[derive(serde::Deserialize, Debug)] |
670 | + pub struct Entry { |
671 | + display_name: String, |
672 | + email: String, |
673 | + } |
674 | + |
675 | + impl Entry { |
676 | + pub fn display_name(&self) -> Option<&str> { |
677 | + if !self.display_name.trim().is_empty() && &self.display_name != "None" { |
678 | + Some(&self.display_name) |
679 | + } else { |
680 | + None |
681 | + } |
682 | + } |
683 | + |
684 | + pub fn email(&self) -> &str { |
685 | + &self.email |
686 | + } |
687 | + |
688 | + pub fn into_subscription(self, list: i64) -> ListSubscription { |
689 | + let Self { |
690 | + display_name, |
691 | + email, |
692 | + } = self; |
693 | + |
694 | + ListSubscription { |
695 | + pk: -1, |
696 | + list, |
697 | + address: email, |
698 | + name: if !display_name.trim().is_empty() && &display_name != "None" { |
699 | + Some(display_name) |
700 | + } else { |
701 | + None |
702 | + }, |
703 | + account: None, |
704 | + enabled: true, |
705 | + verified: true, |
706 | + digest: false, |
707 | + hide_address: false, |
708 | + receive_duplicates: false, |
709 | + receive_own_posts: false, |
710 | + receive_confirmation: false, |
711 | + } |
712 | + } |
713 | + |
714 | + pub fn into_owner(self, list: i64) -> ListOwner { |
715 | + let Self { |
716 | + display_name, |
717 | + email, |
718 | + } = self; |
719 | + |
720 | + ListOwner { |
721 | + pk: -1, |
722 | + list, |
723 | + address: email, |
724 | + name: if !display_name.trim().is_empty() && &display_name != "None" { |
725 | + Some(display_name) |
726 | + } else { |
727 | + None |
728 | + }, |
729 | + } |
730 | + } |
731 | + } |
732 | diff --git a/cli/src/lib.rs b/cli/src/lib.rs |
733 | index b9439d7..67aad61 100644 |
734 | --- a/cli/src/lib.rs |
735 | +++ b/cli/src/lib.rs |
736 | @@ -17,448 +17,11 @@ |
737 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
738 | */ |
739 | |
740 | + extern crate base64; |
741 | + extern crate ureq; |
742 | pub use std::path::PathBuf; |
743 | |
744 | + mod args; |
745 | + pub mod import; |
746 | + pub use args::*; |
747 | pub use clap::{Args, CommandFactory, Parser, Subcommand}; |
748 | - |
749 | - #[derive(Debug, Parser)] |
750 | - #[command( |
751 | - name = "mpot", |
752 | - about = "mailing list manager", |
753 | - long_about = "Tool for mailpot mailing list management.", |
754 | - before_long_help = "GNU Affero version 3 or later <https://www.gnu.org/licenses/>", |
755 | - author, |
756 | - version |
757 | - )] |
758 | - pub struct Opt { |
759 | - /// Print logs. |
760 | - #[arg(short, long)] |
761 | - pub debug: bool, |
762 | - /// Configuration file to use. |
763 | - #[arg(short, long, value_parser)] |
764 | - pub config: Option<PathBuf>, |
765 | - #[command(subcommand)] |
766 | - pub cmd: Command, |
767 | - /// Silence all output. |
768 | - #[arg(short, long)] |
769 | - pub quiet: bool, |
770 | - /// Verbose mode (-v, -vv, -vvv, etc). |
771 | - #[arg(short, long, action = clap::ArgAction::Count)] |
772 | - pub verbose: u8, |
773 | - /// Debug log timestamp (sec, ms, ns, none). |
774 | - #[arg(short, long)] |
775 | - pub ts: Option<stderrlog::Timestamp>, |
776 | - } |
777 | - |
778 | - #[derive(Debug, Subcommand)] |
779 | - pub enum Command { |
780 | - /// Prints a sample config file to STDOUT. |
781 | - /// |
782 | - /// You can generate a new configuration file by writing the output to a |
783 | - /// file, e.g: mpot sample-config --with-smtp > config.toml |
784 | - SampleConfig { |
785 | - /// Use an SMTP connection instead of a shell process. |
786 | - #[arg(long)] |
787 | - with_smtp: bool, |
788 | - }, |
789 | - /// Dumps database data to STDOUT. |
790 | - DumpDatabase, |
791 | - /// Lists all registered mailing lists. |
792 | - ListLists, |
793 | - /// Mailing list management. |
794 | - List { |
795 | - /// Selects mailing list to operate on. |
796 | - list_id: String, |
797 | - #[command(subcommand)] |
798 | - cmd: ListCommand, |
799 | - }, |
800 | - /// Create new list. |
801 | - CreateList { |
802 | - /// List name. |
803 | - #[arg(long)] |
804 | - name: String, |
805 | - /// List ID. |
806 | - #[arg(long)] |
807 | - id: String, |
808 | - /// List e-mail address. |
809 | - #[arg(long)] |
810 | - address: String, |
811 | - /// List description. |
812 | - #[arg(long)] |
813 | - description: Option<String>, |
814 | - /// List archive URL. |
815 | - #[arg(long)] |
816 | - archive_url: Option<String>, |
817 | - }, |
818 | - /// Post message from STDIN to list. |
819 | - Post { |
820 | - /// Show e-mail processing result without actually consuming it. |
821 | - #[arg(long)] |
822 | - dry_run: bool, |
823 | - }, |
824 | - /// Flush outgoing e-mail queue. |
825 | - FlushQueue { |
826 | - /// Show e-mail processing result without actually consuming it. |
827 | - #[arg(long)] |
828 | - dry_run: bool, |
829 | - }, |
830 | - /// Mail that has not been handled properly end up in the error queue. |
831 | - ErrorQueue { |
832 | - #[command(subcommand)] |
833 | - cmd: ErrorQueueCommand, |
834 | - }, |
835 | - /// Import a maildir folder into an existing list. |
836 | - ImportMaildir { |
837 | - /// List-ID or primary key value. |
838 | - list_id: String, |
839 | - /// Path to a maildir mailbox. |
840 | - /// Must contain {cur, tmp, new} folders. |
841 | - #[arg(long, value_parser)] |
842 | - maildir_path: PathBuf, |
843 | - }, |
844 | - /// Update postfix maps and master.cf (probably needs root permissions). |
845 | - UpdatePostfixConfig { |
846 | - #[arg(short = 'p', long)] |
847 | - /// Override location of master.cf file (default: |
848 | - /// /etc/postfix/master.cf) |
849 | - master_cf: Option<PathBuf>, |
850 | - #[clap(flatten)] |
851 | - config: PostfixConfig, |
852 | - }, |
853 | - /// Print postfix maps and master.cf entry to STDOUT. |
854 | - /// |
855 | - /// Map output should be added to transport_maps and local_recipient_maps |
856 | - /// parameters in postfix's main.cf. It must be saved in a plain text |
857 | - /// file. To make postfix be able to read them, the postmap application |
858 | - /// must be executed with the path to the map file as its sole argument. |
859 | - /// |
860 | - /// postmap /path/to/mylist_maps |
861 | - /// |
862 | - /// postmap is usually distributed along with the other postfix binaries. |
863 | - /// |
864 | - /// The master.cf entry must be manually appended to the master.cf file. See <https://www.postfix.org/master.5.html>. |
865 | - PrintPostfixConfig { |
866 | - #[clap(flatten)] |
867 | - config: PostfixConfig, |
868 | - }, |
869 | - /// All Accounts. |
870 | - Accounts, |
871 | - /// Account info. |
872 | - AccountInfo { |
873 | - /// Account address. |
874 | - address: String, |
875 | - }, |
876 | - /// Add account. |
877 | - AddAccount { |
878 | - /// E-mail address. |
879 | - #[arg(long)] |
880 | - address: String, |
881 | - /// SSH public key for authentication. |
882 | - #[arg(long)] |
883 | - password: String, |
884 | - /// Name. |
885 | - #[arg(long)] |
886 | - name: Option<String>, |
887 | - /// Public key. |
888 | - #[arg(long)] |
889 | - public_key: Option<String>, |
890 | - #[arg(long)] |
891 | - /// Is account enabled. |
892 | - enabled: Option<bool>, |
893 | - }, |
894 | - /// Remove account. |
895 | - RemoveAccount { |
896 | - #[arg(long)] |
897 | - /// E-mail address. |
898 | - address: String, |
899 | - }, |
900 | - /// Update account info. |
901 | - UpdateAccount { |
902 | - /// Address to edit. |
903 | - address: String, |
904 | - /// Public key for authentication. |
905 | - #[arg(long)] |
906 | - password: Option<String>, |
907 | - /// Name. |
908 | - #[arg(long)] |
909 | - name: Option<Option<String>>, |
910 | - /// Public key. |
911 | - #[arg(long)] |
912 | - public_key: Option<Option<String>>, |
913 | - #[arg(long)] |
914 | - /// Is account enabled. |
915 | - enabled: Option<Option<bool>>, |
916 | - }, |
917 | - /// Show and fix possible data mistakes or inconsistencies. |
918 | - Repair { |
919 | - /// Fix errors (default: false) |
920 | - #[arg(long, default_value = "false")] |
921 | - fix: bool, |
922 | - /// Select all tests (default: false) |
923 | - #[arg(long, default_value = "false")] |
924 | - all: bool, |
925 | - /// Post `datetime` column must have the Date: header value, in RFC2822 |
926 | - /// format. |
927 | - #[arg(long, default_value = "false")] |
928 | - datetime_header_value: bool, |
929 | - /// Remove accounts that have no matching subscriptions. |
930 | - #[arg(long, default_value = "false")] |
931 | - remove_empty_accounts: bool, |
932 | - /// Remove subscription requests that have been accepted. |
933 | - #[arg(long, default_value = "false")] |
934 | - remove_accepted_subscription_requests: bool, |
935 | - /// Warn if a list has no owners. |
936 | - #[arg(long, default_value = "false")] |
937 | - warn_list_no_owner: bool, |
938 | - }, |
939 | - } |
940 | - |
941 | - /// Postfix config values. |
942 | - #[derive(Debug, Args)] |
943 | - pub struct PostfixConfig { |
944 | - /// User that runs mailpot when postfix relays a message. |
945 | - /// |
946 | - /// Must not be the `postfix` user. |
947 | - /// Must have permissions to access the database file and the data |
948 | - /// directory. |
949 | - #[arg(short, long)] |
950 | - pub user: String, |
951 | - /// Group that runs mailpot when postfix relays a message. |
952 | - /// Optional. |
953 | - #[arg(short, long)] |
954 | - pub group: Option<String>, |
955 | - /// The path to the mailpot binary postfix will execute. |
956 | - #[arg(long)] |
957 | - pub binary_path: PathBuf, |
958 | - /// Limit the number of mailpot instances that can exist at the same time. |
959 | - /// |
960 | - /// Default is 1. |
961 | - #[arg(long, default_value = "1")] |
962 | - pub process_limit: Option<u64>, |
963 | - /// The directory in which the map files are saved. |
964 | - /// |
965 | - /// Default is `data_path` from [`Configuration`](mailpot::Configuration). |
966 | - #[arg(long)] |
967 | - pub map_output_path: Option<PathBuf>, |
968 | - /// The name of the postfix service name to use. |
969 | - /// Default is `mailpot`. |
970 | - /// |
971 | - /// A postfix service is a daemon managed by the postfix process. |
972 | - /// Each entry in the `master.cf` configuration file defines a single |
973 | - /// service. |
974 | - /// |
975 | - /// The `master.cf` file is documented in [`master(5)`](https://www.postfix.org/master.5.html): |
976 | - /// <https://www.postfix.org/master.5.html>. |
977 | - #[arg(long)] |
978 | - pub transport_name: Option<String>, |
979 | - } |
980 | - |
981 | - #[derive(Debug, Subcommand)] |
982 | - pub enum ErrorQueueCommand { |
983 | - /// List. |
984 | - List, |
985 | - /// Print entry in RFC5322 or JSON format. |
986 | - Print { |
987 | - /// index of entry. |
988 | - #[arg(long)] |
989 | - index: Vec<i64>, |
990 | - }, |
991 | - /// Delete entry and print it in stdout. |
992 | - Delete { |
993 | - /// index of entry. |
994 | - #[arg(long)] |
995 | - index: Vec<i64>, |
996 | - /// Do not print in stdout. |
997 | - #[arg(long)] |
998 | - quiet: bool, |
999 | - }, |
1000 | - } |
1001 | - |
1002 | - /// Subscription options. |
1003 | - #[derive(Debug, Args)] |
1004 | - pub struct SubscriptionOptions { |
1005 | - /// Name. |
1006 | - #[arg(long)] |
1007 | - pub name: Option<String>, |
1008 | - /// Send messages as digest. |
1009 | - #[arg(long, default_value = "false")] |
1010 | - pub digest: Option<bool>, |
1011 | - /// Hide message from list when posting. |
1012 | - #[arg(long, default_value = "false")] |
1013 | - pub hide_address: Option<bool>, |
1014 | - /// Hide message from list when posting. |
1015 | - #[arg(long, default_value = "false")] |
1016 | - /// E-mail address verification status. |
1017 | - pub verified: Option<bool>, |
1018 | - #[arg(long, default_value = "true")] |
1019 | - /// Receive confirmation email when posting. |
1020 | - pub receive_confirmation: Option<bool>, |
1021 | - #[arg(long, default_value = "true")] |
1022 | - /// Receive posts from list even if address exists in To or Cc header. |
1023 | - pub receive_duplicates: Option<bool>, |
1024 | - #[arg(long, default_value = "false")] |
1025 | - /// Receive own posts from list. |
1026 | - pub receive_own_posts: Option<bool>, |
1027 | - #[arg(long, default_value = "true")] |
1028 | - /// Is subscription enabled. |
1029 | - pub enabled: Option<bool>, |
1030 | - } |
1031 | - |
1032 | - /// Account options. |
1033 | - #[derive(Debug, Args)] |
1034 | - pub struct AccountOptions { |
1035 | - /// Name. |
1036 | - #[arg(long)] |
1037 | - pub name: Option<String>, |
1038 | - /// Public key. |
1039 | - #[arg(long)] |
1040 | - pub public_key: Option<String>, |
1041 | - #[arg(long)] |
1042 | - /// Is account enabled. |
1043 | - pub enabled: Option<bool>, |
1044 | - } |
1045 | - |
1046 | - #[derive(Debug, Subcommand)] |
1047 | - pub enum ListCommand { |
1048 | - /// List subscriptions of list. |
1049 | - Subscriptions, |
1050 | - /// Add subscription to list. |
1051 | - AddSubscription { |
1052 | - /// E-mail address. |
1053 | - #[arg(long)] |
1054 | - address: String, |
1055 | - #[clap(flatten)] |
1056 | - subscription_options: SubscriptionOptions, |
1057 | - }, |
1058 | - /// Remove subscription from list. |
1059 | - RemoveSubscription { |
1060 | - #[arg(long)] |
1061 | - /// E-mail address. |
1062 | - address: String, |
1063 | - }, |
1064 | - /// Update subscription info. |
1065 | - UpdateSubscription { |
1066 | - /// Address to edit. |
1067 | - address: String, |
1068 | - #[clap(flatten)] |
1069 | - subscription_options: SubscriptionOptions, |
1070 | - }, |
1071 | - /// Add a new post policy. |
1072 | - AddPolicy { |
1073 | - #[arg(long)] |
1074 | - /// Only list owners can post. |
1075 | - announce_only: bool, |
1076 | - #[arg(long)] |
1077 | - /// Only subscriptions can post. |
1078 | - subscription_only: bool, |
1079 | - #[arg(long)] |
1080 | - /// Subscriptions can post. |
1081 | - /// Other posts must be approved by list owners. |
1082 | - approval_needed: bool, |
1083 | - #[arg(long)] |
1084 | - /// Anyone can post without restrictions. |
1085 | - open: bool, |
1086 | - #[arg(long)] |
1087 | - /// Allow posts, but handle it manually. |
1088 | - custom: bool, |
1089 | - }, |
1090 | - // Remove post policy. |
1091 | - RemovePolicy { |
1092 | - #[arg(long)] |
1093 | - /// Post policy primary key. |
1094 | - pk: i64, |
1095 | - }, |
1096 | - /// Add subscription policy to list. |
1097 | - AddSubscribePolicy { |
1098 | - #[arg(long)] |
1099 | - /// Send confirmation e-mail when subscription is finalized. |
1100 | - send_confirmation: bool, |
1101 | - #[arg(long)] |
1102 | - /// Anyone can subscribe without restrictions. |
1103 | - open: bool, |
1104 | - #[arg(long)] |
1105 | - /// Only list owners can manually add subscriptions. |
1106 | - manual: bool, |
1107 | - #[arg(long)] |
1108 | - /// Anyone can request to subscribe. |
1109 | - request: bool, |
1110 | - #[arg(long)] |
1111 | - /// Allow subscriptions, but handle it manually. |
1112 | - custom: bool, |
1113 | - }, |
1114 | - RemoveSubscribePolicy { |
1115 | - #[arg(long)] |
1116 | - /// Subscribe policy primary key. |
1117 | - pk: i64, |
1118 | - }, |
1119 | - /// Add list owner to list. |
1120 | - AddListOwner { |
1121 | - #[arg(long)] |
1122 | - address: String, |
1123 | - #[arg(long)] |
1124 | - name: Option<String>, |
1125 | - }, |
1126 | - RemoveListOwner { |
1127 | - #[arg(long)] |
1128 | - /// List owner primary key. |
1129 | - pk: i64, |
1130 | - }, |
1131 | - /// Alias for update-subscription --enabled true. |
1132 | - EnableSubscription { |
1133 | - /// Subscription address. |
1134 | - address: String, |
1135 | - }, |
1136 | - /// Alias for update-subscription --enabled false. |
1137 | - DisableSubscription { |
1138 | - /// Subscription address. |
1139 | - address: String, |
1140 | - }, |
1141 | - /// Update mailing list details. |
1142 | - Update { |
1143 | - /// New list name. |
1144 | - #[arg(long)] |
1145 | - name: Option<String>, |
1146 | - /// New List-ID. |
1147 | - #[arg(long)] |
1148 | - id: Option<String>, |
1149 | - /// New list address. |
1150 | - #[arg(long)] |
1151 | - address: Option<String>, |
1152 | - /// New list description. |
1153 | - #[arg(long)] |
1154 | - description: Option<String>, |
1155 | - /// New list archive URL. |
1156 | - #[arg(long)] |
1157 | - archive_url: Option<String>, |
1158 | - /// New owner address local part. |
1159 | - /// If empty, it defaults to '+owner'. |
1160 | - #[arg(long)] |
1161 | - owner_local_part: Option<String>, |
1162 | - /// New request address local part. |
1163 | - /// If empty, it defaults to '+request'. |
1164 | - #[arg(long)] |
1165 | - request_local_part: Option<String>, |
1166 | - /// Require verification of e-mails for new subscriptions. |
1167 | - /// |
1168 | - /// Subscriptions that are initiated from the subscription's address are |
1169 | - /// verified automatically. |
1170 | - #[arg(long)] |
1171 | - verify: Option<bool>, |
1172 | - /// Public visibility of list. |
1173 | - /// |
1174 | - /// If hidden, the list will not show up in public APIs unless |
1175 | - /// requests to it won't work. |
1176 | - #[arg(long)] |
1177 | - hidden: Option<bool>, |
1178 | - /// Enable or disable the list's functionality. |
1179 | - /// |
1180 | - /// If not enabled, the list will continue to show up in the database |
1181 | - /// but e-mails and requests to it won't work. |
1182 | - #[arg(long)] |
1183 | - enabled: Option<bool>, |
1184 | - }, |
1185 | - /// Show mailing list health status. |
1186 | - Health, |
1187 | - /// Show mailing list info. |
1188 | - Info, |
1189 | - } |
1190 | diff --git a/cli/src/main.rs b/cli/src/main.rs |
1191 | index 0a5c90a..dc9d80b 100644 |
1192 | --- a/cli/src/main.rs |
1193 | +++ b/cli/src/main.rs |
1194 | @@ -421,6 +421,61 @@ fn run_app(opt: Opt) -> Result<()> { |
1195 | }; |
1196 | db.update_list(changeset)?; |
1197 | } |
1198 | + ImportMembers { |
1199 | + url, |
1200 | + username, |
1201 | + password, |
1202 | + list_id, |
1203 | + dry_run, |
1204 | + skip_owners, |
1205 | + } => { |
1206 | + let conn = import::Mailman3Connection::new(&url, &username, &password).unwrap(); |
1207 | + if dry_run { |
1208 | + let entries = conn.users(&list_id).unwrap(); |
1209 | + println!("{} result(s)", entries.len()); |
1210 | + for e in entries { |
1211 | + println!( |
1212 | + "{}{}<{}>", |
1213 | + if let Some(n) = e.display_name() { |
1214 | + n |
1215 | + } else { |
1216 | + "" |
1217 | + }, |
1218 | + if e.display_name().is_none() { "" } else { " " }, |
1219 | + e.email() |
1220 | + ); |
1221 | + } |
1222 | + if !skip_owners { |
1223 | + let entries = conn.owners(&list_id).unwrap(); |
1224 | + println!("\nOwners: {} result(s)", entries.len()); |
1225 | + for e in entries { |
1226 | + println!( |
1227 | + "{}{}<{}>", |
1228 | + if let Some(n) = e.display_name() { |
1229 | + n |
1230 | + } else { |
1231 | + "" |
1232 | + }, |
1233 | + if e.display_name().is_none() { "" } else { " " }, |
1234 | + e.email() |
1235 | + ); |
1236 | + } |
1237 | + } |
1238 | + } else { |
1239 | + let entries = conn.users(&list_id).unwrap(); |
1240 | + let tx = db.transaction(Default::default()).unwrap(); |
1241 | + for sub in entries.into_iter().map(|e| e.into_subscription(list.pk)) { |
1242 | + tx.add_subscription(list.pk, sub)?; |
1243 | + } |
1244 | + if !skip_owners { |
1245 | + let entries = conn.owners(&list_id).unwrap(); |
1246 | + for sub in entries.into_iter().map(|e| e.into_owner(list.pk)) { |
1247 | + tx.add_list_owner(sub)?; |
1248 | + } |
1249 | + } |
1250 | + tx.commit()?; |
1251 | + } |
1252 | + } |
1253 | } |
1254 | } |
1255 | CreateList { |
1256 | diff --git a/core/src/connection.rs b/core/src/connection.rs |
1257 | index 7d3e619..ff9f2b5 100644 |
1258 | --- a/core/src/connection.rs |
1259 | +++ b/core/src/connection.rs |
1260 | @@ -670,4 +670,140 @@ impl Connection { |
1261 | tx.commit()?; |
1262 | Ok(()) |
1263 | } |
1264 | + |
1265 | + /// Execute operations inside an SQL transaction. |
1266 | + pub fn transaction( |
1267 | + &'_ self, |
1268 | + behavior: transaction::TransactionBehavior, |
1269 | + ) -> Result<transaction::Transaction<'_>> { |
1270 | + use transaction::*; |
1271 | + |
1272 | + let query = match behavior { |
1273 | + TransactionBehavior::Deferred => "BEGIN DEFERRED", |
1274 | + TransactionBehavior::Immediate => "BEGIN IMMEDIATE", |
1275 | + TransactionBehavior::Exclusive => "BEGIN EXCLUSIVE", |
1276 | + }; |
1277 | + self.connection.execute_batch(query)?; |
1278 | + Ok(Transaction { |
1279 | + conn: self, |
1280 | + drop_behavior: DropBehavior::Rollback, |
1281 | + }) |
1282 | + } |
1283 | + } |
1284 | + |
1285 | + /// Execute operations inside an SQL transaction. |
1286 | + pub mod transaction { |
1287 | + use super::*; |
1288 | + |
1289 | + /// A transaction handle. |
1290 | + #[derive(Debug)] |
1291 | + pub struct Transaction<'conn> { |
1292 | + pub(super) conn: &'conn Connection, |
1293 | + pub(super) drop_behavior: DropBehavior, |
1294 | + } |
1295 | + |
1296 | + impl Drop for Transaction<'_> { |
1297 | + fn drop(&mut self) { |
1298 | + _ = self.finish_(); |
1299 | + } |
1300 | + } |
1301 | + |
1302 | + impl Transaction<'_> { |
1303 | + /// Commit and consume transaction. |
1304 | + pub fn commit(mut self) -> Result<()> { |
1305 | + self.commit_() |
1306 | + } |
1307 | + |
1308 | + fn commit_(&mut self) -> Result<()> { |
1309 | + self.conn.connection.execute_batch("COMMIT")?; |
1310 | + Ok(()) |
1311 | + } |
1312 | + |
1313 | + /// Configure the transaction to perform the specified action when it is |
1314 | + /// dropped. |
1315 | + #[inline] |
1316 | + pub fn set_drop_behavior(&mut self, drop_behavior: DropBehavior) { |
1317 | + self.drop_behavior = drop_behavior; |
1318 | + } |
1319 | + |
1320 | + /// A convenience method which consumes and rolls back a transaction. |
1321 | + #[inline] |
1322 | + pub fn rollback(mut self) -> Result<()> { |
1323 | + self.rollback_() |
1324 | + } |
1325 | + |
1326 | + fn rollback_(&mut self) -> Result<()> { |
1327 | + self.conn.connection.execute_batch("ROLLBACK")?; |
1328 | + Ok(()) |
1329 | + } |
1330 | + |
1331 | + /// Consumes the transaction, committing or rolling back according to |
1332 | + /// the current setting (see `drop_behavior`). |
1333 | + /// |
1334 | + /// Functionally equivalent to the `Drop` implementation, but allows |
1335 | + /// callers to see any errors that occur. |
1336 | + #[inline] |
1337 | + pub fn finish(mut self) -> Result<()> { |
1338 | + self.finish_() |
1339 | + } |
1340 | + |
1341 | + #[inline] |
1342 | + fn finish_(&mut self) -> Result<()> { |
1343 | + if self.conn.connection.is_autocommit() { |
1344 | + return Ok(()); |
1345 | + } |
1346 | + match self.drop_behavior { |
1347 | + DropBehavior::Commit => self.commit_().or_else(|_| self.rollback_()), |
1348 | + DropBehavior::Rollback => self.rollback_(), |
1349 | + DropBehavior::Ignore => Ok(()), |
1350 | + DropBehavior::Panic => panic!("Transaction dropped unexpectedly."), |
1351 | + } |
1352 | + } |
1353 | + } |
1354 | + |
1355 | + impl std::ops::Deref for Transaction<'_> { |
1356 | + type Target = Connection; |
1357 | + |
1358 | + #[inline] |
1359 | + fn deref(&self) -> &Connection { |
1360 | + self.conn |
1361 | + } |
1362 | + } |
1363 | + |
1364 | + /// Options for transaction behavior. See [BEGIN |
1365 | + /// TRANSACTION](http://www.sqlite.org/lang_transaction.html) for details. |
1366 | + #[derive(Copy, Clone, Default)] |
1367 | + #[non_exhaustive] |
1368 | + pub enum TransactionBehavior { |
1369 | + /// DEFERRED means that the transaction does not actually start until |
1370 | + /// the database is first accessed. |
1371 | + Deferred, |
1372 | + /// IMMEDIATE cause the database connection to start a new write |
1373 | + /// immediately, without waiting for a writes statement. |
1374 | + Immediate, |
1375 | + #[default] |
1376 | + /// EXCLUSIVE prevents other database connections from reading the |
1377 | + /// database while the transaction is underway. |
1378 | + Exclusive, |
1379 | + } |
1380 | + |
1381 | + /// Options for how a Transaction or Savepoint should behave when it is |
1382 | + /// dropped. |
1383 | + #[derive(Default, Copy, Clone, Debug, PartialEq, Eq)] |
1384 | + #[non_exhaustive] |
1385 | + pub enum DropBehavior { |
1386 | + #[default] |
1387 | + /// Roll back the changes. This is the default. |
1388 | + Rollback, |
1389 | + |
1390 | + /// Commit the changes. |
1391 | + Commit, |
1392 | + |
1393 | + /// Do not commit or roll back changes - this will leave the transaction |
1394 | + /// or savepoint open, so should be used with care. |
1395 | + Ignore, |
1396 | + |
1397 | + /// Panic. Used to enforce intentional behavior during development. |
1398 | + Panic, |
1399 | + } |
1400 | } |
1401 | diff --git a/docs/mpot.1 b/docs/mpot.1 |
1402 | index 888ba26..02a0504 100644 |
1403 | --- a/docs/mpot.1 |
1404 | +++ b/docs/mpot.1 |
1405 | @@ -569,6 +569,37 @@ Show mailing list info. |
1406 | .ie \n(.g .ds Aq \(aq |
1407 | .el .ds Aq ' |
1408 | .\fB |
1409 | + .SS mpot list import-members |
1410 | + .\fR |
1411 | + .br |
1412 | + |
1413 | + .br |
1414 | + |
1415 | + mpot list import\-members \-\-url \fIURL\fR \-\-username \fIUSERNAME\fR \-\-password \fIPASSWORD\fR \-\-list\-id \fILIST_ID\fR [\-\-dry\-run \fIDRY_RUN\fR] [\-\-skip\-owners \fISKIP_OWNERS\fR] |
1416 | + .br |
1417 | + |
1418 | + Import members in a local list from a remote mailman3 REST API instance. |
1419 | + .TP |
1420 | + \-\-url \fIURL\fR |
1421 | + REST HTTP endpoint e.g. http://localhost:9001/3.0/. |
1422 | + .TP |
1423 | + \-\-username \fIUSERNAME\fR |
1424 | + REST HTTP Basic Authentication username. |
1425 | + .TP |
1426 | + \-\-password \fIPASSWORD\fR |
1427 | + REST HTTP Basic Authentication password. |
1428 | + .TP |
1429 | + \-\-list\-id \fILIST_ID\fR |
1430 | + List ID of remote list to query. |
1431 | + .TP |
1432 | + \-\-dry\-run |
1433 | + Show what would be inserted without performing any changes. |
1434 | + .TP |
1435 | + \-\-skip\-owners |
1436 | + Don\*(Aqt import list owners. |
1437 | + .ie \n(.g .ds Aq \(aq |
1438 | + .el .ds Aq ' |
1439 | + .\fB |
1440 | .SS mpot create-list |
1441 | .\fR |
1442 | .br |