Compare commits
1 Commits
master
...
jmap-event
Author | SHA1 | Date |
---|---|---|
Manos Pitsidianakis | a36a444b2c |
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
|
|
|
@ -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")
|
||||
|
||||
| | | |
|
||||
:---:|:---:|:---:
|
||||
|
|
|
@ -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)
|
||||
|
|
133
docs/meli.conf.5
133
docs/meli.conf.5
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
#
|
||||
#
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
pub mod eventsource;
|
||||
use super::*;
|
||||
use isahc::config::Configurable;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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,
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -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`: Message’s 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: Message’s allocated UID
|
||||
/// - Status: R (Seen) and O (non-Recent) flags
|
||||
/// - X-Status: A (Answered), F (Flagged), T (Draft) and D (Deleted) flags
|
||||
/// - X-Keywords: Message’s 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(),
|
||||
};
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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]> {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
17
src/jobs.rs
17
src/jobs.rs
|
@ -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>;
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>);
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue