1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
pub mod config;
use async_trait::async_trait;
use log::info;
use maildirpp::Maildir;
use shellexpand_utils::{shellexpand_path, try_shellexpand_path};
use std::{
io,
ops::Deref,
path::{self, PathBuf},
sync::Arc,
};
use thiserror::Error;
use tokio::sync::Mutex;
use crate::{
account::config::AccountConfig, backend::BackendContextBuilder, folder::FolderKind, maildir,
Result,
};
use self::config::MaildirConfig;
#[derive(Debug, Error)]
pub enum Error {
#[error("maildir: cannot init folders structure at {1}")]
InitFoldersStructureError(#[source] maildirpp::Error, PathBuf),
#[error("maildir: cannot read folder: invalid path {0}")]
ReadFolderInvalidError(path::PathBuf),
#[error("maildir: cannot get current folder")]
GetCurrentFolderError(#[source] io::Error),
}
/// The Maildir session builder.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct MaildirSessionBuilder {
/// The account configuration.
pub account_config: AccountConfig,
/// The Maildir configuration.
pub maildir_config: MaildirConfig,
}
impl MaildirSessionBuilder {
pub fn new(account_config: AccountConfig, maildir_config: MaildirConfig) -> Self {
Self {
account_config,
maildir_config,
}
}
}
#[async_trait]
impl BackendContextBuilder for MaildirSessionBuilder {
type Context = MaildirSessionSync;
/// Build an IMAP sync session.
///
/// The IMAP session is created at this moment. If the session
/// cannot be created using the OAuth 2.0 authentication, the
/// access token is refreshed first then a new session is created.
async fn build(self) -> Result<Self::Context> {
info!("building new maildir session");
let path = shellexpand_path(&self.maildir_config.root_dir);
let session = MaildirSession {
account_config: self.account_config.clone(),
maildir_config: self.maildir_config.clone(),
session: Maildir::from(path),
};
session.create_dirs()?;
Ok(MaildirSessionSync {
account_config: self.account_config,
maildir_config: self.maildir_config,
session: Arc::new(Mutex::new(session)),
})
}
}
/// The Maildir session.
///
/// This session is unsync, which means it cannot be shared between
/// threads. For the sync version, see [`MaildirSessionSync`].
pub struct MaildirSession {
/// The account configuration.
pub account_config: AccountConfig,
/// The Maildir configuration.
pub maildir_config: MaildirConfig,
/// The current Maildir session.
session: Maildir,
}
impl Deref for MaildirSession {
type Target = Maildir;
fn deref(&self) -> &Self::Target {
&self.session
}
}
impl MaildirSession {
pub fn create_dirs(&self) -> Result<()> {
self.session
.create_dirs()
.map_err(|err| Error::InitFoldersStructureError(err, self.session.path().to_owned()))?;
Ok(())
}
/// Creates a maildir instance from a folder name.
pub fn get_maildir_from_folder_name(&self, folder: &str) -> Result<Maildir> {
// If the folder matches to the inbox folder kind, create a
// maildir instance from the root folder.
if FolderKind::matches_inbox(folder) {
return try_shellexpand_path(self.session.path().to_owned())
.map(Maildir::from)
.map_err(Into::into);
}
let folder = self.account_config.get_folder_alias(folder);
// If the folder is a valid maildir path, creates a maildir
// instance from it. First check for absolute path…
try_shellexpand_path(&folder)
// then check for relative path to `maildir-dir`…
.or_else(|_| try_shellexpand_path(self.session.path().join(&folder)))
// TODO: should move to CLI
// // and finally check for relative path to the current
// // directory
// .or_else(|_| {
// try_shellexpand_path(
// env::current_dir()
// .map_err(Error::GetCurrentFolderError)?
// .join(&folder),
// )
// })
.or_else(|_| {
// Otherwise creates a maildir instance from a maildir
// subdirectory by adding a "." in front of the name
// as described in the [spec].
//
// [spec]: http://www.courier-mta.org/imap/README.maildirquota.html
let folder = maildir::encode_folder(&folder);
try_shellexpand_path(self.session.path().join(format!(".{}", folder)))
})
.map(Maildir::from)
.map_err(Into::into)
}
}
/// The sync version of the Maildir session.
///
/// This is just a Maildir session wrapped into a mutex, so the same
/// Maildir session can be shared and updated across multiple threads.
#[derive(Clone)]
pub struct MaildirSessionSync {
/// The account configuration.
pub account_config: AccountConfig,
/// The MAILDIR configuration.
pub maildir_config: MaildirConfig,
/// The MAILDIR session wrapped into a mutex.
session: Arc<Mutex<MaildirSession>>,
}
impl MaildirSessionSync {
/// Create a new MAILDIR sync session from an MAILDIR session.
pub fn new(
account_config: AccountConfig,
maildir_config: MaildirConfig,
session: MaildirSession,
) -> Self {
Self {
account_config,
maildir_config,
session: Arc::new(Mutex::new(session)),
}
}
}
impl Deref for MaildirSessionSync {
type Target = Mutex<MaildirSession>;
fn deref(&self) -> &Self::Target {
&self.session
}
}
/// URL-encodes the given folder. The aim is to avoid naming
/// issues due to special characters.
pub fn encode_folder(folder: impl AsRef<str> + ToString) -> String {
urlencoding::encode(folder.as_ref()).to_string()
}
/// URL-decodes the given folder.
pub fn decode_folder(folder: impl AsRef<str> + ToString) -> String {
urlencoding::decode(folder.as_ref())
.map(|folder| folder.to_string())
.unwrap_or_else(|_| folder.to_string())
}