Compare commits
No commits in common. "56311bbe05a8559939db585ccd0c0d69e67a736c" and "fc87fd702c164169b62b9cf45a3ba6fd30804bb3" have entirely different histories.
56311bbe05
...
fc87fd702c
@ -23,12 +23,6 @@ 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,
|
||||||
@ -322,6 +316,7 @@ 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)?;
|
||||||
@ -431,10 +426,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 {
|
||||||
@ -449,6 +444,7 @@ 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,
|
||||||
}))
|
}))
|
||||||
@ -459,100 +455,59 @@ 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.as_str() == 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.as_str() == 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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for sp in &m.subparts {
|
Err("extract_alternative".into())
|
||||||
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.as_str() == MULTIPART_ALTERNATIVE {
|
if sp.ctype.mimetype == "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(format!(
|
Err("extract_mixed".into())
|
||||||
"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(format!(
|
Err("extract_related".into())
|
||||||
"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,10 +27,6 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message .header .media-right {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message .headers {
|
.message .headers {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -58,13 +54,6 @@
|
|||||||
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;
|
||||||
}
|
}
|
||||||
@ -72,9 +61,7 @@
|
|||||||
.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-wrap;
|
white-space: pre-line;
|
||||||
word-break: break-word;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
@ -109,7 +96,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.index .date {
|
.index .date {
|
||||||
width: 7em;
|
width: 10em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
@ -196,7 +183,6 @@
|
|||||||
|
|
||||||
.search-results .row .summary {
|
.search-results .row .summary {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-results .row .subject {
|
.search-results .row .subject {
|
||||||
@ -205,20 +191,6 @@
|
|||||||
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;
|
||||||
@ -278,9 +250,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content-tree {
|
.content-tree {
|
||||||
white-space: pre-wrap;
|
white-space: pre-line;
|
||||||
word-break: break-word;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@ -21,9 +21,6 @@ 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));
|
||||||
@ -171,7 +168,7 @@ fn view_search_results(
|
|||||||
tags_chiclet(&tags, false),
|
tags_chiclet(&tags, false),
|
||||||
" ",
|
" ",
|
||||||
a![
|
a![
|
||||||
C!["has-text-light", "text"],
|
C!["has-text-light"],
|
||||||
attrs! {
|
attrs! {
|
||||||
At::Href => urls::thread(&tid)
|
At::Href => urls::thread(&tid)
|
||||||
},
|
},
|
||||||
@ -183,7 +180,6 @@ 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![
|
||||||
@ -240,7 +236,6 @@ 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"],
|
||||||
@ -425,13 +420,11 @@ 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() =>
|
||||||
tr![
|
msg.to.iter().enumerate().map(|(i, to)|
|
||||||
td![ "To" ],
|
tr![
|
||||||
//td![ if i==0 { "To" }else { "" } ],
|
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),
|
||||||
@ -443,20 +436,18 @@ 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() =>
|
||||||
tr![
|
msg.cc.iter().enumerate().map(|(i, cc)|
|
||||||
td![ "CC" ],
|
tr![
|
||||||
td![
|
td![ if i==0 { "CC" }else { "" } ],
|
||||||
msg.cc.iter().enumerate().map(|(i, cc)|
|
td![
|
||||||
small![
|
small![
|
||||||
if i>0 { ", " }else { "" },
|
|
||||||
match cc {
|
match cc {
|
||||||
ShowThreadQueryThreadMessagesCc {
|
ShowThreadQueryThreadMessagesCc {
|
||||||
name: Some(name),
|
name: Some(name),
|
||||||
@ -472,9 +463,8 @@ 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)])]
|
||||||
@ -487,19 +477,21 @@ fn render_open_header(msg: &ShowThreadQueryThreadMessages) -> Node<Msg> {
|
|||||||
C!["media-right"],
|
C!["media-right"],
|
||||||
span![
|
span![
|
||||||
C!["read-status"],
|
C!["read-status"],
|
||||||
i![C![
|
i![
|
||||||
"far",
|
C![
|
||||||
if is_unread {
|
"far",
|
||||||
"fa-envelope"
|
if is_unread {
|
||||||
} else {
|
"fa-envelope"
|
||||||
"fa-envelope-open"
|
} else {
|
||||||
},
|
"fa-envelope-open"
|
||||||
]]
|
},
|
||||||
],
|
],
|
||||||
ev(Ev::Click, move |e| {
|
ev(Ev::Click, move |e| {
|
||||||
e.stop_propagation();
|
e.stop_propagation();
|
||||||
Msg::SetUnread(format!("id:{id}"), !is_unread)
|
Msg::SetUnread(format!("id:{id}"), !is_unread)
|
||||||
})
|
})
|
||||||
|
]
|
||||||
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -623,14 +615,14 @@ fn message_render(msg: &ShowThreadQueryThreadMessages, open: bool) -> Node<Msg>
|
|||||||
C!["view-part-text-html"],
|
C!["view-part-text-html"],
|
||||||
raw![contents],
|
raw![contents],
|
||||||
IF!(!msg.attachments.is_empty() =>
|
IF!(!msg.attachments.is_empty() =>
|
||||||
div![
|
div![
|
||||||
C!["attachments"],
|
C!["attachments"],
|
||||||
hr![],
|
br![],
|
||||||
h2!["Attachments"],
|
h2!["Attachments"],
|
||||||
msg.attachments
|
msg.attachments
|
||||||
.iter()
|
.iter()
|
||||||
.map(|a| div!["Filename: ", &a.filename, " ", &a.content_type])
|
.map(|a| div!["Filename: ", &a.filename, " ", &a.content_type])
|
||||||
]),
|
]),
|
||||||
view_content_tree(&content_tree),
|
view_content_tree(&content_tree),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@ -663,7 +655,6 @@ 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"],
|
||||||
@ -700,6 +691,7 @@ 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