accounts: add default_mailbox setting #370

Merged
Manos Pitsidianakis merged 7 commits from default-mailbox into master 2024-03-16 18:36:14 +02:00
12 changed files with 965 additions and 636 deletions

View File

@ -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,]

View File

@ -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);
}
}
}

View File

@ -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)
}
}

View File

@ -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);
}
}
}

View File

@ -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: _,

View File

@ -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);

View File

@ -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);
}
_ => {}
}

View File

@ -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,

View File

@ -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]

View File

@ -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);
}

View File

@ -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

View File

@ -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 {