1
30
/*
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::borrow::Cow;
21

            
22
use super::*;
23
use crate::mail::ListRequest;
24

            
25
impl Connection {
26
    /// Insert a mailing list post into the database.
27
5
    pub fn insert_post(&self, list_pk: i64, message: &[u8], env: &Envelope) -> Result<i64> {
28
5
        let from_ = env.from();
29
5
        let address = if from_.is_empty() {
30
            String::new()
31
        } else {
32
5
            from_[0].get_email()
33
        };
34
5
        let datetime: std::borrow::Cow<'_, str> = if env.timestamp != 0 {
35
5
            melib::datetime::timestamp_to_string(
36
5
                env.timestamp,
37
5
                Some(melib::datetime::RFC3339_FMT_WITH_TIME),
38
                true,
39
            )
40
            .into()
41
        } else {
42
            env.date.as_str().into()
43
        };
44
5
        let message_id = env.message_id_display();
45
5
        let mut stmt = self.connection.prepare(
46
            "INSERT OR REPLACE INTO post(list, address, message_id, message, datetime, timestamp) \
47
             VALUES(?, ?, ?, ?, ?, ?) RETURNING pk;",
48
        )?;
49
5
        let pk = stmt.query_row(
50
5
            rusqlite::params![
51
5
                &list_pk,
52
5
                &address,
53
5
                &message_id,
54
5
                &message,
55
5
                &datetime,
56
5
                &env.timestamp
57
            ],
58
5
            |row| {
59
5
                let pk: i64 = row.get("pk")?;
60
5
                Ok(pk)
61
5
            },
62
        )?;
63

            
64
5
        trace!(
65
            "insert_post list_pk {}, from {:?} message_id {:?} post_pk {}.",
66
            list_pk,
67
            address,
68
            message_id,
69
            pk
70
        );
71
5
        Ok(pk)
72
5
    }
73

            
74
    /// Process a new mailing list post.
75
20
    pub fn post(&mut self, env: &Envelope, raw: &[u8], _dry_run: bool) -> Result<()> {
76
20
        let result = self.inner_post(env, raw, _dry_run);
77
20
        if let Err(err) = result {
78
3
            return match self.insert_to_queue(QueueEntry::new(
79
3
                Queue::Error,
80
3
                None,
81
3
                Some(Cow::Borrowed(env)),
82
                raw,
83
3
                Some(err.to_string()),
84
            )?) {
85
3
                Ok(idx) => {
86
3
                    log::info!(
87
                        "Inserted mail from {:?} into error_queue at index {}",
88
3
                        env.from(),
89
                        idx
90
                    );
91
3
                    Err(err)
92
3
                }
93
                Err(err2) => {
94
                    log::error!(
95
                        "Could not insert mail from {:?} into error_queue: {err2}",
96
                        env.from(),
97
                    );
98

            
99
                    Err(err.chain_err(|| err2))
100
                }
101
            };
102
3
        }
103
17
        result
104
20
    }
105

            
106
20
    fn inner_post(&mut self, env: &Envelope, raw: &[u8], _dry_run: bool) -> Result<()> {
107
20
        trace!("Received envelope to post: {:#?}", &env);
108
20
        let tos = env.to().to_vec();
109
20
        if tos.is_empty() {
110
            return Err("Envelope To: field is empty!".into());
111
        }
112
20
        if env.from().is_empty() {
113
            return Err("Envelope From: field is empty!".into());
114
        }
115
20
        let mut lists = self.lists()?;
116
20
        if lists.is_empty() {
117
            return Err("No active mailing lists found.".into());
118
        }
119
20
        let prev_list_len = lists.len();
120
28
        for t in &tos {
121
20
            if let Some((addr, subaddr)) = t.subaddress("+") {
122
24
                lists.retain(|list| {
123
12
                    if !addr.contains_address(&list.address()) {
124
                        return true;
125
                    }
126
36
                    if let Err(err) = ListRequest::try_from((subaddr.as_str(), env))
127
36
                        .and_then(|req| self.request(list, req, env, raw))
128
                    {
129
                        info!("Processing request returned error: {}", err);
130
12
                    }
131
12
                    false
132
12
                });
133
12
                if lists.len() != prev_list_len {
134
                    // Was request, handled above.
135
12
                    return Ok(());
136
                }
137
12
            }
138
20
        }
139

            
140
16
        lists.retain(|list| {
141
8
            trace!(
142
                "Is post related to list {}? {}",
143
6
                &list,
144
12
                tos.iter().any(|a| a.contains_address(&list.address()))
145
            );
146

            
147
16
            tos.iter().any(|a| a.contains_address(&list.address()))
148
8
        });
149
8
        if lists.is_empty() {
150
            return Err(format!(
151
                "No relevant mailing list found for these addresses: {:?}",
152
                tos
153
            )
154
            .into());
155
        }
156

            
157
8
        trace!("Configuration is {:#?}", &self.conf);
158
        use crate::mail::{ListContext, Post, PostAction};
159
16
        for mut list in lists {
160
8
            trace!("Examining list {}", list.display_name());
161
8
            let filters = self.list_filters(&list);
162
8
            let subscriptions = self.list_subscriptions(list.pk)?;
163
8
            let owners = self.list_owners(list.pk)?;
164
8
            trace!("List subscriptions {:#?}", &subscriptions);
165
8
            let mut list_ctx = ListContext {
166
8
                post_policy: self.list_post_policy(list.pk)?,
167
8
                subscription_policy: self.list_subscription_policy(list.pk)?,
168
8
                list_owners: &owners,
169
8
                list: &mut list,
170
8
                subscriptions: &subscriptions,
171
8
                scheduled_jobs: vec![],
172
            };
173
8
            let mut post = Post {
174
8
                from: env.from()[0].clone(),
175
8
                bytes: raw.to_vec(),
176
8
                to: env.to().to_vec(),
177
8
                action: PostAction::Hold,
178
            };
179
16
            let result = filters
180
                .into_iter()
181
40
                .fold(Ok((&mut post, &mut list_ctx)), |p, f| {
182
58
                    p.and_then(|(p, c)| f.feed(p, c))
183
32
                });
184
8
            trace!("result {:#?}", result);
185

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

            
281
5
        Ok(())
282
20
    }
283

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

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

            
415
                                /* send error details to list owners */
