Compare commits

..

1 Commits

Author SHA1 Message Date
Manos Pitsidianakis a36a444b2c
JMAP eventsource WIP 2021-01-05 17:12:14 +02:00
61 changed files with 1189 additions and 1985 deletions

744
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -37,7 +37,7 @@ serde = "1.0.71"
serde_derive = "1.0.71"
serde_json = "1.0"
toml = { version = "0.5.6", features = ["preserve_order", ] }
indexmap = { version = "^1.6", features = ["serde-1", ] }
indexmap = { version = "^1.5", features = ["serde-1", ] }
linkify = "0.4.0"
notify = "4.0.1" # >:c
termion = "1.5.1"

View File

@ -3,7 +3,7 @@
**BSD/Linux terminal email client with support for multiple accounts and Maildir / mbox / notmuch / IMAP / JMAP.**
Community links:
[mailing lists](https://lists.meli.delivery/) | `#meli` on OFTC IRC | Report bugs and/or feature requests in [meli's issue tracker](https://git.meli.delivery/meli/meli/issues "meli gitea issue tracker")
[mailing lists](https://lists.meli.delivery/) | `#meli` on freenode IRC | Report bugs and/or feature requests in [meli's issue tracker](https://git.meli.delivery/meli/meli/issues "meli gitea issue tracker")
| | | |
:---:|:---:|:---:

View File

@ -171,7 +171,7 @@ To search in specific fields, prepend your search keyword with "field:" like so:
.Pp
.D1 not ((from:unrealistic and (to:complex or not "query")) or flags:seen,draft)
.Pp
.D1 alladdresses:mailing@example.com and cc:me@example.com
.D1 alladdresses:mailing@list.tld and cc:me@domain.tld
.Pp
Boolean operators are
.Em or Ns
@ -418,8 +418,6 @@ Copy or move to other mailbox.
Copy or move to another account's mailbox.
.It Cm delete
Delete selected threads.
.It Cm export-mbox Ar FILEPATH
Export selected threads to mboxcl2 file.
.It Cm create-mailbox Ar ACCOUNT Ar MAILBOX_PATH
create mailbox with given path.
Be careful with backends and separator sensitivity (eg IMAP)

View File

@ -78,7 +78,7 @@ example configuration
root_mailbox = "/path/to/root/folder"
format = "Maildir"
index_style = "Compact"
identity="email@example.com"
identity="email@address.tld"
subscribed_mailboxes = ["folder", "folder/Sent"] # or [ "*", ] for all mailboxes
display_name = "Name"
@ -107,7 +107,7 @@ script = "notify-send"
[composing]
# required for sending e-mail
send_mail = 'msmtp --read-recipients --read-envelope-from'
#send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
#send_mail = { hostname = "smtp.mail.tld", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
editor_command = 'vim +/^$'
[shortcuts]
@ -152,7 +152,7 @@ plain:shows one row per mail, regardless of threading
.Bl -tag -width 36n
.It Ic display_name Ar String
.Pq Em optional
A name which can be combined with your address: "Name <email@example.com>".
A name which can be combined with your address: "Name <email@address.tld>".
.It Ic read_only Ar boolean
Attempt to not make any changes to this account.
.Pq Em false
@ -199,14 +199,14 @@ format = "notmuch"
[accounts.notmuch.mailboxes]
"INBOX" = { query="tag:inbox", subscribe = true }
"Drafts" = { query="tag:draft", subscribe = true }
"Sent" = { query="from:username@example.com from:username2@example.com", subscribe = true }
"Sent" = { query="from:username@server.tld from:username2@server.tld", subscribe = true }
.Ed
.Ss IMAP only
IMAP specific options are:
.Bl -tag -width 36n
.It Ic server_hostname Ar String
example:
.Qq mail.example.com
.Qq mail.example.tld
.It Ic server_username Ar String
Server username
.It Ic server_password Ar String
@ -295,7 +295,7 @@ JMAP specific options
.Bl -tag -width 36n
.It Ic server_hostname Ar String
example:
.Qq mail.example.com
.Qq mail.example.tld
.It Ic server_username Ar String
Server username
.It Ic server_password Ar String
@ -403,9 +403,9 @@ and
\&.
Example:
.Bd -literal
[accounts."imap.example.com".mailboxes."INBOX"]
[accounts."imap.domain.tld".mailboxes."INBOX"]
index_style = "plain"
[accounts."imap.example.com".mailboxes."INBOX".pager]
[accounts."imap.domain.tld".mailboxes."INBOX".pager]
filter = ""
.Ed
.El
@ -446,31 +446,6 @@ Store sent mail after successful submission.
This setting is meant to be disabled for non-standard behaviour in gmail, which auto-saves sent mail on its own.
.\" default value
.Pq Em true
.It Ic attribution_format_string Ar String
.Pq Em optional
The attribution line appears above the quoted reply text.
The format specifiers for the replied address are:
.Bl -bullet -compact
.It
.Li %+f
— the sender's name and email address.
.It
.Li %+n
— the sender's name (or email address, if no name is included).
.It
.Li %+a
— the sender's email address.
.El
The format string is passed to
.Xr strftime 3
with the replied envelope's date.
.\" default value
.Pq Em "On %a, %0e %b %Y %H:%M, %+f wrote:%n"
.It Ic attribution_use_posix_locale Ar boolean
.Pq Em optional
Whether the strftime call for the attribution string uses the POSIX locale instead of the user's active locale.
.\" default value
.Pq Em true
.El
.Sh SHORTCUTS
Shortcuts can take the following values:
@ -1039,67 +1014,49 @@ This setting can be toggled with
String to show in status bar if mouse is active.
.\" default value
.Pq Em 🖱️
.It Ic progress_spinner_sequence Ar Either \&< Integer, ProgressSpinner \&>
Choose between 37 built in sequences (integers between 0-36) or define your own list of strings for the progress spinner animation.
.It Ic progress_spinner_sequence Ar Either \&< Integer, [String] \&>
Choose between 30-something built in sequences (integers between 0-30) or define your own list of strings for the progress spinner animation.
Set to an empty array to disable the progress spinner.
.\" default value
.Pq Em 20
.Pp
.Pq Em 19
Builtin sequences are:
.Bd -literal
0 ["-", "\\", "|", "/"]
1 ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]
2 ["⣀", "⣄", "⣤", "⣦", "⣶", "⣷", "⣿"]
3 ["⣀", "⣄", "⣆", "⣇", "⣧", "⣷", "⣿"]
4 ["○", "◔", "◐", "◕", "⬤"]
5 ["□", "◱", "◧", "▣", "■"]
6 ["□", "◱", "▨", "▩", "■"]
7 ["□", "◱", "▥", "▦", "■"]
8 ["░", "▒", "▓", "█"]
9 ["░", "█"]
10 ["⬜", "⬛"]
11 ["▱", "▰"]
12 ["▭", "◼"]
13 ["▯", "▮"]
14 ["◯", "⬤"]
15 ["⚪", "⚫"]
16 ["▖", "▗", "▘", "▝", "▞", "▚", "▙", "▟", "▜", "▛"]
17 ["|", "/", "-", "\\"]
18 [".", "o", "O", "@", "*"]
19 ["◡◡", "⊙⊙", "◠◠", "⊙⊙"]
20 ["◜ ", " ◝", " ◞", "◟ "]
21 ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"]
22 ["▁", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃"]
23 [ "▉", "▊", "▋", "▌", "▍", "▎", "▏", "▎", "▍", "▌", "▋", "▊", "▉" ]
24 ["▖", "▘", "▝", "▗"]
25 ["▌", "▀", "▐", "▄"]
26 ["┤", "┘", "┴", "└", "├", "┌", "┬", "┐"]
27 ["◢", "◣", "◤", "◥"]
28 ["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"]
29 ["⢎⡰", "⢎⡡", "⢎⡑", "⢎⠱", "⠎⡱", "⢊⡱", "⢌⡱", "⢆⡱"]
30 [".", "o", "O", "°", "O", "o", "."]
31 ["㊂", "㊀", "㊁"]
32 ["💛 ", "💙 ", "💜 ", "💚 ", "❤️ "]
33 [ "🕛 ", "🕐 ", "🕑 ", "🕒 ", "🕓 ", "🕔 ", "🕕 ", "🕖 ", "🕗 ", "🕘 ", "🕙 ", "🕚 " ]
34 ["🌍 ", "🌎 ", "🌏 "]
35 [ "[ ]", "[= ]", "[== ]", "[=== ]", "[ ===]", "[ ==]", "[ =]", "[ ]", "[ =]", "[ ==]", "[ ===]", "[====]", "[=== ]", "[== ]", "[= ]" ]
36 ["🌑 ", "🌒 ", "🌓 ", "🌔 ", "🌕 ", "🌖 ", "🌗 ", "🌘 "]
0 ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]
1 ["⣀", "⣄", "⣤", "⣦", "⣶", "⣷", "⣿"]
2 ["⣀", "⣄", "⣆", "⣇", "⣧", "⣷", "⣿"]
3 ["○", "◔", "◐", "◕", "⬤"]
4 ["□", "◱", "◧", "▣", "■"]
5 ["□", "◱", "▨", "▩", "■"]
6 ["□", "◱", "▥", "▦", "■"]
7 ["░", "▒", "▓", "█"]
8 ["░", "█"]
9 ["⬜", "⬛"]
10 ["▱", "▰"]
11 ["▭", "◼"]
12 ["▯", "▮"]
13 ["◯", "⬤"]
14 ["⚪", "⚫"]
15 ["▖", "▗", "▘", "▝", "▞", "▚", "▙", "▟", "▜", "▛"]
16 ["|", "/", "-", "\\"]
17 [".", "o", "O", "@", "*"]
18 ["◡◡", "⊙⊙", "◠◠", "⊙⊙"]
19 ["◜ ", " ◝", " ◞", "◟ "]
10 ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"]
11 ["▁", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃"]
22 ["▉", "▊", "▋", "▌", "▍", "▎", "▏", "▎", "▍", "▌", "▋", "▊", "▉"]
23 ["▖", "▘", "▝", "▗"]
24 ["▌", "▀", "▐", "▄"]
25 ["┤", "┘", "┴", "└", "├", "┌", "┬", "┐"]
26 ["◢", "◣", "◤", "◥"]
27 ["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"]
28 ["⢎⡰", "⢎⡡", "⢎⡑", "⢎⠱", "⠎⡱", "⢊⡱", "⢌⡱", "⢆⡱"]
29 [".", "o", "O", "°", "O", "o", "."]
.Ed
.Pp
Or, define an array of strings each consisting of a frame in the progress sequence indicator for a custom spinner:
.Bl -tag -width 36n
.It Ic interval_ms Ar u64
.Pq Em optional
Frame interval.
.\" default value
.Pq 50
.It Ic frames Ar [String]
The animation frames.
.El
.Pp
Example:
Or, define an array of strings each consisting of a frame in the progress sequence indicator:
.Bd -literal
progress_spinner_sequence = { interval_ms = 150, frames = [ "-", "=", "≡" ] }
# 𝄈⡂″⡈߳܃⢂:߳̈⢁܄ː“⢐″„⠑։ ⡁⡈;ܹ⡂։𝂬̤⡂꞉⣀ܹ⢁⠊𝄈⠉⠑ܸ̈׃ ;⢐;߳⠡܉˸⠒߳꞉⁚𝂬⠑⠒܅⠊;⠔⠢܄ ”⠉ֵ”⢂⢁̈⁚⠊˸⠌ܸ̤⣀𝂬⠤⠨⠢‥¨ ⡠܉꞉꞉⠑׃⠑⡐⠨؛ܸ܆„ܹ⡈⢁;⢄܄؛ ܲ⢄⠡⡁‥؛ܲ⢂“⢈։⠔⢄”꞉܉⠔
# Taken from @SmoothUnicode@botsin.space
progress_spinner_sequence = ["։","𝄈","⡂","″","⡈߳","܃","⢂",":߳̈","⢁","܄","ː","“","⢐","″","„","⠑","։"," ","⡁","⡈",";ܹ","⡂","։","𝂬̤","⡂","","⣀ܹ","⢁","⠊","𝄈","⠉","⠑ܸ̈","׃"," ",";","⢐",";߳","⠡","܉","˸","⠒߳","","","𝂬","⠑","⠒","܅","⠊",";","⠔","⠢","܄"," ","”","⠉ֵ","”","⢂","⢁̈","","⠊","˸","⠌ܸ̤","⣀","𝂬","⠤","⠨","⠢","‥","¨"," ","⡠","܉","","","⠑","׃","⠑","⡐","⠨","؛ܸ","܆","„ܹ","⡈","⢁",";","⢄܄","؛"," ܲ","⢄","⠡","⡁","‥","؛ܲ","⢂","“","⢈","։","⠔","⢄","”","","܉","⠔"]
.Ed
.El
.Sh LOG

View File

@ -12,7 +12,7 @@
#root_mailbox = "/path/to/root/mailbox"
#format = "Maildir"
#index_style = "Conversations" # or [plain, threaded, compact]
#identity="email@example.com"
#identity="email@address.tld"
#display_name = "Name"
#subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
#
@ -33,14 +33,14 @@
#[accounts."imap"]
#root_mailbox = "INBOX"
#format = "imap"
#server_hostname="mail.example.com"
#server_hostname="mail.server.tld"
#server_password="pha2hiLohs2eeeish2phaii1We3ood4chakaiv0hien2ahie3m"
#server_username="username@example.com"
#server_username="username@server.tld"
#server_port="993" # imaps
#server_port="143" # STARTTLS
#use_starttls=true #optional
#index_style = "Conversations"
#identity = "username@example.com"
#identity = "username@server.tld"
#display_name = "Name Name"
### match every mailbox:
#subscribed_mailboxes = ["*" ]
@ -52,13 +52,13 @@
#root_mailbox = "/path/to/folder" # where .notmuch/ directory is located
#format = "notmuch"
#index_style = "conversations"
#identity="username@example.com"
#identity="username@server.tld"
#display_name = "Name Name"
# # notmuch mailboxes are virtual, they are defined by their alias and the notmuch query that corresponds to their content.
# [accounts.notmuch.mailboxes]
# "INBOX" = { query="tag:inbox", subscribe = true }
# "Drafts" = { query="tag:draft", subscribe = true }
# "Sent" = { query="from:username@example.com from:username2@example.com", subscribe = true }
# "Sent" = { query="from:username@server.tld from:username2@server.tld", subscribe = true }
#
## Setting up a Gmail account
#[accounts."gmail"]
@ -69,7 +69,7 @@
#server_username="username@gmail.com"
#server_port="993"
#index_style = "Conversations"
#identity = "username@gmail.com"
#identity = "username@server.tld"
#display_name = "Name Name"
### match every mailbox:
#subscribed_mailboxes = ["*" ]
@ -123,7 +123,7 @@
#[composing]
##required for sending e-mail
#send_mail = 'msmtp --read-recipients --read-envelope-from'
##send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
##send_mail = { hostname = "smtp.mail.tld", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
#editor_command = 'vim +/^$' # optional, by default $EDITOR is used.
#
#

View File

@ -29,11 +29,11 @@ and body structure. Addresses in `To`, `From` fields etc are parsed into
```rust
use melib::{Attachment, Envelope};
let raw_mail = r#"From: "some name" <some@example.com>
To: "me" <myself@example.com>
let raw_mail = r#"From: "some name" <some@address.com>
To: "me" <myself@i.tld>
Cc:
Subject: =?utf-8?Q?gratuitously_encoded_subject?=
Message-ID: <h2g7f.z0gy2pgaen5m@example.com>
Message-ID: <h2g7f.z0gy2pgaen5m@address.com>
MIME-Version: 1.0
Content-Type: multipart/mixed; charset="utf-8";
boundary="bzz_bzz__bzz__"
@ -74,7 +74,7 @@ ouiijDaaCCGQRgrpH3q4QYYXWDihxBE+7KCDDjnUIEVAADs=
let envelope = Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
assert_eq!(envelope.subject().as_ref(), "gratuitously encoded subject");
assert_eq!(envelope.message_id_display().as_ref(), "<h2g7f.z0gy2pgaen5m@example.com>");
assert_eq!(envelope.message_id_display().as_ref(), "<h2g7f.z0gy2pgaen5m@address.com>");
let body = envelope.body_bytes(raw_mail.as_bytes());
assert_eq!(body.content_type().to_string().as_str(), "multipart/mixed");

View File

@ -192,7 +192,7 @@ impl Card {
self.key.as_str()
}
pub fn last_edited(&self) -> String {
datetime::timestamp_to_string(self.last_edited, None, false)
datetime::timestamp_to_string(self.last_edited, None)
}
pub fn set_id(&mut self, new_val: CardId) {

View File

@ -201,7 +201,7 @@ impl<V: VCardVersion> TryInto<Card> for VCard<V> {
T102200Z
T102200-0800
*/
card.birthday = crate::datetime::timestamp_from_string(val.value.as_str(), "%Y%m%d\0")
card.birthday = crate::datetime::timestamp_from_string(val.value.as_str(), "%Y%m%d")
.unwrap_or_default();
}
if let Some(val) = self.0.remove("EMAIL") {

View File

@ -58,15 +58,15 @@ use self::maildir::MaildirType;
use self::mbox::MboxType;
use super::email::{Envelope, EnvelopeHash, Flag};
use std::any::Any;
use std::collections::BTreeSet;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::fmt::Debug;
use std::ops::Deref;
use std::sync::{Arc, RwLock};
use futures::stream::Stream;
pub use futures::stream::Stream;
use std::future::Future;
use std::pin::Pin;
pub use std::pin::Pin;
use std::collections::HashMap;
@ -246,14 +246,6 @@ pub enum RefreshEventKind {
NewFlags(EnvelopeHash, (Flag, Vec<String>)),
Rescan,
Failure(MeliError),
MailboxCreate(Mailbox),
MailboxDelete(MailboxHash),
MailboxRename {
old_mailbox_hash: MailboxHash,
new_mailbox: Mailbox,
},
MailboxSubscribe(MailboxHash),
MailboxUnsubscribe(MailboxHash),
}
#[derive(Debug, Clone)]
@ -348,7 +340,9 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
mailbox_hash: MailboxHash,
) -> ResultFuture<()>;
fn collection(&self) -> crate::Collection;
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
None
}
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;

View File

@ -42,21 +42,20 @@ use crate::backends::{
*,
};
use crate::collection::Collection;
use crate::conf::AccountSettings;
use crate::connections::timeout;
use crate::email::{parser::BytesExt, *};
use crate::error::{MeliError, Result, ResultIntoMeliError};
use futures::lock::Mutex as FutureMutex;
use futures::stream::Stream;
use std::collections::hash_map::DefaultHasher;
use std::collections::{hash_map::DefaultHasher, BTreeMap};
use std::collections::{BTreeSet, HashMap, HashSet};
use std::convert::TryFrom;
use std::convert::TryInto;
use std::hash::Hasher;
use std::pin::Pin;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::sync::{Arc, Mutex, RwLock};
use std::time::{Duration, SystemTime};
pub type ImapNum = usize;
@ -143,7 +142,7 @@ pub struct UIDStore {
msn_index: Arc<Mutex<HashMap<MailboxHash, Vec<UID>>>>,
byte_cache: Arc<Mutex<HashMap<UID, EnvelopeCache>>>,
collection: Collection,
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
/* Offline caching */
uidvalidity: Arc<Mutex<HashMap<MailboxHash, UID>>>,
@ -179,7 +178,7 @@ impl UIDStore {
msn_index: Default::default(),
byte_cache: Default::default(),
mailboxes: Arc::new(FutureMutex::new(Default::default())),
collection: Default::default(),
tag_index: Arc::new(RwLock::new(Default::default())),
is_online: Arc::new(Mutex::new((
SystemTime::now(),
Err(MeliError::new("Account is uninitialised.")),
@ -711,7 +710,7 @@ impl MailBackend for ImapType {
/* Set flags/tags to true */
let mut set_seen = false;
let command = {
let mut tag_lck = uid_store.collection.tag_index.write().unwrap();
let mut tag_lck = uid_store.tag_index.write().unwrap();
let mut cmd = format!("UID STORE {}", uids[0]);
for uid in uids.iter().skip(1) {
cmd = format!("{},{}", cmd, uid);
@ -860,6 +859,10 @@ impl MailBackend for ImapType {
}))
}
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
Some(self.uid_store.tag_index.clone())
}
fn as_any(&self) -> &dyn Any {
self
}
@ -868,10 +871,6 @@ impl MailBackend for ImapType {
self
}
fn collection(&self) -> Collection {
self.uid_store.collection.clone()
}
fn create_mailbox(
&mut self,
mut path: String,
@ -1760,9 +1759,21 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
let env = envelope.as_mut().unwrap();
env.set_hash(generate_envelope_hash(&mailbox_path, &uid));
if let Some(value) = references {
let parse_result = crate::email::parser::address::msg_id_list(value);
if let Ok((_, value)) = parse_result {
let prev_val = env.references.take();
for v in value {
env.push_references(v);
}
if let Some(prev) = prev_val {
for v in prev.refs {
env.push_references(v);
}
}
}
env.set_references(value);
}
let mut tag_lck = uid_store.collection.tag_index.write().unwrap();
let mut tag_lck = uid_store.tag_index.write().unwrap();
if let Some((flags, keywords)) = flags {
env.set_flags(*flags);
if !env.is_seen() {

View File

@ -239,7 +239,7 @@ mod sqlite3_m {
.entry(mailbox_hash)
.and_modify(|entry| *entry = uidvalidity)
.or_insert(uidvalidity);
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
let mut tag_lck = self.uid_store.tag_index.write().unwrap();
for f in to_str!(&flags).split('\0') {
let hash = tag_hash!(f);
//debug!("hash {} flag {}", hash, &f);

View File

@ -157,9 +157,21 @@ impl ImapConnection {
let env = envelope.as_mut().unwrap();
env.set_hash(generate_envelope_hash(&mailbox_path, &uid));
if let Some(value) = references {
let parse_result = crate::email::parser::address::msg_id_list(value);
if let Ok((_, value)) = parse_result {
let prev_val = env.references.take();
for v in value {
env.push_references(v);
}
if let Some(prev) = prev_val {
for v in prev.refs {
env.push_references(v);
}
}
}
env.set_references(value);
}
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
let mut tag_lck = self.uid_store.tag_index.write().unwrap();
if let Some((flags, keywords)) = flags {
env.set_flags(*flags);
if !env.is_seen() {
@ -443,9 +455,21 @@ impl ImapConnection {
let env = envelope.as_mut().unwrap();
env.set_hash(generate_envelope_hash(&mailbox_path, &uid));
if let Some(value) = references {
let parse_result = crate::email::parser::address::msg_id_list(value);
if let Ok((_, value)) = parse_result {
let prev_val = env.references.take();
for v in value {
env.push_references(v);
}
if let Some(prev) = prev_val {
for v in prev.refs {
env.push_references(v);
}
}
}
env.set_references(value);
}
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
let mut tag_lck = self.uid_store.tag_index.write().unwrap();
if let Some((flags, keywords)) = flags {
env.set_flags(*flags);
if !env.is_seen() {

View File

@ -1318,9 +1318,7 @@ pub fn envelope(input: &[u8]) -> IResult<&[u8], Envelope> {
}
if let Some(in_reply_to) = in_reply_to {
env.set_in_reply_to(&in_reply_to);
if let Some(in_reply_to) = env.in_reply_to().cloned() {
env.push_references(in_reply_to);
}
env.push_references(env.in_reply_to().unwrap().clone());
}
if let Some(message_id) = message_id {

View File

@ -224,9 +224,21 @@ impl ImapConnection {
let env = envelope.as_mut().unwrap();
env.set_hash(generate_envelope_hash(&mailbox.imap_path(), &uid));
if let Some(value) = references {
let parse_result = crate::email::parser::address::msg_id_list(value);
if let Ok((_, value)) = parse_result {
let prev_val = env.references.take();
for v in value {
env.push_references(v);
}
if let Some(prev) = prev_val {
for v in prev.refs {
env.push_references(v);
}
}
}
env.set_references(value);
}
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
let mut tag_lck = self.uid_store.tag_index.write().unwrap();
if let Some((flags, keywords)) = flags {
env.set_flags(*flags);
if !env.is_seen() {
@ -354,9 +366,22 @@ impl ImapConnection {
let env = envelope.as_mut().unwrap();
env.set_hash(generate_envelope_hash(&mailbox.imap_path(), &uid));
if let Some(value) = references {
let parse_result =
crate::email::parser::address::msg_id_list(value);
if let Ok((_, value)) = parse_result {
let prev_val = env.references.take();
for v in value {
env.push_references(v);
}
if let Some(prev) = prev_val {
for v in prev.refs {
env.push_references(v);
}
}
}
env.set_references(value);
}
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
let mut tag_lck = self.uid_store.tag_index.write().unwrap();
if let Some((flags, keywords)) = flags {
env.set_flags(*flags);
if !env.is_seen() {

View File

@ -374,9 +374,21 @@ pub async fn examine_updates(
let env = envelope.as_mut().unwrap();
env.set_hash(generate_envelope_hash(&mailbox.imap_path(), &uid));
if let Some(value) = references {
let parse_result = crate::email::parser::address::msg_id_list(value);
if let Ok((_, value)) = parse_result {
let prev_val = env.references.take();
for v in value {
env.push_references(v);
}
if let Some(prev) = prev_val {
for v in prev.refs {
env.push_references(v);
}
}
}
env.set_references(value);
}
let mut tag_lck = uid_store.collection.tag_index.write().unwrap();
let mut tag_lck = uid_store.tag_index.write().unwrap();
if let Some((flags, keywords)) = flags {
env.set_flags(*flags);
if !env.is_seen() {

View File

@ -23,13 +23,12 @@ use crate::backends::*;
use crate::conf::AccountSettings;
use crate::email::*;
use crate::error::{MeliError, Result};
use crate::Collection;
use futures::lock::Mutex as FutureMutex;
use isahc::config::RedirectPolicy;
use isahc::prelude::HttpClient;
use isahc::ResponseExt;
use serde_json::Value;
use std::collections::{hash_map::DefaultHasher, HashMap, HashSet};
use std::collections::{hash_map::DefaultHasher, BTreeMap, HashMap, HashSet};
use std::convert::TryFrom;
use std::hash::{Hash, Hasher};
use std::str::FromStr;
@ -184,7 +183,7 @@ pub struct Store {
pub id_store: Arc<Mutex<HashMap<EnvelopeHash, Id<EmailObject>>>>,
pub reverse_id_store: Arc<Mutex<HashMap<Id<EmailObject>, EnvelopeHash>>>,
pub blob_id_store: Arc<Mutex<HashMap<EnvelopeHash, Id<BlobObject>>>>,
pub collection: Collection,
pub tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
pub mailboxes: Arc<RwLock<HashMap<MailboxHash, JmapMailbox>>>,
pub mailboxes_index: Arc<RwLock<HashMap<MailboxHash, HashSet<EnvelopeHash>>>>,
pub mailbox_state: Arc<Mutex<State<MailboxObject>>>,
@ -195,7 +194,7 @@ pub struct Store {
impl Store {
pub fn add_envelope(&self, obj: EmailObject) -> Envelope {
let mut tag_lck = self.collection.tag_index.write().unwrap();
let mut tag_lck = self.tag_index.write().unwrap();
let tags = obj
.keywords()
.keys()
@ -342,32 +341,7 @@ impl MailBackend for JmapType {
}
fn watch(&self) -> ResultFuture<()> {
let connection = self.connection.clone();
let store = self.store.clone();
Ok(Box::pin(async move {
{
let mut conn = connection.lock().await;
conn.connect().await?;
}
loop {
{
let mailbox_hashes = {
store
.mailboxes
.read()
.unwrap()
.keys()
.cloned()
.collect::<SmallVec<[MailboxHash; 16]>>()
};
let conn = connection.lock().await;
for mailbox_hash in mailbox_hashes {
conn.email_changes(mailbox_hash).await?;
}
}
crate::connections::sleep(std::time::Duration::from_secs(60)).await;
}
}))
Err(MeliError::from("JMAP watch for updates is unimplemented"))
}
fn mailboxes(&self) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
@ -484,6 +458,10 @@ impl MailBackend for JmapType {
}))
}
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
Some(self.store.tag_index.clone())
}
fn as_any(&self) -> &dyn Any {
self
}
@ -492,10 +470,6 @@ impl MailBackend for JmapType {
self
}
fn collection(&self) -> Collection {
self.store.collection.clone()
}
fn search(
&self,
q: crate::search::Query,
@ -754,7 +728,7 @@ impl MailBackend for JmapType {
}
{
let mut tag_index_lck = store.collection.tag_index.write().unwrap();
let mut tag_index_lck = store.tag_index.write().unwrap();
for (flag, value) in flags.iter() {
match flag {
Ok(_) => {}
@ -842,12 +816,12 @@ impl JmapType {
online_status,
event_consumer,
is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)),
collection: Collection::default(),
byte_cache: Default::default(),
id_store: Default::default(),
reverse_id_store: Default::default(),
blob_id_store: Default::default(),
tag_index: Default::default(),
mailboxes: Default::default(),
mailboxes_index: Default::default(),
mailbox_state: Default::default(),

View File

@ -19,6 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
pub mod eventsource;
use super::*;
use isahc::config::Configurable;

View File

@ -0,0 +1,266 @@
/*
* meli - jmap module.
*
* Copyright 2021 Manos Pitsidianakis
*
* 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/>.
*/
use super::{HttpClient, JmapConnection, JmapServerConf, Store};
use crate::error::Result;
use std::convert::TryFrom;
use std::io::{BufRead, BufReader};
use std::sync::Arc;
use std::time::{Duration, Instant};
const DEFAULT_RETRY: u64 = 5000;
/// A single Server-Sent Event.
#[derive(Debug, Default)]
pub struct Event {
/// Corresponds to the `id` field.
pub id: Option<String>,
/// Corresponds to the `event` field.
pub event_type: Option<String>,
/// All `data` fields concatenated by newlines.
pub data: String,
}
/// Possible results from parsing a single event-stream line.
#[derive(Debug, PartialEq)]
pub enum ParseResult {
/// Line parsed successfully, but the event is not complete yet.
Next,
/// The event is complete now. Pass a new (empty) event for the next call.
Dispatch,
/// Set retry time.
SetRetry(Duration),
}
pub fn parse_event_line(line: &str, event: &mut Event) -> ParseResult {
let line = line.trim_end_matches(|c| c == '\r' || c == '\n');
if line == "" {
ParseResult::Dispatch
} else {
let (field, value) = if let Some(pos) = line.find(':') {
let (f, v) = line.split_at(pos);
// Strip : and an optional space.
let v = &v[1..];
let v = if v.starts_with(' ') { &v[1..] } else { v };
(f, v)
} else {
(line, "")
};
match field {
"event" => {
event.event_type = Some(value.to_string());
}
"data" => {
event.data.push_str(value);
event.data.push('\n');
}
"id" => {
event.id = Some(value.to_string());
}
"retry" => {
if let Ok(retry) = value.parse::<u64>() {
return ParseResult::SetRetry(Duration::from_millis(retry));
}
}
_ => (), // ignored
}
ParseResult::Next
}
}
impl Event {
/// Creates an empty event.
pub fn new() -> Event {
Event::default()
}
/// Returns `true` if the event is empty.
///
/// An event is empty if it has no id or event type and its data field is empty.
pub fn is_empty(&self) -> bool {
self.id.is_none() && self.event_type.is_none() && self.data.is_empty()
}
/// Makes the event empty.
pub fn clear(&mut self) {
self.id = None;
self.event_type = None;
self.data.clear();
}
}
/// A client for a Server-Sent Events endpoint.
///
/// Read events by iterating over the client.
pub struct JmapEventSourceConnection {
pub client: Arc<HttpClient>,
pub store: Arc<Store>,
pub server_conf: JmapServerConf,
response: Option<BufReader<isahc::Body>>,
url: isahc::http::Uri,
last_event_id: Option<String>,
last_try: Option<Instant>,
pub retry: Duration,
}
impl JmapEventSourceConnection {
pub fn new(conn: &JmapConnection) -> Result<Self> {
let url =
isahc::http::Uri::try_from(conn.session.lock().unwrap().event_source_url.as_str())
.map_err(|err| err.to_string())?;
debug!("event_source {}", &url);
Ok(Self {
client: conn.client.clone(),
server_conf: conn.server_conf.clone(),
store: conn.store.clone(),
response: None,
url: url,
last_event_id: None,
last_try: None,
retry: Duration::from_millis(DEFAULT_RETRY),
})
}
pub async fn next_request(&mut self) -> Result<()> {
use isahc::{http, http::request::Request, prelude::*};
let mut request = Request::get(&self.url)
.timeout(std::time::Duration::from_secs(10))
.redirect_policy(isahc::config::RedirectPolicy::Limit(10))
.authentication(isahc::auth::Authentication::basic())
.credentials(isahc::auth::Credentials::new(
&self.server_conf.server_username,
&self.server_conf.server_password,
))
.header(http::header::ACCEPT, "text/event-stream");
if let Some(ref id) = self.last_event_id {
request = request.header("Last-Event-ID", id.as_str());
}
let request = request.body(()).map_err(|err| err.to_string())?;
debug!(&request);
let mut response = self.client.send_async(request).await?;
debug!(&response);
//debug_assert!(response.status().is_success());
/*
let mut headers = HeaderMap::with_capacity(2);
headers.insert(ACCEPT, HeaderValue::from_str("text/event-stream").unwrap());
if let Some(ref id) = self.last_event_id {
headers.insert("Last-Event-ID", HeaderValue::from_str(id).unwrap());
}
let res = self.client.get(self.url.clone()).headers(headers).send()?;
*/
// Check status code and Content-Type.
{
let status = response.status();
if !status.is_success() {
let res_text = response.text_async().await?;
return Err(debug!(format!("{} {}", status.as_str(), res_text)).into());
}
if let Some(content_type_hv) = response.headers().get(isahc::http::header::CONTENT_TYPE)
{
if content_type_hv.to_str().unwrap() != "text/event-stream" {
panic!(content_type_hv.to_str().unwrap().to_string());
}
/*
let content_type = content_type_hv
.to_str()
.unwrap()
.to_string()
.parse::<mime::Mime>()
.unwrap();
// Compare type and subtype only, MIME parameters are ignored.
if (content_type.type_(), content_type.subtype())
!= (mime::TEXT, mime::EVENT_STREAM)
{
return Err(ErrorKind::InvalidContentType(content_type.clone()).into());
}
*/
}
}
self.response = Some(BufReader::new(response.into_body()));
Ok(())
}
} /*
pub async fn next_event(&mut self) -> Result<Event> {
let mut line = String::new();
'main_loop: loop {
match self.response.as_mut() {
None => {
// wait for the next request.
if let Some(last_try) = self.last_try {
let elapsed = last_try.elapsed();
if elapsed < self.retry {
crate::connection::sleep(self.retry - elapsed).await;
}
}
// Set here in case the request fails.
self.last_try = Some(Instant::now());
self.next_request().await?;
}
Some(reader) => {
let mut event = Event::new();
loop {
match reader.read_line(&mut line) {
// Got new bytes from stream
Ok(_n) if _n > 0 => {
match parse_event_line(&line, &mut event) {
ParseResult::Next => {} // okay, just continue
ParseResult::Dispatch => {
if let Some(ref id) = event.id {
self.last_event_id = Some(id.clone());
}
return Ok(event);
}
ParseResult::SetRetry(ref retry) => {
self.retry = *retry;
}
}
line.clear();
}
Ok(0) => {
// EOF or a stream error, retry after timeout
self.last_try = Some(Instant::now());
self.response = None;
continue 'main_loop;
}
Err(err) => {
// EOF or a stream error, retry after timeout
self.last_try = Some(Instant::now());
self.response = None;
debug!(&err);
continue 'main_loop;
}
}
}
}
}
}
}
}
*/

View File

@ -266,11 +266,15 @@ impl std::convert::From<EmailObject> for crate::Envelope {
}
if let Some(ref in_reply_to) = t.in_reply_to {
env.set_in_reply_to(in_reply_to[0].as_bytes());
if let Some(in_reply_to) = env.in_reply_to().cloned() {
env.push_references(in_reply_to);
}
env.push_references(env.in_reply_to().unwrap().clone());
}
if let Some(v) = t.headers.get("References") {
let parse_result = crate::email::parser::address::msg_id_list(v.as_bytes());
if let Ok((_, v)) = parse_result {
for v in v {
env.push_references(v);
}
}
env.set_references(v.as_bytes());
}
if let Some(v) = t.headers.get("Date") {
@ -278,8 +282,6 @@ impl std::convert::From<EmailObject> for crate::Envelope {
if let Ok(d) = crate::email::parser::dates::rfc5322_date(v.as_bytes()) {
env.set_datetime(d);
}
} else if let Ok(d) = crate::email::parser::dates::rfc5322_date(t.received_at.as_bytes()) {
env.set_datetime(d);
}
env.set_has_attachments(t.has_attachment);
if let Some(ref mut subject) = t.subject {
@ -579,7 +581,6 @@ impl From<crate::search::Query> for Filter<EmailFilterCondition, EmailObject> {
fn from(val: crate::search::Query) -> Self {
let mut ret = Filter::Condition(EmailFilterCondition::new().into());
fn rec(q: &crate::search::Query, f: &mut Filter<EmailFilterCondition, EmailObject>) {
use crate::datetime::{timestamp_to_string, RFC3339_FMT};
use crate::search::Query::*;
match q {
Subject(t) => {
@ -603,48 +604,23 @@ impl From<crate::search::Query> for Filter<EmailFilterCondition, EmailObject> {
Body(t) => {
*f = Filter::Condition(EmailFilterCondition::new().body(t.clone()).into());
}
Before(t) => {
*f = Filter::Condition(
EmailFilterCondition::new()
.before(timestamp_to_string(*t, Some(RFC3339_FMT), true))
.into(),
);
Before(_) => {
//TODO, convert UNIX timestamp into UtcDate
}
After(t) => {
*f = Filter::Condition(
EmailFilterCondition::new()
.after(timestamp_to_string(*t, Some(RFC3339_FMT), true))
.into(),
);
After(_) => {
//TODO
}
Between(a, b) => {
*f = Filter::Condition(
EmailFilterCondition::new()
.after(timestamp_to_string(*a, Some(RFC3339_FMT), true))
.into(),
);
*f &= Filter::Condition(
EmailFilterCondition::new()
.before(timestamp_to_string(*b, Some(RFC3339_FMT), true))
.into(),
);
Between(_, _) => {
//TODO
}
On(t) => {
rec(&Between(*t, *t), f);
On(_) => {
//TODO
}
InReplyTo(ref s) => {
*f = Filter::Condition(
EmailFilterCondition::new()
.header(vec!["In-Reply-To".to_string().into(), s.to_string().into()])
.into(),
);
InReplyTo(_) => {
//TODO, look inside Headers
}
References(ref s) => {
*f = Filter::Condition(
EmailFilterCondition::new()
.header(vec!["References".to_string().into(), s.to_string().into()])
.into(),
);
References(_) => {
//TODO
}
AllAddresses(_) => {
//TODO

View File

@ -109,7 +109,7 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
list, account_id, ..
} = m;
*conn.store.account_id.lock().unwrap() = account_id;
let mut ret: HashMap<MailboxHash, JmapMailbox> = list
Ok(list
.into_iter()
.map(|r| {
let MailboxObject {
@ -157,13 +157,7 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
},
)
})
.collect();
for key in ret.keys().cloned().collect::<SmallVec<[MailboxHash; 24]>>() {
if let Some(parent_hash) = ret[&key].parent_hash.clone() {
ret.entry(parent_hash).and_modify(|e| e.children.push(key));
}
}
Ok(ret)
.collect())
}
pub async fn get_message_list(

View File

@ -30,7 +30,7 @@ use crate::backends::*;
use crate::email::Flag;
use crate::error::{MeliError, Result};
use crate::shellexpand::ShellExpandTrait;
use futures::stream::Stream;
pub use futures::stream::Stream;
use std::collections::hash_map::DefaultHasher;
use std::fs;
use std::hash::{Hash, Hasher};

View File

@ -25,7 +25,6 @@ use crate::conf::AccountSettings;
use crate::email::{Envelope, EnvelopeHash, Flag};
use crate::error::{ErrorKind, MeliError, Result};
use crate::shellexpand::ShellExpandTrait;
use crate::Collection;
use futures::prelude::Stream;
extern crate notify;
@ -110,7 +109,6 @@ pub struct MaildirType {
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
hash_indexes: HashIndexes,
event_consumer: BackendEventConsumer,
collection: Collection,
path: PathBuf,
}
@ -1005,10 +1003,6 @@ impl MailBackend for MaildirType {
}))
}
fn collection(&self) -> Collection {
self.collection.clone()
}
fn create_mailbox(
&mut self,
new_path: String,
@ -1242,7 +1236,6 @@ impl MaildirType {
hash_indexes: Arc::new(Mutex::new(hash_indexes)),
mailbox_index: Default::default(),
event_consumer,
collection: Default::default(),
path: root_path,
}))
}

View File

@ -19,109 +19,11 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
//! # Mbox formats
//!
//! ## Resources
//!
//! - [0] <https://web.archive.org/web/20160812091518/https://jdebp.eu./FGA/mail-mbox-formats.html>
//! - [1] <https://wiki2.dovecot.org/MailboxFormat/mbox>
//! - [2] <https://manpages.debian.org/buster/mutt/mbox.5.en.html>
//!
//! ## `mbox` format
//! `mbox` describes a family of incompatible legacy formats.
//!
//! "All of the 'mbox' formats store all of the messages in the mailbox in a single file. Delivery appends new messages to the end of the file." [0]
//!
//! "Each message is preceded by a From_ line and followed by a blank line. A From_ line is a line that begins with the five characters 'F', 'r', 'o', 'm', and ' '." [0]
//!
//! ## `From ` / postmark line
//!
//! "An mbox is a text file containing an arbitrary number of e-mail messages. Each message
//! consists of a postmark, followed by an e-mail message formatted according to RFC822, RFC2822.
//! The file format is line-oriented. Lines are separated by line feed characters (ASCII 10).
//!
//! "A postmark line consists of the four characters 'From', followed by a space character,
//! followed by the message's envelope sender address, followed by whitespace, and followed by a
//! time stamp. This line is often called From_ line.
//!
//! "The sender address is expected to be addr-spec as defined in RFC2822 3.4.1. The date is expected
//! to be date-time as output by asctime(3). For compatibility reasons with legacy software,
//! two-digit years greater than or equal to 70 should be interpreted as the years 1970+, while
//! two-digit years less than 70 should be interpreted as the years 2000-2069. Software reading
//! files in this format should also be prepared to accept non-numeric timezone information such as
//! 'CET DST' for Central European Time, daylight saving time.
//!
//! "Example:
//!
//!```text
//!From example@example.com Fri Jun 23 02:56:55 2000
//!```
//!
//! "In order to avoid misinterpretation of lines in message bodies which begin with the four
//! characters 'From', followed by a space character, the mail delivery agent must quote
//! any occurrence of 'From ' at the start of a body line." [2]
//!
//! ## Metadata
//!
//! `melib` recognizes the CClient (a [Pine client API](https://web.archive.org/web/20050203003235/http://www.washington.edu/imap/)) convention for metadata in `mbox` format:
//!
//! - `Status`: R (Seen) and O (non-Recent) flags
//! - `X-Status`: A (Answered), F (Flagged), T (Draft) and D (Deleted) flags
//! - `X-Keywords`: Messages keywords
//!
//! ## Parsing an mbox file
//!
//! ```
//! # use melib::{Result, Envelope, EnvelopeHash, mbox::*};
//! # use std::collections::HashMap;
//! # use std::sync::{Arc, Mutex};
//! let file_contents = vec![]; // Replace with actual mbox file contents
//! let index: Arc<Mutex<HashMap<EnvelopeHash, (Offset, Length)>>> = Arc::new(Mutex::new(HashMap::default()));
//! let mut message_iter = MessageIterator {
//! index: index.clone(),
//! input: &file_contents.as_slice(),
//! offset: 0,
//! file_offset: 0,
//! format: Some(MboxFormat::MboxCl2),
//! };
//! let envelopes: Result<Vec<Envelope>> = message_iter.collect();
//! ```
//!
//! ## Writing / Appending an mbox file
//!
//! ```no_run
//! # use melib::mbox::*;
//! # use std::io::Write;
//! let mbox_1: &[u8] = br#"From: <a@b.c>\n\nHello World"#;
//! let mbox_2: &[u8] = br#"From: <d@e.f>\n\nHello World #2"#;
//! let mut file = std::io::BufWriter::new(std::fs::File::create(&"out.mbox")?);
//! let format = MboxFormat::MboxCl2;
//! format.append(
//! &mut file,
//! mbox_1,
//! None, // Envelope From
//! Some(melib::datetime::now()), // Delivered date
//! Default::default(), // Flags and tags
//! MboxMetadata::None,
//! true,
//! false,
//! )?;
//! format.append(
//! &mut file,
//! mbox_2,
//! None,
//! Some(melib::datetime::now()),
//! Default::default(), // Flags and tags
//! MboxMetadata::None,
//! false,
//! false,
//! )?;
//! file.flush()?;
//! # Ok::<(), melib::MeliError>(())
//! ```
/*!
* https://wiki2.dovecot.org/MailboxFormat/mbox
*/
use crate::backends::*;
use crate::collection::Collection;
use crate::conf::AccountSettings;
use crate::email::parser::BytesExt;
use crate::email::*;
@ -145,10 +47,8 @@ use std::str::FromStr;
use std::sync::mpsc::channel;
use std::sync::{Arc, Mutex, RwLock};
pub mod write;
pub type Offset = usize;
pub type Length = usize;
type Offset = usize;
type Length = usize;
#[cfg(target_os = "linux")]
const F_OFD_SETLKW: libc::c_int = 38;
@ -371,30 +271,14 @@ impl BackendOp for MboxOp {
}
#[derive(Debug, Clone, Copy)]
pub enum MboxMetadata {
/// Dovecot uses C-Client (ie. UW-IMAP, Pine) compatible headers in mbox messages to store me
/// - X-IMAPbase: Contains UIDVALIDITY, last used UID and list of used keywords
/// - X-IMAP: Same as X-IMAPbase but also specifies that the message is a “pseudo message”
/// - X-UID: Messages allocated UID
/// - Status: R (Seen) and O (non-Recent) flags
/// - X-Status: A (Answered), F (Flagged), T (Draft) and D (Deleted) flags
/// - X-Keywords: Messages keywords
/// - Content-Length: Length of the message body in bytes
CClient,
None,
}
/// Choose between "mboxo", "mboxrd", "mboxcl", "mboxcl2". For new mailboxes, prefer "mboxcl2"
/// which does not alter the mail body.
#[derive(Debug, Clone, Copy)]
pub enum MboxFormat {
pub enum MboxReader {
MboxO,
MboxRd,
MboxCl,
MboxCl2,
}
impl Default for MboxFormat {
impl Default for MboxReader {
fn default() -> Self {
Self::MboxCl2
}
@ -437,8 +321,8 @@ macro_rules! find_From__line {
}};
}
impl MboxFormat {
pub fn parse<'i>(&self, input: &'i [u8]) -> IResult<&'i [u8], Envelope> {
impl MboxReader {
fn parse<'i>(&self, input: &'i [u8]) -> IResult<&'i [u8], Envelope> {
let orig_input = input;
let mut input = input;
match self {
@ -721,7 +605,7 @@ pub fn mbox_parse(
index: Arc<Mutex<HashMap<EnvelopeHash, (Offset, Length)>>>,
input: &[u8],
file_offset: usize,
format: Option<MboxFormat>,
reader: Option<MboxReader>,
) -> IResult<&[u8], Vec<Envelope>> {
if input.is_empty() {
return Err(nom::Err::Error((input, ErrorKind::Tag)));
@ -730,9 +614,9 @@ pub fn mbox_parse(
let mut index = index.lock().unwrap();
let mut envelopes = Vec::with_capacity(32);
let format = format.unwrap_or(MboxFormat::MboxCl2);
let reader = reader.unwrap_or(MboxReader::MboxCl2);
while !input[offset + file_offset..].is_empty() {
let (next_input, env) = match format.parse(&input[offset + file_offset..]) {
let (next_input, env) = match reader.parse(&input[offset + file_offset..]) {
Ok(v) => v,
Err(e) => {
// Try to recover from this error by finding a new candidate From_ line
@ -764,12 +648,12 @@ pub fn mbox_parse(
Ok((&[], envelopes))
}
pub struct MessageIterator<'a> {
pub index: Arc<Mutex<HashMap<EnvelopeHash, (Offset, Length)>>>,
pub input: &'a [u8],
pub file_offset: usize,
pub offset: usize,
pub format: Option<MboxFormat>,
struct MessageIterator<'a> {
index: Arc<Mutex<HashMap<EnvelopeHash, (Offset, Length)>>>,
input: &'a [u8],
file_offset: usize,
offset: usize,
reader: Option<MboxReader>,
}
impl<'a> Iterator for MessageIterator<'a> {
@ -780,10 +664,10 @@ impl<'a> Iterator for MessageIterator<'a> {
}
let mut index = self.index.lock().unwrap();
let format = self.format.unwrap_or(MboxFormat::MboxCl2);
let reader = self.reader.unwrap_or(MboxReader::MboxCl2);
while !self.input[self.offset + self.file_offset..].is_empty() {
let (next_input, env) =
match format.parse(&self.input[self.offset + self.file_offset..]) {
match reader.parse(&self.input[self.offset + self.file_offset..]) {
Ok(v) => v,
Err(e) => {
// Try to recover from this error by finding a new candidate From_ line
@ -824,10 +708,9 @@ impl<'a> Iterator for MessageIterator<'a> {
pub struct MboxType {
account_name: String,
path: PathBuf,
collection: Collection,
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
mailboxes: Arc<Mutex<HashMap<MailboxHash, MboxMailbox>>>,
prefer_mbox_type: Option<MboxFormat>,
prefer_mbox_type: Option<MboxReader>,
event_consumer: BackendEventConsumer,
}
@ -856,7 +739,7 @@ impl MailBackend for MboxType {
mailbox_hash: MailboxHash,
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
mailboxes: Arc<Mutex<HashMap<MailboxHash, MboxMailbox>>>,
prefer_mbox_type: Option<MboxFormat>,
prefer_mbox_type: Option<MboxReader>,
offset: usize,
file_offset: usize,
contents: Vec<u8>,
@ -871,7 +754,7 @@ impl MailBackend for MboxType {
input: &self.contents.as_slice(),
offset: self.offset,
file_offset: self.file_offset,
format: self.prefer_mbox_type,
reader: self.prefer_mbox_type,
};
let mut payload = vec![];
let mut done = false;
@ -1174,10 +1057,6 @@ impl MailBackend for MboxType {
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn collection(&self) -> Collection {
self.collection.clone()
}
}
macro_rules! get_conf_val {
@ -1229,10 +1108,10 @@ impl MboxType {
path,
prefer_mbox_type: match prefer_mbox_type.as_str() {
"auto" => None,
"mboxo" => Some(MboxFormat::MboxO),
"mboxrd" => Some(MboxFormat::MboxRd),
"mboxcl" => Some(MboxFormat::MboxCl),
"mboxcl2" => Some(MboxFormat::MboxCl2),
"mboxo" => Some(MboxReader::MboxO),
"mboxrd" => Some(MboxReader::MboxRd),
"mboxcl" => Some(MboxReader::MboxCl),
"mboxcl2" => Some(MboxReader::MboxCl2),
_ => {
return Err(MeliError::new(format!(
"{} invalid `prefer_mbox_type` value: `{}`",
@ -1241,7 +1120,6 @@ impl MboxType {
)))
}
},
collection: Collection::default(),
mailbox_index: Default::default(),
mailboxes: Default::default(),
};

View File

@ -1,260 +0,0 @@
/*
* meli - mailbox module.
*
* Copyright 2021 Manos Pitsidianakis
*
* 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/>.
*/
use super::*;
impl MboxFormat {
pub fn append(
&self,
writer: &mut dyn std::io::Write,
input: &[u8],
envelope_from: Option<&Address>,
delivery_date: Option<crate::UnixTimestamp>,
(flags, tags): (Flag, Vec<&str>),
metadata_format: MboxMetadata,
is_empty: bool,
crlf: bool,
) -> Result<()> {
if tags.iter().any(|t| t.contains(' ')) {
return Err(MeliError::new("mbox tags/keywords can't contain spaces"));
}
let line_ending: &'static [u8] = if crlf { &b"\r\n"[..] } else { &b"\n"[..] };
if !is_empty {
writer.write_all(line_ending)?;
writer.write_all(line_ending)?;
}
writer.write_all(&b"From "[..])?;
if let Some(from) = envelope_from {
writer.write_all(from.address_spec_raw())?;
} else {
writer.write_all(&b"MAILER-DAEMON"[..])?;
}
writer.write_all(&b" "[..])?;
writer.write_all(
crate::datetime::timestamp_to_string(
delivery_date.unwrap_or_else(|| crate::datetime::now()),
Some(crate::datetime::ASCTIME_FMT),
true,
)
.trim()
.as_bytes(),
)?;
writer.write_all(line_ending)?;
let (mut headers, body) = parser::mail(input)?;
headers.retain(|(header_name, _)| {
!header_name.eq_ignore_ascii_case(b"Status")
&& !header_name.eq_ignore_ascii_case(b"X-Status")
&& !header_name.eq_ignore_ascii_case(b"X-Keywords")
&& !header_name.eq_ignore_ascii_case(b"Content-Length")
});
let write_header_val_fn = |writer: &mut dyn std::io::Write, bytes: &[u8]| {
let mut i = 0;
if crlf {
while i < bytes.len() {
if bytes[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\r', b'\n'])?;
i += 2;
continue;
} else if bytes[i] == b'\n' {
writer.write_all(&[b'\r', b'\n'])?;
} else {
writer.write_all(&[bytes[i]])?;
}
i += 1;
}
} else {
while i < bytes.len() {
if bytes[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\n'])?;
i += 2;
} else {
writer.write_all(&[bytes[i]])?;
i += 1;
}
}
}
Ok::<(), MeliError>(())
};
let write_metadata_fn = |writer: &mut dyn std::io::Write| match metadata_format {
MboxMetadata::CClient => {
for (h, v) in {
if flags.is_seen() {
Some((&b"Status"[..], "R".into()))
} else {
None
}
.into_iter()
.chain(
if !flags.is_flagged()
&& !flags.is_replied()
&& !flags.is_draft()
&& !flags.is_trashed()
{
None
} else {
Some((
&b"X-Status"[..],
format!(
"{flagged}{replied}{draft}{trashed}",
flagged = if flags.is_flagged() { "F" } else { "" },
replied = if flags.is_replied() { "A" } else { "" },
draft = if flags.is_draft() { "T" } else { "" },
trashed = if flags.is_trashed() { "D" } else { "" }
),
))
},
)
.chain(if tags.is_empty() {
None
} else {
Some((&b"X-Keywords"[..], tags.as_slice().join(" ")))
})
} {
writer.write_all(h)?;
writer.write_all(&b": "[..])?;
writer.write_all(v.as_bytes())?;
writer.write_all(line_ending)?;
}
Ok::<(), MeliError>(())
}
MboxMetadata::None => Ok(()),
};
let body_len = {
let mut len = body.len();
if crlf {
let stray_lfs = body.iter().filter(|b| **b == b'\n').count()
- body.windows(b"\r\n".len()).filter(|w| w == b"\r\n").count();
len += stray_lfs;
} else {
let crlfs = body.windows(b"\r\n".len()).filter(|w| w == b"\r\n").count();
len -= crlfs;
}
len
};
match self {
MboxFormat::MboxO | MboxFormat::MboxRd => Err(MeliError::new("Unimplemented.")),
MboxFormat::MboxCl => {
let len = (body_len
+ body
.windows(b"\nFrom ".len())
.filter(|w| w == b"\nFrom ")
.count()
+ if body.starts_with(b"From ") { 1 } else { 0 })
.to_string();
for (h, v) in headers
.into_iter()
.chain(Some((&b"Content-Length"[..], len.as_bytes())))
{
writer.write_all(h)?;
writer.write_all(&b": "[..])?;
write_header_val_fn(writer, v)?;
writer.write_all(line_ending)?;
}
write_metadata_fn(writer)?;
writer.write_all(line_ending)?;
if body.starts_with(b"From ") {
writer.write_all(&[b'>'])?;
}
let mut i = 0;
if crlf {
while i < body.len() {
if body[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\r', b'\n'])?;
if body[i..].starts_with(b"\r\nFrom ") {
writer.write_all(&[b'>'])?;
}
i += 2;
} else if body[i] == b'\n' {
writer.write_all(&[b'\r', b'\n'])?;
if body[i..].starts_with(b"\nFrom ") {
writer.write_all(&[b'>'])?;
}
i += 1;
} else {
writer.write_all(&[body[i]])?;
i += 1;
}
}
} else {
while i < body.len() {
if body[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\n'])?;
if body[i..].starts_with(b"\r\nFrom ") {
writer.write_all(&[b'>'])?;
}
i += 2;
} else {
writer.write_all(&[body[i]])?;
if body[i..].starts_with(b"\nFrom ") {
writer.write_all(&[b'>'])?;
}
i += 1;
}
}
}
Ok(())
}
MboxFormat::MboxCl2 => {
let len = body_len.to_string();
for (h, v) in headers
.into_iter()
.chain(Some((&b"Content-Length"[..], len.as_bytes())))
{
writer.write_all(h)?;
writer.write_all(&b": "[..])?;
write_header_val_fn(writer, v)?;
writer.write_all(line_ending)?;
}
write_metadata_fn(writer)?;
writer.write_all(line_ending)?;
let mut i = 0;
if crlf {
while i < body.len() {
if body[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\r', b'\n'])?;
i += 2;
continue;
} else if body[i] == b'\n' {
writer.write_all(&[b'\r', b'\n'])?;
} else {
writer.write_all(&[body[i]])?;
}
i += 1;
}
} else {
while i < body.len() {
if body[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\n'])?;
i += 2;
} else {
writer.write_all(&[body[i]])?;
i += 1;
}
}
}
Ok(())
}
}
}
}

View File

@ -32,19 +32,19 @@ pub use operations::*;
mod connection;
pub use connection::*;
use crate::backends::*;
use crate::conf::AccountSettings;
use crate::connections::timeout;
use crate::email::*;
use crate::error::{MeliError, Result, ResultIntoMeliError};
use crate::{backends::*, Collection};
use futures::lock::Mutex as FutureMutex;
use futures::stream::Stream;
use std::collections::{hash_map::DefaultHasher, BTreeSet, HashMap, HashSet};
use std::collections::{hash_map::DefaultHasher, BTreeMap, BTreeSet, HashMap, HashSet};
use std::future::Future;
use std::hash::Hasher;
use std::pin::Pin;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use std::sync::{Arc, Mutex, RwLock};
use std::time::Instant;
pub type UID = usize;
pub static SUPPORTED_CAPABILITIES: &[&str] = &[
@ -77,7 +77,6 @@ pub struct UIDStore {
hash_index: Arc<Mutex<HashMap<EnvelopeHash, (UID, MailboxHash)>>>,
uid_index: Arc<Mutex<HashMap<(MailboxHash, UID), EnvelopeHash>>>,
collection: Collection,
mailboxes: Arc<FutureMutex<HashMap<MailboxHash, NntpMailbox>>>,
is_online: Arc<Mutex<(Instant, Result<()>)>>,
event_consumer: BackendEventConsumer,
@ -98,7 +97,6 @@ impl UIDStore {
hash_index: Default::default(),
uid_index: Default::default(),
mailboxes: Arc::new(FutureMutex::new(Default::default())),
collection: Collection::new(),
is_online: Arc::new(Mutex::new((
Instant::now(),
Err(MeliError::new("Account is uninitialised.")),
@ -222,11 +220,10 @@ impl MailBackend for NntpType {
fn is_online(&self) -> ResultFuture<()> {
let connection = self.connection.clone();
Ok(Box::pin(async move {
match timeout(Some(Duration::from_secs(60 * 16)), connection.lock()).await {
match timeout(std::time::Duration::from_secs(3), connection.lock()).await {
Ok(mut conn) => {
debug!("is_online");
match debug!(timeout(Some(Duration::from_secs(60 * 16)), conn.connect()).await)
{
match debug!(timeout(std::time::Duration::from_secs(3), conn.connect()).await) {
Ok(Ok(())) => Ok(()),
Err(err) | Ok(Err(err)) => {
conn.stream = Err(err.clone());
@ -297,6 +294,10 @@ impl MailBackend for NntpType {
Err(MeliError::new("NNTP doesn't support deletion."))
}
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
None
}
fn as_any(&self) -> &dyn Any {
self
}
@ -305,10 +306,6 @@ impl MailBackend for NntpType {
self
}
fn collection(&self) -> Collection {
self.uid_store.collection.clone()
}
fn create_mailbox(
&mut self,
_path: String,
@ -631,3 +628,15 @@ impl FetchState {
Ok(Some(ret))
}
}
use futures::future::{self, Either};
async fn timeout<O>(dur: std::time::Duration, f: impl Future<Output = O>) -> Result<O> {
futures::pin_mut!(f);
match future::select(f, smol::Timer::after(dur)).await {
Either::Left((out, _)) => Ok(out),
Either::Right(_) => {
Err(MeliError::new("Timedout").set_kind(crate::error::ErrorKind::Network))
}
}
}

View File

@ -90,7 +90,7 @@ impl NntpStream {
let stream = {
let addr = lookup_ipv4(path, server_conf.server_port)?;
AsyncWrapper::new(Connection::Tcp(
TcpStream::connect_timeout(&addr, std::time::Duration::new(16, 0))
TcpStream::connect_timeout(&addr, std::time::Duration::new(4, 0))
.chain_err_kind(crate::error::ErrorKind::Network)?,
))
.chain_err_kind(crate::error::ErrorKind::Network)?
@ -130,7 +130,7 @@ impl NntpStream {
.any(|cap| cap.eq_ignore_ascii_case("VERSION 2"))
{
return Err(MeliError::new(format!(
"Could not connect to {}: server is not NNTP VERSION 2 compliant",
"Could not connect to {}: server is not NNTP compliant",
&server_conf.server_hostname
)));
}
@ -190,12 +190,8 @@ impl NntpStream {
ret.stream = AsyncWrapper::new(Connection::Tls(
conn_result.chain_err_kind(crate::error::ErrorKind::Network)?,
))
.chain_err_summary(|| format!("Could not initiate TLS negotiation to {}.", path))
.chain_err_kind(crate::error::ErrorKind::Network)?;
}
} else {
ret.read_response(&mut res, false, &["200 ", "201 "])
.await?;
}
//ret.send_command(
// format!(
@ -205,17 +201,9 @@ impl NntpStream {
// .as_bytes(),
//)
//.await?;
if let Err(err) = ret
.stream
.get_ref()
.set_keepalive(Some(std::time::Duration::new(60 * 9, 0)))
{
crate::log(
format!("Could not set TCP keepalive in NNTP connection: {}", err),
crate::LoggingLevel::WARN,
);
}
ret.read_response(&mut res, false, &["200 ", "201 "])
.await?;
ret.send_command(b"CAPABILITIES").await?;
ret.read_response(&mut res, true, command_to_replycodes("CAPABILITIES"))
.await?;

View File

@ -144,6 +144,15 @@ pub fn over_article(input: &str) -> IResult<&str, (UID, Envelope)> {
}
if let Some(references) = references {
{
if let Ok((_, r)) =
crate::email::parser::address::msg_id_list(references.as_bytes())
{
for v in r {
env.push_references(v);
}
}
}
env.set_references(references.as_bytes());
}

View File

@ -19,11 +19,11 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use crate::backends::*;
use crate::conf::AccountSettings;
use crate::email::{Envelope, EnvelopeHash, Flag};
use crate::error::{MeliError, Result};
use crate::shellexpand::ShellExpandTrait;
use crate::{backends::*, Collection};
use smallvec::SmallVec;
use std::collections::{
hash_map::{DefaultHasher, HashMap},
@ -220,7 +220,7 @@ pub struct NotmuchDb {
mailboxes: Arc<RwLock<HashMap<MailboxHash, NotmuchMailbox>>>,
index: Arc<RwLock<HashMap<EnvelopeHash, CString>>>,
mailbox_index: Arc<RwLock<HashMap<EnvelopeHash, SmallVec<[MailboxHash; 16]>>>>,
collection: Collection,
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
path: PathBuf,
account_name: Arc<String>,
account_hash: AccountHash,
@ -358,7 +358,7 @@ impl NotmuchDb {
path,
index: Arc::new(RwLock::new(Default::default())),
mailbox_index: Arc::new(RwLock::new(Default::default())),
collection: Collection::default(),
tag_index: Arc::new(RwLock::new(Default::default())),
mailboxes: Arc::new(RwLock::new(mailboxes)),
save_messages_to: None,
@ -510,7 +510,7 @@ impl MailBackend for NotmuchDb {
)?);
let index = self.index.clone();
let mailbox_index = self.mailbox_index.clone();
let tag_index = self.collection.tag_index.clone();
let tag_index = self.tag_index.clone();
let mailboxes = self.mailboxes.clone();
let v: Vec<CString>;
{
@ -561,7 +561,7 @@ impl MailBackend for NotmuchDb {
let mailboxes = self.mailboxes.clone();
let index = self.index.clone();
let mailbox_index = self.mailbox_index.clone();
let tag_index = self.collection.tag_index.clone();
let tag_index = self.tag_index.clone();
let event_consumer = self.event_consumer.clone();
Ok(Box::pin(async move {
let new_revision_uuid = database.get_revision_uuid();
@ -586,13 +586,13 @@ impl MailBackend for NotmuchDb {
use notify::{watcher, RecursiveMode, Watcher};
let account_hash = self.account_hash;
let collection = self.collection.clone();
let lib = self.lib.clone();
let path = self.path.clone();
let revision_uuid = self.revision_uuid.clone();
let mailboxes = self.mailboxes.clone();
let index = self.index.clone();
let mailbox_index = self.mailbox_index.clone();
let tag_index = self.tag_index.clone();
let event_consumer = self.event_consumer.clone();
let (tx, rx) = std::sync::mpsc::channel();
@ -616,7 +616,7 @@ impl MailBackend for NotmuchDb {
mailboxes.clone(),
index.clone(),
mailbox_index.clone(),
collection.tag_index.clone(),
tag_index.clone(),
account_hash.clone(),
event_consumer.clone(),
new_revision_uuid,
@ -651,7 +651,7 @@ impl MailBackend for NotmuchDb {
hash,
index: self.index.clone(),
bytes: None,
collection: self.collection.clone(),
tag_index: self.tag_index.clone(),
}))
}
@ -693,7 +693,7 @@ impl MailBackend for NotmuchDb {
self.lib.clone(),
true,
)?;
let collection = self.collection.clone();
let tag_index = self.tag_index.clone();
let index = self.index.clone();
Ok(Box::pin(async move {
@ -781,11 +781,7 @@ impl MailBackend for NotmuchDb {
for (f, v) in flags.iter() {
if let (Err(tag), true) = (f, v) {
let hash = tag_hash!(tag);
collection
.tag_index
.write()
.unwrap()
.insert(hash, tag.to_string());
tag_index.write().unwrap().insert(hash, tag.to_string());
}
}
@ -838,8 +834,8 @@ impl MailBackend for NotmuchDb {
}))
}
fn collection(&self) -> Collection {
self.collection.clone()
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
Some(self.tag_index.clone())
}
fn as_any(&self) -> &dyn Any {
@ -855,7 +851,7 @@ impl MailBackend for NotmuchDb {
struct NotmuchOp {
hash: EnvelopeHash,
index: Arc<RwLock<HashMap<EnvelopeHash, CString>>>,
collection: Collection,
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
database: Arc<DbConnection>,
bytes: Option<Vec<u8>>,
lib: Arc<libloading::Library>,

View File

@ -25,7 +25,7 @@ use smallvec::SmallVec;
use std::ops::{Deref, DerefMut};
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::collections::{HashMap, HashSet};
pub struct EnvelopeRef<'g> {
guard: RwLockReadGuard<'g, HashMap<EnvelopeHash, Envelope>>,
@ -64,9 +64,8 @@ pub struct Collection {
pub envelopes: Arc<RwLock<HashMap<EnvelopeHash, Envelope>>>,
pub message_id_index: Arc<RwLock<HashMap<Vec<u8>, EnvelopeHash>>>,
pub threads: Arc<RwLock<HashMap<MailboxHash, Threads>>>,
pub sent_mailbox: Arc<RwLock<Option<MailboxHash>>>,
sent_mailbox: Arc<RwLock<Option<MailboxHash>>>,
pub mailboxes: Arc<RwLock<HashMap<MailboxHash, HashSet<EnvelopeHash>>>>,
pub tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
}
impl Default for Collection {
@ -116,7 +115,6 @@ impl Collection {
Collection {
envelopes: Arc::new(RwLock::new(Default::default())),
tag_index: Arc::new(RwLock::new(BTreeMap::default())),
message_id_index,
threads,
mailboxes,

View File

@ -291,7 +291,3 @@ pub async fn timeout<O>(dur: Option<Duration>, f: impl Future<Output = O>) -> cr
Ok(f.await)
}
}
pub async fn sleep(dur: Duration) {
smol::Timer::after(dur).await;
}

View File

@ -34,47 +34,41 @@
//! assert_eq!(timestamp, 1578509043);
//!
//! // Convert timestamp back to string
//! let s = timestamp_to_string(timestamp, Some("%Y-%m-%d"), true);
//! let s = timestamp_to_string(timestamp, Some("%Y-%m-%d"));
//! assert_eq!(s, "2020-01-08");
//! ```
use crate::error::{Result, ResultIntoMeliError};
use std::borrow::Cow;
use std::convert::TryInto;
use std::ffi::{CStr, CString};
pub type UnixTimestamp = u64;
pub const RFC3339_FMT_WITH_TIME: &str = "%Y-%m-%dT%H:%M:%S\0";
pub const RFC3339_FMT: &str = "%Y-%m-%d\0";
pub const RFC822_FMT_WITH_TIME: &str = "%a, %e %h %Y %H:%M:%S \0";
pub const RFC822_FMT: &str = "%e %h %Y %H:%M:%S \0";
pub const DEFAULT_FMT: &str = "%a, %d %b %Y %R\0";
//"Tue May 21 13:46:22 1991\n"
pub const ASCTIME_FMT: &str = "%a %b %d %H:%M:%S %Y\n\0";
use libc::{locale_t, timeval, timezone};
extern "C" {
fn strptime(
s: *const std::os::raw::c_char,
format: *const std::os::raw::c_char,
tm: *mut libc::tm,
) -> *const std::os::raw::c_char;
s: *const ::std::os::raw::c_char,
format: *const ::std::os::raw::c_char,
tm: *mut ::libc::tm,
) -> *const ::std::os::raw::c_char;
fn strftime(
s: *mut std::os::raw::c_char,
max: libc::size_t,
format: *const std::os::raw::c_char,
tm: *const libc::tm,
) -> libc::size_t;
s: *mut ::std::os::raw::c_char,
max: ::libc::size_t,
format: *const ::std::os::raw::c_char,
tm: *const ::libc::tm,
) -> ::libc::size_t;
fn mktime(tm: *const libc::tm) -> libc::time_t;
fn mktime(tm: *const ::libc::tm) -> ::libc::time_t;
fn localtime_r(timep: *const libc::time_t, tm: *mut libc::tm) -> *mut libc::tm;
fn localtime_r(timep: *const ::libc::time_t, tm: *mut ::libc::tm) -> *mut ::libc::tm;
fn gettimeofday(tv: *mut libc::timeval, tz: *mut libc::timezone) -> i32;
fn gettimeofday(tv: *mut timeval, tz: *mut timezone) -> i32;
}
struct Locale {
new_locale: libc::locale_t,
old_locale: libc::locale_t,
new_locale: locale_t,
old_locale: locale_t,
}
impl Drop for Locale {
@ -89,9 +83,9 @@ impl Drop for Locale {
// How to unit test this? Test machine is not guaranteed to have non-english locales.
impl Locale {
fn new(
mask: std::os::raw::c_int,
locale: *const std::os::raw::c_char,
base: libc::locale_t,
mask: ::std::os::raw::c_int,
locale: *const ::std::os::raw::c_char,
base: locale_t,
) -> Result<Self> {
let new_locale = unsafe { libc::newlocale(mask, locale, base) };
if new_locale.is_null() {
@ -109,58 +103,35 @@ impl Locale {
}
}
pub fn timestamp_to_string(timestamp: UnixTimestamp, fmt: Option<&str>, posix: bool) -> String {
let mut new_tm: libc::tm = unsafe { std::mem::zeroed() };
pub fn timestamp_to_string(timestamp: UnixTimestamp, fmt: Option<&str>) -> String {
let mut new_tm: ::libc::tm = unsafe { std::mem::zeroed() };
unsafe {
let i: i64 = timestamp.try_into().unwrap_or(0);
localtime_r(&i as *const i64, &mut new_tm as *mut libc::tm);
localtime_r(&i as *const i64, &mut new_tm as *mut ::libc::tm);
}
let format: Cow<'_, CStr> = if let Some(cs) = fmt
.map(str::as_bytes)
.map(CStr::from_bytes_with_nul)
.and_then(|res| res.ok())
{
Cow::from(cs)
} else if let Some(cstring) = fmt
.map(str::as_bytes)
let fmt = fmt
.map(CString::new)
.and_then(|res| res.ok())
{
Cow::from(cstring)
.map(|res| res.ok())
.and_then(|opt| opt);
let format: &CStr = if let Some(ref s) = fmt {
&s
} else {
unsafe { CStr::from_bytes_with_nul_unchecked(DEFAULT_FMT.as_bytes()).into() }
unsafe { CStr::from_bytes_with_nul_unchecked(b"%a, %d %b %Y %T %z\0") }
};
let mut vec: [u8; 256] = [0; 256];
let ret = {
let _with_locale: Option<Result<Locale>> = if posix {
Some(
Locale::new(
libc::LC_TIME,
b"C\0".as_ptr() as *const i8,
std::ptr::null_mut(),
)
.chain_err_summary(|| "Could not set locale for datetime conversion")
.chain_err_kind(crate::error::ErrorKind::External),
)
} else {
None
};
unsafe {
strftime(
vec.as_mut_ptr() as *mut _,
256,
format.as_ptr(),
&new_tm as *const _,
)
}
let ret = unsafe {
strftime(
vec.as_mut_ptr() as *mut _,
256,
format.as_ptr(),
&new_tm as *const _,
)
};
String::from_utf8_lossy(&vec[0..ret]).into_owned()
}
fn tm_to_secs(tm: libc::tm) -> std::result::Result<i64, ()> {
fn tm_to_secs(tm: ::libc::tm) -> std::result::Result<i64, ()> {
let mut is_leap = false;
let mut year = tm.tm_year;
let mut month = tm.tm_mon;
@ -273,58 +244,63 @@ where
T: Into<Vec<u8>>,
{
let s = CString::new(s)?;
let mut new_tm: libc::tm = unsafe { std::mem::zeroed() };
for fmt in &[RFC822_FMT_WITH_TIME, RFC822_FMT] {
let fmt = unsafe { CStr::from_bytes_with_nul_unchecked(fmt.as_bytes()) };
let ret = {
let _with_locale = Locale::new(
libc::LC_TIME,
b"C\0".as_ptr() as *const i8,
std::ptr::null_mut(),
)
.chain_err_summary(|| "Could not set locale for datetime conversion")
.chain_err_kind(crate::error::ErrorKind::External)?;
unsafe { strptime(s.as_ptr(), fmt.as_ptr(), &mut new_tm as *mut _) }
};
let mut new_tm: ::libc::tm = unsafe { std::mem::zeroed() };
for fmt in &[
&b"%a, %e %h %Y %H:%M:%S \0"[..],
&b"%e %h %Y %H:%M:%S \0"[..],
] {
unsafe {
let fmt = CStr::from_bytes_with_nul_unchecked(fmt);
if ret.is_null() {
continue;
}
let rest = unsafe { CStr::from_ptr(ret) };
let tm_gmtoff = if rest.to_bytes().len() > 4
&& rest.to_bytes().is_ascii()
&& rest.to_bytes()[1..5].iter().all(u8::is_ascii_digit)
{
// safe since rest.to_bytes().is_ascii()
let offset = unsafe { std::str::from_utf8_unchecked(&rest.to_bytes()[0..5]) };
if let (Ok(mut hr_offset), Ok(mut min_offset)) =
(offset[1..3].parse::<i64>(), offset[3..5].parse::<i64>())
{
if rest.to_bytes()[0] == b'-' {
hr_offset = -hr_offset;
min_offset = -min_offset;
}
hr_offset * 60 * 60 + min_offset * 60
} else {
0
}
} else {
let rest = if rest.to_bytes().starts_with(b"(") && rest.to_bytes().ends_with(b")") {
&rest.to_bytes()[1..rest.to_bytes().len() - 1]
} else {
rest.to_bytes()
let ret = {
let _with_locale = Locale::new(
::libc::LC_TIME,
b"C\0".as_ptr() as *const i8,
std::ptr::null_mut(),
)
.chain_err_summary(|| "Could not set locale for datetime conversion")
.chain_err_kind(crate::error::ErrorKind::External)?;
strptime(s.as_ptr(), fmt.as_ptr(), &mut new_tm as *mut _)
};
if let Ok(idx) = TIMEZONE_ABBR.binary_search_by(|probe| probe.0.cmp(rest)) {
let (hr_offset, min_offset) = TIMEZONE_ABBR[idx].1;
(hr_offset as i64) * 60 * 60 + (min_offset as i64) * 60
} else {
0
if ret.is_null() {
continue;
}
};
return Ok(tm_to_secs(new_tm)
.map(|res| (res - tm_gmtoff) as u64)
.unwrap_or(0));
let rest = CStr::from_ptr(ret);
let tm_gmtoff = if rest.to_bytes().len() > 4
&& rest.to_bytes().is_ascii()
&& rest.to_bytes()[1..5].iter().all(u8::is_ascii_digit)
{
let offset = std::str::from_utf8_unchecked(&rest.to_bytes()[0..5]);
if let (Ok(mut hr_offset), Ok(mut min_offset)) =
(offset[1..3].parse::<i64>(), offset[3..5].parse::<i64>())
{
if rest.to_bytes()[0] == b'-' {
hr_offset = -hr_offset;
min_offset = -min_offset;
}
hr_offset * 60 * 60 + min_offset * 60
} else {
0
}
} else {
let rest = if rest.to_bytes().starts_with(b"(") && rest.to_bytes().ends_with(b")") {
&rest.to_bytes()[1..rest.to_bytes().len() - 1]
} else {
rest.to_bytes()
};
if let Ok(idx) = TIMEZONE_ABBR.binary_search_by(|probe| probe.0.cmp(rest)) {
let (hr_offset, min_offset) = TIMEZONE_ABBR[idx].1;
(hr_offset as i64) * 60 * 60 + (min_offset as i64) * 60
} else {
0
}
};
return Ok(tm_to_secs(new_tm)
.map(|res| (res - tm_gmtoff) as u64)
.unwrap_or(0));
}
}
Ok(0)
}
@ -334,58 +310,59 @@ where
T: Into<Vec<u8>>,
{
let s = CString::new(s)?;
let mut new_tm: libc::tm = unsafe { std::mem::zeroed() };
for fmt in &[RFC3339_FMT_WITH_TIME, RFC3339_FMT] {
let fmt = unsafe { CStr::from_bytes_with_nul_unchecked(fmt.as_bytes()) };
let ret = {
let _with_locale = Locale::new(
libc::LC_TIME,
b"C\0".as_ptr() as *const i8,
std::ptr::null_mut(),
)
.chain_err_summary(|| "Could not set locale for datetime conversion")
.chain_err_kind(crate::error::ErrorKind::External)?;
unsafe { strptime(s.as_ptr(), fmt.as_ptr(), &mut new_tm as *mut _) }
};
if ret.is_null() {
continue;
}
let rest = unsafe { CStr::from_ptr(ret) };
let tm_gmtoff = if rest.to_bytes().len() > 4
&& rest.to_bytes().is_ascii()
&& rest.to_bytes()[1..3].iter().all(u8::is_ascii_digit)
&& rest.to_bytes()[4..6].iter().all(u8::is_ascii_digit)
{
// safe since rest.to_bytes().is_ascii()
let offset = unsafe { std::str::from_utf8_unchecked(&rest.to_bytes()[0..6]) };
if let (Ok(mut hr_offset), Ok(mut min_offset)) =
(offset[1..3].parse::<i64>(), offset[4..6].parse::<i64>())
{
if rest.to_bytes()[0] == b'-' {
hr_offset = -hr_offset;
min_offset = -min_offset;
}
hr_offset * 60 * 60 + min_offset * 60
} else {
0
}
} else {
let rest = if rest.to_bytes().starts_with(b"(") && rest.to_bytes().ends_with(b")") {
&rest.to_bytes()[1..rest.to_bytes().len() - 1]
} else {
rest.to_bytes()
let mut new_tm: ::libc::tm = unsafe { std::mem::zeroed() };
for fmt in &[&b"%Y-%m-%dT%H:%M:%S\0"[..], &b"%Y-%m-%d\0"[..]] {
unsafe {
let fmt = CStr::from_bytes_with_nul_unchecked(fmt);
let ret = {
let _with_locale = Locale::new(
::libc::LC_TIME,
b"C\0".as_ptr() as *const i8,
std::ptr::null_mut(),
)
.chain_err_summary(|| "Could not set locale for datetime conversion")
.chain_err_kind(crate::error::ErrorKind::External)?;
strptime(s.as_ptr(), fmt.as_ptr(), &mut new_tm as *mut _)
};
if let Ok(idx) = TIMEZONE_ABBR.binary_search_by(|probe| probe.0.cmp(rest)) {
let (hr_offset, min_offset) = TIMEZONE_ABBR[idx].1;
(hr_offset as i64) * 60 * 60 + (min_offset as i64) * 60
} else {
0
if ret.is_null() {
continue;
}
};
return Ok(tm_to_secs(new_tm)
.map(|res| (res - tm_gmtoff) as u64)
.unwrap_or(0));
let rest = CStr::from_ptr(ret);
let tm_gmtoff = if rest.to_bytes().len() > 4
&& rest.to_bytes().is_ascii()
&& rest.to_bytes()[1..3].iter().all(u8::is_ascii_digit)
&& rest.to_bytes()[4..6].iter().all(u8::is_ascii_digit)
{
let offset = std::str::from_utf8_unchecked(&rest.to_bytes()[0..6]);
if let (Ok(mut hr_offset), Ok(mut min_offset)) =
(offset[1..3].parse::<i64>(), offset[4..6].parse::<i64>())
{
if rest.to_bytes()[0] == b'-' {
hr_offset = -hr_offset;
min_offset = -min_offset;
}
hr_offset * 60 * 60 + min_offset * 60
} else {
0
}
} else {
let rest = if rest.to_bytes().starts_with(b"(") && rest.to_bytes().ends_with(b")") {
&rest.to_bytes()[1..rest.to_bytes().len() - 1]
} else {
rest.to_bytes()
};
if let Ok(idx) = TIMEZONE_ABBR.binary_search_by(|probe| probe.0.cmp(rest)) {
let (hr_offset, min_offset) = debug!(TIMEZONE_ABBR[idx]).1;
(hr_offset as i64) * 60 * 60 + (min_offset as i64) * 60
} else {
0
}
};
return Ok(tm_to_secs(new_tm)
.map(|res| (res - tm_gmtoff) as u64)
.unwrap_or(0));
}
}
Ok(0)
}
@ -395,12 +372,8 @@ pub fn timestamp_from_string<T>(s: T, fmt: &str) -> Result<Option<UnixTimestamp>
where
T: Into<Vec<u8>>,
{
let mut new_tm: libc::tm = unsafe { std::mem::zeroed() };
let fmt: Cow<'_, CStr> = if let Ok(cs) = CStr::from_bytes_with_nul(fmt.as_bytes()) {
Cow::from(cs)
} else {
Cow::from(CString::new(fmt.as_bytes())?)
};
let mut new_tm: ::libc::tm = unsafe { std::mem::zeroed() };
let fmt = CString::new(fmt)?;
unsafe {
let ret = strptime(
CString::new(s)?.as_ptr(),
@ -416,8 +389,8 @@ where
pub fn now() -> UnixTimestamp {
use std::mem::MaybeUninit;
let mut tv = MaybeUninit::<libc::timeval>::uninit();
let mut tz = MaybeUninit::<libc::timezone>::uninit();
let mut tv = MaybeUninit::<::libc::timeval>::uninit();
let mut tz = MaybeUninit::<::libc::timezone>::uninit();
unsafe {
let ret = gettimeofday(tv.as_mut_ptr(), tz.as_mut_ptr());
if ret == -1 {
@ -428,12 +401,12 @@ pub fn now() -> UnixTimestamp {
}
#[test]
fn test_datetime_timestamp() {
timestamp_to_string(0, None, false);
fn test_timestamp() {
timestamp_to_string(0, None);
}
#[test]
fn test_datetime_rfcs() {
fn test_rfcs() {
if unsafe { libc::setlocale(libc::LC_ALL, b"\0".as_ptr() as _) }.is_null() {
println!("Unable to set locale.");
}
@ -447,7 +420,7 @@ fn test_datetime_rfcs() {
/*
macro_rules! mkt {
($year:literal, $month:literal, $day:literal, $hour:literal, $minute:literal, $second:literal) => {
libc::tm {
::libc::tm {
tm_sec: $second,
tm_min: $minute,
tm_hour: $hour,

View File

@ -30,11 +30,11 @@
* ```
* use melib::{Attachment, Envelope};
*
* let raw_mail = r#"From: "some name" <some@example.com>
* To: "me" <myself@example.com>
* let raw_mail = r#"From: "some name" <some@address.com>
* To: "me" <myself@i.tld>
* Cc:
* Subject: =?utf-8?Q?gratuitously_encoded_subject?=
* Message-ID: <h2g7f.z0gy2pgaen5m@example.com>
* Message-ID: <h2g7f.z0gy2pgaen5m@address.com>
* MIME-Version: 1.0
* Content-Type: multipart/mixed; charset="utf-8";
* boundary="bzz_bzz__bzz__"
@ -75,7 +75,7 @@
*
* let envelope = Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
* assert_eq!(envelope.subject().as_ref(), "gratuitously encoded subject");
* assert_eq!(envelope.message_id_display().as_ref(), "<h2g7f.z0gy2pgaen5m@example.com>");
* assert_eq!(envelope.message_id_display().as_ref(), "<h2g7f.z0gy2pgaen5m@address.com>");
*
* let body = envelope.body_bytes(raw_mail.as_bytes());
* assert_eq!(body.content_type().to_string().as_str(), "multipart/mixed");
@ -143,23 +143,6 @@ impl PartialEq<&str> for Flag {
}
}
macro_rules! flag_impl {
(fn $name:ident, $val:expr) => {
pub const fn $name(&self) -> bool {
self.contains($val)
}
};
}
impl Flag {
flag_impl!(fn is_seen, Flag::SEEN);
flag_impl!(fn is_draft, Flag::DRAFT);
flag_impl!(fn is_trashed, Flag::TRASHED);
flag_impl!(fn is_passed, Flag::PASSED);
flag_impl!(fn is_replied, Flag::REPLIED);
flag_impl!(fn is_flagged, Flag::FLAGGED);
}
///`Mail` holds both the envelope info of an email in its `envelope` field and the raw bytes that
///describe the email in `bytes`. Its body as an `melib::email::Attachment` can be parsed on demand
///with the `melib::email::Mail::body` method.

View File

@ -53,7 +53,7 @@ impl Default for Draft {
let mut headers = HeaderMap::default();
headers.insert(
HeaderName::new_unchecked("Date"),
crate::datetime::timestamp_to_string(crate::datetime::now(), None, true),
crate::datetime::timestamp_to_string(crate::datetime::now(), None),
);
headers.insert(HeaderName::new_unchecked("From"), "".into());
headers.insert(HeaderName::new_unchecked("To"), "".into());

View File

@ -41,11 +41,7 @@ pub mod dbg {
() => {
eprint!(
"[{}][{:?}] {}:{}_{}: ",
crate::datetime::timestamp_to_string(
crate::datetime::now(),
Some("%Y-%m-%d %T"),
false
),
crate::datetime::timestamp_to_string(crate::datetime::now(), Some("%Y-%m-%d %T")),
std::thread::current()
.name()
.map(std::string::ToString::to_string)

View File

@ -85,8 +85,7 @@ pub fn log<S: AsRef<str>>(val: S, level: LoggingLevel) {
if level <= b.level {
b.dest
.write_all(
crate::datetime::timestamp_to_string(crate::datetime::now(), None, false)
.as_bytes(),
crate::datetime::timestamp_to_string(crate::datetime::now(), None).as_bytes(),
)
.unwrap();
b.dest.write_all(b" [").unwrap();

View File

@ -44,6 +44,7 @@ pub use iterators::*;
use crate::text_processing::grapheme_clusters::*;
use uuid::Uuid;
use std::cell::RefCell;
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::fmt;
@ -130,7 +131,7 @@ macro_rules! make {
e.parent = Some($p);
});
let old_group = std::mem::replace($threads.groups.entry(old_group_hash).or_default(), ThreadGroup::Node {
parent: Arc::new(RwLock::new(parent_group_hash)),
parent: RefCell::new(parent_group_hash),
});
$threads.thread_nodes.entry($c).and_modify(|e| {
e.group = parent_group_hash;
@ -291,7 +292,7 @@ pub struct Thread {
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum ThreadGroup {
Root(Thread),
Node { parent: Arc<RwLock<ThreadHash>> },
Node { parent: RefCell<ThreadHash> },
}
impl Default for ThreadGroup {
@ -410,16 +411,16 @@ impl ThreadNode {
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct Threads {
pub thread_nodes: HashMap<ThreadNodeHash, ThreadNode>,
root_set: Arc<RwLock<Vec<ThreadNodeHash>>>,
tree_index: Arc<RwLock<Vec<ThreadNodeHash>>>,
root_set: RefCell<Vec<ThreadNodeHash>>,
tree_index: RefCell<Vec<ThreadNodeHash>>,
pub groups: HashMap<ThreadHash, ThreadGroup>,
message_ids: HashMap<Vec<u8>, ThreadNodeHash>,
pub message_ids_set: HashSet<Vec<u8>>,
pub missing_message_ids: HashSet<Vec<u8>>,
pub hash_set: HashSet<EnvelopeHash>,
sort: Arc<RwLock<(SortField, SortOrder)>>,
subsort: Arc<RwLock<(SortField, SortOrder)>>,
sort: RefCell<(SortField, SortOrder)>,
subsort: RefCell<(SortField, SortOrder)>,
}
impl PartialEq for ThreadNode {
@ -453,13 +454,13 @@ impl Threads {
pub fn find_group(&self, h: ThreadHash) -> ThreadHash {
let p = match self.groups[&h] {
ThreadGroup::Root(_) => return h,
ThreadGroup::Node { ref parent } => *parent.read().unwrap(),
ThreadGroup::Node { ref parent } => *parent.borrow(),
};
let parent_group = self.find_group(p);
match self.groups[&h] {
ThreadGroup::Node { ref parent } => {
*parent.write().unwrap() = parent_group;
*parent.borrow_mut() = parent_group;
}
_ => unreachable!(),
}
@ -490,8 +491,8 @@ impl Threads {
message_ids_set,
missing_message_ids,
hash_set,
sort: Arc::new(RwLock::new((SortField::Date, SortOrder::Desc))),
subsort: Arc::new(RwLock::new((SortField::Subject, SortOrder::Desc))),
sort: RefCell::new((SortField::Date, SortOrder::Desc)),
subsort: RefCell::new((SortField::Subject, SortOrder::Desc)),
..Default::default()
}
@ -572,7 +573,7 @@ impl Threads {
};
if self.thread_nodes[&t_id].parent.is_none() {
let mut tree_index = self.tree_index.write().unwrap();
let mut tree_index = self.tree_index.borrow_mut();
if let Some(i) = tree_index.iter().position(|t| *t == t_id) {
tree_index.remove(i);
}
@ -844,7 +845,7 @@ impl Threads {
/*
save_graph(
&self.tree_index.read().unwrap(),
&self.tree_index.borrow(),
&self.thread_nodes,
&self
.message_ids
@ -870,7 +871,7 @@ impl Threads {
ref thread_nodes,
..
} = self;
let tree = &mut tree_index.write().unwrap();
let tree = &mut tree_index.borrow_mut();
for t in tree.iter_mut() {
thread_nodes[t].children.sort_by(|a, b| match subsort {
(SortField::Date, SortOrder::Desc) => {
@ -1089,7 +1090,7 @@ impl Threads {
});
}
fn inner_sort_by(&self, sort: (SortField, SortOrder), envelopes: &Envelopes) {
let tree = &mut self.tree_index.write().unwrap();
let tree = &mut self.tree_index.borrow_mut();
let envelopes = envelopes.read().unwrap();
tree.sort_by(|a, b| match sort {
(SortField::Date, SortOrder::Desc) => {
@ -1171,13 +1172,13 @@ impl Threads {
subsort: (SortField, SortOrder),
envelopes: &Envelopes,
) {
if *self.sort.read().unwrap() != sort {
if *self.sort.borrow() != sort {
self.inner_sort_by(sort, envelopes);
*self.sort.write().unwrap() = sort;
*self.sort.borrow_mut() = sort;
}
if *self.subsort.read().unwrap() != subsort {
if *self.subsort.borrow() != subsort {
self.inner_subsort_by(subsort, envelopes);
*self.subsort.write().unwrap() = subsort;
*self.subsort.borrow_mut() = subsort;
}
}
@ -1195,11 +1196,11 @@ impl Threads {
}
pub fn root_len(&self) -> usize {
self.tree_index.read().unwrap().len()
self.tree_index.borrow().len()
}
pub fn root_set(&self, idx: usize) -> ThreadNodeHash {
self.tree_index.read().unwrap()[idx]
self.tree_index.borrow()[idx]
}
pub fn roots(&self) -> SmallVec<[ThreadHash; 1024]> {

View File

@ -419,19 +419,6 @@ define_commands!([
}
)
},
{ tags: ["export-mbox "],
desc: "export-mbox PATH",
tokens: &[One(Literal("export-mbox")), One(Filepath)],
parser:(
fn export_mbox(input: &[u8]) -> IResult<&[u8], Action> {
let (input, _) = tag("export-mbox")(input.trim())?;
let (input, _) = is_a(" ")(input)?;
let (input, path) = quoted_argument(input.trim())?;
let (input, _) = eof(input)?;
Ok((input, Listing(ExportMbox(Some(melib::backends::mbox::MboxFormat::MboxCl2), path.to_string().into()))))
}
)
},
{ tags: ["list-archive", "list-post", "list-unsubscribe", "list-"],
desc: "list-[unsubscribe/post/archive]",
tokens: &[One(Alternatives(&[to_stream!(One(Literal("list-archive"))), to_stream!(One(Literal("list-post"))), to_stream!(One(Literal("list-unsubscribe")))]))],
@ -865,7 +852,6 @@ fn listing_action(input: &[u8]) -> IResult<&[u8], Action> {
select,
toggle_thread_snooze,
open_in_new_tab,
export_mbox,
_tag,
))(input)
}

View File

@ -51,7 +51,6 @@ pub enum ListingAction {
MoveTo(MailboxPath),
MoveToOtherAccount(AccountName, MailboxPath),
Import(PathBuf, MailboxPath),
ExportMbox(Option<melib::backends::mbox::MboxFormat>, PathBuf),
Delete,
OpenInNewTab,
Tag(TagAction),

View File

@ -66,22 +66,6 @@ pub enum PageMovement {
End,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ScrollContext {
shown_lines: usize,
total_lines: usize,
has_more_lines: bool,
}
#[derive(Debug, Clone, Copy)]
pub enum ScrollUpdate {
End(ComponentId),
Update {
id: ComponentId,
context: ScrollContext,
},
}
/// Types implementing this Trait can draw on the terminal and receive events.
/// If a type wants to skip drawing if it has not changed anything, it can hold some flag in its
/// fields (eg self.dirty = false) and act upon that in their `draw` implementation.

View File

@ -414,27 +414,6 @@ impl ContactList {
let top_idx = page_no * rows;
if self.length >= rows {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::Update {
id: self.id,
context: ScrollContext {
shown_lines: top_idx + rows,
total_lines: self.length,
has_more_lines: false,
},
},
)));
} else {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
}
/* If cursor position has changed, remove the highlight from the previous position and
* apply it in the new one. */
if self.cursor_pos != self.new_cursor_pos && prev_page_no == page_no {
@ -642,11 +621,6 @@ impl Component for ContactList {
self.mode = ViewMode::View(manager.id());
self.view = Some(manager);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
return true;
}
@ -665,11 +639,6 @@ impl Component for ContactList {
self.mode = ViewMode::View(manager.id());
self.view = Some(manager);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
return true;
}

View File

@ -31,7 +31,6 @@ use indexmap::IndexSet;
use nix::sys::wait::WaitStatus;
use std::convert::TryInto;
use std::future::Future;
use std::pin::Pin;
use std::process::{Command, Stdio};
use std::str::FromStr;
use std::sync::{Arc, Mutex};
@ -315,21 +314,10 @@ impl Composer {
}
}
ret.draft.body = {
let mut ret = attribution_string(
account_settings!(
context[ret.account_hash]
.composing
.attribution_format_string
)
.as_ref()
.map(|s| s.as_str()),
envelope.from().get(0),
envelope.date(),
*account_settings!(
context[ret.account_hash]
.composing
.attribution_use_posix_locale
),
let mut ret = format!(
"On {} {} wrote:\n",
envelope.date_as_str(),
envelope.from()[0],
);
for l in reply_body.lines() {
ret.push('>');
@ -937,9 +925,6 @@ impl Component for Composer {
}
fn process_event(&mut self, mut event: &mut UIEvent, context: &mut Context) -> bool {
if let UIEvent::VisibilityChange(_) = event {
self.pager.process_event(event, context);
}
let shortcuts = self.get_shortcuts(context);
match (&mut self.mode, &mut event) {
(ViewMode::Edit, _) => {
@ -2222,36 +2207,3 @@ pub fn send_draft_async(
ret
}))
}
/* Sender details
* %+f the sender's name and email address.
* %+n the sender's name (or email address, if no name is included).
* %+a the sender's email address.
*/
fn attribution_string(
fmt: Option<&str>,
sender: Option<&Address>,
date: UnixTimestamp,
posix: bool,
) -> String {
let fmt = fmt.unwrap_or("On %a, %0e %b %Y %H:%M, %+f wrote:%n");
let fmt = fmt.replace(
"%+f",
&sender
.map(|addr| addr.to_string())
.unwrap_or_else(|| "\"\"".to_string()),
);
let fmt = fmt.replace(
"%+n",
&sender
.map(|addr| addr.get_display_name().unwrap_or_else(|| addr.get_email()))
.unwrap_or_else(|| "\"\"".to_string()),
);
let fmt = fmt.replace(
"%+a",
&sender
.map(|addr| addr.get_email())
.unwrap_or_else(|| "\"\"".to_string()),
);
melib::datetime::timestamp_to_string(date, Some(fmt.as_str()), posix)
}

View File

@ -342,103 +342,6 @@ pub trait MailListingTrait: ListingTrait {
}
}
}
ListingAction::ExportMbox(format, ref path) => {
use futures::future::try_join_all;
use std::future::Future;
use std::io::Write;
use std::pin::Pin;
let futures: Result<Vec<_>> = envs_to_set
.iter()
.map(|&env_hash| account.operation(env_hash).and_then(|mut op| op.as_bytes()))
.collect::<Result<Vec<_>>>();
let path_ = path.to_path_buf();
let format = format.clone().unwrap_or_default();
let collection = account.collection.clone();
let (sender, mut receiver) = crate::jobs::oneshot::channel();
let fut: Pin<Box<dyn Future<Output = Result<()>> + Send + 'static>> =
Box::pin(async move {
let cl = async move {
use melib::backends::mbox::MboxMetadata;
let bytes: Vec<Vec<u8>> = try_join_all(futures?).await?;
let envs: Vec<_> = envs_to_set
.iter()
.map(|&env_hash| collection.get_env(env_hash))
.collect();
let mut file = std::io::BufWriter::new(std::fs::File::create(&path_)?);
let mut iter = envs.iter().zip(bytes.into_iter());
let tags_lck = collection.tag_index.read().unwrap();
if let Some((env, ref bytes)) = iter.next() {
let tags: Vec<&str> = env
.labels()
.iter()
.filter_map(|h| tags_lck.get(h).map(|s| s.as_str()))
.collect();
format.append(
&mut file,
bytes.as_slice(),
env.from().get(0),
Some(env.date()),
(env.flags(), tags),
MboxMetadata::CClient,
true,
false,
)?;
}
for (env, bytes) in iter {
let tags: Vec<&str> = env
.labels()
.iter()
.filter_map(|h| tags_lck.get(h).map(|s| s.as_str()))
.collect();
format.append(
&mut file,
bytes.as_slice(),
env.from().get(0),
Some(env.date()),
(env.flags(), tags),
MboxMetadata::CClient,
false,
false,
)?;
}
file.flush()?;
Ok(())
};
let r: Result<()> = cl.await;
let _ = sender.send(r);
Ok(())
});
let handle = account.job_executor.spawn_blocking(fut);
let path = path.to_path_buf();
account.insert_job(
handle.job_id,
JobRequest::Generic {
name: "exporting mbox".into(),
handle,
on_finish: Some(CallbackFn(Box::new(move |context: &mut Context| {
context.replies.push_back(match receiver.try_recv() {
Err(_) | Ok(None) => UIEvent::Notification(
Some("Could not export mbox".to_string()),
"Job was canceled.".to_string(),
Some(NotificationType::Info),
),
Ok(Some(Err(err))) => UIEvent::Notification(
Some("Could not export mbox".to_string()),
err.to_string(),
Some(NotificationType::Error(err.kind)),
),
Ok(Some(Ok(()))) => UIEvent::Notification(
Some("Succesfully exported mbox".to_string()),
format!("Wrote to file {}", path.display()),
Some(NotificationType::Info),
),
});
}))),
logging_level: melib::LoggingLevel::INFO,
},
);
}
ListingAction::MoveToOtherAccount(ref _account_name, ref _mailbox_path) => {
context
.replies
@ -800,8 +703,6 @@ impl Component for Listing {
fallback = *cur;
}
if self.component.coordinates() == (*account_hash, *mailbox_hash) {
self.component
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.component.set_coordinates((
self.accounts[self.cursor_pos.0].hash,
self.accounts[self.cursor_pos.0].entries[fallback].3,
@ -829,8 +730,6 @@ impl Component for Listing {
let account_hash = self.accounts[self.cursor_pos.0].hash;
self.cursor_pos.1 = MenuEntryCursor::Mailbox(*idx);
self.status = None;
self.component
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.component
.set_coordinates((account_hash, *mailbox_hash));
self.menu_content.empty();
@ -1064,7 +963,6 @@ impl Component for Listing {
| Action::Listing(a @ ListingAction::MoveTo(_))
| Action::Listing(a @ ListingAction::CopyToOtherAccount(_, _))
| Action::Listing(a @ ListingAction::MoveToOtherAccount(_, _))
| Action::Listing(a @ ListingAction::ExportMbox(_, _))
| Action::Listing(a @ ListingAction::Tag(_)) => {
let focused = self.component.get_focused_items(context);
self.component.perform_action(context, focused, a);
@ -1250,11 +1148,6 @@ impl Component for Listing {
match *event {
UIEvent::Input(Key::Right) => {
self.focus = ListingFocus::Mailbox;
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
self.ratio = 90;
self.set_dirty(true);
return true;
@ -1268,11 +1161,6 @@ impl Component for Listing {
self.set_dirty(true);
self.focus = ListingFocus::Mailbox;
self.ratio = 90;
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
return true;
}
UIEvent::Input(ref k)
@ -1283,11 +1171,6 @@ impl Component for Listing {
self.focus = ListingFocus::Mailbox;
self.ratio = 90;
self.set_dirty(true);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
@ -1767,20 +1650,6 @@ impl Listing {
),
);
if self.show_menu_scrollbar == ShowMenuScrollbar::True && total_height > rows {
if self.focus == ListingFocus::Menu {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::Update {
id: self.id,
context: ScrollContext {
shown_lines: skip_offset + rows,
total_lines: total_height,
has_more_lines: false,
},
},
)));
}
ScrollBar::default().set_show_arrows(true).draw(
grid,
(
@ -1795,12 +1664,6 @@ impl Listing {
/* length */
total_height,
);
} else if total_height < rows {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
}
context.dirty_areas.push_back(area);
@ -2101,8 +1964,6 @@ impl Listing {
if let Some((_, _, _, mailbox_hash)) =
self.accounts[self.cursor_pos.0].entries.get(idx)
{
self.component
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.component
.set_coordinates((account_hash, *mailbox_hash));
/* Check if per-mailbox configuration overrides general configuration */

View File

@ -895,9 +895,9 @@ impl CompactListing {
let thread = threads.thread_ref(hash);
let mut tags = String::new();
let mut colors: SmallVec<[_; 8]> = SmallVec::new();
let account = &context.accounts[&self.cursor_pos.0];
if account.backend_capabilities.supports_tags {
let tags_lck = account.collection.tag_index.read().unwrap();
let backend_lck = context.accounts[&self.cursor_pos.0].backend.read().unwrap();
if let Some(t) = backend_lck.tags() {
let tags_lck = t.read().unwrap();
for t in e.labels().iter() {
if mailbox_settings!(
context[self.cursor_pos.0][&self.cursor_pos.1]
@ -1627,8 +1627,6 @@ impl Component for CompactListing {
) =>
{
self.unfocused = false;
self.view
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.dirty = true;
/* If self.row_updates is not empty and we exit a thread, the row_update events
* will be performed but the list will not be drawn. So force a draw in any case.

View File

@ -908,9 +908,9 @@ impl ConversationsListing {
let thread = threads.thread_ref(hash);
let mut tags = String::new();
let mut colors = SmallVec::new();
let account = &context.accounts[&self.cursor_pos.0];
if account.backend_capabilities.supports_tags {
let tags_lck = account.collection.tag_index.read().unwrap();
let backend_lck = context.accounts[&self.cursor_pos.0].backend.read().unwrap();
if let Some(t) = backend_lck.tags() {
let tags_lck = t.read().unwrap();
for t in e.labels().iter() {
if mailbox_settings!(
context[self.cursor_pos.0][&self.cursor_pos.1]
@ -1002,7 +1002,6 @@ impl ConversationsListing {
.as_ref()
.map(String::as_str)
.or(Some("%Y-%m-%d %T")),
false,
),
}
}
@ -1491,8 +1490,6 @@ impl Component for ConversationsListing {
) =>
{
self.unfocused = false;
self.view
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.dirty = true;
/* If self.row_updates is not empty and we exit a thread, the row_update events
* will be performed but the list will not be drawn. So force a draw in any case.

View File

@ -732,9 +732,9 @@ impl PlainListing {
fn make_entry_string(&self, e: EnvelopeRef, context: &Context) -> EntryStrings {
let mut tags = String::new();
let mut colors = SmallVec::new();
let account = &context.accounts[&self.cursor_pos.0];
if account.backend_capabilities.supports_tags {
let tags_lck = account.collection.tag_index.read().unwrap();
let backend_lck = context.accounts[&self.cursor_pos.0].backend.read().unwrap();
if let Some(t) = backend_lck.tags() {
let tags_lck = t.read().unwrap();
for t in e.labels().iter() {
if mailbox_settings!(
context[self.cursor_pos.0][&self.cursor_pos.1]
@ -1010,7 +1010,7 @@ impl PlainListing {
n if n < 4 * 24 * 60 * 60 => {
format!("{} days ago{}", n / (24 * 60 * 60), " ".repeat(9))
}
_ => melib::datetime::timestamp_to_string(envelope.datetime(), None, false),
_ => melib::datetime::timestamp_to_string(envelope.datetime(), None),
}
}
@ -1146,8 +1146,6 @@ impl Component for PlainListing {
&& shortcut!(k == shortcuts[PlainListing::DESCRIPTION]["exit_thread"]) =>
{
self.unfocused = false;
self.view
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.dirty = true;
/* If self.row_updates is not empty and we exit a thread, the row_update events
* will be performed but the list will not be drawn. So force a draw in any case.

View File

@ -839,9 +839,9 @@ impl ThreadListing {
fn make_entry_string(&self, e: &Envelope, context: &Context) -> EntryStrings {
let mut tags = String::new();
let mut colors: SmallVec<[_; 8]> = SmallVec::new();
let account = &context.accounts[&self.cursor_pos.0];
if account.backend_capabilities.supports_tags {
let tags_lck = account.collection.tag_index.read().unwrap();
let backend_lck = context.accounts[&self.cursor_pos.0].backend.read().unwrap();
if let Some(t) = backend_lck.tags() {
let tags_lck = t.read().unwrap();
for t in e.labels().iter() {
if mailbox_settings!(
context[self.cursor_pos.0][&self.cursor_pos.1]
@ -1224,9 +1224,6 @@ impl Component for ThreadListing {
}
UIEvent::Input(Key::Char('i')) if self.unfocused => {
self.unfocused = false;
if let Some(ref mut s) = self.view {
s.process_event(&mut UIEvent::VisibilityChange(false), context);
}
self.dirty = true;
self.view = None;
return true;

View File

@ -188,27 +188,29 @@ impl Component for NotificationCommand {
fn draw(&mut self, _grid: &mut CellBuffer, _area: Area, _context: &mut Context) {}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
if !context.settings.notifications.enable {
return false;
}
if let UIEvent::Notification(ref title, ref body, ref kind) = event {
if context.settings.notifications.enable {
if let Some(ref bin) = context.settings.notifications.script {
match Command::new(bin)
.arg(&kind.map(|k| k.to_string()).unwrap_or_default())
.arg(title.as_ref().map(String::as_str).unwrap_or("meli"))
.arg(body)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
{
Ok(child) => {
context.children.push(child);
}
Err(err) => {
log(
format!("Could not run notification script: {}.", err.to_string()),
ERROR,
);
debug!("Could not run notification script: {:?}", err);
}
if let Some(ref bin) = context.settings.notifications.script {
match Command::new(bin)
.arg(&kind.map(|k| k.to_string()).unwrap_or_default())
.arg(title.as_ref().map(String::as_str).unwrap_or("meli"))
.arg(body)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
{
Ok(child) => {
context.children.push(child);
}
Err(err) => {
log(
format!("Could not run notification script: {}.", err.to_string()),
ERROR,
);
debug!("Could not run notification script: {:?}", err);
}
}
}

View File

@ -393,7 +393,6 @@ impl Component for SVGScreenshotFilter {
let mut filename = melib::datetime::timestamp_to_string(
melib::datetime::now(),
Some("meli Screenshot - %e %h %Y %H:%M:%S.svg"),
true,
);
while std::path::Path::new(&filename).exists() {
filename.pop();

View File

@ -64,7 +64,6 @@ pub struct StatusBar {
progress_spinner: ProgressSpinner,
in_progress_jobs: HashSet<JobId>,
done_jobs: HashSet<JobId>,
scroll_contexts: IndexMap<ComponentId, ScrollContext>,
auto_complete: AutoComplete,
cmd_history: Vec<String>,
@ -78,16 +77,13 @@ impl fmt::Display for StatusBar {
impl StatusBar {
pub fn new(context: &Context, container: Box<dyn Component>) -> Self {
let mut progress_spinner = ProgressSpinner::new(20, context);
let mut progress_spinner = ProgressSpinner::new(19, context);
match context.settings.terminal.progress_spinner_sequence.as_ref() {
Some(conf::terminal::ProgressSpinnerSequence::Integer(k)) => {
progress_spinner.set_kind(*k);
}
Some(conf::terminal::ProgressSpinnerSequence::Custom {
ref frames,
ref interval_ms,
}) => {
progress_spinner.set_custom_kind(frames.clone(), *interval_ms);
Some(conf::terminal::ProgressSpinnerSequence::Custom(ref s)) => {
progress_spinner.set_custom_kind(s.clone());
}
None => {}
}
@ -108,7 +104,6 @@ impl StatusBar {
progress_spinner,
in_progress_jobs: HashSet::default(),
done_jobs: HashSet::default(),
scroll_contexts: IndexMap::default(),
cmd_history: crate::command::history::old_cmd_history(),
}
}
@ -145,33 +140,6 @@ impl StatusBar {
grid[(x, y)].set_attrs(attribute.attrs | Attr::BOLD);
}
}
if let Some((
_,
ScrollContext {
shown_lines,
total_lines,
has_more_lines,
},
)) = self.scroll_contexts.last()
{
let s = format!(
"| {shown_percentage}% {line_desc}{shown_lines}/{total_lines}{has_more_lines}",
line_desc = if grid.ascii_drawing { "lines:" } else { "" },
shown_percentage = (*shown_lines as f32 / (*total_lines as f32) * 100.0) as usize,
shown_lines = *shown_lines,
total_lines = *total_lines,
has_more_lines = if *has_more_lines { "(+)" } else { "" }
);
write_string_to_grid(
&s,
grid,
attribute.fg,
attribute.bg,
attribute.attrs,
((x + 1, y), bottom_right!(area)),
None,
);
}
let (mut x, y) = bottom_right!(area);
if self.progress_spinner.is_active() {
@ -475,16 +443,13 @@ impl Component for StatusBar {
match event {
UIEvent::ConfigReload { old_settings: _ } => {
let mut progress_spinner = ProgressSpinner::new(20, context);
let mut progress_spinner = ProgressSpinner::new(19, context);
match context.settings.terminal.progress_spinner_sequence.as_ref() {
Some(conf::terminal::ProgressSpinnerSequence::Integer(k)) => {
progress_spinner.set_kind(*k);
}
Some(conf::terminal::ProgressSpinnerSequence::Custom {
ref frames,
ref interval_ms,
}) => {
progress_spinner.set_custom_kind(frames.clone(), *interval_ms);
Some(conf::terminal::ProgressSpinnerSequence::Custom(ref s)) => {
progress_spinner.set_custom_kind(s.clone());
}
None => {}
}
@ -741,21 +706,6 @@ impl Component for StatusBar {
self.progress_spinner.set_dirty(true);
self.in_progress_jobs.insert(*job_id);
}
UIEvent::StatusEvent(StatusEvent::ScrollUpdate(ScrollUpdate::End(component_id))) => {
if self.scroll_contexts.remove(component_id).is_some() {
self.dirty = true;
}
return true;
}
UIEvent::StatusEvent(StatusEvent::ScrollUpdate(ScrollUpdate::Update {
id,
context,
})) => {
if self.scroll_contexts.insert(*id, *context) != Some(*context) {
self.dirty = true;
}
return true;
}
UIEvent::Timer(_) => {
if self.progress_spinner.process_event(event, context) {
return true;
@ -1025,21 +975,6 @@ impl Component for Tabbed {
),
);
if height.wrapping_div(rows + 1) > 0 || width.wrapping_div(cols + 1) > 0 {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::Update {
id: self.id,
context: ScrollContext {
shown_lines: std::cmp::min(
(height).saturating_sub(rows + 1),
self.help_screen_cursor.1,
) + rows,
total_lines: height,
has_more_lines: false,
},
},
)));
ScrollBar::default().set_show_arrows(true).draw(
grid,
(
@ -1054,12 +989,6 @@ impl Component for Tabbed {
/* length */
height,
);
} else {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
}
self.dirty = false;
return;
@ -1263,21 +1192,6 @@ impl Component for Tabbed {
),
);
if height.wrapping_div(rows + 1) > 0 || width.wrapping_div(cols + 1) > 0 {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::Update {
id: self.id,
context: ScrollContext {
shown_lines: std::cmp::min(
(height).saturating_sub(rows),
self.help_screen_cursor.1,
) + rows,
total_lines: height,
has_more_lines: false,
},
},
)));
ScrollBar::default().set_show_arrows(true).draw(
grid,
(
@ -1292,12 +1206,6 @@ impl Component for Tabbed {
/* length */
height,
);
} else {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
}
}
self.dirty = false;
@ -1311,9 +1219,7 @@ impl Component for Tabbed {
}
UIEvent::Input(Key::Alt(no)) if *no >= '1' && *no <= '9' => {
let no = *no as usize - '1' as usize;
if no < self.children.len() && self.cursor_pos != no % self.children.len() {
self.children[self.cursor_pos]
.process_event(&mut UIEvent::VisibilityChange(false), context);
if no < self.children.len() {
self.cursor_pos = no % self.children.len();
let mut children_maps = self.children[self.cursor_pos].get_shortcuts(context);
children_maps.extend(self.get_shortcuts(context));
@ -1328,8 +1234,6 @@ impl Component for Tabbed {
return true;
}
UIEvent::Input(ref key) if shortcut!(key == shortcuts["general"]["next_tab"]) => {
self.children[self.cursor_pos]
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.cursor_pos = (self.cursor_pos + 1) % self.children.len();
let mut children_maps = self.children[self.cursor_pos].get_shortcuts(context);
children_maps.extend(self.get_shortcuts(context));
@ -1346,11 +1250,6 @@ impl Component for Tabbed {
if self.show_shortcuts {
/* children below the shortcut overlay must be redrawn */
self.set_dirty(true);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
}
self.show_shortcuts = !self.show_shortcuts;
self.dirty = true;
@ -1358,8 +1257,6 @@ impl Component for Tabbed {
}
UIEvent::Action(Tab(New(ref mut e))) if e.is_some() => {
self.add_component(e.take().unwrap());
self.children[self.cursor_pos]
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.cursor_pos = self.children.len() - 1;
self.children[self.cursor_pos].set_dirty(true);
let mut children_maps = self.children[self.cursor_pos].get_shortcuts(context);
@ -1384,8 +1281,6 @@ impl Component for Tabbed {
return true;
}
if let Some(c_idx) = self.children.iter().position(|x| x.id() == *id) {
self.children[c_idx]
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.children.remove(c_idx);
self.cursor_pos = 0;
self.set_dirty(true);

View File

@ -533,30 +533,28 @@ impl Component for Pager {
}
if (rows < height) || self.search.is_some() {
const RESULTS_STR: &str = "Results for ";
let shown_percentage =
((self.cursor.1 + rows) as f32 / (height as f32) * 100.0) as usize;
let shown_lines = self.cursor.1 + rows;
let total_lines = height;
if rows < height {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::Update {
id: self.id,
context: ScrollContext {
shown_lines,
total_lines,
has_more_lines: !self.line_breaker.is_finished(),
},
},
)));
let scrolling = if rows < height {
format!(
"{shown_percentage}% {line_desc}{shown_lines}/{total_lines}{has_more_lines}",
line_desc = if grid.ascii_drawing { "lines:" } else { "" },
shown_percentage = shown_percentage,
shown_lines = shown_lines,
total_lines = total_lines,
has_more_lines = if self.line_breaker.is_finished() {
""
} else {
"(+)"
}
)
} else {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
String::new()
};
if let Some(ref search) = self.search {
let status_message = format!(
let search_results = if let Some(ref search) = self.search {
format!(
"{results_str}{search_pattern}: {current_pos}/{total_results}{has_more_lines}",
results_str = RESULTS_STR,
search_pattern = &search.pattern,
@ -571,29 +569,39 @@ impl Component for Pager {
} else {
"(+)"
}
);
let mut attribute = crate::conf::value(context, "status.bar");
if !context.settings.terminal.use_color() {
attribute.attrs |= Attr::REVERSE;
}
let (_, y) = write_string_to_grid(
&status_message,
grid,
attribute.fg,
attribute.bg,
attribute.attrs,
(
set_y(upper_left!(area), get_y(bottom_right!(area))),
bottom_right!(area),
),
None,
);
/* set search pattern to italics */
)
} else {
String::new()
};
let status_message = format!(
"{search_results}{divider}{scrolling}",
search_results = search_results,
divider = if self.search.is_some() { " " } else { "" },
scrolling = scrolling,
);
let mut attribute = crate::conf::value(context, "status.bar");
if !context.settings.terminal.use_color() {
attribute.attrs |= Attr::REVERSE;
}
let (_, y) = write_string_to_grid(
&status_message,
grid,
attribute.fg,
attribute.bg,
attribute.attrs,
(
set_y(upper_left!(area), get_y(bottom_right!(area))),
bottom_right!(area),
),
None,
);
/* set search pattern to italics */
if let Some(ref search) = self.search {
let start_x = get_x(upper_left!(area)) + RESULTS_STR.len();
for c in grid.row_iter(start_x..(start_x + search.pattern.grapheme_width()), y) {
grid[c].set_attrs(attribute.attrs | Attr::ITALICS);
}
};
}
}
context.dirty_areas.push_back(area);
}
@ -738,13 +746,6 @@ impl Component for Pager {
self.initialised = false;
self.dirty = true;
}
UIEvent::VisibilityChange(false) => {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
}
_ => {}
}
false

View File

@ -22,7 +22,6 @@
use super::*;
use std::borrow::Cow;
use std::collections::HashMap;
use std::time::Duration;
type AutoCompleteFn = Box<dyn Fn(&Context, &str) -> Vec<AutoCompleteEntry> + Send + Sync>;
@ -1239,93 +1238,54 @@ pub struct ProgressSpinner {
}
impl ProgressSpinner {
pub const KINDS: [(Duration, &'static [&'static str]); 37] = [
(Duration::from_millis(130), &["-", "\\", "|", "/"]),
(Self::INTERVAL, &["", "", "", "", "", "", "", ""]),
(Self::INTERVAL, &["", "", "", "", "", "", ""]),
(Self::INTERVAL, &["", "", "", "", "", "", ""]),
(Self::INTERVAL, &["", "", "", "", ""]),
(Self::INTERVAL, &["", "", "", "", ""]),
(Self::INTERVAL, &["", "", "", "", ""]),
(Self::INTERVAL, &["", "", "", "", ""]),
(Self::INTERVAL, &["", "", "", ""]),
(Self::INTERVAL, &["", ""]),
(Self::INTERVAL, &["", ""]),
(Self::INTERVAL, &["", ""]),
(Self::INTERVAL, &["", ""]),
(Self::INTERVAL, &["", ""]),
(Self::INTERVAL, &["", ""]),
(Self::INTERVAL, &["", ""]),
(
Self::INTERVAL,
&["", "", "", "", "", "", "", "", "", ""],
),
(Self::INTERVAL, &["|", "/", "-", "\\"]),
(Self::INTERVAL, &[".", "o", "O", "@", "*"]),
(Self::INTERVAL, &["◡◡", "⊙⊙", "◠◠", "⊙⊙"]),
(Self::INTERVAL, &["", "", "", ""]),
(Self::INTERVAL, &["", "", "", "", "", "", "", ""]),
(
Self::INTERVAL,
&["", "", "", "", "", "", "", "", "", "", "", ""],
),
(
Self::INTERVAL,
&[
"", "", "", "", "", "", "", "", "", "", "", "", "",
],
),
(Self::INTERVAL, &["", "", "", ""]),
(Self::INTERVAL, &["", "", "", ""]),
(Self::INTERVAL, &["", "", "", "", "", "", "", ""]),
(Self::INTERVAL, &["", "", "", ""]),
(Self::INTERVAL, &["", "", "", "", "", "", "", ""]),
(
Self::INTERVAL,
&["⢎⡰", "⢎⡡", "⢎⡑", "⢎⠱", "⠎⡱", "⢊⡱", "⢌⡱", "⢆⡱"],
),
(Self::INTERVAL, &[".", "o", "O", "°", "O", "o", "."]),
(Duration::from_millis(100), &["", "", ""]),
(
Duration::from_millis(100),
&["💛 ", "💙 ", "💜 ", "💚 ", "❤️ "],
),
(
Duration::from_millis(100),
&[
"🕛 ", "🕐 ", "🕑 ", "🕒 ", "🕓 ", "🕔 ", "🕕 ", "🕖 ", "🕗 ", "🕘 ", "🕙 ", "🕚 ",
],
),
(Duration::from_millis(100), &["🌍 ", "🌎 ", "🌏 "]),
(
Duration::from_millis(80),
&[
"[ ]", "[= ]", "[== ]", "[=== ]", "[ ===]", "[ ==]", "[ =]", "[ ]",
"[ =]", "[ ==]", "[ ===]", "[====]", "[=== ]", "[== ]", "[= ]",
],
),
(
Duration::from_millis(80),
&["🌑 ", "🌒 ", "🌓 ", "🌔 ", "🌕 ", "🌖 ", "🌗 ", "🌘 "],
),
pub const KINDS: [&'static [&'static str]; 30] = [
&["", "", "", "", "", "", "", ""],
&["", "", "", "", "", "", ""],
&["", "", "", "", "", "", ""],
&["", "", "", "", ""],
&["", "", "", "", ""],
&["", "", "", "", ""],
&["", "", "", "", ""],
&["", "", "", ""],
&["", ""],
&["", ""],
&["", ""],
&["", ""],
&["", ""],
&["", ""],
&["", ""],
&["", "", "", "", "", "", "", "", "", ""],
&["|", "/", "-", "\\"],
&[".", "o", "O", "@", "*"],
&["◡◡", "⊙⊙", "◠◠", "⊙⊙"],
&["", "", "", ""],
&["", "", "", "", "", "", "", ""],
&["", "", "", "", "", "", "", "", "", "", "", ""],
&[
"", "", "", "", "", "", "", "", "", "", "", "", "",
],
&["", "", "", ""],
&["", "", "", ""],
&["", "", "", "", "", "", "", ""],
&["", "", "", ""],
&["", "", "", "", "", "", "", ""],
&["⢎⡰", "⢎⡡", "⢎⡑", "⢎⠱", "⠎⡱", "⢊⡱", "⢌⡱", "⢆⡱"],
&[".", "o", "O", "°", "O", "o", "."],
];
pub const INTERVAL_MS: u64 = 50;
const INTERVAL: std::time::Duration = std::time::Duration::from_millis(Self::INTERVAL_MS);
const INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
pub fn new(kind: usize, context: &Context) -> Self {
let timer = context
.job_executor
.clone()
.create_timer(Self::INTERVAL, Self::INTERVAL);
let kind = kind % Self::KINDS.len();
let width = Self::KINDS[kind]
.1
.iter()
.map(|f| f.grapheme_len())
.max()
.unwrap_or(0);
let interval = Self::KINDS[kind].0;
let timer = context
.job_executor
.clone()
.create_timer(interval, interval);
let mut theme_attr = crate::conf::value(context, "status.bar");
if !context.settings.terminal.use_color() {
theme_attr.attrs |= Attr::REVERSE;
@ -1350,25 +1310,21 @@ impl ProgressSpinner {
pub fn set_kind(&mut self, kind: usize) {
self.stage = 0;
self.width = Self::KINDS[kind % Self::KINDS.len()]
.1
.iter()
.map(|f| f.grapheme_len())
.max()
.unwrap_or(0);
self.kind = Ok(kind % Self::KINDS.len());
let interval = Self::KINDS[kind % Self::KINDS.len()].0;
self.timer.set_interval(interval);
self.dirty = true;
}
pub fn set_custom_kind(&mut self, frames: Vec<String>, interval: u64) {
pub fn set_custom_kind(&mut self, custom: Vec<String>) {
self.stage = 0;
self.width = frames.iter().map(|f| f.grapheme_len()).max().unwrap_or(0);
self.width = custom.iter().map(|f| f.grapheme_len()).max().unwrap_or(0);
if self.width == 0 {
self.stop();
}
self.kind = Err(frames);
self.timer.set_interval(Duration::from_millis(interval));
self.kind = Err(custom);
self.dirty = true;
}
@ -1387,6 +1343,12 @@ impl ProgressSpinner {
}
}
impl Drop for ProgressSpinner {
fn drop(&mut self) {
self.stop();
}
}
impl fmt::Display for ProgressSpinner {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "progress bar")
@ -1400,7 +1362,7 @@ impl Component for ProgressSpinner {
if self.active {
write_string_to_grid(
match self.kind.as_ref() {
Ok(kind) => (Self::KINDS[*kind].1)[self.stage].as_ref(),
Ok(kind) => Self::KINDS[*kind][self.stage].as_ref(),
Err(custom) => custom[self.stage].as_ref(),
},
grid,
@ -1421,7 +1383,7 @@ impl Component for ProgressSpinner {
UIEvent::Timer(id) if *id == self.timer.id() => {
match self.kind.as_ref() {
Ok(kind) => {
self.stage = (self.stage + 1).wrapping_rem(Self::KINDS[*kind].1.len());
self.stage = (self.stage + 1).wrapping_rem(Self::KINDS[*kind].len());
}
Err(custom) => {
self.stage = (self.stage + 1).wrapping_rem(custom.len());

View File

@ -40,10 +40,9 @@ use std::collections::{HashMap, HashSet};
use crate::types::UIEvent::{self, EnvelopeRemove, EnvelopeRename, EnvelopeUpdate, Notification};
use crate::{StatusEvent, ThreadEvent};
use crossbeam::Sender;
use futures::{
future::FutureExt,
stream::{Stream, StreamExt},
};
use futures::future::FutureExt;
pub use futures::stream::Stream;
use futures::stream::StreamExt;
use std::borrow::Cow;
use std::collections::VecDeque;
use std::convert::TryFrom;
@ -518,7 +517,7 @@ impl Account {
tree: Default::default(),
address_book,
sent_mailbox: Default::default(),
collection: backend.collection(),
collection: Default::default(),
settings,
sender,
job_executor,
@ -1012,14 +1011,6 @@ impl Account {
Some(crate::types::NotificationType::Error(err.kind)),
));
}
RefreshEventKind::MailboxCreate(_new_mailbox) => {}
RefreshEventKind::MailboxDelete(_mailbox_hash) => {}
RefreshEventKind::MailboxRename {
old_mailbox_hash: _,
new_mailbox: _,
} => {}
RefreshEventKind::MailboxSubscribe(_mailbox_hash) => {}
RefreshEventKind::MailboxUnsubscribe(_mailbox_hash) => {}
}
}
None

View File

@ -58,20 +58,6 @@ pub struct ComposingSettings {
/// Default: true
#[serde(default = "true_val")]
pub store_sent_mail: bool,
/// The attribution line appears above the quoted reply text.
/// The format specifiers for the replied address are:
/// - `%+f` — the sender's name and email address.
/// - `%+n` — the sender's name (or email address, if no name is included).
/// - `%+a` — the sender's email address.
/// The format string is passed to strftime(3) with the replied envelope's date.
/// Default: "On %a, %0e %b %Y %H:%M, %+f wrote:%n"
#[serde(default = "none")]
pub attribution_format_string: Option<String>,
/// Whether the strftime call for the attribution string uses the POSIX locale instead of
/// the user's active locale
/// Default: true
#[serde(default = "true_val")]
pub attribution_use_posix_locale: bool,
}
impl Default for ComposingSettings {
@ -84,8 +70,6 @@ impl Default for ComposingSettings {
insert_user_agent: true,
default_header_values: HashMap::default(),
store_sent_mail: true,
attribution_format_string: None,
attribution_use_posix_locale: true,
}
}
}

View File

@ -266,20 +266,6 @@ pub struct ComposingSettingsOverride {
#[doc = " Default: true"]
#[serde(default)]
pub store_sent_mail: Option<bool>,
#[doc = " The attribution line appears above the quoted reply text."]
#[doc = " The format specifiers for the replied address are:"]
#[doc = " - `%+f` — the sender's name and email address."]
#[doc = " - `%+n` — the sender's name (or email address, if no name is included)."]
#[doc = " - `%+a` — the sender's email address."]
#[doc = " The format string is passed to strftime(3) with the replied envelope's date."]
#[doc = " Default: \"On %a, %0e %b %Y %H:%M, %+f wrote:%n\""]
#[serde(default)]
pub attribution_format_string: Option<Option<String>>,
#[doc = " Whether the strftime call for the attribution string uses the POSIX locale instead of"]
#[doc = " the user's active locale"]
#[doc = " Default: true"]
#[serde(default)]
pub attribution_use_posix_locale: Option<bool>,
}
impl Default for ComposingSettingsOverride {
fn default() -> Self {
@ -291,8 +277,6 @@ impl Default for ComposingSettingsOverride {
insert_user_agent: None,
default_header_values: None,
store_sent_mail: None,
attribution_format_string: None,
attribution_use_posix_locale: None,
}
}
}

View File

@ -114,15 +114,7 @@ impl DotAddressable for TerminalSettings {
#[serde(untagged)]
pub enum ProgressSpinnerSequence {
Integer(usize),
Custom {
frames: Vec<String>,
#[serde(default = "interval_ms_val")]
interval_ms: u64,
},
}
const fn interval_ms_val() -> u64 {
crate::components::utilities::ProgressSpinner::INTERVAL_MS
Custom(Vec<String>),
}
impl DotAddressable for ProgressSpinnerSequence {}

View File

@ -138,16 +138,6 @@ impl Timer {
pub fn disable(&self) {
self.job_executor.disable_timer(self.id);
}
pub fn set_interval(&self, new_val: Duration) {
self.job_executor.set_interval(self.id, new_val);
}
}
impl Drop for Timer {
fn drop(&mut self) {
self.disable();
}
}
impl JobExecutor {
@ -339,13 +329,6 @@ impl JobExecutor {
timer.active = false;
}
}
fn set_interval(&self, id: Uuid, new_val: Duration) {
let mut timers_lck = self.timers.lock().unwrap();
if let Some(timer) = timers_lck.get_mut(&id) {
timer.interval = new_val;
}
}
}
pub type JobChannel<T> = oneshot::Receiver<T>;

View File

@ -46,7 +46,7 @@ pub struct PluginBackend {
plugin: Plugin,
child: std::process::Child,
channel: Arc<Mutex<RpcChannel>>,
collection: melib::Collection,
tag_index: Option<Arc<RwLock<BTreeMap<u64, String>>>>,
is_online: Arc<Mutex<(std::time::Instant, Result<()>)>>,
}
@ -155,6 +155,15 @@ impl MailBackend for PluginBackend {
env.set_subject(value);
}
if !references.is_empty() {
let parse_result =
melib::email::parser::address::references(
references.as_bytes(),
);
if parse_result.is_ok() {
for v in parse_result.unwrap().1 {
env.push_references(v);
}
}
env.set_references(references.as_bytes());
}
@ -191,6 +200,7 @@ impl MailBackend for PluginBackend {
Ok(Box::new(PluginOp {
hash,
channel: self.channel.clone(),
tag_index: self.tag_index.clone(),
bytes: None,
}))
}
@ -209,8 +219,8 @@ impl MailBackend for PluginBackend {
) -> ResultFuture<(MailboxHash, HashMap<MailboxHash, Mailbox>)> {
Err(MeliError::new("Unimplemented."))
}
fn collection(&self) -> melib::Collection {
self.collection.clone()
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
self.tag_index.clone()
}
fn as_any(&self) -> &dyn ::std::any::Any {
self
@ -247,7 +257,7 @@ impl PluginBackend {
child,
plugin,
channel: Arc::new(Mutex::new(channel)),
collection: Default::default(),
tag_index: None,
is_online: Arc::new(Mutex::new((now, Err(MeliError::new("Unitialized"))))),
}))
}
@ -275,6 +285,7 @@ impl PluginBackend {
struct PluginOp {
hash: EnvelopeHash,
channel: Arc<Mutex<RpcChannel>>,
tag_index: Option<Arc<RwLock<BTreeMap<u64, String>>>>,
bytes: Option<String>,
}

View File

@ -683,9 +683,11 @@ impl State {
}
}
let ((x, mut y), box_displ_area_bottom_right) = box_displ_area;
for line in msg_lines.into_iter().chain(Some(String::new())).chain(Some(
melib::datetime::timestamp_to_string(*timestamp, None, false),
)) {
for line in msg_lines
.into_iter()
.chain(Some(String::new()))
.chain(Some(melib::datetime::timestamp_to_string(*timestamp, None)))
{
write_string_to_grid(
&line,
&mut self.overlay_grid,

View File

@ -38,7 +38,7 @@ pub use self::helpers::*;
use super::command::Action;
use super::jobs::{JobExecutor, JobId};
use super::terminal::*;
use crate::components::{Component, ComponentId, ScrollUpdate};
use crate::components::{Component, ComponentId};
use std::sync::Arc;
use melib::backends::{AccountHash, BackendEvent, MailboxHash};
@ -57,7 +57,6 @@ pub enum StatusEvent {
JobFinished(JobId),
JobCanceled(JobId),
SetMouse(bool),
ScrollUpdate(ScrollUpdate),
}
/// `ThreadEvent` encapsulates all of the possible values we need to transfer between our threads
@ -150,7 +149,6 @@ pub enum UIEvent {
ConfigReload {
old_settings: crate::conf::Settings,
},
VisibilityChange(bool),
}
pub struct CallbackFn(pub Box<dyn FnOnce(&mut crate::Context) -> () + Send + 'static>);

View File

@ -8,7 +8,7 @@ This crate holds a collection of small binaries used for meli development. Of no
cd tools/
cargo build --bin imapshell
# Usage: imap_conn server_hostname server_username server_password server_port
rlwrap ./target/debug/imapshell "mail.example.com" "epilys@example.com" "hunter2" 143
rlwrap ./target/debug/imapshell "mail.domain.tld" "epilys@domain.tld" "hunter2" 143
```
Example session:
@ -18,7 +18,7 @@ First, the IMAP connections performs its own non-interactive setup:
```text
[2020-08-27 17:11:33]["main"] melib/src/backends/imap/connection.rs:459_25: sent: M1 CAPABILITY
[2020-08-27 17:11:33]["main"] melib/src/backends/imap/connection.rs:408_33: &ret[last_line_idx..] = "M1 OK Pre-login capabilities listed, post-login capabilities have more.\r\n"
[2020-08-27 17:11:33]["main"] melib/src/backends/imap/connection.rs:459_25: sent: M2 LOGIN "epilys@example.com" "hunter2"
[2020-08-27 17:11:33]["main"] melib/src/backends/imap/connection.rs:459_25: sent: M2 LOGIN "epilys@domain.tld" "hunter2"
[2020-08-27 17:11:34]["main"] melib/src/backends/imap/connection.rs:459_25: sent: M3 CAPABILITY
[2020-08-27 17:11:34]["main"] melib/src/backends/imap/connection.rs:408_33: &ret[last_line_idx..] = "M3 OK Capability completed (0.000 + 0.120 + 0.119 secs).\r\n"
[2020-08-27 17:11:34]["main"] melib/src/backends/imap/connection.rs:459_25: sent: M4 ENABLE CONDSTORE