use std::fs;
use std::io::{self, Read};
use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt};
use std::path::{Path, PathBuf};
use rand::{rngs::OsRng, Rng};
use serde::{Deserialize, Serialize};
use tempfile::TempDir;
use crate::account::model::*;
use crate::support::error::Error;
use crate::support::file_ops::{self, ErrorTransforms, IgnoreKinds};
use crate::support::safe_name::is_safe_name;
#[derive(Clone, Debug)]
pub struct MailboxPath {
pub(super) name: String,
pub(super) base_path: PathBuf,
pub(super) data_path: PathBuf,
pub(super) metadata_path: PathBuf,
pub(super) socks_path: PathBuf,
pub(super) shadow_path: PathBuf,
pub(super) sub_path: PathBuf,
}
impl MailboxPath {
fn from_name_and_path(
name: String,
base_path: PathBuf,
shadow_path: PathBuf,
) -> Self {
let data_path = base_path.join("%");
MailboxPath {
metadata_path: data_path.join("mailbox.toml"),
sub_path: shadow_path.join("%subscribe"),
socks_path: data_path.join("socks"),
name,
base_path,
data_path,
shadow_path,
}
}
pub fn root(
name: String,
root: &Path,
shadow_root: &Path,
) -> Result<Self, Error> {
if !is_safe_name(&name) {
return Err(Error::UnsafeName);
}
let path = root.join(&name);
let shadow_path = shadow_root.join(&name);
Ok(MailboxPath::from_name_and_path(name, path, shadow_path))
}
pub fn child(&self, name: &str) -> Result<Self, Error> {
if !is_safe_name(&name) {
return Err(Error::UnsafeName);
}
if !self.allows_children() {
return Err(Error::BadOperationOnInbox);
}
Ok(MailboxPath::from_name_and_path(
format!("{}/{}", self.name, name),
self.base_path.join(name),
self.shadow_path.join(name),
))
}
pub fn name(&self) -> &str {
&self.name
}
pub fn current_uid_validity(&self) -> Result<u32, Error> {
if !self.exists() {
return Err(Error::NxMailbox);
}
parse_uid_validity(
&nix::fcntl::readlink(&self.data_path)
.on_not_found(Error::MailboxUnselectable)?,
)
}
pub fn scoped_data_path(&self) -> Result<PathBuf, Error> {
Ok(
self.scoped_data_path_for_uid_validity(
self.current_uid_validity()?,
),
)
}
fn scoped_data_path_for_uid_validity(&self, uid_validity: u32) -> PathBuf {
self.base_path.join(format!("%{:x}", uid_validity))
}
pub fn allows_children(&self) -> bool {
"INBOX" != &self.name
}
pub fn is_selectable(&self) -> bool {
self.data_path.is_dir()
}
pub fn exists(&self) -> bool {
self.base_path.is_dir()
}
pub fn is_subscribed(&self) -> bool {
self.sub_path.is_file()
}
pub fn children<'a>(&'a self) -> impl Iterator<Item = MailboxPath> + 'a {
self.children_impl(&self.base_path)
}
fn children_impl<'a>(
&'a self,
path: &Path,
) -> impl Iterator<Item = MailboxPath> + 'a {
fs::read_dir(path)
.ok()
.map(move |it| {
it.filter_map(|r| r.ok())
.filter_map(|entry| entry.file_name().into_string().ok())
.filter_map(move |name| self.child(&name).ok())
})
.map(|it| {
Box::new(it) as Box<dyn Iterator<Item = MailboxPath> + 'a>
})
.unwrap_or_else(|| Box::new(std::iter::empty()))
}
pub fn create(
&self,
tmp: &Path,
special_use: Option<MailboxAttribute>,
) -> Result<String, Error> {
let uid_validity = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs() as u32;
let mut uid_validity = uid_validity.wrapping_sub(1577836800).max(1);
for trie in 0.. {
match fs::OpenOptions::new()
.mode(0o600)
.create_new(true)
.write(true)
.open(tmp.join(format!("uv-{:x}", uid_validity)))
{
Ok(_) => break,
Err(e) if io::ErrorKind::AlreadyExists == e.kind() => {
if trie < 1000 {
uid_validity = uid_validity.wrapping_add(1).max(1);
} else {
return Err(Error::GaveUpInsertion);
}
}
Err(e) => return Err(e.into()),
}
}
let stage = TempDir::new_in(tmp)?;
let stage_mbox = MailboxPath::from_name_and_path(
String::new(),
stage.path().to_owned(),
stage.path().to_owned(),
);
let scoped_path =
stage_mbox.scoped_data_path_for_uid_validity(uid_validity);
fs::DirBuilder::new().mode(0o750).create(&scoped_path)?;
std::os::unix::fs::symlink(
scoped_path.file_name().unwrap(),
&stage_mbox.data_path,
)?;
let mailbox_id = gen_mailbox_id();
let full_mailbox_id = self.format_mailbox_id(&mailbox_id);
let metadata = MailboxMetadata {
imap: MailboxImapMetadata {
special_use,
mailbox_id,
},
};
let metadata_toml = format!(
"# Edit this file at your own peril!\n\
# Crymap assumes it never changes.\n{}",
toml::to_string_pretty(&metadata).unwrap()
);
file_ops::spit(
tmp,
&stage_mbox.metadata_path,
false,
0o440,
metadata_toml.as_bytes(),
)?;
fs::rename(stage.into_path(), &self.base_path)
.on_exists(Error::MailboxExists)
.map_err(|e| match e {
Error::Io(e)
if Some(nix::libc::ENOTEMPTY) == e.raw_os_error() =>
{
Error::MailboxExists
}
e => e,
})?;
Ok(full_mailbox_id)
}
pub fn create_if_nx(&self, tmp: &Path) -> Result<(), Error> {
if !self.exists() {
match self.create(tmp, None) {
Err(Error::MailboxExists) => Ok(()),
Err(e) => Err(e),
Ok(_) => Ok(()),
}
} else {
Ok(())
}
}
pub fn delete(&self, garbage: &Path) -> Result<(), Error> {
if &self.name == "INBOX" {
return Err(Error::BadOperationOnInbox);
}
if self.children().next().is_none() {
file_ops::delete_async(&self.base_path, garbage)
.on_not_found(Error::NxMailbox)
} else {
let selectable = match self.scoped_data_path().and_then(|p| {
file_ops::delete_async(p, garbage).map_err(|e| e.into())
}) {
Ok(()) => true,
Err(Error::MailboxUnselectable) => false,
Err(Error::Io(e)) if io::ErrorKind::NotFound == e.kind() => {
false
}
Err(e) => return Err(e),
};
let _ = fs::remove_file(&self.data_path);
if selectable {
Ok(())
} else {
Err(Error::MailboxHasInferiors)
}
}
}
pub fn rename(&self, dst: &MailboxPath, tmp: &Path) -> Result<(), Error> {
fs::rename(&self.base_path, &dst.base_path)
.on_not_found(Error::NxMailbox)
.on_exists(Error::MailboxExists)
.map_err(|e| match e {
Error::Io(e)
if Some(nix::libc::ENOTEMPTY) == e.raw_os_error() =>
{
Error::MailboxExists
}
e => e,
})?;
if &self.name == "INBOX" {
match self.create(tmp, None) {
Ok(_) => (),
Err(Error::MailboxExists) => (),
Err(e) => return Err(e),
}
}
Ok(())
}
pub fn metadata(&self) -> Result<MailboxMetadata, Error> {
if !self.exists() {
return Err(Error::NxMailbox);
}
let mut reader = fs::File::open(&self.metadata_path)
.on_not_found(Error::MailboxUnselectable)?;
let mut data = Vec::new();
reader.read_to_end(&mut data)?;
Ok(toml::from_slice(&data)?)
}
pub fn mailbox_id(&self) -> Result<String, Error> {
let base_id = self.metadata()?.imap.mailbox_id;
Ok(self.format_mailbox_id(&base_id))
}
fn format_mailbox_id(&self, base_id: &str) -> String {
if "INBOX" == self.name {
format!("I{}", base_id)
} else {
format!("M{}", base_id)
}
}
pub fn subscribe(&self) -> Result<(), Error> {
fs::DirBuilder::new()
.mode(0o700)
.recursive(true)
.create(&self.shadow_path)
.ignore_already_exists()?;
fs::OpenOptions::new()
.create(true)
.write(true)
.mode(0o600)
.open(&self.sub_path)
.map(|_| ())?;
Ok(())
}
pub fn unsubscribe(&self) -> Result<(), Error> {
Ok(fs::remove_file(&self.sub_path).ignore_not_found()?)
}
pub fn list(
&self,
dst: &mut Vec<ListResponse>,
request: &ListRequest,
matcher: &impl Fn(&str) -> bool,
) -> ChildListResult {
let mut self_result = ChildListResult::default();
let selectable = self.is_selectable();
self_result.exists = self.exists();
let self_matches = matcher(&self.name);
let subscribed = (request.select_subscribed
|| request.return_subscribed)
&& self.is_subscribed();
let special_use =
if request.select_special_use || request.return_special_use {
self.metadata().ok().and_then(|md| md.imap.special_use)
} else {
None
};
let mut self_selected = (!request.select_subscribed || subscribed)
&& (!request.select_special_use || special_use.is_some());
let mut has_children = false;
let children = self.children_impl(if request.select_subscribed {
&self.shadow_path
} else {
&self.base_path
});
for child in children {
let child_result = child.list(dst, request, matcher);
self_result.selected_subscribe |= child_result.selected_subscribe;
self_result.selected_special_use |=
child_result.selected_special_use;
self_result.unmatched_but_selected |=
child_result.unmatched_but_selected;
has_children |= child_result.exists;
}
self_selected &= request.select_subscribed || self_result.exists;
if self_matches
&& (self_selected
|| (request.recursive_match
&& self_result.unmatched_but_selected))
{
let mut info = ListResponse {
name: self.name.clone(),
..ListResponse::default()
};
if !self_selected && request.lsub_style {
info.attributes.push(MailboxAttribute::Noselect);
} else if !self_result.exists {
if !request.lsub_style {
info.attributes.push(MailboxAttribute::NonExistent);
}
} else if !selectable {
if !request.lsub_style {
info.attributes.push(MailboxAttribute::Noselect);
}
}
if !self.allows_children() {
info.attributes.push(MailboxAttribute::Noinferiors);
}
if subscribed && request.return_subscribed {
info.attributes.push(MailboxAttribute::Subscribed);
}
if request.return_children {
if has_children {
info.attributes.push(MailboxAttribute::HasChildren);
} else if self.allows_children() {
info.attributes.push(MailboxAttribute::HasNoChildren);
}
}
if request.return_special_use {
if let Some(special_use) = special_use {
info.attributes.push(special_use);
}
}
if request.recursive_match {
if self_result.selected_special_use {
info.child_info.push("SPECIAL-USE");
}
if self_result.selected_subscribe {
info.child_info.push("SUBSCRIBED");
}
self_result.unmatched_but_selected = false;
}
dst.push(info);
}
self_result.selected_special_use |=
request.select_special_use && special_use.is_some();
self_result.selected_subscribe |=
request.select_subscribed && subscribed;
if !self_matches && self_selected {
self_result.unmatched_but_selected = true;
}
self_result
}
}
#[derive(Clone, Copy, Default)]
pub struct ChildListResult {
selected_subscribe: bool,
selected_special_use: bool,
unmatched_but_selected: bool,
exists: bool,
}
pub fn parse_mailbox_path<'a>(
path: &'a str,
) -> impl Iterator<Item = &'a str> + 'a {
path.split('/')
.filter(|s| !s.is_empty())
.enumerate()
.map(|(ix, s)| {
if 0 == ix && "inbox".eq_ignore_ascii_case(s) {
"INBOX"
} else {
s
}
})
}
pub fn mailbox_path_matcher<'a>(
patterns: impl IntoIterator<Item = &'a str>,
) -> impl Fn(&str) -> bool + 'a {
let mut rx = "^(".to_owned();
for (pattern_ix, pattern) in patterns.into_iter().enumerate() {
if pattern_ix > 0 {
rx.push('|');
}
for (part_ix, part) in parse_mailbox_path(pattern).enumerate() {
if part_ix > 0 {
rx.push('/');
}
let mut start = 0;
for end in part
.match_indices(|c| '%' == c || '*' == c)
.map(|(ix, _)| ix)
.chain(part.len()..=part.len())
{
let chunk = &part[start..end];
start = (end + 1).min(part.len());
rx.push_str(®ex::escape(chunk));
match part.get(end..end + 1) {
Some("*") => rx.push_str(".*"),
Some("%") => rx.push_str("[^/]*"),
_ => (),
}
}
}
}
rx.push_str(")$");
let rx = regex::Regex::new(&rx).expect("Built invalid regex?");
move |s| rx.is_match(s)
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct MailboxMetadata {
pub imap: MailboxImapMetadata,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct MailboxImapMetadata {
pub special_use: Option<MailboxAttribute>,
pub mailbox_id: String,
}
pub fn parse_uid_validity(path: impl AsRef<Path>) -> Result<u32, Error> {
let path = path.as_ref();
let name = path
.file_name()
.ok_or(Error::CorruptFileLayout)?
.to_str()
.ok_or(Error::CorruptFileLayout)?;
if !name.starts_with('%') {
return Err(Error::CorruptFileLayout);
}
u32::from_str_radix(&name[1..], 16).map_err(|_| Error::CorruptFileLayout)
}
fn gen_mailbox_id() -> String {
let data: [u8; 15] = OsRng.gen();
base64::encode_config(&data, base64::URL_SAFE)
}
#[cfg(test)]
mod test {
use std::iter;
use super::*;
#[test]
fn test_parse_mailbox_path() {
fn p(p: &'static str) -> Vec<&'static str> {
parse_mailbox_path(p).collect()
}
assert_eq!(vec!["INBOX"], p("inbox"));
assert_eq!(vec!["INBOX", "foo"], p("Inbox/foo"));
assert_eq!(vec!["bar"], p("/bar"));
assert_eq!(vec!["bar"], p("bar/"));
assert_eq!(vec!["foo", "bar"], p("foo//bar"));
assert_eq!(vec!["foo", "InBoX"], p("foo/InBoX"));
}
#[test]
fn test_mailbox_patterns() {
fn matches(pat: &str, mb: &str) -> bool {
mailbox_path_matcher(iter::once(pat))(mb)
}
assert!(matches("*", "INBOX"));
assert!(matches("%", "INBOX"));
assert!(matches("INB*X", "INBOX"));
assert!(matches("INB*X", "INB/BOX"));
assert!(!matches("INB*X", "INBOX/plugh"));
assert!(!matches("INB*X", "foo/INBOX"));
assert!(matches("INB%X", "INBOX"));
assert!(!matches("INB%X", "INB/BOX"));
assert!(!matches("INB%X", "INBOX/plugh"));
assert!(matches("INB*", "INBOX"));
assert!(matches("INB*", "INBOX/plugh"));
assert!(matches("INB%", "INBOX"));
assert!(!matches("INB%", "INBOX/plugh"));
assert!(!matches("INB%", "foo/INBOX"));
assert!(matches("*X", "INBOX"));
assert!(matches("*X", "foo/boX"));
assert!(matches("%X", "INBOX"));
assert!(!matches("%X", "foo/boX"));
assert!(matches("foo/bar", "foo/bar"));
assert!(!matches("foo/bar", "foo/bar/baz"));
assert!(!matches("foo/*", "foo"));
assert!(matches("foo/*", "foo/bar"));
assert!(matches("foo/*", "foo/bar/baz"));
assert!(matches("foo/%", "foo/bar"));
assert!(!matches("foo/%", "foo/bar/baz"));
assert!(matches("inbox", "INBOX"));
}
}