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
//! Named templates, for generated e-mail like confirmations, alerts etc.
21
//!
22
//! Template database model: [`Template`].
23

            
24
use log::trace;
25
use rusqlite::OptionalExtension;
26

            
27
use crate::{
28
    errors::{ErrorKind::*, *},
29
    Connection, DbVal,
30
};
31

            
32
/// A named template.
33
8
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
34
pub struct Template {
35
    /// Database primary key.
36
4
    pub pk: i64,
37
    /// Name.
38
4
    pub name: String,
39
    /// Associated list foreign key, optional.
40
    pub list: Option<i64>,
41
    /// Subject template.
42
4
    pub subject: Option<String>,
43
    /// Extra headers template.
44
4
    pub headers_json: Option<serde_json::Value>,
45
    /// Body template.
46
4
    pub body: String,
47
}
48

            
49
impl std::fmt::Display for Template {
50
    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
51
        write!(fmt, "{:?}", self)
52
    }
53
}
54

            
55
impl Template {
56
    /// Template name for generic list help e-mail.
57
    pub const GENERIC_HELP: &'static str = "generic-help";
58
    /// Template name for generic failure e-mail.
59
    pub const GENERIC_FAILURE: &'static str = "generic-failure";
60
    /// Template name for generic success e-mail.
61
    pub const GENERIC_SUCCESS: &'static str = "generic-success";
62
    /// Template name for subscription confirmation e-mail.
63
    pub const SUBSCRIPTION_CONFIRMATION: &'static str = "subscription-confirmation";
64
    /// Template name for unsubscription confirmation e-mail.
65
    pub const UNSUBSCRIPTION_CONFIRMATION: &'static str = "unsubscription-confirmation";
66
    /// Template name for subscription request notice e-mail (for list owners).
67
    pub const SUBSCRIPTION_REQUEST_NOTICE_OWNER: &'static str = "subscription-notice-owner";
68
    /// Template name for subscription request acceptance e-mail (for the
69
    /// candidates).
70
    pub const SUBSCRIPTION_REQUEST_CANDIDATE_ACCEPT: &'static str =
71
        "subscription-notice-candidate-accept";
72
    /// Template name for admin notices.
73
    pub const ADMIN_NOTICE: &'static str = "admin-notice";
74

            
75
    /// Render a message body from a saved named template.
76
17
    pub fn render(&self, context: minijinja::value::Value) -> Result<melib::Draft> {
77
        use melib::{Draft, HeaderName};
78

            
79
17
        let env = minijinja::Environment::new();
80
17
        let mut draft: Draft = Draft {
81
17
            body: env.render_named_str("body", &self.body, &context)?,
82
17
            ..Draft::default()
83
17
        };
84
17
        if let Some(ref subject) = self.subject {
85
17
            draft.headers.insert(
86
17
                HeaderName::SUBJECT,
87
17
                env.render_named_str("subject", subject, &context)?,
88
17
            );
89
        }
90

            
91
17
        Ok(draft)
92
17
    }
93

            
94
    /// Template name for generic failure e-mail.
95
5
    pub fn default_generic_failure() -> Self {
96
5
        Self {
97
            pk: -1,
98
5
            name: Self::GENERIC_FAILURE.to_string(),
99
5
            list: None,
100
5
            subject: Some(
101
5
                "{{ subject if subject else \"Your e-mail was not processed successfully.\" }}"
102
                    .to_string(),
103
            ),
104
5
            headers_json: None,
105
5
            body: "{{ details|safe if details else \"The list owners and administrators have been \
106
                   notified.\" }}"
107
                .to_string(),
108
        }
109
5
    }
110

            
111
    /// Create a plain template for generic success e-mails.
112
    pub fn default_generic_success() -> Self {
113
        Self {
114
            pk: -1,
115
            name: Self::GENERIC_SUCCESS.to_string(),
116
            list: None,
117
            subject: Some(
118
                "{{ subject if subject else \"Your e-mail was processed successfully.\" }}"
119
                    .to_string(),
120
            ),
121
            headers_json: None,
122
            body: "{{ details|safe if details else \"\" }}".to_string(),
123
        }
124
    }
125

            
126
    /// Create a plain template for subscription confirmation.
127
7
    pub fn default_subscription_confirmation() -> Self {
128
7
        Self {
129
            pk: -1,
130
7
            name: Self::SUBSCRIPTION_CONFIRMATION.to_string(),
131
7
            list: None,
132
7
            subject: Some(
133
7
                "{% if list and (list.id or list.name) %}{% if list.id %}[{{ list.id }}] {% endif \
134
                 %}You have successfully subscribed to {{ list.name if list.name else list.id \
135
                 }}{% else %}You have successfully subscribed to this list{% endif %}."
136
                    .to_string(),
137
            ),
138
7
            headers_json: None,
139
7
            body: "{{ details|safe if details else \"\" }}".to_string(),
140
        }
141
7
    }
