use super::*; use crate::builtin; use crate::filter::FilterArgs; use crate::format; use crate::mailbox; use crate::md_thread; use crate::redact; use crate::secrets; use crate::theme; use crate::thread::ThreadBuilder; use anyhow::{anyhow, Result}; use std::path::{Path, PathBuf}; // --------------------------------------------------------------------------- // thread // --------------------------------------------------------------------------- /// Reconstructs conversation threads from request message histories. /// /// Each record's `capture.requestBody.messages` echoes its full parent path, so /// the trie over message-content hashes recovers the branching structure. #[derive(Clone, Copy, PartialEq, Eq, clap::ValueEnum)] pub enum ThreadFormat { Json, Html, Mbox, Maildir, Md, } #[derive(clap::Args)] pub struct ThreadArgs { #[arg(required = true)] pub files: Vec, /// Write JSON/HTML/MBOX/Maildir to this path instead of stdout (`-` for stdout). #[arg(short, long)] pub output: Option, /// Output format: json (default), html (built-in renderer), mbox, maildir, md. #[arg(long, value_enum, default_value = "json")] pub format: ThreadFormat, /// Body rendering for mbox/maildir: plain, html (multipart/alternative), html-only. #[arg(long, value_name = "MODE", default_value = "html")] pub body: String, /// Dark mode for the built-in HTML renderer. #[arg(long)] pub dark: bool, /// Render HTML using an Adium `.AdiumMessageStyle` bundle instead of the /// built-in renderer. #[arg(long, value_name = "BUNDLE")] pub theme: Option, /// Adium style variant to apply (e.g. "Dark"). #[arg(long)] pub variant: Option, #[command(flatten)] pub redact: redact::RedactArgs, /// Suppress the un-redacted-secrets warning for HTML/Markdown output. /// The warning fires when output is emitted without `--redact*` and a /// likely secret (API key, bearer token, JWT, …) is detected. This flag /// acknowledges the risk; it does not disable detection for other paths. #[arg(long = "i-know", default_value_t = false)] pub i_know: bool, #[command(flatten)] pub filter: FilterArgs, } pub fn cmd_thread(args: &ThreadArgs) -> Result<()> { let filter = args.filter.build()?; let redactor = args.redact.build()?; let do_redact = redactor.is_active(); // Secrets-safety net: when emitting human-readable output (HTML, Markdown) // without `--redact*`, scan the rendered bytes for likely-secret patterns // and warn on stderr. Skipped when redaction was applied (already scrubbed // for valid-UTF-8 bodies) or when the user passed `--i-know`. mbox/maildir // are not scanned here (their renderers stream to files/dirs without a // single in-memory buffer); the warning is a best-effort heuristic anyway. let warn_secrets = |bytes: &[u8]| { if do_redact || args.i_know { return; } if let Ok(text) = std::str::from_utf8(bytes) { if let Some(w) = secrets::scan(text).warning() { eprintln!("{w}"); } } }; let mut builder = ThreadBuilder::new(); let mut total = 0u64; let mut with_messages = 0u64; for_each_matching_record(&args.files, &filter, |mut rec| { if do_redact { // Scrub text and valid-UTF-8 byte bodies (e.g. raw HTTP) before // the thread builder extracts message content / metadata. format::redact_strings(&mut rec, &|s| redactor.apply(s)); } total += 1; if builder.add_record(&rec)? { with_messages += 1; } Ok(()) })?; let j = builder.to_json(total, with_messages); // Built-in long-form HTML renderer (no external theme bundle). if args.format == ThreadFormat::Html && args.theme.is_none() { let html = builtin::render_html(&j, args.dark)?; warn_secrets(html.as_bytes()); write_output(args.output.as_ref(), html.as_bytes(), "html")?; eprintln!( "{}; built-in HTML{}", thread_summary(total, with_messages, &j), if args.dark { " (dark)" } else { "" } ); return Ok(()); } // HTML (themed) path: render the forest through an Adium message style. if let Some(theme_path) = &args.theme { let html = theme::render_forest(&j, theme_path, args.variant.as_deref())?; warn_secrets(html.as_bytes()); write_output(args.output.as_ref(), html.as_bytes(), "html")?; eprintln!( "{}; themed -> {}", thread_summary(total, with_messages, &j), theme_path.display() ); return Ok(()); } // MBOX / Maildir export: each trie node becomes one email, threaded by // Message-ID / In-Reply-To. Selected by --format mbox|maildir. if args.format == ThreadFormat::Mbox || args.format == ThreadFormat::Maildir { let mode = mailbox::BodyMode::parse(&args.body)?; if args.format == ThreadFormat::Mbox { // mbox may write to stdout (`-` or no -o) or a file. let n = match args.output.as_ref() { Some(p) if p.as_path() != Path::new("-") => mailbox::write_mbox(&j, mode, p)?, _ => { let mut out = std::io::stdout(); mailbox::write_mbox_to(&mut out, &j, mode)? } }; eprintln!( "{} record(s) ({} with messages) -> {} message(s) [{} body] (mbox)", total, with_messages, n, args.body, ); return Ok(()); } // maildir requires a real directory path. let out = args .output .as_ref() .filter(|p| p.as_path() != Path::new("-")) .ok_or_else(|| anyhow!("--format maildir requires -o DIR"))?; let n = mailbox::write_maildir(&j, mode, out)?; eprintln!( "{} record(s) ({} with messages) -> {} message(s) [{} body] -> {} (maildir)", total, with_messages, n, args.body, out.display() ); return Ok(()); } if args.format == ThreadFormat::Md { let md = md_thread::render_md(&j); warn_secrets(md.as_bytes()); write_output(args.output.as_ref(), md.as_bytes(), "md")?; eprintln!("{}; markdown", thread_summary(total, with_messages, &j)); return Ok(()); } let pretty = serde_json::to_string_pretty(&j)?; let bytes = pretty.into_bytes(); write_output(args.output.as_ref(), &bytes, "json")?; eprintln!("{}", thread_summary(total, with_messages, &j)); Ok(()) } /// Common summary line for `cmd_thread`: records scanned, how many carried /// messages, and the reconstructed thread / branch-point counts. Used by the /// built-in HTML, themed, and JSON output paths (each appends its own suffix). fn thread_summary(total: u64, with_messages: u64, j: &serde_json::Value) -> String { format!( "{} record(s) ({} with messages) -> {} thread(s), {} branch point(s)", total, with_messages, j["root_count"], j["branch_count"], ) }