use std::{
collections::{HashMap, HashSet, VecDeque},
fs,
io::{self, Read, Write},
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
sync::{Arc, Mutex},
time::Duration,
};
use futures::{channel::mpsc, SinkExt};
use notify::Watcher;
use regex::Regex;
pub mod utilities;
pub mod watch;
#[cfg(test)]
mod tests;
use utilities::{
HashIndex, HashIndexes, MaildirFilePathExt, MaildirMailbox, MaildirMailboxPathExt, MaildirOp,
PathMod,
};
use crate::{
backends::{prelude::*, RefreshEventKind::*},
error::{Error, ErrorKind, IntoError, Result, ResultIntoError},
utils::shellexpand::ShellExpandTrait,
};
#[derive(Debug, Default)]
pub struct Configuration {
pub rename_regex: Option<Regex>,
pub path: PathBuf,
pub root_mailbox_name: String,
pub is_root_a_mailbox: bool,
pub settings: AccountSettings,
}
impl Configuration {
pub fn new(settings: &AccountSettings) -> Result<Self> {
const RENAME_REGEX_FIELDNAME: &str = "rename_regex";
let rename_regex = if let Some(v) = settings.extra.get(RENAME_REGEX_FIELDNAME).map(|v| {
Regex::new(v).map_err(|e| {
Error::new(format!(
"Configuration error ({}): Invalid value for field \
`{RENAME_REGEX_FIELDNAME}`: {v}",
settings.name.as_str(),
))
.set_source(Some(crate::src_err_arc_wrap!(e)))
.set_kind(ErrorKind::ValueError)
})
}) {
Some(v?)
} else {
None
};
Ok(Self {
rename_regex,
settings: settings.clone(),
..Self::default()
})
}
}
#[derive(Debug)]
pub struct MaildirType {
pub account_name: String,
pub account_hash: AccountHash,
pub mailboxes: HashMap<MailboxHash, MaildirMailbox>,
pub mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
pub hash_indexes: HashIndexes,
pub event_consumer: BackendEventConsumer,
pub is_subscribed: IsSubscribedFn,
pub collection: Collection,
pub config: Arc<Configuration>,
}
impl MailBackend for MaildirType {
fn capabilities(&self) -> MailBackendCapabilities {
const CAPABILITIES: MailBackendCapabilities = MailBackendCapabilities {
is_async: false,
is_remote: false,
supports_search: false,
extensions: None,
supports_tags: false,
supports_submission: false,
extra_submission_headers: &[],
metadata: None,
};
CAPABILITIES
}
fn is_online(&self) -> ResultFuture<()> {
Ok(Box::pin(async { Ok(()) }))
}
fn mailboxes(&self) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
let res = Ok(self
.mailboxes
.iter()
.map(|(h, f)| (*h, BackendMailbox::clone(f)))
.collect());
Ok(Box::pin(async { res }))
}
fn fetch(&mut self, mailbox_hash: MailboxHash) -> ResultStream<Vec<Envelope>> {
let mailbox: &MaildirMailbox = &self.mailboxes[&mailbox_hash];
let unseen = mailbox.unseen.clone();
let total = mailbox.total.clone();
let mut path: PathBuf = mailbox.fs_path().into();
let hash_indexes = self.hash_indexes.clone();
let mailbox_index = self.mailbox_index.clone();
let chunk_size = 2048;
path.push("new");
for p in path.read_dir()?.flatten() {
_ = utilities::move_to_cur(&self.config, &p.path());
}
path.pop();
path.push("cur");
let files: Vec<PathBuf> = path
.read_dir()?
.flatten()
.map(|e| e.path())
.collect::<Vec<_>>();
async fn fetch(
chunk: Vec<std::path::PathBuf>,
mailbox_hash: MailboxHash,
unseen: Arc<Mutex<usize>>,
total: Arc<Mutex<usize>>,
hash_indexes: HashIndexes,
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
) -> Result<Option<Vec<Envelope>>> {
let mut local_r: Vec<Envelope> = Vec::with_capacity(chunk.len());
let mut unseen_total: usize = 0;
let mut buf = Vec::with_capacity(4096);
for file in chunk {
let env_hash = file.to_envelope_hash();
{
let mut hash_indexes = hash_indexes.lock().unwrap();
let hi = hash_indexes.entry(mailbox_hash).or_default();
hi.index.insert(env_hash, file.to_path_buf().into());
hi.reverse_index.insert(file.to_path_buf(), env_hash);
}
let mut reader = io::BufReader::new(fs::File::open(&file)?);
buf.clear();
reader.read_to_end(&mut buf)?;
match Envelope::from_bytes(buf.as_slice(), Some(file.flags())) {
Ok(mut env) => {
env.set_hash(env_hash);
mailbox_index.lock().unwrap().insert(env_hash, mailbox_hash);
if !env.is_seen() {
unseen_total += 1;
}
local_r.push(env);
}
Err(err) => {
debug!(
"DEBUG: hash {env_hash}, path: {} couldn't be parsed, {err}",
file.as_path().display()
);
continue;
}
}
}
*total.lock().unwrap() += local_r.len();
*unseen.lock().unwrap() += unseen_total;
if local_r.is_empty() {
Ok(None)
} else {
Ok(Some(local_r))
}
}
Ok(Box::pin(try_fn_stream(|emitter| async move {
for chunk in files.chunks(chunk_size) {
if let Some(res) = fetch(
chunk.to_vec(),
mailbox_hash,
unseen.clone(),
total.clone(),
hash_indexes.clone(),
mailbox_index.clone(),
)
.await
.map_err(|err| {
log::debug!("fetch err {err:?}");
err
})? {
emitter.emit(res).await;
}
}
Ok(())
})))
}
fn refresh(&mut self, mailbox_hash: MailboxHash) -> ResultFuture<()> {
let account_hash = self.account_hash;
let sender = self.event_consumer.clone();
let mailbox: &MaildirMailbox = &self.mailboxes[&mailbox_hash];
let path: PathBuf = mailbox.fs_path().into();
let hash_indexes = self.hash_indexes.clone();
let mailbox_index = self.mailbox_index.clone();
let config = self.config.clone();
Ok(Box::pin(async move {
let thunk = move |sender: &BackendEventConsumer| {
log::trace!("refreshing {mailbox_hash:?}");
let mut buf = Vec::with_capacity(4096);
let files = Self::list_mail_in_maildir_fs(&config, path.clone(), false)?;
let mut removed_hashes = {
let mut map = hash_indexes.lock().unwrap();
let map = map.entry(mailbox_hash).or_default();
map.index.keys().cloned().collect::<HashSet<EnvelopeHash>>()
};
for file in files {
let env_hash = file.to_envelope_hash();
{
let mut map = hash_indexes.lock().unwrap();
let map = map.entry(mailbox_hash).or_default();
if map.index.contains_key(&env_hash) {
removed_hashes.remove(&env_hash);
continue;
}
map.index.insert(env_hash, file.to_path_buf().into());
map.reverse_index.insert(file.to_path_buf(), env_hash);
}
let mut reader = io::BufReader::new(fs::File::open(&file)?);
buf.clear();
reader.read_to_end(&mut buf)?;
if let Ok(mut env) = Envelope::from_bytes(buf.as_slice(), Some(file.flags())) {
env.set_hash(env_hash);
mailbox_index
.lock()
.unwrap()
.insert(env.hash(), mailbox_hash);
(sender)(
account_hash,
BackendEvent::Refresh(RefreshEvent {
account_hash,
mailbox_hash,
kind: Create(Box::new(env)),
}),
);
} else {
log::trace!(
"DEBUG: hash {env_hash}, path: {} couldn't be parsed",
file.as_path().display()
);
continue;
}
}
for ev in removed_hashes.into_iter().map(|h| {
BackendEvent::Refresh(RefreshEvent {
account_hash,
mailbox_hash,
kind: Remove(h),
})
}) {
(sender)(account_hash, ev);
}
Ok(())
};
if let Err(err) = thunk(&sender) {
(sender)(
account_hash,
BackendEvent::Refresh(RefreshEvent {
account_hash,
mailbox_hash,
kind: Failure(err),
}),
);
}
Ok(())
}))
}
fn watch(&self) -> ResultStream<BackendEvent> {
let root_mailbox = self.config.path.to_path_buf();
let (mut tx, rx) = mpsc::channel(16);
let watcher = {
let watcher = notify::RecommendedWatcher::new(
move |res| {
futures::executor::block_on(async {
_ = tx.send(res).await;
})
},
notify::Config::default().with_poll_interval(Duration::from_secs(2)),
);
watcher
.and_then(|mut watcher| {
watcher.watch(&root_mailbox, notify::RecursiveMode::Recursive)?;
Ok(Box::new(watcher))
})
.map_err(|err| err.set_err_details("Failed to create file change monitor."))?
};
let mailbox_counts = self
.mailboxes
.iter()
.map(|(&k, v)| (k, (v.unseen.clone(), v.total.clone())))
.collect::<HashMap<MailboxHash, (Arc<Mutex<usize>>, Arc<Mutex<usize>>)>>();
let watch_state = watch::MaildirWatch {
watcher,
account_hash: self.account_hash,
rx,
hash_indexes: self.hash_indexes.clone(),
mailbox_index: self.mailbox_index.clone(),
mailbox_counts,
config: self.config.clone(),
};
let stream = watch_state.watch();
Ok(Box::pin(stream))
}
fn envelope_bytes_by_hash(&self, hash: EnvelopeHash) -> ResultFuture<Vec<u8>> {
let op = MaildirOp::new(
hash,
self.hash_indexes.clone(),
self.mailbox_index.lock().unwrap()[&hash],
);
Ok(Box::pin(async move { op.as_bytes().await }))
}
fn save(
&self,
bytes: Vec<u8>,
mailbox_hash: MailboxHash,
flags: Option<Flag>,
) -> ResultFuture<()> {
let path = self.mailboxes[&mailbox_hash].fs_path.clone();
Ok(Box::pin(async move {
_ = Self::save_to_mailbox(path, bytes, flags)?;
Ok(())
}))
}
fn set_flags(
&mut self,
env_hashes: EnvelopeHashBatch,
mailbox_hash: MailboxHash,
flag_ops: Vec<FlagOp>,
) -> ResultFuture<()> {
if flag_ops.iter().any(|op| op.is_tag()) {
return Err(Error::new("Maildir doesn't support tags."));
}
let hash_index = self.hash_indexes.clone();
let config = self.config.clone();
Ok(Box::pin(async move {
let mut hash_indexes_lck = hash_index.lock().unwrap();
let hash_index = hash_indexes_lck.entry(mailbox_hash).or_default();
for env_hash in env_hashes.iter() {
let path = {
if !hash_index.contains_key(&env_hash) {
continue;
}
if let Some(modif) = &hash_index[&env_hash].modified {
match modif {
PathMod::Path(ref path) => path.clone(),
PathMod::Hash(hash) => hash_index[hash].to_path_buf(),
}
} else {
hash_index[&env_hash].to_path_buf()
}
};
let mut new_flags = path.flags();
for op in flag_ops.iter() {
if let FlagOp::Set(f) | FlagOp::UnSet(f) = op {
new_flags.set(*f, op.as_bool());
}
}
let new_name: PathBuf = path.set_flags(new_flags, &config)?;
hash_index.entry(env_hash).or_default().modified =
Some(PathMod::Path(new_name.clone()));
log::debug!("renaming {path:?} to {new_name:?}");
fs::rename(path, &new_name)?;
log::debug!("success in rename");
}
Ok(())
}))
}
fn delete_messages(
&mut self,
env_hashes: EnvelopeHashBatch,
mailbox_hash: MailboxHash,
) -> ResultFuture<()> {
let hash_index = self.hash_indexes.clone();
Ok(Box::pin(async move {
let mut hash_indexes_lck = hash_index.lock().unwrap();
let hash_index = hash_indexes_lck.entry(mailbox_hash).or_default();
for env_hash in env_hashes.iter() {
let path = {
if !hash_index.contains_key(&env_hash) {
continue;
}
if let Some(modif) = &hash_index[&env_hash].modified {
match modif {
PathMod::Path(ref path) => path.clone(),
PathMod::Hash(hash) => hash_index[hash].to_path_buf(),
}
} else {
hash_index[&env_hash].to_path_buf()
}
};
fs::remove_file(&path)?;
}
Ok(())
}))
}
fn copy_messages(
&mut self,
env_hashes: EnvelopeHashBatch,
source_mailbox_hash: MailboxHash,
destination_mailbox_hash: MailboxHash,
move_: bool,
) -> ResultFuture<()> {
if !self.mailboxes.contains_key(&source_mailbox_hash) {
return Err(Error::new("Invalid source mailbox hash").set_kind(ErrorKind::Bug));
} else if !self.mailboxes.contains_key(&destination_mailbox_hash) {
return Err(Error::new("Invalid destination mailbox hash").set_kind(ErrorKind::Bug));
}
let hash_index = self.hash_indexes.clone();
let config = self.config.clone();
let mut dest_dir: PathBuf = self.mailboxes[&destination_mailbox_hash].fs_path().into();
dest_dir.push("cur");
Ok(Box::pin(async move {
let mut hash_indexes_lck = hash_index.lock().unwrap();
let hash_index = hash_indexes_lck.entry(source_mailbox_hash).or_default();
for env_hash in env_hashes.iter() {
let path_src = {
if !hash_index.contains_key(&env_hash) {
continue;
}
if let Some(modif) = &hash_index[&env_hash].modified {
match modif {
PathMod::Path(ref path) => path.clone(),
PathMod::Hash(hash) => hash_index[hash].to_path_buf(),
}
} else {
hash_index[&env_hash].to_path_buf()
}
};
let dest_path = path_src.place_in_dir(&dest_dir, &config)?;
hash_index.entry(env_hash).or_default().modified =
Some(PathMod::Path(dest_path.clone()));
if move_ {
log::trace!("renaming {path_src:?} to {dest_path:?}");
fs::rename(&path_src, &dest_path)
.chain_err_summary(|| {
format!(
"Could not rename {} to {}",
path_src.display(),
dest_path.display()
)
})
.chain_err_related_path(&path_src)?;
log::trace!("success in rename");
} else {
log::trace!("copying {path_src:?} to {dest_path:?}");
fs::copy(&path_src, &dest_path)
.chain_err_summary(|| {
format!(
"Could not copy {} to {}",
path_src.display(),
dest_path.display()
)
})
.chain_err_related_path(&path_src)?;
log::trace!("success in copy");
}
}
Ok(())
}))
}
fn collection(&self) -> Collection {
self.collection.clone()
}
fn create_mailbox(
&mut self,
name: String,
) -> ResultFuture<(MailboxHash, HashMap<MailboxHash, Mailbox>)> {
let (mut fs_path, suffix) =
match Self::mailbox_path_to_fs_path_and_suffix(&self.config, &name) {
Ok(v) => v,
Err(err) => {
return Ok(Box::pin(async move { Err(err) }));
}
};
if let Err(err) = std::fs::create_dir(&fs_path)
.chain_err_summary(|| "Could not create new mailbox")
.chain_err_related_path(&fs_path)
{
return Ok(Box::pin(async move { Err(err) }));
};
let mut undo_ops = VecDeque::new();
let mut push_undo_op = |fs_path: &Path| {
undo_ops.push_back(Box::new({
let fs_path = fs_path.to_path_buf();
move || -> Result<()> {
std::fs::remove_dir(&fs_path)
.chain_err_summary(|| "Could not cleanup filesystem folder")
.chain_err_related_path(&fs_path)?;
Ok(())
}
}));
};
push_undo_op(&fs_path);
for d in &["cur", "new", "tmp"] {
fs_path.push(d);
if let Err(err) = std::fs::create_dir(&fs_path)
.chain_err_summary(|| "Could not create new mailbox")
.chain_err_related_path(&fs_path)
{
while let Some(op) = undo_ops.pop_back() {
if let Err(err) = op() {
log::error!("{err}");
}
}
return Ok(Box::pin(async move { Err(err) }));
};
push_undo_op(&fs_path);
fs_path.pop();
}
let mailbox_hash = match self.mailbox_from_path(fs_path, suffix) {
Ok(v) => v,
Err(err) => {
while let Some(op) = undo_ops.pop_back() {
if let Err(err) = op() {
log::error!("{err}");
}
}
return Ok(Box::pin(async move { Err(err) }));
}
};
let mailboxes_fut = self.mailboxes()?;
Ok(Box::pin(
async move { Ok((mailbox_hash, mailboxes_fut.await?)) },
))
}
fn delete_mailbox(
&mut self,
_mailbox_hash: MailboxHash,
) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
Err(
Error::new("Deleting mailboxes is currently unimplemented for maildir backend.")
.set_kind(ErrorKind::NotImplemented),
)
}
fn set_mailbox_subscription(
&mut self,
_mailbox_hash: MailboxHash,
_val: bool,
) -> ResultFuture<()> {
Err(Error::new(
"Mailbox subscriptions are not possible for the maildir backend.",
))
}
fn rename_mailbox(
&mut self,
_mailbox_hash: MailboxHash,
_new_path: String,
) -> ResultFuture<Mailbox> {
Err(
Error::new("Renaming mailboxes is currently unimplemented for maildir backend.")
.set_kind(ErrorKind::NotImplemented),
)
}
fn set_mailbox_permissions(
&mut self,
_mailbox_hash: MailboxHash,
_val: crate::backends::MailboxPermissions,
) -> ResultFuture<()> {
Err(Error::new(
"Setting mailbox permissions is not possible for the maildir backend.",
))
}
fn search(
&self,
_query: crate::search::Query,
_mailbox_hash: Option<MailboxHash>,
) -> ResultFuture<Vec<EnvelopeHash>> {
Err(
Error::new("Search is unimplemented for the maildir backend.")
.set_kind(ErrorKind::NotImplemented),
)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
impl MaildirType {
pub fn new(
settings: &AccountSettings,
is_subscribed: IsSubscribedFn,
event_consumer: BackendEventConsumer,
) -> Result<Box<Self>> {
let mut mailboxes: HashMap<MailboxHash, MaildirMailbox> = Default::default();
fn recurse_mailboxes<P: AsRef<Path>>(
mailboxes: &mut HashMap<MailboxHash, MaildirMailbox>,
config: &Configuration,
p: P,
) -> Result<Vec<MailboxHash>> {
if !p.as_ref().try_exists().unwrap_or(false) || !p.as_ref().is_dir() {
return Err(Error::new(format!(
"Configuration error: Path \"{}\" {}",
p.as_ref().display(),
if !p.as_ref().try_exists().unwrap_or(false) {
"does not exist."
} else {
"is not a directory."
}
)));
}
let mut children = Vec::new();
for mut f in fs::read_dir(&p).unwrap() {
'entries: for f in f.iter_mut() {
{
let path = f.path();
if path.ends_with("cur") || path.ends_with("new") || path.ends_with("tmp") {
continue 'entries;
}
if path.is_dir() {
if let Ok(mut f) = MaildirMailbox::new(
path.to_str().unwrap().to_string(),
path.file_name().unwrap().to_str().unwrap().to_string(),
None,
Vec::new(),
false,
config,
) {
f.children = recurse_mailboxes(mailboxes, config, &path)?;
for c in &f.children {
if let Some(f) = mailboxes.get_mut(c) {
f.parent = Some(f.hash);
}
}
children.push(f.hash);
mailboxes.insert(f.hash, f);
} else {
let subdirs = recurse_mailboxes(mailboxes, config, &path)?;
if !subdirs.is_empty() {
if let Ok(f) = MaildirMailbox::new(
path.to_str().unwrap().to_string(),
path.file_name().unwrap().to_str().unwrap().to_string(),
None,
subdirs,
true,
config,
) {
for c in &f.children {
if let Some(f) = mailboxes.get_mut(c) {
f.parent = Some(f.hash);
}
}
children.push(f.hash);
mailboxes.insert(f.hash, f);
}
}
}
}
}
}
}
Ok(children)
}
let root_mailbox = PathBuf::from(&settings.root_mailbox).expand();
if !root_mailbox.try_exists().unwrap_or(false) {
return Err(Error::new(format!(
"Configuration error ({}): root_mailbox `{}` is not a valid directory.",
settings.name,
settings.root_mailbox.as_str()
)));
} else if !root_mailbox.is_dir() {
return Err(Error::new(format!(
"Configuration error ({}): root_mailbox `{}` is not a directory.",
settings.name,
settings.root_mailbox.as_str()
)));
}
let (is_root_a_mailbox, root_mailbox_name) = if let Ok(f) = MaildirMailbox::new_root_mailbox(
root_mailbox.to_str().unwrap().to_string(),
root_mailbox
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.to_string(),
None,
Vec::with_capacity(0),
false,
settings,
) {
let name = f.name.clone();
mailboxes.insert(f.hash, f);
(true, name)
} else {
(
false,
root_mailbox
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.to_string(),
)
};
let config = Arc::new(Configuration {
path: root_mailbox.clone(),
root_mailbox_name,
is_root_a_mailbox,
..Configuration::new(settings)?
});
if mailboxes.is_empty() {
let children = recurse_mailboxes(&mut mailboxes, &config, &root_mailbox)?;
for c in &children {
if let Some(f) = mailboxes.get_mut(c) {
f.parent = None;
}
}
} else {
let root_hash = *mailboxes.keys().next().unwrap();
let children = recurse_mailboxes(&mut mailboxes, &config, &root_mailbox)?;
for c in &children {
if let Some(f) = mailboxes.get_mut(c) {
f.parent = Some(root_hash);
}
}
if let Some(f) = mailboxes.get_mut(&root_hash) {
f.children = children;
}
}
for f in mailboxes.values_mut() {
if is_subscribed(f.path()) {
f.is_subscribed = true;
}
}
let mut hash_indexes =
HashMap::with_capacity_and_hasher(mailboxes.len(), Default::default());
for &fh in mailboxes.keys() {
hash_indexes.insert(
fh,
HashIndex {
index: HashMap::new(),
reverse_index: HashMap::new(),
mailbox_hash: fh,
},
);
}
Ok(Box::new(Self {
account_name: settings.name.to_string(),
account_hash: AccountHash::from_bytes(settings.name.as_bytes()),
mailboxes,
is_subscribed,
hash_indexes: Arc::new(Mutex::new(hash_indexes)),
mailbox_index: Default::default(),
event_consumer,
collection: Default::default(),
config,
}))
}
pub fn save_to_mailbox(
mut path: PathBuf,
bytes: Vec<u8>,
flags: Option<Flag>,
) -> Result<PathBuf> {
path.validate_fs_subdirs()?;
path.push("cur");
{
type BeBytes128 = [u8; 16];
debug_assert_eq!(std::mem::size_of::<u64>(), 8);
debug_assert_eq!(
std::mem::size_of::<BeBytes128>(),
2 * std::mem::size_of::<[u8; 8]>()
);
let mut rand_num: BeBytes128 = [0u8; 16];
rand_num[0..8].copy_from_slice(&crate::utils::random::random_u64().to_be_bytes());
rand_num[8..].copy_from_slice(&crate::utils::random::random_u64().to_be_bytes());
let hostname = crate::utils::hostname::hostname()
.ok()
.and_then(|osstr| osstr.into_string().ok().map(Cow::Owned))
.unwrap_or(Cow::Borrowed("localhost"));
let mut filename = format!(
"{}.{:x}_{}.{}:2,",
crate::utils::random::clock_millis(),
u128::from_be_bytes(rand_num),
std::process::id(),
hostname.trim()
);
if let Some(flags) = flags {
if !(flags & Flag::DRAFT).is_empty() {
filename.push('D');
}
if !(flags & Flag::FLAGGED).is_empty() {
filename.push('F');
}
if !(flags & Flag::PASSED).is_empty() {
filename.push('P');
}
if !(flags & Flag::REPLIED).is_empty() {
filename.push('R');
}
if !(flags & Flag::SEEN).is_empty() {
filename.push('S');
}
if !(flags & Flag::TRASHED).is_empty() {
filename.push('T');
}
}
path.push(filename);
}
log::trace!("saving at {}", path.display());
let file = fs::File::create(&path)
.chain_err_summary(|| format!("Could not create {}", path.display()))
.chain_err_related_path(&path)?;
let metadata = file
.metadata()
.chain_err_summary(|| format!("Could not retrieve file metadata of {}", path.display()))
.chain_err_related_path(&path)?;
let mut permissions = metadata.permissions();
permissions.set_mode(0o600); file.set_permissions(permissions)
.chain_err_summary(|| {
format!("Could not set new file permissions to {}", path.display())
})
.chain_err_related_path(&path)?;
let mut writer = io::BufWriter::new(file);
writer
.write_all(&bytes)
.chain_err_summary(|| format!("Could not write bytes to new file {}", path.display()))
.chain_err_related_path(&path)?;
Ok(path)
}
pub fn validate_config(s: &mut AccountSettings) -> Result<()> {
let root_mailbox = PathBuf::from(&s.root_mailbox).expand();
if !root_mailbox.try_exists().unwrap_or(false) {
return Err(Error::new(format!(
"Configuration error ({}): root_mailbox `{}` is not a valid directory.",
s.name,
s.root_mailbox.as_str()
)));
} else if !root_mailbox.is_dir() {
return Err(Error::new(format!(
"Configuration error ({}): root_mailbox `{}` is not a directory.",
s.name,
s.root_mailbox.as_str()
)));
}
_ = Configuration::new(s)?;
_ = s.extra.swap_remove("rename_regex");
Ok(())
}
pub fn list_mail_in_maildir_fs(
config: &Configuration,
mut path: PathBuf,
read_only: bool,
) -> Result<Vec<PathBuf>> {
path.validate_fs_subdirs()?;
let mut files: Vec<PathBuf> = vec![];
path.push("new");
for p in path.read_dir().chain_err_related_path(&path)?.flatten() {
if !read_only {
_ = utilities::move_to_cur(config, &p.path());
} else {
files.push(p.path());
}
}
path.pop();
path.push("cur");
for e in path.read_dir().chain_err_related_path(&path)?.flatten() {
files.push(e.path());
}
Ok(files)
}
fn mailbox_path_to_fs_path_and_suffix(
config: &Configuration,
name: &str,
) -> Result<(PathBuf, String)> {
let mut fs_path = config.path.clone();
let mut suffix = name.to_string();
let root_mailbox_path_str = PathBuf::from(&config.settings.root_mailbox)
.expand()
.display()
.to_string();
if suffix.starts_with(&root_mailbox_path_str)
&& suffix.get(root_mailbox_path_str.len()..).is_some()
&& suffix[root_mailbox_path_str.len()..].starts_with("/")
{
suffix.replace_range(0..=root_mailbox_path_str.len(), "");
}
if suffix.starts_with(&config.root_mailbox_name)
&& suffix.get(config.root_mailbox_name.len()..).is_some()
&& suffix[config.root_mailbox_name.len()..].starts_with("/")
{
suffix.replace_range(0..=config.root_mailbox_name.len(), "");
}
fs_path.push(&suffix);
let not_in_root = config
.path
.parent()
.map(|p| fs_path.starts_with(p) && !fs_path.starts_with(&config.path))
.unwrap_or(false);
if not_in_root {
return Err(Error::new(format!(
"Path given, `{name}`, is not included in the root mailbox path `{}`. A maildir \
backend cannot contain mailboxes outside of its root path.",
config.path.display()
)));
}
if !fs_path.starts_with(&config.path) {
return Err(Error::new(format!(
"Path given (`{name}`) is absolute. Please provide a path relative to the \
account's root mailbox.",
)));
}
Ok((fs_path, suffix))
}
pub fn mailbox_from_path(&mut self, fs_path: PathBuf, suffix: String) -> Result<MailboxHash> {
fs_path.validate_fs_subdirs()?;
let parent = fs_path.parent().and_then(|p| {
self.mailboxes
.iter()
.find(|(_, f)| f.fs_path == p)
.map(|item| *item.0)
});
let Some(mailbox_hash): Option<MailboxHash> = fs_path.to_mailbox_hash() else {
return Err(Error::new(format!(
"Path (`{}`) does not exist or is not a valid mailbox.",
fs_path.display()
))
.set_related_path(Some(fs_path)));
};
if let Some(parent) = parent {
self.mailboxes
.entry(parent)
.and_modify(|entry| entry.children.push(mailbox_hash));
}
let path = if self.config.is_root_a_mailbox {
let mut path = PathBuf::from(&self.config.root_mailbox_name);
path.push(suffix);
path
} else {
PathBuf::from(&suffix)
};
let name = fs_path.file_name().unwrap().to_str().unwrap().to_string();
let new_mailbox = MaildirMailbox {
hash: mailbox_hash,
path,
name,
fs_path,
parent,
children: vec![],
usage: Default::default(),
is_subscribed: true,
permissions: Default::default(),
unseen: Default::default(),
total: Default::default(),
};
self.mailboxes.insert(mailbox_hash, new_mailbox);
Ok(mailbox_hash)
}
}