142

            
143
    /// Create a plain template for unsubscription confirmations.
144
2
    pub fn default_unsubscription_confirmation() -> Self {
145
2
        Self {
146
            pk: -1,
147
2
            name: Self::UNSUBSCRIPTION_CONFIRMATION.to_string(),
148
2
            list: None,
149
2
            subject: Some(
150
2
                "{% if list and (list.id or list.name) %}{% if list.id %}[{{ list.id }}] {% endif \
151
                 %}You have successfully unsubscribed from {{ list.name if list.name else list.id \
152
                 }}{% else %}You have successfully unsubscribed from this list{% endif %}."
153
                    .to_string(),
154
            ),
155
2
            headers_json: None,
156
2
            body: "{{ details|safe if details else \"\" }}".to_string(),
157
        }
158
2
    }
159

            
160
    /// Create a plain template for admin notices.
161
    pub fn default_admin_notice() -> Self {
162
        Self {
163
            pk: -1,
164
            name: Self::ADMIN_NOTICE.to_string(),
165
            list: None,
166
            subject: Some(
167
                "{% if list %}An error occured with list {{ list.id }}{% else %}An error \
168
                 occured{% endif %}"
169
                    .to_string(),
170
            ),
171
            headers_json: None,
172
            body: "{{ details|safe if details else \"\" }}".to_string(),
173
        }
174
    }
175

            
176
    /// Create a plain template for subscription requests for list owners.
177
    pub fn default_subscription_request_owner() -> Self {
178
        Self {
179
            pk: -1,
180
            name: Self::SUBSCRIPTION_REQUEST_NOTICE_OWNER.to_string(),
181
            list: None,
182
            subject: Some("Subscription request for {{ list.id }}".to_string()),
183
            headers_json: None,
184
            body: "Candidate {{ candidate.name if candidate.name else \"\" }} <{{ \
185
                   candidate.address }}> Primary key: {{ candidate.pk }}\n\n{{ details|safe if \
186
                   details else \"\" }}"
187
                .to_string(),
188
        }
189
    }
190

            
191
    /// Create a plain template for subscription requests for candidates.
192
    pub fn default_subscription_request_candidate_accept() -> Self {
193
        Self {
194
            pk: -1,
195
            name: Self::SUBSCRIPTION_REQUEST_CANDIDATE_ACCEPT.to_string(),
196
            list: None,
197
            subject: Some("Your subscription to {{ list.id }} is now active.".to_string()),
198
            headers_json: None,
199
            body: "{{ details|safe if details else \"\" }}".to_string(),
200
        }
201
    }
202

            
203
    /// Create a plain template for generic list help replies.
204
1
    pub fn default_generic_help() -> Self {
205
1
        Self {
206
            pk: -1,
207
1
            name: Self::GENERIC_HELP.to_string(),
208
1
            list: None,
209
1
            subject: Some("{{ subject if subject else \"Help for mailing list\" }}".to_string()),
210
1
            headers_json: None,
211
1
            body: "{{ details }}".to_string(),
212
        }
213
1
    }
