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
use super::*;
42

            
43
/// Filter that modifies and/or verifies a post candidate. On rejection, return
44
/// a string describing the error and optionally set `post.action` to `Reject`
45
/// or `Defer`
46
pub trait PostFilter {
47
    /// Feed post into the filter. Perform modifications to `post` and / or
48
    /// `ctx`, and return them with `Result::Ok` unless you want to the
49
    /// processing to stop and return an `Result::Err`.
50
    fn feed<'p, 'list>(
51
        self: Box<Self>,
52
        post: &'p mut Post,
53
        ctx: &'p mut ListContext<'list>,
54
    ) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()>;
55
}
56

            
57
/// Check that submitter can post to list, for now it accepts everything.
58
pub struct PostRightsCheck;
59
impl PostFilter for PostRightsCheck {
60
8
    fn feed<'p, 'list>(
61
        self: Box<Self>,
62
        post: &'p mut Post,
63
        ctx: &'p mut ListContext<'list>,
64
    ) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> {
65
8
        trace!("Running PostRightsCheck filter");
66
8
        if let Some(ref policy) = ctx.post_policy {
67
8
            if policy.announce_only {
68
                trace!("post policy is announce_only");
69
                let owner_addresses = ctx
70
                    .list_owners
71
                    .iter()
72
                    .map(|lo| lo.address())
73
                    .collect::<Vec<Address>>();
74
                trace!("Owner addresses are: {:#?}", &owner_addresses);
75
                trace!("Envelope from is: {:?}", &post.from);
76
                if !owner_addresses.iter().any(|addr| *addr == post.from) {
77
                    trace!("Envelope From does not include any owner");
78
                    post.action = PostAction::Reject {
79
                        reason: "You are not allowed to post on this list.".to_string(),
80
                    };
81
                    return Err(());
82
                }
83
8
            } else if policy.subscription_only {
84
7
                trace!("post policy is subscription_only");
85
7
                let email_from = post.from.get_email();
86
7
                trace!("post from is {:?}", &email_from);
87
7
                trace!("post subscriptions are {:#?}", &ctx.subscriptions);
88
11
                if !ctx.subscriptions.iter().any(|lm| lm.address == email_from) {
89
3
                    trace!("Envelope from is not subscribed to this list");
90
3
                    post.action = PostAction::Reject {
91
3
                        reason: "Only subscriptions can post to this list.".to_string(),
92
                    };
93
3
                    return Err(());
94
                }
95
8
            } else if policy.approval_needed {
96
                trace!("post policy says approval_needed");
97
                let email_from = post.from.get_email();
98
                trace!("post from is {:?}", &email_from);
99
                trace!("post subscriptions are {:#?}", &ctx.subscriptions);
100
                if !ctx.subscriptions.iter().any(|lm| lm.address == email_from) {
101
                    trace!("Envelope from is not subscribed to this list");
102
                    post.action = PostAction::Defer {
103
                        reason: "Your posting has been deferred. Approval from the list's \
104
                                 moderators is required before it is submitted."
105
                            .to_string(),
106
                    };
107
                    return Err(());
108
                }
109
            }
110
        }
111
5
        Ok((post, ctx))
112
8
    }
113
}
114

            
115
/// Ensure message contains only `\r\n` line terminators, required by SMTP.
116
pub struct FixCRLF;
117
impl PostFilter for FixCRLF {
118
8
    fn feed<'p, 'list>(
119
        self: Box<Self>,
120
        post: &'p mut Post,
121
        ctx: &'p mut ListContext<'list>,
122
    ) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> {
123
8
        trace!("Running FixCRLF filter");
124
        use std::io::prelude::*;
125
8
        let mut new_vec = Vec::with_capacity(post.bytes.len());
126
152
        for line in post.bytes.lines() {
127
144
            new_vec.extend_from_slice(line.unwrap().as_bytes());
128
144
            new_vec.extend_from_slice(b"\r\n");
129
        }
130
8
        post.bytes = new_vec;
131
8
        Ok((post, ctx))
132
8
    }
133
}
134

            
135
/// Add `List-*` headers
136
pub struct AddListHeaders;
137
impl PostFilter for AddListHeaders {
138
5
    fn feed<'p, 'list>(
139
        self: Box<Self>,
140
        post: &'p mut Post,
141
        ctx: &'p mut ListContext<'list>,
142
    ) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> {
143
5
        trace!("Running AddListHeaders filter");
144
5
        let (mut headers, body) = melib::email::parser::mail(&post.bytes).unwrap();
145
5
        let sender = format!("<{}>", ctx.list.address);
146
5
        headers.push((&b"Sender"[..], sender.as_bytes()));
147
5
        let mut subject = format!("[{}] ", ctx.list.id).into_bytes();
148
5
        if let Some((_, subj_val)) = headers
149
            .iter_mut()
150
21
            .find(|(k, _)| k.eq_ignore_ascii_case(b"Subject"))
151
        {
152
5
            subject.extend(subj_val.iter().cloned());
153
5
            *subj_val = subject.as_slice();
154
        } else {
155
            headers.push((&b"Subject"[..], subject.as_slice()));
156
        }
157

            
158
5
        let list_id = Some(ctx.list.id_header());
159
5
        let list_help = ctx.list.help_header();
160
5
        let list_post = ctx.list.post_header(ctx.post_policy.as_deref());
161
10
        let list_unsubscribe = ctx
162
            .list
163
5
            .unsubscribe_header(ctx.subscription_policy.as_deref());
164
10
        let list_subscribe = ctx
165
            .list
166
5
            .subscribe_header(ctx.subscription_policy.as_deref());
167
5
        let list_archive = ctx.list.archive_header();
168

            
169
35
        for (hdr, val) in [
170
5
            (b"List-Id".as_slice(), &list_id),
171
5
            (b"List-Help".as_slice(), &list_help),
172
5
            (b"List-Post".as_slice(), &list_post),
173
5
            (b"List-Unsubscribe".as_slice(), &list_unsubscribe),
174
5
            (b"List-Subscribe".as_slice(), &list_subscribe),
175
5
            (b"List-Archive".as_slice(), &list_archive),
176
        ] {
177
30
            if let Some(val) = val {
178
15
                headers.push((hdr, val.as_bytes()));
179
            }
180
        }
181

            
182
5
        let mut new_vec = Vec::with_capacity(
183
10
            headers
184
                .iter()
185
64
                .map(|(h, v)| h.len() + v.len() + ": \r\n".len())
186
                .sum::<usize>()
187
5
                + "\r\n\r\n".len()
188
                + body.len(),
189
        );
190
69
        for (h, v) in headers {
191
64
            new_vec.extend_from_slice(h);
192
64
            new_vec.extend_from_slice(b": ");
193
64
            new_vec.extend_from_slice(v);
194
64
            new_vec.extend_from_slice(b"\r\n");
195
        }
196
5
        new_vec.extend_from_slice(b"\r\n\r\n");
197
5
        new_vec.extend_from_slice(body);
198

            
199
5
        post.bytes = new_vec;
200
5
        Ok((post, ctx))
201
5
    }
202
}
203

            
204
/// Adds `Archived-At` field, if configured.
205
pub struct ArchivedAtLink;
206
impl PostFilter for ArchivedAtLink {
207
    fn feed<'p, 'list>(
208
        self: Box<Self>,
209
        post: &'p mut Post,
210
        ctx: &'p mut ListContext<'list>,
211
    ) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> {
212
        trace!("Running ArchivedAtLink filter");
213
        Ok((post, ctx))
214
    }
215
}
216

            
217
/// Assuming there are no more changes to be done on the post, it finalizes
218
/// which list subscriptions will receive the post in `post.action` field.
219
pub struct FinalizeRecipients;
220
impl PostFilter for FinalizeRecipients {
221
5
    fn feed<'p, 'list>(
222
        self: Box<Self>,
223
        post: &'p mut Post,
224
        ctx: &'p mut ListContext<'list>,
225
    ) -> std::result::Result<(&'p mut Post, &'p mut ListContext<'list>), ()> {
226
5
        trace!("Running FinalizeRecipients filter");
227
5
        let mut recipients = vec![];
228
5
        let mut digests = vec![];
229
5
        let email_from = post.from.get_email();
230
12
        for subscription in ctx.subscriptions {
231
7
            trace!("examining subscription {:?}", &subscription);
232
7
            if subscription.address == email_from {
233
5
                trace!("subscription is submitter");
234
            }
235
7
            if subscription.digest {
236
                if subscription.address != email_from || subscription.receive_own_posts {
237
                    trace!("Subscription gets digest");
238
                    digests.push(subscription.address());
239
                }
240
                continue;
241
            }
242
7
            if subscription.address != email_from || subscription.receive_own_posts {
243
4
                trace!("Subscription gets copy");
244
4
                recipients.push(subscription.address());
245
            }
246
        }
247
5
        ctx.scheduled_jobs.push(MailJob::Send { recipients });
248
5
        if !digests.is_empty() {
249
            ctx.scheduled_jobs.push(MailJob::StoreDigest {
250
                recipients: digests,
251
            });
252
        }
253
5
        post.action = PostAction::Accept;
254
5
        Ok((post, ctx))
255
5
    }
256
}