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
//! Processing new posts.
21

            
22
use std::borrow::Cow;
23

            
24
use log::{info, trace};
25
use melib::Envelope;
26
use rusqlite::OptionalExtension;
27

            
28
use crate::{
29
    errors::*,
30
    mail::{ListContext, ListRequest, PostAction, PostEntry},
31
    models::{changesets::AccountChangeset, Account, DbVal, ListSubscription, MailingList, Post},
32
    queue::{Queue, QueueEntry},
33
    templates::Template,
34
    Connection,
35
};
36

            
37
impl Connection {
38
    /// Insert a mailing list post into the database.
39
9
    pub fn insert_post(&self, list_pk: i64, message: &[u8], env: &Envelope) -> Result<i64> {
40
9
        let from_ = env.from();
41
9
        let address = if from_.is_empty() {
42
            String::new()
43
        } else {
44
9
            from_[0].get_email()
45
        };
46
9
        let datetime: std::borrow::Cow<'_, str> = if !env.date.is_empty() {
47
7
            env.date.as_str().into()
48
        } else {
49
4
            melib::utils::datetime::timestamp_to_string(
50
2
                env.timestamp,
51
2
                Some(melib::utils::datetime::formats::RFC822_DATE),
52
                true,
53
            )
54
            .into()
55
        };
56
9
        let message_id = env.message_id_display();
57
9
        let mut stmt = self.connection.prepare(
58
            "INSERT OR REPLACE INTO post(list, address, message_id, message, datetime, timestamp) \
59
             VALUES(?, ?, ?, ?, ?, ?) RETURNING pk;",
60
        )?;
61
9
        let pk = stmt.query_row(
62
9
            rusqlite::params![
63
9
                &list_pk,
64
9
                &address,
65
9
                &message_id,
66
9
                &message,
67
9
                &datetime,
68
9
                &env.timestamp
69
            ],
70
9
            |row| {
71
9
                let pk: i64 = row.get("pk")?;
72
9
                Ok(pk)
73
9
            },
74
        )?;
75

            
76
9
        trace!(
77
            "insert_post list_pk {}, from {:?} message_id {:?} post_pk {}.",
78
            list_pk,
79
            address,
80
            message_id,
81
            pk
82
        );
83
9
        Ok(pk)
84
9
    }
85

            
86
    /// Process a new mailing list post.
87
    ///
88
    /// In case multiple processes can access the database at any time, use an
89
    /// `EXCLUSIVE` transaction before calling this function.
90
    /// See [`Connection::transaction`].
91
28
    pub fn post(&self, env: &Envelope, raw: &[u8], _dry_run: bool) -> Result<()> {
92
28
        let result = self.inner_post(env, raw, _dry_run);
93
28
        if let Err(err) = result {
94
            return match self.insert_to_queue(QueueEntry::new(
95
                Queue::Error,
96
                None,
97
                Some(Cow::Borrowed(env)),
98
                raw,
99
                Some(err.to_string()),
100
            )?) {
101
                Ok(idx) => {
102
                    log::info!(
103
                        "Inserted mail from {:?} into error_queue at index {}",
104
                        env.from(),
105
                        idx
106
                    );
107
                    Err(err)
108
                }
109
                Err(err2) => {
110
                    log::error!(
111
                        "Could not insert mail from {:?} into error_queue: {err2}",
112
                        env.from(),
113
                    );
114

            
115
                    Err(err.chain_err(|| err2))
116
                }
117
            };
118
        }
119
28
        result
120
56
    }
