use alloc::{string::String, vec::Vec};
use std::{
fs, io, process,
time::{SystemTime, UNIX_EPOCH},
};
use gethostname::gethostname;
use io_maildir::{client::MaildirClient as InnerMaildirClient, coroutine::*, path::FsPath};
use log::trace;
use thiserror::Error;
#[cfg(feature = "search")]
use crate::{
envelope::maildir::search::{MaildirEnvelopeSearch, MaildirEnvelopeSearchError},
search::query::SearchEmailsQuery,
};
use crate::{
envelope::{
maildir::list::{MaildirEnvelopeList, MaildirEnvelopeListError},
types::Envelope,
},
flag::{
maildir::store::{MaildirFlagStore, MaildirFlagStoreError},
types::{Flag, FlagOp},
},
mailbox::{
maildir::{
create::{MaildirMailboxCreate, MaildirMailboxCreateError},
delete::{MaildirMailboxDelete, MaildirMailboxDeleteError},
list::{MaildirMailboxList, MaildirMailboxListError},
},
types::Mailbox,
},
message::maildir::{
add::{MaildirMessageAdd, MaildirMessageAddError},
copy::{MaildirMessageCopy, MaildirMessageCopyError},
delete::{MaildirMessageDelete, MaildirMessageDeleteError},
get::{MaildirMessageGet, MaildirMessageGetError},
r#move::{MaildirMessageMove, MaildirMessageMoveError},
},
};
#[derive(Debug, Error)]
pub enum MaildirClientError {
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
MailboxList(#[from] MaildirMailboxListError),
#[error(transparent)]
EnvelopeList(#[from] MaildirEnvelopeListError),
#[cfg(feature = "search")]
#[error(transparent)]
EnvelopeSearch(#[from] MaildirEnvelopeSearchError),
#[error(transparent)]
FlagStore(#[from] MaildirFlagStoreError),
#[error(transparent)]
MailboxCreate(#[from] MaildirMailboxCreateError),
#[error(transparent)]
MailboxDelete(#[from] MaildirMailboxDeleteError),
#[error(transparent)]
MessageAdd(#[from] MaildirMessageAddError),
#[error(transparent)]
MessageCopy(#[from] MaildirMessageCopyError),
#[error(transparent)]
MessageDelete(#[from] MaildirMessageDeleteError),
#[error(transparent)]
MessageGet(#[from] MaildirMessageGetError),
#[error(transparent)]
MessageMove(#[from] MaildirMessageMoveError),
#[error(transparent)]
Inner(#[from] io_maildir::client::MaildirClientError),
}
pub struct MaildirClient {
pub inner: InnerMaildirClient,
}
impl MaildirClient {
pub fn new(root: impl Into<FsPath>) -> Self {
Self {
inner: InnerMaildirClient::new(root),
}
}
pub fn run<C, T, E>(&self, mut coroutine: C) -> Result<T, MaildirClientError>
where
C: MaildirCoroutine<Yield = MaildirYield, Return = Result<T, E>>,
MaildirClientError: From<E>,
{
let mut arg: Option<MaildirReply> = None;
loop {
match coroutine.resume(arg.take()) {
MaildirCoroutineState::Complete(Ok(out)) => return Ok(out),
MaildirCoroutineState::Complete(Err(err)) => return Err(err.into()),
MaildirCoroutineState::Yielded(MaildirYield::WantsFileExists(paths)) => {
let mut out = alloc::collections::BTreeMap::new();
for path in paths {
let exists = fs::metadata(path.as_str())
.map(|m| m.is_file())
.unwrap_or(false);
trace!("file_exists {path}: {exists}");
out.insert(path, exists);
}
arg = Some(MaildirReply::FileExists(out));
}
MaildirCoroutineState::Yielded(MaildirYield::WantsDirExists(paths)) => {
let mut out = alloc::collections::BTreeMap::new();
for path in paths {
let exists = fs::metadata(path.as_str())
.map(|m| m.is_dir())
.unwrap_or(false);
trace!("dir_exists {path}: {exists}");
out.insert(path, exists);
}
arg = Some(MaildirReply::DirExists(out));
}
MaildirCoroutineState::Yielded(MaildirYield::WantsDirRead(paths)) => {
let mut entries = alloc::collections::BTreeMap::new();
for path in paths {
trace!("read_dir {path}");
let mut names = alloc::collections::BTreeSet::new();
match fs::read_dir(path.as_str()) {
Ok(iter) => {
for entry in iter {
let entry = entry?;
names.insert(FsPath::from(entry.path()));
}
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}
entries.insert(path, names);
}
arg = Some(MaildirReply::DirRead(entries));
}
MaildirCoroutineState::Yielded(MaildirYield::WantsFileRead(paths)) => {
let mut contents = alloc::collections::BTreeMap::new();
for path in paths {
trace!("read_file {path}");
let bytes = fs::read(path.as_str())?;
contents.insert(path, bytes);
}
arg = Some(MaildirReply::FileRead(contents));
}
MaildirCoroutineState::Yielded(MaildirYield::WantsFileCreate(files)) => {
for (path, bytes) in files {
trace!("write {path} ({} bytes)", bytes.len());
if let Some(parent) = std::path::Path::new(path.as_str()).parent() {
fs::create_dir_all(parent)?;
}
fs::write(path.as_str(), &bytes)?;
}
arg = Some(MaildirReply::FileCreate);
}
MaildirCoroutineState::Yielded(MaildirYield::WantsDirCreate(paths)) => {
for path in paths {
trace!("create_dir_all {path}");
fs::create_dir_all(path.as_str())?;
}
arg = Some(MaildirReply::DirCreate);
}
MaildirCoroutineState::Yielded(MaildirYield::WantsDirRemove(paths)) => {
for path in paths {
trace!("remove_dir_all {path}");
fs::remove_dir_all(path.as_str())?;
}
arg = Some(MaildirReply::DirRemove);
}
MaildirCoroutineState::Yielded(MaildirYield::WantsRename(pairs)) => {
for (from, to) in pairs {
trace!("rename {from} -> {to}");
fs::rename(from.as_str(), to.as_str())?;
}
arg = Some(MaildirReply::Rename);
}
MaildirCoroutineState::Yielded(MaildirYield::WantsCopy(pairs)) => {
for (from, to) in pairs {
trace!("copy {from} -> {to}");
fs::copy(from.as_str(), to.as_str())?;
}
arg = Some(MaildirReply::Copy);
}
MaildirCoroutineState::Yielded(MaildirYield::WantsTime) => {
let ts = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
arg = Some(MaildirReply::Time {
secs: ts.as_secs(),
nanos: ts.subsec_nanos(),
});
}
MaildirCoroutineState::Yielded(MaildirYield::WantsPid) => {
arg = Some(MaildirReply::Pid(process::id()));
}
MaildirCoroutineState::Yielded(MaildirYield::WantsHostname) => {
let hostname = gethostname().into_string().unwrap_or_default();
arg = Some(MaildirReply::Hostname(hostname));
}
}
}
}
pub fn list_mailboxes(&self, with_counts: bool) -> Result<Vec<Mailbox>, MaildirClientError> {
self.run(MaildirMailboxList::new(&self.inner.store, with_counts))
}
pub fn list_envelopes(
&self,
mailbox: &str,
page: Option<u32>,
page_size: Option<u32>,
_with_attachment: bool,
) -> Result<Vec<Envelope>, MaildirClientError> {
self.run(MaildirEnvelopeList::new(
&self.inner.store,
mailbox,
page,
page_size,
)?)
}
#[cfg(feature = "search")]
pub fn search_envelopes(
&self,
mailbox: &str,
query: Option<&SearchEmailsQuery>,
page: Option<u32>,
page_size: Option<u32>,
_with_attachment: bool,
) -> Result<Vec<Envelope>, MaildirClientError> {
self.run(MaildirEnvelopeSearch::new(
&self.inner.store,
mailbox,
query,
page,
page_size,
)?)
}
pub fn store_flags(
&self,
mailbox: &str,
ids: &[&str],
flags: &[Flag],
op: FlagOp,
) -> Result<(), MaildirClientError> {
self.run(MaildirFlagStore::new(
&self.inner.store,
mailbox,
ids,
flags,
op,
)?)
}
pub fn get_message(&self, mailbox: &str, id: &str) -> Result<Vec<u8>, MaildirClientError> {
self.run(MaildirMessageGet::new(&self.inner.store, mailbox, id)?)
}
pub fn add_message(
&self,
mailbox: &str,
flags: &[Flag],
raw: Vec<u8>,
) -> Result<String, MaildirClientError> {
self.run(MaildirMessageAdd::new(
&self.inner.store,
mailbox,
flags,
raw,
)?)
}
pub fn create_mailbox(&self, name: &str) -> Result<(), MaildirClientError> {
self.run(MaildirMailboxCreate::new(&self.inner.store, name)?)
}
pub fn delete_mailbox(&self, name: &str) -> Result<(), MaildirClientError> {
self.run(MaildirMailboxDelete::new(&self.inner.store, name)?)
}
pub fn delete_message(&self, mailbox: &str, id: &str) -> Result<(), MaildirClientError> {
self.run(MaildirMessageDelete::new(&self.inner.store, mailbox, id)?)
}
pub fn copy_messages(
&self,
from: &str,
to: &str,
ids: &[&str],
) -> Result<(), MaildirClientError> {
self.run(MaildirMessageCopy::new(&self.inner.store, from, to, ids)?)
}
pub fn move_messages(
&self,
from: &str,
to: &str,
ids: &[&str],
) -> Result<(), MaildirClientError> {
self.run(MaildirMessageMove::new(&self.inner.store, from, to, ids)?)
}
}