Compare commits
3 Commits
285ff1d098
...
17ad5b3b0b
| Author | SHA1 | Date | |
|---|---|---|---|
| 17ad5b3b0b | |||
| 285b2f1591 | |||
| 1537333e76 |
52
Cargo.lock
generated
52
Cargo.lock
generated
@ -3131,6 +3131,20 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "letterbox-notmuch"
|
||||
version = "0.17.44"
|
||||
source = "sparse+https://git.z.xinu.tv/api/packages/wathiede/cargo/"
|
||||
checksum = "c134bb4ba84b2d44a2e4fce8369edc2c5a32bd7e00a4a32b2c179148ab4251b2"
|
||||
dependencies = [
|
||||
"log",
|
||||
"mailparse",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "letterbox-notmuch"
|
||||
version = "0.17.45"
|
||||
dependencies = [
|
||||
"itertools",
|
||||
"log",
|
||||
@ -3143,28 +3157,14 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "letterbox-notmuch"
|
||||
version = "0.17.44"
|
||||
source = "sparse+https://git.z.xinu.tv/api/packages/wathiede/cargo/"
|
||||
checksum = "c134bb4ba84b2d44a2e4fce8369edc2c5a32bd7e00a4a32b2c179148ab4251b2"
|
||||
dependencies = [
|
||||
"log",
|
||||
"mailparse",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "letterbox-procmail2notmuch"
|
||||
version = "0.17.44"
|
||||
version = "0.17.45"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"letterbox-notmuch 0.17.44 (sparse+https://git.z.xinu.tv/api/packages/wathiede/cargo/)",
|
||||
"letterbox-shared 0.17.44 (sparse+https://git.z.xinu.tv/api/packages/wathiede/cargo/)",
|
||||
"letterbox-notmuch 0.17.44",
|
||||
"letterbox-shared 0.17.44",
|
||||
"serde",
|
||||
"sqlx",
|
||||
"tokio 1.48.0",
|
||||
@ -3172,7 +3172,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "letterbox-server"
|
||||
version = "0.17.44"
|
||||
version = "0.17.45"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"anyhow",
|
||||
@ -3195,8 +3195,8 @@ dependencies = [
|
||||
"html-escape",
|
||||
"html2text",
|
||||
"ical",
|
||||
"letterbox-notmuch 0.17.44",
|
||||
"letterbox-shared 0.17.44",
|
||||
"letterbox-notmuch 0.17.45",
|
||||
"letterbox-shared 0.17.45",
|
||||
"linkify",
|
||||
"lol_html",
|
||||
"mailparse",
|
||||
@ -3223,6 +3223,8 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "letterbox-shared"
|
||||
version = "0.17.44"
|
||||
source = "sparse+https://git.z.xinu.tv/api/packages/wathiede/cargo/"
|
||||
checksum = "6b56f113a3e37cf56af9a73c180689661cf5a501262feb45b518b6ddcd0daaaf"
|
||||
dependencies = [
|
||||
"build-info",
|
||||
"letterbox-notmuch 0.17.44",
|
||||
@ -3235,12 +3237,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "letterbox-shared"
|
||||
version = "0.17.44"
|
||||
source = "sparse+https://git.z.xinu.tv/api/packages/wathiede/cargo/"
|
||||
checksum = "6b56f113a3e37cf56af9a73c180689661cf5a501262feb45b518b6ddcd0daaaf"
|
||||
version = "0.17.45"
|
||||
dependencies = [
|
||||
"build-info",
|
||||
"letterbox-notmuch 0.17.44 (sparse+https://git.z.xinu.tv/api/packages/wathiede/cargo/)",
|
||||
"letterbox-notmuch 0.17.45",
|
||||
"regex",
|
||||
"serde",
|
||||
"sqlx",
|
||||
@ -3250,7 +3250,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "letterbox-web"
|
||||
version = "0.17.44"
|
||||
version = "0.17.45"
|
||||
dependencies = [
|
||||
"build-info",
|
||||
"build-info-build",
|
||||
@ -3262,7 +3262,7 @@ dependencies = [
|
||||
"graphql_client",
|
||||
"human_format",
|
||||
"itertools",
|
||||
"letterbox-shared 0.17.44",
|
||||
"letterbox-shared 0.17.45",
|
||||
"log",
|
||||
"seed",
|
||||
"seed_hooks",
|
||||
|
||||
@ -8,7 +8,7 @@ authors = ["Bill Thiede <git@xinu.tv>"]
|
||||
edition = "2021"
|
||||
license = "UNLICENSED"
|
||||
publish = ["xinu"]
|
||||
version = "0.17.44"
|
||||
version = "0.17.45"
|
||||
repository = "https://git.z.xinu.tv/wathiede/letterbox"
|
||||
|
||||
[profile.dev]
|
||||
|
||||
@ -32,8 +32,8 @@ futures = "0.3.31"
|
||||
headers = "0.4.0"
|
||||
html-escape = "0.2.13"
|
||||
ical = "0.11"
|
||||
letterbox-notmuch = { path = "../notmuch", version = "0.17.44", registry = "xinu" }
|
||||
letterbox-shared = { path = "../shared", version = "0.17.44", registry = "xinu" }
|
||||
letterbox-notmuch = { path = "../notmuch", version = "0.17.45", registry = "xinu" }
|
||||
letterbox-shared = { path = "../shared", version = "0.17.45", registry = "xinu" }
|
||||
linkify = "0.10.0"
|
||||
lol_html = "2.3.0"
|
||||
mailparse = "0.16.1"
|
||||
|
||||
@ -17,9 +17,11 @@ use crate::{
|
||||
const APPLICATION_GZIP: &'static str = "application/gzip";
|
||||
|
||||
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_PJPEG: &'static str = "image/pjpeg";
|
||||
const IMAGE_PNG: &'static str = "image/png";
|
||||
const MESSAGE_DELIVERY_STATUS: &'static str = "message/delivery-status";
|
||||
const MESSAGE_RFC822: &'static str = "message/rfc822";
|
||||
const MULTIPART_ALTERNATIVE: &'static str = "multipart/alternative";
|
||||
const MULTIPART_MIXED: &'static str = "multipart/mixed";
|
||||
@ -641,115 +643,186 @@ pub fn extract_gzip(m: &ParsedMail) -> Result<(Body, Option<String>), ServerErro
|
||||
Ok((extract_unhandled(m)?, None))
|
||||
}
|
||||
|
||||
pub fn extract_report(m: &ParsedMail, _part_addr: &mut Vec<String>) -> Result<Body, ServerError> {
|
||||
let mut html_part = None;
|
||||
let mut tlsrpt_part = None;
|
||||
pub fn extract_report(m: &ParsedMail, part_addr: &mut Vec<String>) -> Result<Body, ServerError> {
|
||||
let mut parts = Vec::new();
|
||||
|
||||
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() {
|
||||
TEXT_HTML => html_part = Some(sp.get_body()?),
|
||||
"application/tlsrpt+gzip" => tlsrpt_part = Some(sp.get_body_raw()?),
|
||||
_ => {} // Ignore other parts for now
|
||||
}
|
||||
}
|
||||
|
||||
let tlsrpt_summary_html = if let Some(gz_bytes) = tlsrpt_part {
|
||||
let mut decoder = flate2::read::GzDecoder::new(&gz_bytes[..]);
|
||||
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) {
|
||||
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(),
|
||||
APPLICATION_TLSRPT_GZIP => {
|
||||
let gz_bytes = sp.get_body_raw()?;
|
||||
let mut decoder = flate2::read::GzDecoder::new(&gz_bytes[..]);
|
||||
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) {
|
||||
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,
|
||||
},
|
||||
summary: policy.summary,
|
||||
failure_details: policy
|
||||
.failure_details
|
||||
.unwrap_or_else(|| Vec::new())
|
||||
contact_info: tlsrpt
|
||||
.contact_info
|
||||
.unwrap_or_else(|| "".to_string()),
|
||||
report_id: tlsrpt.report_id,
|
||||
policies: tlsrpt
|
||||
.policies
|
||||
.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()),
|
||||
.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(),
|
||||
},
|
||||
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 {
|
||||
report: &formatted_tlsrpt,
|
||||
};
|
||||
template.render().unwrap_or_else(|e| format!("<div class=\"tlsrpt-error\">Failed to render TLS report template: {}</div>", e))
|
||||
};
|
||||
let template = TlsReportTemplate {
|
||||
report: &formatted_tlsrpt,
|
||||
};
|
||||
let html = template.render().unwrap_or_else(|e| format!("<div class=\"tlsrpt-error\">Failed to render TLS report template: {}</div>", e));
|
||||
parts.push(Body::html(html));
|
||||
}
|
||||
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()
|
||||
};
|
||||
|
||||
let final_html = if let Some(html) = html_part {
|
||||
format!("{}<hr>{} ", html, tlsrpt_summary_html)
|
||||
} else {
|
||||
tlsrpt_summary_html
|
||||
};
|
||||
part_addr.pop();
|
||||
}
|
||||
|
||||
Ok(Body::html(final_html))
|
||||
if parts.is_empty() {
|
||||
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> {
|
||||
|
||||
@ -12,7 +12,7 @@ version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
build-info = "0.0.41"
|
||||
letterbox-notmuch = { path = "../notmuch", version = "0.17.44", registry = "xinu" }
|
||||
letterbox-notmuch = { path = "../notmuch", version = "0.17.45", registry = "xinu" }
|
||||
regex = "1.11.1"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
sqlx = "0.8.5"
|
||||
|
||||
@ -33,7 +33,7 @@ wasm-bindgen = "=0.2.100"
|
||||
uuid = { version = "1.16.0", features = [
|
||||
"js",
|
||||
] } # direct dep to set js feature, prevents Rng issues
|
||||
letterbox-shared = { path = "../shared/", version = "0.17.44", registry = "xinu" }
|
||||
letterbox-shared = { path = "../shared/", version = "0.17.45", registry = "xinu" }
|
||||
seed_hooks = { version = "0.4.1", registry = "xinu" }
|
||||
strum_macros = "0.27.1"
|
||||
gloo-console = "0.3.0"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user