121

            
122
28
    fn inner_post(&self, env: &Envelope, raw: &[u8], _dry_run: bool) -> Result<()> {
123
        trace!("Received envelope to post: {:#?}", &env);
124
28
        let tos = env.to().to_vec();
125
28
        if tos.is_empty() {
126
            return Err("Envelope To: field is empty!".into());
127
        }
128
28
        if env.from().is_empty() {
129
            return Err("Envelope From: field is empty!".into());
130
        }
131
28
        let mut lists = self.lists()?;
132
28
        let prev_list_len = lists.len();
133
42
        for t in &tos {
134
28
            if let Some((addr, subaddr)) = t.subaddress("+") {
135
28
                lists.retain(|list| {
136
14
                    if !addr.contains_address(&list.address()) {
137
                        return true;
138
                    }
139
42
                    if let Err(err) = ListRequest::try_from((subaddr.as_str(), env))
140
42
                        .and_then(|req| self.request(list, req, env, raw))
141
                    {
142
                        info!("Processing request returned error: {}", err);
143
14
                    }
144
14
                    false
145
14
                });
146
14
                if lists.len() != prev_list_len {
147
                    // Was request, handled above.
148
14
                    return Ok(());
149
                }
150
28
            }
151
28
        }
152

            
153
28
        lists.retain(|list| {
154
            trace!(
155
                "Is post related to list {}? {}",
156
                &list,
157
10
                tos.iter().any(|a| a.contains_address(&list.address()))
158
            );
159

            
160
28
            tos.iter().any(|a| a.contains_address(&list.address()))
161
14
        });
162
14
        if lists.is_empty() {
163
            return Err(format!(
164
                "No relevant mailing list found for these addresses: {:?}",
165
                tos
166
            )
167
            .into());
168
        }
169

            
170
14
        trace!("Configuration is {:#?}", &self.conf);
171
28
        for mut list in lists {
172
14
            trace!("Examining list {}", list.display_name());
173
14
            let filters = self.list_filters(&list);
174
14
            let subscriptions = self.list_subscriptions(list.pk)?;
175
14
            let owners = self.list_owners(list.pk)?;
176
14
            trace!("List subscriptions {:#?}", &subscriptions);
177
14
            let mut list_ctx = ListContext {
178
14
                post_policy: self.list_post_policy(list.pk)?,
179
14
                subscription_policy: self.list_subscription_policy(list.pk)?,
180
14
                list_owners: &owners,
181
14
                subscriptions: &subscriptions,
182
14
                scheduled_jobs: vec![],
183
14
                filter_settings: self.get_settings(list.pk)?,
184
14
                list: &mut list,
185
            };
186
14
            let mut post = PostEntry {
187
14
                message_id: env.message_id().clone(),
188
14
                from: env.from()[0].clone(),
189
14
                bytes: raw.to_vec(),
190
14
                to: env.to().to_vec(),
191
14
                action: PostAction::Hold,
192
            };
193
28
            let result = filters
194
                .into_iter()
195
96
                .try_fold((&mut post, &mut list_ctx), |(p, c), f| f.feed(p, c));
196
14
            trace!("result {:#?}", result);
197

            
198
14
            let PostEntry { bytes, action, .. } = post;
199
14
            trace!("Action is {:#?}", action);
200
14
            let post_env = melib::Envelope::from_bytes(&bytes, None)?;
201
14
            match action {
202
                PostAction::Accept => {
203
9
                    let _post_pk = self.insert_post(list_ctx.list.pk, &bytes, &post_env)?;
204
9
                    trace!("post_pk is {:#?}", _post_pk);
205
18
                    for job in list_ctx.scheduled_jobs.iter() {
206
9
                        trace!("job is {:#?}", &job);
207
18
                        if let crate::mail::MailJob::Send { recipients } = job {
208
9
                            trace!("recipients: {:?}", &recipients);
209
9
                            if recipients.is_empty() {
210
                                trace!("list has no recipients");
211
                            }
212
15
                            for recipient in recipients {
213
6
                                let mut env = post_env.clone();
214
6
                                env.set_to(melib::smallvec::smallvec![recipient.clone()]);
215
6
                                self.insert_to_queue(QueueEntry::new(
216
6
                                    Queue::Out,
217
6
                                    Some(list.pk),
218
6
                                    Some(Cow::Owned(env)),
219
6
                                    &bytes,
220
6
                                    None,
221
12
                                )?)?;
222
                            }
223
                        }
224
                    }
225
                }
226
4
                PostAction::Reject { reason } => {
227
4
                    log::info!("PostAction::Reject {{ reason: {} }}", reason);
228
8
                    for f in env.from() {
229
                        /* send error notice to e-mail sender */
230
4
                        self.send_reply_with_list_template(
231
4
                            TemplateRenderContext {
232
                                template: Template::GENERIC_FAILURE,
233
4
                                default_fn: Some(Template::default_generic_failure),
234
                                list: &list,
235
8
                                context: minijinja::context! {
236
                                    list => &list,
237
                                    subject => format!("Your post to {} was rejected.", list.id),
238
                                    details => &reason,
239
                                },
240
4
                                queue: Queue::Out,
241
4
                                comment: format!("PostAction::Reject {{ reason: {} }}", reason)
242
                                    .into(),
243
                            },
244
4
                            std::iter::once(Cow::Borrowed(f)),
245
                        )?;
246
                    }
247
                    /* error handled by notifying submitter */
248
4
                    return Ok(());
249
4
                }
250
1
                PostAction::Defer { reason } => {
251
1
                    trace!("PostAction::Defer {{ reason: {} }}", reason);
252
2
                    for f in env.from() {
253
                        /* send error notice to e-mail sender */
254
29
                        self.send_reply_with_list_template(
255
1
                            TemplateRenderContext {
256
                                template: Template::GENERIC_FAILURE,
257
1
                                default_fn: Some(Template::default_generic_failure),
258
                                list: &list,
259
2
                                context: minijinja::context! {
260
                                    list => &list,
261
                                    subject => format!("Your post to {} was deferred.", list.id),
262
                                    details => &reason,
263
                                },
264
1
                                queue: Queue::Out,
265
1
                                comment: format!("PostAction::Defer {{ reason: {} }}", reason)
266
                                    .into(),
267
                            },
268
1
                            std::iter::once(Cow::Borrowed(f)),
269
                        )?;
270
                    }
271
1
                    self.insert_to_queue(QueueEntry::new(
272
1
                        Queue::Deferred,
273
1
                        Some(list.pk),
274
1
                        Some(Cow::Borrowed(&post_env)),
275
1
                        &bytes,
276
1
                        Some(format!("PostAction::Defer {{ reason: {} }}", reason)),
277
2
                    )?)?;
278
1
                    return Ok(());
279
1
                }
280
                PostAction::Hold => {
281
                    trace!("PostAction::Hold");
282
                    self.insert_to_queue(QueueEntry::new(
283
                        Queue::Hold,
284
                        Some(list.pk),
285
                        Some(Cow::Borrowed(&post_env)),
286
                        &bytes,
287
                        Some("PostAction::Hold".to_string()),
288
                    )?)?;
289
                    return Ok(());
290
                }
291
            }
292
14
        }
293

            
294
9
        Ok(())
295
28
    }
