core: queue template replies after Request
parent
454c181089
commit
fee4649d5d
|
@ -17,6 +17,8 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::*;
|
||||
use crate::mail::ListRequest;
|
||||
|
||||
|
@ -205,7 +207,6 @@ impl Connection {
|
|||
}
|
||||
}
|
||||
}
|
||||
/* - FIXME Save digest metadata in database */
|
||||
}
|
||||
PostAction::Reject { reason } => {
|
||||
/* FIXME - Notify submitter */
|
||||
|
@ -239,6 +240,7 @@ impl Connection {
|
|||
env: &Envelope,
|
||||
raw: &[u8],
|
||||
) -> Result<()> {
|
||||
let post_policy = self.list_post_policy(list.pk)?;
|
||||
match request {
|
||||
ListRequest::Subscribe => {
|
||||
trace!(
|
||||
|
@ -247,8 +249,6 @@ impl Connection {
|
|||
list
|
||||
);
|
||||
|
||||
let post_policy = self.list_post_policy(list.pk)?;
|
||||
let subscription_policy = self.list_subscription_policy(list.pk)?;
|
||||
let approval_needed = post_policy
|
||||
.as_ref()
|
||||
.map(|p| p.approval_needed)
|
||||
|
@ -270,54 +270,119 @@ impl Connection {
|
|||
};
|
||||
if approval_needed {
|
||||
match self.add_candidate_subscription(list.pk, subscription) {
|
||||
Ok(_) => {}
|
||||
Err(_err) => {}
|
||||
}
|
||||
//FIXME: send notification to list-owner
|
||||
} else if let Err(_err) = self.add_subscription(list.pk, subscription) {
|
||||
//FIXME: send failure notice to f
|
||||
} else {
|
||||
let templ = self
|
||||
.fetch_template(Template::SUBSCRIPTION_CONFIRMATION, Some(list.pk))?
|
||||
.map(DbVal::into_inner)
|
||||
.unwrap_or_else(Template::default_subscription_confirmation);
|
||||
Ok(v) => {
|
||||
let list_owners = self.list_owners(list.pk)?;
|
||||
self.send_reply_with_list_template(
|
||||
TemplateRenderContext {
|
||||
template: Template::SUBSCRIPTION_REQUEST_NOTICE_OWNER,
|
||||
default_fn: Some(
|
||||
Template::default_subscription_request_owner,
|
||||
),
|
||||
list,
|
||||
context: minijinja::context! {
|
||||
list => &list,
|
||||
candidate => &v,
|
||||
},
|
||||
queue: Queue::Out,
|
||||
comment: Template::SUBSCRIPTION_REQUEST_NOTICE_OWNER
|
||||
.to_string(),
|
||||
},
|
||||
list_owners.iter().map(|owner| Cow::Owned(owner.address())),
|
||||
)?;
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Could not create candidate subscription for {f:?}: {err}"
|
||||
);
|
||||
/* send error notice to e-mail sender */
|
||||
self.send_reply_with_list_template(
|
||||
TemplateRenderContext {
|
||||
template: Template::GENERIC_FAILURE,
|
||||
default_fn: Some(Template::default_generic_failure),
|
||||
list,
|
||||
context: minijinja::context! {
|
||||
list => &list,
|
||||
},
|
||||
queue: Queue::Out,
|
||||
comment: format!(
|
||||
"Could not create candidate subscription for {f:?}: \
|
||||
{err}"
|
||||
),
|
||||
},
|
||||
std::iter::once(Cow::Borrowed(f)),
|
||||
)?;
|
||||
|
||||
let mut confirmation = templ.render(minijinja::context! {
|
||||
list => &list,
|
||||
})?;
|
||||
confirmation.headers.insert(
|
||||
melib::HeaderName::new_unchecked("From"),
|
||||
list.request_subaddr(),
|
||||
);
|
||||
confirmation
|
||||
.headers
|
||||
.insert(melib::HeaderName::new_unchecked("To"), f.to_string());
|
||||
for (hdr, val) in [
|
||||
("List-Id", Some(list.id_header())),
|
||||
("List-Help", list.help_header()),
|
||||
("List-Post", list.post_header(post_policy.as_deref())),
|
||||
(
|
||||
"List-Unsubscribe",
|
||||
list.unsubscribe_header(subscription_policy.as_deref()),
|
||||
),
|
||||
(
|
||||
"List-Subscribe",
|
||||
list.subscribe_header(subscription_policy.as_deref()),
|
||||
),
|
||||
("List-Archive", list.archive_header()),
|
||||
] {
|
||||
if let Some(val) = val {
|
||||
confirmation
|
||||
.headers
|
||||
.insert(melib::HeaderName::new_unchecked(hdr), val);
|
||||
/* send error details to list owners */
|
||||
|
||||
let list_owners = self.list_owners(list.pk)?;
|
||||
self.send_reply_with_list_template(
|
||||
TemplateRenderContext {
|
||||
template: Template::ADMIN_NOTICE,
|
||||
default_fn: Some(Template::default_admin_notice),
|
||||
list,
|
||||
context: minijinja::context! {
|
||||
list => &list,
|
||||
details => err.to_string(),
|
||||
},
|
||||
queue: Queue::Out,
|
||||
comment: format!(
|
||||
"Could not create candidate subscription for {f:?}: \
|
||||
{err}"
|
||||
),
|
||||
},
|
||||
list_owners.iter().map(|owner| Cow::Owned(owner.address())),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
self.insert_to_queue(
|
||||
Queue::Out,
|
||||
Some(list.pk),
|
||||
None,
|
||||
confirmation.finalise()?.as_bytes(),
|
||||
String::new(),
|
||||
} else if let Err(err) = self.add_subscription(list.pk, subscription) {
|
||||
log::error!("Could not create subscription for {f:?}: {err}");
|
||||
|
||||
/* send error notice to e-mail sender */
|
||||
|
||||
self.send_reply_with_list_template(
|
||||
TemplateRenderContext {
|
||||
template: Template::GENERIC_FAILURE,
|
||||
default_fn: Some(Template::default_generic_failure),
|
||||
list,
|
||||
context: minijinja::context! {
|
||||
list => &list,
|
||||
},
|
||||
queue: Queue::Out,
|
||||
comment: format!("Could not create subscription for {f:?}: {err}"),
|
||||
},
|
||||
std::iter::once(Cow::Borrowed(f)),
|
||||
)?;
|
||||
|
||||
/* send error details to list owners */
|
||||
|
||||
let list_owners = self.list_owners(list.pk)?;
|
||||
self.send_reply_with_list_template(
|
||||
TemplateRenderContext {
|
||||
template: Template::ADMIN_NOTICE,
|
||||
default_fn: Some(Template::default_admin_notice),
|
||||
list,
|
||||
context: minijinja::context! {
|
||||
list => &list,
|
||||
details => err.to_string(),
|
||||
},
|
||||
queue: Queue::Out,
|
||||
comment: format!("Could not create subscription for {f:?}: {err}"),
|
||||
},
|
||||
list_owners.iter().map(|owner| Cow::Owned(owner.address())),
|
||||
)?;
|
||||
} else {
|
||||
self.send_reply_with_list_template(
|
||||
TemplateRenderContext {
|
||||
template: Template::SUBSCRIPTION_CONFIRMATION,
|
||||
default_fn: Some(Template::default_subscription_confirmation),
|
||||
list,
|
||||
context: minijinja::context! {
|
||||
list => &list,
|
||||
},
|
||||
queue: Queue::Out,
|
||||
comment: Template::SUBSCRIPTION_CONFIRMATION.to_string(),
|
||||
},
|
||||
std::iter::once(Cow::Borrowed(f)),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
@ -329,10 +394,55 @@ impl Connection {
|
|||
list
|
||||
);
|
||||
for f in env.from() {
|
||||
if let Err(_err) = self.remove_subscription(list.pk, &f.get_email()) {
|
||||
//FIXME: send failure notice to f
|
||||
if let Err(err) = self.remove_subscription(list.pk, &f.get_email()) {
|
||||
log::error!("Could not unsubscribe {f:?}: {err}");
|
||||
/* send error notice to e-mail sender */
|
||||
|
||||
self.send_reply_with_list_template(
|
||||
TemplateRenderContext {
|
||||
template: Template::GENERIC_FAILURE,
|
||||
default_fn: Some(Template::default_generic_failure),
|
||||
list,
|
||||
context: minijinja::context! {
|
||||
list => &list,
|
||||
},
|
||||
queue: Queue::Out,
|
||||
comment: format!("Could not unsubscribe {f:?}: {err}"),
|
||||
},
|
||||
std::iter::once(Cow::Borrowed(f)),
|
||||
)?;
|
||||
|
||||
/* send error details to list owners */
|
||||
|
||||
let list_owners = self.list_owners(list.pk)?;
|
||||
self.send_reply_with_list_template(
|
||||
TemplateRenderContext {
|
||||
template: Template::ADMIN_NOTICE,
|
||||
default_fn: Some(Template::default_admin_notice),
|
||||
list,
|
||||
context: minijinja::context! {
|
||||
list => &list,
|
||||
details => err.to_string(),
|
||||
},
|
||||
queue: Queue::Out,
|
||||
comment: format!("Could not unsubscribe {f:?}: {err}"),
|
||||
},
|
||||
list_owners.iter().map(|owner| Cow::Owned(owner.address())),
|
||||
)?;
|
||||
} else {
|
||||
//FIXME: send success notice to f
|
||||
self.send_reply_with_list_template(
|
||||
TemplateRenderContext {
|
||||
template: Template::UNSUBSCRIPTION_CONFIRMATION,
|
||||
default_fn: Some(Template::default_unsubscription_confirmation),
|
||||
list,
|
||||
context: minijinja::context! {
|
||||
list => &list,
|
||||
},
|
||||
queue: Queue::Out,
|
||||
comment: Template::UNSUBSCRIPTION_CONFIRMATION.to_string(),
|
||||
},
|
||||
std::iter::once(Cow::Borrowed(f)),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -342,7 +452,19 @@ impl Connection {
|
|||
env.from(),
|
||||
list
|
||||
);
|
||||
return Err("list-owner emails are not implemented yet.".into());
|
||||
//FIXME: mail to list-owner
|
||||
/*
|
||||
for _owner in self.list_owners(list.pk)? {
|
||||
self.insert_to_queue(
|
||||
Queue::Out,
|
||||
Some(list.pk),
|
||||
None,
|
||||
draft.finalise()?.as_bytes(),
|
||||
"list-owner-forward".to_string(),
|
||||
)?;
|
||||
}
|
||||
*/
|
||||
}
|
||||
ListRequest::Other(ref req) if req.trim().eq_ignore_ascii_case("password") => {
|
||||
trace!(
|
||||
|
@ -352,7 +474,7 @@ impl Connection {
|
|||
);
|
||||
let body = env.body_bytes(raw);
|
||||
let password = body.text();
|
||||
// FIXME: validate SSH public key with `ssh-keygen`.
|
||||
// TODO: validate SSH public key with `ssh-keygen`.
|
||||
for f in env.from() {
|
||||
let email_from = f.get_email();
|
||||
if let Ok(sub) = self.list_subscription_by_address(list.pk, &email_from) {
|
||||
|
@ -389,7 +511,7 @@ impl Connection {
|
|||
env.from(),
|
||||
list
|
||||
);
|
||||
//FIXME
|
||||
return Err("message retrievals are not implemented yet.".into());
|
||||
}
|
||||
ListRequest::RetrieveArchive(ref from, ref to) => {
|
||||
trace!(
|
||||
|
@ -399,7 +521,7 @@ impl Connection {
|
|||
env.from(),
|
||||
list
|
||||
);
|
||||
//FIXME
|
||||
return Err("message retrievals are not implemented yet.".into());
|
||||
}
|
||||
ListRequest::SetDigest(ref toggle) => {
|
||||
trace!(
|
||||
|
@ -408,6 +530,7 @@ impl Connection {
|
|||
env.from(),
|
||||
list
|
||||
);
|
||||
return Err("setting digest options via e-mail is not implemented yet.".into());
|
||||
}
|
||||
ListRequest::Other(ref req) => {
|
||||
trace!(
|
||||
|
@ -416,6 +539,7 @@ impl Connection {
|
|||
env.from(),
|
||||
list
|
||||
);
|
||||
return Err(format!("Unknown request {req}.").into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
@ -473,4 +597,73 @@ impl Connection {
|
|||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Helper function to send a template reply.
|
||||
pub fn send_reply_with_list_template<'ctx, F: Fn() -> Template>(
|
||||
&self,
|
||||
render_context: TemplateRenderContext<'ctx, F>,
|
||||
recipients: impl Iterator<Item = Cow<'ctx, melib::Address>>,
|
||||
) -> Result<()> {
|
||||
let TemplateRenderContext {
|
||||
template,
|
||||
default_fn,
|
||||
list,
|
||||
context,
|
||||
queue,
|
||||
comment,
|
||||
} = render_context;
|
||||
|
||||
let post_policy = self.list_post_policy(list.pk)?;
|
||||
let subscription_policy = self.list_subscription_policy(list.pk)?;
|
||||
|
||||
let templ = self
|
||||
.fetch_template(template, Some(list.pk))?
|
||||
.map(DbVal::into_inner)
|
||||
.or_else(|| default_fn.map(|f| f()))
|
||||
.ok_or_else(|| -> crate::Error {
|
||||
format!("Template with name {template:?} was not found.").into()
|
||||
})?;
|
||||
|
||||
let mut draft = templ.render(context)?;
|
||||
draft.headers.insert(
|
||||
melib::HeaderName::new_unchecked("From"),
|
||||
list.request_subaddr(),
|
||||
);
|
||||
for addr in recipients {
|
||||
let mut draft = draft.clone();
|
||||
draft
|
||||
.headers
|
||||
.insert(melib::HeaderName::new_unchecked("To"), addr.to_string());
|
||||
list.insert_headers(
|
||||
&mut draft,
|
||||
post_policy.as_deref(),
|
||||
subscription_policy.as_deref(),
|
||||
);
|
||||
self.insert_to_queue(
|
||||
queue,
|
||||
Some(list.pk),
|
||||
None,
|
||||
draft.finalise()?.as_bytes(),
|
||||
comment.clone(),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper type for [`Connection::send_reply_with_list_template`].
|
||||
#[derive(Debug)]
|
||||
pub struct TemplateRenderContext<'ctx, F: Fn() -> Template> {
|
||||
/// Template name.
|
||||
pub template: &'ctx str,
|
||||
/// If template is not found, call a function that returns one.
|
||||
pub default_fn: Option<F>,
|
||||
/// The pertinent list.
|
||||
pub list: &'ctx DbVal<MailingList>,
|
||||
/// [`minijinja`]'s template context.
|
||||
pub context: minijinja::value::Value,
|
||||
/// Destination queue in the database.
|
||||
pub queue: Queue,
|
||||
/// Comment for the queue entry in the database.
|
||||
pub comment: String,
|
||||
}
|
||||
|
|
|
@ -179,24 +179,31 @@ impl Connection {
|
|||
&mut self,
|
||||
list_pk: i64,
|
||||
mut new_val: ListSubscription,
|
||||
) -> Result<i64> {
|
||||
) -> Result<DbVal<ListCandidateSubscription>> {
|
||||
new_val.list = list_pk;
|
||||
let mut stmt = self.connection.prepare(
|
||||
"INSERT INTO candidate_subscription(list, address, name, accepted) VALUES(?, ?, ?, ?) \
|
||||
RETURNING pk;",
|
||||
RETURNING *;",
|
||||
)?;
|
||||
let ret = stmt.query_row(
|
||||
rusqlite::params![&new_val.list, &new_val.address, &new_val.name, None::<i64>,],
|
||||
|row| {
|
||||
let pk: i64 = row.get("pk")?;
|
||||
Ok(pk)
|
||||
let pk = row.get("pk")?;
|
||||
Ok(DbVal(
|
||||
ListCandidateSubscription {
|
||||
pk,
|
||||
list: row.get("list")?,
|
||||
address: row.get("address")?,
|
||||
name: row.get("name")?,
|
||||
accepted: row.get("accepted")?,
|
||||
},
|
||||
pk,
|
||||
))
|
||||
},
|
||||
)?;
|
||||
drop(stmt);
|
||||
|
||||
trace!("add_candidate_subscription {:?}.", &ret);
|
||||
self.accept_candidate_subscription(ret)?;
|
||||
// [ref:FIXME]: add approval required option for subscriptions.
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
|
|
|
@ -143,11 +143,13 @@ pub mod mail;
|
|||
pub mod models;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub mod postfix;
|
||||
mod templates;
|
||||
|
||||
pub use config::{Configuration, SendMail};
|
||||
pub use db::*;
|
||||
pub use errors::*;
|
||||
use models::*;
|
||||
pub use templates::*;
|
||||
|
||||
/// A `mailto:` value.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
|
|
|
@ -231,6 +231,32 @@ impl MailingList {
|
|||
pub fn archive_url(&self) -> Option<&str> {
|
||||
self.archive_url.as_deref()
|
||||
}
|
||||
|
||||
/// Insert all available list headers.
|
||||
pub fn insert_headers(
|
||||
&self,
|
||||
draft: &mut melib::Draft,
|
||||
post_policy: Option<&PostPolicy>,
|
||||
subscription_policy: Option<&SubscriptionPolicy>,
|
||||
) {
|
||||
for (hdr, val) in [
|
||||
("List-Id", Some(self.id_header())),
|
||||
("List-Help", self.help_header()),
|
||||
("List-Post", self.post_header(post_policy)),
|
||||
(
|
||||
"List-Unsubscribe",
|
||||
self.unsubscribe_header(subscription_policy),
|
||||
),
|
||||
("List-Subscribe", self.subscribe_header(subscription_policy)),
|
||||
("List-Archive", self.archive_header()),
|
||||
] {
|
||||
if let Some(val) = val {
|
||||
draft
|
||||
.headers
|
||||
.insert(melib::HeaderName::new_unchecked(hdr), val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A mailing list subscription entry.
|
||||
|
@ -448,113 +474,27 @@ impl std::fmt::Display for Account {
|
|||
}
|
||||
}
|
||||
|
||||
/// A named template.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct Template {
|
||||
/// A mailing list subscription candidate.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ListCandidateSubscription {
|
||||
/// 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,
|
||||
/// Mailing list foreign key (See [`MailingList`]).
|
||||
pub list: i64,
|
||||
/// Subscription's e-mail address.
|
||||
pub address: String,
|
||||
/// Subscription's name, optional.
|
||||
pub name: Option<String>,
|
||||
/// Accepted, foreign key on [`ListSubscription`].
|
||||
pub accepted: Option<i64>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Template {
|
||||
impl std::fmt::Display for ListCandidateSubscription {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Template {
|
||||
/// Template name for generic failure e-mail.
|
||||
pub const GENERIC_FAILURE: &str = "generic-failure";
|
||||
/// Template name for generic success e-mail.
|
||||
pub const GENERIC_SUCCESS: &str = "generic-success";
|
||||
/// Template name for subscription confirmation e-mail.
|
||||
pub const SUBSCRIPTION_CONFIRMATION: &str = "subscription-confirmation";
|
||||
/// Template name for unsubscription confirmation e-mail.
|
||||
pub const UNSUBSCRIPTION_CONFIRMATION: &str = "unsubscription-confirmation";
|
||||
|
||||
/// 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::new_unchecked("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("Your e-mail was not processed successfully.".to_string()),
|
||||
headers_json: None,
|
||||
body: "{{ details|safe if details else \"\" }}".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("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(),
|
||||
}
|
||||
write!(
|
||||
fmt,
|
||||
"List_pk: {} name: {:?} address: {} accepted: {:?}",
|
||||
self.list, self.name, self.address, self.accepted,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,6 +68,7 @@ CREATE TABLE IF NOT EXISTS subscription (
|
|||
receive_duplicates BOOLEAN CHECK (receive_duplicates in (0, 1)) NOT NULL DEFAULT 1,
|
||||
receive_own_posts BOOLEAN CHECK (receive_own_posts in (0, 1)) NOT NULL DEFAULT 0,
|
||||
receive_confirmation BOOLEAN CHECK (receive_confirmation in (0, 1)) NOT NULL DEFAULT 1,
|
||||
last_digest INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
created INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
last_modified INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
FOREIGN KEY (list) REFERENCES list(pk) ON DELETE CASCADE,
|
||||
|
|
|
@ -78,6 +78,7 @@ CREATE TABLE IF NOT EXISTS subscription (
|
|||
BOOLEAN_TYPE(receive_duplicates) DEFAULT BOOLEAN_TRUE(),
|
||||
BOOLEAN_TYPE(receive_own_posts) DEFAULT BOOLEAN_FALSE(),
|
||||
BOOLEAN_TYPE(receive_confirmation) DEFAULT BOOLEAN_TRUE(),
|
||||
last_digest INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
created INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
last_modified INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
FOREIGN KEY (list) REFERENCES list(pk) ON DELETE CASCADE,
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
//! Template database model: [`Template`].
|
||||
|
||||
use super::*;
|
||||
|
||||
/// 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 failure e-mail.
|
||||
pub const GENERIC_FAILURE: &str = "generic-failure";
|
||||
/// Template name for generic success e-mail.
|
||||
pub const GENERIC_SUCCESS: &str = "generic-success";
|
||||
/// Template name for subscription confirmation e-mail.
|
||||
pub const SUBSCRIPTION_CONFIRMATION: &str = "subscription-confirmation";
|
||||
/// Template name for unsubscription confirmation e-mail.
|
||||
pub const UNSUBSCRIPTION_CONFIRMATION: &str = "unsubscription-confirmation";
|
||||
/// Template name for subscription request notice e-mail (for list owners).
|
||||
pub const SUBSCRIPTION_REQUEST_NOTICE_OWNER: &str = "subscription-notice-owner";
|
||||
/// Template name for subscription request acceptance e-mail (for the
|
||||
/// candidates).
|
||||
pub const SUBSCRIPTION_REQUEST_CANDIDATE_ACCEPT: &str = "subscription-notice-candidate-accept";
|
||||
/// Template name for admin notices.
|
||||
pub const ADMIN_NOTICE: &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::new_unchecked("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("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("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 }} by {{ candidate }}".to_string()),
|
||||
headers_json: None,
|
||||
body: "Candidate 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(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
mod utils;
|
||||
|
||||
use mailpot::{models::*, Configuration, Connection, Queue, SendMail};
|
||||
use mailpot::{models::*, Configuration, Connection, Queue, SendMail, Template};
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
|
@ -142,7 +142,7 @@ MIME-Version: 1.0
|
|||
assert_eq!(db.error_queue().unwrap().len(), 0);
|
||||
|
||||
let out_queue = db.queue(Queue::Out).unwrap();
|
||||
assert_eq!(out_queue.len(), 1);
|
||||
assert_eq!(out_queue.len(), 2);
|
||||
|
||||
let mut _templ = _templ.into_inner();
|
||||
let _templ2 = db
|
||||
|
@ -160,8 +160,8 @@ MIME-Version: 1.0
|
|||
|
||||
let out_queue = db.queue(Queue::Out).unwrap();
|
||||
|
||||
assert_eq!(out_queue.len(), 2);
|
||||
let out = &out_queue[1];
|
||||
assert_eq!(out_queue.len(), 3);
|
||||
let out = &out_queue[2];
|
||||
let out_bytes = serde_json::from_value::<Vec<u8>>(out["message"].clone()).unwrap();
|
||||
let out_env = melib::Envelope::from_bytes(&out_bytes, None).unwrap();
|
||||
// eprintln!("{}", String::from_utf8_lossy(&out_bytes));
|
||||
|
|
Loading…
Reference in New Issue