416

            
417
                                let list_owners = self.list_owners(list.pk)?;
418
                                self.send_reply_with_list_template(
419
                                    TemplateRenderContext {
420
                                        template: Template::ADMIN_NOTICE,
421
                                        default_fn: Some(Template::default_admin_notice),
422
                                        list,
423
                                        context: minijinja::context! {
424
                                            list => &list,
425
                                            details => err.to_string(),
426
                                        },
427
                                        queue: Queue::Out,
428
                                        comment: format!(
429
                                            "Could not create candidate subscription for {f:?}: \
430
                                             {err}"
431
                                        )
432
                                        .into(),
433
                                    },
434
                                    list_owners.iter().map(|owner| Cow::Owned(owner.address())),
435
                                )?;
436
                            }
437
                        }
438
8
                    } else if let Err(err) = self.add_subscription(list.pk, subscription) {
439
                        log::error!("Could not create subscription for {f:?}: {err}");
440

            
441
                        /* send error notice to e-mail sender */
442

            
443
                        self.send_reply_with_list_template(
444
                            TemplateRenderContext {
445
                                template: Template::GENERIC_FAILURE,
446
                                default_fn: Some(Template::default_generic_failure),
447
                                list,
448
                                context: minijinja::context! {
449
                                    list => &list,
450
                                },
451
                                queue: Queue::Out,
452
                                comment: format!("Could not create subscription for {f:?}: {err}")
453
                                    .into(),
454
                            },
455
                            std::iter::once(Cow::Borrowed(f)),
456
                        )?;
457

            
458
                        /* send error details to list owners */
459

            
460
                        let list_owners = self.list_owners(list.pk)?;
461
                        self.send_reply_with_list_template(
462
                            TemplateRenderContext {
463
                                template: Template::ADMIN_NOTICE,
464
                                default_fn: Some(Template::default_admin_notice),
465
                                list,
466
                                context: minijinja::context! {
467
                                    list => &list,
468
                                    details => err.to_string(),
469
                                },
470
                                queue: Queue::Out,
471
                                comment: format!("Could not create subscription for {f:?}: {err}")
472
                                    .into(),
473
                            },
474
                            list_owners.iter().map(|owner| Cow::Owned(owner.address())),
475
                        )?;
476
                    } else {
477
8
                        log::trace!(
478
                            "Added subscription to list {list:?} for address {f:?}, sending \
479
                             confirmation."
480
                        );
481
8
                        self.send_reply_with_list_template(
482
8
                            TemplateRenderContext {
483
                                template: Template::SUBSCRIPTION_CONFIRMATION,
484
8
                                default_fn: Some(Template::default_subscription_confirmation),
485
                                list,
486
16
                                context: minijinja::context! {
487
8
                                    list => &list,
488
                                },
489
8
                                queue: Queue::Out,
490
8
                                comment: Template::SUBSCRIPTION_CONFIRMATION.into(),
491
                            },
492
8
                            std::iter::once(Cow::Borrowed(f)),
493
                        )?;
494
8
                    }
495
8
                }
496
            }
