melib/mbox: add MboxMetadata type and write support
parent
3fa9e355c2
commit
cf9457882a
|
@ -101,6 +101,8 @@
|
||||||
//! mbox_1,
|
//! mbox_1,
|
||||||
//! None, // Envelope From
|
//! None, // Envelope From
|
||||||
//! Some(melib::datetime::now()), // Delivered date
|
//! Some(melib::datetime::now()), // Delivered date
|
||||||
|
//! Default::default(), // Flags and tags
|
||||||
|
//! MboxMetadata::None,
|
||||||
//! true,
|
//! true,
|
||||||
//! false,
|
//! false,
|
||||||
//! )?;
|
//! )?;
|
||||||
|
@ -109,6 +111,8 @@
|
||||||
//! mbox_2,
|
//! mbox_2,
|
||||||
//! None,
|
//! None,
|
||||||
//! Some(melib::datetime::now()),
|
//! Some(melib::datetime::now()),
|
||||||
|
//! Default::default(), // Flags and tags
|
||||||
|
//! MboxMetadata::None,
|
||||||
//! false,
|
//! false,
|
||||||
//! 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"
|
/// Choose between "mboxo", "mboxrd", "mboxcl", "mboxcl2". For new mailboxes, prefer "mboxcl2"
|
||||||
/// which does not alter the mail body.
|
/// which does not alter the mail body.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
|
|
@ -28,9 +28,14 @@ impl MboxFormat {
|
||||||
input: &[u8],
|
input: &[u8],
|
||||||
envelope_from: Option<&Address>,
|
envelope_from: Option<&Address>,
|
||||||
delivery_date: Option<crate::UnixTimestamp>,
|
delivery_date: Option<crate::UnixTimestamp>,
|
||||||
|
(flags, tags): (Flag, Vec<&str>),
|
||||||
|
metadata_format: MboxMetadata,
|
||||||
is_empty: bool,
|
is_empty: bool,
|
||||||
crlf: bool,
|
crlf: bool,
|
||||||
) -> Result<()> {
|
) -> 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"[..] };
|
let line_ending: &'static [u8] = if crlf { &b"\r\n"[..] } else { &b"\n"[..] };
|
||||||
if !is_empty {
|
if !is_empty {
|
||||||
writer.write_all(line_ending)?;
|
writer.write_all(line_ending)?;
|
||||||
|
@ -54,12 +59,60 @@ impl MboxFormat {
|
||||||
)?;
|
)?;
|
||||||
writer.write_all(line_ending)?;
|
writer.write_all(line_ending)?;
|
||||||
let (mut headers, body) = parser::mail(input)?;
|
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 {
|
match self {
|
||||||
MboxFormat::MboxO | MboxFormat::MboxRd => Err(MeliError::new("Unimplemented.")),
|
MboxFormat::MboxO | MboxFormat::MboxRd => Err(MeliError::new("Unimplemented.")),
|
||||||
MboxFormat::MboxCl => {
|
MboxFormat::MboxCl => {
|
||||||
headers.retain(|(header_name, _)| {
|
|
||||||
!header_name.eq_ignore_ascii_case(b"Content-Length")
|
|
||||||
});
|
|
||||||
let len = (body.len()
|
let len = (body.len()
|
||||||
+ body
|
+ body
|
||||||
.windows(b"\nFrom ".len())
|
.windows(b"\nFrom ".len())
|
||||||
|
@ -76,6 +129,7 @@ impl MboxFormat {
|
||||||
writer.write_all(v)?;
|
writer.write_all(v)?;
|
||||||
writer.write_all(line_ending)?;
|
writer.write_all(line_ending)?;
|
||||||
}
|
}
|
||||||
|
write_metadata_fn(writer)?;
|
||||||
writer.write_all(line_ending)?;
|
writer.write_all(line_ending)?;
|
||||||
|
|
||||||
if body.starts_with(b"From ") {
|
if body.starts_with(b"From ") {
|
||||||
|
@ -90,9 +144,6 @@ impl MboxFormat {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
MboxFormat::MboxCl2 => {
|
MboxFormat::MboxCl2 => {
|
||||||
headers.retain(|(header_name, _)| {
|
|
||||||
!header_name.eq_ignore_ascii_case(b"Content-Length")
|
|
||||||
});
|
|
||||||
let len = body.len().to_string();
|
let len = body.len().to_string();
|
||||||
for (h, v) in headers
|
for (h, v) in headers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -103,6 +154,7 @@ impl MboxFormat {
|
||||||
writer.write_all(v)?;
|
writer.write_all(v)?;
|
||||||
writer.write_all(line_ending)?;
|
writer.write_all(line_ending)?;
|
||||||
}
|
}
|
||||||
|
write_metadata_fn(writer)?;
|
||||||
writer.write_all(line_ending)?;
|
writer.write_all(line_ending)?;
|
||||||
writer.write_all(body)?;
|
writer.write_all(body)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -359,6 +359,7 @@ pub trait MailListingTrait: ListingTrait {
|
||||||
let fut: Pin<Box<dyn Future<Output = Result<()>> + Send + 'static>> =
|
let fut: Pin<Box<dyn Future<Output = Result<()>> + Send + 'static>> =
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let cl = async move {
|
let cl = async move {
|
||||||
|
use melib::backends::mbox::MboxMetadata;
|
||||||
let bytes: Vec<Vec<u8>> = try_join_all(futures?).await?;
|
let bytes: Vec<Vec<u8>> = try_join_all(futures?).await?;
|
||||||
let envs: Vec<_> = envs_to_set
|
let envs: Vec<_> = envs_to_set
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -366,22 +367,37 @@ pub trait MailListingTrait: ListingTrait {
|
||||||
.collect();
|
.collect();
|
||||||
let mut file = std::io::BufWriter::new(std::fs::File::create(&path_)?);
|
let mut file = std::io::BufWriter::new(std::fs::File::create(&path_)?);
|
||||||
let mut iter = envs.iter().zip(bytes.into_iter());
|
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() {
|
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(
|
format.append(
|
||||||
&mut file,
|
&mut file,
|
||||||
bytes.as_slice(),
|
bytes.as_slice(),
|
||||||
env.from().get(0),
|
env.from().get(0),
|
||||||
Some(env.date()),
|
Some(env.date()),
|
||||||
|
(env.flags(), tags),
|
||||||
|
MboxMetadata::CClient,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
for (env, bytes) in iter {
|
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(
|
format.append(
|
||||||
&mut file,
|
&mut file,
|
||||||
bytes.as_slice(),
|
bytes.as_slice(),
|
||||||
env.from().get(0),
|
env.from().get(0),
|
||||||
Some(env.date()),
|
Some(env.date()),
|
||||||
|
(env.flags(), tags),
|
||||||
|
MboxMetadata::CClient,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
)?;
|
)?;
|
||||||
|
|
Loading…
Reference in New Issue