Author: Kevin Schoon [me@kevinschoon.com]
Committer: Manos Pitsidianakis [manos@pitsidianak.is] Tue, 09 Jan 2024 12:52:04 +0000
Hash: 0007bb30c50334657d577b903619c8d7cae91293
Timestamp: Tue, 09 Jan 2024 12:52:04 +0000 (8 months ago)

+126 -10 +/-2 browse
add ability to export lists in mbox format
add ability to export lists in mbox format

This adds the ability to export mailing lists, threads, or individual messages
in the mboxcl2 format directly from the database.
1diff --git a/core/Cargo.toml b/core/Cargo.toml
2index 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
15index 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 }