296

            
297
    /// Process a new mailing list request.
298
14
    pub fn request(
299
        &self,
300
        list: &DbVal<MailingList>,
301
        request: ListRequest,
302
        env: &Envelope,
303
        raw: &[u8],
304
    ) -> Result<()> {
305
14
        match request {
306
            ListRequest::Help => {
307
1
                trace!(
308
                    "help action for addresses {:?} in list {}",
309
                    env.from(),
310
                    list
311
                );
312
1
                let subscription_policy = self.list_subscription_policy(list.pk)?;
313
1
                let post_policy = self.list_post_policy(list.pk)?;
314
1
                let subject = format!("Help for {}", list.name);
315
2
                let details = list
316
1
                    .generate_help_email(post_policy.as_deref(), subscription_policy.as_deref());
317
2
                for f in env.from() {
318
1
                    self.send_reply_with_list_template(
319
1
                        TemplateRenderContext {
320
                            template: Template::GENERIC_HELP,
321
1
                            default_fn: Some(Template::default_generic_help),
322
1
                            list,
323
2
                            context: minijinja::context! {
324
                                list => &list,
325
                                subject => &subject,
326
                                details => &details,
327
                            },
328
1
                            queue: Queue::Out,
329
1
                            comment: "Help request".into(),
330
                        },
331
1
                        std::iter::once(Cow::Borrowed(f)),
332
                    )?;
333
                }
334
1
            }
335
9
            ListRequest::Subscribe => {
336
9
                trace!(
337
                    "subscribe action for addresses {:?} in list {}",
338
                    env.from(),
339
                    list
340
                );
341
9
                let subscription_policy = self.list_subscription_policy(list.pk)?;
342
9
                let approval_needed = subscription_policy
343
                    .as_ref()
344
                    .map(|p| !p.open)
345
                    .unwrap_or(false);
346
18
                for f in env.from() {
347
18
                    let email_from = f.get_email();
348
9
                    if self
349
9
                        .list_subscription_by_address(list.pk, &email_from)
350
9
                        .is_ok()
351
                    {
352
                        /* send error notice to e-mail sender */
353
                        self.send_reply_with_list_template(
354
                            TemplateRenderContext {
355
                                template: Template::GENERIC_FAILURE,
356
                                default_fn: Some(Template::default_generic_failure),
357
                                list,
358
                                context: minijinja::context! {
359
                                    list => &list,
360
                                    subject => format!("You are already subscribed to {}.", list.id),
361
                                    details => "No action has been taken since you are already subscribed to the list.",
362
                                },
363
                                queue: Queue::Out,
364
                                comment: format!("Address {} is already subscribed to list {}", f, list.id).into(),
365
                            },
366
                            std::iter::once(Cow::Borrowed(f)),
367
                        )?;
368
                        continue;
369
                    }
370

            
371
9
                    let subscription = ListSubscription {
372
                        pk: 0,
373
9
                        list: list.pk,
374
9
                        address: f.get_email(),
375
9
                        account: None,
376
9
                        name: f.get_display_name(),
377
                        digest: false,
378
                        hide_address: false,
379
                        receive_duplicates: true,
380
                        receive_own_posts: false,
381
                        receive_confirmation: true,
382
9
                        enabled: !approval_needed,
383
                        verified: true,
384
                    };
385
18
                    if approval_needed {
386
                        match self.add_candidate_subscription(list.pk, subscription) {
387
                            Ok(v) => {
388
                                let list_owners = self.list_owners(list.pk)?;
389
                                self.send_reply_with_list_template(
390
                                    TemplateRenderContext {
391
                                        template: Template::SUBSCRIPTION_REQUEST_NOTICE_OWNER,
392
                                        default_fn: Some(
393
                                            Template::default_subscription_request_owner,
394
                                        ),
395
                                        list,
396
                                        context: minijinja::context! {
397
                                            list => &list,
398
                                            candidate => &v,
399
                                        },
400
                                        queue: Queue::Out,
401
                                        comment: Template::SUBSCRIPTION_REQUEST_NOTICE_OWNER.into(),
402
                                    },
403
                                    list_owners.iter().map(|owner| Cow::Owned(owner.address())),
404
                                )?;
405
                            }
406
                            Err(err) => {
407
                                log::error!(
408
                                    "Could not create candidate subscription for {f:?}: {err}"
409
                                );
410
                                /* send error notice to e-mail sender */
411
                                self.send_reply_with_list_template(
412
                                    TemplateRenderContext {
413
                                        template: Template::GENERIC_FAILURE,
414
                                        default_fn: Some(Template::default_generic_failure),
415
                                        list,
416
                                        context: minijinja::context! {
417
                                            list => &list,
418
                                        },
419
                                        queue: Queue::Out,
420
                                        comment: format!(
421
                                            "Could not create candidate subscription for {f:?}: \
422
                                             {err}"
423
                                        )
424
                                        .into(),
425
                                    },
426
                                    std::iter::once(Cow::Borrowed(f)),
427
                                )?;
428

            
429
                                /* send error details to list owners */
430

            
431
                                let list_owners = self.list_owners(list.pk)?;
432
                                self.send_reply_with_list_template(
433
                                    TemplateRenderContext {
434
                                        template: Template::ADMIN_NOTICE,
435
                                        default_fn: Some(Template::default_admin_notice),
436
                                        list,
437
                                        context: minijinja::context! {
438
                                            list => &list,
439
                                            details => err.to_string(),
440
                                        },
441
                                        queue: Queue::Out,
442
                                        comment: format!(
443
                                            "Could not create candidate subscription for {f:?}: \
444
                                             {err}"
445
                                        )
446
                                        .into(),
447
                                    },
448
                                    list_owners.iter().map(|owner| Cow::Owned(owner.address())),
449
                                )?;
450
                            }
451
                        }
452
9
                    } else if let Err(err) = self.add_subscription(list.pk, subscription) {
453
                        log::error!("Could not create subscription for {f:?}: {err}");
454

            
455
                        /* send error notice to e-mail sender */
456

            
457
                        self.send_reply_with_list_template(
458
                            TemplateRenderContext {
459
                                template: Template::GENERIC_FAILURE,
460
                                default_fn: Some(Template::default_generic_failure),
461
                                list,
462
                                context: minijinja::context! {
463
                                    list => &list,
464
                                },
465
                                queue: Queue::Out,
466
                                comment: format!("Could not create subscription for {f:?}: {err}")
467
                                    .into(),
468
                            },
469
                            std::iter::once(Cow::Borrowed(f)),
470
                        )?;
471

            
472
                        /* send error details to list owners */
473

            
474
                        let list_owners = self.list_owners(list.pk)?;
475
                        self.send_reply_with_list_template(
476
                            TemplateRenderContext {
477
                                template: Template::ADMIN_NOTICE,
478
                                default_fn: Some(Template::default_admin_notice),
479
                                list,
480
                                context: minijinja::context! {
481
                                    list => &list,
482
                                    details => err.to_string(),
483
                                },
484
                                queue: Queue::Out,
485
                                comment: format!("Could not create subscription for {f:?}: {err}")
486
                                    .into(),
487
                            },
488
                            list_owners.iter().map(|owner| Cow::Owned(owner.address())),
489
                        )?;
490
                    } else {
491
9
                        self.send_subscription_confirmation(list, f)?;
492
9
                    }
493
9
                }
494
9
            }
495
2
            ListRequest::Unsubscribe => {
496
2
                trace!(
497
                    "unsubscribe action for addresses {:?} in list {}",
498
                    env.from(),
499
                    list
500
                );
501
4
                for f in env.from() {
502
4
                    if let Err(err) = self.remove_subscription(list.pk, &f.get_email()) {
503
                        log::error!("Could not unsubscribe {f:?}: {err}");
504
                        /* send error notice to e-mail sender */
505

            
506
                        self.send_reply_with_list_template(
507
                            TemplateRenderContext {
508
                                template: Template::GENERIC_FAILURE,
509
                                default_fn: Some(Template::default_generic_failure),
510
                                list,
511
                                context: minijinja::context! {
512
                                    list => &list,
513
                                },
514
                                queue: Queue::Out,
515
                                comment: format!("Could not unsubscribe {f:?}: {err}").into(),
516
                            },
517
                            std::iter::once(Cow::Borrowed(f)),
518
                        )?;
519

            
520
                        /* send error details to list owners */
521

            
522
                        let list_owners = self.list_owners(list.pk)?;
523
                        self.send_reply_with_list_template(
524
                            TemplateRenderContext {
525
                                template: Template::ADMIN_NOTICE,
526
                                default_fn: Some(Template::default_admin_notice),
527
                                list,
528
                                context: minijinja::context! {
529
                                    list => &list,
530
                                    details => err.to_string(),
531
                                },
532
                                queue: Queue::Out,
533
                                comment: format!("Could not unsubscribe {f:?}: {err}").into(),
534
                            },
535
                            list_owners.iter().map(|owner| Cow::Owned(owner.address())),
536
                        )?;
537
                    } else {
538
2
                        self.send_unsubscription_confirmation(list, f)?;
539
                    }
540
2
                }
541
            }
542
2
            ListRequest::Other(ref req) if req == "owner" => {
543
                trace!(
544
                    "list-owner mail action for addresses {:?} in list {}",
545
                    env.from(),
546
                    list
547
                );
548
                return Err("list-owner emails are not implemented yet.".into());
549
                //FIXME: mail to list-owner
550
                /*
551
                for _owner in self.list_owners(list.pk)? {
552
                        self.insert_to_queue(
553
                            Queue::Out,
554
                            Some(list.pk),
555
                            None,
556
                            draft.finalise()?.as_bytes(),
557
                            "list-owner-forward".to_string(),
558
                        )?;
559
                }
560
                */
561
            }
562
2
            ListRequest::Other(ref req) if req.trim().eq_ignore_ascii_case("password") => {
563
                trace!(
564
                    "list-request password set action for addresses {:?} in list {list}",
565
                    env.from(),
566
                );
567
2
                let body = env.body_bytes(raw);
568
2
                let password = body.text();
569
                // TODO: validate SSH public key with `ssh-keygen`.
570
4
                for f in env.from() {
571
4
                    let email_from = f.get_email();
572
2
                    if let Ok(sub) = self.list_subscription_by_address(list.pk, &email_from) {
573
2
                        match self.account_by_address(&email_from)? {
574
1
                            Some(_acc) => {
575
1
                                let changeset = AccountChangeset {
576
1
                                    address: email_from.clone(),
577
1
                                    name: None,
578
1
                                    public_key: None,
579
1
                                    password: Some(password.clone()),
580
1
                                    enabled: None,
581
                                };
582
15
                                self.update_account(changeset)?;
583
1
                            }
584
                            None => {
585
                                // Create new account.
586
1
                                self.add_account(Account {
587
                                    pk: 0,
588
1
                                    name: sub.name.clone(),
589
1
                                    address: sub.address.clone(),
590
1
                                    public_key: None,
591
1
                                    password: password.clone(),
592
1
                                    enabled: sub.enabled,
593
1
                                })?;
594
                            }
595
                        }
596
2
                    }
597
2
                }
598
2
            }
599
            ListRequest::RetrieveMessages(ref message_ids) => {
600
                trace!(
601
                    "retrieve messages {message_ids:?} action for addresses {:?} in list {list}",
602
                    env.from(),
603
                );
604
                return Err("message retrievals are not implemented yet.".into());
605
            }
606
            ListRequest::RetrieveArchive(ref from, ref to) => {
607
                trace!(
608
                    "retrieve archive action from {from:?} to {to:?} for addresses {:?} in list \
609
                     {list}",
610
                    env.from(),
611
                );
612
                return Err("message retrievals are not implemented yet.".into());
613
            }
614
            ListRequest::ChangeSetting(ref setting, ref toggle) => {
615
                trace!(
616
                    "change setting {setting}, request with value {toggle:?} for addresses {:?} \
617
                     in list {list}",
618
                    env.from(),
619
                );
620
                return Err("setting digest options via e-mail is not implemented yet.".into());
621
            }
622
            ListRequest::Other(ref req) => {
623
                trace!(
624
                    "unknown request action {req} for addresses {:?} in list {list}",
625
                    env.from(),
626
                );
627
                return Err(format!("Unknown request {req}.").into());
628
            }
629
        }
630
14
        Ok(())
631
14
    }
