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
use std::{
21
    collections::hash_map::DefaultHasher,
22
    hash::{Hash, Hasher},
23
    io::{Read, Write},
24
    path::{Path, PathBuf},
25
    process::Stdio,
26
};
27

            
28
use mailpot::{
29
    melib,
30
    melib::{maildir::MaildirPathTrait, smol, Envelope, EnvelopeHash},
31
    models::{changesets::*, *},
32
    queue::{Queue, QueueEntry},
33
    transaction::TransactionBehavior,
34
    Connection, Context, Error, ErrorKind, Result,
35
};
36

            
37
use crate::{lints::*, *};
38

            
39
macro_rules! list {
40
    ($db:ident, $list_id:expr) => {{
41
        $db.list_by_id(&$list_id)?.or_else(|| {
42
            $list_id
43
                .parse::<i64>()
44
                .ok()
45
                .map(|pk| $db.list(pk).ok())
46
                .flatten()
47
                .flatten()
48
        })
49
    }};
50
}
51

            
52
macro_rules! string_opts {
53
    ($field:ident) => {
54
        if $field.as_deref().map(str::is_empty).unwrap_or(false) {
55
            None
56
        } else {
57
            Some($field)
58
        }
59
    };
60
}
61

            
62
pub fn dump_database(db: &mut Connection) -> Result<()> {
63
    let lists = db.lists()?;
64
    let mut stdout = std::io::stdout();
65
    serde_json::to_writer_pretty(&mut stdout, &lists)?;
66
    for l in &lists {
67
        serde_json::to_writer_pretty(
68
            &mut stdout,
69
            &db.list_subscriptions(l.pk)
70
                .context("Could not retrieve list subscriptions.")?,
71
        )?;
72
    }
73
    Ok(())
74
}
75

            
76
5
pub fn list_lists(db: &mut Connection) -> Result<()> {
77
5
    let lists = db.lists().context("Could not retrieve lists.")?;
78
5
    if lists.is_empty() {
79
1
        println!("No lists found.");
80
    } else {
81
11
        for l in lists {
82
7
            println!("- {} {:?}", l.id, l);
83
7
            let list_owners = db
84
7
                .list_owners(l.pk)
85
                .context("Could not retrieve list owners.")?;
86
7
            if list_owners.is_empty() {
87
6
                println!("\tList owners: None");
88
            } else {
89
1
                println!("\tList owners:");
90
2
                for o in list_owners {
91
2
                    println!("\t- {}", o);
92
1
                }
93
            }
94
7
            if let Some(s) = db
95
7
                .list_post_policy(l.pk)
96
                .context("Could not retrieve list post policy.")?
97
            {
98
                println!("\tPost policy: {}", s);
99
            } else {
100
7
                println!("\tPost policy: None");
101
            }
102
7
            if let Some(s) = db
103
7
                .list_subscription_policy(l.pk)
104
                .context("Could not retrieve list subscription policy.")?
105
            {
106
                println!("\tSubscription policy: {}", s);
107
            } else {
108
7
                println!("\tSubscription policy: None");
109
            }
110
7
            println!();
111
7
        }
112
    }
113
5
    Ok(())
114
5
}
115

            
116
2
pub fn list(db: &mut Connection, list_id: &str, cmd: ListCommand, quiet: bool) -> Result<()> {
117
2
    let list = match list!(db, list_id) {
118
2
        Some(v) => v,
119
        None => {
120
            return Err(format!("No list with id or pk {} was found", list_id).into());
121
        }
122
    };
123
    use ListCommand::*;
124
2
    match cmd {
125
        Subscriptions => {
126
            let subscriptions = db.list_subscriptions(list.pk)?;
127
            if subscriptions.is_empty() {
128
                if !quiet {
129
                    println!("No subscriptions found.");
130
                }
131
            } else {
132
                if !quiet {
133
                    println!("Subscriptions of list {}", list.id);
134
                }
135
                for l in subscriptions {
136
                    println!("- {}", &l);
137
                }
138
            }
139
        }
140
        AddSubscription {
141
            address,
142
            subscription_options:
143
                SubscriptionOptions {
144
                    name,
145
                    digest,
146
                    hide_address,
147
                    receive_duplicates,
148
                    receive_own_posts,
149
                    receive_confirmation,
150
                    enabled,
151
                    verified,
152
                },
153
        } => {
154
            db.add_subscription(
155
                list.pk,
156
                ListSubscription {
157
                    pk: 0,
158
                    list: list.pk,
159
                    address,
160
                    account: None,
161
                    name,
162
                    digest: digest.unwrap_or(false),
163
                    hide_address: hide_address.unwrap_or(false),
164
                    receive_confirmation: receive_confirmation.unwrap_or(true),
165
                    receive_duplicates: receive_duplicates.unwrap_or(true),
166
                    receive_own_posts: receive_own_posts.unwrap_or(false),
167
                    enabled: enabled.unwrap_or(true),
168
                    verified: verified.unwrap_or(false),
169
                },
170
            )?;
171
        }
172
        RemoveSubscription { address } => {
173
            let mut input = String::new();
174
            loop {
175
                println!(
176
                    "Are you sure you want to remove subscription of {} from list {}? [Yy/n]",
177
                    address, list
178
                );
179
                input.clear();
180
                std::io::stdin().read_line(&mut input)?;
181
                if input.trim() == "Y" || input.trim() == "y" || input.trim() == "" {
182
                    break;
183
                } else if input.trim() == "n" {
184
                    return Ok(());
185
                }
186
            }
187

            
188
            db.remove_subscription(list.pk, &address)?;
189
        }
190
        Health => {
191
            if !quiet {
192
                println!("{} health:", list);
193
            }
194
            let list_owners = db
195
                .list_owners(list.pk)
196
                .context("Could not retrieve list owners.")?;
197
            let post_policy = db
198
                .list_post_policy(list.pk)
199
                .context("Could not retrieve list post policy.")?;
200
            let subscription_policy = db
201
                .list_subscription_policy(list.pk)
202
                .context("Could not retrieve list subscription policy.")?;
203
            if list_owners.is_empty() {
204
                println!("\tList has no owners: you should add at least one.");
205
            } else {
206
                for owner in list_owners {
207
                    println!("\tList owner: {}.", owner);
208
                }
209
            }
210
            if let Some(p) = post_policy {
211
                println!("\tList has post policy: {p}.");
212
            } else {
213
                println!("\tList has no post policy: you should add one.");
214
            }
215
            if let Some(p) = subscription_policy {
216
                println!("\tList has subscription policy: {p}.");
217
            } else {
218
                println!("\tList has no subscription policy: you should add one.");
219
            }
220
        }
221
        Info => {
222
            println!("{} info:", list);
223
            let list_owners = db
224
                .list_owners(list.pk)
225
                .context("Could not retrieve list owners.")?;
226
            let post_policy = db
227
                .list_post_policy(list.pk)
228
                .context("Could not retrieve list post policy.")?;
229
            let subscription_policy = db
230
                .list_subscription_policy(list.pk)
231
                .context("Could not retrieve list subscription policy.")?;
232
            let subscriptions = db
233
                .list_subscriptions(list.pk)
234
                .context("Could not retrieve list subscriptions.")?;
235
            if subscriptions.is_empty() {
236
                println!("No subscriptions.");
237
            } else if subscriptions.len() == 1 {
238
                println!("1 subscription.");
239
            } else {
240
                println!("{} subscriptions.", subscriptions.len());
241
            }
242
            if list_owners.is_empty() {
243
                println!("List owners: None");
244
            } else {
245
                println!("List owners:");
246
                for o in list_owners {
247
                    println!("\t- {}", o);
248
                }
249
            }
250
            if let Some(s) = post_policy {
251
                println!("Post policy: {s}");
252
            } else {
253
                println!("Post policy: None");
254
            }
255
            if let Some(s) = subscription_policy {
256
                println!("Subscription policy: {s}");
257
            } else {
258
                println!("Subscription policy: None");
259
            }
260
        }
261
        UpdateSubscription {
262
            address,
263
            subscription_options:
264
                SubscriptionOptions {
265
                    name,
266
                    digest,
267
                    hide_address,
268
                    receive_duplicates,
269
                    receive_own_posts,
270
                    receive_confirmation,
271
                    enabled,
272
                    verified,
273
                },
274
        } => {
275
            let name = if name
276
                .as_ref()
277
                .map(|s: &String| s.is_empty())
278
                .unwrap_or(false)
279
            {
280
                None
281
            } else {
282
                Some(name)
283
            };
284
            let changeset = ListSubscriptionChangeset {
285
                list: list.pk,
286
                address,
287
                account: None,
288
                name,
289
                digest,
290
                verified,
291
                hide_address,
292
                receive_duplicates,
293
                receive_own_posts,
294
                receive_confirmation,
295
                enabled,
296
            };
297
            db.update_subscription(changeset)?;
298
        }
299
        AddPostPolicy {
300
            announce_only,
301
            subscription_only,
302
            approval_needed,
303
            open,
304
            custom,
305
        } => {
306
            let policy = PostPolicy {
307
                pk: 0,
308
                list: list.pk,
309
                announce_only,
310
                subscription_only,
311
                approval_needed,
312
                open,
313
                custom,
314
            };
315
            let new_val = db.set_list_post_policy(policy)?;
316
            println!("Added new policy with pk = {}", new_val.pk());
317
        }
318
        RemovePostPolicy { pk } => {
319
            db.remove_list_post_policy(list.pk, pk)?;
320
            println!("Removed policy with pk = {}", pk);
321
        }
322
        AddSubscriptionPolicy {
323
            send_confirmation,
324
            open,
325
            manual,
326
            request,
327
            custom,
328
        } => {
329
            let policy = SubscriptionPolicy {
330
                pk: 0,
331
                list: list.pk,
332
                send_confirmation,
333
                open,
334
                manual,
335
                request,
336
                custom,
337
            };
338
            let new_val = db.set_list_subscription_policy(policy)?;
339
            println!("Added new subscribe policy with pk = {}", new_val.pk());
340
        }
341
        RemoveSubscriptionPolicy { pk } => {
342
            db.remove_list_subscription_policy(list.pk, pk)?;
343
            println!("Removed subscribe policy with pk = {}", pk);
344
        }
345
1
        AddListOwner { address, name } => {
346
1
            let list_owner = ListOwner {
347
                pk: 0,
348
1
                list: list.pk,
349
                address,
350
                name,
351
            };
352
1
            let new_val = db.add_list_owner(list_owner)?;
353
1
            println!("Added new list owner {}", new_val);
354
1
        }
355
1
        RemoveListOwner { pk } => {
356
1
            db.remove_list_owner(list.pk, pk)?;
357
1
            println!("Removed list owner with pk = {}", pk);
358
        }
359
        EnableSubscription { address } => {
360
            let changeset = ListSubscriptionChangeset {
361
                list: list.pk,
362
                address,
363
                account: None,
364
                name: None,
365
                digest: None,
366
                verified: None,
367
                enabled: Some(true),
368
                hide_address: None,
369
                receive_duplicates: None,
370
                receive_own_posts: None,
371
                receive_confirmation: None,
372
            };
373
            db.update_subscription(changeset)?;
374
        }
375
        DisableSubscription { address } => {
376
            let changeset = ListSubscriptionChangeset {
377
                list: list.pk,
378
                address,
379
                account: None,
380
                name: None,
381
                digest: None,
382
                enabled: Some(false),
383
                verified: None,
384
                hide_address: None,
385
                receive_duplicates: None,
386
                receive_own_posts: None,
387
                receive_confirmation: None,
388
            };
389
            db.update_subscription(changeset)?;
390
        }
391
        Update {
392
            name,
393
            id,
394
            address,
395
            description,
396
            archive_url,
397
            owner_local_part,
398
            request_local_part,
399
            verify,
400
            hidden,
401
            enabled,
402
        } => {
403
            let description = string_opts!(description);
404
            let archive_url = string_opts!(archive_url);
405
            let owner_local_part = string_opts!(owner_local_part);
406
            let request_local_part = string_opts!(request_local_part);
407
            let changeset = MailingListChangeset {
408
                pk: list.pk,
409
                name,
410
                id,
411
                address,
412
                description,
413
                archive_url,
414
                owner_local_part,
415
                request_local_part,
416
                verify,
417
                hidden,
418
                enabled,
419
            };
420
            db.update_list(changeset)?;
421
        }
422
        ImportMembers {
423
            url,
424
            username,
425
            password,
426
            list_id,
427
            dry_run,
428
            skip_owners,
429
        } => {
430
            let conn = import::Mailman3Connection::new(&url, &username, &password).unwrap();
431
            if dry_run {
432
                let entries = conn.users(&list_id).unwrap();
433
                println!("{} result(s)", entries.len());
434
                for e in entries {
435
                    println!(
436
                        "{}{}<{}>",
437
                        if let Some(n) = e.display_name() {
438
                            n
439
                        } else {
440
                            ""
441
                        },
442
                        if e.display_name().is_none() { "" } else { " " },
443
                        e.email()
444
                    );
445
                }
446
                if !skip_owners {
447
                    let entries = conn.owners(&list_id).unwrap();
448
                    println!("\nOwners: {} result(s)", entries.len());
449
                    for e in entries {
450
                        println!(
451
                            "{}{}<{}>",
452
                            if let Some(n) = e.display_name() {
453
                                n
454
                            } else {
455
                                ""
456
                            },
457
                            if e.display_name().is_none() { "" } else { " " },
458
                            e.email()
459
                        );
460
                    }
461
                }
462
            } else {
463
                let entries = conn.users(&list_id).unwrap();
464
                let tx = db.transaction(Default::default()).unwrap();
465
                for sub in entries.into_iter().map(|e| e.into_subscription(list.pk)) {
466
                    tx.add_subscription(list.pk, sub)?;
467
                }
468
                if !skip_owners {
469
                    let entries = conn.owners(&list_id).unwrap();
470
                    for sub in entries.into_iter().map(|e| e.into_owner(list.pk)) {
471
                        tx.add_list_owner(sub)?;
472
                    }
473
                }
474
2
                tx.commit()?;
475
            }
476
        }
477
        SubscriptionRequests => {
478
            let subscriptions = db.list_subscription_requests(list.pk)?;
479
            if subscriptions.is_empty() {
480
                println!("No subscription requests found.");
481
            } else {
482
                println!("Subscription requests of list {}", list.id);
483
                for l in subscriptions {
484
                    println!("- {}", &l);
485
                }
486
            }
487
        }
488
        AcceptSubscriptionRequest {
489
            pk,
490
            do_not_send_confirmation,
491
        } => match db.accept_candidate_subscription(pk) {
492
            Ok(subscription) => {
493
                println!("Added: {subscription:#?}");
494
                if !do_not_send_confirmation {
495
                    if let Err(err) = db
496
                        .list(subscription.list)
497
                        .and_then(|v| match v {
498
                            Some(v) => Ok(v),
499
                            None => Err(format!(
500
                                "No list with id or pk {} was found",
501
                                subscription.list
502
                            )
503
                            .into()),
504
                        })
505
                        .and_then(|list| {
506
                            db.send_subscription_confirmation(&list, &subscription.address())
507
                        })
508
                    {
509
                        eprintln!("Could not send subscription confirmation!");
510
                        return Err(err);
511
                    }
512
                    println!("Sent confirmation e-mail to {}", subscription.address());
513
                } else {
514
                    println!(
515
                        "Did not sent confirmation e-mail to {}. You can do it manually with the \
516
                         appropriate command.",
517
                        subscription.address()
518
                    );
519
                }
520
            }
521
            Err(err) => {
522
                eprintln!("Could not accept subscription request!");
523
                return Err(err);
524
            }
525
        },
526
        SendConfirmationForSubscription { pk } => {
527
            let req = match db.candidate_subscription(pk) {
528
                Ok(req) => req,
529
                Err(err) => {
530
                    eprintln!("Could not find subscription request by that pk!");
531

            
532
                    return Err(err);
533
                }
534
            };
535
            log::info!("Found {:#?}", req);
536
            if req.accepted.is_none() {
537
                return Err("Request has not been accepted!".into());
538
            }
539
            if let Err(err) = db
540
                .list(req.list)
541
                .and_then(|v| match v {
542
                    Some(v) => Ok(v),
543
                    None => Err(format!("No list with id or pk {} was found", req.list).into()),
544
                })
545
                .and_then(|list| db.send_subscription_confirmation(&list, &req.address()))
546
            {
547
                eprintln!("Could not send subscription request confirmation!");
548
                return Err(err);
549
            }
550

            
551
            println!("Sent confirmation e-mail to {}", req.address());
552
        }
553
    }
554
2
    Ok(())
555
2
}
556

            
557
1
pub fn create_list(
558
    db: &mut Connection,
559
    name: String,
560
    id: String,
561
    address: String,
562
    description: Option<String>,
563
    archive_url: Option<String>,
564
    quiet: bool,
565
) -> Result<()> {
566
1
    let new = db.create_list(MailingList {
567
        pk: 0,
568
        name,
569
        id,
570
        description,
571
1
        topics: vec![],
572
        address,
573
        archive_url,
574
    })?;
575
1
    log::trace!("created new list {:#?}", new);
576
1
    if !quiet {
577
2
        println!(
578
            "Created new list {:?} with primary key {}",
579
1
            new.id,
580
1
            new.pk()
581
        );
582
    }
583
1
    Ok(())
584
1
}
585

            
586
pub fn post(db: &mut Connection, dry_run: bool, debug: bool) -> Result<()> {
587
    if debug {
588
        println!("Post dry_run = {:?}", dry_run);
589
    }
590

            
591
    let tx = db
592
        .transaction(TransactionBehavior::Exclusive)
593
        .context("Could not open Exclusive transaction in database.")?;
594
    let mut input = String::new();
595
    std::io::stdin()
596
        .read_to_string(&mut input)
597
        .context("Could not read from stdin")?;
598
    match Envelope::from_bytes(input.as_bytes(), None) {
599
        Ok(env) => {
600
            if debug {
601
                eprintln!("Parsed envelope is:\n{:?}", &env);
602
            }
603
            tx.post(&env, input.as_bytes(), dry_run)?;
604
        }
605
        Err(err) if input.trim().is_empty() => {
606
            eprintln!("Empty input, abort.");
607
            return Err(err.into());
608
        }
609
        Err(err) => {
610
            eprintln!("Could not parse message: {}", err);
611
            let p = tx.conf().save_message(input)?;
612
            eprintln!("Message saved at {}", p.display());
613
            return Err(err.into());
614
        }
615
    }
616
    tx.commit()
617
}
618

            
619
5
pub fn flush_queue(db: &mut Connection, dry_run: bool, verbose: u8, debug: bool) -> Result<()> {
620
10
    let tx = db
621
5
        .transaction(TransactionBehavior::Exclusive)
622
        .context("Could not open Exclusive transaction in database.")?;
623
5
    let messages = tx.delete_from_queue(mailpot::queue::Queue::Out, vec![])?;
624
5
    if verbose > 0 || debug {
625
5
        println!("Queue out has {} messages.", messages.len());
626
    }
627

            
628
5
    let mut failures = Vec::with_capacity(messages.len());
629

            
630
5
    let send_mail = tx.conf().send_mail.clone();
631
5
    match send_mail {
632
        mailpot::SendMail::ShellCommand(cmd) => {
633
            fn submit(cmd: &str, msg: &QueueEntry, dry_run: bool) -> Result<()> {
634
                if dry_run {
635
                    return Ok(());
636
                }
637
                let mut child = std::process::Command::new("sh")
638
                    .arg("-c")
639
                    .arg(cmd)
640
                    .env("TO_ADDRESS", msg.to_addresses.clone())
641
                    .stdout(Stdio::piped())
642
                    .stdin(Stdio::piped())
643
                    .stderr(Stdio::piped())
644
                    .spawn()
645
                    .context("sh command failed to start")?;
646
                let mut stdin = child
647
                    .stdin
648
                    .take()
649
                    .ok_or_else(|| Error::from("Failed to open stdin"))?;
650

            
651
                let builder = std::thread::Builder::new();
652

            
653
                std::thread::scope(|s| {
654
                    let handler = builder
655
                        .spawn_scoped(s, move || {
656
                            stdin
657
                                .write_all(&msg.message)
658
                                .expect("Failed to write to stdin");
659
                        })
660
                        .context(
661
                            "Could not spawn IPC communication thread for SMTP ShellCommand \
662
                             process",
663
                        )?;
664

            
665
                    handler.join().map_err(|_| {
666
                        ErrorKind::External(mailpot::anyhow::anyhow!(
667
                            "Could not join with IPC communication thread for SMTP ShellCommand \
668
                             process"
669
                        ))
670
                    })?;
671
                    let result = child.wait_with_output()?;
672
                    if !result.status.success() {
673
                        return Err(Error::new_external(format!(
674
                            "{} proccess failed with exit code: {:?}\n{}",
675
                            cmd,
676
                            result.status.code(),
677
                            String::from_utf8(result.stderr).unwrap()
678
                        )));
679
                    }
680
                    Ok::<(), Error>(())
681
                })?;
682
                Ok(())
683
            }
684
            for msg in messages {
685
                if let Err(err) = submit(&cmd, &msg, dry_run) {
686
                    if verbose > 0 || debug {
687
                        eprintln!("Message {msg:?} failed with: {err}.");
688
                    }
689
                    failures.push((err, msg));
690
                } else if verbose > 0 || debug {
691
                    eprintln!("Submitted message {}", msg.message_id);
692
                }
693
            }
694
        }
695
        mailpot::SendMail::Smtp(_) => {
696
5
            let conn_future = tx.new_smtp_connection()?;
697
35
            failures = smol::future::block_on(smol::spawn(async move {
698
10
                let mut conn = conn_future.await?;
699
10
                for msg in messages {
700
25
                    if let Err(err) = Connection::submit(&mut conn, &msg, dry_run).await {
701
                        failures.push((err, msg));
702
                    }
703
5
                }
704
5
                Ok::<_, Error>(failures)
705
5
            }))?;
706
5
        }
707
    }
708

            
709
5
    for (err, mut msg) in failures {
710
        log::error!("Message {msg:?} failed with: {err}. Inserting to Deferred queue.");
711

            
712
        msg.queue = mailpot::queue::Queue::Deferred;
713
        tx.insert_to_queue(msg)?;
714
    }
715

            
716
10
    if !dry_run {
717
10
        tx.commit()?;
718
    }
719
5
    Ok(())
720
5
}
721

            
722
pub fn queue_(db: &mut Connection, queue: Queue, cmd: QueueCommand, quiet: bool) -> Result<()> {
723
    match cmd {
724
        QueueCommand::List => {
725
            let entries = db.queue(queue)?;
726
            if entries.is_empty() {
727
                if !quiet {
728
                    println!("Queue {queue} is empty.");
729
                }
730
            } else {
731
                for e in entries {
732
                    println!(
733
                        "- {} {} {} {} {}",
734
                        e.pk, e.datetime, e.from_address, e.to_addresses, e.subject
735
                    );
736
                }
737
            }
738
        }
739
        QueueCommand::Print { index } => {
740
            let mut entries = db.queue(queue)?;
741
            if !index.is_empty() {
742
                entries.retain(|el| index.contains(&el.pk()));
743
            }
744
            if entries.is_empty() {
745
                if !quiet {
746
                    println!("Queue {queue} is empty.");
747
                }
748
            } else {
749
                for e in entries {
750
                    println!("{e:?}");
751
                }
752
            }
753
        }
754
        QueueCommand::Delete { index } => {
755
            let mut entries = db.queue(queue)?;
756
            if !index.is_empty() {
757
                entries.retain(|el| index.contains(&el.pk()));
758
            }
759
            if entries.is_empty() {
760
                if !quiet {
761
                    println!("Queue {queue} is empty.");
762
                }
763
            } else {
764
                if !quiet {
765
                    println!("Deleting queue {queue} elements {:?}", &index);
766
                }
767
                db.delete_from_queue(queue, index)?;
768
                if !quiet {
769
                    for e in entries {
770
                        println!("{e:?}");
771
                    }
772
                }
773
            }
774
        }
775
    }
776
    Ok(())
777
}
778

            
779
pub fn import_maildir(
780
    db: &mut Connection,
781
    list_id: &str,
782
    mut maildir_path: PathBuf,
783
    quiet: bool,
784
    debug: bool,
785
    verbose: u8,
786
) -> Result<()> {
787
    let list = match list!(db, list_id) {
788
        Some(v) => v,
789
        None => {
790
            return Err(format!("No list with id or pk {} was found", list_id).into());
791
        }
792
    };
793
    if !maildir_path.is_absolute() {
794
        maildir_path = std::env::current_dir()
795
            .context("could not detect current directory")?
796
            .join(&maildir_path);
797
    }
798

            
799
    fn get_file_hash(file: &std::path::Path) -> EnvelopeHash {
800
        let mut hasher = DefaultHasher::default();
801
        file.hash(&mut hasher);
802
        EnvelopeHash(hasher.finish())
803
    }
804
    let mut buf = Vec::with_capacity(4096);
805
    let files = melib::maildir::MaildirType::list_mail_in_maildir_fs(maildir_path, true)
806
        .context("Could not parse files in maildir path")?;
807
    let mut ctr = 0;
808
    for file in files {
809
        let hash = get_file_hash(&file);
810
        let mut reader = std::io::BufReader::new(
811
            std::fs::File::open(&file)
812
                .with_context(|| format!("Could not open {}.", file.display()))?,
813
        );
814
        buf.clear();
815
        reader
816
            .read_to_end(&mut buf)
817
            .with_context(|| format!("Could not read from {}.", file.display()))?;
818
        match Envelope::from_bytes(buf.as_slice(), Some(file.flags())) {
819
            Ok(mut env) => {
820
                env.set_hash(hash);
821
                if verbose > 1 {
822
                    println!(
823
                        "Inserting post from {:?} with subject `{}` and Message-ID `{}`.",
824
                        env.from(),
825
                        env.subject(),
826
                        env.message_id()
827
                    );
828
                }
829
                db.insert_post(list.pk, &buf, &env).with_context(|| {
830
                    format!(
831
                        "Could not insert post `{}` from path `{}`",
832
                        env.message_id(),
833
                        file.display()
834
                    )
835
                })?;
836
                ctr += 1;
837
            }
838
            Err(err) => {
839
                if verbose > 0 || debug {
840
                    log::error!(
841
                        "Could not parse Envelope from file {}: {err}",
842
                        file.display()
843
                    );
844
                }
845
            }
846
        }
847
    }
848
    if !quiet {
849
        println!("Inserted {} posts to {}.", ctr, list_id);
850
    }
851
    Ok(())
852
}
853

            
854
pub fn update_postfix_config(
855
    config_path: &Path,
856
    db: &mut Connection,
857
    master_cf: Option<PathBuf>,
858
    PostfixConfig {
859
        user,
860
        group,
861
        binary_path,
862
        process_limit,
863
        map_output_path,
864
        transport_name,
865
    }: PostfixConfig,
866
) -> Result<()> {
867
    let pfconf = mailpot::postfix::PostfixConfiguration {
868
        user: user.into(),
869
        group: group.map(Into::into),
870
        binary_path,
871
        process_limit,
872
        map_output_path,
873
        transport_name: transport_name.map(std::borrow::Cow::from),
874
    };
875
    pfconf
876
        .save_maps(db.conf())
877
        .context("Could not save maps.")?;
878
    pfconf
879
        .save_master_cf_entry(db.conf(), config_path, master_cf.as_deref())
880
        .context("Could not save master.cf file.")?;
881

            
882
    Ok(())
883
}
884

            
885
pub fn print_postfix_config(
886
    config_path: &Path,
887
    db: &mut Connection,
888
    PostfixConfig {
889
        user,
890
        group,
891
        binary_path,
892
        process_limit,
893
        map_output_path,
894
        transport_name,
895
    }: PostfixConfig,
896
) -> Result<()> {
897
    let pfconf = mailpot::postfix::PostfixConfiguration {
898
        user: user.into(),
899
        group: group.map(Into::into),
900
        binary_path,
901
        process_limit,
902
        map_output_path,
903
        transport_name: transport_name.map(std::borrow::Cow::from),
904
    };
905
    let lists = db.lists().context("Could not retrieve lists.")?;
906
    let lists_post_policies = lists
907
        .into_iter()
908
        .map(|l| {
909
            let pk = l.pk;
910
            Ok((
911
                l,
912
                db.list_post_policy(pk).with_context(|| {
913
                    format!("Could not retrieve list post policy for list_pk = {pk}.")
914
                })?,
915
            ))
916
        })
917
        .collect::<Result<Vec<(DbVal<MailingList>, Option<DbVal<PostPolicy>>)>>>()?;
918
    let maps = pfconf.generate_maps(&lists_post_policies);
919
    let mastercf = pfconf.generate_master_cf_entry(db.conf(), config_path);
920

            
921
    println!("{maps}\n\n{mastercf}\n");
922
    Ok(())
923
}
924

            
925
pub fn accounts(db: &mut Connection, quiet: bool) -> Result<()> {
926
    let accounts = db.accounts()?;
927
    if accounts.is_empty() {
928
        if !quiet {
929
            println!("No accounts found.");
930
        }
931
    } else {
932
        for a in accounts {
933
            println!("- {:?}", a);
934
        }
935
    }
936
    Ok(())
937
}
938

            
939
pub fn account_info(db: &mut Connection, address: &str, quiet: bool) -> Result<()> {
940
    if let Some(acc) = db.account_by_address(address)? {
941
        let subs = db
942
            .account_subscriptions(acc.pk())
943
            .context("Could not retrieve account subscriptions for this account.")?;
944
        if subs.is_empty() {
945
            if !quiet {
946
                println!("No subscriptions found.");
947
            }
948
        } else {
949
            for s in subs {
950
                let list = db
951
                    .list(s.list)
952
                    .with_context(|| {
953
                        format!(
954
                            "Found subscription with list_pk = {} but could not retrieve the \
955
                             list.\nListSubscription = {:?}",
956
                            s.list, s
957
                        )
958
                    })?
959
                    .ok_or_else(|| {
960
                        format!(
961
                            "Found subscription with list_pk = {} but no such list \
962
                             exists.\nListSubscription = {:?}",
963
                            s.list, s
964
                        )
965
                    })?;
966
                println!("- {:?} {}", s, list);
967
            }
968
        }
969
    } else {
970
        return Err(format!("Account with address {address} not found!").into());
971
    }
972
    Ok(())
973
}
974

            
975
pub fn add_account(
976
    db: &mut Connection,
977
    address: String,
978
    password: String,
979
    name: Option<String>,
980
    public_key: Option<String>,
981
    enabled: Option<bool>,
982
) -> Result<()> {
983
    db.add_account(Account {
984
        pk: 0,
985
        name,
986
        address,
987
        public_key,
988
        password,
989
        enabled: enabled.unwrap_or(true),
990
    })?;
991
    Ok(())
992
}
993

            
994
pub fn remove_account(db: &mut Connection, address: &str, quiet: bool) -> Result<()> {
995
    let mut input = String::new();
996
    if !quiet {
997
        loop {
998
            println!(
999
                "Are you sure you want to remove account with address {}? [Yy/n]",
                address
            );
            input.clear();
            std::io::stdin().read_line(&mut input)?;
            if input.trim() == "Y" || input.trim() == "y" || input.trim() == "" {
                break;
            } else if input.trim() == "n" {
                return Ok(());
            }
        }
    }
    db.remove_account(address)?;
    Ok(())
}
pub fn update_account(
    db: &mut Connection,
    address: String,
    password: Option<String>,
    name: Option<Option<String>>,
    public_key: Option<Option<String>>,
    enabled: Option<Option<bool>>,
) -> Result<()> {
    let changeset = AccountChangeset {
        address,
        name,
        public_key,
        password,
        enabled,
    };
    db.update_account(changeset)?;
    Ok(())
}
pub fn repair(
    db: &mut Connection,
    fix: bool,
    all: bool,
    mut datetime_header_value: bool,
    mut remove_empty_accounts: bool,
    mut remove_accepted_subscription_requests: bool,
    mut warn_list_no_owner: bool,
) -> Result<()> {
    type LintFn = fn(&'_ mut mailpot::Connection, bool) -> std::result::Result<(), mailpot::Error>;
    let dry_run = !fix;
    if all {
        datetime_header_value = true;
        remove_empty_accounts = true;
        remove_accepted_subscription_requests = true;
        warn_list_no_owner = true;
    }
    if !(datetime_header_value
        | remove_empty_accounts
        | remove_accepted_subscription_requests
        | warn_list_no_owner)
    {
        return Err("No lints selected: specify them with flag arguments. See --help".into());
    }
    if dry_run {
        println!("running without making modifications (dry run)");
    }
    for (name, flag, lint_fn) in [
        (
            stringify!(datetime_header_value),
            datetime_header_value,
            datetime_header_value_lint as LintFn,
        ),
        (
            stringify!(remove_empty_accounts),
            remove_empty_accounts,
            remove_empty_accounts_lint as _,
        ),
        (
            stringify!(remove_accepted_subscription_requests),
            remove_accepted_subscription_requests,
            remove_accepted_subscription_requests_lint as _,
        ),
        (
            stringify!(warn_list_no_owner),
            warn_list_no_owner,
            warn_list_no_owner_lint as _,
        ),
    ] {
        if flag {
            lint_fn(db, dry_run).with_context(|| format!("Lint {name} failed."))?;
        }
    }
    Ok(())
}