Compare commits

..

1 Commits

Author SHA1 Message Date
cea063abc4 chore(deps): update rust crate serde to v1.0.220
Some checks failed
Continuous integration / Check (push) Successful in 2m6s
Continuous integration / Test Suite (push) Failing after 4m22s
Continuous integration / Trunk (push) Successful in 7m29s
Continuous integration / Rustfmt (push) Successful in 28s
Continuous integration / build (push) Successful in 4m20s
Continuous integration / Disallow unused dependencies (push) Successful in 2m5s
2025-09-13 22:01:42 +00:00
7 changed files with 731 additions and 754 deletions

1156
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ authors = ["Bill Thiede <git@xinu.tv>"]
edition = "2021" edition = "2021"
license = "UNLICENSED" license = "UNLICENSED"
publish = ["xinu"] publish = ["xinu"]
version = "0.17.45" version = "0.17.43"
repository = "https://git.z.xinu.tv/wathiede/letterbox" repository = "https://git.z.xinu.tv/wathiede/letterbox"
[profile.dev] [profile.dev]

View File

@@ -32,8 +32,8 @@ futures = "0.3.31"
headers = "0.4.0" headers = "0.4.0"
html-escape = "0.2.13" html-escape = "0.2.13"
ical = "0.11" ical = "0.11"
letterbox-notmuch = { path = "../notmuch", version = "0.17.45", registry = "xinu" } letterbox-notmuch = { path = "../notmuch", version = "0.17.43", registry = "xinu" }
letterbox-shared = { path = "../shared", version = "0.17.45", registry = "xinu" } letterbox-shared = { path = "../shared", version = "0.17.43", registry = "xinu" }
linkify = "0.10.0" linkify = "0.10.0"
lol_html = "2.3.0" lol_html = "2.3.0"
mailparse = "0.16.1" mailparse = "0.16.1"
@@ -56,7 +56,7 @@ urlencoding = "2.1.3"
#xtracing = { git = "http://git-private.h.xinu.tv/wathiede/xtracing.git" } #xtracing = { git = "http://git-private.h.xinu.tv/wathiede/xtracing.git" }
#xtracing = { path = "../../xtracing" } #xtracing = { path = "../../xtracing" }
xtracing = { version = "0.3.2", registry = "xinu" } xtracing = { version = "0.3.2", registry = "xinu" }
zip = "5.0.0" zip = "4.3.0"
[build-dependencies] [build-dependencies]

View File