632

            
633
    /// Fetch all year and month values for which at least one post exists in
634
    /// `yyyy-mm` format.
635
3
    pub fn months(&self, list_pk: i64) -> Result<Vec<String>> {
636
3
        let mut stmt = self.connection.prepare(
637
            "SELECT DISTINCT strftime('%Y-%m', CAST(timestamp AS INTEGER), 'unixepoch') FROM post \
638
             WHERE list = ?;",
639
        )?;
640
6
        let months_iter = stmt.query_map([list_pk], |row| {
641
3
            let val: String = row.get(0)?;
642
3
            Ok(val)
643
3
        })?;
644

            
645
3
        let mut ret = vec![];
646
6
        for month in months_iter {
647
3
            let month = month?;
648
3
            ret.push(month);
649
        }
650
3
        Ok(ret)
651
3
    }
652

            
653
    /// Find a post by its `Message-ID` email header.
654
3
    pub fn list_post_by_message_id(
655
        &self,
656
        list_pk: i64,
657
        message_id: &str,
658
    ) -> Result<Option<DbVal<Post>>> {
659
3
        let mut stmt = self.connection.prepare(
660
            "SELECT *, strftime('%Y-%m', CAST(timestamp AS INTEGER), 'unixepoch') AS month_year \
661
             FROM post WHERE list = ? AND message_id = ?;",
662
        )?;
663
3
        let ret = stmt
664
6
            .query_row(rusqlite::params![&list_pk, &message_id], |row| {
665
3
                let pk = row.get("pk")?;
666
3
                Ok(DbVal(
667
3
                    Post {
668
                        pk,
669
3
                        list: row.get("list")?,
670
3
                        envelope_from: row.get("envelope_from")?,
671
3
                        address: row.get("address")?,
672
3
                        message_id: row.get("message_id")?,
673
3
                        message: row.get("message")?,
674
3
                        timestamp: row.get("timestamp")?,
675
3
                        datetime: row.get("datetime")?,
676
3
                        month_year: row.get("month_year")?,
677
                    },
678
                    pk,
679
                ))
680
3
            })
681
3
            .optional()?;
682

            
683
3
        Ok(ret)
684
3
    }
