+126 -10 +/-2 browse
1 | diff --git a/core/Cargo.toml b/core/Cargo.toml |
2 | index f7a436c..1f633e8 100644 |
3 | --- a/core/Cargo.toml |
4 | +++ b/core/Cargo.toml |
5 | @@ -18,7 +18,7 @@ anyhow = "1.0.58" |
6 | chrono = { version = "^0.4", features = ["serde", ] } |
7 | jsonschema = { version = "0.17", default-features = false } |
8 | log = "0.4" |
9 | - melib = { default-features = false, features = ["smtp", "unicode-algorithms", "maildir"], git = "https://git.meli-email.org/meli/meli.git", rev = "64e60cb" } |
10 | + melib = { default-features = false, features = ["mbox", "smtp", "unicode-algorithms", "maildir"], git = "https://git.meli-email.org/meli/meli.git", rev = "64e60cb" } |
11 | minijinja = { version = "0.31.0", features = ["source", ] } |
12 | percent-encoding = { version = "^2.1" } |
13 | rusqlite = { version = "^0.30", features = ["bundled", "functions", "trace", "hooks", "serde_json", "array", "chrono", "unlock_notify"] } |
14 | diff --git a/core/src/connection.rs b/core/src/connection.rs |
15 | index 6aabd6f..5f122eb 100644 |
16 | --- a/core/src/connection.rs |
17 | +++ b/core/src/connection.rs |
18 | @@ -674,19 +674,19 @@ impl Connection { |
19 | SELECT * FROM cte_thread WHERE root = ? ORDER BY root, depth;", |
20 | ) |
21 | .unwrap(); |
22 | - let iter = stmt |
23 | - .query_map(rusqlite::params![root], |row| { |
24 | - let parent: String = row.get("parent")?; |
25 | - let root: String = row.get("root")?; |
26 | - let depth: i64 = row.get("depth")?; |
27 | - Ok((parent, root, depth)) |
28 | - })?; |
29 | + let iter = stmt.query_map(rusqlite::params![root], |row| { |
30 | + let parent: String = row.get("parent")?; |
31 | + let root: String = row.get("root")?; |
32 | + let depth: i64 = row.get("depth")?; |
33 | + Ok((parent, root, depth)) |
34 | + })?; |
35 | let mut ret = vec![]; |
36 | for post in iter { |
37 | ret.push(post?); |
38 | } |
39 | - let posts = self.list_posts(list_pk, None).unwrap(); |
40 | - let ret = ret.into_iter() |
41 | + let posts = self.list_posts(list_pk, None)?; |
42 | + let ret = ret |
43 | + .into_iter() |
44 | .filter_map(|(m, _, depth)| { |
45 | posts |
46 | .iter() |
47 | @@ -698,6 +698,59 @@ SELECT * FROM cte_thread WHERE root = ? ORDER BY root, depth;", |
48 | Ok(ret) |
49 | } |
50 | |
51 | + /// Export a list, message, or thread in mbox format |
52 | + pub fn export_mbox( |
53 | + &self, |
54 | + pk: i64, |
55 | + message_id: Option<&str>, |
56 | + as_thread: bool, |
57 | + ) -> Result<Vec<u8>> { |
58 | + let posts: Result<Vec<DbVal<Post>>> = { |
59 | + if let Some(message_id) = message_id { |
60 | + if as_thread { |
61 | + // export a thread |
62 | + let thread = self.list_thread(pk, message_id)?; |
63 | + Ok(thread.iter().map(|item| item.1.clone()).collect()) |
64 | + } else { |
65 | + // export a single message |
66 | + let message = |
67 | + self.list_post_by_message_id(pk, message_id)? |
68 | + .ok_or_else(|| { |
69 | + Error::from(format!("no message with id: {}", message_id)) |
70 | + })?; |
71 | + Ok(vec![message]) |
72 | + } |
73 | + } else { |
74 | + // export the entire mailing list |
75 | + let posts = self.list_posts(pk, None)?; |
76 | + Ok(posts) |
77 | + } |
78 | + }; |
79 | + let mut buf: Vec<u8> = Vec::new(); |
80 | + let mailbox = melib::mbox::MboxFormat::default(); |
81 | + for post in posts? { |
82 | + let envelope_from = if let Some(address) = post.0.envelope_from { |
83 | + let address = melib::Address::try_from(address.as_str())?; |
84 | + Some(address) |
85 | + } else { |
86 | + None |
87 | + }; |
88 | + let envelope = melib::Envelope::from_bytes(&post.0.message, None)?; |
89 | + mailbox.append( |
90 | + &mut buf, |
91 | + &post.0.message.to_vec(), |
92 | + envelope_from.as_ref(), |
93 | + Some(envelope.timestamp), |
94 | + (melib::Flag::PASSED, vec![]), |
95 | + melib::mbox::MboxMetadata::None, |
96 | + false, |
97 | + false, |
98 | + )?; |
99 | + } |
100 | + buf.flush()?; |
101 | + Ok(buf) |
102 | + } |
103 | + |
104 | /// Fetch the owners of a mailing list. |
105 | pub fn list_owners(&self, pk: i64) -> Result<Vec<DbVal<ListOwner>>> { |
106 | let mut stmt = self |
107 | @@ -1262,4 +1315,67 @@ mod tests { |
108 | tx.commit().unwrap(); |
109 | assert_eq!(&db.lists().unwrap(), &[new, new2, new3]); |
110 | } |
111 | + |
112 | + #[test] |
113 | + fn test_mbox_export() { |
114 | + use tempfile::TempDir; |
115 | + |
116 | + use crate::SendMail; |
117 | + |
118 | + let tmp_dir = TempDir::new().unwrap(); |
119 | + let db_path = tmp_dir.path().join("mpot.db"); |
120 | + let data_path = tmp_dir.path().to_path_buf(); |
121 | + let config = Configuration { |
122 | + send_mail: SendMail::ShellCommand("/usr/bin/false".to_string()), |
123 | + db_path, |
124 | + data_path, |
125 | + administrators: vec![], |
126 | + }; |
127 | + let list = MailingList { |
128 | + pk: 0, |
129 | + name: "test".into(), |
130 | + id: "test".into(), |
131 | + description: None, |
132 | + topics: vec![], |
133 | + address: "test@example.com".into(), |
134 | + archive_url: None, |
135 | + }; |
136 | + |
137 | + let test_emails = vec![ |
138 | + r#"From: "User Name" <user@example.com> |
139 | + To: "test" <test@example.com> |
140 | + Subject: Hello World |
141 | + |
142 | + Hello, this is a message. |
143 | + |
144 | + Goodbye! |
145 | + |
146 | + "#, |
147 | + r#"From: "User Name" <user@example.com> |
148 | + To: "test" <test@example.com> |
149 | + Subject: Fuu Bar |
150 | + |
151 | + Baz, |
152 | + |
153 | + Qux! |
154 | + |
155 | + "#, |
156 | + ]; |
157 | + let db = Connection::open_or_create_db(config).unwrap().trusted(); |
158 | + db.create_list(list).unwrap(); |
159 | + for email in test_emails { |
160 | + let envelope = melib::Envelope::from_bytes(email.as_bytes(), None).unwrap(); |
161 | + db.post(&envelope, email.as_bytes(), false).unwrap(); |
162 | + } |
163 | + let mbox = String::from_utf8(db.export_mbox(1, None, false).unwrap()).unwrap(); |
164 | + assert!( |
165 | + mbox.split('\n').fold(0, |accm, line| { |
166 | + if line.starts_with("From MAILER-DAEMON") { |
167 | + accm + 1 |
168 | + } else { |
169 | + accm |
170 | + } |
171 | + }) == 2 |
172 | + ) |
173 | + } |
174 | } |