497
            ListRequest::Unsubscribe => {
498
1
                trace!(
499
                    "unsubscribe action for addresses {:?} in list {}",
500
1
                    env.from(),
501
                    list
502
                );
503
2
                for f in env.from() {
504
1
                    if let Err(err) = self.remove_subscription(list.pk, &f.get_email()) {
505
                        log::error!("Could not unsubscribe {f:?}: {err}");
506
                        /* send error notice to e-mail sender */
507

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

            
522
                        /* send error details to list owners */
523

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

            
647
    /// Fetch all year and month values for which at least one post exists in
648
    /// `yyyy-mm` format.
649
    pub fn months(&self, list_pk: i64) -> Result<Vec<String>> {
650
        let mut stmt = self.connection.prepare(
651
            "SELECT DISTINCT strftime('%Y-%m', CAST(timestamp AS INTEGER), 'unixepoch') FROM post \
652
             WHERE list = ?;",
653
        )?;
654
        let months_iter = stmt.query_map([list_pk], |row| {
655
            let val: String = row.get(0)?;
656
            Ok(val)
657
        })?;
658

            
659
        let mut ret = vec![];
660
        for month in months_iter {
661
            let month = month?;
662
            ret.push(month);
663
        }
664
        Ok(ret)
665
    }
666

            
667
    /// Find a post by its `Message-ID` email header.
668
    pub fn list_post_by_message_id(
669
        &self,
670
        list_pk: i64,
671
        message_id: &str,
672
    ) -> Result<Option<DbVal<Post>>> {
673
        let mut stmt = self.connection.prepare(
674
            "SELECT *, strftime('%Y-%m', CAST(timestamp AS INTEGER), 'unixepoch') AS month_year \
675
             FROM post WHERE list = ? AND message_id = ?;",
676
        )?;
677
        let ret = stmt
678
            .query_row(rusqlite::params![&list_pk, &message_id], |row| {
679
                let pk = row.get("pk")?;
680
                Ok(DbVal(
681
                    Post {
682
                        pk,
683
                        list: row.get("list")?,
684
                        envelope_from: row.get("envelope_from")?,
685
                        address: row.get("address")?,
686
                        message_id: row.get("message_id")?,
687
                        message: row.get("message")?,
688
                        timestamp: row.get("timestamp")?,
689
                        datetime: row.get("datetime")?,
690
                        month_year: row.get("month_year")?,
691
                    },
692
                    pk,
693
                ))
694
            })
695
            .optional()?;
696

            
697
        Ok(ret)
698
    }
699

            
700
    /// Helper function to send a template reply.
701
13
    pub fn send_reply_with_list_template<'ctx, F: Fn() -> Template>(
702
        &self,
703
        render_context: TemplateRenderContext<'ctx, F>,
704
        recipients: impl Iterator<Item = Cow<'ctx, melib::Address>>,
705
    ) -> Result<()> {
706
        let TemplateRenderContext {
707
13
            template,
708
13
            default_fn,
709
13
            list,
710
13
            context,
711
13
            queue,
712
13
            comment,
713
13
        } = render_context;
714

            
715
13
        let post_policy = self.list_post_policy(list.pk)?;
716
13
        let subscription_policy = self.list_subscription_policy(list.pk)?;
717

            
718
13
        let templ = self
719
13
            .fetch_template(template, Some(list.pk))?
720
            .map(DbVal::into_inner)
721
37
            .or_else(|| default_fn.map(|f| f()))
722
13
            .ok_or_else(|| -> crate::Error {
723
                format!("Template with name {template:?} was not found.").into()
724
            })?;
725

            
726
13
        let mut draft = templ.render(context)?;
727
26
        draft.headers.insert(
728
13
            melib::HeaderName::new_unchecked("From"),
729
13
            list.request_subaddr(),
730
13
        );
731
26
        for addr in recipients {
732
13
            let mut draft = draft.clone();
733
26
            draft
734
                .headers
735
26
                .insert(melib::HeaderName::new_unchecked("To"), addr.to_string());
736
26
            list.insert_headers(
737
                &mut draft,
738
13
                post_policy.as_deref(),
739
13
                subscription_policy.as_deref(),
740
            );
741
13
            self.insert_to_queue(QueueEntry::new(
742
                queue,
743
13
                Some(list.pk),
744
13
                None,
745
13
                draft.finalise()?.as_bytes(),
746
13
                Some(comment.to_string()),
747
13
            )?)?;
748
13
        }
749
13
        Ok(())
750
13
    }
751
}
752

            
753
/// Helper type for [`Connection::send_reply_with_list_template`].
754
#[derive(Debug)]
755
pub struct TemplateRenderContext<'ctx, F: Fn() -> Template> {
756
    /// Template name.
757
    pub template: &'ctx str,
758
    /// If template is not found, call a function that returns one.
759
    pub default_fn: Option<F>,
760
    /// The pertinent list.
761
    pub list: &'ctx DbVal<MailingList>,
762
    /// [`minijinja`]'s template context.
763
    pub context: minijinja::value::Value,
764
    /// Destination queue in the database.
765
    pub queue: Queue,
766
    /// Comment for the queue entry in the database.
767
    pub comment: Cow<'static, str>,
768
}