Compare commits
9 Commits
fc87fd702c
...
56311bbe05
| Author | SHA1 | Date | |
|---|---|---|---|
| 56311bbe05 | |||
| 994631e872 | |||
| 43471d162f | |||
| b997a61da8 | |||
| f69dd0b198 | |||
| 523584fbbc | |||
| 4139ec38d8 | |||
| 5379ae09dc | |||
| ebb16aef9e |
@ -23,6 +23,12 @@ pub type UnixTime = isize;
|
|||||||
/// # Thread ID, sans "thread:"
|
/// # Thread ID, sans "thread:"
|
||||||
pub type ThreadId = String;
|
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)]
|
#[derive(Debug, SimpleObject)]
|
||||||
pub struct ThreadSummary {
|
pub struct ThreadSummary {
|
||||||
pub thread: ThreadId,
|
pub thread: ThreadId,
|
||||||
@ -316,7 +322,6 @@ impl QueryRoot {
|
|||||||
let mut messages = Vec::new();
|
let mut messages = Vec::new();
|
||||||
for (path, id) in std::iter::zip(nm.files(&thread_id)?, nm.message_ids(&thread_id)?) {
|
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}"))?;
|
let tags = nm.tags_for_query(&format!("id:{id}"))?;
|
||||||
info!("{id}: {tags:?}\nfile: {path}");
|
|
||||||
let file = File::open(&path)?;
|
let file = File::open(&path)?;
|
||||||
let mmap = unsafe { MmapOptions::new().map(&file)? };
|
let mmap = unsafe { MmapOptions::new().map(&file)? };
|
||||||
let m = parse_mail(&mmap)?;
|
let m = parse_mail(&mmap)?;
|
||||||
@ -426,10 +431,10 @@ impl Mutation {
|
|||||||
fn extract_body(m: &ParsedMail) -> Result<Body, Error> {
|
fn extract_body(m: &ParsedMail) -> Result<Body, Error> {
|
||||||
let body = m.get_body()?;
|
let body = m.get_body()?;
|
||||||
let ret = match m.ctype.mimetype.as_str() {
|
let ret = match m.ctype.mimetype.as_str() {
|
||||||
"text/plain" => return Ok(Body::text(body)),
|
TEXT_PLAIN => return Ok(Body::text(body)),
|
||||||
"text/html" => return Ok(Body::html(body)),
|
TEXT_HTML => return Ok(Body::html(body)),
|
||||||
"multipart/mixed" => extract_mixed(m),
|
MULTIPART_MIXED => extract_mixed(m),
|
||||||
"multipart/alternative" => extract_alternative(m),
|
MULTIPART_ALTERNATIVE => extract_alternative(m),
|
||||||
_ => extract_unhandled(m),
|
_ => extract_unhandled(m),
|
||||||
};
|
};
|
||||||
if let Err(err) = ret {
|
if let Err(err) = ret {
|
||||||
@ -444,7 +449,6 @@ fn extract_unhandled(m: &ParsedMail) -> Result<Body, Error> {
|
|||||||
"Unhandled body content type:\n{}",
|
"Unhandled body content type:\n{}",
|
||||||
render_content_type_tree(m)
|
render_content_type_tree(m)
|
||||||
);
|
);
|
||||||
warn!("{}", msg);
|
|
||||||
Ok(Body::UnhandledContentType(UnhandledContentType {
|
Ok(Body::UnhandledContentType(UnhandledContentType {
|
||||||
text: msg,
|
text: msg,
|
||||||
}))
|
}))
|
||||||
@ -455,59 +459,100 @@ fn extract_unhandled(m: &ParsedMail) -> Result<Body, Error> {
|
|||||||
// then give up.
|
// then give up.
|
||||||
fn extract_alternative(m: &ParsedMail) -> Result<Body, Error> {
|
fn extract_alternative(m: &ParsedMail) -> Result<Body, Error> {
|
||||||
for sp in &m.subparts {
|
for sp in &m.subparts {
|
||||||
if sp.ctype.mimetype == "text/html" {
|
if sp.ctype.mimetype.as_str() == TEXT_HTML {
|
||||||
let body = sp.get_body()?;
|
let body = sp.get_body()?;
|
||||||
return Ok(Body::html(body));
|
return Ok(Body::html(body));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for sp in &m.subparts {
|
for sp in &m.subparts {
|
||||||
if sp.ctype.mimetype == "text/plain" {
|
if sp.ctype.mimetype.as_str() == TEXT_PLAIN {
|
||||||
let body = sp.get_body()?;
|
let body = sp.get_body()?;
|
||||||
return Ok(Body::text(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
|
// multipart/mixed defines multiple types of context all of which should be presented to the user
|
||||||
// 'serially'.
|
// 'serially'.
|
||||||
fn extract_mixed(m: &ParsedMail) -> Result<Body, Error> {
|
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 {
|
for sp in &m.subparts {
|
||||||
if sp.ctype.mimetype == "multipart/alternative" {
|
if sp.ctype.mimetype.as_str() == MULTIPART_ALTERNATIVE {
|
||||||
return extract_alternative(sp);
|
return extract_alternative(sp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for sp in &m.subparts {
|
for sp in &m.subparts {
|
||||||
if sp.ctype.mimetype == "multipart/related" {
|
if sp.ctype.mimetype == MULTIPART_RELATED {
|
||||||
return extract_related(sp);
|
return extract_related(sp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for sp in &m.subparts {
|
for sp in &m.subparts {
|
||||||
let body = sp.get_body()?;
|
let body = sp.get_body()?;
|
||||||
match sp.ctype.mimetype.as_str() {
|
match sp.ctype.mimetype.as_str() {
|
||||||
"text/plain" => return Ok(Body::text(body)),
|
TEXT_PLAIN => return Ok(Body::text(body)),
|
||||||
"text/html" => return Ok(Body::html(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> {
|
fn extract_related(m: &ParsedMail) -> 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![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 {
|
for sp in &m.subparts {
|
||||||
if sp.ctype.mimetype == "text/html" {
|
if sp.ctype.mimetype == TEXT_HTML {
|
||||||
let body = sp.get_body()?;
|
let body = sp.get_body()?;
|
||||||
return Ok(Body::html(body));
|
return Ok(Body::html(body));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for sp in &m.subparts {
|
for sp in &m.subparts {
|
||||||
if sp.ctype.mimetype == "text/plain" {
|
if sp.ctype.mimetype == TEXT_PLAIN {
|
||||||
let body = sp.get_body()?;
|
let body = sp.get_body()?;
|
||||||
return Ok(Body::text(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.
|
// TODO(wathiede): make this walk_attachments that takes a closure.
|
||||||
|
|||||||
@ -27,6 +27,10 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message .header .media-right {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.message .headers {
|
.message .headers {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -54,6 +58,13 @@
|
|||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message .body .attachments hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px dashed #888;
|
||||||
|
background-color: #f000;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
background-color: red;
|
background-color: red;
|
||||||
}
|
}
|
||||||
@ -61,7 +72,9 @@
|
|||||||
.view-part-text-plain {
|
.view-part-text-plain {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
white-space: pre-line;
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
@ -96,7 +109,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.index .date {
|
.index .date {
|
||||||
width: 10em;
|
width: 7em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
@ -183,6 +196,7 @@
|
|||||||
|
|
||||||
.search-results .row .summary {
|
.search-results .row .summary {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-results .row .subject {
|
.search-results .row .subject {
|
||||||
@ -191,6 +205,20 @@
|
|||||||
white-space: nowrap;
|
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 {
|
.search-results .row .from {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@ -250,7 +278,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content-tree {
|
.content-tree {
|
||||||
white-space: pre-line;
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@ -21,6 +21,9 @@ mod desktop;
|
|||||||
mod mobile;
|
mod mobile;
|
||||||
mod tablet;
|
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;
|
const MAX_RAW_MESSAGE_SIZE: usize = 100_000;
|
||||||
fn set_title(title: &str) {
|
fn set_title(title: &str) {
|
||||||
seed::document().set_title(&format!("lb: {}", title));
|
seed::document().set_title(&format!("lb: {}", title));
|
||||||
@ -168,7 +171,7 @@ fn view_search_results(
|
|||||||
tags_chiclet(&tags, false),
|
tags_chiclet(&tags, false),
|
||||||
" ",
|
" ",
|
||||||
a![
|
a![
|
||||||
C!["has-text-light"],
|
C!["has-text-light", "text"],
|
||||||
attrs! {
|
attrs! {
|
||||||
At::Href => urls::thread(&tid)
|
At::Href => urls::thread(&tid)
|
||||||
},
|
},
|
||||||
@ -180,6 +183,7 @@ fn view_search_results(
|
|||||||
});
|
});
|
||||||
|
|
||||||
div![
|
div![
|
||||||
|
C!["search-results"],
|
||||||
search_toolbar(count, pager, show_bulk_edit),
|
search_toolbar(count, pager, show_bulk_edit),
|
||||||
table![
|
table![
|
||||||
C![
|
C![
|
||||||
@ -236,6 +240,7 @@ fn search_toolbar(
|
|||||||
C!["level-left"],
|
C!["level-left"],
|
||||||
IF!(show_bulk_edit =>
|
IF!(show_bulk_edit =>
|
||||||
span![
|
span![
|
||||||
|
// TODO(wathiede): add "Mark as spam"
|
||||||
C!["level-item", "buttons", "has-addons"],
|
C!["level-item", "buttons", "has-addons"],
|
||||||
button![
|
button![
|
||||||
C!["button"],
|
C!["button"],
|
||||||
@ -420,11 +425,13 @@ fn render_open_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
|
|||||||
small![from_detail],
|
small![from_detail],
|
||||||
table![
|
table![
|
||||||
IF!(!msg.to.is_empty() =>
|
IF!(!msg.to.is_empty() =>
|
||||||
msg.to.iter().enumerate().map(|(i, to)|
|
|
||||||
tr![
|
tr![
|
||||||
td![ if i==0 { "To" }else { "" } ],
|
td![ "To" ],
|
||||||
|
//td![ if i==0 { "To" }else { "" } ],
|
||||||
td![
|
td![
|
||||||
|
msg.to.iter().enumerate().map(|(i, to)|
|
||||||
small![
|
small![
|
||||||
|
if i>0 { ", " }else { "" },
|
||||||
match to {
|
match to {
|
||||||
ShowThreadQueryThreadMessagesTo {
|
ShowThreadQueryThreadMessagesTo {
|
||||||
name: Some(name),
|
name: Some(name),
|
||||||
@ -436,18 +443,20 @@ fn render_open_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
|
|||||||
} => format!("{name}"),
|
} => format!("{name}"),
|
||||||
ShowThreadQueryThreadMessagesTo {
|
ShowThreadQueryThreadMessagesTo {
|
||||||
addr: Some(addr), ..
|
addr: Some(addr), ..
|
||||||
} => format!("<{addr}>"),
|
} => format!("{addr}"),
|
||||||
_ => String::from("UNKNOWN"),
|
_ => String::from("UNKNOWN"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
])
|
||||||
]
|
]
|
||||||
]])),
|
]),
|
||||||
IF!(!msg.cc.is_empty() =>
|
IF!(!msg.cc.is_empty() =>
|
||||||
msg.cc.iter().enumerate().map(|(i, cc)|
|
|
||||||
tr![
|
tr![
|
||||||
td![ if i==0 { "CC" }else { "" } ],
|
td![ "CC" ],
|
||||||
td![
|
td![
|
||||||
|
msg.cc.iter().enumerate().map(|(i, cc)|
|
||||||
small![
|
small![
|
||||||
|
if i>0 { ", " }else { "" },
|
||||||
match cc {
|
match cc {
|
||||||
ShowThreadQueryThreadMessagesCc {
|
ShowThreadQueryThreadMessagesCc {
|
||||||
name: Some(name),
|
name: Some(name),
|
||||||
@ -463,8 +472,9 @@ fn render_open_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
|
|||||||
_ => String::from("UNKNOWN"),
|
_ => String::from("UNKNOWN"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
])
|
||||||
]
|
]
|
||||||
]])),
|
]),
|
||||||
tr![
|
tr![
|
||||||
td!["Date"],
|
td!["Date"],
|
||||||
td![msg.timestamp.map(|ts| span![C!["header"], human_age(ts)])]
|
td![msg.timestamp.map(|ts| span![C!["header"], human_age(ts)])]
|
||||||
@ -477,14 +487,14 @@ fn render_open_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
|
|||||||
C!["media-right"],
|
C!["media-right"],
|
||||||
span![
|
span![
|
||||||
C!["read-status"],
|
C!["read-status"],
|
||||||
i![
|
i![C![
|
||||||
C![
|
|
||||||
"far",
|
"far",
|
||||||
if is_unread {
|
if is_unread {
|
||||||
"fa-envelope"
|
"fa-envelope"
|
||||||
} else {
|
} else {
|
||||||
"fa-envelope-open"
|
"fa-envelope-open"
|
||||||
},
|
},
|
||||||
|
]]
|
||||||
],
|
],
|
||||||
ev(Ev::Click, move |e| {
|
ev(Ev::Click, move |e| {
|
||||||
e.stop_propagation();
|
e.stop_propagation();
|
||||||
@ -492,8 +502,6 @@ fn render_open_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
|
|||||||
})
|
})
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_closed_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
|
fn render_closed_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
|
||||||
@ -617,7 +625,7 @@ fn message_render(msg: &ShowThreadQueryThreadMessages, open: bool) -> Node<Msg>
|
|||||||
IF!(!msg.attachments.is_empty() =>
|
IF!(!msg.attachments.is_empty() =>
|
||||||
div![
|
div![
|
||||||
C!["attachments"],
|
C!["attachments"],
|
||||||
br![],
|
hr![],
|
||||||
h2!["Attachments"],
|
h2!["Attachments"],
|
||||||
msg.attachments
|
msg.attachments
|
||||||
.iter()
|
.iter()
|
||||||
@ -655,6 +663,7 @@ fn thread(thread: &ShowThreadQueryThread, open_messages: &HashSet<String>) -> No
|
|||||||
h3![C!["is-size-5"], &thread.subject],
|
h3![C!["is-size-5"], &thread.subject],
|
||||||
span![C!["tags"], tags_chiclet(&tags, false)],
|
span![C!["tags"], tags_chiclet(&tags, false)],
|
||||||
span![
|
span![
|
||||||
|
// TODO(wathiede): add "Mark as spam"
|
||||||
C!["level-item", "buttons", "has-addons"],
|
C!["level-item", "buttons", "has-addons"],
|
||||||
button![
|
button![
|
||||||
C!["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> {
|
fn view_content_tree(content_tree: &str) -> Node<Msg> {
|
||||||
let debug_open = use_state(|| false);
|
let debug_open = use_state(|| false);
|
||||||
div![
|
div![
|
||||||
hr![],
|
|
||||||
small![
|
small![
|
||||||
i![C![
|
i![C![
|
||||||
"fa-solid",
|
"fa-solid",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user