accounts: add default_mailbox setting #370
|
@ -217,6 +217,11 @@ Default values are shown in parentheses.
|
|||
The backend-specific path of the root_mailbox, usually
|
||||
.Sy INBOX Ns
|
||||
\&.
|
||||
.It Ic default_mailbox Ar String
|
||||
.Pq Em optional
|
||||
The mailbox that is the default to open or view for this account.
|
||||
Must be a valid mailbox path.
|
||||
If not specified, the default will be the root mailbox.
|
||||
.It Ic format Ar String Op maildir mbox imap notmuch jmap
|
||||
The format of the mail backend.
|
||||
.It Ic subscribed_mailboxes Ar [String,]
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
//! Account management from user configuration.
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{BTreeMap, HashMap, HashSet, VecDeque},
|
||||
convert::TryFrom,
|
||||
fs,
|
||||
|
@ -36,10 +35,7 @@ use std::{
|
|||
time::Duration,
|
||||
};
|
||||
|
||||
use futures::{
|
||||
future::FutureExt,
|
||||
stream::{Stream, StreamExt},
|
||||
};
|
||||
use futures::{future::FutureExt, stream::StreamExt};
|
||||
use indexmap::IndexMap;
|
||||
use melib::{
|
||||
backends::*,
|
||||
|
@ -62,6 +58,11 @@ use crate::{
|
|||
};
|
||||
|
||||
mod backend_ops;
|
||||
mod jobs;
|
||||
mod mailbox;
|
||||
|
||||
pub use jobs::*;
|
||||
pub use mailbox::*;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! try_recv_timeout {
|
||||
|
@ -79,6 +80,7 @@ macro_rules! try_recv_timeout {
|
|||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! is_variant {
|
||||
($n:ident, $($var:tt)+) => {
|
||||
#[inline]
|
||||
|
@ -88,86 +90,6 @@ macro_rules! is_variant {
|
|||
};
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub enum MailboxStatus {
|
||||
Available,
|
||||
Failed(Error),
|
||||
/// first argument is done work, and second is total work
|
||||
Parsing(usize, usize),
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
impl MailboxStatus {
|
||||
is_variant! { is_available, Available }
|
||||
is_variant! { is_parsing, Parsing(_, _) }
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MailboxEntry {
|
||||
pub status: MailboxStatus,
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub ref_mailbox: Mailbox,
|
||||
pub conf: FileMailboxConf,
|
||||
}
|
||||
|
||||
impl MailboxEntry {
|
||||
pub fn new(
|
||||
status: MailboxStatus,
|
||||
name: String,
|
||||
ref_mailbox: Mailbox,
|
||||
conf: FileMailboxConf,
|
||||
) -> Self {
|
||||
let mut ret = Self {
|
||||
status,
|
||||
name,
|
||||
path: ref_mailbox.path().into(),
|
||||
ref_mailbox,
|
||||
conf,
|
||||
};
|
||||
match ret.conf.mailbox_conf.extra.get("encoding") {
|
||||
None => {}
|
||||
Some(v) if ["utf-8", "utf8"].iter().any(|e| v.eq_ignore_ascii_case(e)) => {}
|
||||
Some(v) if ["utf-7", "utf7"].iter().any(|e| v.eq_ignore_ascii_case(e)) => {
|
||||
ret.name = melib::backends::utf7::decode_utf7_imap(&ret.name);
|
||||
ret.path = melib::backends::utf7::decode_utf7_imap(&ret.path);
|
||||
}
|
||||
Some(other) => {
|
||||
log::warn!(
|
||||
"mailbox `{}`: unrecognized mailbox name charset: {}",
|
||||
&ret.name,
|
||||
other
|
||||
);
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn status(&self) -> String {
|
||||
match self.status {
|
||||
MailboxStatus::Available => format!(
|
||||
"{} [{} messages]",
|
||||
self.name(),
|
||||
self.ref_mailbox.count().ok().unwrap_or((0, 0)).1
|
||||
),
|
||||
MailboxStatus::Failed(ref e) => e.to_string(),
|
||||
MailboxStatus::None => "Retrieving mailbox.".to_string(),
|
||||
MailboxStatus::Parsing(done, total) => {
|
||||
format!("Parsing messages. [{}/{}]", done, total)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
if let Some(name) = self.conf.mailbox_conf.alias.as_ref() {
|
||||
name
|
||||
} else {
|
||||
self.ref_mailbox.name()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub enum IsOnline {
|
||||
#[default]
|
||||
|
@ -194,6 +116,19 @@ impl IsOnline {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_recoverable(err: &Error) -> bool {
|
||||
!(err.kind.is_authentication()
|
||||
|| err.kind.is_configuration()
|
||||
|| err.kind.is_bug()
|
||||
|| err.kind.is_external()
|
||||
|| (err.kind.is_network() && !err.kind.is_network_down())
|
||||
|| err.kind.is_not_implemented()
|
||||
|| err.kind.is_not_supported()
|
||||
|| err.kind.is_protocol_error()
|
||||
|| err.kind.is_protocol_not_supported()
|
||||
|| err.kind.is_value_error())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -204,7 +139,6 @@ pub struct Account {
|
|||
pub mailbox_entries: IndexMap<MailboxHash, MailboxEntry>,
|
||||
pub mailboxes_order: Vec<MailboxHash>,
|
||||
pub tree: Vec<MailboxNode>,
|
||||
pub sent_mailbox: Option<MailboxHash>,
|
||||
pub collection: Collection,
|
||||
pub address_book: AddressBook,
|
||||
pub settings: AccountConf,
|
||||
|
@ -217,199 +151,6 @@ pub struct Account {
|
|||
pub backend_capabilities: MailBackendCapabilities,
|
||||
}
|
||||
|
||||
pub enum JobRequest {
|
||||
Mailboxes {
|
||||
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
|
||||
},
|
||||
Fetch {
|
||||
mailbox_hash: MailboxHash,
|
||||
#[allow(clippy::type_complexity)]
|
||||
handle: JoinHandle<(
|
||||
Option<Result<Vec<Envelope>>>,
|
||||
Pin<Box<dyn Stream<Item = Result<Vec<Envelope>>> + Send + 'static>>,
|
||||
)>,
|
||||
},
|
||||
Generic {
|
||||
name: Cow<'static, str>,
|
||||
log_level: LogLevel,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
on_finish: Option<crate::types::CallbackFn>,
|
||||
},
|
||||
IsOnline {
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
Refresh {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SetFlags {
|
||||
env_hashes: EnvelopeHashBatch,
|
||||
mailbox_hash: MailboxHash,
|
||||
flags: SmallVec<[FlagOp; 8]>,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SaveMessage {
|
||||
bytes: Vec<u8>,
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SendMessage,
|
||||
SendMessageBackground {
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
DeleteMessages {
|
||||
env_hashes: EnvelopeHashBatch,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
CreateMailbox {
|
||||
path: String,
|
||||
handle: JoinHandle<Result<(MailboxHash, HashMap<MailboxHash, Mailbox>)>>,
|
||||
},
|
||||
DeleteMailbox {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
|
||||
},
|
||||
//RenameMailbox,
|
||||
SetMailboxPermissions {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SetMailboxSubscription {
|
||||
mailbox_hash: MailboxHash,
|
||||
new_value: bool,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
Watch {
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Drop for JobRequest {
|
||||
fn drop(&mut self) {
|
||||
match self {
|
||||
Self::Generic { handle, .. } |
|
||||
Self::IsOnline { handle, .. } |
|
||||
Self::Refresh { handle, .. } |
|
||||
Self::SetFlags { handle, .. } |
|
||||
Self::SaveMessage { handle, .. } |
|
||||
//JobRequest::RenameMailbox,
|
||||
Self::SetMailboxPermissions { handle, .. } |
|
||||
Self::SetMailboxSubscription { handle, .. } |
|
||||
Self::Watch { handle, .. } |
|
||||
Self::SendMessageBackground { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::DeleteMessages { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::CreateMailbox { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::DeleteMailbox { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::Fetch { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::Mailboxes { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::SendMessage => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for JobRequest {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Generic { name, .. } => write!(f, "JobRequest::Generic({})", name),
|
||||
Self::Mailboxes { .. } => write!(f, "JobRequest::Mailboxes"),
|
||||
Self::Fetch { mailbox_hash, .. } => {
|
||||
write!(f, "JobRequest::Fetch({})", mailbox_hash)
|
||||
}
|
||||
Self::IsOnline { .. } => write!(f, "JobRequest::IsOnline"),
|
||||
Self::Refresh { .. } => write!(f, "JobRequest::Refresh"),
|
||||
Self::SetFlags {
|
||||
env_hashes,
|
||||
mailbox_hash,
|
||||
flags,
|
||||
..
|
||||
} => f
|
||||
.debug_struct(stringify!(JobRequest::SetFlags))
|
||||
.field("env_hashes", &env_hashes)
|
||||
.field("mailbox_hash", &mailbox_hash)
|
||||
.field("flags", &flags)
|
||||
.finish(),
|
||||
Self::SaveMessage { .. } => write!(f, "JobRequest::SaveMessage"),
|
||||
Self::DeleteMessages { .. } => write!(f, "JobRequest::DeleteMessages"),
|
||||
Self::CreateMailbox { .. } => write!(f, "JobRequest::CreateMailbox"),
|
||||
Self::DeleteMailbox { mailbox_hash, .. } => {
|
||||
write!(f, "JobRequest::DeleteMailbox({})", mailbox_hash)
|
||||
}
|
||||
//JobRequest::RenameMailbox,
|
||||
Self::SetMailboxPermissions { .. } => {
|
||||
write!(f, "JobRequest::SetMailboxPermissions")
|
||||
}
|
||||
Self::SetMailboxSubscription { .. } => {
|
||||
write!(f, "JobRequest::SetMailboxSubscription")
|
||||
}
|
||||
Self::Watch { .. } => write!(f, "JobRequest::Watch"),
|
||||
Self::SendMessage => write!(f, "JobRequest::SendMessage"),
|
||||
Self::SendMessageBackground { .. } => {
|
||||
write!(f, "JobRequest::SendMessageBackground")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for JobRequest {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Generic { name, .. } => write!(f, "{}", name),
|
||||
Self::Mailboxes { .. } => write!(f, "Get mailbox list"),
|
||||
Self::Fetch { .. } => write!(f, "Mailbox fetch"),
|
||||
Self::IsOnline { .. } => write!(f, "Online status check"),
|
||||
Self::Refresh { .. } => write!(f, "Refresh mailbox"),
|
||||
Self::SetFlags {
|
||||
env_hashes, flags, ..
|
||||
} => write!(
|
||||
f,
|
||||
"Set flags for {} message{}: {:?}",
|
||||
env_hashes.len(),
|
||||
if env_hashes.len() == 1 { "" } else { "s" },
|
||||
flags
|
||||
),
|
||||
Self::SaveMessage { .. } => write!(f, "Save message"),
|
||||
Self::DeleteMessages { env_hashes, .. } => write!(
|
||||
f,
|
||||
"Delete {} message{}",
|
||||
env_hashes.len(),
|
||||
if env_hashes.len() == 1 { "" } else { "s" }
|
||||
),
|
||||
Self::CreateMailbox { path, .. } => write!(f, "Create mailbox {}", path),
|
||||
Self::DeleteMailbox { .. } => write!(f, "Delete mailbox"),
|
||||
//JobRequest::RenameMailbox,
|
||||
Self::SetMailboxPermissions { .. } => write!(f, "Set mailbox permissions"),
|
||||
Self::SetMailboxSubscription { .. } => write!(f, "Set mailbox subscription"),
|
||||
Self::Watch { .. } => write!(f, "Background watch"),
|
||||
Self::SendMessageBackground { .. } | Self::SendMessage => {
|
||||
write!(f, "Sending message")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JobRequest {
|
||||
is_variant! { is_watch, Watch { .. } }
|
||||
is_variant! { is_online, IsOnline { .. } }
|
||||
|
||||
pub fn is_fetch(&self, mailbox_hash: MailboxHash) -> bool {
|
||||
matches!(self, Self::Fetch {
|
||||
mailbox_hash: h, ..
|
||||
} if *h == mailbox_hash)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Account {
|
||||
fn drop(&mut self) {
|
||||
if let Ok(data_dir) = xdg::BaseDirectories::with_profile("meli", &self.name) {
|
||||
|
@ -461,15 +202,6 @@ impl Drop for Account {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct MailboxNode {
|
||||
pub hash: MailboxHash,
|
||||
pub depth: usize,
|
||||
pub indentation: u32,
|
||||
pub has_sibling: bool,
|
||||
pub children: Vec<MailboxNode>,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
pub fn new(
|
||||
hash: AccountHash,
|
||||
|
@ -590,7 +322,6 @@ impl Account {
|
|||
mailboxes_order: Default::default(),
|
||||
tree: Default::default(),
|
||||
address_book,
|
||||
sent_mailbox: Default::default(),
|
||||
collection: backend.collection(),
|
||||
settings,
|
||||
main_loop_handler,
|
||||
|
@ -608,8 +339,6 @@ impl Account {
|
|||
IndexMap::with_capacity_and_hasher(ref_mailboxes.len(), Default::default());
|
||||
let mut mailboxes_order: Vec<MailboxHash> = Vec::with_capacity(ref_mailboxes.len());
|
||||
|
||||
let mut sent_mailbox = None;
|
||||
|
||||
/* Keep track of which mailbox config values we encounter in the actual
|
||||
* mailboxes returned by the backend. For each of the actual
|
||||
* mailboxes, delete the key from the hash set. If any are left, they
|
||||
|
@ -621,9 +350,19 @@ impl Account {
|
|||
.keys()
|
||||
.cloned()
|
||||
.collect::<HashSet<String>>();
|
||||
let mut default_mailbox = self
|
||||
.settings
|
||||
.conf
|
||||
.default_mailbox
|
||||
.clone()
|
||||
.into_iter()
|
||||
.collect::<HashSet<String>>();
|
||||
for f in ref_mailboxes.values_mut() {
|
||||
if let Some(conf) = self.settings.mailbox_confs.get_mut(f.path()) {
|
||||
mailbox_conf_hash_set.remove(f.path());
|
||||
if default_mailbox.remove(f.path()) {
|
||||
self.settings.default_mailbox = Some(f.hash());
|
||||
}
|
||||
conf.mailbox_conf.usage = if f.special_usage() != SpecialUsageMailbox::Normal {
|
||||
Some(f.special_usage())
|
||||
} else {
|
||||
|
@ -635,11 +374,11 @@ impl Account {
|
|||
};
|
||||
match conf.mailbox_conf.usage {
|
||||
Some(SpecialUsageMailbox::Sent) => {
|
||||
sent_mailbox = Some(f.hash());
|
||||
self.settings.sent_mailbox = Some(f.hash());
|
||||
}
|
||||
None => {
|
||||
if f.special_usage() == SpecialUsageMailbox::Sent {
|
||||
sent_mailbox = Some(f.hash());
|
||||
self.settings.sent_mailbox = Some(f.hash());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
@ -665,7 +404,7 @@ impl Account {
|
|||
tmp
|
||||
};
|
||||
if new.mailbox_conf.usage == Some(SpecialUsageMailbox::Sent) {
|
||||
sent_mailbox = Some(f.hash());
|
||||
self.settings.sent_mailbox = Some(f.hash());
|
||||
}
|
||||
|
||||
mailbox_entries.insert(
|
||||
|
@ -718,6 +457,23 @@ impl Account {
|
|||
)));
|
||||
}
|
||||
|
||||
match self.settings.conf.default_mailbox {
|
||||
Some(ref v) if !default_mailbox.is_empty() => {
|
||||
let err = Error::new(format!(
|
||||
"Account `{}` has default mailbox set as `{}` but it doesn't exist.",
|
||||
&self.name, v
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration);
|
||||
self.is_online.set_err(err.clone());
|
||||
self.main_loop_handler
|
||||
.send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange(
|
||||
self.hash, None,
|
||||
)));
|
||||
return Err(err);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut tree: Vec<MailboxNode> = Vec::new();
|
||||
for (h, f) in ref_mailboxes.iter() {
|
||||
if !f.is_subscribed() {
|
||||
|
@ -766,7 +522,6 @@ impl Account {
|
|||
self.mailboxes_order = mailboxes_order;
|
||||
self.mailbox_entries = mailbox_entries;
|
||||
self.tree = tree;
|
||||
self.sent_mailbox = sent_mailbox;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -1012,6 +767,7 @@ impl Account {
|
|||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn refresh(&mut self, mailbox_hash: MailboxHash) -> Result<()> {
|
||||
if let Some(ref refresh_command) = self.settings.conf().refresh_command {
|
||||
let child = std::process::Command::new("sh")
|
||||
|
@ -1053,7 +809,9 @@ impl Account {
|
|||
}
|
||||
|
||||
pub fn watch(&mut self) {
|
||||
if self.settings.account().manual_refresh {
|
||||
if self.settings.account().manual_refresh
|
||||
|| matches!(self.is_online, IsOnline::Err { ref value, ..} if !IsOnline::is_recoverable(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1110,9 +868,10 @@ impl Account {
|
|||
ret
|
||||
}
|
||||
|
||||
pub fn mailboxes_order(&self) -> &Vec<MailboxHash> {
|
||||
pub fn mailboxes_order(&self) -> &[MailboxHash] {
|
||||
&self.mailboxes_order
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
@ -1571,17 +1330,7 @@ impl Account {
|
|||
ref mut retries,
|
||||
} => {
|
||||
let ret = Err(value.clone());
|
||||
if value.kind.is_authentication()
|
||||
|| value.kind.is_bug()
|
||||
|| value.kind.is_configuration()
|
||||
|| value.kind.is_external()
|
||||
|| (value.kind.is_network() && !value.kind.is_network_down())
|
||||
|| value.kind.is_not_implemented()
|
||||
|| value.kind.is_not_supported()
|
||||
|| value.kind.is_protocol_error()
|
||||
|| value.kind.is_protocol_not_supported()
|
||||
|| value.kind.is_value_error()
|
||||
{
|
||||
if !IsOnline::is_recoverable(value) {
|
||||
return ret;
|
||||
}
|
||||
let wait = if value.kind.is_timeout()
|
||||
|
@ -1672,6 +1421,12 @@ impl Account {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn default_mailbox(&self) -> Option<MailboxHash> {
|
||||
self.settings
|
||||
.default_mailbox
|
||||
.or_else(|| Some(*self.mailboxes_order.first()?))
|
||||
}
|
||||
|
||||
pub fn mailbox_by_path(&self, path: &str) -> Result<MailboxHash> {
|
||||
if let Some((mailbox_hash, _)) = self
|
||||
.mailbox_entries
|
||||
|
@ -1696,21 +1451,22 @@ impl Account {
|
|||
JobRequest::Mailboxes { ref mut handle } => {
|
||||
if let Ok(Some(mailboxes)) = handle.chan.try_recv() {
|
||||
if let Err(err) = mailboxes.and_then(|mailboxes| self.init(mailboxes)) {
|
||||
if err.kind.is_authentication() {
|
||||
if !IsOnline::is_recoverable(&err) {
|
||||
self.main_loop_handler.send(ThreadEvent::UIEvent(
|
||||
UIEvent::Notification {
|
||||
title: Some(
|
||||
format!("{}: authentication error", &self.name).into(),
|
||||
),
|
||||
source: None,
|
||||
title: Some(self.name.clone().into()),
|
||||
source: Some(err.clone()),
|
||||
body: err.to_string().into(),
|
||||
kind: Some(crate::types::NotificationType::Error(err.kind)),
|
||||
},
|
||||
));
|
||||
self.is_online.set_err(err);
|
||||
self.main_loop_handler.send(ThreadEvent::UIEvent(
|
||||
UIEvent::AccountStatusChange(self.hash, None),
|
||||
UIEvent::AccountStatusChange(
|
||||
self.hash,
|
||||
Some(err.to_string().into()),
|
||||
),
|
||||
));
|
||||
self.is_online.set_err(err);
|
||||
self.main_loop_handler
|
||||
.job_executor
|
||||
.set_job_success(job_id, false);
|
||||
|
@ -1813,10 +1569,11 @@ impl Account {
|
|||
.into_iter()
|
||||
.map(|e| (e.hash(), e))
|
||||
.collect::<HashMap<EnvelopeHash, Envelope>>();
|
||||
if let Some(updated_mailboxes) =
|
||||
self.collection
|
||||
.merge(envelopes, mailbox_hash, self.sent_mailbox)
|
||||
{
|
||||
if let Some(updated_mailboxes) = self.collection.merge(
|
||||
envelopes,
|
||||
mailbox_hash,
|
||||
self.settings.sent_mailbox,
|
||||
) {
|
||||
for f in updated_mailboxes {
|
||||
self.main_loop_handler.send(ThreadEvent::UIEvent(
|
||||
UIEvent::MailboxUpdate((self.hash, f)),
|
||||
|
@ -1830,13 +1587,17 @@ impl Account {
|
|||
}
|
||||
}
|
||||
JobRequest::IsOnline { ref mut handle, .. } => {
|
||||
if matches!(self.is_online, IsOnline::Err { ref value, ..} if !IsOnline::is_recoverable(value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if let Ok(Some(is_online)) = handle.chan.try_recv() {
|
||||
self.main_loop_handler.send(ThreadEvent::UIEvent(
|
||||
UIEvent::AccountStatusChange(self.hash, None),
|
||||
));
|
||||
match is_online {
|
||||
Ok(()) => {
|
||||
if matches!(self.is_online, IsOnline::Err { ref value, ..} if !value.kind.is_authentication())
|
||||
if matches!(self.is_online, IsOnline::Err { .. })
|
||||
|| matches!(self.is_online, IsOnline::Uninit)
|
||||
{
|
||||
self.watch();
|
||||
|
@ -1845,7 +1606,7 @@ impl Account {
|
|||
return true;
|
||||
}
|
||||
Err(value) => {
|
||||
self.is_online = IsOnline::Err { value, retries: 1 };
|
||||
self.is_online.set_err(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1864,12 +1625,15 @@ impl Account {
|
|||
};
|
||||
}
|
||||
JobRequest::Refresh { ref mut handle, .. } => {
|
||||
if matches!(self.is_online, IsOnline::Err { ref value, ..} if !IsOnline::is_recoverable(value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
match handle.chan.try_recv() {
|
||||
Err(_) => { /* canceled */ }
|
||||
Ok(None) => {}
|
||||
Ok(Some(Ok(()))) => {
|
||||
if matches!(self.is_online, IsOnline::Err { ref value, ..} if !value.kind.is_authentication())
|
||||
{
|
||||
if matches!(self.is_online, IsOnline::Err { .. }) {
|
||||
self.watch();
|
||||
}
|
||||
self.is_online = IsOnline::True;
|
||||
|
@ -2155,8 +1919,8 @@ impl Account {
|
|||
{
|
||||
self.tree.remove(pos);
|
||||
}
|
||||
if self.sent_mailbox == Some(mailbox_hash) {
|
||||
self.sent_mailbox = None;
|
||||
if self.settings.sent_mailbox == Some(mailbox_hash) {
|
||||
self.settings.sent_mailbox = None;
|
||||
}
|
||||
self.collection
|
||||
.threads
|
||||
|
@ -2408,224 +2172,3 @@ impl IndexMut<&MailboxHash> for Account {
|
|||
self.mailbox_entries.get_mut(index).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn build_mailboxes_order(
|
||||
tree: &mut Vec<MailboxNode>,
|
||||
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
|
||||
mailboxes_order: &mut Vec<MailboxHash>,
|
||||
) {
|
||||
tree.clear();
|
||||
mailboxes_order.clear();
|
||||
for (h, f) in mailbox_entries.iter() {
|
||||
if f.ref_mailbox.parent().is_none() {
|
||||
fn rec(
|
||||
h: MailboxHash,
|
||||
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
|
||||
depth: usize,
|
||||
) -> MailboxNode {
|
||||
let mut node = MailboxNode {
|
||||
hash: h,
|
||||
children: Vec::new(),
|
||||
depth,
|
||||
indentation: 0,
|
||||
has_sibling: false,
|
||||
};
|
||||
for &c in mailbox_entries[&h].ref_mailbox.children() {
|
||||
if mailbox_entries.contains_key(&c) {
|
||||
node.children.push(rec(c, mailbox_entries, depth + 1));
|
||||
}
|
||||
}
|
||||
node
|
||||
}
|
||||
|
||||
tree.push(rec(*h, mailbox_entries, 0));
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! mailbox_eq_key {
|
||||
($mailbox:expr) => {{
|
||||
if let Some(sort_order) = $mailbox.conf.mailbox_conf.sort_order {
|
||||
(0, sort_order, $mailbox.ref_mailbox.path())
|
||||
} else {
|
||||
(1, 0, $mailbox.ref_mailbox.path())
|
||||
}
|
||||
}};
|
||||
}
|
||||
tree.sort_unstable_by(|a, b| {
|
||||
if mailbox_entries[&b.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&b.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Greater
|
||||
} else if mailbox_entries[&a.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&a.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Less
|
||||
} else {
|
||||
mailbox_eq_key!(mailbox_entries[&a.hash])
|
||||
.cmp(&mailbox_eq_key!(mailbox_entries[&b.hash]))
|
||||
}
|
||||
});
|
||||
|
||||
let mut stack: SmallVec<[Option<&MailboxNode>; 16]> = SmallVec::new();
|
||||
for n in tree.iter_mut() {
|
||||
mailboxes_order.push(n.hash);
|
||||
n.children.sort_unstable_by(|a, b| {
|
||||
if mailbox_entries[&b.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&b.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Greater
|
||||
} else if mailbox_entries[&a.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&a.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Less
|
||||
} else {
|
||||
mailbox_eq_key!(mailbox_entries[&a.hash])
|
||||
.cmp(&mailbox_eq_key!(mailbox_entries[&b.hash]))
|
||||
}
|
||||
});
|
||||
stack.extend(n.children.iter().rev().map(Some));
|
||||
while let Some(Some(next)) = stack.pop() {
|
||||
mailboxes_order.push(next.hash);
|
||||
stack.extend(next.children.iter().rev().map(Some));
|
||||
}
|
||||
}
|
||||
drop(stack);
|
||||
for node in tree.iter_mut() {
|
||||
fn rec(
|
||||
node: &mut MailboxNode,
|
||||
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
|
||||
mut indentation: u32,
|
||||
has_sibling: bool,
|
||||
) {
|
||||
node.indentation = indentation;
|
||||
node.has_sibling = has_sibling;
|
||||
let mut iter = (0..node.children.len())
|
||||
.filter(|i| {
|
||||
mailbox_entries[&node.children[*i].hash]
|
||||
.ref_mailbox
|
||||
.is_subscribed()
|
||||
})
|
||||
.collect::<SmallVec<[_; 8]>>()
|
||||
.into_iter()
|
||||
.peekable();
|
||||
indentation <<= 1;
|
||||
if has_sibling {
|
||||
indentation |= 1;
|
||||
}
|
||||
while let Some(i) = iter.next() {
|
||||
let c = &mut node.children[i];
|
||||
rec(c, mailbox_entries, indentation, iter.peek().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
rec(node, mailbox_entries, 0, false);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_mailbox_utf7() {
|
||||
#[derive(Debug)]
|
||||
struct TestMailbox(String);
|
||||
|
||||
impl melib::BackendMailbox for TestMailbox {
|
||||
fn hash(&self) -> MailboxHash {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn path(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn children(&self) -> &[MailboxHash] {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn clone(&self) -> Mailbox {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn special_usage(&self) -> SpecialUsageMailbox {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn parent(&self) -> Option<MailboxHash> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn permissions(&self) -> MailboxPermissions {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn is_subscribed(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_is_subscribed(&mut self, _: bool) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_special_usage(&mut self, _: SpecialUsageMailbox) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn count(&self) -> Result<(usize, usize)> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
for (n, d) in [
|
||||
("~peter/mail/&U,BTFw-/&ZeVnLIqe-", "~peter/mail/台北/日本語"),
|
||||
("&BB4EQgQ,BEAEMAQyBDsENQQ9BD0ESwQ1-", "Отправленные"),
|
||||
] {
|
||||
let ref_mbox = TestMailbox(n.to_string());
|
||||
let mut conf: melib::MailboxConf = Default::default();
|
||||
conf.extra.insert("encoding".to_string(), "utf7".into());
|
||||
|
||||
let entry = MailboxEntry::new(
|
||||
MailboxStatus::None,
|
||||
n.to_string(),
|
||||
Box::new(ref_mbox),
|
||||
FileMailboxConf {
|
||||
mailbox_conf: conf,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(&entry.path, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
//
|
||||
// meli - accounts module.
|
||||
//
|
||||
// Copyright 2017 Emmanouil Pitsidianakis <manos@pitsidianak.is>
|
||||
//
|
||||
// This file is part of meli.
|
||||
//
|
||||
// meli is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// meli is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
use std::{borrow::Cow, collections::HashMap, pin::Pin};
|
||||
|
||||
use futures::stream::Stream;
|
||||
use melib::{backends::*, email::*, error::Result, LogLevel};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{is_variant, jobs::JoinHandle};
|
||||
|
||||
pub enum JobRequest {
|
||||
Mailboxes {
|
||||
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
|
||||
},
|
||||
Fetch {
|
||||
mailbox_hash: MailboxHash,
|
||||
#[allow(clippy::type_complexity)]
|
||||
handle: JoinHandle<(
|
||||
Option<Result<Vec<Envelope>>>,
|
||||
Pin<Box<dyn Stream<Item = Result<Vec<Envelope>>> + Send + 'static>>,
|
||||
)>,
|
||||
},
|
||||
Generic {
|
||||
name: Cow<'static, str>,
|
||||
log_level: LogLevel,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
on_finish: Option<crate::types::CallbackFn>,
|
||||
},
|
||||
IsOnline {
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
Refresh {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SetFlags {
|
||||
env_hashes: EnvelopeHashBatch,
|
||||
mailbox_hash: MailboxHash,
|
||||
flags: SmallVec<[FlagOp; 8]>,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SaveMessage {
|
||||
bytes: Vec<u8>,
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SendMessage,
|
||||
SendMessageBackground {
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
DeleteMessages {
|
||||
env_hashes: EnvelopeHashBatch,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
CreateMailbox {
|
||||
path: String,
|
||||
handle: JoinHandle<Result<(MailboxHash, HashMap<MailboxHash, Mailbox>)>>,
|
||||
},
|
||||
DeleteMailbox {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
|
||||
},
|
||||
//RenameMailbox,
|
||||
SetMailboxPermissions {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
SetMailboxSubscription {
|
||||
mailbox_hash: MailboxHash,
|
||||
new_value: bool,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
Watch {
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Drop for JobRequest {
|
||||
fn drop(&mut self) {
|
||||
match self {
|
||||
Self::Generic { handle, .. } |
|
||||
Self::IsOnline { handle, .. } |
|
||||
Self::Refresh { handle, .. } |
|
||||
Self::SetFlags { handle, .. } |
|
||||
Self::SaveMessage { handle, .. } |
|
||||
//JobRequest::RenameMailbox,
|
||||
Self::SetMailboxPermissions { handle, .. } |
|
||||
Self::SetMailboxSubscription { handle, .. } |
|
||||
Self::Watch { handle, .. } |
|
||||
Self::SendMessageBackground { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::DeleteMessages { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::CreateMailbox { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::DeleteMailbox { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::Fetch { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::Mailboxes { handle, .. } => {
|
||||
handle.cancel();
|
||||
}
|
||||
Self::SendMessage => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for JobRequest {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Generic { name, .. } => write!(f, "JobRequest::Generic({})", name),
|
||||
Self::Mailboxes { .. } => write!(f, "JobRequest::Mailboxes"),
|
||||
Self::Fetch { mailbox_hash, .. } => {
|
||||
write!(f, "JobRequest::Fetch({})", mailbox_hash)
|
||||
}
|
||||
Self::IsOnline { .. } => write!(f, "JobRequest::IsOnline"),
|
||||
Self::Refresh { .. } => write!(f, "JobRequest::Refresh"),
|
||||
Self::SetFlags {
|
||||
env_hashes,
|
||||
mailbox_hash,
|
||||
flags,
|
||||
..
|
||||
} => f
|
||||
.debug_struct(stringify!(JobRequest::SetFlags))
|
||||
.field("env_hashes", &env_hashes)
|
||||
.field("mailbox_hash", &mailbox_hash)
|
||||
.field("flags", &flags)
|
||||
.finish(),
|
||||
Self::SaveMessage { .. } => write!(f, "JobRequest::SaveMessage"),
|
||||
Self::DeleteMessages { .. } => write!(f, "JobRequest::DeleteMessages"),
|
||||
Self::CreateMailbox { .. } => write!(f, "JobRequest::CreateMailbox"),
|
||||
Self::DeleteMailbox { mailbox_hash, .. } => {
|
||||
write!(f, "JobRequest::DeleteMailbox({})", mailbox_hash)
|
||||
}
|
||||
//JobRequest::RenameMailbox,
|
||||
Self::SetMailboxPermissions { .. } => {
|
||||
write!(f, "JobRequest::SetMailboxPermissions")
|
||||
}
|
||||
Self::SetMailboxSubscription { .. } => {
|
||||
write!(f, "JobRequest::SetMailboxSubscription")
|
||||
}
|
||||
Self::Watch { .. } => write!(f, "JobRequest::Watch"),
|
||||
Self::SendMessage => write!(f, "JobRequest::SendMessage"),
|
||||
Self::SendMessageBackground { .. } => {
|
||||
write!(f, "JobRequest::SendMessageBackground")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for JobRequest {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Generic { name, .. } => write!(f, "{}", name),
|
||||
Self::Mailboxes { .. } => write!(f, "Get mailbox list"),
|
||||
Self::Fetch { .. } => write!(f, "Mailbox fetch"),
|
||||
Self::IsOnline { .. } => write!(f, "Online status check"),
|
||||
Self::Refresh { .. } => write!(f, "Refresh mailbox"),
|
||||
Self::SetFlags {
|
||||
env_hashes, flags, ..
|
||||
} => write!(
|
||||
f,
|
||||
"Set flags for {} message{}: {:?}",
|
||||
env_hashes.len(),
|
||||
if env_hashes.len() == 1 { "" } else { "s" },
|
||||
flags
|
||||
),
|
||||
Self::SaveMessage { .. } => write!(f, "Save message"),
|
||||
Self::DeleteMessages { env_hashes, .. } => write!(
|
||||
f,
|
||||
"Delete {} message{}",
|
||||
env_hashes.len(),
|
||||
if env_hashes.len() == 1 { "" } else { "s" }
|
||||
),
|
||||
Self::CreateMailbox { path, .. } => write!(f, "Create mailbox {}", path),
|
||||
Self::DeleteMailbox { .. } => write!(f, "Delete mailbox"),
|
||||
//JobRequest::RenameMailbox,
|
||||
Self::SetMailboxPermissions { .. } => write!(f, "Set mailbox permissions"),
|
||||
Self::SetMailboxSubscription { .. } => write!(f, "Set mailbox subscription"),
|
||||
Self::Watch { .. } => write!(f, "Background watch"),
|
||||
Self::SendMessageBackground { .. } | Self::SendMessage => {
|
||||
write!(f, "Sending message")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JobRequest {
|
||||
is_variant! { is_watch, Watch { .. } }
|
||||
is_variant! { is_online, IsOnline { .. } }
|
||||
|
||||
pub fn is_fetch(&self, mailbox_hash: MailboxHash) -> bool {
|
||||
matches!(self, Self::Fetch {
|
||||
mailbox_hash: h, ..
|
||||
} if *h == mailbox_hash)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,347 @@
|
|||
//
|
||||
// meli - accounts module.
|
||||
//
|
||||
// Copyright 2017 Emmanouil Pitsidianakis <manos@pitsidianak.is>
|
||||
//
|
||||
// This file is part of meli.
|
||||
//
|
||||
// meli is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// meli is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use melib::{
|
||||
backends::{Mailbox, MailboxHash},
|
||||
error::Error,
|
||||
log,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{conf::FileMailboxConf, is_variant};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub enum MailboxStatus {
|
||||
Available,
|
||||
Failed(Error),
|
||||
/// first argument is done work, and second is total work
|
||||
Parsing(usize, usize),
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
impl MailboxStatus {
|
||||
is_variant! { is_available, Available }
|
||||
is_variant! { is_parsing, Parsing(_, _) }
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MailboxEntry {
|
||||
pub status: MailboxStatus,
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub ref_mailbox: Mailbox,
|
||||
pub conf: FileMailboxConf,
|
||||
}
|
||||
|
||||
impl MailboxEntry {
|
||||
pub fn new(
|
||||
status: MailboxStatus,
|
||||
name: String,
|
||||
ref_mailbox: Mailbox,
|
||||
conf: FileMailboxConf,
|
||||
) -> Self {
|
||||
let mut ret = Self {
|
||||
status,
|
||||
name,
|
||||
path: ref_mailbox.path().into(),
|
||||
ref_mailbox,
|
||||
conf,
|
||||
};
|
||||
match ret.conf.mailbox_conf.extra.get("encoding") {
|
||||
None => {}
|
||||
Some(v) if ["utf-8", "utf8"].iter().any(|e| v.eq_ignore_ascii_case(e)) => {}
|
||||
Some(v) if ["utf-7", "utf7"].iter().any(|e| v.eq_ignore_ascii_case(e)) => {
|
||||
ret.name = melib::backends::utf7::decode_utf7_imap(&ret.name);
|
||||
ret.path = melib::backends::utf7::decode_utf7_imap(&ret.path);
|
||||
}
|
||||
Some(other) => {
|
||||
log::warn!(
|
||||
"mailbox `{}`: unrecognized mailbox name charset: {}",
|
||||
&ret.name,
|
||||
other
|
||||
);
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn status(&self) -> String {
|
||||
match self.status {
|
||||
MailboxStatus::Available => format!(
|
||||
"{} [{} messages]",
|
||||
self.name(),
|
||||
self.ref_mailbox.count().ok().unwrap_or((0, 0)).1
|
||||
),
|
||||
MailboxStatus::Failed(ref e) => e.to_string(),
|
||||
MailboxStatus::None => "Retrieving mailbox.".to_string(),
|
||||
MailboxStatus::Parsing(done, total) => {
|
||||
format!("Parsing messages. [{}/{}]", done, total)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
if let Some(name) = self.conf.mailbox_conf.alias.as_ref() {
|
||||
name
|
||||
} else {
|
||||
self.ref_mailbox.name()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct MailboxNode {
|
||||
pub hash: MailboxHash,
|
||||
pub depth: usize,
|
||||
pub indentation: u32,
|
||||
pub has_sibling: bool,
|
||||
pub children: Vec<MailboxNode>,
|
||||
}
|
||||
|
||||
pub fn build_mailboxes_order(
|
||||
tree: &mut Vec<MailboxNode>,
|
||||
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
|
||||
mailboxes_order: &mut Vec<MailboxHash>,
|
||||
) {
|
||||
tree.clear();
|
||||
mailboxes_order.clear();
|
||||
for (h, f) in mailbox_entries.iter() {
|
||||
if f.ref_mailbox.parent().is_none() {
|
||||
fn rec(
|
||||
h: MailboxHash,
|
||||
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
|
||||
depth: usize,
|
||||
) -> MailboxNode {
|
||||
let mut node = MailboxNode {
|
||||
hash: h,
|
||||
children: Vec::new(),
|
||||
depth,
|
||||
indentation: 0,
|
||||
has_sibling: false,
|
||||
};
|
||||
for &c in mailbox_entries[&h].ref_mailbox.children() {
|
||||
if mailbox_entries.contains_key(&c) {
|
||||
node.children.push(rec(c, mailbox_entries, depth + 1));
|
||||
}
|
||||
}
|
||||
node
|
||||
}
|
||||
|
||||
tree.push(rec(*h, mailbox_entries, 0));
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! mailbox_eq_key {
|
||||
($mailbox:expr) => {{
|
||||
if let Some(sort_order) = $mailbox.conf.mailbox_conf.sort_order {
|
||||
(0, sort_order, $mailbox.ref_mailbox.path())
|
||||
} else {
|
||||
(1, 0, $mailbox.ref_mailbox.path())
|
||||
}
|
||||
}};
|
||||
}
|
||||
tree.sort_unstable_by(|a, b| {
|
||||
if mailbox_entries[&b.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&b.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Greater
|
||||
} else if mailbox_entries[&a.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&a.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Less
|
||||
} else {
|
||||
mailbox_eq_key!(mailbox_entries[&a.hash])
|
||||
.cmp(&mailbox_eq_key!(mailbox_entries[&b.hash]))
|
||||
}
|
||||
});
|
||||
|
||||
let mut stack: SmallVec<[Option<&MailboxNode>; 16]> = SmallVec::new();
|
||||
for n in tree.iter_mut() {
|
||||
mailboxes_order.push(n.hash);
|
||||
n.children.sort_unstable_by(|a, b| {
|
||||
if mailbox_entries[&b.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&b.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Greater
|
||||
} else if mailbox_entries[&a.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&a.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Less
|
||||
} else {
|
||||
mailbox_eq_key!(mailbox_entries[&a.hash])
|
||||
.cmp(&mailbox_eq_key!(mailbox_entries[&b.hash]))
|
||||
}
|
||||
});
|
||||
stack.extend(n.children.iter().rev().map(Some));
|
||||
while let Some(Some(next)) = stack.pop() {
|
||||
mailboxes_order.push(next.hash);
|
||||
stack.extend(next.children.iter().rev().map(Some));
|
||||
}
|
||||
}
|
||||
drop(stack);
|
||||
for node in tree.iter_mut() {
|
||||
fn rec(
|
||||
node: &mut MailboxNode,
|
||||
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
|
||||
mut indentation: u32,
|
||||
has_sibling: bool,
|
||||
) {
|
||||
node.indentation = indentation;
|
||||
node.has_sibling = has_sibling;
|
||||
let mut iter = (0..node.children.len())
|
||||
.filter(|i| {
|
||||
mailbox_entries[&node.children[*i].hash]
|
||||
.ref_mailbox
|
||||
.is_subscribed()
|
||||
})
|
||||
.collect::<SmallVec<[_; 8]>>()
|
||||
.into_iter()
|
||||
.peekable();
|
||||
indentation <<= 1;
|
||||
if has_sibling {
|
||||
indentation |= 1;
|
||||
}
|
||||
while let Some(i) = iter.next() {
|
||||
let c = &mut node.children[i];
|
||||
rec(c, mailbox_entries, indentation, iter.peek().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
rec(node, mailbox_entries, 0, false);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use melib::{
|
||||
backends::{Mailbox, MailboxHash},
|
||||
error::Result,
|
||||
MailboxPermissions, SpecialUsageMailbox,
|
||||
};
|
||||
|
||||
use crate::accounts::{FileMailboxConf, MailboxEntry, MailboxStatus};
|
||||
|
||||
#[test]
|
||||
fn test_mailbox_utf7() {
|
||||
#[derive(Debug)]
|
||||
struct TestMailbox(String);
|
||||
|
||||
impl melib::BackendMailbox for TestMailbox {
|
||||
fn hash(&self) -> MailboxHash {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn path(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn children(&self) -> &[MailboxHash] {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn clone(&self) -> Mailbox {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn special_usage(&self) -> SpecialUsageMailbox {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn parent(&self) -> Option<MailboxHash> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn permissions(&self) -> MailboxPermissions {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn is_subscribed(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_is_subscribed(&mut self, _: bool) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn set_special_usage(&mut self, _: SpecialUsageMailbox) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn count(&self) -> Result<(usize, usize)> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
for (n, d) in [
|
||||
("~peter/mail/&U,BTFw-/&ZeVnLIqe-", "~peter/mail/台北/日本語"),
|
||||
("&BB4EQgQ,BEAEMAQyBDsENQQ9BD0ESwQ1-", "Отправленные"),
|
||||
] {
|
||||
let ref_mbox = TestMailbox(n.to_string());
|
||||
let mut conf: melib::MailboxConf = Default::default();
|
||||
conf.extra.insert("encoding".to_string(), "utf7".into());
|
||||
|
||||
let entry = MailboxEntry::new(
|
||||
MailboxStatus::None,
|
||||
n.to_string(),
|
||||
Box::new(ref_mbox),
|
||||
FileMailboxConf {
|
||||
mailbox_conf: conf,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(&entry.path, d);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,7 +31,11 @@ use std::{
|
|||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
use melib::{backends::TagHash, search::Query, SortField, SortOrder, StderrLogger};
|
||||
use melib::{
|
||||
backends::{MailboxHash, TagHash},
|
||||
search::Query,
|
||||
SortField, SortOrder, StderrLogger,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
conf::deserializers::non_empty_opt_string,
|
||||
|
@ -161,11 +165,17 @@ use crate::conf::deserializers::extra_settings;
|
|||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct FileAccount {
|
||||
pub root_mailbox: String,
|
||||
/// The mailbox that is the default to open / view for this account. Must be
|
||||
/// a valid mailbox path.
|
||||
///
|
||||
/// If not specified, the default is [`Self::root_mailbox`].
|
||||
#[serde(default = "none", skip_serializing_if = "Option::is_none")]
|
||||
pub default_mailbox: Option<String>,
|
||||
pub format: String,
|
||||
pub identity: String,
|
||||
#[serde(default)]
|
||||
pub extra_identities: Vec<String>,
|
||||
#[serde(default = "none")]
|
||||
#[serde(default = "none", skip_serializing_if = "Option::is_none")]
|
||||
pub display_name: Option<String>,
|
||||
|
||||
#[serde(default = "false_val")]
|
||||
|
@ -180,14 +190,18 @@ pub struct FileAccount {
|
|||
pub order: (SortField, SortOrder),
|
||||
#[serde(default = "false_val")]
|
||||
pub manual_refresh: bool,
|
||||
#[serde(default = "none")]
|
||||
#[serde(default = "none", skip_serializing_if = "Option::is_none")]
|
||||
pub refresh_command: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub conf_override: MailUIConf,
|
||||
#[serde(flatten)]
|
||||
#[serde(deserialize_with = "extra_settings")]
|
||||
pub extra: IndexMap<String, String>, /* use custom deserializer to convert any given value
|
||||
* (eg bool, number, etc) to string */
|
||||
#[serde(
|
||||
deserialize_with = "extra_settings",
|
||||
skip_serializing_if = "IndexMap::is_empty"
|
||||
)]
|
||||
/// Use custom deserializer to convert any given value (eg `bool`, number,
|
||||
/// etc) to `String`.
|
||||
pub extra: IndexMap<String, String>,
|
||||
}
|
||||
|
||||
impl FileAccount {
|
||||
|
@ -195,10 +209,6 @@ impl FileAccount {
|
|||
&self.mailboxes
|
||||
}
|
||||
|
||||
pub fn mailbox(&self) -> &str {
|
||||
&self.root_mailbox
|
||||
}
|
||||
|
||||
pub fn search_backend(&self) -> &SearchBackend {
|
||||
&self.search_backend
|
||||
}
|
||||
|
@ -230,6 +240,8 @@ pub struct FileSettings {
|
|||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct AccountConf {
|
||||
pub account: AccountSettings,
|
||||
pub default_mailbox: Option<MailboxHash>,
|
||||
pub sent_mailbox: Option<MailboxHash>,
|
||||
pub conf: FileAccount,
|
||||
pub conf_override: MailUIConf,
|
||||
pub mailbox_confs: IndexMap<String, FileMailboxConf>,
|
||||
|
@ -281,6 +293,8 @@ impl From<FileAccount> for AccountConf {
|
|||
let mailbox_confs = x.mailboxes.clone();
|
||||
Self {
|
||||
account: acc,
|
||||
default_mailbox: None,
|
||||
sent_mailbox: None,
|
||||
conf_override: x.conf_override.clone(),
|
||||
conf: x,
|
||||
mailbox_confs,
|
||||
|
@ -532,6 +546,7 @@ This is required so that you don't accidentally start meli and find out later th
|
|||
mailboxes,
|
||||
extra,
|
||||
manual_refresh,
|
||||
default_mailbox: _,
|
||||
refresh_command: _,
|
||||
search_backend: _,
|
||||
conf_override: _,
|
||||
|
|
|
@ -475,6 +475,18 @@ struct AccountMenuEntry {
|
|||
entries: SmallVec<[MailboxMenuEntry; 16]>,
|
||||
}
|
||||
|
||||
impl AccountMenuEntry {
|
||||
fn entry_by_hash(&self, needle: MailboxHash) -> Option<usize> {
|
||||
self.entries.iter().enumerate().find_map(|(i, e)| {
|
||||
if e.mailbox_hash == needle {
|
||||
Some(i)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MailListingTrait: ListingTrait {
|
||||
fn as_component(&self) -> &dyn Component
|
||||
where
|
||||
|
@ -1568,7 +1580,15 @@ impl Component for Listing {
|
|||
k if shortcut!(k == shortcuts[Shortcuts::LISTING]["next_account"]) => {
|
||||
if self.cursor_pos.account + amount < self.accounts.len() {
|
||||
self.cursor_pos.account += amount;
|
||||
self.cursor_pos.menu = MenuEntryCursor::Mailbox(0);
|
||||
let _new_val = self.cursor_pos.account;
|
||||
self.cursor_pos.menu = if let Some(idx) = context.accounts[_new_val]
|
||||
.default_mailbox()
|
||||
.and_then(|h| self.accounts[_new_val].entry_by_hash(h))
|
||||
{
|
||||
MenuEntryCursor::Mailbox(idx)
|
||||
} else {
|
||||
MenuEntryCursor::Status
|
||||
};
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
@ -1576,7 +1596,15 @@ impl Component for Listing {
|
|||
k if shortcut!(k == shortcuts[Shortcuts::LISTING]["prev_account"]) => {
|
||||
if self.cursor_pos.account >= amount {
|
||||
self.cursor_pos.account -= amount;
|
||||
self.cursor_pos.menu = MenuEntryCursor::Mailbox(0);
|
||||
let _new_val = self.cursor_pos.account;
|
||||
self.cursor_pos.menu = if let Some(idx) = context.accounts[_new_val]
|
||||
.default_mailbox()
|
||||
.and_then(|h| self.accounts[_new_val].entry_by_hash(h))
|
||||
{
|
||||
MenuEntryCursor::Mailbox(idx)
|
||||
} else {
|
||||
MenuEntryCursor::Status
|
||||
};
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
@ -1989,7 +2017,7 @@ impl Component for Listing {
|
|||
&& self.menu_cursor_pos.menu == MenuEntryCursor::Status =>
|
||||
{
|
||||
self.cursor_pos = self.menu_cursor_pos;
|
||||
self.open_status(self.menu_cursor_pos.account, context);
|
||||
self.change_account(context);
|
||||
self.set_dirty(true);
|
||||
self.focus = ListingFocus::Mailbox;
|
||||
self.ratio = self.prev_ratio;
|
||||
|
@ -2076,9 +2104,17 @@ impl Component for Listing {
|
|||
} => {
|
||||
if *account > 0 {
|
||||
*account -= 1;
|
||||
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(
|
||||
self.accounts[*account].entries.len().saturating_sub(1),
|
||||
);
|
||||
self.menu_cursor_pos.menu =
|
||||
if self.accounts[*account].entries.is_empty() {
|
||||
MenuEntryCursor::Status
|
||||
} else {
|
||||
MenuEntryCursor::Mailbox(
|
||||
self.accounts[*account]
|
||||
.entries
|
||||
.len()
|
||||
.saturating_sub(1),
|
||||
)
|
||||
};
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
@ -2111,7 +2147,12 @@ impl Component for Listing {
|
|||
} if !self.accounts[*account].entries.is_empty()
|
||||
&& *menu == MenuEntryCursor::Status =>
|
||||
{
|
||||
*menu = MenuEntryCursor::Mailbox(0);
|
||||
if let Some(idx) = context.accounts[*account]
|
||||
.default_mailbox()
|
||||
.and_then(|h| self.accounts[*account].entry_by_hash(h))
|
||||
{
|
||||
*menu = MenuEntryCursor::Mailbox(idx);
|
||||
}
|
||||
}
|
||||
// If current account has no mailboxes, go to next account
|
||||
CursorPos {
|
||||
|
@ -2250,15 +2291,32 @@ impl Component for Listing {
|
|||
{
|
||||
if self.menu_cursor_pos.account + amount >= self.accounts.len() {
|
||||
// Go to last mailbox.
|
||||
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(
|
||||
self.accounts[self.menu_cursor_pos.account]
|
||||
.entries
|
||||
.len()
|
||||
.saturating_sub(1),
|
||||
);
|
||||
self.menu_cursor_pos.menu = if self.accounts
|
||||
[self.menu_cursor_pos.account]
|
||||
.entries
|
||||
.is_empty()
|
||||
{
|
||||
MenuEntryCursor::Status
|
||||
} else {
|
||||
MenuEntryCursor::Mailbox(
|
||||
self.accounts[self.menu_cursor_pos.account]
|
||||
.entries
|
||||
.len()
|
||||
.saturating_sub(1),
|
||||
)
|
||||
};
|
||||
} else if self.menu_cursor_pos.account + amount < self.accounts.len() {
|
||||
self.menu_cursor_pos.account += amount;
|
||||
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(0);
|
||||
let _new_val = self.menu_cursor_pos.account;
|
||||
self.menu_cursor_pos.menu = if let Some(idx) = context.accounts
|
||||
[_new_val]
|
||||
.default_mailbox()
|
||||
.and_then(|h| self.accounts[_new_val].entry_by_hash(h))
|
||||
{
|
||||
MenuEntryCursor::Mailbox(idx)
|
||||
} else {
|
||||
MenuEntryCursor::Status
|
||||
};
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
@ -2268,7 +2326,16 @@ impl Component for Listing {
|
|||
{
|
||||
if self.menu_cursor_pos.account >= amount {
|
||||
self.menu_cursor_pos.account -= amount;
|
||||
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(0);
|
||||
let _new_val = self.menu_cursor_pos.account;
|
||||
self.menu_cursor_pos.menu = if let Some(idx) = context.accounts
|
||||
[_new_val]
|
||||
.default_mailbox()
|
||||
.and_then(|h| self.accounts[_new_val].entry_by_hash(h))
|
||||
{
|
||||
MenuEntryCursor::Mailbox(idx)
|
||||
} else {
|
||||
MenuEntryCursor::Status
|
||||
};
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
@ -2294,12 +2361,24 @@ impl Component for Listing {
|
|||
menu: MenuEntryCursor::Mailbox(0)
|
||||
}
|
||||
) {
|
||||
// Can't go anywhere upwards, we're on top already.
|
||||
return true;
|
||||
}
|
||||
if self.menu_cursor_pos.menu == MenuEntryCursor::Mailbox(0) {
|
||||
self.menu_cursor_pos.account = 0;
|
||||
} else {
|
||||
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(0);
|
||||
match (
|
||||
self.menu_cursor_pos.menu,
|
||||
context.accounts[self.menu_cursor_pos.account]
|
||||
.default_mailbox()
|
||||
.and_then(|h| {
|
||||
self.accounts[self.menu_cursor_pos.account].entry_by_hash(h)
|
||||
}),
|
||||
) {
|
||||
(MenuEntryCursor::Mailbox(0), _) => {
|
||||
self.menu_cursor_pos.account = 0;
|
||||
}
|
||||
(MenuEntryCursor::Mailbox(_), Some(v)) => {
|
||||
self.menu_cursor_pos.menu = MenuEntryCursor::Mailbox(v);
|
||||
}
|
||||
_ => return true,
|
||||
}
|
||||
if self.show_menu_scrollbar != ShowMenuScrollbar::Never {
|
||||
self.menu_scrollbar_show_timer.rearm();
|
||||
|
@ -2337,11 +2416,20 @@ impl Component for Listing {
|
|||
self.accounts[*account].entries.len().saturating_sub(1)
|
||||
) {
|
||||
*account = self.accounts.len().saturating_sub(1);
|
||||
*menu = MenuEntryCursor::Mailbox(0);
|
||||
} else {
|
||||
*menu = if let Some(idx) = context.accounts[*account]
|
||||
.default_mailbox()
|
||||
.and_then(|h| self.accounts[*account].entry_by_hash(h))
|
||||
{
|
||||
MenuEntryCursor::Mailbox(idx)
|
||||
} else {
|
||||
MenuEntryCursor::Status
|
||||
};
|
||||
} else if !self.accounts[*account].entries.is_empty() {
|
||||
*menu = MenuEntryCursor::Mailbox(
|
||||
self.accounts[*account].entries.len().saturating_sub(1),
|
||||
);
|
||||
} else {
|
||||
*menu = MenuEntryCursor::Status;
|
||||
}
|
||||
if self.show_menu_scrollbar != ShowMenuScrollbar::Never {
|
||||
self.menu_scrollbar_show_timer.rearm();
|
||||
|
@ -3184,9 +3272,24 @@ impl Listing {
|
|||
self.status(context),
|
||||
)));
|
||||
}
|
||||
MenuEntryCursor::Status => {
|
||||
MenuEntryCursor::Status if context.is_online(account_hash).is_ok() => {
|
||||
self.open_status(self.cursor_pos.account, context);
|
||||
}
|
||||
MenuEntryCursor::Status => {
|
||||
self.component.unrealize(context);
|
||||
self.component =
|
||||
Offline(OfflineListing::new((account_hash, MailboxHash::default())));
|
||||
self.component.realize(self.id().into(), context);
|
||||
self.component
|
||||
.process_event(&mut UIEvent::VisibilityChange(true), context);
|
||||
self.status = None;
|
||||
self.cursor_pos.menu = MenuEntryCursor::Mailbox(0);
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
|
||||
self.status(context),
|
||||
)));
|
||||
}
|
||||
}
|
||||
self.sidebar_divider = *account_settings!(context[account_hash].listing.sidebar_divider);
|
||||
self.set_dirty(true);
|
||||
|
|
|
@ -220,7 +220,13 @@ impl Component for OfflineListing {
|
|||
if let Some(msg) = msg.clone() {
|
||||
self.messages.push(msg);
|
||||
}
|
||||
self.dirty = true
|
||||
self.set_dirty(true);
|
||||
}
|
||||
UIEvent::ChangeMode(UIMode::Normal)
|
||||
| UIEvent::Resize
|
||||
| UIEvent::ConfigReload { old_settings: _ }
|
||||
| UIEvent::VisibilityChange(_) => {
|
||||
self.set_dirty(true);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,10 @@ pub use crate::{SortField, SortOrder};
|
|||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct AccountSettings {
|
||||
pub name: String,
|
||||
/// Name of mailbox that is the root of the mailbox hierarchy.
|
||||
///
|
||||
/// Note that this may have special or no meaning depending on the e-mail
|
||||
/// backend.
|
||||
pub root_mailbox: String,
|
||||
pub format: String,
|
||||
pub identity: String,
|
||||
|
|
|
@ -630,6 +630,13 @@ impl From<std::num::ParseIntError> for Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<std::fmt::Error> for Error {
|
||||
#[inline]
|
||||
fn from(kind: std::fmt::Error) -> Self {
|
||||
Self::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
impl From<&isahc::error::ErrorKind> for NetworkErrorKind {
|
||||
#[inline]
|
||||
|
|
|
@ -42,6 +42,7 @@ use crate::{
|
|||
},
|
||||
},
|
||||
error::ResultIntoError,
|
||||
text::Truncate,
|
||||
utils::parsec::CRLF,
|
||||
};
|
||||
|
||||
|
@ -479,7 +480,7 @@ pub fn list_mailbox_result(input: &[u8]) -> IResult<&[u8], ImapMailbox> {
|
|||
};
|
||||
f.separator = separator;
|
||||
|
||||
debug!(f)
|
||||
f
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
@ -578,10 +579,14 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
ret.uid =
|
||||
Some(UID::from_str(unsafe { std::str::from_utf8_unchecked(uid) }).unwrap());
|
||||
} else {
|
||||
return debug!(Err(Error::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
|
||||
log::debug!(
|
||||
"Unexpected input while parsing UID FETCH response. Got: `{}`",
|
||||
String::from_utf8_lossy(input)
|
||||
))));
|
||||
);
|
||||
return Err(Error::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Got: `{}`",
|
||||
String::from_utf8_lossy(input).as_ref().trim_at_boundary(40)
|
||||
)));
|
||||
}
|
||||
} else if input[i..].starts_with(b"FLAGS (") {
|
||||
i += b"FLAGS (".len();
|
||||
|
@ -589,11 +594,17 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
ret.flags = Some(flags);
|
||||
i += (input.len() - i - rest.len()) + 1;
|
||||
} else {
|
||||
return debug!(Err(Error::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse FLAGS: \
|
||||
{:.40}.",
|
||||
log::debug!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse FLAGS: {}.",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
))));
|
||||
);
|
||||
return Err(Error::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse FLAGS: \
|
||||
`{}`.",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
.as_ref()
|
||||
.trim_at_boundary(40)
|
||||
)));
|
||||
}
|
||||
} else if input[i..].starts_with(b"MODSEQ (") {
|
||||
i += b"MODSEQ (".len();
|
||||
|
@ -606,10 +617,14 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
.and_then(std::num::NonZeroU64::new)
|
||||
.map(ModSequence);
|
||||
} else {
|
||||
return debug!(Err(Error::new(format!(
|
||||
"Unexpected input while parsing MODSEQ in UID FETCH response. Got: `{:.40}`",
|
||||
log::debug!(
|
||||
"Unexpected input while parsing MODSEQ in UID FETCH response. Got: `{}`",
|
||||
String::from_utf8_lossy(input)
|
||||
))));
|
||||
);
|
||||
return Err(Error::new(format!(
|
||||
"Unexpected input while parsing MODSEQ in UID FETCH response. Got: `{}`",
|
||||
String::from_utf8_lossy(input).as_ref().trim_at_boundary(40)
|
||||
)));
|
||||
}
|
||||
} else if input[i..].starts_with(b"RFC822 {") {
|
||||
i += b"RFC822 ".len();
|
||||
|
@ -625,11 +640,16 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
ret.body = Some(body);
|
||||
i += input.len() - i - rest.len();
|
||||
} else {
|
||||
return debug!(Err(Error::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse RFC822: \
|
||||
{:.40}",
|
||||
log::debug!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse RFC822: {}",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
))));
|
||||
);
|
||||
return Err(Error::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse RFC822: {}",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
.as_ref()
|
||||
.trim_at_boundary(40)
|
||||
)));
|
||||
}
|
||||
} else if input[i..].starts_with(b"ENVELOPE (") {
|
||||
i += b"ENVELOPE ".len();
|
||||
|
@ -637,11 +657,18 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
ret.envelope = Some(envelope);
|
||||
i += input.len() - i - rest.len();
|
||||
} else {
|
||||
return debug!(Err(Error::new(format!(
|
||||
log::debug!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse ENVELOPE: \
|
||||
{:.40}",
|
||||
{}",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
))));
|
||||
);
|
||||
return Err(Error::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse ENVELOPE: \
|
||||
{}",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
.as_ref()
|
||||
.trim_at_boundary(40)
|
||||
)));
|
||||
}
|
||||
} else if input[i..].starts_with(b"BODYSTRUCTURE ") {
|
||||
i += b"BODYSTRUCTURE ".len();
|
||||
|
@ -660,11 +687,18 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
}
|
||||
i += input.len() - i - rest.len();
|
||||
} else {
|
||||
return debug!(Err(Error::new(format!(
|
||||
log::debug!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse \
|
||||
BODY[HEADER.FIELDS (REFERENCES)]: {:.40}",
|
||||
BODY[HEADER.FIELDS (REFERENCES)]: {}",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
))));
|
||||
);
|
||||
return Err(Error::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse \
|
||||
BODY[HEADER.FIELDS (REFERENCES)]: {}",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
.as_ref()
|
||||
.trim_at_boundary(40)
|
||||
)));
|
||||
}
|
||||
} else if input[i..].starts_with(b"BODY[HEADER.FIELDS (\"REFERENCES\")] ") {
|
||||
i += b"BODY[HEADER.FIELDS (\"REFERENCES\")] ".len();
|
||||
|
@ -677,24 +711,33 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
}
|
||||
i += input.len() - i - rest.len();
|
||||
} else {
|
||||
return debug!(Err(Error::new(format!(
|
||||
log::debug!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse \
|
||||
BODY[HEADER.FIELDS (\"REFERENCES\"): {:.40}",
|
||||
BODY[HEADER.FIELDS (\"REFERENCES\"): {}",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
))));
|
||||
);
|
||||
return Err(Error::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse \
|
||||
BODY[HEADER.FIELDS (\"REFERENCES\"): {}",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
.as_ref()
|
||||
.trim_at_boundary(40)
|
||||
)));
|
||||
}
|
||||
} else if input[i..].starts_with(b")\r\n") {
|
||||
i += b")\r\n".len();
|
||||
break;
|
||||
} else {
|
||||
debug!(
|
||||
log::debug!(
|
||||
"Got unexpected token while parsing UID FETCH response:\n`{}`\n",
|
||||
String::from_utf8_lossy(input)
|
||||
);
|
||||
return debug!(Err(Error::new(format!(
|
||||
"Got unexpected token while parsing UID FETCH response: `{:.40}`",
|
||||
return Err(Error::new(format!(
|
||||
"Got unexpected token while parsing UID FETCH response: `{}`",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
))));
|
||||
.as_ref()
|
||||
.trim_at_boundary(40)
|
||||
)));
|
||||
}
|
||||
}
|
||||
ret.raw_fetch_value = &input[..i];
|
||||
|
@ -870,7 +913,7 @@ pub fn untagged_responses(input: &[u8]) -> ImapParseResult<Option<UntaggedRespon
|
|||
let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(b" ")(input)?;
|
||||
let (input, _tag) = take_until::<_, &[u8], (&[u8], nom::error::ErrorKind)>(CRLF)(input)?;
|
||||
let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(CRLF)(input)?;
|
||||
debug!(
|
||||
log::trace!(
|
||||
"Parse untagged response from {:?}",
|
||||
String::from_utf8_lossy(orig_input)
|
||||
);
|
||||
|
@ -884,9 +927,10 @@ pub fn untagged_responses(input: &[u8]) -> ImapParseResult<Option<UntaggedRespon
|
|||
b"RECENT" => Some(Recent(num)),
|
||||
_ if _tag.starts_with(b"FETCH ") => Some(Fetch(fetch_response(orig_input)?.1)),
|
||||
_ => {
|
||||
debug!(
|
||||
"unknown untagged_response: {}",
|
||||
String::from_utf8_lossy(_tag)
|
||||
log::error!(
|
||||
"unknown untagged_response: {}, message was {:?}",
|
||||
String::from_utf8_lossy(_tag),
|
||||
String::from_utf8_lossy(orig_input)
|
||||
);
|
||||
None
|
||||
}
|
||||
|
@ -1018,13 +1062,13 @@ pub fn select_response(input: &[u8]) -> Result<SelectResponse> {
|
|||
} else if l.starts_with(b"* OK [NOMODSEQ") {
|
||||
ret.highestmodseq = Some(Err(()));
|
||||
} else if !l.is_empty() {
|
||||
debug!("select response: {}", String::from_utf8_lossy(l));
|
||||
log::trace!("select response: {}", String::from_utf8_lossy(l));
|
||||
}
|
||||
}
|
||||
Ok(ret)
|
||||
} else {
|
||||
let ret = String::from_utf8_lossy(input).to_string();
|
||||
debug!("BAD/NO response in select: {}", &ret);
|
||||
log::error!("BAD/NO response in select: {}", &ret);
|
||||
Err(Error::new(ret))
|
||||
}
|
||||
}
|
||||
|
@ -1592,7 +1636,6 @@ mod tests {
|
|||
let response =
|
||||
&b"* 1040 FETCH (UID 1064 FLAGS ())\r\nM15 OK Fetch completed (0.001 + 0.299 secs).\r\n"[..];
|
||||
for l in response.split_rn() {
|
||||
/* debug!("check line: {}", &l); */
|
||||
if required_responses.check(l) {
|
||||
ret.extend_from_slice(l);
|
||||
}
|
||||
|
|
|
@ -226,6 +226,29 @@ impl GlobMatch for str {
|
|||
}
|
||||
}
|
||||
|
||||
pub mod hex {
|
||||
use std::fmt::Write;
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
pub fn bytes_to_hex(bytes: &[u8]) -> Result<String> {
|
||||
let mut retval = String::with_capacity(bytes.len() / 2 + bytes.len() / 4);
|
||||
for (i, c) in bytes.chunks(2).enumerate() {
|
||||
if i % 16 == 0 {
|
||||
writeln!(&mut retval)?;
|
||||
} else if i % 4 == 0 {
|
||||
write!(&mut retval, " ")?;
|
||||
}
|
||||
if c.len() == 2 {
|
||||
write!(&mut retval, "{:02x}{:02x}", c[0], c[1])?;
|
||||
} else {
|
||||
write!(&mut retval, "{:02x}", c[0])?;
|
||||
}
|
||||
}
|
||||
Ok(retval)
|
||||
}
|
||||
}
|
||||
|
||||
pub const _ALICE_CHAPTER_1: &str = r#"CHAPTER I. Down the Rabbit-Hole
|
||||
|
||||
Alice was beginning to get very tired of sitting by her sister on the
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
*/
|
||||
|
||||
//! Connections layers (TCP/fd/TLS/Deflate) to use with remote backends.
|
||||
use std::{os::unix::io::AsRawFd, time::Duration};
|
||||
use std::{borrow::Cow, os::unix::io::AsRawFd, time::Duration};
|
||||
|
||||
use flate2::{read::DeflateDecoder, write::DeflateEncoder, Compression};
|
||||
#[cfg(any(target_os = "openbsd", target_os = "netbsd", target_os = "haiku"))]
|
||||
|
@ -399,25 +399,33 @@ impl std::io::Read for Connection {
|
|||
};
|
||||
if self.is_trace_enabled() {
|
||||
let id = self.id();
|
||||
if let Ok(len) = &res {
|
||||
log::trace!(
|
||||
"{}{}{}{:?} read {:?} bytes:{:?}",
|
||||
if id.is_some() { "[" } else { "" },
|
||||
if let Some(id) = id.as_ref() { id } else { "" },
|
||||
if id.is_some() { "]: " } else { "" },
|
||||
self,
|
||||
len,
|
||||
String::from_utf8_lossy(&buf[..*len])
|
||||
);
|
||||
} else {
|
||||
log::trace!(
|
||||
"{}{}{}{:?} could not read {:?}",
|
||||
if id.is_some() { "[" } else { "" },
|
||||
if let Some(id) = id.as_ref() { id } else { "" },
|
||||
if id.is_some() { "]: " } else { "" },
|
||||
self,
|
||||
&res
|
||||
);
|
||||
match &res {
|
||||
Ok(len) => {
|
||||
let slice = &buf[..*len];
|
||||
log::trace!(
|
||||
"{}{}{}{:?} read {:?} bytes:{}",
|
||||
if id.is_some() { "[" } else { "" },
|
||||
if let Some(id) = id.as_ref() { id } else { "" },
|
||||
if id.is_some() { "]: " } else { "" },
|
||||
self,
|
||||
len,
|
||||
std::str::from_utf8(slice)
|
||||
.map(Cow::Borrowed)
|
||||
.or_else(|_| crate::text::hex::bytes_to_hex(slice).map(Cow::Owned))
|
||||
.unwrap_or(Cow::Borrowed("Could not convert to hex."))
|
||||
);
|
||||
}
|
||||
Err(err) if matches!(err.kind(), std::io::ErrorKind::WouldBlock) => {}
|
||||
Err(err) => {
|
||||
log::trace!(
|
||||
"{}{}{}{:?} could not read {:?}",
|
||||
if id.is_some() { "[" } else { "" },
|
||||
if let Some(id) = id.as_ref() { id } else { "" },
|
||||
if id.is_some() { "]: " } else { "" },
|
||||
self,
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
res
|
||||
|
@ -429,13 +437,16 @@ impl std::io::Write for Connection {
|
|||
if self.is_trace_enabled() {
|
||||
let id = self.id();
|
||||
log::trace!(
|
||||
"{}{}{}{:?} writing {} bytes:{:?}",
|
||||
"{}{}{}{:?} writing {} bytes:{}",
|
||||
if id.is_some() { "[" } else { "" },
|
||||
if let Some(id) = id.as_ref() { id } else { "" },
|
||||
if id.is_some() { "]: " } else { "" },
|
||||
self,
|
||||
buf.len(),
|
||||
String::from_utf8_lossy(buf)
|
||||
std::str::from_utf8(buf)
|
||||
.map(Cow::Borrowed)
|
||||
.or_else(|_| crate::text::hex::bytes_to_hex(buf).map(Cow::Owned))
|
||||
.unwrap_or(Cow::Borrowed("Could not convert to hex."))
|
||||
);
|
||||
}
|
||||
match self {
|
||||
|
|
Loading…
Reference in New Issue