Compare commits
2 Commits
acfa9212f3
...
568472a2e7
Author | SHA1 | Date |
---|---|---|
Manos Pitsidianakis | 568472a2e7 | |
Manos Pitsidianakis | 393446ea61 |
11
Cargo.toml
11
Cargo.toml
|
@ -1,12 +1,13 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
members = [
|
||||
"archive-http",
|
||||
"cli",
|
||||
"core",
|
||||
"mailpot",
|
||||
"mailpot-archives",
|
||||
"mailpot-cli",
|
||||
"mailpot-http",
|
||||
"mailpot-tests",
|
||||
"rest-http",
|
||||
"web",
|
||||
"mailpot-web",
|
||||
]
|
||||
|
||||
[profile.release]
|
||||
|
|
2
Makefile
2
Makefile
|
@ -6,7 +6,7 @@ DJHTMLBIN = djhtml
|
|||
BLACKBIN = black
|
||||
PRINTF = /usr/bin/printf
|
||||
|
||||
HTML_FILES := $(shell find web/src/templates -type f -print0 | tr '\0' ' ')
|
||||
HTML_FILES := $(shell find mailpot-web/src/templates -type f -print0 | tr '\0' ' ')
|
||||
PY_FILES := $(shell find . -type f -name '*.py' -print0 | tr '\0' ' ')
|
||||
|
||||
.PHONY: check
|
||||
|
|
|
@ -18,7 +18,7 @@ path = "src/main.rs"
|
|||
[dependencies]
|
||||
chrono = { version = "^0.4" }
|
||||
lazy_static = "^1.4"
|
||||
mailpot = { version = "^0.1", path = "../core" }
|
||||
mailpot = { version = "^0.1", path = "../mailpot" }
|
||||
minijinja = { version = "0.31.0", features = ["source", ] }
|
||||
percent-encoding = { version = "^2.1", optional = true }
|
||||
serde = { version = "^1", features = ["derive", ] }
|
|
@ -20,7 +20,7 @@ doc-scrape-examples = true
|
|||
base64 = { version = "0.21" }
|
||||
clap = { version = "^4.2", default-features = false, features = ["std", "derive", "cargo", "unicode", "help", "usage", "error-context", "suggestions"] }
|
||||
log = "0.4"
|
||||
mailpot = { version = "^0.1", path = "../core" }
|
||||
mailpot = { version = "^0.1", path = "../mailpot" }
|
||||
serde = { version = "^1", features = ["derive", ] }
|
||||
serde_json = "^1"
|
||||
stderrlog = { version = "^0.6" }
|
||||
|
@ -35,5 +35,5 @@ tempfile = { version = "3.9" }
|
|||
[build-dependencies]
|
||||
clap = { version = "^4.2", default-features = false, features = ["std", "derive", "cargo", "unicode", "wrap_help", "help", "usage", "error-context", "suggestions"] }
|
||||
clap_mangen = "0.2.10"
|
||||
mailpot = { version = "^0.1", path = "../core" }
|
||||
mailpot = { version = "^0.1", path = "../mailpot" }
|
||||
stderrlog = { version = "^0.6" }
|
|
@ -25,8 +25,8 @@ config = "0.13"
|
|||
http = "0.2"
|
||||
lazy_static = "1.4"
|
||||
log = "0.4"
|
||||
mailpot = { version = "^0.1", path = "../core" }
|
||||
mailpot-web = { version = "^0.1", path = "../web" }
|
||||
mailpot = { version = "^0.1", path = "../mailpot" }
|
||||
mailpot-web = { version = "^0.1", path = "../mailpot-web" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
stderrlog = { version = "^0.6" }
|
|
@ -13,7 +13,7 @@ publish = false
|
|||
assert_cmd = "2"
|
||||
log = "0.4"
|
||||
mailin-embedded = { version = "0.7", features = ["rtls"] }
|
||||
mailpot = { version = "^0.1", path = "../core" }
|
||||
mailpot = { version = "^0.1", path = "../mailpot" }
|
||||
predicates = "3"
|
||||
stderrlog = { version = "^0.6" }
|
||||
tempfile = { version = "3.9" }
|
||||
|
|
|
@ -33,7 +33,7 @@ eyre = { version = "0.6" }
|
|||
http = "0.2"
|
||||
indexmap = { version = "1.9" }
|
||||
lazy_static = "^1.4"
|
||||
mailpot = { version = "^0.1", path = "../core" }
|
||||
mailpot = { version = "^0.1", path = "../mailpot" }
|
||||
minijinja = { version = "0.31.0", features = ["source", ] }
|
||||
percent-encoding = { version = "^2.1" }
|
||||
rand = { version = "^0.8", features = ["min_const_gen"] }
|
|
@ -92,7 +92,6 @@ pub mod typed_paths;
|
|||
pub mod utils;
|
||||
|
||||
pub use auth::*;
|
||||
pub use cal::{calendarize, *};
|
||||
pub use help::*;
|
||||
pub use lists::{
|
||||
list, list_candidates, list_edit, list_edit_POST, list_post, list_post_eml, list_post_raw,
|
|
@ -218,7 +218,7 @@ pub async fn list_post(
|
|||
url: ListPath(list.id.to_string().into()).to_crumb(),
|
||||
},
|
||||
Crumb {
|
||||
label: format!("{} {msg_id}", subject_ref).into(),
|
||||
label: format!("{} <{}>", subject_ref, msg_id.as_str().strip_carets()).into(),
|
||||
url: ListPostPath(list.id.to_string().into(), msg_id.to_string()).to_crumb(),
|
||||
},
|
||||
];
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
//! Utils for templates with the [`minijinja`] crate.
|
||||
|
||||
pub use mailpot::StripCarets;
|
||||
|
||||
use super::*;
|
||||
|
||||
mod compressed;
|
||||
mod filters;
|
||||
mod objects;
|
||||
|
||||
pub use filters::*;
|
||||
pub use objects::*;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref TEMPLATES: Environment<'static> = {
|
||||
let mut env = Environment::new();
|
||||
macro_rules! add {
|
||||
(function $($id:ident),*$(,)?) => {
|
||||
$(env.add_function(stringify!($id), $id);)*
|
||||
};
|
||||
(filter $($id:ident),*$(,)?) => {
|
||||
$(env.add_filter(stringify!($id), $id);)*
|
||||
}
|
||||
}
|
||||
add!(function calendarize,
|
||||
strip_carets,
|
||||
ensure_carets,
|
||||
urlize,
|
||||
url_encode,
|
||||
heading,
|
||||
topics,
|
||||
login_path,
|
||||
logout_path,
|
||||
settings_path,
|
||||
help_path,
|
||||
list_path,
|
||||
list_settings_path,
|
||||
list_edit_path,
|
||||
list_subscribers_path,
|
||||
list_candidates_path,
|
||||
list_post_path,
|
||||
post_raw_path,
|
||||
post_eml_path,
|
||||
post_mbox_path
|
||||
);
|
||||
add!(filter pluralize);
|
||||
// Load compressed templates. They are constructed in build.rs. See
|
||||
// [ref:embed_templates]
|
||||
let mut source = minijinja::Source::new();
|
||||
for (name, bytes) in compressed::COMPRESSED {
|
||||
let mut de_bytes = vec![];
|
||||
zstd::stream::copy_decode(*bytes,&mut de_bytes).unwrap();
|
||||
source.add_template(*name, String::from_utf8(de_bytes).unwrap()).unwrap();
|
||||
}
|
||||
env.set_source(source);
|
||||
|
||||
env.add_global("root_url_prefix", Value::from_safe_string( std::env::var("ROOT_URL_PREFIX").unwrap_or_default()));
|
||||
env.add_global("public_url",Value::from_safe_string(std::env::var("PUBLIC_URL").unwrap_or_default()));
|
||||
env.add_global("site_title", Value::from_safe_string(std::env::var("SITE_TITLE").unwrap_or_else(|_| "mailing list archive".to_string())));
|
||||
env.add_global("site_subtitle", std::env::var("SITE_SUBTITLE").ok().map(Value::from_safe_string).unwrap_or_default());
|
||||
|
||||
env
|
||||
};
|
||||
}
|
|
@ -17,4 +17,8 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//[tag:embed_templates]
|
||||
/// This is an array of all templates compressed for smaller binary size.
|
||||
///
|
||||
/// Compression happens at compile-time in the `build.rs` script.
|
||||
pub const COMPRESSED: &[(&str, &[u8])] = include!("compressed.data");
|
|
@ -21,199 +21,10 @@
|
|||
|
||||
use std::fmt::Write;
|
||||
|
||||
use mailpot::models::ListOwner;
|
||||
pub use mailpot::StripCarets;
|
||||
|
||||
use super::*;
|
||||
|
||||
mod compressed;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref TEMPLATES: Environment<'static> = {
|
||||
let mut env = Environment::new();
|
||||
macro_rules! add {
|
||||
(function $($id:ident),*$(,)?) => {
|
||||
$(env.add_function(stringify!($id), $id);)*
|
||||
};
|
||||
(filter $($id:ident),*$(,)?) => {
|
||||
$(env.add_filter(stringify!($id), $id);)*
|
||||
}
|
||||
}
|
||||
add!(function calendarize,
|
||||
strip_carets,
|
||||
urlize,
|
||||
heading,
|
||||
topics,
|
||||
login_path,
|
||||
logout_path,
|
||||
settings_path,
|
||||
help_path,
|
||||
list_path,
|
||||
list_settings_path,
|
||||
list_edit_path,
|
||||
list_subscribers_path,
|
||||
list_candidates_path,
|
||||
list_post_path,
|
||||
post_raw_path,
|
||||
post_eml_path
|
||||
);
|
||||
add!(filter pluralize);
|
||||
// Load compressed templates. They are constructed in build.rs. See
|
||||
// [ref:embed_templates]
|
||||
let mut source = minijinja::Source::new();
|
||||
for (name, bytes) in compressed::COMPRESSED {
|
||||
let mut de_bytes = vec![];
|
||||
zstd::stream::copy_decode(*bytes,&mut de_bytes).unwrap();
|
||||
source.add_template(*name, String::from_utf8(de_bytes).unwrap()).unwrap();
|
||||
}
|
||||
env.set_source(source);
|
||||
|
||||
env.add_global("root_url_prefix", Value::from_safe_string( std::env::var("ROOT_URL_PREFIX").unwrap_or_default()));
|
||||
env.add_global("public_url",Value::from_safe_string(std::env::var("PUBLIC_URL").unwrap_or_default()));
|
||||
env.add_global("site_title", Value::from_safe_string(std::env::var("SITE_TITLE").unwrap_or_else(|_| "mailing list archive".to_string())));
|
||||
env.add_global("site_subtitle", std::env::var("SITE_SUBTITLE").ok().map(Value::from_safe_string).unwrap_or_default());
|
||||
|
||||
env
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct MailingList {
|
||||
pub pk: i64,
|
||||
pub name: String,
|
||||
pub id: String,
|
||||
pub address: String,
|
||||
pub description: Option<String>,
|
||||
pub topics: Vec<String>,
|
||||
#[serde(serialize_with = "super::utils::to_safe_string_opt")]
|
||||
pub archive_url: Option<String>,
|
||||
pub inner: DbVal<mailpot::models::MailingList>,
|
||||
#[serde(default)]
|
||||
pub is_description_html_safe: bool,
|
||||
}
|
||||
|
||||
impl MailingList {
|
||||
/// Set whether it's safe to not escape the list's description field.
|
||||
///
|
||||
/// If anyone can display arbitrary html in the server, that's bad.
|
||||
///
|
||||
/// Note: uses `Borrow` so that it can use both `DbVal<ListOwner>` and
|
||||
/// `ListOwner` slices.
|
||||
pub fn set_safety<O: std::borrow::Borrow<ListOwner>>(
|
||||
&mut self,
|
||||
owners: &[O],
|
||||
administrators: &[String],
|
||||
) {
|
||||
if owners.is_empty() || administrators.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.is_description_html_safe = owners
|
||||
.iter()
|
||||
.any(|o| administrators.contains(&o.borrow().address));
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DbVal<mailpot::models::MailingList>> for MailingList {
|
||||
fn from(val: DbVal<mailpot::models::MailingList>) -> Self {
|
||||
let DbVal(
|
||||
mailpot::models::MailingList {
|
||||
pk,
|
||||
name,
|
||||
id,
|
||||
address,
|
||||
description,
|
||||
topics,
|
||||
archive_url,
|
||||
},
|
||||
_,
|
||||
) = val.clone();
|
||||
|
||||
Self {
|
||||
pk,
|
||||
name,
|
||||
id,
|
||||
address,
|
||||
description,
|
||||
topics,
|
||||
archive_url,
|
||||
inner: val,
|
||||
is_description_html_safe: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MailingList {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
self.id.fmt(fmt)
|
||||
}
|
||||
}
|
||||
|
||||
impl Object for MailingList {
|
||||
fn kind(&self) -> minijinja::value::ObjectKind {
|
||||
minijinja::value::ObjectKind::Struct(self)
|
||||
}
|
||||
|
||||
fn call_method(
|
||||
&self,
|
||||
_state: &minijinja::State,
|
||||
name: &str,
|
||||
_args: &[Value],
|
||||
) -> std::result::Result<Value, Error> {
|
||||
match name {
|
||||
"subscription_mailto" => {
|
||||
Ok(Value::from_serializable(&self.inner.subscription_mailto()))
|
||||
}
|
||||
"unsubscription_mailto" => Ok(Value::from_serializable(
|
||||
&self.inner.unsubscription_mailto(),
|
||||
)),
|
||||
"topics" => topics_common(&self.topics),
|
||||
_ => Err(Error::new(
|
||||
minijinja::ErrorKind::UnknownMethod,
|
||||
format!("object has no method named {name}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl minijinja::value::StructObject for MailingList {
|
||||
fn get_field(&self, name: &str) -> Option<Value> {
|
||||
match name {
|
||||
"pk" => Some(Value::from_serializable(&self.pk)),
|
||||
"name" => Some(Value::from_serializable(&self.name)),
|
||||
"id" => Some(Value::from_serializable(&self.id)),
|
||||
"address" => Some(Value::from_serializable(&self.address)),
|
||||
"description" if self.is_description_html_safe => {
|
||||
self.description.as_ref().map_or_else(
|
||||
|| Some(Value::from_serializable(&self.description)),
|
||||
|d| Some(Value::from_safe_string(d.clone())),
|
||||
)
|
||||
}
|
||||
"description" => Some(Value::from_serializable(&self.description)),
|
||||
"topics" => Some(Value::from_serializable(&self.topics)),
|
||||
"archive_url" => Some(Value::from_serializable(&self.archive_url)),
|
||||
"is_description_html_safe" => {
|
||||
Some(Value::from_serializable(&self.is_description_html_safe))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn static_fields(&self) -> Option<&'static [&'static str]> {
|
||||
Some(
|
||||
&[
|
||||
"pk",
|
||||
"name",
|
||||
"id",
|
||||
"address",
|
||||
"description",
|
||||
"topics",
|
||||
"archive_url",
|
||||
"is_description_html_safe",
|
||||
][..],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a vector of weeks, with each week being a vector of 7 days and
|
||||
/// corresponding sum of posts per day.
|
||||
pub fn calendarize(
|
||||
|
@ -438,6 +249,25 @@ pub fn strip_carets(_state: &minijinja::State, arg: Value) -> std::result::Resul
|
|||
))
|
||||
}
|
||||
|
||||
/// `ensure_carets` filter for [`minijinja`].
|
||||
///
|
||||
/// Makes sure message id value is surrounded by carets `[<>].
|
||||
pub fn ensure_carets(_state: &minijinja::State, arg: Value) -> std::result::Result<Value, Error> {
|
||||
Ok({
|
||||
let s = arg.as_str().ok_or_else(|| {
|
||||
minijinja::Error::new(
|
||||
minijinja::ErrorKind::InvalidOperation,
|
||||
format!("argument to ensure_carets() is of type {}", arg.kind()),
|
||||
)
|
||||
})?;
|
||||
if !s.trim().starts_with('<') && !s.ends_with('>') {
|
||||
Value::from(format!("<{s}>"))
|
||||
} else {
|
||||
Value::from(s)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `urlize` filter for [`minijinja`].
|
||||
///
|
||||
/// Returns a safe string for use in `<a href=..` attributes.
|
||||
|
@ -471,6 +301,24 @@ pub fn urlize(state: &minijinja::State, arg: Value) -> std::result::Result<Value
|
|||
Ok(Value::from_safe_string(format!("{prefix}{arg}")))
|
||||
}
|
||||
|
||||
pub fn url_encode(_state: &minijinja::State, arg: Value) -> std::result::Result<Value, Error> {
|
||||
Ok(Value::from_safe_string(
|
||||
utf8_percent_encode(
|
||||
arg.as_str().ok_or_else(|| {
|
||||
minijinja::Error::new(
|
||||
minijinja::ErrorKind::InvalidOperation,
|
||||
format!(
|
||||
"url_decode() argument is not a string but of type {}",
|
||||
arg.kind()
|
||||
),
|
||||
)
|
||||
})?,
|
||||
crate::typed_paths::PATH_SEGMENT,
|
||||
)
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Make an html heading: `h1, h2, h3` etc.
|
||||
///
|
||||
/// # Example
|
||||
|
@ -603,7 +451,7 @@ pub fn topics(topics: Value) -> std::result::Result<Value, Error> {
|
|||
topics_common(&topics)
|
||||
}
|
||||
|
||||
pub(crate) fn topics_common(topics: &[String]) -> std::result::Result<Value, Error> {
|
||||
pub fn topics_common(topics: &[String]) -> std::result::Result<Value, Error> {
|
||||
let mut ul = String::new();
|
||||
write!(&mut ul, r#"<ul class="tags">"#)?;
|
||||
for topic in topics {
|
||||
|
@ -628,6 +476,8 @@ pub(crate) fn topics_common(topics: &[String]) -> std::result::Result<Value, Err
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use mailpot::models::ListOwner;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
//! Utils for templates with the [`minijinja`] crate.
|
||||
|
||||
use mailpot::models::ListOwner;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct MailingList {
|
||||
pub pk: i64,
|
||||
pub name: String,
|
||||
pub id: String,
|
||||
pub address: String,
|
||||
pub description: Option<String>,
|
||||
pub topics: Vec<String>,
|
||||
#[serde(serialize_with = "super::utils::to_safe_string_opt")]
|
||||
pub archive_url: Option<String>,
|
||||
pub inner: DbVal<mailpot::models::MailingList>,
|
||||
#[serde(default)]
|
||||
pub is_description_html_safe: bool,
|
||||
}
|
||||
|
||||
impl MailingList {
|
||||
/// Set whether it's safe to not escape the list's description field.
|
||||
///
|
||||
/// If anyone can display arbitrary html in the server, that's bad.
|
||||
///
|
||||
/// Note: uses `Borrow` so that it can use both `DbVal<ListOwner>` and
|
||||
/// `ListOwner` slices.
|
||||
pub fn set_safety<O: std::borrow::Borrow<ListOwner>>(
|
||||
&mut self,
|
||||
owners: &[O],
|
||||
administrators: &[String],
|
||||
) {
|
||||
if owners.is_empty() || administrators.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.is_description_html_safe = owners
|
||||
.iter()
|
||||
.any(|o| administrators.contains(&o.borrow().address));
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DbVal<mailpot::models::MailingList>> for MailingList {
|
||||
fn from(val: DbVal<mailpot::models::MailingList>) -> Self {
|
||||
let DbVal(
|
||||
mailpot::models::MailingList {
|
||||
pk,
|
||||
name,
|
||||
id,
|
||||
address,
|
||||
description,
|
||||
topics,
|
||||
archive_url,
|
||||
},
|
||||
_,
|
||||
) = val.clone();
|
||||
|
||||
Self {
|
||||
pk,
|
||||
name,
|
||||
id,
|
||||
address,
|
||||
description,
|
||||
topics,
|
||||
archive_url,
|
||||
inner: val,
|
||||
is_description_html_safe: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MailingList {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
self.id.fmt(fmt)
|
||||
}
|
||||
}
|
||||
|
||||
impl Object for MailingList {
|
||||
fn kind(&self) -> minijinja::value::ObjectKind {
|
||||
minijinja::value::ObjectKind::Struct(self)
|
||||
}
|
||||
|
||||
fn call_method(
|
||||
&self,
|
||||
_state: &minijinja::State,
|
||||
name: &str,
|
||||
_args: &[Value],
|
||||
) -> std::result::Result<Value, Error> {
|
||||
match name {
|
||||
"subscription_mailto" => {
|
||||
Ok(Value::from_serializable(&self.inner.subscription_mailto()))
|
||||
}
|
||||
"unsubscription_mailto" => Ok(Value::from_serializable(
|
||||
&self.inner.unsubscription_mailto(),
|
||||
)),
|
||||
"topics" => topics_common(&self.topics),
|
||||
_ => Err(Error::new(
|
||||
minijinja::ErrorKind::UnknownMethod,
|
||||
format!("object has no method named {name}"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl minijinja::value::StructObject for MailingList {
|
||||
fn get_field(&self, name: &str) -> Option<Value> {
|
||||
match name {
|
||||
"pk" => Some(Value::from_serializable(&self.pk)),
|
||||
"name" => Some(Value::from_serializable(&self.name)),
|
||||
"id" => Some(Value::from_serializable(&self.id)),
|
||||
"address" => Some(Value::from_serializable(&self.address)),
|
||||
"description" if self.is_description_html_safe => {
|
||||
self.description.as_ref().map_or_else(
|
||||
|| Some(Value::from_serializable(&self.description)),
|
||||
|d| Some(Value::from_safe_string(d.clone())),
|
||||
)
|
||||
}
|
||||
"description" => Some(Value::from_serializable(&self.description)),
|
||||
"topics" => Some(Value::from_serializable(&self.topics)),
|
||||
"archive_url" => Some(Value::from_serializable(&self.archive_url)),
|
||||
"is_description_html_safe" => {
|
||||
Some(Value::from_serializable(&self.is_description_html_safe))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn static_fields(&self) -> Option<&'static [&'static str]> {
|
||||
Some(
|
||||
&[
|
||||
"pk",
|
||||
"name",
|
||||
"id",
|
||||
"address",
|
||||
"description",
|
||||
"topics",
|
||||
"archive_url",
|
||||
"is_description_html_safe",
|
||||
][..],
|
||||
)
|
||||
}
|
||||
}
|
|
@ -671,7 +671,7 @@
|
|||
}
|
||||
|
||||
table.headers tr>th {
|
||||
text-align: left;
|
||||
text-align: right;
|
||||
color: var(--text-faded);
|
||||
}
|
||||
|
||||
|
@ -711,6 +711,7 @@
|
|||
|
||||
td.message-id,
|
||||
span.message-id{
|
||||
user-select: all;
|
||||
color: var(--text-faded);
|
||||
}
|
||||
.message-id>a {
|
|
@ -9,13 +9,25 @@
|
|||
<th scope="row">From:</th>
|
||||
<td><bdi>{{ post.address }}</bdi></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">To:</th>
|
||||
<td><bdi>{% if post.to %}{{ post.to }}{% else %}{{ list.address }}{% endif %}</bdi></td>
|
||||
</tr>
|
||||
{% if post.cc %}
|
||||
<tr>
|
||||
<th scope="row">Cc:</th>
|
||||
<td><bdi>{{ post.cc }}</bdi></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">Date:</th>
|
||||
<td class="faded">{{ post.datetime }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Message-ID:</th>
|
||||
<td class="faded message-id"><a href="{{ list_post_path(list.id, post.message_id) }}">{{ strip_carets(post.message_id) }}</a></td>
|
||||
<td class="faded"><span class="message-id">{{ strip_carets(post.message_id) }}</span>
|
||||
<a href="{{ list_post_path(list.id, post.message_id) }}">permalink</a> / <a href="{{ post_raw_path(list.id, post.message_id) }}" title="View raw content" type="text/plain">raw</a> / <a href="{{ post_eml_path(list.id, post.message_id) }}" title="Download as RFC 5322 format" type="message/rfc822" download>eml</a> / <a href="{{ post_mbox_path(list.id, post.message_id) }}" title="Download as an MBOX" type="application/mbox" download>mbox</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% if in_reply_to %}
|
||||
<tr>
|
||||
|
@ -29,11 +41,11 @@
|
|||
<td>{% for r in references %}<span class="faded message-id"><a href="{{ list_post_path(list.id, r) }}">{{ r }}</a></span>{% endfor %}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td colspan="2"><details class="reply-details"><summary>more …</summary><a href="{{ post_raw_path(list.id, post.message_id) }}">View raw</a> <a href="{{ post_eml_path(list.id, post.message_id) }}">Download as <code>eml</code> (RFC 5322 format)</a></details></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="post-body">
|
||||
<pre {% if odd %}style="--background-secondary: var(--background-critical);" {% endif %}title="E-mail text content">{{ body|trim }}</pre>
|
||||
</div>
|
||||
<div class="post-reply-link">{# [ref:TODO] also reply to list email. #}
|
||||
<a href="mailto:{{ url_encode(post.address) }}?In-Reply-To={{ url_encode(ensure_carets(post.message_id)) }}&{% if post.cc %}Cc={{ url_encode(post.cc) }}&{% endif %}Subject=Re%3A{{ url_encode(subject) }}">Reply</a>
|
||||
</div>
|
||||
</div>
|
|
@ -94,6 +94,10 @@ pub struct ListPostRawPath(pub ListPathIdentifier, pub String);
|
|||
#[typed_path("/list/:id/posts/:msgid/eml/")]
|
||||
pub struct ListPostEmlPath(pub ListPathIdentifier, pub String);
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
|
||||
#[typed_path("/list/:id/posts/:msgid/mbox/")]
|
||||
pub struct ListPostMboxPath(pub ListPathIdentifier, pub String);
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize, TypedPath)]
|
||||
#[typed_path("/list/:id/edit/")]
|
||||
pub struct ListEditPath(pub ListPathIdentifier);
|
||||
|
@ -209,6 +213,7 @@ macro_rules! list_post_impl {
|
|||
list_post_impl!(list_post_path, ListPostPath);
|
||||
list_post_impl!(post_raw_path, ListPostRawPath);
|
||||
list_post_impl!(post_eml_path, ListPostEmlPath);
|
||||
list_post_impl!(post_mbox_path, ListPostMboxPath);
|
||||
|
||||
pub mod tsr {
|
||||
use std::{borrow::Cow, convert::Infallible};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue