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("&"),
57 '<' => result.push_str("<"),
58 '>' => result.push_str(">"),
59 '"' => result.push_str("""),
60 '\'' => result.push_str("'"),
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&>"), "<test&>");
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}