pub mod config;
use async_trait::async_trait;
use imap::{
extensions::{
idle::{stop_on_any, SetReadTimeout},
sort::SortCharset,
},
Authenticator, Client, Session,
};
use imap_proto::{NameAttribute, UidSetMember};
use log::{debug, error, info, log_enabled, warn, Level};
use once_cell::sync::Lazy;
use process::Cmd;
use rustls::{
client::{ServerCertVerified, ServerCertVerifier},
Certificate, ClientConfig, ClientConnection, RootCertStore, StreamOwned,
};
use std::{
any::Any,
collections::HashSet,
io::{self, Read, Write},
net::TcpStream,
result, string,
time::Duration,
};
use thiserror::Error;
use utf7_imap::{decode_utf7_imap as decode_utf7, encode_utf7_imap as encode_utf7};
use crate::{
account::{AccountConfig, OAuth2Method},
backend::Backend,
email::{envelope, Envelope, Envelopes, Flag, Flags, Messages},
folder::{Folder, Folders},
Result,
};
#[doc(inline)]
pub use self::config::{ImapAuthConfig, ImapConfig};
#[derive(Error, Debug)]
pub enum Error {
#[error("cannot create imap backend: imap not initialized")]
InitError,
#[error("cannot create imap folder {1}")]
CreateFolderError(#[source] imap::Error, String),
#[error("cannot select imap folder {1}")]
SelectFolderError(#[source] imap::Error, String),
#[error("cannot list imap folders")]
ListFoldersError(#[source] imap::Error),
#[error("cannot examine folder {1}")]
ExamineFolderError(#[source] imap::Error, String),
#[error("cannot expunge imap folder {1}")]
ExpungeFolderError(#[source] imap::Error, String),
#[error("cannot delete imap folder {1}")]
DeleteFolderError(#[source] imap::Error, String),
#[error("cannot parse headers of imap email {0}")]
ParseHeadersOfFetchError(String),
#[error("cannot get imap envelope of email {0}")]
GetEnvelopeError(String),
#[error("cannot list imap envelopes: page {0} out of bounds")]
BuildPageRangeOutOfBoundsError(usize),
#[error("cannot fetch new imap envelopes")]
FetchNewEnvelopesError(#[source] imap::Error),
#[error("cannot search new imap envelopes")]
SearchNewEnvelopesError(#[source] imap::Error),
#[error("cannot search imap envelopes in folder {1} with query: {2}")]
SearchEnvelopesError(#[source] imap::Error, String, String),
#[error("cannot sort imap envelopes in folder {1} with query: {2}")]
SortEnvelopesError(#[source] imap::Error, String, String),
#[error("cannot get next imap envelope uid of folder {0}")]
GetNextEnvelopeUidError(String),
#[error("cannot parse imap header date {0}")]
ParseHeaderDateError(String),
#[error("cannot add flags {1} to imap email(s) {2}")]
AddFlagsError(#[source] imap::Error, String, String),
#[error("cannot set flags {1} to emails(s) {2}")]
SetFlagsError(#[source] imap::Error, String, String),
#[error("cannot remove flags {1} from email(s) {2}")]
RemoveFlagsError(#[source] imap::Error, String, String),
#[error("cannot copy imap email(s) {1} from {2} to {3}")]
CopyEmailError(#[source] imap::Error, String, String, String),
#[error("cannot move email(s) {1} from {2} to {3}")]
MoveEmailError(#[source] imap::Error, String, String, String),
#[error("cannot fetch imap email {1}")]
FetchEmailsByUidError(#[source] imap::Error, String),
#[error("cannot fetch imap emails within uid range {1}")]
FetchEmailsByUidRangeError(#[source] imap::Error, String),
#[error("cannot get added email uid from range {0}")]
GetAddedEmailUidFromRangeError(String),
#[error("cannot get added email uid (extensions UIDPLUS not enabled on the server?)")]
GetAddedEmailUidError,
#[error("cannot append email to folder {1}")]
AppendEmailError(#[source] imap::Error, String),
#[error("cannot parse sender from imap envelope")]
ParseSenderFromImapEnvelopeError,
#[error("cannot decode sender name from imap envelope")]
DecodeSenderNameFromImapEnvelopeError(rfc2047_decoder::Error),
#[error("cannot decode sender mailbox from imap envelope")]
DecodeSenderMailboxFromImapEnvelopeError(rfc2047_decoder::Error),
#[error("cannot decode sender host from imap envelope")]
DecodeSenderHostFromImapEnvelopeError(rfc2047_decoder::Error),
#[error("cannot decode date from imap envelope")]
DecodeDateFromImapEnvelopeError(rfc2047_decoder::Error),
#[error("cannot parse imap sort criterion {0}")]
ParseSortCriterionError(String),
#[error("cannot decode subject of imap email {1}")]
DecodeSubjectError(#[source] rfc2047_decoder::Error, String),
#[error("cannot get imap sender of email {0}")]
GetSenderError(String),
#[error("cannot find session from pool at cursor {0}")]
FindSessionByCursorError(usize),
#[error("cannot parse Message-ID of email {0}")]
ParseMessageIdError(#[source] string::FromUtf8Error, String),
#[error("cannot lock imap session: {0}")]
LockSessionError(String),
#[error("cannot lock imap sessions pool cursor: {0}")]
LockSessionsPoolCursorError(String),
#[error("cannot create tls connector")]
CreateTlsConnectorError(#[source] rustls::Error),
#[error("cannot connect to imap server")]
ConnectError(#[source] imap::Error),
#[error("cannot login to imap server")]
LoginError(#[source] imap::Error),
#[error("cannot authenticate to imap server")]
AuthenticateError(#[source] imap::Error),
#[error("cannot start the idle mode")]
StartIdleModeError(#[source] imap::Error),
#[error("cannot close imap session")]
CloseError(#[source] imap::Error),
#[error("cannot get imap password from global keyring")]
GetPasswdError(#[source] secret::Error),
#[error("cannot get imap password: password is empty")]
GetPasswdEmptyError,
}
const ENVELOPE_QUERY: &str =
"(UID FLAGS BODY.PEEK[HEADER.FIELDS (MESSAGE-ID FROM TO SUBJECT DATE)])";
const ROOT_CERT_STORE: Lazy<RootCertStore> = Lazy::new(|| {
let mut store = RootCertStore::empty();
for cert in rustls_native_certs::load_native_certs().unwrap() {
store.add(&Certificate(cert.0)).unwrap();
}
store
});
type ImapSession = Session<ImapSessionStream>;
type TlsStream = StreamOwned<ClientConnection, TcpStream>;
enum ImapSessionStream {
Tls(TlsStream),
Tcp(TcpStream),
}
impl SetReadTimeout for ImapSessionStream {
fn set_read_timeout(&mut self, timeout: Option<Duration>) -> imap::Result<()> {
match self {
Self::Tls(stream) => Ok(stream.get_mut().set_read_timeout(timeout)?),
Self::Tcp(stream) => stream.set_read_timeout(timeout),
}
}
}
impl Read for ImapSessionStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
Self::Tls(stream) => stream.read(buf),
Self::Tcp(stream) => stream.read(buf),
}
}
}
impl Write for ImapSessionStream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
Self::Tls(stream) => stream.write(buf),
Self::Tcp(stream) => stream.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Self::Tls(stream) => stream.flush(),
Self::Tcp(stream) => stream.flush(),
}
}
}
struct XOAuth2 {
user: String,
access_token: String,
}
impl XOAuth2 {
pub fn new(user: String, access_token: String) -> Self {
Self { user, access_token }
}
}
impl Authenticator for XOAuth2 {
type Response = String;
fn process(&self, _: &[u8]) -> Self::Response {
format!(
"user={}\x01auth=Bearer {}\x01\x01",
self.user, self.access_token
)
}
}
struct OAuthBearer {
user: String,
host: String,
port: u16,
access_token: String,
}
impl OAuthBearer {
pub fn new(user: String, host: String, port: u16, access_token: String) -> Self {
Self {
user,
host,
port,
access_token,
}
}
}
impl Authenticator for OAuthBearer {
type Response = String;
fn process(&self, _: &[u8]) -> Self::Response {
format!(
"n,a={},\x01host={}\x01port={}\x01auth=Bearer {}\x01\x01",
self.user, self.host, self.port, self.access_token
)
}
}
pub struct ImapBackend {
account_config: AccountConfig,
imap_config: ImapConfig,
session: ImapSession,
}
impl ImapBackend {
pub async fn new(
account_config: AccountConfig,
imap_config: ImapConfig,
default_credentials: Option<String>,
) -> Result<ImapBackend> {
let session = match &imap_config.auth {
ImapAuthConfig::Passwd(_) => {
Self::build_session(&imap_config, default_credentials.clone()).await
}
ImapAuthConfig::OAuth2(oauth2_config) => {
match Self::build_session(&imap_config, default_credentials.clone()).await {
Ok(sess) => Ok(sess),
Err(err) => match err {
crate::Error::ImapError(Error::AuthenticateError(imap::Error::Parse(
imap::error::ParseError::Authentication(_, _),
))) => {
warn!("error while authenticating user, refreshing access token");
oauth2_config.refresh_access_token().await?;
Self::build_session(&imap_config, None).await
}
err => Err(err),
},
}
}
}?;
Ok(Self {
account_config,
imap_config,
session,
})
}
async fn build_session(
imap_config: &ImapConfig,
credentials: Option<String>,
) -> Result<ImapSession> {
let mut session = match &imap_config.auth {
ImapAuthConfig::Passwd(passwd) => {
debug!("creating session using login and password");
let passwd = match credentials {
Some(passwd) => passwd,
None => passwd
.get()
.await
.map_err(Error::GetPasswdError)?
.lines()
.next()
.ok_or_else(|| Error::GetPasswdEmptyError)?
.to_owned(),
};
Self::build_client(imap_config)?
.login(&imap_config.login, passwd)
.map_err(|res| Error::LoginError(res.0))
}
ImapAuthConfig::OAuth2(oauth2_config) => {
let access_token = match credentials {
Some(access_token) => access_token,
None => oauth2_config.access_token().await?,
};
match oauth2_config.method {
OAuth2Method::XOAuth2 => {
debug!("creating session using xoauth2");
let xoauth2 = XOAuth2::new(imap_config.login.clone(), access_token);
Self::build_client(imap_config)?
.authenticate("XOAUTH2", &xoauth2)
.map_err(|(err, _client)| Error::AuthenticateError(err))
}
OAuth2Method::OAuthBearer => {
debug!("creating session using oauthbearer");
let bearer = OAuthBearer::new(
imap_config.login.clone(),
imap_config.host.clone(),
imap_config.port,
access_token,
);
Self::build_client(imap_config)?
.authenticate("OAUTHBEARER", &bearer)
.map_err(|(err, _client)| Error::AuthenticateError(err))
}
}
}
}?;
session.debug = log_enabled!(Level::Trace);
Ok(session)
}
fn build_client(imap_config: &ImapConfig) -> Result<Client<ImapSessionStream>> {
let mut client_builder = imap::ClientBuilder::new(&imap_config.host, imap_config.port);
if imap_config.starttls() {
client_builder.starttls();
}
let client = if imap_config.ssl() {
client_builder.connect(Self::tls_handshake(imap_config)?)
} else {
client_builder.connect(Self::tcp_handshake()?)
}
.map_err(Error::ConnectError)?;
Ok(client)
}
fn tcp_handshake() -> Result<Box<dyn FnOnce(&str, TcpStream) -> imap::Result<ImapSessionStream>>>
{
Ok(Box::new(|_domain, tcp| Ok(ImapSessionStream::Tcp(tcp))))
}
fn tls_handshake(
imap_config: &ImapConfig,
) -> Result<Box<dyn FnOnce(&str, TcpStream) -> imap::Result<ImapSessionStream>>> {
use rustls::client::WebPkiVerifier;
use std::sync::Arc;
struct DummyCertVerifier;
impl ServerCertVerifier for DummyCertVerifier {
fn verify_server_cert(
&self,
_end_entity: &Certificate,
_intermediates: &[Certificate],
_server_name: &rustls::ServerName,
_scts: &mut dyn Iterator<Item = &[u8]>,
_ocsp_response: &[u8],
_now: std::time::SystemTime,
) -> result::Result<rustls::client::ServerCertVerified, rustls::Error> {
Ok(ServerCertVerified::assertion())
}
fn request_scts(&self) -> bool {
false
}
}
let tlsconfig = ClientConfig::builder().with_safe_defaults();
let tlsconfig = if imap_config.insecure() {
tlsconfig.with_custom_certificate_verifier(Arc::new(DummyCertVerifier))
} else {
let verifier = WebPkiVerifier::new(ROOT_CERT_STORE.clone(), None);
tlsconfig.with_custom_certificate_verifier(Arc::new(verifier))
}
.with_no_client_auth();
let tlsconfig = Arc::new(tlsconfig);
Ok(Box::new(|domain, tcp| {
let name = rustls::ServerName::try_from(domain).map_err(|err| {
imap::Error::Io(io::Error::new(
io::ErrorKind::InvalidData,
format!("Invalid domain name ({:?}): {}", err, domain),
))
})?;
let connection = ClientConnection::new(tlsconfig, name)
.map_err(|err| io::Error::new(io::ErrorKind::ConnectionAborted, err))?;
let stream = StreamOwned::new(connection, tcp);
Ok(ImapSessionStream::Tls(stream))
}))
}
async fn with_session<T>(
&mut self,
action: impl Fn(&mut ImapSession) -> imap::Result<T>,
map_err: impl Fn(imap::Error) -> Error,
) -> Result<T> {
match &self.imap_config.auth {
ImapAuthConfig::Passwd(_) => {
action(&mut self.session).or_else(|err| Ok(Err(map_err(err))?))
}
ImapAuthConfig::OAuth2(oauth2_config) => match action(&mut self.session) {
Ok(res) => Ok(res),
Err(err) => match err {
imap::Error::Parse(imap::error::ParseError::Authentication(_, _)) => {
warn!("error while authenticating user, refreshing access token");
oauth2_config.refresh_access_token().await?;
self.session = Self::build_session(&self.imap_config, None).await?;
action(&mut self.session).or_else(|err| Ok(Err(map_err(err))?))
}
err => Ok(Err(map_err(err))?),
},
},
}
}
async fn search_new_envelopes(&mut self) -> Result<Vec<u32>> {
let query = self.imap_config.notify_query();
let uids: Vec<u32> = self
.with_session(
|session| session.uid_search(&query),
Error::SearchNewEnvelopesError,
)
.await?
.into_iter()
.collect();
debug!("found {} new envelopes", uids.len());
debug!("uids: {:?}", uids);
Ok(uids)
}
pub async fn notify(&mut self, keepalive: u64, folder: &str) -> Result<()> {
info!("notifying imap folder {folder}");
let folder = self.account_config.get_folder_alias(folder)?;
let folder_encoded = encode_utf7(folder.clone());
debug!("utf7 encoded folder: {folder_encoded}");
self.with_session(
|session| session.examine(&folder_encoded),
|err| Error::ExamineFolderError(err, folder.to_owned()),
)
.await?;
debug!("init messages hashset");
let mut msgs_set: HashSet<u32> = self
.search_new_envelopes()
.await?
.iter()
.cloned()
.collect::<HashSet<_>>();
debug!("messages hashset: {:?}", msgs_set);
loop {
debug!("begin loop");
self.with_session(
|session| {
session
.idle()
.timeout(Duration::new(keepalive, 0))
.wait_while(stop_on_any)
},
Error::StartIdleModeError,
)
.await?;
let uids: Vec<u32> = self
.search_new_envelopes()
.await?
.into_iter()
.filter(|uid| msgs_set.get(uid).is_none())
.collect();
debug!("found {} new messages not in hashset", uids.len());
debug!("messages hashet: {:?}", msgs_set);
if !uids.is_empty() {
let uids = uids
.iter()
.map(|uid| uid.to_string())
.collect::<Vec<_>>()
.join(",");
let fetches = self
.with_session(
|session| session.uid_fetch(&uids, ENVELOPE_QUERY),
Error::FetchNewEnvelopesError,
)
.await?;
for fetch in fetches.iter() {
let envelope = Envelope::from_imap_fetch(fetch)?;
let uid = fetch.uid.expect("UID should be included in the IMAP fetch");
let from = envelope.from.addr.clone();
self.imap_config
.run_notify_cmd(uid, &envelope.subject, &from)
.await?;
debug!("notify message: {}", uid);
debug!("inserting message {} in hashset", uid);
msgs_set.insert(uid);
debug!("messages hashset: {:?}", msgs_set);
}
}
debug!("end loop");
}
}
pub async fn watch(&mut self, keepalive: u64, folder: &str) -> Result<()> {
info!("watching imap folder {folder}");
let folder = self.account_config.get_folder_alias(folder)?;
let folder_encoded = encode_utf7(folder.clone());
debug!("utf7 encoded folder: {folder_encoded}");
self.with_session(
|session| session.examine(&folder_encoded),
|err| Error::ExamineFolderError(err, folder.clone()),
)
.await?;
loop {
debug!("begin loop");
for (i, cmd) in self.imap_config.watch_cmds().iter().enumerate() {
debug!("running watch command {}: {cmd}", i + 1);
match Cmd::from(cmd.clone()).run().await {
Ok(_) => {
debug!("watch command {} successfully executed", i + 1);
}
Err(err) => {
warn!("error while running command {cmd}, skipping it");
warn!("{err}")
}
}
}
self.with_session(
|session| {
session
.idle()
.timeout(Duration::new(keepalive, 0))
.wait_while(stop_on_any)
},
Error::StartIdleModeError,
)
.await?;
debug!("end loop");
}
}
}
#[async_trait]
impl Backend for ImapBackend {
fn name(&self) -> String {
self.account_config.name.clone()
}
async fn add_folder(&mut self, folder: &str) -> Result<()> {
info!("adding imap folder {folder}");
let folder = self.account_config.get_folder_alias(folder)?;
let folder_encoded = encode_utf7(folder.clone());
debug!("utf7 encoded folder: {folder_encoded}");
self.with_session(
|session| session.create(&folder_encoded),
|err| Error::CreateFolderError(err, folder.clone()),
)
.await?;
Ok(())
}
async fn list_folders(&mut self) -> Result<Folders> {
info!("listing imap folders");
let folders = self
.with_session(
|session| session.list(Some(""), Some("*")),
Error::ListFoldersError,
)
.await?;
let folders = Folders::from_iter(folders.iter().filter_map(|folder| {
if folder.attributes().contains(&NameAttribute::NoSelect) {
None
} else {
Some(Folder {
name: decode_utf7(folder.name().into()),
desc: folder
.attributes()
.iter()
.map(|attr| format!("{attr:?}"))
.collect::<Vec<_>>()
.join(", "),
})
}
}));
debug!("imap folders: {folders:#?}");
Ok(folders)
}
async fn expunge_folder(&mut self, folder: &str) -> Result<()> {
info!("expunging imap folder {folder}");
let folder = self.account_config.get_folder_alias(folder)?;
let folder_encoded = encode_utf7(folder.clone());
debug!("utf7 encoded folder: {folder_encoded}");
self.with_session(
|session| session.select(&folder_encoded),
|err| Error::SelectFolderError(err, folder.clone()),
)
.await?;
self.with_session(
|session| session.expunge(),
|err| Error::ExpungeFolderError(err, folder.clone()),
)
.await?;
Ok(())
}
async fn purge_folder(&mut self, folder: &str) -> Result<()> {
info!("purging imap folder {folder}");
let folder = self.account_config.get_folder_alias(folder)?;
let folder_encoded = encode_utf7(folder.clone());
debug!("utf7 encoded folder: {folder_encoded}");
let flags = Flags::from_iter([Flag::Deleted]);
let uids = String::from("1:*");
self.with_session(
|session| session.select(&folder_encoded),
|err| Error::SelectFolderError(err, folder.clone()),
)
.await?;
self.with_session(
|session| {
session.uid_store(&uids, format!("+FLAGS ({})", flags.to_imap_query_string()))
},
|err| Error::AddFlagsError(err, flags.to_imap_query_string(), uids.clone()),
)
.await?;
self.with_session(
|session| session.expunge(),
|err| Error::ExpungeFolderError(err, folder.clone()),
)
.await?;
Ok(())
}
async fn delete_folder(&mut self, folder: &str) -> Result<()> {
info!("deleting imap folder {folder}");
let folder = self.account_config.get_folder_alias(folder)?;
let folder_encoded = encode_utf7(folder.clone());
debug!("utf7 encoded folder: {folder_encoded}");
self.with_session(
|session| session.delete(&folder_encoded),
|err| Error::DeleteFolderError(err, folder.clone()),
)
.await?;
Ok(())
}
async fn get_envelope(&mut self, folder: &str, uid: &str) -> Result<Envelope> {
info!("getting imap envelope {uid} from folder {folder}");
let folder = self.account_config.get_folder_alias(folder)?;
let folder_encoded = encode_utf7(folder.clone());
debug!("utf7 encoded folder: {folder_encoded}");
self.with_session(
|session| session.select(&folder_encoded),
|err| Error::SelectFolderError(err, folder.clone()),
)
.await?;
let fetches = self
.with_session(
|session| session.uid_fetch(uid, ENVELOPE_QUERY),
|err| Error::FetchEmailsByUidError(err, uid.to_owned()),
)
.await?;
let fetch = fetches
.get(0)
.ok_or_else(|| Error::GetEnvelopeError(uid.to_owned()))?;
let envelope = Envelope::from_imap_fetch(fetch)?;
debug!("imap envelope: {envelope:#?}");
Ok(envelope)
}
async fn list_envelopes(
&mut self,
folder: &str,
page_size: usize,
page: usize,
) -> Result<Envelopes> {
info!("listing imap envelopes from folder {folder}");
let folder = self.account_config.get_folder_alias(folder)?;
let folder_encoded = encode_utf7(folder.clone());
debug!("utf7 encoded folder: {folder_encoded}");
let folder_size = self
.with_session(
|session| session.select(&folder_encoded),
|err| Error::SelectFolderError(err, folder.clone()),
)
.await?
.exists as usize;
debug!("folder size: {folder_size}");
if folder_size == 0 {
return Ok(Envelopes::default());
}
let range = build_page_range(page, page_size, folder_size)?;
debug!("page range: {range}");
let fetches = self
.with_session(
|session| session.fetch(&range, ENVELOPE_QUERY),
|err| Error::FetchEmailsByUidRangeError(err, range.clone()),
)
.await?;
let envelopes = Envelopes::from_imap_fetches(fetches);
debug!("imap envelopes: {envelopes:#?}");
Ok(envelopes)
}
async fn search_envelopes(
&mut self,
folder: &str,
query: &str,
sort: &str,
page_size: usize,
page: usize,
) -> Result<Envelopes> {
info!("searching imap envelopes from folder {folder}");
let folder = self.account_config.get_folder_alias(folder)?;
let folder_encoded = encode_utf7(folder.clone());
debug!("utf7 encoded folder: {folder_encoded}");
let folder_size = self
.with_session(
|session| session.select(&folder_encoded),
|err| Error::SelectFolderError(err, folder.clone()),
)
.await?
.exists as usize;
debug!("folder size: {folder_size}");
if folder_size == 0 {
return Ok(Envelopes::default());
}
let uids: Vec<String> = if sort.is_empty() {
self.with_session(
|session| session.uid_search(query),
|err| Error::SearchEnvelopesError(err, folder.clone(), query.to_owned()),
)
.await?
.iter()
.map(ToString::to_string)
.collect()
} else {
let sort: envelope::imap::SortCriteria = sort.parse()?;
self.with_session(
|session| session.uid_sort(&sort, SortCharset::Utf8, query),
|err| Error::SortEnvelopesError(err, folder.clone(), query.to_owned()),
)
.await?
.iter()
.map(ToString::to_string)
.collect()
};
debug!("uids: {uids:?}");
if uids.is_empty() {
return Ok(Envelopes::default());
}
let uid_range = if page_size > 0 {
let begin = uids.len().min(page * page_size);
let end = begin + uids.len().min(page_size);
if end > begin + 1 {
uids[begin..end].join(",")
} else {
uids[0].to_string()
}
} else {
uids.join(",")
};
debug!("page: {page}");
debug!("page size: {page_size}");
debug!("uid range: {uid_range}");
let fetches = self
.with_session(
|session| session.uid_fetch(&uid_range, ENVELOPE_QUERY),
|err| Error::FetchEmailsByUidRangeError(err, uid_range.clone()),
)
.await?;
let envelopes = Envelopes::from_imap_fetches(fetches);
debug!("imap envelopes: {envelopes:#?}");
Ok(envelopes)
}
async fn add_email(&mut self, folder: &str, email: &[u8], flags: &Flags) -> Result<String> {
info!(
"adding imap email to folder {folder} with flags {flags}",
flags = flags.to_string(),
);
let folder = self.account_config.get_folder_alias(folder)?;
let folder_encoded = encode_utf7(folder.clone());
debug!("utf7 encoded folder: {folder_encoded}");
let appended = self
.with_session(
|session| {
session
.append(&folder, email)
.flags(flags.to_imap_flags_vec())
.finish()
},
|err| Error::AppendEmailError(err, folder.clone()),
)
.await?;
let uid = match appended.uids {
Some(mut uids) if uids.len() == 1 => match uids.get_mut(0).unwrap() {
UidSetMember::Uid(uid) => Ok(*uid),
UidSetMember::UidRange(uids) => Ok(uids.next().ok_or_else(|| {
Error::GetAddedEmailUidFromRangeError(uids.fold(String::new(), |range, uid| {
if range.is_empty() {
uid.to_string()
} else {
range + ", " + &uid.to_string()
}
}))
})?),
},
_ => {
Err(Error::GetAddedEmailUidError)
}
}?;
debug!("uid: {uid}");
Ok(uid.to_string())
}
async fn preview_emails(&mut self, folder: &str, uids: Vec<&str>) -> Result<Messages> {
let uids = uids.join(",");
info!("previewing imap emails {uids} from folder {folder}");
let folder = self.account_config.get_folder_alias(folder)?;
let folder_encoded = encode_utf7(folder.clone());
debug!("utf7 encoded folder: {folder_encoded}");
self.with_session(
|session| session.select(&folder_encoded),
|err| Error::SelectFolderError(err, folder.clone()),
)
.await?;
let fetches = self
.with_session(
|session| session.uid_fetch(&uids, "BODY.PEEK[]"),
|err| Error::FetchEmailsByUidRangeError(err, uids.clone()),
)
.await?;
Ok(Messages::try_from(fetches)?)
}
async fn get_emails(&mut self, folder: &str, uids: Vec<&str>) -> Result<Messages> {
let uids = uids.join(",");
info!("getting imap emails {uids} from folder {folder}");
let folder = self.account_config.get_folder_alias(folder)?;
let folder_encoded = encode_utf7(folder.clone());
debug!("utf7 encoded folder: {folder_encoded}");
self.with_session(
|session| session.select(&folder_encoded),
|err| Error::SelectFolderError(err, folder.clone()),
)
.await?;
let fetches = self
.with_session(
|session| session.uid_fetch(&uids, "BODY[]"),
|err| Error::FetchEmailsByUidRangeError(err, uids.clone()),
)
.await?;
Ok(Messages::try_from(fetches)?)
}
async fn copy_emails(
&mut self,
from_folder: &str,
to_folder: &str,
uids: Vec<&str>,
) -> Result<()> {
let uids = uids.join(",");
info!("copying imap emails {uids} from folder {from_folder} to folder {to_folder}");
let from_folder = self.account_config.get_folder_alias(from_folder)?;
let from_folder_encoded = encode_utf7(from_folder.clone());
debug!("utf7 encoded from folder: {from_folder_encoded}");
let to_folder = self.account_config.get_folder_alias(to_folder)?;
let to_folder_encoded = encode_utf7(to_folder.clone());
debug!("utf7 encoded to folder: {to_folder_encoded}");
self.with_session(
|session| session.select(&from_folder_encoded),
|err| Error::SelectFolderError(err, from_folder.clone()),
)
.await?;
self.with_session(
|session| session.uid_copy(&uids, &to_folder_encoded),
|err| Error::CopyEmailError(err, uids.clone(), from_folder.clone(), to_folder.clone()),
)
.await?;
Ok(())
}
async fn move_emails(
&mut self,
from_folder: &str,
to_folder: &str,
uids: Vec<&str>,
) -> Result<()> {
let uids = uids.join(",");
info!("moving imap emails {uids} from folder {from_folder} to folder {to_folder}");
let from_folder = self.account_config.get_folder_alias(from_folder)?;
let from_folder_encoded = encode_utf7(from_folder.clone());
debug!("utf7 encoded from folder: {from_folder_encoded}");
let to_folder = self.account_config.get_folder_alias(to_folder)?;
let to_folder_encoded = encode_utf7(to_folder.clone());
debug!("utf7 encoded to folder: {to_folder_encoded}");
self.with_session(
|session| session.select(&from_folder_encoded),
|err| Error::SelectFolderError(err, from_folder.clone()),
)
.await?;
self.with_session(
|session| session.uid_mv(&uids, &to_folder_encoded),
|err| Error::MoveEmailError(err, uids.clone(), from_folder.clone(), to_folder.clone()),
)
.await?;
Ok(())
}
async fn delete_emails(&mut self, folder: &str, uids: Vec<&str>) -> Result<()> {
let trash_folder = self.account_config.trash_folder_alias()?;
if self.account_config.get_folder_alias(folder)? == trash_folder {
self.mark_emails_as_deleted(folder, uids).await
} else {
self.move_emails(folder, &trash_folder, uids).await
}
}
async fn add_flags(&mut self, folder: &str, uids: Vec<&str>, flags: &Flags) -> Result<()> {
let uids = uids.join(",");
info!(
"addings flags {flags} to imap emails {uids} from folder {folder}",
flags = flags.to_string(),
);
let folder = self.account_config.get_folder_alias(folder)?;
let folder_encoded = encode_utf7(folder.clone());
debug!("utf7 encoded folder: {folder_encoded}");
self.with_session(
|session| session.select(&folder_encoded),
|err| Error::SelectFolderError(err, folder.clone()),
)
.await?;
self.with_session(
|session| {
session.uid_store(&uids, format!("+FLAGS ({})", flags.to_imap_query_string()))
},
|err| Error::AddFlagsError(err, flags.to_imap_query_string(), uids.clone()),
)
.await?;
Ok(())
}
async fn set_flags(&mut self, folder: &str, uids: Vec<&str>, flags: &Flags) -> Result<()> {
let uids = uids.join(",");
info!(
"setting flags {flags} to imap emails {uids} from folder {folder}",
flags = flags.to_string(),
);
let folder = self.account_config.get_folder_alias(folder)?;
let folder_encoded = encode_utf7(folder.clone());
debug!("utf7 encoded folder: {folder_encoded}");
self.with_session(
|session| session.select(&folder_encoded),
|err| Error::SelectFolderError(err, folder.clone()),
)
.await?;
self.with_session(
|session| session.uid_store(&uids, format!("FLAGS ({})", flags.to_imap_query_string())),
|err| Error::SetFlagsError(err, flags.to_imap_query_string(), uids.clone()),
)
.await?;
Ok(())
}
async fn remove_flags(&mut self, folder: &str, uids: Vec<&str>, flags: &Flags) -> Result<()> {
let uids = uids.join(",");
info!(
"removing flags {flags} to imap emails {uids} from folder {folder}",
flags = flags.to_string(),
);
let folder = self.account_config.get_folder_alias(folder)?;
let folder_encoded = encode_utf7(folder.clone());
debug!("utf7 encoded folder: {folder_encoded}");
self.with_session(
|session| session.select(&folder_encoded),
|err| Error::SelectFolderError(err, folder.clone()),
)
.await?;
self.with_session(
|session| {
session.uid_store(&uids, format!("-FLAGS ({})", flags.to_imap_query_string()))
},
|err| Error::RemoveFlagsError(err, flags.to_imap_query_string(), uids.clone()),
)
.await?;
Ok(())
}
fn close(&mut self) -> Result<()> {
debug!("closing imap backend session");
self.session
.check()
.and_then(|()| self.session.close())
.or(imap::Result::Ok(()))
.map_err(Error::CloseError)?;
Ok(())
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl Drop for ImapBackend {
fn drop(&mut self) {
if let Err(err) = self.close() {
warn!("cannot close imap session: {err}");
debug!("cannot close imap session: {err:?}");
}
}
}
fn build_page_range(page: usize, page_size: usize, size: usize) -> Result<String> {
let page_cursor = page * page_size;
if page_cursor >= size {
return Err(Error::BuildPageRangeOutOfBoundsError(page + 1))?;
}
let range = if page_size == 0 {
String::from("1:*")
} else {
let page_size = page_size.min(size);
let mut count = 1;
let mut cursor = size - (size.min(page_cursor));
let mut range = cursor.to_string();
while cursor > 1 && count < page_size {
count += 1;
cursor -= 1;
if count > 1 {
range.push(',');
}
range.push_str(&cursor.to_string());
}
range
};
Ok(range)
}
#[cfg(test)]
mod tests {
#[test]
fn build_page_range_out_of_bounds() {
assert_eq!(super::build_page_range(0, 5, 5).unwrap(), "5,4,3,2,1");
assert!(matches!(
super::build_page_range(1, 5, 5).unwrap_err(),
crate::Error::ImapError(super::Error::BuildPageRangeOutOfBoundsError(2)),
));
assert!(matches!(
super::build_page_range(2, 5, 5).unwrap_err(),
crate::Error::ImapError(super::Error::BuildPageRangeOutOfBoundsError(3)),
));
}
#[test]
fn build_page_range_page_size_0() {
assert_eq!(super::build_page_range(0, 0, 3).unwrap(), "1:*");
assert_eq!(super::build_page_range(1, 0, 4).unwrap(), "1:*");
assert_eq!(super::build_page_range(2, 0, 5).unwrap(), "1:*");
}
#[test]
fn build_page_range_page_size_smaller_than_size() {
assert_eq!(super::build_page_range(0, 3, 5).unwrap(), "5,4,3");
assert_eq!(super::build_page_range(1, 3, 5).unwrap(), "2,1");
assert_eq!(super::build_page_range(1, 4, 5).unwrap(), "1");
}
#[test]
fn build_page_range_page_bigger_than_size() {
assert_eq!(super::build_page_range(0, 10, 5).unwrap(), "5,4,3,2,1");
}
}