use std::collections::BTreeMap;
use anyhow::{Result, anyhow};
use chrono::DateTime;
use io_mbox::{
client::MboxClientError,
entry::{
MboxEntry,
append::{MboxEntryAppend, MboxEntryAppendItem, MboxEntryAppendOptions},
copy::{MboxEntryCopy, MboxEntryCopyOptions},
get::{MboxEntryGet, MboxEntryGetError, MboxEntryGetOptions},
r#move::{MboxEntryMove, MboxEntryMoveOptions},
update::{MboxEntryUpdate, MboxEntryUpdateEdit, MboxEntryUpdateOptions},
},
flag::{MboxFlag, MboxFlags},
index::MboxIndex,
path::MboxFsPath,
store::INBOX,
};
use mail_parser::{Address as MailParserAddress, HeaderValue, MessageParser};
use crate::{
email::{
address::Address,
envelope::{Envelope, normalize_message_id, parse_message_ids},
flag::{Flag, FlagOp, IanaFlag},
mailbox::{Mailbox, MailboxRole},
search::{eval, query::SearchEmailsQuery},
},
mbox::{cache::MboxCache, client::MboxClient},
};
impl MboxClient {
pub fn list_mailboxes(&self, with_counts: bool) -> Result<Vec<Mailbox>> {
let mut mailboxes = Vec::new();
for mbox in self.list_mboxes()? {
let name = mbox.name.to_string();
let role = (self.store.inbox.is_some() && name == INBOX).then_some(MailboxRole::Inbox);
let (total, unread) = if with_counts {
let index = self.index(&mbox.path, false)?.0;
let total = index.messages().count() as u64;
let unread = index
.messages()
.filter(|entry| !entry.flags.contains(&MboxFlag::Seen))
.count() as u64;
(Some(total), Some(unread))
} else {
(None, None)
};
mailboxes.push(Mailbox {
id: name.clone(),
name,
role,
total,
unread,
});
}
Ok(mailboxes)
}
pub fn list_envelopes(
&self,
mailbox: &str,
page: Option<u32>,
page_size: Option<u32>,
_with_attachment: bool,
) -> Result<Vec<Envelope>> {
let path = self.resolve_mbox(mailbox);
let mut envelopes: Vec<Envelope> = self
.envelopes(&path, false)?
.into_iter()
.map(|(envelope, _)| envelope)
.collect();
envelopes.sort_by_key(|envelope| std::cmp::Reverse(envelope.date));
Ok(paginate(envelopes, page, page_size))
}
pub fn search_envelopes(
&self,
mailbox: &str,
query: Option<&SearchEmailsQuery>,
page: Option<u32>,
page_size: Option<u32>,
_with_attachment: bool,
) -> Result<Vec<Envelope>> {
let path = self.resolve_mbox(mailbox);
let filter = query.and_then(|q| q.filter.as_ref());
let mut hits: Vec<Envelope> = Vec::new();
for (envelope, contents) in self.envelopes(&path, filter.is_some())? {
let keep = match (filter, contents) {
(Some(filter), Some(contents)) => {
eval::matches_filter(&envelope, &contents, filter)
}
_ => true,
};
if keep {
hits.push(envelope);
}
}
eval::sort_envelopes(&mut hits, query.and_then(|q| q.sort.as_deref()));
Ok(paginate(hits, page, page_size))
}
pub fn store_flags(
&self,
mailbox: &str,
ids: &[&str],
flags: &[Flag],
op: FlagOp,
) -> Result<()> {
let path = self.resolve_mbox(mailbox);
let flags = flags_to_mbox(flags);
self.edit_flags(&path, ids, |current| {
let mut next = current.clone();
match op {
FlagOp::Add => next.extend(flags.iter().cloned()),
FlagOp::Remove => {
for flag in flags.iter() {
next.remove(flag);
}
}
FlagOp::Set => {
next = flags.clone();
if current.contains(&MboxFlag::Old) {
next.insert(MboxFlag::Old);
}
}
}
read_is_old(&mut next);
next
})
}
pub fn edit_flags(
&self,
path: &MboxFsPath,
ids: &[&str],
edit: impl Fn(&MboxFlags) -> MboxFlags,
) -> Result<()> {
let (index, mut cache) = self.index(path, false)?;
let mut edits = BTreeMap::new();
for id in ids {
let entry = find(&index, id, path)?;
let flags = edit(&entry.flags);
edits.insert(id.to_string(), MboxEntryUpdateEdit::Flags(flags));
}
self.update(path, edits, index, &mut cache)
}
pub fn get_message(&self, mailbox: &str, id: &str, seen: bool) -> Result<Vec<u8>> {
let path = self.resolve_mbox(mailbox);
let (index, _) = self.index(&path, false)?;
let contents = match self.read(&path, find(&index, id, &path)?) {
Err(MboxClientError::EntryGet(MboxEntryGetError::Stale(_))) => {
let (index, _) = self.index(&path, true)?;
self.read(&path, find(&index, id, &path)?)?
}
result => result?,
};
if seen {
let seen = Flag::from_iana(IanaFlag::Seen);
self.store_flags(mailbox, &[id], &[seen], FlagOp::Add)?;
}
Ok(contents)
}
pub fn add_message(&self, mailbox: &str, flags: &[Flag], raw: Vec<u8>) -> Result<String> {
let path = self.resolve_mbox(mailbox);
let mut flags = flags_to_mbox(flags);
read_is_old(&mut flags);
self.append(&path, flags, raw)
}
pub fn append(&self, path: &MboxFsPath, flags: MboxFlags, raw: Vec<u8>) -> Result<String> {
let item = MboxEntryAppendItem {
contents: raw,
flags,
..Default::default()
};
let opts = MboxEntryAppendOptions {
format: self.format,
lock: self.lock.clone(),
scanner: self.scanner.clone(),
};
let appended = self.run(MboxEntryAppend::new(path.clone(), vec![item], opts))?;
let offset = appended.first().map(|entry| entry.offset);
let (index, _) = self.index(path, false)?;
index
.entries
.iter()
.find(|entry| Some(entry.offset) == offset)
.map(|entry| entry.id.clone())
.ok_or_else(|| anyhow!("Cannot find the message appended to {path}"))
}
pub fn copy_messages(&self, from: &str, to: &str, ids: &[&str]) -> Result<usize> {
let source = self.resolve_mbox(from);
let target = self.resolve_mbox(to);
let (index, _) = self.index(&source, false)?;
let entries = ids
.iter()
.map(|id| find(&index, id, &source).cloned())
.collect::<Result<Vec<_>>>()?;
let opts = MboxEntryCopyOptions {
format: self.format,
lock: self.lock.clone(),
scanner: self.scanner.clone(),
chunk_size: self.chunk_size,
};
let copied = self.run(MboxEntryCopy::new(source, target, entries, opts))?;
Ok(copied.len())
}
pub fn move_messages(&self, from: &str, to: &str, ids: &[&str]) -> Result<usize> {
let source = self.resolve_mbox(from);
let target = self.resolve_mbox(to);
let (index, mut cache) = self.index(&source, false)?;
let entries = ids
.iter()
.map(|id| find(&index, id, &source).cloned())
.collect::<Result<Vec<_>>>()?;
let opts = MboxEntryMoveOptions {
index: Some(index),
format: self.format,
lock: self.lock.clone(),
scanner: self.scanner.clone(),
chunk_size: self.chunk_size,
};
let output = self.run(MboxEntryMove::new(source.clone(), target, entries, opts))?;
let moved = output.entries.len();
cache.index = Some(output.index);
cache.save(self.cache_dir.as_deref(), &source);
Ok(moved)
}
pub fn delete_messages(&self, mailbox: &str, ids: &[&str]) -> Result<()> {
let path = self.resolve_mbox(mailbox);
let (index, mut cache) = self.index(&path, false)?;
let mut edits = BTreeMap::new();
for id in ids {
find(&index, id, &path)?;
edits.insert(id.to_string(), MboxEntryUpdateEdit::Remove);
}
self.update(&path, edits, index, &mut cache)
}
pub fn role_mailbox_id(&self, role: &MailboxRole) -> Option<String> {
(*role == MailboxRole::Inbox && self.store.inbox.is_some()).then(|| INBOX.to_string())
}
fn index(&self, path: &MboxFsPath, rebuild: bool) -> Result<(MboxIndex, MboxCache)> {
let mut cache = self.sync(path, rebuild)?;
let index = cache.index.take().unwrap_or_default();
Ok((index, cache))
}
fn read(&self, path: &MboxFsPath, entry: &MboxEntry) -> Result<Vec<u8>, MboxClientError> {
let opts = MboxEntryGetOptions {
format: self.format,
scanner: self.scanner.clone(),
chunk_size: self.chunk_size,
};
let message = self.run(MboxEntryGet::new(path.clone(), entry.clone(), opts))?;
Ok(message.contents)
}
fn update(
&self,
path: &MboxFsPath,
edits: BTreeMap<String, MboxEntryUpdateEdit>,
index: MboxIndex,
cache: &mut MboxCache,
) -> Result<()> {
let opts = MboxEntryUpdateOptions {
index: Some(index),
lock: self.lock.clone(),
scanner: self.scanner.clone(),
chunk_size: self.chunk_size,
};
let output = self.run(MboxEntryUpdate::new(path.clone(), edits, opts))?;
cache.index = Some(output.index);
cache.save(self.cache_dir.as_deref(), path);
Ok(())
}
fn envelopes(
&self,
path: &MboxFsPath,
contents: bool,
) -> Result<Vec<(Envelope, Option<Vec<u8>>)>> {
let (index, mut cache) = self.index(path, false)?;
let mut envelopes = Vec::new();
let mut parsed = false;
for entry in index.messages() {
let cached = cache.envelopes.get(&entry.id).cloned();
let bytes = match (&cached, contents) {
(Some(_), false) => None,
_ => Some(self.read(path, entry)?),
};
let mut envelope = match cached {
Some(envelope) => envelope,
None => {
let envelope = envelope_from(bytes.as_deref().unwrap_or_default());
cache.envelopes.insert(entry.id.clone(), envelope.clone());
parsed = true;
envelope
}
};
envelope.id = entry.id.clone();
envelope.flags = entry.flags.iter().filter_map(flag_from_mbox).collect();
envelopes.push((envelope, bytes));
}
if parsed {
cache.index = Some(index);
cache.save(self.cache_dir.as_deref(), path);
}
Ok(envelopes)
}
}
fn find<'a>(index: &'a MboxIndex, id: &str, path: &MboxFsPath) -> Result<&'a MboxEntry> {
index
.get(id)
.ok_or_else(|| anyhow!("Message `{id}` not found in mbox {path}"))
}
fn envelope_from(contents: &[u8]) -> Envelope {
let parsed = MessageParser::new().parse(contents);
let subject = parsed
.as_ref()
.and_then(|m| m.subject())
.unwrap_or_default()
.to_string();
let from = parsed
.as_ref()
.and_then(|m| m.from())
.map(addresses_from)
.unwrap_or_default();
let to = parsed
.as_ref()
.and_then(|m| m.to())
.map(addresses_from)
.unwrap_or_default();
let date = parsed
.as_ref()
.and_then(|m| m.date())
.and_then(|d| DateTime::parse_from_rfc3339(&d.to_rfc3339()).ok());
let has_attachment = parsed.as_ref().map(|m| m.attachment_count() > 0);
let message_id = parsed
.as_ref()
.and_then(|m| m.message_id())
.and_then(normalize_message_id);
let in_reply_to = parsed
.as_ref()
.map(|m| ids_from_header(m.in_reply_to()))
.unwrap_or_default();
Envelope {
id: String::new(),
message_id,
in_reply_to,
flags: Default::default(),
subject,
from,
to,
date,
size: contents.len() as u64,
has_attachment,
}
}
fn ids_from_header(value: &HeaderValue<'_>) -> Vec<String> {
match value {
HeaderValue::TextList(ids) => ids
.iter()
.filter_map(|id| normalize_message_id(id))
.collect(),
HeaderValue::Text(id) => parse_message_ids(id),
_ => Vec::new(),
}
}
fn addresses_from(addrs: &MailParserAddress<'_>) -> Vec<Address> {
addrs
.clone()
.into_list()
.into_iter()
.filter_map(|a| {
let email = a.address?.into_owned();
if email.is_empty() {
return None;
}
let name = a.name.map(|s| s.into_owned());
Some(Address { name, email })
})
.collect()
}
fn flag_to_mbox(flag: &Flag) -> MboxFlag {
match flag.iana() {
Some(IanaFlag::Seen) => MboxFlag::Seen,
Some(IanaFlag::Answered) => MboxFlag::Answered,
Some(IanaFlag::Flagged) => MboxFlag::Flagged,
Some(IanaFlag::Draft) => MboxFlag::Draft,
Some(IanaFlag::Deleted) => MboxFlag::Deleted,
Some(_) | None => MboxFlag::Keyword(flag.raw().to_string()),
}
}
fn read_is_old(flags: &mut MboxFlags) {
if flags.contains(&MboxFlag::Seen) {
flags.insert(MboxFlag::Old);
}
}
fn flags_to_mbox(flags: &[Flag]) -> MboxFlags {
flags.iter().map(flag_to_mbox).collect()
}
fn flag_from_mbox(flag: &MboxFlag) -> Option<Flag> {
let flag = match flag {
MboxFlag::Seen => Flag::from_iana(IanaFlag::Seen),
MboxFlag::Answered => Flag::from_iana(IanaFlag::Answered),
MboxFlag::Flagged => Flag::from_iana(IanaFlag::Flagged),
MboxFlag::Draft => Flag::from_iana(IanaFlag::Draft),
MboxFlag::Deleted => Flag::from_iana(IanaFlag::Deleted),
MboxFlag::Keyword(keyword) => Flag::from_raw(keyword),
MboxFlag::Old => return None,
};
Some(flag)
}
fn paginate<T>(items: Vec<T>, page: Option<u32>, page_size: Option<u32>) -> Vec<T> {
let Some(size) = page_size else {
return items;
};
if size == 0 {
return Vec::new();
}
let page = page.unwrap_or(1).max(1);
let skip = ((page - 1) as usize).saturating_mul(size as usize);
if skip >= items.len() {
return Vec::new();
}
items.into_iter().skip(skip).take(size as usize).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mbox_flags_round_trip_through_the_shared_flag() {
let flags = [
MboxFlag::Seen,
MboxFlag::Answered,
MboxFlag::Flagged,
MboxFlag::Draft,
MboxFlag::Deleted,
MboxFlag::Keyword("NonJunk".into()),
MboxFlag::Keyword("$Junk".into()),
];
for flag in flags {
let shared = flag_from_mbox(&flag).unwrap();
assert_eq!(flag_to_mbox(&shared), flag);
}
assert_eq!(flag_from_mbox(&MboxFlag::Old), None);
}
#[test]
fn a_seen_message_is_old() {
let mut flags = MboxFlags::from_iter([MboxFlag::Seen]);
read_is_old(&mut flags);
assert!(flags.contains(&MboxFlag::Old));
let mut flags = MboxFlags::from_iter([MboxFlag::Flagged]);
read_is_old(&mut flags);
assert!(!flags.contains(&MboxFlag::Old));
}
#[test]
fn envelopes_come_from_the_headers() {
let envelope = envelope_from(
b"Subject: Hi\r\nFrom: A <a@x.org>\r\nMessage-ID: <1@x>\r\nIn-Reply-To: <0@x>\r\n\r\nbody",
);
assert_eq!(envelope.subject, "Hi");
assert_eq!(envelope.from[0].email, "a@x.org");
assert_eq!(envelope.message_id.as_deref(), Some("1@x"));
assert_eq!(envelope.in_reply_to, ["0@x"]);
assert_eq!(envelope.has_attachment, Some(false));
}
}