Compare commits

...

34 Commits

Author SHA1 Message Date
Ludovic LANGE 66c6b62aa6
Cargo.lock: Update lexical-core version
Fixes compilation on macos 10.15.3, rustc 1.53.0
2021-07-05 23:41:55 +03:00
Manos Pitsidianakis eea9ac2b58
README.md: update with new IRC channel location 2021-06-13 11:27:33 +03:00
Manos Pitsidianakis d16866e0f0
notifications: run update_xbiff even if notifications disabled 2021-01-15 16:41:40 +02:00
Manos Pitsidianakis bcca9abe66
docs: Use example.com in documentation
Closes #96
2021-01-15 16:41:40 +02:00
Manos Pitsidianakis 24b4c117e7
melib: don't use both {set,push}_references()
set_references() already calls push_references()
2021-01-15 16:41:40 +02:00
Manos Pitsidianakis b0fba401e6
melib/mbox: consistent line endings in MboxFormat::append 2021-01-11 19:11:08 +02:00
Manos Pitsidianakis 48d4343082
utilities/ProgressSpinner: add interval field and new spinners 2021-01-11 19:11:08 +02:00
Manos Pitsidianakis 2dfeb29b75
jobs/Timer: add set_interval() 2021-01-11 19:11:08 +02:00
Manos Pitsidianakis 63d2fb93f4
melib/nntp: fix not connecting with TLS 2021-01-11 19:11:08 +02:00
Manos Pitsidianakis cf9457882a
melib/mbox: add MboxMetadata type and write support 2021-01-11 19:11:08 +02:00
Manos Pitsidianakis 3fa9e355c2
melib/email: add Flag is_*() methods 2021-01-11 18:46:22 +02:00
Manos Pitsidianakis 3dae84182c
melib/mbox: add module-level doc 2021-01-11 18:46:11 +02:00
Manos Pitsidianakis a4ae4da8b1
Add export-mbox command 2021-01-10 01:45:03 +02:00
Manos Pitsidianakis 4050f6893f
melib/mbox: add MboxFormat::append() method
Add support for writing mbox files
2021-01-10 01:40:54 +02:00
Manos Pitsidianakis dcccd303ac
melib/mbox: rename MboxReader to MboxFormat 2021-01-10 01:40:54 +02:00
Manos Pitsidianakis 22a64e2d76
melib: Remove unnecessary "pub use" std exports 2021-01-10 01:40:27 +02:00
Manos Pitsidianakis 781a1d0e1b
melib/backends: add collection() method to MailBackend
Keep track of the Collection state in the backend side
2021-01-10 01:31:27 +02:00
Manos Pitsidianakis eb8d29813c
utilities/Tabbed: send VisibilityChange event on changing tab 2021-01-08 18:37:51 +02:00
Manos Pitsidianakis 08af46f5ef
melib/datetime: fix test compile failure 2021-01-08 18:37:51 +02:00
Manos Pitsidianakis 2f47f1eebd
melib/jmap: fix mailbox children relationships being ignored 2021-01-08 15:23:25 +02:00
Manos Pitsidianakis 622ded8021
compose: add attribution line for replies 2021-01-08 15:01:38 +02:00
Manos Pitsidianakis 6d63429ad3
Add scrolling context to StatusBar
- Whenever a scrolling context is entered/exited, send a ScrollUpdate event.
- StatusBar maintains a stack of scrolling contexts and displays the
last one, if it exists. Each context is associated with a ComponentId.
- To handle dangling contexts after their Components aren't visible
anymore, send a VisibilityChange event in situations where that scenario
is possible.
2021-01-08 15:01:38 +02:00
Manos Pitsidianakis 5eb4342af8
Update dependencies, update indexmap to ^1.6 2021-01-08 15:01:38 +02:00
Manos Pitsidianakis eca10a5660
melib/backends: add mailbox management events to RefreshEventKind
Add mailbox management events from RFC 5423 Internet Message Store
Events

https://tools.ietf.org/html/rfc5423#page-8
2021-01-08 15:01:38 +02:00
Manos Pitsidianakis a697dfabbd
melib/jmap: use receivedAt as alternative to Date in Envelope gen 2021-01-08 15:01:38 +02:00
Manos Pitsidianakis 23997bdec0
melib/jmap: add UTCDate queries in EmailFilterCondition
Not necessarily working, added as stubs for future work

Closes #62
2021-01-08 15:01:37 +02:00
Manos Pitsidianakis 2e6a1e1ef8
melib/datetime: rename tests for consistency 2021-01-08 15:01:37 +02:00
Manos Pitsidianakis fe200a3218
melib/datetime: isolate unsafe blocks
Isolate unsafe blocks where possible to make code review easier
2021-01-08 15:01:37 +02:00
Manos Pitsidianakis bf9143d8e4
melib/datetime: use Cow<'_, CStr> in timestamp_to_string()
Use Cow to avoid unnecessary allocations when provided a nul-terminated
format string
2021-01-08 15:01:37 +02:00
Manos Pitsidianakis 441dcb62ca
melib/datetime: add format string constants 2021-01-08 15:01:37 +02:00
Manos Pitsidianakis 4cd3e28244
melib/datetime: fix import style inconsistencies 2021-01-08 15:01:37 +02:00
Manos Pitsidianakis 3dba6fdf60
melib/datetime: add posix locale arg in timestamp_to_string() 2021-01-08 15:01:37 +02:00
Manos Pitsidianakis 50cd81772f
melib/jmap: impl watch() with polling
Concerns #22
2021-01-05 19:45:26 +02:00
Manos Pitsidianakis 613c3de3d2
melib/connections: add async sleep(dur: Duration) 2021-01-05 19:45:26 +02:00
59 changed files with 1993 additions and 930 deletions

