371 lines
13 KiB
Rust
371 lines
13 KiB
Rust
/*
|
|
* This file is part of mailpot
|
|
*
|
|
* Copyright 2020 - Manos Pitsidianakis
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
//! Named templates, for generated e-mail like confirmations, alerts etc.
|
|
//!
|
|
//! Template database model: [`Template`].
|
|
|
|
use log::trace;
|
|
use rusqlite::OptionalExtension;
|
|
|
|
use crate::{
|
|
errors::{ErrorKind::*, *},
|
|
Connection, DbVal,
|
|
};
|
|
|
|
/// A named template.
|
|
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
|
pub struct Template {
|
|
/// Database primary key.
|
|
pub pk: i64,
|
|
/// Name.
|
|
pub name: String,
|
|
/// Associated list foreign key, optional.
|
|
pub list: Option<i64>,
|
|
/// Subject template.
|
|
pub subject: Option<String>,
|
|
/// Extra headers template.
|
|
pub headers_json: Option<serde_json::Value>,
|
|
/// Body template.
|
|
pub body: String,
|
|
}
|
|
|
|
impl std::fmt::Display for Template {
|
|
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
write!(fmt, "{:?}", self)
|
|
}
|
|
}
|
|
|
|
impl Template {
|
|
/// Template name for generic list help e-mail.
|
|
pub const GENERIC_HELP: &'static str = "generic-help";
|
|
/// Template name for generic failure e-mail.
|
|
pub const GENERIC_FAILURE: &'static str = "generic-failure";
|
|
/// Template name for generic success e-mail.
|
|
pub const GENERIC_SUCCESS: &'static str = "generic-success";
|
|
/// Template name for subscription confirmation e-mail.
|
|
pub const SUBSCRIPTION_CONFIRMATION: &'static str = "subscription-confirmation";
|
|
/// Template name for unsubscription confirmation e-mail.
|
|
pub const UNSUBSCRIPTION_CONFIRMATION: &'static str = "unsubscription-confirmation";
|
|
/// Template name for subscription request notice e-mail (for list owners).
|
|
pub const SUBSCRIPTION_REQUEST_NOTICE_OWNER: &'static str = "subscription-notice-owner";
|
|
/// Template name for subscription request acceptance e-mail (for the
|
|
/// candidates).
|
|
pub const SUBSCRIPTION_REQUEST_CANDIDATE_ACCEPT: &'static str =
|
|
"subscription-notice-candidate-accept";
|
|
/// Template name for admin notices.
|
|
pub const ADMIN_NOTICE: &'static str = "admin-notice";
|
|
|
|
/// Render a message body from a saved named template.
|
|
pub fn render(&self, context: minijinja::value::Value) -> Result<melib::Draft> {
|
|
use melib::{Draft, HeaderName};
|
|
|
|
let env = minijinja::Environment::new();
|
|
let mut draft: Draft = Draft {
|
|
body: env.render_named_str("body", &self.body, &context)?,
|
|
..Draft::default()
|
|
};
|
|
if let Some(ref subject) = self.subject {
|
|
draft.headers.insert(
|
|
HeaderName::SUBJECT,
|
|
env.render_named_str("subject", subject, &context)?,
|
|
);
|
|
}
|
|
|
|
Ok(draft)
|
|
}
|
|
|
|
/// Template name for generic failure e-mail.
|
|
pub fn default_generic_failure() -> Self {
|
|
Self {
|
|
pk: -1,
|
|
name: Self::GENERIC_FAILURE.to_string(),
|
|
list: None,
|
|
subject: Some(
|
|
"{{ subject if subject else \"Your e-mail was not processed successfully.\" }}"
|
|
.to_string(),
|
|
),
|
|
headers_json: None,
|
|
body: "{{ details|safe if details else \"The list owners and administrators have been \
|
|
notified.\" }}"
|
|
.to_string(),
|
|
}
|
|
}
|
|
|
|
/// Create a plain template for generic success e-mails.
|
|
pub fn default_generic_success() -> Self {
|
|
Self {
|
|
pk: -1,
|
|
name: Self::GENERIC_SUCCESS.to_string(),
|
|
list: None,
|
|
subject: Some(
|
|
"{{ subject if subject else \"Your e-mail was processed successfully.\" }}"
|
|
.to_string(),
|
|
),
|
|
headers_json: None,
|
|
body: "{{ details|safe if details else \"\" }}".to_string(),
|
|
}
|
|
}
|
|
|
|
/// Create a plain template for subscription confirmation.
|
|
pub fn default_subscription_confirmation() -> Self {
|
|
Self {
|
|
pk: -1,
|
|
name: Self::SUBSCRIPTION_CONFIRMATION.to_string(),
|
|
list: None,
|
|
subject: Some(
|
|
"{% if list and (list.id or list.name) %}{% if list.id %}[{{ list.id }}] {% endif \
|
|
%}You have successfully subscribed to {{ list.name if list.name else list.id \
|
|
}}{% else %}You have successfully subscribed to this list{% endif %}."
|
|
.to_string(),
|
|
),
|
|
headers_json: None,
|
|
body: "{{ details|safe if details else \"\" }}".to_string(),
|
|
}
|
|
}
|
|
|
|
/// Create a plain template for unsubscription confirmations.
|
|
pub fn default_unsubscription_confirmation() -> Self {
|
|
Self {
|
|
pk: -1,
|
|
name: Self::UNSUBSCRIPTION_CONFIRMATION.to_string(),
|
|
list: None,
|
|
subject: Some(
|
|
"{% if list and (list.id or list.name) %}{% if list.id %}[{{ list.id }}] {% endif \
|
|
%}You have successfully unsubscribed from {{ list.name if list.name else list.id \
|
|
}}{% else %}You have successfully unsubscribed from this list{% endif %}."
|
|
.to_string(),
|
|
),
|
|
headers_json: None,
|
|
body: "{{ details|safe if details else \"\" }}".to_string(),
|
|
}
|
|
}
|
|
|
|
/// Create a plain template for admin notices.
|
|
pub fn default_admin_notice() -> Self {
|
|
Self {
|
|
pk: -1,
|
|
name: Self::ADMIN_NOTICE.to_string(),
|
|
list: None,
|
|
subject: Some(
|
|
"{% if list %}An error occured with list {{ list.id }}{% else %}An error \
|
|
occured{% endif %}"
|
|
.to_string(),
|
|
),
|
|
headers_json: None,
|
|
body: "{{ details|safe if details else \"\" }}".to_string(),
|
|
}
|
|
}
|
|
|
|
/// Create a plain template for subscription requests for list owners.
|
|
pub fn default_subscription_request_owner() -> Self {
|
|
Self {
|
|
pk: -1,
|
|
name: Self::SUBSCRIPTION_REQUEST_NOTICE_OWNER.to_string(),
|
|
list: None,
|
|
subject: Some("Subscription request for {{ list.id }}".to_string()),
|
|
headers_json: None,
|
|
body: "Candidate {{ candidate.name if candidate.name else \"\" }} <{{ \
|
|
candidate.address }}> Primary key: {{ candidate.pk }}\n\n{{ details|safe if \
|
|
details else \"\" }}"
|
|
.to_string(),
|
|
}
|
|
}
|
|
|
|
/// Create a plain template for subscription requests for candidates.
|
|
pub fn default_subscription_request_candidate_accept() -> Self {
|
|
Self {
|
|
pk: -1,
|
|
name: Self::SUBSCRIPTION_REQUEST_CANDIDATE_ACCEPT.to_string(),
|
|
list: None,
|
|
subject: Some("Your subscription to {{ list.id }} is now active.".to_string()),
|
|
headers_json: None,
|
|
body: "{{ details|safe if details else \"\" }}".to_string(),
|
|
}
|
|
}
|
|
|
|
/// Create a plain template for generic list help replies.
|
|
pub fn default_generic_help() -> Self {
|
|
Self {
|
|
pk: -1,
|
|
name: Self::GENERIC_HELP.to_string(),
|
|
list: None,
|
|
subject: Some("{{ subject if subject else \"Help for mailing list\" }}".to_string()),
|
|
headers_json: None,
|
|
body: "{{ details }}".to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Connection {
|
|
/// Fetch all.
|
|
pub fn fetch_templates(&self) -> Result<Vec<DbVal<Template>>> {
|
|
let mut stmt = self
|
|
.connection
|
|
.prepare("SELECT * FROM template ORDER BY pk;")?;
|
|
let iter = stmt.query_map(rusqlite::params![], |row| {
|
|
let pk = row.get("pk")?;
|
|
Ok(DbVal(
|
|
Template {
|
|
pk,
|
|
name: row.get("name")?,
|
|
list: row.get("list")?,
|
|
subject: row.get("subject")?,
|
|
headers_json: row.get("headers_json")?,
|
|
body: row.get("body")?,
|
|
},
|
|
pk,
|
|
))
|
|
})?;
|
|
|
|
let mut ret = vec![];
|
|
for templ in iter {
|
|
let templ = templ?;
|
|
ret.push(templ);
|
|
}
|
|
Ok(ret)
|
|
}
|
|
|
|
/// Fetch a named template.
|
|
pub fn fetch_template(
|
|
&self,
|
|
template: &str,
|
|
list_pk: Option<i64>,
|
|
) -> Result<Option<DbVal<Template>>> {
|
|
let mut stmt = self
|
|
.connection
|
|
.prepare("SELECT * FROM template WHERE name = ? AND list IS ?;")?;
|
|
let ret = stmt
|
|
.query_row(rusqlite::params![&template, &list_pk], |row| {
|
|
let pk = row.get("pk")?;
|
|
Ok(DbVal(
|
|
Template {
|
|
pk,
|
|
name: row.get("name")?,
|
|
list: row.get("list")?,
|
|
subject: row.get("subject")?,
|
|
headers_json: row.get("headers_json")?,
|
|
body: row.get("body")?,
|
|
},
|
|
pk,
|
|
))
|
|
})
|
|
.optional()?;
|
|
if ret.is_none() && list_pk.is_some() {
|
|
let mut stmt = self
|
|
.connection
|
|
.prepare("SELECT * FROM template WHERE name = ? AND list IS NULL;")?;
|
|
Ok(stmt
|
|
.query_row(rusqlite::params![&template], |row| {
|
|
let pk = row.get("pk")?;
|
|
Ok(DbVal(
|
|
Template {
|
|
pk,
|
|
name: row.get("name")?,
|
|
list: row.get("list")?,
|
|
subject: row.get("subject")?,
|
|
headers_json: row.get("headers_json")?,
|
|
body: row.get("body")?,
|
|
},
|
|
pk,
|
|
))
|
|
})
|
|
.optional()?)
|
|
} else {
|
|
Ok(ret)
|
|
}
|
|
}
|
|
|
|
/// Insert a named template.
|
|
pub fn add_template(&self, template: Template) -> Result<DbVal<Template>> {
|
|
let mut stmt = self.connection.prepare(
|
|
"INSERT INTO template(name, list, subject, headers_json, body) VALUES(?, ?, ?, ?, ?) \
|
|
RETURNING *;",
|
|
)?;
|
|
let ret = stmt
|
|
.query_row(
|
|
rusqlite::params![
|
|
&template.name,
|
|
&template.list,
|
|
&template.subject,
|
|
&template.headers_json,
|
|
&template.body
|
|
],
|
|
|row| {
|
|
let pk = row.get("pk")?;
|
|
Ok(DbVal(
|
|
Template {
|
|
pk,
|
|
name: row.get("name")?,
|
|
list: row.get("list")?,
|
|
subject: row.get("subject")?,
|
|
headers_json: row.get("headers_json")?,
|
|
body: row.get("body")?,
|
|
},
|
|
pk,
|
|
))
|
|
},
|
|
)
|
|
.map_err(|err| {
|
|
if matches!(
|
|
err,
|
|
rusqlite::Error::SqliteFailure(
|
|
rusqlite::ffi::Error {
|
|
code: rusqlite::ffi::ErrorCode::ConstraintViolation,
|
|
extended_code: 787
|
|
},
|
|
_
|
|
)
|
|
) {
|
|
Error::from(err).chain_err(|| NotFound("Could not find a list with this pk."))
|
|
} else {
|
|
err.into()
|
|
}
|
|
})?;
|
|
|
|
trace!("add_template {:?}.", &ret);
|
|
Ok(ret)
|
|
}
|
|
|
|
/// Remove a named template.
|
|
pub fn remove_template(&self, template: &str, list_pk: Option<i64>) -> Result<Template> {
|
|
let mut stmt = self
|
|
.connection
|
|
.prepare("DELETE FROM template WHERE name = ? AND list IS ? RETURNING *;")?;
|
|
let ret = stmt.query_row(rusqlite::params![&template, &list_pk], |row| {
|
|
Ok(Template {
|
|
pk: -1,
|
|
name: row.get("name")?,
|
|
list: row.get("list")?,
|
|
subject: row.get("subject")?,
|
|
headers_json: row.get("headers_json")?,
|
|
body: row.get("body")?,
|
|
})
|
|
})?;
|
|
|
|
trace!(
|
|
"remove_template {} list_pk {:?} {:?}.",
|
|
template,
|
|
&list_pk,
|
|
&ret
|
|
);
|
|
Ok(ret)
|
|
}
|
|
}
|