1use crate::config::Config;
2use crate::date::secs_to_iso;
3use crate::message::EmailInfo;
4use crate::structs::EmailStore;
5use std::fs;
6use std::path::PathBuf;
7
8pub fn gdbm_index_name(config: &Config) -> PathBuf {
9 let dir = config.dir.as_deref().unwrap_or(".");
10 PathBuf::from(dir).join(".hm2index")
11}
12
13pub fn togdbm(store: &EmailStore, config: &Config) -> std::io::Result<()> {
14 let path = gdbm_index_name(config);
15 let mut data = Vec::new();
16
17 for email in &store.emails {
18 let mut entry = Vec::new();
19 push_field(&mut entry, email.from_date_str.as_deref().unwrap_or(""));
20 push_field(&mut entry, email.date_str.as_deref().unwrap_or(""));
21 push_field(&mut entry, email.name.as_deref().unwrap_or(""));
22 push_field(&mut entry, email.email_addr.as_deref().unwrap_or(""));
23 push_field(&mut entry, email.subject.as_deref().unwrap_or(""));
24 push_field(&mut entry, email.msgid.as_deref().unwrap_or(""));
25 push_field(&mut entry, email.inreplyto.as_deref().unwrap_or(""));
26 push_field(&mut entry, email.charset.as_deref().unwrap_or(""));
27 push_field(&mut entry, &secs_to_iso(email.from_date));
28 push_field(&mut entry, &secs_to_iso(email.date));
29 push_field(&mut entry, &email.exp_time.to_string());
30 push_field(&mut entry, &email.is_deleted.to_string());
31
32 let msgnum_bytes = email.msgnum.to_le_bytes();
33 data.extend_from_slice(&msgnum_bytes);
34 data.extend_from_slice(&(entry.len() as u32).to_le_bytes());
35 data.extend_from_slice(&entry);
36 }
37
38 let max_msgnum = store.max_msgnum;
39 let max_key = (-1i32).to_le_bytes();
40 let max_val = max_msgnum.to_string();
41 data.extend_from_slice(&max_key);
42 data.extend_from_slice(&(max_val.len() as u32).to_le_bytes());
43 data.extend_from_slice(max_val.as_bytes());
44
45 let dl_key = "delete_level\0";
46 let dl_val = config.delete_level.to_string();
47 data.extend_from_slice(dl_key.as_bytes());
48 data.extend_from_slice(&(dl_val.len() as u32).to_le_bytes());
49 data.extend_from_slice(dl_val.as_bytes());
50
51 fs::write(&path, &data)
52}
53
54fn push_field(data: &mut Vec<u8>, s: &str) {
55 data.extend_from_slice(s.as_bytes());
56 data.push(0);
57}
58
59pub fn load_from_gdbm(store: &mut EmailStore, config: &Config) -> std::io::Result<i32> {
60 let path = gdbm_index_name(config);
61 let data = fs::read(&path)?;
62 let mut pos = 0;
63 let mut count = 0;
64
65 while pos < data.len() {
66 if pos + 4 > data.len() {
67 break;
68 }
69 let key_int = i32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
70 pos += 4;
71
72 if pos + 4 > data.len() {
73 break;
74 }
75 let val_len =
76 u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
77 pos += 4;
78
79 if pos + val_len > data.len() {
80 break;
81 }
82
83 if key_int == -1 {
84 if let Ok(s) = String::from_utf8(data[pos..pos + val_len].to_vec()) {
86 if let Ok(n) = s.trim().parse::<i32>() {
87 store.max_msgnum = n;
88 }
89 }
90 pos += val_len;
91 continue;
92 }
93
94 if key_int >= 0 {
95 let fields: Vec<&[u8]> = data[pos..pos + val_len].split(|&b| b == 0).collect();
97 let get_field = |idx: usize| -> Option<String> {
98 fields.get(idx).and_then(|f| {
99 if f.is_empty() {
100 None
101 } else {
102 String::from_utf8(f.to_vec()).ok()
103 }
104 })
105 };
106
107 let email = EmailInfo {
108 msgnum: key_int,
109 from_date_str: get_field(0),
110 date_str: get_field(1),
111 name: get_field(2),
112 email_addr: get_field(3),
113 subject: get_field(4),
114 msgid: get_field(5),
115 inreplyto: get_field(6),
116 charset: get_field(7),
117 from_date: get_field(8).and_then(|s| s.parse().ok()).unwrap_or(0),
118 date: get_field(9).and_then(|s| s.parse().ok()).unwrap_or(0),
119 exp_time: get_field(10).and_then(|s| s.parse().ok()).unwrap_or(0),
120 is_deleted: get_field(11).and_then(|s| s.parse().ok()).unwrap_or(0),
121 ..Default::default()
122 };
123
124 if email.msgid.is_some() {
125 let idx = store.add_email(email);
126 store.insert_into_date_list(idx);
127 store.insert_into_subject_list(idx);
128 store.insert_into_author_list(idx);
129 count += 1;
130 }
131 }
132
133 pos += val_len;
134 }
135
136 Ok(count)
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::config::Config;
143 use crate::message::EmailInfo;
144
145 fn make_store() -> EmailStore {
146 let mut store = EmailStore::new();
147 let e1 = EmailInfo {
148 msgnum: 1,
149 msgid: Some("<a@b>".to_string()),
150 name: Some("Alice".to_string()),
151 subject: Some("Hello".to_string()),
152 email_addr: Some("alice@e.com".to_string()),
153 date_str: Some("Mon, 1 Jan 2024 12:00:00 +0000".to_string()),
154 from_date_str: Some("Mon, 1 Jan 2024 12:00:00 +0000".to_string()),
155 date: 1704110400,
156 from_date: 1704110400,
157 ..Default::default()
158 };
159 let e2 = EmailInfo {
160 msgnum: 2,
161 msgid: Some("<c@d>".to_string()),
162 name: Some("Bob".to_string()),
163 subject: Some("Re: Hello".to_string()),
164 email_addr: Some("bob@e.com".to_string()),
165 date_str: Some("Tue, 2 Jan 2024 12:00:00 +0000".to_string()),
166 from_date_str: Some("Tue, 2 Jan 2024 12:00:00 +0000".to_string()),
167 date: 1704196800,
168 from_date: 1704196800,
169 ..Default::default()
170 };
171 store.add_email(e1);
172 store.add_email(e2);
173 store
174 }
175
176 #[test]
177 fn test_gdbm_roundtrip() {
178 let store = make_store();
179 let dir = tempfile::tempdir().unwrap();
180 let mut config = Config::default();
181 config.dir = Some(dir.path().to_string_lossy().to_string());
182
183 togdbm(&store, &config).unwrap();
184
185 let mut loaded = EmailStore::new();
186 let count = load_from_gdbm(&mut loaded, &config).unwrap();
187 assert!(count > 0);
188 assert!(loaded.max_msgnum >= 2);
189 }
190
191 #[test]
192 fn test_gdbm_index_name() {
193 let config = Config::default();
194 let path = gdbm_index_name(&config);
195 assert!(path.to_string_lossy().contains(".hm2index"));
196 }
197
198 #[test]
199 fn test_gdbm_msgnum_over_1m() {
200 let mut store = EmailStore::new();
201 let e1 = EmailInfo {
202 msgnum: 2000000,
203 msgid: Some("<big@num>".to_string()),
204 name: Some("Big".to_string()),
205 subject: Some("Large Msgnum".to_string()),
206 email_addr: Some("big@e.com".to_string()),
207 date_str: Some("Mon, 1 Jan 2024 12:00:00 +0000".to_string()),
208 from_date_str: Some("Mon, 1 Jan 2024 12:00:00 +0000".to_string()),
209 date: 1704110400,
210 from_date: 1704110400,
211 ..Default::default()
212 };
213 store.add_email(e1);
214
215 let dir = tempfile::tempdir().unwrap();
216 let mut config = Config::default();
217 config.dir = Some(dir.path().to_string_lossy().to_string());
218
219 togdbm(&store, &config).unwrap();
220
221 let mut loaded = EmailStore::new();
222 let count = load_from_gdbm(&mut loaded, &config).unwrap();
223 assert_eq!(count, 1, "should have loaded exactly 1 email");
224 assert_eq!(loaded.emails[0].msgnum, 2000000, "msgnum > 1M should survive roundtrip");
225 assert_eq!(loaded.emails[0].subject.as_deref(), Some("Large Msgnum"));
226 }
227}