diff --git a/melib/src/backends/mbox.rs b/melib/src/backends/mbox.rs index 16385224..fbc3cbb4 100644 --- a/melib/src/backends/mbox.rs +++ b/melib/src/backends/mbox.rs @@ -101,6 +101,8 @@ //! mbox_1, //! None, // Envelope From //! Some(melib::datetime::now()), // Delivered date +//! Default::default(), // Flags and tags +//! MboxMetadata::None, //! true, //! false, //! )?; @@ -109,6 +111,8 @@ //! mbox_2, //! None, //! Some(melib::datetime::now()), +//! Default::default(), // Flags and tags +//! MboxMetadata::None, //! false, //! false, //! )?; @@ -366,6 +370,20 @@ impl BackendOp for MboxOp { } } +#[derive(Debug, Clone, Copy)] +pub enum MboxMetadata { + /// Dovecot uses C-Client (ie. UW-IMAP, Pine) compatible headers in mbox messages to store me + /// - X-IMAPbase: Contains UIDVALIDITY, last used UID and list of used keywords + /// - X-IMAP: Same as X-IMAPbase but also specifies that the message is a “pseudo message” + /// - X-UID: Message’s allocated UID + /// - Status: R (Seen) and O (non-Recent) flags + /// - X-Status: A (Answered), F (Flagged), T (Draft) and D (Deleted) flags + /// - X-Keywords: Message’s keywords + /// - Content-Length: Length of the message body in bytes + CClient, + None, +} + /// Choose between "mboxo", "mboxrd", "mboxcl", "mboxcl2". For new mailboxes, prefer "mboxcl2" /// which does not alter the mail body. #[derive(Debug, Clone, Copy)] diff --git a/melib/src/backends/mbox/write.rs b/melib/src/backends/mbox/write.rs index 772c70df..e7edeab3 100644 --- a/melib/src/backends/mbox/write.rs +++ b/melib/src/backends/mbox/write.rs @@ -28,9 +28,14 @@ impl MboxFormat { input: &[u8], envelope_from: Option<&Address>, delivery_date: Option, + (flags, tags): (Flag, Vec<&str>), + metadata_format: MboxMetadata, is_empty: bool, crlf: bool, ) -> Result<()> { + if tags.iter().any(|t| t.contains(' ')) { + return Err(MeliError::new("mbox tags/keywords can't contain spaces")); + } let line_ending: &'static [u8] = if crlf { &b"\r\n"[..] } else { &b"\n"[..] }; if !is_empty { writer.write_all(line_ending)?; @@ -54,12 +59,60 @@ impl MboxFormat { )?; writer.write_all(line_ending)?; let (mut headers, body) = parser::mail(input)?; + headers.retain(|(header_name, _)| { + !header_name.eq_ignore_ascii_case(b"Status") + && !header_name.eq_ignore_ascii_case(b"X-Status") + && !header_name.eq_ignore_ascii_case(b"X-Keywords") + && !header_name.eq_ignore_ascii_case(b"Content-Length") + }); + let write_metadata_fn = |writer: &mut dyn std::io::Write| match metadata_format { + MboxMetadata::CClient => { + for (h, v) in { + if flags.is_seen() { + Some((&b"Status"[..], "R".into())) + } else { + None + } + .into_iter() + .chain( + if !flags.is_flagged() + && !flags.is_replied() + && !flags.is_draft() + && !flags.is_trashed() + { + None + } else { + Some(( + &b"X-Status"[..], + format!( + "{flagged}{replied}{draft}{trashed}", + flagged = if flags.is_flagged() { "F" } else { "" }, + replied = if flags.is_replied() { "A" } else { "" }, + draft = if flags.is_draft() { "T" } else { "" }, + trashed = if flags.is_trashed() { "D" } else { "" } + ), + )) + }, + ) + .chain(if tags.is_empty() { + None + } else { + Some((&b"X-Keywords"[..], tags.as_slice().join(" "))) + }) + } { + writer.write_all(h)?; + writer.write_all(&b": "[..])?; + writer.write_all(v.as_bytes())?; + writer.write_all(line_ending)?; + } + Ok::<(), MeliError>(()) + } + MboxMetadata::None => Ok(()), + }; + match self { MboxFormat::MboxO | MboxFormat::MboxRd => Err(MeliError::new("Unimplemented.")), MboxFormat::MboxCl => { - headers.retain(|(header_name, _)| { - !header_name.eq_ignore_ascii_case(b"Content-Length") - }); let len = (body.len() + body .windows(b"\nFrom ".len()) @@ -76,6 +129,7 @@ impl MboxFormat { writer.write_all(v)?; writer.write_all(line_ending)?; } + write_metadata_fn(writer)?; writer.write_all(line_ending)?; if body.starts_with(b"From ") { @@ -90,9 +144,6 @@ impl MboxFormat { Ok(()) } MboxFormat::MboxCl2 => { - headers.retain(|(header_name, _)| { - !header_name.eq_ignore_ascii_case(b"Content-Length") - }); let len = body.len().to_string(); for (h, v) in headers .into_iter() @@ -103,6 +154,7 @@ impl MboxFormat { writer.write_all(v)?; writer.write_all(line_ending)?; } + write_metadata_fn(writer)?; writer.write_all(line_ending)?; writer.write_all(body)?; Ok(()) diff --git a/src/components/mail/listing.rs b/src/components/mail/listing.rs index 2a49b218..7229dab1 100644 --- a/src/components/mail/listing.rs +++ b/src/components/mail/listing.rs @@ -359,6 +359,7 @@ pub trait MailListingTrait: ListingTrait { let fut: Pin> + Send + 'static>> = Box::pin(async move { let cl = async move { + use melib::backends::mbox::MboxMetadata; let bytes: Vec> = try_join_all(futures?).await?; let envs: Vec<_> = envs_to_set .iter() @@ -366,22 +367,37 @@ pub trait MailListingTrait: ListingTrait { .collect(); let mut file = std::io::BufWriter::new(std::fs::File::create(&path_)?); let mut iter = envs.iter().zip(bytes.into_iter()); + let tags_lck = collection.tag_index.read().unwrap(); if let Some((env, ref bytes)) = iter.next() { + let tags: Vec<&str> = env + .labels() + .iter() + .filter_map(|h| tags_lck.get(h).map(|s| s.as_str())) + .collect(); format.append( &mut file, bytes.as_slice(), env.from().get(0), Some(env.date()), + (env.flags(), tags), + MboxMetadata::CClient, true, false, )?; } for (env, bytes) in iter { + let tags: Vec<&str> = env + .labels() + .iter() + .filter_map(|h| tags_lck.get(h).map(|s| s.as_str())) + .collect(); format.append( &mut file, bytes.as_slice(), env.from().get(0), Some(env.date()), + (env.flags(), tags), + MboxMetadata::CClient, false, false, )?;