Skip to main content

hypermail/
haof.rs

1use std::path::Path;
2
3use crate::config::Config;
4use crate::date::secs_to_iso;
5use crate::error::Result;
6use crate::headers::decode_mime_words;
7use crate::structs::EmailStore;
8
9pub fn write_haof(store: &EmailStore, config: &Config) -> Result<String> {
10    let dir = config.dir.as_deref().unwrap_or(".");
11    let haof_path = Path::new(dir).join("haof.xml");
12
13    let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
14    xml.push_str("<haof version=\"1.0\">\n");
15    xml.push_str(&format!(
16        "  <title>{}</title>\n",
17        escape_xml(config.label.as_deref().unwrap_or("Archive"))
18    ));
19    xml.push_str("  <generator>hypermail-rs</generator>\n");
20    xml.push_str(&format!("  <count>{}</count>\n", store.emails.len()));
21
22    for email in &store.emails {
23        xml.push_str("  <message>\n");
24        xml.push_str(&format!("    <id>{}</id>\n", escape_xml(&email.msgnum.to_string())));
25        xml.push_str(&format!("    <date>{}</date>\n", secs_to_iso(email.date)));
26        xml.push_str(&format!(
27            "    <subject>{}</subject>\n",
28            escape_xml(&decode_mime_words(email.subject.as_deref().unwrap_or("(no subject)")))
29        ));
30        xml.push_str(&format!(
31            "    <from>{}</from>\n",
32            escape_xml(email.name.as_deref().unwrap_or("Unknown"))
33        ));
34        if let Some(ref addr) = email.email_addr {
35            xml.push_str(&format!("    <email>{}</email>\n", escape_xml(addr)));
36        }
37        if let Some(ref msgid) = email.msgid {
38            xml.push_str(&format!("    <msgid>{}</msgid>\n", escape_xml(msgid)));
39        }
40        if let Some(ref inreplyto) = email.inreplyto {
41            xml.push_str(&format!("    <inreplyto>{}</inreplyto>\n", escape_xml(inreplyto)));
42        }
43        xml.push_str("  </message>\n");
44    }
45
46    xml.push_str("</haof>\n");
47
48    std::fs::write(&haof_path, &xml)?;
49    Ok(xml)
50}
51
52fn escape_xml(s: &str) -> String {
53    let mut result = String::with_capacity(s.len());
54    for c in s.chars() {
55        match c {
56            '&' => result.push_str("&amp;"),
57            '<' => result.push_str("&lt;"),
58            '>' => result.push_str("&gt;"),
59            '"' => result.push_str("&quot;"),
60            '\'' => result.push_str("&apos;"),
61            c => result.push(c),
62        }
63    }
64    result
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::config::Config;
71    use crate::message::EmailInfo;
72
73    #[test]
74    fn test_escape_xml() {
75        assert_eq!(escape_xml("<test&>"), "&lt;test&amp;&gt;");
76        assert_eq!(escape_xml("plain"), "plain");
77    }
78
79    #[test]
80    fn test_write_haof_basic() {
81        let mut store = EmailStore::new();
82        let email = EmailInfo {
83            msgnum: 1,
84            name: Some("Alice".to_string()),
85            email_addr: Some("alice@example.com".to_string()),
86            subject: Some("Hello".to_string()),
87            date: 1000000,
88            msgid: Some("<abc@e.com>".to_string()),
89            ..Default::default()
90        };
91        store.add_email(email);
92        let config = Config::default();
93        let xml = write_haof(&store, &config).unwrap();
94        assert!(xml.contains("<subject>Hello</subject>"));
95        assert!(xml.contains("<name>Alice</name>") || xml.contains("<from>Alice</from>"));
96        assert!(xml.contains("<count>1</count>"));
97    }
98}