1
/*
2
 * This file is part of mailpot
3
 *
4
 * Copyright 2020 - Manos Pitsidianakis
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU Affero General Public License as
8
 * published by the Free Software Foundation, either version 3 of the
9
 * License, or (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU Affero General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU Affero General Public License
17
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
 */
19

            
20
pub use std::path::PathBuf;
21

            
22
pub use clap::{builder::TypedValueParser, Args, Parser, Subcommand};
23

            
24
346
#[derive(Debug, Parser)]
25
#[command(
26
    name = "mpot",
27
    about = "mailing list manager",
28
    long_about = "Tool for mailpot mailing list management.",
29
    before_long_help = "GNU Affero version 3 or later <https://www.gnu.org/licenses/>",
30
    author,
31
    version
32
)]
33
pub struct Opt {
34
22
    /// Print logs.
35
    #[arg(short, long)]
36
    pub debug: bool,
37
22
    /// Configuration file to use.
38
44
    #[arg(short, long, value_parser)]
39
    pub config: Option<PathBuf>,
40
16
    #[command(subcommand)]
41
    pub cmd: Command,
42
22
    /// Silence all output.
43
    #[arg(short, long)]
44
    pub quiet: bool,
45
44
    /// Verbose mode (-v, -vv, -vvv, etc).
46
22
    #[arg(short, long, action = clap::ArgAction::Count)]
47
    pub verbose: u8,
48
44
    /// Debug log timestamp (sec, ms, ns, none).
49
    #[arg(short, long)]
50
    pub ts: Option<stderrlog::Timestamp>,
51
}
52

            
53
1549
#[derive(Debug, Subcommand)]
54
pub enum Command {
55
    /// Prints a sample config file to STDOUT.
56
    ///
57
    /// You can generate a new configuration file by writing the output to a
58
    /// file, e.g: mpot sample-config --with-smtp > config.toml
59
    SampleConfig {
60
22
        /// Use an SMTP connection instead of a shell process.
61
        #[arg(long)]
62
        with_smtp: bool,
63
    },
64
    /// Dumps database data to STDOUT.
65
    DumpDatabase,
66
    /// Lists all registered mailing lists.
67
    ListLists,
68
    /// Mailing list management.
69
    List {
70
22
        /// Selects mailing list to operate on.
71
        list_id: String,
72
2
        #[command(subcommand)]
73
        cmd: ListCommand,
74
    },
75
    /// Create new list.
76
    CreateList {
77
22
        /// List name.
78
        #[arg(long)]
79
        name: String,
80
22
        /// List ID.
81
        #[arg(long)]
82
        id: String,
83
22
        /// List e-mail address.
84
        #[arg(long)]
85
        address: String,
86
44
        /// List description.
87
        #[arg(long)]
88
        description: Option<String>,
89
44
        /// List archive URL.
90
        #[arg(long)]
91
        archive_url: Option<String>,
92
    },
93
    /// Post message from STDIN to list.
94
    Post {
95
22
        /// Show e-mail processing result without actually consuming it.
96
        #[arg(long)]
97
        dry_run: bool,
98
    },
99
    /// Flush outgoing e-mail queue.
100
    FlushQueue {
101
22
        /// Show e-mail processing result without actually consuming it.
102
        #[arg(long)]
103
        dry_run: bool,
104
    },
105
    /// Processed mail is stored in queues.
106
    Queue {
107
22
        #[arg(long, value_parser = QueueValueParser)]
108
        queue: mailpot::queue::Queue,
109
        #[command(subcommand)]
110
        cmd: QueueCommand,
111
    },
112
    /// Import a maildir folder into an existing list.
113
    ImportMaildir {
114
22
        /// List-ID or primary key value.
115
        list_id: String,
116
44
        /// Path to a maildir mailbox.
117
        /// Must contain {cur, tmp, new} folders.
118
22
        #[arg(long, value_parser)]
119
        maildir_path: PathBuf,
120
    },
121
    /// Update postfix maps and master.cf (probably needs root permissions).
122
    UpdatePostfixConfig {
123
44
        #[arg(short = 'p', long)]
124
        /// Override location of master.cf file (default:
125
        /// /etc/postfix/master.cf)
126
        master_cf: Option<PathBuf>,
127
22
        #[clap(flatten)]
128
        config: PostfixConfig,
129
    },
130
    /// Print postfix maps and master.cf entry to STDOUT.
131
    ///
132
    /// Map output should be added to transport_maps and local_recipient_maps
133
    /// parameters in postfix's main.cf. It must be saved in a plain text
134
    /// file. To make postfix be able to read them, the postmap application
135
    /// must be executed with the path to the map file as its sole argument.
136
    ///
137
    ///   postmap /path/to/mylist_maps
138
    ///
139
    /// postmap is usually distributed along with the other postfix binaries.
140
    ///
141
    /// The master.cf entry must be manually appended to the master.cf file. See <https://www.postfix.org/master.5.html>.
142
    PrintPostfixConfig {
143
22
        #[clap(flatten)]
144
        config: PostfixConfig,
145
    },
146
    /// All Accounts.
147
    Accounts,
148
    /// Account info.
149
    AccountInfo {
150
22
        /// Account address.
151
        address: String,
152
    },
153
    /// Add account.
154
    AddAccount {
155
22
        /// E-mail address.
156
        #[arg(long)]
157
        address: String,
158
22
        /// SSH public key for authentication.
159
        #[arg(long)]
160
        password: String,
161
44
        /// Name.
162
        #[arg(long)]
163
        name: Option<String>,
164
44
        /// Public key.
165
        #[arg(long)]
166
        public_key: Option<String>,
167
44
        #[arg(long)]
168
        /// Is account enabled.
169
        enabled: Option<bool>,
170
    },
171
    /// Remove account.
172
    RemoveAccount {
173
22
        #[arg(long)]
174
        /// E-mail address.
175
        address: String,
176
    },
177
    /// Update account info.
178
    UpdateAccount {
179
22
        /// Address to edit.
180
        address: String,
181
44
        /// Public key for authentication.
182
        #[arg(long)]
183
        password: Option<String>,
184
66
        /// Name.
185
        #[arg(long)]
186
22
        name: Option<Option<String>>,
187
66
        /// Public key.
188
        #[arg(long)]
189
22
        public_key: Option<Option<String>>,
190
66
        #[arg(long)]
191
        /// Is account enabled.
192
22
        enabled: Option<Option<bool>>,
193
    },
194
    /// Show and fix possible data mistakes or inconsistencies.
195
    Repair {
196
66
        /// Fix errors (default: false)
197
        #[arg(long, default_value = "false")]
198
        fix: bool,
199
66
        /// Select all tests (default: false)
200
        #[arg(long, default_value = "false")]
201
        all: bool,
202
66
        /// Post `datetime` column must have the Date: header value, in RFC2822
203
        /// format.
204
        #[arg(long, default_value = "false")]
205
        datetime_header_value: bool,
206
66
        /// Remove accounts that have no matching subscriptions.
207
        #[arg(long, default_value = "false")]
208
        remove_empty_accounts: bool,
209
66
        /// Remove subscription requests that have been accepted.
210
        #[arg(long, default_value = "false")]
211
        remove_accepted_subscription_requests: bool,
212
66
        /// Warn if a list has no owners.
213
        #[arg(long, default_value = "false")]
214
        warn_list_no_owner: bool,
215
    },
216
}
217

            
218
/// Postfix config values.
219
660
#[derive(Debug, Args)]
220
pub struct PostfixConfig {
221
44
    /// User that runs mailpot when postfix relays a message.
222
    ///
223
    /// Must not be the `postfix` user.
224
    /// Must have permissions to access the database file and the data
225
    /// directory.
226
    #[arg(short, long)]
227
    pub user: String,
228
88
    /// Group that runs mailpot when postfix relays a message.
229
    /// Optional.
230
    #[arg(short, long)]
231
    pub group: Option<String>,
232
44
    /// The path to the mailpot binary postfix will execute.
233
    #[arg(long)]
234
    pub binary_path: PathBuf,
235
88
    /// Limit the number of mailpot instances that can exist at the same time.
236
    ///
237
    /// Default is 1.
238
    #[arg(long, default_value = "1")]
239
    pub process_limit: Option<u64>,
240
88
    /// The directory in which the map files are saved.
241
    ///
242
    /// Default is `data_path` from [`Configuration`](mailpot::Configuration).
243
    #[arg(long)]
244
    pub map_output_path: Option<PathBuf>,
245
88
    /// The name of the postfix service name to use.
246
    /// Default is `mailpot`.
247
    ///
248
    /// A postfix service is a daemon managed by the postfix process.
249
    /// Each entry in the `master.cf` configuration file defines a single
250
    /// service.
251
    ///
252
    /// The `master.cf` file is documented in [`master(5)`](https://www.postfix.org/master.5.html):
253
    /// <https://www.postfix.org/master.5.html>.
254
    #[arg(long)]
255
    pub transport_name: Option<String>,
256
}
257

            
258
110
#[derive(Debug, Subcommand)]
259
pub enum QueueCommand {
260
    /// List.
261
    List,
262
    /// Print entry in RFC5322 or JSON format.
263
    Print {
264
44
        /// index of entry.
265
        #[arg(long)]
266
        index: Vec<i64>,
267
    },
268
    /// Delete entry and print it in stdout.
269
    Delete {
270
44
        /// index of entry.
271
        #[arg(long)]
272
        index: Vec<i64>,
273
    },
274
}
275

            
276
/// Subscription options.
277
748
#[derive(Debug, Args)]
278
pub struct SubscriptionOptions {
279
88
    /// Name.
280
    #[arg(long)]
281
    pub name: Option<String>,
282
88
    /// Send messages as digest.
283
    #[arg(long, default_value = "false")]
284
    pub digest: Option<bool>,
285
88
    /// Hide message from list when posting.
286
    #[arg(long, default_value = "false")]
287
    pub hide_address: Option<bool>,
288
88
    /// Hide message from list when posting.
289
    #[arg(long, default_value = "false")]
290
    /// E-mail address verification status.
291
    pub verified: Option<bool>,
292
88
    #[arg(long, default_value = "true")]
293
    /// Receive confirmation email when posting.
294
    pub receive_confirmation: Option<bool>,
295
88
    #[arg(long, default_value = "true")]
296
    /// Receive posts from list even if address exists in To or Cc header.
297
    pub receive_duplicates: Option<bool>,
298
88
    #[arg(long, default_value = "false")]
299
    /// Receive own posts from list.
300
    pub receive_own_posts: Option<bool>,
301
88
    #[arg(long, default_value = "true")]
302
    /// Is subscription enabled.
303
    pub enabled: Option<bool>,
304
}
305

            
306
/// Account options.
307
#[derive(Debug, Args)]
308
pub struct AccountOptions {
309
    /// Name.
310
    #[arg(long)]
311
    pub name: Option<String>,
312
    /// Public key.
313
    #[arg(long)]
314
    pub public_key: Option<String>,
315
    #[arg(long)]
316
    /// Is account enabled.
317
    pub enabled: Option<bool>,
318
}
319

            
320
2336
#[derive(Debug, Subcommand)]
321
pub enum ListCommand {
322
    /// List subscriptions of list.
323
    Subscriptions,
324
    /// List subscription requests.
325
    SubscriptionRequests,
326
    /// Add subscription to list.
327
    AddSubscription {
328
22
        /// E-mail address.
329
        #[arg(long)]
330
        address: String,
331
22
        #[clap(flatten)]
332
        subscription_options: SubscriptionOptions,
333
    },
334
    /// Remove subscription from list.
335
    RemoveSubscription {
336
22
        #[arg(long)]
337
        /// E-mail address.
338
        address: String,
339
    },
340
    /// Update subscription info.
341
    UpdateSubscription {
342
22
        /// Address to edit.
343
        address: String,
344
22
        #[clap(flatten)]
345
        subscription_options: SubscriptionOptions,
346
    },
347
    /// Accept a subscription request by its primary key.
348
    AcceptSubscriptionRequest {
349
22
        /// The primary key of the request.
350
        pk: i64,
351
66
        /// Do not send confirmation e-mail.
352
        #[arg(long, default_value = "false")]
353
        do_not_send_confirmation: bool,
354
    },
355
    /// Send subscription confirmation manually.
356
    SendConfirmationForSubscription {
357
22
        /// The primary key of the subscription.
358
        pk: i64,
359
    },
360
    /// Add a new post policy.
361
    AddPostPolicy {
362
22
        #[arg(long)]
363
        /// Only list owners can post.
364
        announce_only: bool,
365
22
        #[arg(long)]
366
        /// Only subscriptions can post.
367
        subscription_only: bool,
368
22
        #[arg(long)]
369
        /// Subscriptions can post.
370
        /// Other posts must be approved by list owners.
371
        approval_needed: bool,
372
22
        #[arg(long)]
373
        /// Anyone can post without restrictions.
374
        open: bool,
375
22
        #[arg(long)]
376
        /// Allow posts, but handle it manually.
377
        custom: bool,
378
    },
379
    // Remove post policy.
380
    RemovePostPolicy {
381
22
        #[arg(long)]
382
        /// Post policy primary key.
383
        pk: i64,
384
    },
385
    /// Add subscription policy to list.
386
    AddSubscriptionPolicy {
387
22
        #[arg(long)]
388
        /// Send confirmation e-mail when subscription is finalized.
389
        send_confirmation: bool,
390
22
        #[arg(long)]
391
        /// Anyone can subscribe without restrictions.
392
        open: bool,
393
22
        #[arg(long)]
394
        /// Only list owners can manually add subscriptions.
395
        manual: bool,
396
22
        #[arg(long)]
397
        /// Anyone can request to subscribe.
398
        request: bool,
399
22
        #[arg(long)]
400
        /// Allow subscriptions, but handle it manually.
401
        custom: bool,
402
    },
403
    RemoveSubscriptionPolicy {
404
22
        #[arg(long)]
405
        /// Subscription policy primary key.
406
        pk: i64,
407
    },
408
    /// Add list owner to list.
409
    AddListOwner {
410
22
        #[arg(long)]
411
        address: String,
412
44
        #[arg(long)]
413
        name: Option<String>,
414
    },
415
    RemoveListOwner {
416
22
        #[arg(long)]
417
        /// List owner primary key.
418
        pk: i64,
419
    },
420
    /// Alias for update-subscription --enabled true.
421
    EnableSubscription {
422
22
        /// Subscription address.
423
        address: String,
424
    },
425
    /// Alias for update-subscription --enabled false.
426
    DisableSubscription {
427
22
        /// Subscription address.
428
        address: String,
429
    },
430
    /// Update mailing list details.
431
    Update {
432
44
        /// New list name.
433
        #[arg(long)]
434
        name: Option<String>,
435
44
        /// New List-ID.
436
        #[arg(long)]
437
        id: Option<String>,
438
44
        /// New list address.
439
        #[arg(long)]
440
        address: Option<String>,
441
44
        /// New list description.
442
        #[arg(long)]
443
        description: Option<String>,
444
44
        /// New list archive URL.
445
        #[arg(long)]
446
        archive_url: Option<String>,
447
44
        /// New owner address local part.
448
        /// If empty, it defaults to '+owner'.
449
        #[arg(long)]
450
        owner_local_part: Option<String>,
451
44
        /// New request address local part.
452
        /// If empty, it defaults to '+request'.
453
        #[arg(long)]
454
        request_local_part: Option<String>,
455
44
        /// Require verification of e-mails for new subscriptions.
456
        ///
457
        /// Subscriptions that are initiated from the subscription's address are
458
        /// verified automatically.
459
        #[arg(long)]
460
        verify: Option<bool>,
461
44
        /// Public visibility of list.
462
        ///
463
        /// If hidden, the list will not show up in public APIs unless
464
        /// requests to it won't work.
465
        #[arg(long)]
466
        hidden: Option<bool>,
467
44
        /// Enable or disable the list's functionality.
468
        ///
469
        /// If not enabled, the list will continue to show up in the database
470
        /// but e-mails and requests to it won't work.
471
        #[arg(long)]
472
        enabled: Option<bool>,
473
    },
474
    /// Show mailing list health status.
475
    Health,
476
    /// Show mailing list info.
477
    Info,
478
    /// Import members in a local list from a remote mailman3 REST API instance.
479
    ///
480
    /// To find the id of the remote list, you can check URL/lists.
481
    /// Example with curl:
482
    ///
483
    /// curl --anyauth -u admin:pass "http://localhost:9001/3.0/lists"
484
    ///
485
    /// If you're trying to import an entire list, create it first and then
486
    /// import its users with this command.
487
    ///
488
    /// Example:
489
    /// 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
490
    ImportMembers {
491
22
        #[arg(long)]
492
        /// REST HTTP endpoint e.g. http://localhost:9001/3.0/
493
        url: String,
494
22
        #[arg(long)]
495
        /// REST HTTP Basic Authentication username.
496
        username: String,
497
22
        #[arg(long)]
498
        /// REST HTTP Basic Authentication password.
499
        password: String,
500
22
        #[arg(long)]
501
        /// List ID of remote list to query.
502
        list_id: String,
503
22
        /// Show what would be inserted without performing any changes.
504
        #[arg(long)]
505
        dry_run: bool,
506
22
        /// Don't import list owners.
507
        #[arg(long)]
508
        skip_owners: bool,
509
    },
510
}
511

            
512
#[derive(Clone, Copy, Debug)]
513
pub struct QueueValueParser;
514

            
515
impl QueueValueParser {
516
    pub fn new() -> Self {
517
        Self
518
    }
519
}
520

            
521
impl TypedValueParser for QueueValueParser {
522
    type Value = mailpot::queue::Queue;
523

            
524
    fn parse_ref(
525
        &self,
526
        cmd: &clap::Command,
527
        arg: Option<&clap::Arg>,
528
        value: &std::ffi::OsStr,
529
    ) -> std::result::Result<Self::Value, clap::Error> {
530
        TypedValueParser::parse(self, cmd, arg, value.to_owned())
531
    }
532

            
533
    fn parse(
534
        &self,
535
        cmd: &clap::Command,
536
        _arg: Option<&clap::Arg>,
537
        value: std::ffi::OsString,
538
    ) -> std::result::Result<Self::Value, clap::Error> {
539
        use std::str::FromStr;
540

            
541
        use clap::error::ErrorKind;
542

            
543
        if value.is_empty() {
544
            return Err(cmd.clone().error(
545
                ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand,
546
                "queue value required",
547
            ));
548
        }
549
        Self::Value::from_str(value.to_str().ok_or_else(|| {
550
            cmd.clone().error(
551
                ErrorKind::InvalidValue,
552
                "Queue value is not an UTF-8 string",
553
            )
554
        })?)
555
        .map_err(|err| cmd.clone().error(ErrorKind::InvalidValue, err))
556
    }
557

            
558
    fn possible_values(&self) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue>>> {
559
        Some(Box::new(
560
            mailpot::queue::Queue::possible_values()
561
                .iter()
562
                .map(clap::builder::PossibleValue::new),
563
        ))
564
    }
565
}
566

            
567
impl Default for QueueValueParser {
568
    fn default() -> Self {
569
        Self::new()
570
    }
571
}