685

            
686
    /// Helper function to send a template reply.
687
17
    pub fn send_reply_with_list_template<'ctx, F: Fn() -> Template>(
688
        &self,
689
        render_context: TemplateRenderContext<'ctx, F>,
690
        recipients: impl Iterator<Item = Cow<'ctx, melib::Address>>,
691
    ) -> Result<()> {
692
        let TemplateRenderContext {
693
17
            template,
694
17
            default_fn,
695
17
            list,
696
17
            context,
697
17
            queue,
698
17
            comment,
699
        } = render_context;
700

            
701
34
        let post_policy = self.list_post_policy(list.pk)?;
702
17
        let subscription_policy = self.list_subscription_policy(list.pk)?;
703

            
704
17
        let templ = self
705
17
            .fetch_template(template, Some(list.pk))?
706
            .map(DbVal::into_inner)
707
47
            .or_else(|| default_fn.map(|f| f()))
708
17
            .ok_or_else(|| -> crate::Error {
709
                format!("Template with name {template:?} was not found.").into()
710
            })?;
711

            
712
17
        let mut draft = templ.render(context)?;
713
34
        draft
714
            .headers
715
34
            .insert(melib::HeaderName::FROM, list.request_subaddr());
716
34
        for addr in recipients {
717
17
            let mut draft = draft.clone();
718
34
            draft
719
                .headers
720
34
                .insert(melib::HeaderName::TO, addr.to_string());
721
34
            list.insert_headers(
722
                &mut draft,
723
17
                post_policy.as_deref(),
724
17
                subscription_policy.as_deref(),
725
            );
726
17
            self.insert_to_queue(QueueEntry::new(
727
                queue,
728
17
                Some(list.pk),
729
17
                None,
730
17
                draft.finalise()?.as_bytes(),
731
17
                Some(comment.to_string()),
732
34
            )?)?;
733
17
        }
734
17
        Ok(())
735
17
    }
