Compare commits

..

9 Commits

3 changed files with 141 additions and 58 deletions

View File

@ -23,6 +23,12 @@ pub type UnixTime = isize;
/// # Thread ID, sans "thread:"
pub type ThreadId = String;
const TEXT_PLAIN: &'static str = "text/plain";
const TEXT_HTML: &'static str = "text/html";
const MULTIPART_ALTERNATIVE: &'static str = "multipart/alternative";
const MULTIPART_MIXED: &'static str = "multipart/mixed";
const MULTIPART_RELATED: &'static str = "multipart/related";
#[derive(Debug, SimpleObject)]
pub struct ThreadSummary {
pub thread: ThreadId,
@ -316,7 +322,6 @@ impl QueryRoot {
let mut messages = Vec::new();
for (path, id) in std::iter::zip(nm.files(&thread_id)?, nm.message_ids(&thread_id)?) {
let tags = nm.tags_for_query(&format!("id:{id}"))?;
info!("{id}: {tags:?}\nfile: {path}");
let file = File::open(&path)?;
let mmap = unsafe { MmapOptions::new().map(&file)? };
let m = parse_mail(&mmap)?;
@ -426,10 +431,10 @@ impl Mutation {
fn extract_body(m: &ParsedMail) -> Result<Body, Error> {
let body = m.get_body()?;
let ret = match m.ctype.mimetype.as_str() {
"text/plain" => return Ok(Body::text(body)),
"text/html" => return Ok(Body::html(body)),
"multipart/mixed" => extract_mixed(m),
"multipart/alternative" => extract_alternative(m),
TEXT_PLAIN => return Ok(Body::text(body)),
TEXT_HTML => return Ok(Body::html(body)),
MULTIPART_MIXED => extract_mixed(m),
MULTIPART_ALTERNATIVE => extract_alternative(m),
_ => extract_unhandled(m),
};
if let Err(err) = ret {
@ -444,7 +449,6 @@ fn extract_unhandled(m: &ParsedMail) -> Result<Body, Error> {
"Unhandled body content type:\n{}",
render_content_type_tree(m)
);
warn!("{}", msg);
Ok(Body::UnhandledContentType(UnhandledContentType {
text: msg,
}))
@ -455,59 +459,100 @@ fn extract_unhandled(m: &ParsedMail) -> Result<Body, Error> {
// then give up.
fn extract_alternative(m: &ParsedMail) -> Result<Body, Error> {
for sp in &m.subparts {
if sp.ctype.mimetype == "text/html" {
if sp.ctype.mimetype.as_str() == TEXT_HTML {
let body = sp.get_body()?;
return Ok(Body::html(body));
}
}
for sp in &m.subparts {
if sp.ctype.mimetype == "text/plain" {
if sp.ctype.mimetype.as_str() == TEXT_PLAIN {
let body = sp.get_body()?;
return Ok(Body::text(body));
}
}
Err("extract_alternative".into())
for sp in &m.subparts {
if sp.ctype.mimetype.as_str() == MULTIPART_RELATED {
return extract_related(sp);
}
}
Err(format!(
"extract_alternative failed to find suitable subpart, searched: {:?}",
vec![TEXT_HTML, TEXT_PLAIN]
)
.into())
}
// multipart/mixed defines multiple types of context all of which should be presented to the user
// 'serially'.
fn extract_mixed(m: &ParsedMail) -> Result<Body, Error> {
let handled_types = vec![
MULTIPART_ALTERNATIVE,
MULTIPART_RELATED,
TEXT_HTML,
TEXT_PLAIN,
];
let mut unhandled_types: Vec<_> = m
.subparts
.iter()
.map(|sp| sp.ctype.mimetype.as_str())
.filter(|mt| !handled_types.contains(&mt))
.collect();
unhandled_types.sort();
warn!("{MULTIPART_MIXED} contains the following unhandled mimetypes {unhandled_types:?}");
for sp in &m.subparts {
if sp.ctype.mimetype == "multipart/alternative" {
if sp.ctype.mimetype.as_str() == MULTIPART_ALTERNATIVE {
return extract_alternative(sp);
}
}
for sp in &m.subparts {
if sp.ctype.mimetype == "multipart/related" {
if sp.ctype.mimetype == MULTIPART_RELATED {
return extract_related(sp);
}
}
for sp in &m.subparts {
let body = sp.get_body()?;
match sp.ctype.mimetype.as_str() {
"text/plain" => return Ok(Body::text(body)),
"text/html" => return Ok(Body::html(body)),
TEXT_PLAIN => return Ok(Body::text(body)),
TEXT_HTML => return Ok(Body::html(body)),
_ => (),
}
}
Err("extract_mixed".into())
Err(format!(
"extract_mixed failed to find suitable subpart, searched: {:?}",
handled_types
)
.into())
}
fn extract_related(m: &ParsedMail) -> Result<Body, Error> {
// TODO(wathiede): collect related things and change return type to new Body arm.
let handled_types = vec![TEXT_HTML, TEXT_PLAIN];
let mut unhandled_types: Vec<_> = m
.subparts
.iter()
.map(|sp| sp.ctype.mimetype.as_str())
.filter(|mt| !handled_types.contains(&mt))
.collect();
unhandled_types.sort();
warn!("{MULTIPART_RELATED} contains the following unhandled mimetypes {unhandled_types:?}");
for sp in &m.subparts {
if sp.ctype.mimetype == "text/html" {
if sp.ctype.mimetype == TEXT_HTML {
let body = sp.get_body()?;
return Ok(Body::html(body));
}
}
for sp in &m.subparts {
if sp.ctype.mimetype == "text/plain" {
if sp.ctype.mimetype == TEXT_PLAIN {
let body = sp.get_body()?;
return Ok(Body::text(body));
}
}
Err("extract_related".into())
Err(format!(
"extract_related failed to find suitable subpart, searched: {:?}",
handled_types
)
.into())
}
// TODO(wathiede): make this walk_attachments that takes a closure.

View File

@ -27,6 +27,10 @@
padding: 0;
}
.message .header .media-right {
padding: 1rem;
}
.message .headers {
position: relative;
width: 100%;
@ -54,6 +58,13 @@
overflow-wrap: break-word;
}
.message .body .attachments hr {
border: none;
border-top: 1px dashed #888;
background-color: #f000;
margin: 0.5rem 0;
}
.error {
background-color: red;
}
@ -61,7 +72,9 @@
.view-part-text-plain {
padding: 0.5em;
overflow-wrap: break-word;
white-space: pre-line;
white-space: pre-wrap;
word-break: break-word;
word-wrap: break-word;
}
iframe {
@ -96,7 +109,7 @@
}
.index .date {
width: 10em;
width: 7em;
white-space: nowrap;
text-align: right;
}
@ -183,6 +196,7 @@
.search-results .row .summary {
min-width: 0;
width: 100%;
}
.search-results .row .subject {
@ -191,6 +205,20 @@
white-space: nowrap;
}
.search-results td.subject {
display: flex;
}
.search-results .subject .tag {}
.search-results .subject .text {
padding-left: 0.5rem;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.search-results .row .from {
overflow: hidden;
text-overflow: ellipsis;
@ -250,7 +278,9 @@
}
.content-tree {
white-space: pre-line;
white-space: pre-wrap;
word-break: break-word;
word-wrap: break-word;
}
</style>
</head>

View File

@ -21,6 +21,9 @@ mod desktop;
mod mobile;
mod tablet;
// TODO(wathiede): create a QueryString enum that wraps single and multiple message ids and thread
// ids, and has a to_query_string() that knows notmuch's syntax. Then remove the smattering of
// format!() calls all over with magic strings representing notmuch specific syntax.
const MAX_RAW_MESSAGE_SIZE: usize = 100_000;
fn set_title(title: &str) {
seed::document().set_title(&format!("lb: {}", title));
@ -168,7 +171,7 @@ fn view_search_results(
tags_chiclet(&tags, false),
" ",
a![
C!["has-text-light"],
C!["has-text-light", "text"],
attrs! {
At::Href => urls::thread(&tid)
},
@ -180,6 +183,7 @@ fn view_search_results(
});
div![
C!["search-results"],
search_toolbar(count, pager, show_bulk_edit),
table![
C![
@ -236,6 +240,7 @@ fn search_toolbar(
C!["level-left"],
IF!(show_bulk_edit =>
span![
// TODO(wathiede): add "Mark as spam"
C!["level-item", "buttons", "has-addons"],
button![
C!["button"],
@ -420,11 +425,13 @@ fn render_open_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
small![from_detail],
table![
IF!(!msg.to.is_empty() =>
msg.to.iter().enumerate().map(|(i, to)|
tr![
td![ if i==0 { "To" }else { "" } ],
td![
tr![
td![ "To" ],
//td![ if i==0 { "To" }else { "" } ],
td![
msg.to.iter().enumerate().map(|(i, to)|
small![
if i>0 { ", " }else { "" },
match to {
ShowThreadQueryThreadMessagesTo {
name: Some(name),
@ -436,18 +443,20 @@ fn render_open_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
} => format!("{name}"),
ShowThreadQueryThreadMessagesTo {
addr: Some(addr), ..
} => format!("<{addr}>"),
} => format!("{addr}"),
_ => String::from("UNKNOWN"),
}
]
]])),
])
]
]),
IF!(!msg.cc.is_empty() =>
msg.cc.iter().enumerate().map(|(i, cc)|
tr![
td![ if i==0 { "CC" }else { "" } ],
td![
tr![
td![ "CC" ],
td![
msg.cc.iter().enumerate().map(|(i, cc)|
small![
if i>0 { ", " }else { "" },
match cc {
ShowThreadQueryThreadMessagesCc {
name: Some(name),
@ -463,8 +472,9 @@ fn render_open_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
_ => String::from("UNKNOWN"),
}
]
]])),
])
]
]),
tr![
td!["Date"],
td![msg.timestamp.map(|ts| span![C!["header"], human_age(ts)])]
@ -477,21 +487,19 @@ fn render_open_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
C!["media-right"],
span![
C!["read-status"],
i![
C![
"far",
if is_unread {
"fa-envelope"
} else {
"fa-envelope-open"
},
],
ev(Ev::Click, move |e| {
e.stop_propagation();
Msg::SetUnread(format!("id:{id}"), !is_unread)
})
]
]
i![C![
"far",
if is_unread {
"fa-envelope"
} else {
"fa-envelope-open"
},
]]
],
ev(Ev::Click, move |e| {
e.stop_propagation();
Msg::SetUnread(format!("id:{id}"), !is_unread)
})
]
]
}
@ -615,14 +623,14 @@ fn message_render(msg: &ShowThreadQueryThreadMessages, open: bool) -> Node<Msg>
C!["view-part-text-html"],
raw![contents],
IF!(!msg.attachments.is_empty() =>
div![
C!["attachments"],
br![],
h2!["Attachments"],
msg.attachments
.iter()
.map(|a| div!["Filename: ", &a.filename, " ", &a.content_type])
]),
div![
C!["attachments"],
hr![],
h2!["Attachments"],
msg.attachments
.iter()
.map(|a| div!["Filename: ", &a.filename, " ", &a.content_type])
]),
view_content_tree(&content_tree),
],
}
@ -655,6 +663,7 @@ fn thread(thread: &ShowThreadQueryThread, open_messages: &HashSet<String>) -> No
h3![C!["is-size-5"], &thread.subject],
span![C!["tags"], tags_chiclet(&tags, false)],
span![
// TODO(wathiede): add "Mark as spam"
C!["level-item", "buttons", "has-addons"],
button![
C!["button"],
@ -691,7 +700,6 @@ fn thread(thread: &ShowThreadQueryThread, open_messages: &HashSet<String>) -> No
fn view_content_tree(content_tree: &str) -> Node<Msg> {
let debug_open = use_state(|| false);
div![
hr![],
small![
i![C![
"fa-solid",