754
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -78,7 +78,7 @@ example configuration
root_mailbox = "/path/to/root/folder"
format = "Maildir"
index_style = "Compact"
identity="email@address.tld"
identity="email@example.com"
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.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" } }
#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" } }
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@address.tld>".
A name which can be combined with your address: "Name <email@example.com>".
.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@server.tld from:username2@server.tld", subscribe = true }
"Sent" = { query="from:username@example.com from:username2@example.com", subscribe = true }
.Ed
.Ss IMAP only
IMAP specific options are:
.Bl -tag -width 36n
.It Ic server_hostname Ar String
example:
.Qq mail.example.tld
.Qq mail.example.com
.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.tld
.Qq mail.example.com
.It Ic server_username Ar String
Server username
.It Ic server_password Ar String
@ -403,9 +403,9 @@ and
\&.
Example:
.Bd -literal
[accounts."imap.domain.tld".mailboxes."INBOX"]
[accounts."imap.example.com".mailboxes."INBOX"]
index_style = "plain"
[accounts."imap.domain.tld".mailboxes."INBOX".pager]
[accounts."imap.example.com".mailboxes."INBOX".pager]
filter = ""
.Ed
.El
@ -446,6 +446,31 @@ 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:
@ -1014,49 +1039,67 @@ 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, [String] \&>
Choose between 30-something built in sequences (integers between 0-30) or define your own list of strings for the progress spinner animation.
.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.
Set to an empty array to disable the progress spinner.
.\" default value
.Pq Em 19
.Pq Em 20
.Pp
Builtin sequences are:
.Bd -literal
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", "."]
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 ["🌑 ", "🌒 ", "🌓 ", "🌔 ", "🌕 ", "🌖 ", "🌗 ", "🌘 "]
.Ed
Or, define an array of strings each consisting of a frame in the progress sequence indicator:
.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:
.Bd -literal
# 𝄈⡂″⡈߳܃⢂:߳̈⢁܄ː“⢐″„⠑։ ⡁⡈;ܹ⡂։𝂬̤⡂꞉⣀ܹ⢁⠊𝄈⠉⠑ܸ̈׃ ;⢐;߳⠡܉˸⠒߳꞉⁚𝂬⠑⠒܅⠊;⠔⠢܄ ”⠉ֵ”⢂⢁̈⁚⠊˸⠌ܸ̤⣀𝂬⠤⠨⠢‥¨ ⡠܉꞉꞉⠑׃⠑⡐⠨؛ܸ܆„ܹ⡈⢁;⢄܄؛ ܲ⢄⠡⡁‥؛ܲ⢂“⢈։⠔⢄”꞉܉⠔
# Taken from @SmoothUnicode@botsin.space
progress_spinner_sequence = ["։","𝄈","⡂","″","⡈߳","܃","⢂",":߳̈","⢁","܄","ː","“","⢐","″","„","⠑","։"," ","⡁","⡈",";ܹ","⡂","։","𝂬̤","⡂","","⣀ܹ","⢁","⠊","𝄈","⠉","⠑ܸ̈","׃"," ",";","⢐",";߳","⠡","܉","˸","⠒߳","","","𝂬","⠑","⠒","܅","⠊",";","⠔","⠢","܄"," ","”","⠉ֵ","”","⢂","⢁̈","","⠊","˸","⠌ܸ̤","⣀","𝂬","⠤","⠨","⠢","‥","¨"," ","⡠","܉","","","⠑","׃","⠑","⡐","⠨","؛ܸ","܆","„ܹ","⡈","⢁",";","⢄܄","؛"," ܲ","⢄","⠡","⡁","‥","؛ܲ","⢂","“","⢈","։","⠔","⢄","”","","܉","⠔"]
progress_spinner_sequence = { interval_ms = 150, frames = [ "-", "=", "≡" ] }
.Ed
.El
.Sh LOG

View File

@ -12,7 +12,7 @@
#root_mailbox = "/path/to/root/mailbox"
#format = "Maildir"
#index_style = "Conversations" # or [plain, threaded, compact]
#identity="email@address.tld"
#identity="email@example.com"
#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.server.tld"
#server_hostname="mail.example.com"
#server_password="pha2hiLohs2eeeish2phaii1We3ood4chakaiv0hien2ahie3m"
#server_username="username@server.tld"
#server_username="username@example.com"
#server_port="993" # imaps
#server_port="143" # STARTTLS
#use_starttls=true #optional
#index_style = "Conversations"
#identity = "username@server.tld"
#identity = "username@example.com"
#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@server.tld"
#identity="username@example.com"
#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@server.tld from:username2@server.tld", subscribe = true }
# "Sent" = { query="from:username@example.com from:username2@example.com", 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@server.tld"
#identity = "username@gmail.com"
#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.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" } }
##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" } }
#editor_command = 'vim +/^$' # optional, by default $EDITOR is used.
#
#

View File

