Add support for inline images
This commit is contained in:
parent
55d7aec516
commit
3a5a9bd66a
@ -16,7 +16,9 @@ use rocket::{
|
|||||||
use rocket_cors::{AllowedHeaders, AllowedOrigins};
|
use rocket_cors::{AllowedHeaders, AllowedOrigins};
|
||||||
use server::{
|
use server::{
|
||||||
error::ServerError,
|
error::ServerError,
|
||||||
graphql::{attachment_bytes, Attachment, GraphqlSchema, Mutation, QueryRoot},
|
graphql::{
|
||||||
|
attachment_bytes, cid_attachment_bytes, Attachment, GraphqlSchema, Mutation, QueryRoot,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[get("/refresh")]
|
#[get("/refresh")]
|
||||||
@ -111,6 +113,22 @@ impl<'r, 'o: 'r> Responder<'r, 'o> for DownloadAttachmentResponder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/cid/<id>/<cid>")]
|
||||||
|
async fn view_cid(
|
||||||
|
nm: &State<Notmuch>,
|
||||||
|
id: &str,
|
||||||
|
cid: &str,
|
||||||
|
) -> Result<InlineAttachmentResponder, Debug<ServerError>> {
|
||||||
|
let mid = if id.starts_with("id:") {
|
||||||
|
id.to_string()
|
||||||
|
} else {
|
||||||
|
format!("id:{}", id)
|
||||||
|
};
|
||||||
|
info!("view cid attachment {mid} {cid}");
|
||||||
|
let attachment = cid_attachment_bytes(nm, &mid, &cid)?;
|
||||||
|
Ok(InlineAttachmentResponder(attachment))
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/view/attachment/<id>/<idx>/<_>")]
|
#[get("/view/attachment/<id>/<idx>/<_>")]
|
||||||
async fn view_attachment(
|
async fn view_attachment(
|
||||||
nm: &State<Notmuch>,
|
nm: &State<Notmuch>,
|
||||||
@ -224,6 +242,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
graphql_query,
|
graphql_query,
|
||||||
graphql_request,
|
graphql_request,
|
||||||
graphiql,
|
graphiql,
|
||||||
|
view_cid,
|
||||||
view_attachment,
|
view_attachment,
|
||||||
download_attachment,
|
download_attachment,
|
||||||
],
|
],
|
||||||
|
|||||||
@ -337,6 +337,7 @@ impl QueryRoot {
|
|||||||
.headers
|
.headers
|
||||||
.get_first_value("date")
|
.get_first_value("date")
|
||||||
.and_then(|d| mailparse::dateparse(&d).ok());
|
.and_then(|d| mailparse::dateparse(&d).ok());
|
||||||
|
let cid_prefix = format!("/cid/{id}/");
|
||||||
let body = match extract_body(&m, &id)? {
|
let body = match extract_body(&m, &id)? {
|
||||||
Body::PlainText(PlainText { text, content_tree }) => {
|
Body::PlainText(PlainText { text, content_tree }) => {
|
||||||
let text = if text.len() > MAX_RAW_MESSAGE_SIZE {
|
let text = if text.len() > MAX_RAW_MESSAGE_SIZE {
|
||||||
@ -355,7 +356,7 @@ impl QueryRoot {
|
|||||||
// Trim newlines to prevent excessive white space at the beginning/end of
|
// Trim newlines to prevent excessive white space at the beginning/end of
|
||||||
// presenation. Leave tabs and spaces incase plain text attempts to center a
|
// presenation. Leave tabs and spaces incase plain text attempts to center a
|
||||||
// header on the first line.
|
// header on the first line.
|
||||||
sanitize_html(&linkify_html(&text.trim_matches('\n')))?
|
sanitize_html(&linkify_html(&text.trim_matches('\n')), &cid_prefix)?
|
||||||
),
|
),
|
||||||
content_tree: if debug_content_tree {
|
content_tree: if debug_content_tree {
|
||||||
render_content_type_tree(&m)
|
render_content_type_tree(&m)
|
||||||
@ -365,7 +366,7 @@ impl QueryRoot {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
Body::Html(Html { html, content_tree }) => Body::Html(Html {
|
Body::Html(Html { html, content_tree }) => Body::Html(Html {
|
||||||
html: sanitize_html(&html)?,
|
html: sanitize_html(&html, &cid_prefix)?,
|
||||||
content_tree: if debug_content_tree {
|
content_tree: if debug_content_tree {
|
||||||
render_content_type_tree(&m)
|
render_content_type_tree(&m)
|
||||||
} else {
|
} else {
|
||||||
@ -671,7 +672,13 @@ fn flatten_body_parts(parts: &[Body]) -> Body {
|
|||||||
|
|
||||||
fn extract_related(m: &ParsedMail, part_addr: &mut Vec<String>) -> Result<Body, Error> {
|
fn extract_related(m: &ParsedMail, part_addr: &mut Vec<String>) -> Result<Body, Error> {
|
||||||
// TODO(wathiede): collect related things and change return type to new Body arm.
|
// TODO(wathiede): collect related things and change return type to new Body arm.
|
||||||
let handled_types = vec![MULTIPART_ALTERNATIVE, TEXT_HTML, TEXT_PLAIN];
|
let handled_types = vec![
|
||||||
|
MULTIPART_ALTERNATIVE,
|
||||||
|
TEXT_HTML,
|
||||||
|
TEXT_PLAIN,
|
||||||
|
IMAGE_JPEG,
|
||||||
|
IMAGE_PNG,
|
||||||
|
];
|
||||||
let mut unhandled_types: Vec<_> = m
|
let mut unhandled_types: Vec<_> = m
|
||||||
.subparts
|
.subparts
|
||||||
.iter()
|
.iter()
|
||||||
@ -679,8 +686,21 @@ fn extract_related(m: &ParsedMail, part_addr: &mut Vec<String>) -> Result<Body,
|
|||||||
.filter(|mt| !handled_types.contains(&mt))
|
.filter(|mt| !handled_types.contains(&mt))
|
||||||
.collect();
|
.collect();
|
||||||
unhandled_types.sort();
|
unhandled_types.sort();
|
||||||
warn!("{MULTIPART_RELATED} contains the following unhandled mimetypes {unhandled_types:?}");
|
if !unhandled_types.is_empty() {
|
||||||
|
warn!("{MULTIPART_RELATED} contains the following unhandled mimetypes {unhandled_types:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, sp) in m.subparts.iter().enumerate() {
|
||||||
|
if sp.ctype.mimetype == IMAGE_PNG || sp.ctype.mimetype == IMAGE_JPEG {
|
||||||
|
info!("sp.ctype {:#?}", sp.ctype);
|
||||||
|
//info!("sp.headers {:#?}", sp.headers);
|
||||||
|
if let Some(cid) = sp.headers.get_first_value("Content-Id") {
|
||||||
|
let mut part_id = part_addr.clone();
|
||||||
|
part_id.push(i.to_string());
|
||||||
|
info!("cid: {cid} part_id {part_id:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
for sp in &m.subparts {
|
for sp in &m.subparts {
|
||||||
if sp.ctype.mimetype == MULTIPART_ALTERNATIVE {
|
if sp.ctype.mimetype == MULTIPART_ALTERNATIVE {
|
||||||
return extract_alternative(m, part_addr);
|
return extract_alternative(m, part_addr);
|
||||||
@ -705,17 +725,29 @@ fn extract_related(m: &ParsedMail, part_addr: &mut Vec<String>) -> Result<Body,
|
|||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn walk_attachments<T, F: Fn(&ParsedMail, &[usize]) -> Option<T>>(
|
fn walk_attachments<T, F: Fn(&ParsedMail, &[usize]) -> Option<T> + Copy>(
|
||||||
m: &ParsedMail,
|
m: &ParsedMail,
|
||||||
visitor: F,
|
visitor: F,
|
||||||
) -> Option<T> {
|
) -> Option<T> {
|
||||||
let mut cur_addr = Vec::new();
|
let mut cur_addr = Vec::new();
|
||||||
|
walk_attachments_inner(m, visitor, &mut cur_addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn walk_attachments_inner<T, F: Fn(&ParsedMail, &[usize]) -> Option<T> + Copy>(
|
||||||
|
m: &ParsedMail,
|
||||||
|
visitor: F,
|
||||||
|
cur_addr: &mut Vec<usize>,
|
||||||
|
) -> Option<T> {
|
||||||
for (idx, sp) in m.subparts.iter().enumerate() {
|
for (idx, sp) in m.subparts.iter().enumerate() {
|
||||||
cur_addr.push(idx);
|
cur_addr.push(idx);
|
||||||
let val = visitor(sp, &cur_addr);
|
let val = visitor(sp, &cur_addr);
|
||||||
if val.is_some() {
|
if val.is_some() {
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
let val = walk_attachments_inner(sp, visitor, cur_addr);
|
||||||
|
if val.is_some() {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
cur_addr.pop();
|
cur_addr.pop();
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@ -784,26 +816,18 @@ fn get_attachment_filename(header_value: &str) -> &str {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_content_type<'a>(headers: &[MailHeader<'a>]) -> Option<String> {
|
fn get_content_type<'a>(headers: &[MailHeader<'a>]) -> Option<String> {
|
||||||
for h in headers {
|
if let Some(v) = headers.get_first_value("Content-Type") {
|
||||||
if h.get_key() == "Content-Type" {
|
if let Some(idx) = v.find(';') {
|
||||||
let v = h.get_value();
|
return Some(v[..idx].to_string());
|
||||||
if let Some(idx) = v.find(';') {
|
} else {
|
||||||
return Some(v[..idx].to_string());
|
return Some(v);
|
||||||
} else {
|
|
||||||
return Some(v);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_content_id<'a>(headers: &[MailHeader<'a>]) -> Option<String> {
|
fn get_content_id<'a>(headers: &[MailHeader<'a>]) -> Option<String> {
|
||||||
for h in headers {
|
headers.get_first_value("Content-Id")
|
||||||
if h.get_key() == "Content-ID" {
|
|
||||||
return Some(h.get_value());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_content_type_tree(m: &ParsedMail) -> String {
|
fn render_content_type_tree(m: &ParsedMail) -> String {
|
||||||
@ -903,6 +927,34 @@ fn email_addresses(path: &str, m: &ParsedMail, header_name: &str) -> Result<Vec<
|
|||||||
Ok(addrs)
|
Ok(addrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cid_attachment_bytes(nm: &Notmuch, id: &str, cid: &str) -> Result<Attachment, ServerError> {
|
||||||
|
let files = nm.files(id)?;
|
||||||
|
let Some(path) = files.first() else {
|
||||||
|
warn!("failed to find files for message {id}");
|
||||||
|
return Err(ServerError::PartNotFound);
|
||||||
|
};
|
||||||
|
let file = File::open(&path)?;
|
||||||
|
let mmap = unsafe { MmapOptions::new().map(&file)? };
|
||||||
|
let m = parse_mail(&mmap)?;
|
||||||
|
if let Some(attachment) = walk_attachments(&m, |sp, cur_idx| {
|
||||||
|
info!("{cid} {:?}", get_content_id(&sp.headers));
|
||||||
|
if let Some(h_cid) = get_content_id(&sp.headers) {
|
||||||
|
let h_cid = &h_cid[1..h_cid.len() - 1];
|
||||||
|
if h_cid == cid {
|
||||||
|
let attachment = extract_attachment(&sp, id, &[]).unwrap_or(Attachment {
|
||||||
|
..Attachment::default()
|
||||||
|
});
|
||||||
|
return Some(attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}) {
|
||||||
|
return Ok(attachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(ServerError::PartNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn attachment_bytes(nm: &Notmuch, id: &str, idx: &[usize]) -> Result<Attachment, ServerError> {
|
pub fn attachment_bytes(nm: &Notmuch, id: &str, idx: &[usize]) -> Result<Attachment, ServerError> {
|
||||||
let files = nm.files(id)?;
|
let files = nm.files(id)?;
|
||||||
let Some(path) = files.first() else {
|
let Some(path) = files.first() else {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ pub mod nm;
|
|||||||
|
|
||||||
use css_inline::{CSSInliner, InlineError, InlineOptions};
|
use css_inline::{CSSInliner, InlineError, InlineOptions};
|
||||||
use linkify::{LinkFinder, LinkKind};
|
use linkify::{LinkFinder, LinkKind};
|
||||||
use log::error;
|
use log::{error, info};
|
||||||
use lol_html::{element, errors::RewritingError, rewrite_str, RewriteStrSettings};
|
use lol_html::{element, errors::RewritingError, rewrite_str, RewriteStrSettings};
|
||||||
use maplit::{hashmap, hashset};
|
use maplit::{hashmap, hashset};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@ -43,7 +43,9 @@ pub fn linkify_html(text: &str) -> String {
|
|||||||
parts.join("")
|
parts.join("")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sanitize_html(html: &str) -> Result<String, SanitizeError> {
|
// html contains the content to be cleaned, and cid_prefix is used to resolve mixed part image
|
||||||
|
// referrences
|
||||||
|
pub fn sanitize_html(html: &str, cid_prefix: &str) -> Result<String, SanitizeError> {
|
||||||
let element_content_handlers = vec![
|
let element_content_handlers = vec![
|
||||||
// Open links in new tab
|
// Open links in new tab
|
||||||
element!("a[href]", |el| {
|
element!("a[href]", |el| {
|
||||||
@ -51,6 +53,17 @@ pub fn sanitize_html(html: &str) -> Result<String, SanitizeError> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}),
|
}),
|
||||||
|
// Replace mixed part CID images with URL
|
||||||
|
element!("img[src]", |el| {
|
||||||
|
let src = el
|
||||||
|
.get_attribute("src")
|
||||||
|
.expect("src was required")
|
||||||
|
.replace("cid:", cid_prefix);
|
||||||
|
|
||||||
|
el.set_attribute("src", &src)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}),
|
||||||
// Only secure image URLs
|
// Only secure image URLs
|
||||||
element!("img[src]", |el| {
|
element!("img[src]", |el| {
|
||||||
let src = el
|
let src = el
|
||||||
@ -225,19 +238,19 @@ pub fn sanitize_html(html: &str) -> Result<String, SanitizeError> {
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
let clean_html = ammonia::Builder::default()
|
let rewritten_html = rewrite_str(
|
||||||
.tags(tags)
|
&inlined_html,
|
||||||
.tag_attributes(tag_attributes)
|
|
||||||
.generic_attributes(attributes)
|
|
||||||
.clean(&inlined_html)
|
|
||||||
.to_string();
|
|
||||||
//let clean_html = inlined_html;
|
|
||||||
|
|
||||||
Ok(rewrite_str(
|
|
||||||
&clean_html,
|
|
||||||
RewriteStrSettings {
|
RewriteStrSettings {
|
||||||
element_content_handlers,
|
element_content_handlers,
|
||||||
..RewriteStrSettings::default()
|
..RewriteStrSettings::default()
|
||||||
},
|
},
|
||||||
)?)
|
)?;
|
||||||
|
let clean_html = ammonia::Builder::default()
|
||||||
|
.tags(tags)
|
||||||
|
.tag_attributes(tag_attributes)
|
||||||
|
.generic_attributes(attributes)
|
||||||
|
.clean(&rewritten_html)
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Ok(clean_html)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,8 @@ port = 6758
|
|||||||
backend = "http://localhost:9345/"
|
backend = "http://localhost:9345/"
|
||||||
rewrite= "/api/"
|
rewrite= "/api/"
|
||||||
[[proxy]]
|
[[proxy]]
|
||||||
|
backend="http://localhost:9345/cid"
|
||||||
|
[[proxy]]
|
||||||
backend="http://localhost:9345/original"
|
backend="http://localhost:9345/original"
|
||||||
[[proxy]]
|
[[proxy]]
|
||||||
backend="http://localhost:9345/graphiql"
|
backend="http://localhost:9345/graphiql"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user