736

            
737
    /// Send subscription confirmation.
738
9
    pub fn send_subscription_confirmation(
739
        &self,
740
        list: &DbVal<MailingList>,
741
        address: &melib::Address,
742
    ) -> Result<()> {
743
        log::trace!(
744
            "Added subscription to list {list:?} for address {address:?}, sending confirmation."
745
        );
746
9
        self.send_reply_with_list_template(
747
9
            TemplateRenderContext {
748
                template: Template::SUBSCRIPTION_CONFIRMATION,
749
9
                default_fn: Some(Template::default_subscription_confirmation),
750
9
                list,
751
9
                context: minijinja::context! {
752
                    list => &list,
753
                },
754
9
                queue: Queue::Out,
755
9
                comment: Template::SUBSCRIPTION_CONFIRMATION.into(),
756
            },
757
9
            std::iter::once(Cow::Borrowed(address)),
758
        )
759
9
    }
760

            
761
    /// Send unsubscription confirmation.
762
2
    pub fn send_unsubscription_confirmation(
763
        &self,
764
        list: &DbVal<MailingList>,
765
        address: &melib::Address,
766
    ) -> Result<()> {
767
        log::trace!(
768
            "Removed subscription to list {list:?} for address {address:?}, sending confirmation."
769
        );
770
2
        self.send_reply_with_list_template(
771
2
            TemplateRenderContext {
772
                template: Template::UNSUBSCRIPTION_CONFIRMATION,
773
2
                default_fn: Some(Template::default_unsubscription_confirmation),
774
2
                list,
775
2
                context: minijinja::context! {
776
                    list => &list,
777
                },
778
2
                queue: Queue::Out,
779
2
                comment: Template::UNSUBSCRIPTION_CONFIRMATION.into(),
780
            },
781
2
            std::iter::once(Cow::Borrowed(address)),
782
        )
783
2
    }
784
}
785

            
786
/// Helper type for [`Connection::send_reply_with_list_template`].
787
#[derive(Debug)]
788
pub struct TemplateRenderContext<'ctx, F: Fn() -> Template> {
789
    /// Template name.
790
    pub template: &'ctx str,
791
    /// If template is not found, call a function that returns one.
792
    pub default_fn: Option<F>,
793
    /// The pertinent list.
794
    pub list: &'ctx DbVal<MailingList>,
795
    /// [`minijinja`]'s template context.
796
    pub context: minijinja::value::Value,
797
    /// Destination queue in the database.
798
    pub queue: Queue,
799
    /// Comment for the queue entry in the database.
800
    pub comment: Cow<'static, str>,
801
}