@@ -17,11 +17,9 @@ use crate::{
const APPLICATION_GZIP: &'static str = "application/gzip"; const APPLICATION_GZIP: &'static str = "application/gzip";
const APPLICATION_ZIP: &'static str = "application/zip"; const APPLICATION_ZIP: &'static str = "application/zip";
const APPLICATION_TLSRPT_GZIP: &'static str = "application/tlsrpt+gzip";
const IMAGE_JPEG: &'static str = "image/jpeg"; const IMAGE_JPEG: &'static str = "image/jpeg";
const IMAGE_PJPEG: &'static str = "image/pjpeg"; const IMAGE_PJPEG: &'static str = "image/pjpeg";
const IMAGE_PNG: &'static str = "image/png"; const IMAGE_PNG: &'static str = "image/png";
const MESSAGE_DELIVERY_STATUS: &'static str = "message/delivery-status";
const MESSAGE_RFC822: &'static str = "message/rfc822"; const MESSAGE_RFC822: &'static str = "message/rfc822";
const MULTIPART_ALTERNATIVE: &'static str = "multipart/alternative"; const MULTIPART_ALTERNATIVE: &'static str = "multipart/alternative";
const MULTIPART_MIXED: &'static str = "multipart/mixed"; const MULTIPART_MIXED: &'static str = "multipart/mixed";
@@ -643,186 +641,115 @@ pub fn extract_gzip(m: &ParsedMail) -> Result<(Body, Option<String>), ServerErro
Ok((extract_unhandled(m)?, None)) Ok((extract_unhandled(m)?, None))
} }
pub fn extract_report(m: &ParsedMail, part_addr: &mut Vec<String>) -> Result<Body, ServerError> { pub fn extract_report(m: &ParsedMail, _part_addr: &mut Vec<String>) -> Result<Body, ServerError> {
let mut parts = Vec::new(); let mut html_part = None;
let mut tlsrpt_part = None;
for (idx, sp) in m.subparts.iter().enumerate() {
part_addr.push(idx.to_string());
for sp in &m.subparts {
match sp.ctype.mimetype.as_str() { match sp.ctype.mimetype.as_str() {
APPLICATION_TLSRPT_GZIP => { TEXT_HTML => html_part = Some(sp.get_body()?),
let gz_bytes = sp.get_body_raw()?; "application/tlsrpt+gzip" => tlsrpt_part = Some(sp.get_body_raw()?),
let mut decoder = flate2::read::GzDecoder::new(&gz_bytes[..]); _ => {} // Ignore other parts for now
let mut buffer = Vec::new(); }
if decoder.read_to_end(&mut buffer).is_ok() { }
if let Ok(json_str) = String::from_utf8(buffer) {
match serde_json::from_str::<TlsRpt>(&json_str) { let tlsrpt_summary_html = if let Some(gz_bytes) = tlsrpt_part {
Ok(tlsrpt) => { let mut decoder = flate2::read::GzDecoder::new(&gz_bytes[..]);
let formatted_tlsrpt = FormattedTlsRpt { let mut buffer = Vec::new();
organization_name: tlsrpt.organization_name, if decoder.read_to_end(&mut buffer).is_ok() {
date_range: FormattedTlsRptDateRange { if let Ok(json_str) = String::from_utf8(buffer) {
start_datetime: tlsrpt.date_range.start_datetime, match serde_json::from_str::<TlsRpt>(&json_str) {
end_datetime: tlsrpt.date_range.end_datetime, Ok(tlsrpt) => {
let formatted_tlsrpt = FormattedTlsRpt {
organization_name: tlsrpt.organization_name,
date_range: FormattedTlsRptDateRange {
start_datetime: tlsrpt.date_range.start_datetime,
end_datetime: tlsrpt.date_range.end_datetime,
},
contact_info: tlsrpt.contact_info.unwrap_or_else(|| "".to_string()),
report_id: tlsrpt.report_id,
policies: tlsrpt
.policies
.into_iter()
.map(|policy| FormattedTlsRptPolicy {
policy: FormattedTlsRptPolicyDetails {
policy_type: policy.policy.policy_type,
policy_string: policy.policy.policy_string,
policy_domain: policy.policy.policy_domain,
mx_host: policy
.policy
.mx_host
.unwrap_or_else(|| Vec::new())
.into_iter()
.map(|mx| match mx {
MxHost::String(s) => FormattedTlsRptMxHost {
hostname: s,
failure_count: 0,
result_type: "".to_string(),
},
MxHost::Object(o) => FormattedTlsRptMxHost {
hostname: o.hostname,
failure_count: o.failure_count,
result_type: o.result_type,
},
})
.collect(),
}, },
contact_info: tlsrpt summary: policy.summary,
.contact_info failure_details: policy
.unwrap_or_else(|| "".to_string()), .failure_details
report_id: tlsrpt.report_id, .unwrap_or_else(|| Vec::new())
policies: tlsrpt
.policies
.into_iter() .into_iter()
.map(|policy| FormattedTlsRptPolicy { .map(|detail| FormattedTlsRptFailureDetails {
policy: FormattedTlsRptPolicyDetails { result_type: detail.result_type,
policy_type: policy.policy.policy_type, sending_mta_ip: detail
policy_string: policy.policy.policy_string, .sending_mta_ip
policy_domain: policy.policy.policy_domain, .unwrap_or_else(|| "".to_string()),
mx_host: policy receiving_ip: detail
.policy .receiving_ip
.mx_host .unwrap_or_else(|| "".to_string()),
.unwrap_or_else(|| Vec::new()) receiving_mx_hostname: detail
.into_iter() .receiving_mx_hostname
.map(|mx| match mx { .unwrap_or_else(|| "".to_string()),
MxHost::String(s) => { failed_session_count: detail.failed_session_count,
FormattedTlsRptMxHost { additional_info: detail
hostname: s, .additional_info
failure_count: 0, .unwrap_or_else(|| "".to_string()),
result_type: "".to_string(), failure_reason_code: detail
} .failure_reason_code
} .unwrap_or_else(|| "".to_string()),
MxHost::Object(o) => {
FormattedTlsRptMxHost {
hostname: o.hostname,
failure_count: o.failure_count,
result_type: o.result_type,
}
}
})
.collect(),
},
summary: policy.summary,
failure_details: policy
.failure_details
.unwrap_or_else(|| Vec::new())
.into_iter()
.map(|detail| FormattedTlsRptFailureDetails {
result_type: detail.result_type,
sending_mta_ip: detail
.sending_mta_ip
.unwrap_or_else(|| "".to_string()),
receiving_ip: detail
.receiving_ip
.unwrap_or_else(|| "".to_string()),
receiving_mx_hostname: detail
.receiving_mx_hostname
.unwrap_or_else(|| "".to_string()),
failed_session_count: detail
.failed_session_count,
additional_info: detail
.additional_info
.unwrap_or_else(|| "".to_string()),
failure_reason_code: detail
.failure_reason_code
.unwrap_or_else(|| "".to_string()),
})
.collect(),
}) })
.collect(), .collect(),
}; })
let template = TlsReportTemplate { .collect(),
report: &formatted_tlsrpt, };
}; let template = TlsReportTemplate {
let html = template.render().unwrap_or_else(|e| format!("<div class=\"tlsrpt-error\">Failed to render TLS report template: {}</div>", e)); report: &formatted_tlsrpt,
parts.push(Body::html(html)); };
} template.render().unwrap_or_else(|e| format!("<div class=\"tlsrpt-error\">Failed to render TLS report template: {}</div>", e))
Err(e) => {
let html = format!(
"<div class=\"tlsrpt-error\">Failed to parse TLS report JSON: {}</div>",
e
);
parts.push(Body::html(html));
}
}
} else {
let html = format!("<div class=\"tlsrpt-error\">Failed to convert decompressed data to UTF-8.</div>");
parts.push(Body::html(html));
}
} else {
let html =
format!("<div class=\"tlsrpt-error\">Failed to decompress data.</div>");
parts.push(Body::html(html));
}
}
MESSAGE_RFC822 => {
parts.push(extract_rfc822(&sp, part_addr)?);
}
TEXT_HTML => {
let body = sp.get_body()?;
parts.push(Body::html(body));
}
MESSAGE_DELIVERY_STATUS => {
let body = extract_delivery_status(sp)?;
parts.push(body);
}
TEXT_PLAIN => {
let body = sp.get_body()?;
parts.push(Body::text(body));
}
_ => {
// For any other content type, try to extract the body using the general extract_body function
match extract_body(sp, part_addr) {
Ok(body) => parts.push(body),
Err(_) => {
// If extraction fails, create an unhandled content type body
let msg = format!(
"Unhandled report subpart content type: {}\n{}",
sp.ctype.mimetype,
sp.get_body()
.unwrap_or_else(|_| "Failed to get body".to_string())
);
parts.push(Body::UnhandledContentType(UnhandledContentType {
text: msg,
content_tree: render_content_type_tree(sp),
}));
} }
Err(e) => format!(
"<div class=\"tlsrpt-error\">Failed to parse TLS report JSON: {}</div>",
e
),
} }
} else {
format!("<div class=\"tlsrpt-error\">Failed to convert decompressed data to UTF-8.</div>")
} }
} else {
format!("<div class=\"tlsrpt-error\">Failed to decompressed data.</div>")
} }
} else {
"".to_string()
};
part_addr.pop(); let final_html = if let Some(html) = html_part {
} format!("{}<hr>{} ", html, tlsrpt_summary_html)
} else {
tlsrpt_summary_html
};
if parts.is_empty() { Ok(Body::html(final_html))
return Ok(Body::html(
"<div class=\"report-error\">No report content found</div>".to_string(),
));
}
// Add <hr> tags between subparts for better visual separation
let html = parts
.iter()
.map(|p| match p {
Body::PlainText(PlainText { text, .. }) => {
format!(
r#"<p class="view-part-text-plain font-mono whitespace-pre-line">{}</p>"#,
linkify_html(&html_escape::encode_text(text).trim_matches('\n'))
)
}
Body::Html(Html { html, .. }) => html.clone(),
Body::UnhandledContentType(UnhandledContentType { text, .. }) => {
format!(
r#"<p class="view-part-unhandled">{}</p>"#,
linkify_html(&html_escape::encode_text(text).trim_matches('\n'))
)
}
})
.collect::<Vec<_>>()
.join("<hr>\n");
Ok(Body::html(html))
}
pub fn extract_delivery_status(m: &ParsedMail) -> Result<Body, ServerError> {
Ok(Body::text(m.get_body()?))
} }
pub fn extract_unhandled(m: &ParsedMail) -> Result<Body, ServerError> { pub fn extract_unhandled(m: &ParsedMail) -> Result<Body, ServerError> {
@@ -2230,28 +2157,28 @@ mod tests {
let meta = extract_calendar_metadata_from_mail(&parsed, &body); let meta = extract_calendar_metadata_from_mail(&parsed, &body);
// Assert detection as Google Calendar // Assert detection as Google Calendar
assert!(meta.is_google_calendar_event); assert!(meta.is_google_calendar_event);
// Debug: print the rendered HTML for inspection
let html = meta.body_html.expect("body_html"); let html = meta.body_html.expect("body_html");
// Print event date info for debugging
for part in parsed.subparts.iter() {
if part.ctype.mimetype == TEXT_CALENDAR {
if let Ok(ical) = part.get_body() {
println!("ICAL data: {}", ical);
if let Some(start) = ical.lines().find(|l| l.starts_with("DTSTART:")) {
println!("Start date: {}", start);
}
}
}
}
println!("Rendered HTML: {}", html); println!("Rendered HTML: {}", html);
// Check that the calendar table highlights Thursday, not Friday
// Look for September 11 (Thursday) being highlighted // Look for a table header row with days of week (allow whitespace)
// The calendar should show Sept 11 highlighted with background:#ffd700 and the correct data-event-day let thursday_idx = html
assert!(html.contains(r#"data-event-day="2025-09-11""#)); .find(">\n Thu<")
assert!(html.contains(r#"background:#ffd700"#)); .or_else(|| html.find(">Thu<"))
.expect("Should have a Thursday column");
// Since 1:00 AM UTC on Friday 9/12 is 6:00 PM PDT on Thursday 9/11, verify times are correct let friday_idx = html
assert!(html.contains("6:00 PM Thu Sep 11, 2025")); .find(">\n Fri<")
.or_else(|| html.find(">Fri<"))
.expect("Should have a Friday column");
// Find the first highlighted cell (background:#ffd700)
let highlight_idx = html
.find("background:#ffd700")
.expect("Should highlight a day");
// The highlight should be closer to Thursday than Friday
assert!(
highlight_idx > thursday_idx && highlight_idx < friday_idx,
"Thursday should be highlighted, not Friday"
);
} }
use super::*; use super::*;
#[test] #[test]

View File

@@ -74,7 +74,13 @@
{% for week in all_days|batch(7) %} {% for week in all_days|batch(7) %}
<tr> <tr>
{% for day in week %} {% for day in week %}
{% if event_days.contains(day) %} {% if event_days.contains(day) && today.is_some() && today.unwrap() == day %}
<td
data-event-day="{{ day.format("%Y-%m-%d") }}"
style="background:#ffd700; color:#222; font-weight:bold; border:2px solid #2196f3; border-radius:4px; text-align:center; box-shadow:0 0 0 2px #2196f3;">
{{ day.day() }}
</td>
{% elif event_days.contains(day) %}
<td <td
data-event-day="{{ day.format("%Y-%m-%d") }}" data-event-day="{{ day.format("%Y-%m-%d") }}"
style="background:#ffd700; color:#222; font-weight:bold; border:1px solid #aaa; border-radius:4px; text-align:center;"> style="background:#ffd700; color:#222; font-weight:bold; border:1px solid #aaa; border-radius:4px; text-align:center;">

View File

@@ -12,7 +12,7 @@ version.workspace = true
[dependencies] [dependencies]
build-info = "0.0.41" build-info = "0.0.41"
letterbox-notmuch = { path = "../notmuch", version = "0.17.45", registry = "xinu" } letterbox-notmuch = { path = "../notmuch", version = "0.17.43", registry = "xinu" }
regex = "1.11.1" regex = "1.11.1"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
sqlx = "0.8.5" sqlx = "0.8.5"

View File

@@ -12,7 +12,7 @@ version.workspace = true
build-info-build = "0.0.41" build-info-build = "0.0.41"
[dev-dependencies] [dev-dependencies]
#wasm-bindgen-test = "0.3.50" wasm-bindgen-test = "0.3.50"
[dependencies] [dependencies]
console_error_panic_hook = "0.1.7" console_error_panic_hook = "0.1.7"
@@ -33,7 +33,7 @@ wasm-bindgen = "=0.2.100"
uuid = { version = "1.16.0", features = [ uuid = { version = "1.16.0", features = [
"js", "js",
] } # direct dep to set js feature, prevents Rng issues ] } # direct dep to set js feature, prevents Rng issues
letterbox-shared = { path = "../shared/", version = "0.17.45", registry = "xinu" } letterbox-shared = { path = "../shared/", version = "0.17.43", registry = "xinu" }
seed_hooks = { version = "0.4.1", registry = "xinu" } seed_hooks = { version = "0.4.1", registry = "xinu" }
strum_macros = "0.27.1" strum_macros = "0.27.1"
gloo-console = "0.3.0" gloo-console = "0.3.0"