Skip to main content

hypermail/
gdbm.rs

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            // max_msgnum
85            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            // Parse value fields (null-separated) - regular email entry
96            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}