1use ratatui::backend::TestBackend;
2use ratatui::Terminal;
3use regex::Regex;
4use std::path::{Path, PathBuf};
5use std::sync::OnceLock;
6
7pub const STANDARDS_FIXTURES: &[&str] = &[
8 "alternative-html-first.eml",
9 "duplicate-singletons.eml",
10 "encoded-words.eml",
11 "folded-flowed.eml",
12 "malformed-minimal.eml",
13 "missing-content-type.eml",
14 "missing-message-id.eml",
15 "multipart-calendar.eml",
16 "nested-multipart.eml",
17 "quoted-local-group.eml",
18 "rfc2231-attachment.eml",
19 "unsubscribe-oneclick.eml",
20];
21
22pub fn standards_fixture_names() -> &'static [&'static str] {
23 STANDARDS_FIXTURES
24}
25
26pub fn fixture_stem(name: &str) -> &str {
27 name.strip_suffix(".eml").unwrap_or(name)
28}
29
30pub fn standards_fixture_path(name: &str) -> PathBuf {
31 Path::new(env!("CARGO_MANIFEST_DIR"))
32 .join("fixtures")
33 .join("standards")
34 .join(name)
35}
36
37pub fn standards_fixture_bytes(name: &str) -> Vec<u8> {
38 std::fs::read(standards_fixture_path(name)).unwrap()
39}
40
41pub fn standards_fixture_string(name: &str) -> String {
42 std::fs::read_to_string(standards_fixture_path(name)).unwrap()
43}
44
45pub fn redact_rfc822(raw: &str) -> String {
46 static MESSAGE_ID_RE: OnceLock<Regex> = OnceLock::new();
47 static DATE_RE: OnceLock<Regex> = OnceLock::new();
48 let message_id_re =
49 MESSAGE_ID_RE.get_or_init(|| Regex::new(r"(?m)^Message-ID:\s*<[^>\r\n]+>\r?$").unwrap());
50 let date_re = DATE_RE.get_or_init(|| Regex::new(r"(?m)^Date:\s*[^\r\n]+\r?$").unwrap());
51
52 let mut redacted = message_id_re
53 .replace_all(raw, "Message-ID: <redacted@example.com>")
54 .to_string();
55 redacted = date_re
56 .replace_all(&redacted, "Date: Fri, 20 Mar 2026 00:00:00 +0000")
57 .to_string();
58
59 let boundary_re = Regex::new(r#"boundary="([^"]+)""#).unwrap();
60 let boundaries = boundary_re
61 .captures_iter(raw)
62 .enumerate()
63 .map(|(index, caps)| {
64 (
65 caps[1].to_string(),
66 format!("boundary=\"BOUNDARY_{index}\""),
67 )
68 })
69 .collect::<Vec<_>>();
70
71 for (index, (boundary, replacement)) in boundaries.iter().enumerate() {
72 redacted = redacted.replace(&format!("boundary=\"{boundary}\""), replacement);
73 redacted = redacted.replace(boundary, &format!("BOUNDARY_{index}"));
74 }
75
76 redacted
77}
78
79pub fn render_to_string<F>(width: u16, height: u16, draw: F) -> String
80where
81 F: FnOnce(&mut ratatui::Frame<'_>),
82{
83 let backend = TestBackend::new(width, height);
84 let mut terminal = Terminal::new(backend).unwrap();
85 terminal.draw(draw).unwrap();
86 format!("{}", terminal.backend())
87}