214
}
215

            
216
impl Connection {
217
    /// Fetch all.
218
1
    pub fn fetch_templates(&self) -> Result<Vec<DbVal<Template>>> {
219
1
        let mut stmt = self
220
            .connection
221
            .prepare("SELECT * FROM template ORDER BY pk;")?;
222
3
        let iter = stmt.query_map(rusqlite::params![], |row| {
223
2
            let pk = row.get("pk")?;
224
2
            Ok(DbVal(
225
2
                Template {
226
                    pk,
227
2
                    name: row.get("name")?,
228
2
                    list: row.get("list")?,
229
2
                    subject: row.get("subject")?,
230
2
                    headers_json: row.get("headers_json")?,
231
2
                    body: row.get("body")?,
232
                },
233
                pk,
234
            ))
235
2
        })?;
236

            
237
1
        let mut ret = vec![];
238
3
        for templ in iter {
239
2
            let templ = templ?;
240
2
            ret.push(templ);
241
        }
242
1
        Ok(ret)
243
1
    }
244

            
245
    /// Fetch a named template.
246
17
    pub fn fetch_template(
247
        &self,
248
        template: &str,
249
        list_pk: Option<i64>,
250
    ) -> Result<Option<DbVal<Template>>> {
251
17
        let mut stmt = self
252
            .connection
253
            .prepare("SELECT * FROM template WHERE name = ? AND list IS ?;")?;
254
17
        let ret = stmt
255
18
            .query_row(rusqlite::params![&template, &list_pk], |row| {
256
1
                let pk = row.get("pk")?;
257
1
                Ok(DbVal(
258
1
                    Template {
259
                        pk,
260
1
                        name: row.get("name")?,
261
1
                        list: row.get("list")?,
262
1
                        subject: row.get("subject")?,
263
1
                        headers_json: row.get("headers_json")?,
264
1
                        body: row.get("body")?,
265
                    },
266
                    pk,
267
                ))
268
1
            })
269
            .optional()?;
270
18
        if ret.is_none() && list_pk.is_some() {
271
16
            let mut stmt = self
272
                .connection
273
                .prepare("SELECT * FROM template WHERE name = ? AND list IS NULL;")?;
274
16
            Ok(stmt
275
17
                .query_row(rusqlite::params![&template], |row| {
276
1
                    let pk = row.get("pk")?;
277
1
                    Ok(DbVal(
278
1
                        Template {
279
                            pk,
280
1
                            name: row.get("name")?,
281
1
                            list: row.get("list")?,
282
1
                            subject: row.get("subject")?,
283
1
                            headers_json: row.get("headers_json")?,
284
1
                            body: row.get("body")?,
285
                        },
286
                        pk,
287
                    ))
288
1
                })
289
16
                .optional()?)
290
16
        } else {
291
1
            Ok(ret)
292
        }
293
17
    }
294

            
295
    /// Insert a named template.
296
2
    pub fn add_template(&self, template: Template) -> Result<DbVal<Template>> {
297
2
        let mut stmt = self.connection.prepare(
298
            "INSERT INTO template(name, list, subject, headers_json, body) VALUES(?, ?, ?, ?, ?) \
299
             RETURNING *;",
300
        )?;
301
2
        let ret = stmt
302
            .query_row(
303
2
                rusqlite::params![
304
2
                    &template.name,
305
2
                    &template.list,
306
2
                    &template.subject,
307
2
                    &template.headers_json,
308
2
                    &template.body
309
                ],
310
2
                |row| {
311
2
                    let pk = row.get("pk")?;
312
2
                    Ok(DbVal(
313
2
                        Template {
314
                            pk,
315
2
                            name: row.get("name")?,
316
2
                            list: row.get("list")?,
317
2
                            subject: row.get("subject")?,
318
2
                            headers_json: row.get("headers_json")?,
319
2
                            body: row.get("body")?,
320
                        },
321
                        pk,
322
                    ))
323
2
                },
324
            )
325
            .map_err(|err| {
326
                if matches!(
327
                    err,
328
                    rusqlite::Error::SqliteFailure(
329
                        rusqlite::ffi::Error {
330
                            code: rusqlite::ffi::ErrorCode::ConstraintViolation,
331
                            extended_code: 787
332
                        },
333
                        _
334
                    )
335
                ) {
336
                    Error::from(err).chain_err(|| NotFound("Could not find a list with this pk."))
337
                } else {
338
                    err.into()
339
                }
340
            })?;
341

            
342
2
        trace!("add_template {:?}.", &ret);
343
2
        Ok(ret)
344
2
    }
345

            
346
    /// Remove a named template.
347
2
    pub fn remove_template(&self, template: &str, list_pk: Option<i64>) -> Result<Template> {
348
2
        let mut stmt = self
349
            .connection
350
            .prepare("DELETE FROM template WHERE name = ? AND list IS ? RETURNING *;")?;
351
4
        let ret = stmt.query_row(rusqlite::params![&template, &list_pk], |row| {
352
2
            Ok(Template {
353
                pk: -1,
354
2
                name: row.get("name")?,
355
2
                list: row.get("list")?,
356
2
                subject: row.get("subject")?,
357
2
                headers_json: row.get("headers_json")?,
358
2
                body: row.get("body")?,
359
            })
360
2
        })?;
361

            
362
2
        trace!(
363
            "remove_template {} list_pk {:?} {:?}.",
364
            template,
365
            &list_pk,
366
            &ret
367
        );
368
2
        Ok(ret)
369
2
    }
370
}