@ -29,11 +29,11 @@ and body structure. Addresses in `To`, `From` fields etc are parsed into
```rust
use melib::{Attachment, Envelope};
let raw_mail = r#"From: "some name" <some@address.com>
To: "me" <myself@i.tld>
let raw_mail = r#"From: "some name" <some@example.com>
To: "me" <myself@example.com>
Cc:
Subject: =?utf-8?Q?gratuitously_encoded_subject?=
Message-ID: <h2g7f.z0gy2pgaen5m@address.com>
Message-ID: <h2g7f.z0gy2pgaen5m@example.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@address.com>");
assert_eq!(envelope.message_id_display().as_ref(), "<h2g7f.z0gy2pgaen5m@example.com>");
let body = envelope.body_bytes(raw_mail.as_bytes());
assert_eq!(body.content_type().to_string().as_str(), "multipart/mixed");

View File

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

View File

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

View File

@ -58,15 +58,15 @@ use self::maildir::MaildirType;
use self::mbox::MboxType;
use super::email::{Envelope, EnvelopeHash, Flag};
use std::any::Any;
use std::collections::{BTreeMap, BTreeSet};
use std::collections::BTreeSet;
use std::fmt;
use std::fmt::Debug;
use std::ops::Deref;
use std::sync::{Arc, RwLock};
pub use futures::stream::Stream;
use futures::stream::Stream;
use std::future::Future;
pub use std::pin::Pin;
use std::pin::Pin;
use std::collections::HashMap;
@ -246,6 +246,14 @@ 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)]
@ -340,9 +348,7 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
mailbox_hash: MailboxHash,
) -> ResultFuture<()>;
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
None
}
fn collection(&self) -> crate::Collection;
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;

View File

@ -42,20 +42,21 @@ 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, BTreeMap};
use std::collections::hash_map::DefaultHasher;
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, RwLock};
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime};
pub type ImapNum = usize;
@ -142,7 +143,7 @@ pub struct UIDStore {
msn_index: Arc<Mutex<HashMap<MailboxHash, Vec<UID>>>>,
byte_cache: Arc<Mutex<HashMap<UID, EnvelopeCache>>>,
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
collection: Collection,
/* Offline caching */
uidvalidity: Arc<Mutex<HashMap<MailboxHash, UID>>>,
@ -178,7 +179,7 @@ impl UIDStore {
msn_index: Default::default(),
byte_cache: Default::default(),
mailboxes: Arc::new(FutureMutex::new(Default::default())),
tag_index: Arc::new(RwLock::new(Default::default())),
collection: Default::default(),
is_online: Arc::new(Mutex::new((
SystemTime::now(),
Err(MeliError::new("Account is uninitialised.")),
@ -710,7 +711,7 @@ impl MailBackend for ImapType {
/* Set flags/tags to true */
let mut set_seen = false;
let command = {
let mut tag_lck = uid_store.tag_index.write().unwrap();
let mut tag_lck = uid_store.collection.tag_index.write().unwrap();
let mut cmd = format!("UID STORE {}", uids[0]);
for uid in uids.iter().skip(1) {
cmd = format!("{},{}", cmd, uid);
@ -859,10 +860,6 @@ 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
}
@ -871,6 +868,10 @@ impl MailBackend for ImapType {
self
}
fn collection(&self) -> Collection {
self.uid_store.collection.clone()
}
fn create_mailbox(
&mut self,
mut path: String,
@ -1759,21 +1760,9 @@ 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.tag_index.write().unwrap();
let mut tag_lck = uid_store.collection.tag_index.write().unwrap();
if let Some((flags, keywords)) = flags {
env.set_flags(*flags);
if !env.is_seen() {

View File

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

View File

@ -157,21 +157,9 @@ 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.tag_index.write().unwrap();
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
if let Some((flags, keywords)) = flags {
env.set_flags(*flags);
if !env.is_seen() {
@ -455,21 +443,9 @@ 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.tag_index.write().unwrap();
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
if let Some((flags, keywords)) = flags {
env.set_flags(*flags);
if !env.is_seen() {

View File

@ -1318,7 +1318,9 @@ 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);
env.push_references(env.in_reply_to().unwrap().clone());
if let Some(in_reply_to) = env.in_reply_to().cloned() {
env.push_references(in_reply_to);
}
}
if let Some(message_id) = message_id {

View File

@ -224,21 +224,9 @@ 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.tag_index.write().unwrap();
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
if let Some((flags, keywords)) = flags {
env.set_flags(*flags);
if !env.is_seen() {
@ -366,22 +354,9 @@ 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.tag_index.write().unwrap();
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
if let Some((flags, keywords)) = flags {
env.set_flags(*flags);
if !env.is_seen() {

View File

@ -374,21 +374,9 @@ 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.tag_index.write().unwrap();
let mut tag_lck = uid_store.collection.tag_index.write().unwrap();
if let Some((flags, keywords)) = flags {
env.set_flags(*flags);
if !env.is_seen() {

View File

@ -23,12 +23,13 @@ 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, BTreeMap, HashMap, HashSet};
use std::collections::{hash_map::DefaultHasher, HashMap, HashSet};
use std::convert::TryFrom;
use std::hash::{Hash, Hasher};
use std::str::FromStr;
@ -183,7 +184,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 tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
pub collection: Collection,
pub mailboxes: Arc<RwLock<HashMap<MailboxHash, JmapMailbox>>>,
pub mailboxes_index: Arc<RwLock<HashMap<MailboxHash, HashSet<EnvelopeHash>>>>,
pub mailbox_state: Arc<Mutex<State<MailboxObject>>>,
@ -194,7 +195,7 @@ pub struct Store {
impl Store {
pub fn add_envelope(&self, obj: EmailObject) -> Envelope {
let mut tag_lck = self.tag_index.write().unwrap();
let mut tag_lck = self.collection.tag_index.write().unwrap();
let tags = obj
.keywords()
.keys()
@ -341,7 +342,32 @@ impl MailBackend for JmapType {
}
fn watch(&self) -> ResultFuture<()> {
Err(MeliError::from("JMAP watch for updates is unimplemented"))
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;
}
}))
}
fn mailboxes(&self) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
@ -458,10 +484,6 @@ 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
}
@ -470,6 +492,10 @@ impl MailBackend for JmapType {
self
}
fn collection(&self) -> Collection {
self.store.collection.clone()
}
fn search(
&self,
q: crate::search::Query,
@ -728,7 +754,7 @@ impl MailBackend for JmapType {
}
{
let mut tag_index_lck = store.tag_index.write().unwrap();
let mut tag_index_lck = store.collection.tag_index.write().unwrap();
for (flag, value) in flags.iter() {
match flag {
Ok(_) => {}
@ -816,12 +842,12 @@ impl JmapType {
online_status,
event_consumer,
is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)),
collection: Collection::default(),
byte_cache: Default::default(),
id_store: Default::default(),
reverse_id_store: Default::default(),
blob_id_store: Default::default(),
tag_index: Default::default(),
mailboxes: Default::default(),
mailboxes_index: Default::default(),
mailbox_state: Default::default(),

View File

@ -266,15 +266,11 @@ 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());
env.push_references(env.in_reply_to().unwrap().clone());
if let Some(in_reply_to) = env.in_reply_to().cloned() {
env.push_references(in_reply_to);
}
}
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") {
@ -282,6 +278,8 @@ 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 {
@ -581,6 +579,7 @@ 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) => {
@ -604,23 +603,48 @@ impl From<crate::search::Query> for Filter<EmailFilterCondition, EmailObject> {
Body(t) => {
*f = Filter::Condition(EmailFilterCondition::new().body(t.clone()).into());
}
Before(_) => {
//TODO, convert UNIX timestamp into UtcDate
Before(t) => {
*f = Filter::Condition(
EmailFilterCondition::new()
.before(timestamp_to_string(*t, Some(RFC3339_FMT), true))
.into(),
);
}
After(_) => {
//TODO
After(t) => {
*f = Filter::Condition(
EmailFilterCondition::new()
.after(timestamp_to_string(*t, Some(RFC3339_FMT), true))
.into(),
);
}
Between(_, _) => {
//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(),
);
}
On(_) => {
//TODO
On(t) => {
rec(&Between(*t, *t), f);
}
InReplyTo(_) => {
//TODO, look inside Headers
InReplyTo(ref s) => {
*f = Filter::Condition(
EmailFilterCondition::new()
.header(vec!["In-Reply-To".to_string().into(), s.to_string().into()])
.into(),
);
}
References(_) => {
//TODO
References(ref s) => {
*f = Filter::Condition(
EmailFilterCondition::new()
.header(vec!["References".to_string().into(), s.to_string().into()])
.into(),
);
}
AllAddresses(_) => {
//TODO

View File

@ -109,7 +109,7 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
list, account_id, ..
} = m;
*conn.store.account_id.lock().unwrap() = account_id;
Ok(list
let mut ret: HashMap<MailboxHash, JmapMailbox> = list
.into_iter()
.map(|r| {
let MailboxObject {
@ -157,7 +157,13 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
},
)
})
.collect())
.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)
}
pub async fn get_message_list(

View File

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

View File

@ -25,6 +25,7 @@ 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;
@ -109,6 +110,7 @@ pub struct MaildirType {
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
hash_indexes: HashIndexes,
event_consumer: BackendEventConsumer,
collection: Collection,
path: PathBuf,
}
@ -1003,6 +1005,10 @@ impl MailBackend for MaildirType {
}))
}
fn collection(&self) -> Collection {
self.collection.clone()
}
fn create_mailbox(
&mut self,
new_path: String,
@ -1236,6 +1242,7 @@ impl MaildirType {
hash_indexes: Arc::new(Mutex::new(hash_indexes)),
mailbox_index: Default::default(),
event_consumer,
collection: Default::default(),
path: root_path,
}))
}

View File

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

View File

@ -0,0 +1,260 @@
/*
* meli - mailbox module.
*
* Copyright 2021 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use super::*;
impl MboxFormat {
pub fn append(
&self,
writer: &mut dyn std::io::Write,
input: &[u8],
envelope_from: Option<&Address>,
delivery_date: Option<crate::UnixTimestamp>,
(flags, tags): (Flag, Vec<&str>),
metadata_format: MboxMetadata,
is_empty: bool,
crlf: bool,
) -> Result<()> {
if tags.iter().any(|t| t.contains(' ')) {
return Err(MeliError::new("mbox tags/keywords can't contain spaces"));
}
let line_ending: &'static [u8] = if crlf { &b"\r\n"[..] } else { &b"\n"[..] };
if !is_empty {
writer.write_all(line_ending)?;
writer.write_all(line_ending)?;
}
writer.write_all(&b"From "[..])?;
if let Some(from) = envelope_from {
writer.write_all(from.address_spec_raw())?;
} else {
writer.write_all(&b"MAILER-DAEMON"[..])?;
}
writer.write_all(&b" "[..])?;
writer.write_all(
crate::datetime::timestamp_to_string(
delivery_date.unwrap_or_else(|| crate::datetime::now()),
Some(crate::datetime::ASCTIME_FMT),
true,
)
.trim()
.as_bytes(),
)?;
writer.write_all(line_ending)?;
let (mut headers, body) = parser::mail(input)?;
headers.retain(|(header_name, _)| {
!header_name.eq_ignore_ascii_case(b"Status")
&& !header_name.eq_ignore_ascii_case(b"X-Status")
&& !header_name.eq_ignore_ascii_case(b"X-Keywords")
&& !header_name.eq_ignore_ascii_case(b"Content-Length")
});
let write_header_val_fn = |writer: &mut dyn std::io::Write, bytes: &[u8]| {
let mut i = 0;
if crlf {
while i < bytes.len() {
if bytes[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\r', b'\n'])?;
i += 2;
continue;
} else if bytes[i] == b'\n' {
writer.write_all(&[b'\r', b'\n'])?;
} else {
writer.write_all(&[bytes[i]])?;
}
i += 1;
}
} else {
while i < bytes.len() {
if bytes[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\n'])?;
i += 2;
} else {
writer.write_all(&[bytes[i]])?;
i += 1;
}
}
}
Ok::<(), MeliError>(())
};
let write_metadata_fn = |writer: &mut dyn std::io::Write| match metadata_format {
MboxMetadata::CClient => {
for (h, v) in {
if flags.is_seen() {
Some((&b"Status"[..], "R".into()))
} else {
None
}
.into_iter()
.chain(
if !flags.is_flagged()
&& !flags.is_replied()
&& !flags.is_draft()
&& !flags.is_trashed()
{
None
} else {
Some((
&b"X-Status"[..],
format!(
"{flagged}{replied}{draft}{trashed}",
flagged = if flags.is_flagged() { "F" } else { "" },
replied = if flags.is_replied() { "A" } else { "" },
draft = if flags.is_draft() { "T" } else { "" },
trashed = if flags.is_trashed() { "D" } else { "" }
),
))
},
)
.chain(if tags.is_empty() {
None
} else {
Some((&b"X-Keywords"[..], tags.as_slice().join(" ")))
})
} {
writer.write_all(h)?;
writer.write_all(&b": "[..])?;
writer.write_all(v.as_bytes())?;
writer.write_all(line_ending)?;
}
Ok::<(), MeliError>(())
}
MboxMetadata::None => Ok(()),
};
let body_len = {
let mut len = body.len();
if crlf {
let stray_lfs = body.iter().filter(|b| **b == b'\n').count()
- body.windows(b"\r\n".len()).filter(|w| w == b"\r\n").count();
len += stray_lfs;
} else {
let crlfs = body.windows(b"\r\n".len()).filter(|w| w == b"\r\n").count();
len -= crlfs;
}
len
};
match self {
MboxFormat::MboxO | MboxFormat::MboxRd => Err(MeliError::new("Unimplemented.")),
MboxFormat::MboxCl => {
let len = (body_len
+ body
.windows(b"\nFrom ".len())
.filter(|w| w == b"\nFrom ")
.count()
+ if body.starts_with(b"From ") { 1 } else { 0 })
.to_string();
for (h, v) in headers
.into_iter()
.chain(Some((&b"Content-Length"[..], len.as_bytes())))
{
writer.write_all(h)?;
writer.write_all(&b": "[..])?;
write_header_val_fn(writer, v)?;
writer.write_all(line_ending)?;
}
write_metadata_fn(writer)?;
writer.write_all(line_ending)?;
if body.starts_with(b"From ") {
writer.write_all(&[b'>'])?;
}
let mut i = 0;
if crlf {
while i < body.len() {
if body[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\r', b'\n'])?;
if body[i..].starts_with(b"\r\nFrom ") {
writer.write_all(&[b'>'])?;
}
i += 2;
} else if body[i] == b'\n' {
writer.write_all(&[b'\r', b'\n'])?;
if body[i..].starts_with(b"\nFrom ") {
writer.write_all(&[b'>'])?;
}
i += 1;
} else {
writer.write_all(&[body[i]])?;
i += 1;
}
}
} else {
while i < body.len() {
if body[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\n'])?;
if body[i..].starts_with(b"\r\nFrom ") {
writer.write_all(&[b'>'])?;
}
i += 2;
} else {
writer.write_all(&[body[i]])?;
if body[i..].starts_with(b"\nFrom ") {
writer.write_all(&[b'>'])?;
}
i += 1;
}
}
}
Ok(())
}
MboxFormat::MboxCl2 => {
let len = body_len.to_string();
for (h, v) in headers
.into_iter()
.chain(Some((&b"Content-Length"[..], len.as_bytes())))
{
writer.write_all(h)?;
writer.write_all(&b": "[..])?;
write_header_val_fn(writer, v)?;
writer.write_all(line_ending)?;
}
write_metadata_fn(writer)?;
writer.write_all(line_ending)?;
let mut i = 0;
if crlf {
while i < body.len() {
if body[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\r', b'\n'])?;
i += 2;
continue;
} else if body[i] == b'\n' {
writer.write_all(&[b'\r', b'\n'])?;
} else {
writer.write_all(&[body[i]])?;
}
i += 1;
}
} else {
while i < body.len() {
if body[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\n'])?;
i += 2;
} else {
writer.write_all(&[body[i]])?;
i += 1;
}
}
}
Ok(())
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,41 +34,47 @@
//! assert_eq!(timestamp, 1578509043);
//!
//! // Convert timestamp back to string
//! let s = timestamp_to_string(timestamp, Some("%Y-%m-%d"));
//! let s = timestamp_to_string(timestamp, Some("%Y-%m-%d"), true);
//! 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;
use libc::{locale_t, timeval, timezone};
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";
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 timeval, tz: *mut timezone) -> i32;
fn gettimeofday(tv: *mut libc::timeval, tz: *mut libc::timezone) -> i32;
}
struct Locale {
new_locale: locale_t,
old_locale: locale_t,
new_locale: libc::locale_t,
old_locale: libc::locale_t,
}
impl Drop for Locale {
@ -83,9 +89,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: locale_t,
mask: std::os::raw::c_int,
locale: *const std::os::raw::c_char,
base: libc::locale_t,
) -> Result<Self> {
let new_locale = unsafe { libc::newlocale(mask, locale, base) };
if new_locale.is_null() {
@ -103,35 +109,58 @@ impl Locale {
}
}
pub fn timestamp_to_string(timestamp: UnixTimestamp, fmt: Option<&str>) -> String {
let mut new_tm: ::libc::tm = unsafe { std::mem::zeroed() };
pub fn timestamp_to_string(timestamp: UnixTimestamp, fmt: Option<&str>, posix: bool) -> 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 fmt = fmt
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)
.map(CString::new)
.map(|res| res.ok())
.and_then(|opt| opt);
let format: &CStr = if let Some(ref s) = fmt {
&s
.and_then(|res| res.ok())
{
Cow::from(cstring)
} else {
unsafe { CStr::from_bytes_with_nul_unchecked(b"%a, %d %b %Y %T %z\0") }
unsafe { CStr::from_bytes_with_nul_unchecked(DEFAULT_FMT.as_bytes()).into() }
};
let mut vec: [u8; 256] = [0; 256];
let ret = unsafe {
strftime(
vec.as_mut_ptr() as *mut _,
256,
format.as_ptr(),
&new_tm as *const _,
)
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 _,
)
}
};
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;
@ -244,63 +273,58 @@ where
T: Into<Vec<u8>>,
{
let s = CString::new(s)?;
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);
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 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 ret.is_null() {
continue;
}
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));
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()
};
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)
}
@ -310,59 +334,58 @@ where
T: Into<Vec<u8>>,
{
let s = CString::new(s)?;
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 ret.is_null() {
continue;
}
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));
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()
};
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)
}
@ -372,8 +395,12 @@ 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 = CString::new(fmt)?;
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())?)
};
unsafe {
let ret = strptime(
CString::new(s)?.as_ptr(),
@ -389,8 +416,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 {
@ -401,12 +428,12 @@ pub fn now() -> UnixTimestamp {
}
#[test]
fn test_timestamp() {
timestamp_to_string(0, None);
fn test_datetime_timestamp() {
timestamp_to_string(0, None, false);
}
#[test]
fn test_rfcs() {
fn test_datetime_rfcs() {
if unsafe { libc::setlocale(libc::LC_ALL, b"\0".as_ptr() as _) }.is_null() {
println!("Unable to set locale.");
}
@ -420,7 +447,7 @@ fn test_rfcs() {
/*
macro_rules! mkt {
($year:literal, $month:literal, $day:literal, $hour:literal, $minute:literal, $second:literal) => {
::libc::tm {
libc::tm {
tm_sec: $second,
tm_min: $minute,
tm_hour: $hour,

View File

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

View File

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

View File

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

View File

@ -85,7 +85,8 @@ 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).as_bytes(),
crate::datetime::timestamp_to_string(crate::datetime::now(), None, false)
.as_bytes(),
)
.unwrap();
b.dest.write_all(b" [").unwrap();

View File

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

View File

@ -419,6 +419,19 @@ 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")))]))],
@ -852,6 +865,7 @@ fn listing_action(input: &[u8]) -> IResult<&[u8], Action> {
select,
toggle_thread_snooze,
open_in_new_tab,
export_mbox,
_tag,
))(input)
}

View File

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

View File

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

View File

@ -414,6 +414,27 @@ 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 {
@ -621,6 +642,11 @@ 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;
}
@ -639,6 +665,11 @@ impl Component for ContactList {
self.mode = ViewMode::View(manager.id());
self.view = Some(manager);
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
return true;
}

View File

@ -31,6 +31,7 @@ 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};
@ -314,10 +315,21 @@ impl Composer {
}
}
ret.draft.body = {
let mut ret = format!(
"On {} {} wrote:\n",
envelope.date_as_str(),
envelope.from()[0],
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
),
);
for l in reply_body.lines() {
ret.push('>');
@ -925,6 +937,9 @@ 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, _) => {
@ -2207,3 +2222,36 @@ pub fn send_draft_async(
ret
}))
}
/* Sender details
* %+f the sender's name and email address.
* %+n the sender's name (or email address, if no name is included).
* %+a the sender's email address.
*/
fn attribution_string(
fmt: Option<&str>,
sender: Option<&Address>,
date: UnixTimestamp,
posix: bool,
) -> String {
let fmt = fmt.unwrap_or("On %a, %0e %b %Y %H:%M, %+f wrote:%n");
let fmt = fmt.replace(
"%+f",
&sender
.map(|addr| addr.to_string())
.unwrap_or_else(|| "\"\"".to_string()),
);
let fmt = fmt.replace(
"%+n",
&sender
.map(|addr| addr.get_display_name().unwrap_or_else(|| addr.get_email()))
.unwrap_or_else(|| "\"\"".to_string()),
);
let fmt = fmt.replace(
"%+a",
&sender
.map(|addr| addr.get_email())
.unwrap_or_else(|| "\"\"".to_string()),
);
melib::datetime::timestamp_to_string(date, Some(fmt.as_str()), posix)
}

View File

@ -342,6 +342,103 @@ 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
@ -703,6 +800,8 @@ 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,
@ -730,6 +829,8 @@ 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();
@ -963,6 +1064,7 @@ 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);
@ -1148,6 +1250,11 @@ 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;
@ -1161,6 +1268,11 @@ 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)
@ -1171,6 +1283,11 @@ 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(
@ -1650,6 +1767,20 @@ 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,
(
@ -1664,6 +1795,12 @@ 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);
@ -1964,6 +2101,8 @@ impl Listing {
if let Some((_, _, _, mailbox_hash)) =
self.accounts[self.cursor_pos.0].entries.get(idx)
{
self.component
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.component
.set_coordinates((account_hash, *mailbox_hash));
/* Check if per-mailbox configuration overrides general configuration */

View File

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

View File

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

View File

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

View File

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

View File

@ -188,29 +188,27 @@ 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 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 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);
}
}
}
}

View File

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

View File

@ -64,6 +64,7 @@ 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>,
@ -77,13 +78,16 @@ impl fmt::Display for StatusBar {
impl StatusBar {
pub fn new(context: &Context, container: Box<dyn Component>) -> Self {
let mut progress_spinner = ProgressSpinner::new(19, context);
let mut progress_spinner = ProgressSpinner::new(20, 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 s)) => {
progress_spinner.set_custom_kind(s.clone());
Some(conf::terminal::ProgressSpinnerSequence::Custom {
ref frames,
ref interval_ms,
}) => {
progress_spinner.set_custom_kind(frames.clone(), *interval_ms);
}
None => {}
}
@ -104,6 +108,7 @@ 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(),
}
}
@ -140,6 +145,33 @@ 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() {
@ -443,13 +475,16 @@ impl Component for StatusBar {
match event {
UIEvent::ConfigReload { old_settings: _ } => {
let mut progress_spinner = ProgressSpinner::new(19, context);
let mut progress_spinner = ProgressSpinner::new(20, 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 s)) => {
progress_spinner.set_custom_kind(s.clone());
Some(conf::terminal::ProgressSpinnerSequence::Custom {
ref frames,
ref interval_ms,
}) => {
progress_spinner.set_custom_kind(frames.clone(), *interval_ms);
}
None => {}
}
@ -706,6 +741,21 @@ 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;
@ -975,6 +1025,21 @@ 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,
(
@ -989,6 +1054,12 @@ impl Component for Tabbed {
/* length */
height,
);
} else {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
}
self.dirty = false;
return;
@ -1192,6 +1263,21 @@ 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,
(
@ -1206,6 +1292,12 @@ impl Component for Tabbed {
/* length */
height,
);
} else {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
}
}
self.dirty = false;
@ -1219,7 +1311,9 @@ 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() {
if no < self.children.len() && self.cursor_pos != no % self.children.len() {
self.children[self.cursor_pos]
.process_event(&mut UIEvent::VisibilityChange(false), context);
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));
@ -1234,6 +1328,8 @@ 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));
@ -1250,6 +1346,11 @@ 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;
@ -1257,6 +1358,8 @@ 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);
@ -1281,6 +1384,8 @@ impl Component for Tabbed {
return true;
}
if let Some(c_idx) = self.children.iter().position(|x| x.id() == *id) {
self.children[c_idx]
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.children.remove(c_idx);
self.cursor_pos = 0;
self.set_dirty(true);

View File

@ -533,28 +533,30 @@ 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;
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 {
"(+)"
}
)
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(),
},
},
)));
} else {
String::new()
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
};
let search_results = if let Some(ref search) = self.search {
format!(
if let Some(ref search) = self.search {
let status_message = format!(
"{results_str}{search_pattern}: {current_pos}/{total_results}{has_more_lines}",
results_str = RESULTS_STR,
search_pattern = &search.pattern,
@ -569,39 +571,29 @@ impl Component for Pager {
} else {
"(+)"
}
)
} 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 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 */
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);
}
@ -746,6 +738,13 @@ impl Component for Pager {
self.initialised = false;
self.dirty = true;
}
UIEvent::VisibilityChange(false) => {
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
ScrollUpdate::End(self.id),
)));
}
_ => {}
}
false

View File

@ -22,6 +22,7 @@
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>;
@ -1238,54 +1239,93 @@ pub struct ProgressSpinner {
}
impl ProgressSpinner {
pub const KINDS: [&'static [&'static str]; 30] = [
&["", "", "", "", "", "", "", ""],
&["", "", "", "", "", "", ""],
&["", "", "", "", "", "", ""],
&["", "", "", "", ""],
&["", "", "", "", ""],
&["", "", "", "", ""],
&["", "", "", "", ""],
&["", "", "", ""],
&["", ""],
&["", ""],
&["", ""],
&["", ""],
&["", ""],
&["", ""],
&["", ""],
&["", "", "", "", "", "", "", "", "", ""],
&["|", "/", "-", "\\"],
&[".", "o", "O", "@", "*"],
&["◡◡", "⊙⊙", "◠◠", "⊙⊙"],
&["", "", "", ""],
&["", "", "", "", "", "", "", ""],
&["", "", "", "", "", "", "", "", "", "", "", ""],
&[
"", "", "", "", "", "", "", "", "", "", "", "", "",
],
&["", "", "", ""],
&["", "", "", ""],
&["", "", "", "", "", "", "", ""],
&["", "", "", ""],
&["", "", "", "", "", "", "", ""],
&["⢎⡰", "⢎⡡", "⢎⡑", "⢎⠱", "⠎⡱", "⢊⡱", "⢌⡱", "⢆⡱"],
&[".", "o", "O", "°", "O", "o", "."],
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),
&["🌑 ", "🌒 ", "🌓 ", "🌔 ", "🌕 ", "🌖 ", "🌗 ", "🌘 "],
),
];
const INTERVAL: std::time::Duration = std::time::Duration::from_millis(50);
pub const INTERVAL_MS: u64 = 50;
const INTERVAL: std::time::Duration = std::time::Duration::from_millis(Self::INTERVAL_MS);
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;
@ -1310,21 +1350,25 @@ 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, custom: Vec<String>) {
pub fn set_custom_kind(&mut self, frames: Vec<String>, interval: u64) {
self.stage = 0;
self.width = custom.iter().map(|f| f.grapheme_len()).max().unwrap_or(0);
self.width = frames.iter().map(|f| f.grapheme_len()).max().unwrap_or(0);
if self.width == 0 {
self.stop();
}
self.kind = Err(custom);
self.kind = Err(frames);
self.timer.set_interval(Duration::from_millis(interval));
self.dirty = true;
}
@ -1343,12 +1387,6 @@ 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")
@ -1362,7 +1400,7 @@ impl Component for ProgressSpinner {
if self.active {
write_string_to_grid(
match self.kind.as_ref() {
Ok(kind) => Self::KINDS[*kind][self.stage].as_ref(),
Ok(kind) => (Self::KINDS[*kind].1)[self.stage].as_ref(),
Err(custom) => custom[self.stage].as_ref(),
},
grid,
@ -1383,7 +1421,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].len());
self.stage = (self.stage + 1).wrapping_rem(Self::KINDS[*kind].1.len());
}
Err(custom) => {
self.stage = (self.stage + 1).wrapping_rem(custom.len());

View File

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

View File

@ -58,6 +58,20 @@ 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 {
@ -70,6 +84,8 @@ impl Default for ComposingSettings {
insert_user_agent: true,
default_header_values: HashMap::default(),
store_sent_mail: true,
attribution_format_string: None,
attribution_use_posix_locale: true,
}
}
}

View File

@ -266,6 +266,20 @@ 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 {
@ -277,6 +291,8 @@ impl Default for ComposingSettingsOverride {
insert_user_agent: None,
default_header_values: None,
store_sent_mail: None,
attribution_format_string: None,
attribution_use_posix_locale: None,
}
}
}

View File

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

View File

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

View File

@ -46,7 +46,7 @@ pub struct PluginBackend {
plugin: Plugin,
child: std::process::Child,
channel: Arc<Mutex<RpcChannel>>,
tag_index: Option<Arc<RwLock<BTreeMap<u64, String>>>>,
collection: melib::Collection,
is_online: Arc<Mutex<(std::time::Instant, Result<()>)>>,
}
@ -155,15 +155,6 @@ 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());
}
@ -200,7 +191,6 @@ impl MailBackend for PluginBackend {
Ok(Box::new(PluginOp {
hash,
channel: self.channel.clone(),
tag_index: self.tag_index.clone(),
bytes: None,
}))
}
@ -219,8 +209,8 @@ impl MailBackend for PluginBackend {
) -> ResultFuture<(MailboxHash, HashMap<MailboxHash, Mailbox>)> {
Err(MeliError::new("Unimplemented."))
}
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
self.tag_index.clone()
fn collection(&self) -> melib::Collection {
self.collection.clone()
}
fn as_any(&self) -> &dyn ::std::any::Any {
self
@ -257,7 +247,7 @@ impl PluginBackend {
child,
plugin,
channel: Arc::new(Mutex::new(channel)),
tag_index: None,
collection: Default::default(),
is_online: Arc::new(Mutex::new((now, Err(MeliError::new("Unitialized"))))),
}))
}
@ -285,7 +275,6 @@ impl PluginBackend {
struct PluginOp {
hash: EnvelopeHash,
channel: Arc<Mutex<RpcChannel>>,
tag_index: Option<Arc<RwLock<BTreeMap<u64, String>>>>,
bytes: Option<String>,
}

View File

@ -683,11 +683,9 @@ 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)))
{
for line in msg_lines.into_iter().chain(Some(String::new())).chain(Some(
melib::datetime::timestamp_to_string(*timestamp, None, false),
)) {
write_string_to_grid(
&line,
&mut self.overlay_grid,

View File

@ -38,7 +38,7 @@ pub use self::helpers::*;
use super::command::Action;
use super::jobs::{JobExecutor, JobId};
use super::terminal::*;
use crate::components::{Component, ComponentId};
use crate::components::{Component, ComponentId, ScrollUpdate};
use std::sync::Arc;
use melib::backends::{AccountHash, BackendEvent, MailboxHash};
@ -57,6 +57,7 @@ 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
@ -149,6 +150,7 @@ pub enum UIEvent {
ConfigReload {
old_settings: crate::conf::Settings,
},
VisibilityChange(bool),
}
pub struct CallbackFn(pub Box<dyn FnOnce(&mut crate::Context) -> () + Send + 'static>);

View File

@ -8,7 +8,7 @@ This crate holds a collection of small binaries used for meli development. Of no
cd tools/
cargo build --bin imapshell
# Usage: imap_conn server_hostname server_username server_password server_port
rlwrap ./target/debug/imapshell "mail.domain.tld" "epilys@domain.tld" "hunter2" 143
rlwrap ./target/debug/imapshell "mail.example.com" "epilys@example.com" "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@domain.tld" "hunter2"
[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: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