core: queue template replies after Request

grcov
Manos Pitsidianakis 2023-04-17 00:25:19 +03:00
parent 454c181089
commit fee4649d5d
Signed by: Manos Pitsidianakis
GPG Key ID: 7729C7707F7E09D0
8 changed files with 495 additions and 167 deletions

View File

@ -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,
}

View File

@ -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)
}

View File

@ -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)]

View File

@ -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,
)
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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(),
}
}
}

View File

@ -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));