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
#![allow(clippy::result_unit_err)]
21

            
22
//! Filters to pass each mailing list post through. Filters are functions that
23
//! implement the [`PostFilter`] trait that can:
24
//!
25
//! - transform post content.
26
//! - modify the final [`PostAction`] to take.
27
//! - modify the final scheduled jobs to perform. (See [`MailJob`]).
28
//!
29
//! Filters are executed in sequence like this:
30
//!
31
//! ```ignore
32
//! let result = filters
33
//!     .into_iter()
34
//!     .fold(Ok((&mut post, &mut list_ctx)), |p, f| {
35
//!         p.and_then(|(p, c)| f.feed(p, c))
36
//!     });
37
//! ```
38
//!
39
//! so the processing stops at the first returned error.
40

            
41
mod settings;
42
use log::trace;
43
use melib::{Address, HeaderName};
44
use percent_encoding::utf8_percent_encode;
45

            
46
use crate::{
47
    mail::{ListContext, MailJob, PostAction, PostEntry},
48
    models::{DbVal, MailingList},
49
    Connection, StripCarets, PATH_SEGMENT,
50
};
51

            
52
impl Connection {
53
    /// Return the post filters of a mailing list.
54
14
    pub fn list_filters(&self, _list: &DbVal<MailingList>) -> Vec<Box<dyn PostFilter>> {
55
28
        vec![
56
14
            Box::new(PostRightsCheck),
57
14
            Box::new(MimeReject),
58
14
            Box::new(FixCRLF),
59
14
            Box::new(AddListHeaders),
60
14
            Box::new(ArchivedAtLink),
61
14
            Box::new(AddSubjectTagPrefix),
62
14
            Box::new(FinalizeRecipients),
63
        ]
64
14
    }
65
}
66

            
67
/// Filter that modifies and/or verifies a post candidate. On rejection, return
68
/// a string describing the error and optionally set `post.action` to `Reject`
69
/// or `Defer`
70
pub trait PostFilter {
71
    /// Feed post into the filter. Perform modifications to `post` and / or
72
    /// `ctx`, and return them with `Result::Ok` unless you want to the
73
    /// processing to stop and return an `Result::Err`.
74
    fn feed<'p, 'list>(
75
        self: Box<Self>,
76
        post: &'p mut PostEntry,
77
        ctx: &'p mut ListContext<'list>,
78
    ) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()>;
79
}
80

            
81
/// Check that submitter can post to list, for now it accepts everything.
82
pub struct PostRightsCheck;
83
impl PostFilter for PostRightsCheck {
84
14
    fn feed<'p, 'list>(
85
        self: Box<Self>,
86
        post: &'p mut PostEntry,
87
        ctx: &'p mut ListContext<'list>,
88
    ) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()> {
89
        trace!("Running PostRightsCheck filter");
90
14
        if let Some(ref policy) = ctx.post_policy {
91
12
            if policy.announce_only {
92
                trace!("post policy is announce_only");
93
1
                let owner_addresses = ctx
94
                    .list_owners
95
                    .iter()
96
                    .map(|lo| lo.address())
97
                    .collect::<Vec<Address>>();
98
1
                trace!("Owner addresses are: {:#?}", &owner_addresses);
99
1
                trace!("Envelope from is: {:?}", &post.from);
100
1
                if !owner_addresses.iter().any(|addr| *addr == post.from) {
101
1
                    trace!("Envelope From does not include any owner");
102
1
                    post.action = PostAction::Reject {
103
1
                        reason: "You are not allowed to post on this list.".to_string(),
104
                    };
105
1
                    return Err(());
106
                }
107
12
            } else if policy.subscription_only {
108
                trace!("post policy is subscription_only");
109
7
                let email_from = post.from.get_email();
110
7
                trace!("post from is {:?}", &email_from);
111
7
                trace!("post subscriptions are {:#?}", &ctx.subscriptions);
112
12
                if !ctx.subscriptions.iter().any(|lm| lm.address == email_from) {
113
3
                    trace!("Envelope from is not subscribed to this list");
114
3
                    post.action = PostAction::Reject {
115
3
                        reason: "Only subscriptions can post to this list.".to_string(),
116
                    };
117
3
                    return Err(());
118
                }
119
11
            } else if policy.approval_needed {
120
1
                trace!("post policy says approval_needed");
121
1
                let email_from = post.from.get_email();
122
1
                trace!("post from is {:?}", &email_from);
123
1
                trace!("post subscriptions are {:#?}", &ctx.subscriptions);
124
1
                if !ctx.subscriptions.iter().any(|lm| lm.address == email_from) {
125
1
                    trace!("Envelope from is not subscribed to this list");
126
1
                    post.action = PostAction::Defer {
127
1
                        reason: "Your posting has been deferred. Approval from the list's \
128
                                 moderators is required before it is submitted."
129
                            .to_string(),
130
                    };
131
1
                    return Err(());
132
                }
133
1
            }
134
        }
135
9
        Ok((post, ctx))
136
14
    }
137
}
138

            
139
/// Ensure message contains only `\r\n` line terminators, required by SMTP.
140
pub struct FixCRLF;
141
impl PostFilter for FixCRLF {
142
9
    fn feed<'p, 'list>(
143
        self: Box<Self>,
144
        post: &'p mut PostEntry,
145
        ctx: &'p mut ListContext<'list>,
146
    ) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()> {
147
        trace!("Running FixCRLF filter");
148
        use std::io::prelude::*;
149
9
        let mut new_vec = Vec::with_capacity(post.bytes.len());
150
116
        for line in post.bytes.lines() {
151
214
            new_vec.extend_from_slice(line.unwrap().as_bytes());
152
107
            new_vec.extend_from_slice(b"\r\n");
153
        }
154
9
        post.bytes = new_vec;
155
9
        Ok((post, ctx))
156
9
    }
157
}
158

            
159
/// Add `List-*` headers
160
pub struct AddListHeaders;
161
impl PostFilter for AddListHeaders {
162
9
    fn feed<'p, 'list>(
163
        self: Box<Self>,
164
        post: &'p mut PostEntry,
165
        ctx: &'p mut ListContext<'list>,
166
    ) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()> {
167
        trace!("Running AddListHeaders filter");
168
9
        let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap();
169
9
        let sender = format!("<{}>", ctx.list.address);
170
9
        headers.push((HeaderName::SENDER, sender.as_bytes()));
171

            
172
9
        let list_id = Some(ctx.list.id_header());
173
9
        let list_help = ctx.list.help_header();
174
9
        let list_post = ctx.list.post_header(ctx.post_policy.as_deref());
175
18
        let list_unsubscribe = ctx
176
            .list
177
9
            .unsubscribe_header(ctx.subscription_policy.as_deref());
178
18
        let list_subscribe = ctx
179
            .list
180
9
            .subscribe_header(ctx.subscription_policy.as_deref());
181
9
        let list_archive = ctx.list.archive_header();
182

            
183
63
        for (hdr, val) in [
184
9
            (HeaderName::LIST_ID, &list_id),
185
9
            (HeaderName::LIST_HELP, &list_help),
186
9
            (HeaderName::LIST_POST, &list_post),
187
9
            (HeaderName::LIST_UNSUBSCRIBE, &list_unsubscribe),
188
9
            (HeaderName::LIST_SUBSCRIBE, &list_subscribe),
189
9
            (HeaderName::LIST_ARCHIVE, &list_archive),
190
        ] {
191
108
            if let Some(val) = val {
192
27
                headers.push((hdr, val.as_bytes()));
193
            }
194
54
        }
195

            
196
9
        let mut new_vec = Vec::with_capacity(
197
18
            headers
198
                .iter()
199
101
                .map(|(h, v)| h.as_str().as_bytes().len() + v.len() + ": \r\n".len())
200
                .sum::<usize>()
201
9
                + "\r\n\r\n".len()
202
                + body.len(),
203
        );
204
110
        for (h, v) in headers {
205
202
            new_vec.extend_from_slice(h.as_str().as_bytes());
206
101
            new_vec.extend_from_slice(b": ");
207
101
            new_vec.extend_from_slice(v);
208
101
            new_vec.extend_from_slice(b"\r\n");
209
101
        }
210
9
        new_vec.extend_from_slice(b"\r\n\r\n");
211
9
        new_vec.extend_from_slice(body);
212

            
213
9
        post.bytes = new_vec;
214
9
        Ok((post, ctx))
215
9
    }
216
}
217

            
218
/// Add List ID prefix in Subject header (e.g. `[list-id] ...`)
219
pub struct AddSubjectTagPrefix;
220
impl PostFilter for AddSubjectTagPrefix {
221
9
    fn feed<'p, 'list>(
222
        self: Box<Self>,
223
        post: &'p mut PostEntry,
224
        ctx: &'p mut ListContext<'list>,
225
    ) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()> {
226
9
        if let Some(mut settings) = ctx.filter_settings.remove("AddSubjectTagPrefixSettings") {
227
1
            let map = settings.as_object_mut().unwrap();
228
1
            let enabled = serde_json::from_value::<bool>(map.remove("enabled").unwrap()).unwrap();
229
1
            if !enabled {
230
1
                trace!(
231
                    "AddSubjectTagPrefix is disabled from settings found for list.pk = {} \
232
                     skipping filter",
233
                    ctx.list.pk
234
                );
235
1
                return Ok((post, ctx));
236
            }
237
9
        }
238
8
        trace!("Running AddSubjectTagPrefix filter");
239
8
        let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap();
240
        let mut subject;
241
38
        if let Some((_, subj_val)) = headers.iter_mut().find(|(k, _)| k == HeaderName::SUBJECT) {
242
8
            subject = format!("[{}] ", ctx.list.id).into_bytes();
243
8
            subject.extend(subj_val.iter().cloned());
244
8
            *subj_val = subject.as_slice();
245
        } else {
246
            subject = format!("[{}] (no subject)", ctx.list.id).into_bytes();
247
            headers.push((HeaderName::SUBJECT, subject.as_slice()));
248
        }
249

            
250
8
        let mut new_vec = Vec::with_capacity(
251
16
            headers
252
                .iter()
253
88
                .map(|(h, v)| h.as_str().as_bytes().len() + v.len() + ": \r\n".len())
254
                .sum::<usize>()
255
8
                + "\r\n\r\n".len()
256
                + body.len(),
257
        );
258
96
        for (h, v) in headers {
259
176
            new_vec.extend_from_slice(h.as_str().as_bytes());
260
88
            new_vec.extend_from_slice(b": ");
261
88
            new_vec.extend_from_slice(v);
262
88
            new_vec.extend_from_slice(b"\r\n");
263
88
        }
264
8
        new_vec.extend_from_slice(b"\r\n\r\n");
265
8
        new_vec.extend_from_slice(body);
266

            
267
8
        post.bytes = new_vec;
268
8
        Ok((post, ctx))
269
9
    }
270
}
271

            
272
/// Adds `Archived-At` field, if configured.
273
pub struct ArchivedAtLink;
274
impl PostFilter for ArchivedAtLink {
275
9
    fn feed<'p, 'list>(
276
        self: Box<Self>,
277
        post: &'p mut PostEntry,
278
        ctx: &'p mut ListContext<'list>,
279
    ) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()> {
280
9
        let Some(mut settings) = ctx.filter_settings.remove("ArchivedAtLinkSettings") else {
281
9
            trace!(
282
                "No ArchivedAtLink settings found for list.pk = {} skipping filter",
283
                ctx.list.pk
284
            );
285
9
            return Ok((post, ctx));
286
9
        };
287
        trace!("Running ArchivedAtLink filter");
288

            
289
        let map = settings.as_object_mut().unwrap();
290
        let template = serde_json::from_value::<String>(map.remove("template").unwrap()).unwrap();
291
        let preserve_carets =
292
            serde_json::from_value::<bool>(map.remove("preserve_carets").unwrap()).unwrap();
293

            
294
        let env = minijinja::Environment::new();
295
        let message_id = post.message_id.to_string();
296
        let header_val = env
297
            .render_named_str(
298
                "ArchivedAtLinkSettings.template",
299
                &template,
300
                &if preserve_carets {
301
                    minijinja::context! {
302
                    msg_id =>  utf8_percent_encode(message_id.as_str(), PATH_SEGMENT).to_string()
303
                    }
304
                } else {
305
                    minijinja::context! {
306
                    msg_id =>  utf8_percent_encode(message_id.as_str().strip_carets(), PATH_SEGMENT).to_string()
307
                    }
308
                },
309
            )
310
            .map_err(|err| {
311
                log::error!("ArchivedAtLink: {}", err);
312
9
            })?;
313
        let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap();
314
        headers.push((HeaderName::ARCHIVED_AT, header_val.as_bytes()));
315

            
316
        let mut new_vec = Vec::with_capacity(
317
            headers
318
                .iter()
319
                .map(|(h, v)| h.as_str().as_bytes().len() + v.len() + ": \r\n".len())
320
                .sum::<usize>()
321
                + "\r\n\r\n".len()
322
                + body.len(),
323
        );
324
        for (h, v) in headers {
325
            new_vec.extend_from_slice(h.as_str().as_bytes());
326
            new_vec.extend_from_slice(b": ");
327
            new_vec.extend_from_slice(v);
328
            new_vec.extend_from_slice(b"\r\n");
329
        }
330
        new_vec.extend_from_slice(b"\r\n\r\n");
331
        new_vec.extend_from_slice(body);
332

            
333
        post.bytes = new_vec;
334

            
335
        Ok((post, ctx))
336
9
    }
337
}
338

            
339
/// Assuming there are no more changes to be done on the post, it finalizes
340
/// which list subscriptions will receive the post in `post.action` field.
341
pub struct FinalizeRecipients;
342
impl PostFilter for FinalizeRecipients {
343
9
    fn feed<'p, 'list>(
344
        self: Box<Self>,
345
        post: &'p mut PostEntry,
346
        ctx: &'p mut ListContext<'list>,
347
    ) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()> {
348
        trace!("Running FinalizeRecipients filter");
349
9
        let mut recipients = vec![];
350
9
        let mut digests = vec![];
351
9
        let email_from = post.from.get_email();
352
18
        for subscription in ctx.subscriptions {
353
9
            trace!("examining subscription {:?}", &subscription);
354
9
            if subscription.address == email_from {
355
                trace!("subscription is submitter");
356
            }
357
9
            if subscription.digest {
358
                if subscription.address != email_from || subscription.receive_own_posts {
359
                    trace!("Subscription gets digest");
360
                    digests.push(subscription.address());
361
                }
362
                continue;
363
            }
364
9
            if subscription.address != email_from || subscription.receive_own_posts {
365
6
                trace!("Subscription gets copy");
366
6
                recipients.push(subscription.address());
367
            }
368
        }
369
9
        ctx.scheduled_jobs.push(MailJob::Send { recipients });
370
9
        if !digests.is_empty() {
371
            ctx.scheduled_jobs.push(MailJob::StoreDigest {
372
                recipients: digests,
373
            });
374
        }
375
9
        post.action = PostAction::Accept;
376
9
        Ok((post, ctx))
377
9
    }
378
}
379

            
380
/// Allow specific MIMEs only.
381
pub struct MimeReject;
382

            
383
impl PostFilter for MimeReject {
384
9
    fn feed<'p, 'list>(
385
        self: Box<Self>,
386
        post: &'p mut PostEntry,
387
        ctx: &'p mut ListContext<'list>,
388
    ) -> std::result::Result<(&'p mut PostEntry, &'p mut ListContext<'list>), ()> {
389
9
        let reject = if let Some(mut settings) = ctx.filter_settings.remove("MimeRejectSettings") {
390
            let map = settings.as_object_mut().unwrap();
391
            let enabled = serde_json::from_value::<bool>(map.remove("enabled").unwrap()).unwrap();
392
            if !enabled {
393
                trace!(
394
                    "MimeReject is disabled from settings found for list.pk = {} skipping filter",
395
                    ctx.list.pk
396
                );
397
                return Ok((post, ctx));
398
            }
399
            serde_json::from_value::<Vec<String>>(map.remove("reject").unwrap())
400
        } else {
401
9
            return Ok((post, ctx));
402
9
        };
403
        trace!("Running MimeReject filter with reject = {:?}", reject);
404
        Ok((post, ctx))
405
9
    }
406
}