Compare commits
2 Commits
master
...
lazy_fetch
Author | SHA1 | Date |
---|---|---|
Manos Pitsidianakis | 77e4488637 | |
Manos Pitsidianakis | 819d993f11 |
65
CHANGELOG.md
65
CHANGELOG.md
|
@ -7,68 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [alpha-0.7.2] - 2021-10-15
|
||||
|
||||
### Added
|
||||
|
||||
- Add forward mail option
|
||||
- Add url_launcher config setting
|
||||
- Add add_addresses_to_contacts command
|
||||
- Add show_date_in_my_timezone pager config flag
|
||||
- docs: add pager filter documentation
|
||||
- mail/view: respect per-folder/account pager filter override
|
||||
- pager: add filter command, esc to clear filter
|
||||
- Show compile time features in with command argument
|
||||
|
||||
### Fixed
|
||||
|
||||
- melib/email/address: quote display_name if it contains ","
|
||||
- melib/smtp: fix Cc and Bcc ignored when sending mail
|
||||
- melib/email/address: quote display_name if it contains "."
|
||||
|
||||
## [alpha-0.7.1] - 2021-09-08
|
||||
|
||||
### Added
|
||||
|
||||
- Change all Down/Up shortcuts to j/k
|
||||
- add 'GB18030' charset
|
||||
- melib/nntp: implement refresh
|
||||
- melib/nntp: update total/new counters on new articles
|
||||
- melib/nntp: implement NNTP posting
|
||||
- configs: throw error on extra unusued conf flags in some imap/nntp
|
||||
- configs: throw error on missing `composing` section with explanation
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix compilation for netbsd-9.2
|
||||
- conf: fixed some boolean flag values requiring to be string e.g. "true"
|
||||
|
||||
## [alpha-0.7.0] - 2021-09-03
|
||||
|
||||
### Added
|
||||
|
||||
Notable changes:
|
||||
|
||||
- add import command to import email from files into accounts
|
||||
- add add-attachment-file-picker command and `file_picker_command` setting to
|
||||
- Add import command to import email from files into accounts
|
||||
- Add add-attachment-file-picker command and `file_picker_command` setting to
|
||||
use external commands to choose files when composing new mail
|
||||
- ask confirm for delete
|
||||
- add export-mbox command
|
||||
- add export-mail command
|
||||
- add TLS support with nntp
|
||||
- add JMAP watch with polling
|
||||
- add reload-config command
|
||||
- add import-mail command
|
||||
- imap: implement gmail XOAUTH2 authentication method
|
||||
- imap: implement OAUTH2 authentication
|
||||
- compose: treat inline message/rfc822 as attachments
|
||||
- add gpg support via libgpgme
|
||||
|
||||
### Fixed
|
||||
|
||||
- Loading notmuch library on macos
|
||||
- Limit dbus dependency to target_os = "linux"
|
||||
- IMAP, notmuch, mbox backends: various performance fixes
|
||||
|
||||
## [alpha-0.6.2] - 2020-09-24
|
||||
|
||||
|
@ -167,6 +109,3 @@ Notable changes:
|
|||
[alpha-0.6.0]: https://github.com/meli/meli/releases/tag/alpha-0.6.0
|
||||
[alpha-0.6.1]: https://github.com/meli/meli/releases/tag/alpha-0.6.1
|
||||
[alpha-0.6.2]: https://github.com/meli/meli/releases/tag/alpha-0.6.2
|
||||
[alpha-0.7.0]: https://github.com/meli/meli/releases/tag/alpha-0.7.0
|
||||
[alpha-0.7.1]: https://github.com/meli/meli/releases/tag/alpha-0.7.1
|
||||
[alpha-0.7.2]: https://github.com/meli/meli/releases/tag/alpha-0.7.2
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "adler"
|
||||
version = "0.2.3"
|
||||
|
@ -968,13 +966,13 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
|||
|
||||
[[package]]
|
||||
name = "lexical-core"
|
||||
version = "0.7.5"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374"
|
||||
checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bitflags 1.2.1",
|
||||
"cfg-if 1.0.0",
|
||||
"cfg-if 0.1.10",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
@ -1098,7 +1096,7 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
|
|||
|
||||
[[package]]
|
||||
name = "meli"
|
||||
version = "0.7.2"
|
||||
version = "0.6.2"
|
||||
dependencies = [
|
||||
"async-task 3.0.0",
|
||||
"bincode",
|
||||
|
@ -1135,7 +1133,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "melib"
|
||||
version = "0.7.2"
|
||||
version = "0.6.2"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"base64 0.12.3",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "meli"
|
||||
version = "0.7.2"
|
||||
version = "0.6.2"
|
||||
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
|
||||
edition = "2018"
|
||||
|
||||
|
@ -31,7 +31,7 @@ crossbeam = "0.7.2"
|
|||
signal-hook = "0.1.12"
|
||||
signal-hook-registry = "1.2.0"
|
||||
nix = "0.17.0"
|
||||
melib = { path = "melib", version = "0.7.2" }
|
||||
melib = { path = "melib", version = "0.6.2" }
|
||||
|
||||
serde = "1.0.71"
|
||||
serde_derive = "1.0.71"
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
**BSD/Linux terminal email client with support for multiple accounts and Maildir / mbox / notmuch / IMAP / JMAP.**
|
||||
|
||||
Community links:
|
||||
[mailing lists](https://lists.meli.delivery/) | `#meli` on OFTC IRC | Report bugs and/or feature requests in [meli's issue tracker](https://git.meli.delivery/meli/meli/issues "meli gitea issue tracker")
|
||||
[mailing lists](https://lists.meli.delivery/) | `#meli` on freenode IRC | Report bugs and/or feature requests in [meli's issue tracker](https://git.meli.delivery/meli/meli/issues "meli gitea issue tracker")
|
||||
|
||||
| | | |
|
||||
:---:|:---:|:---:
|
||||
|
|
|
@ -1,40 +1,3 @@
|
|||
meli (0.7.2-1) bullseye; urgency=low
|
||||
Added
|
||||
|
||||
- Add forward mail option
|
||||
- Add url_launcher config setting
|
||||
- Add add_addresses_to_contacts command
|
||||
- Add show_date_in_my_timezone pager config flag
|
||||
- docs: add pager filter documentation
|
||||
- mail/view: respect per-folder/account pager filter override
|
||||
- pager: add filter command, esc to clear filter
|
||||
- Show compile time features in with command argument
|
||||
|
||||
Fixed
|
||||
|
||||
- melib/email/address: quote display_name if it contains ","
|
||||
- melib/smtp: fix Cc and Bcc ignored when sending mail
|
||||
- melib/email/address: quote display_name if it contains "."
|
||||
|
||||
-- Manos Pitsidianakis <epilys@nessuent.xyz> Fri, 15 Oct 2021 12:34:00 +0200
|
||||
meli (0.7.1-1) bullseye; urgency=low
|
||||
|
||||
Added
|
||||
- Change all Down/Up shortcuts to j/k
|
||||
- add 'GB18030' charset
|
||||
- melib/nntp: implement refresh
|
||||
- melib/nntp: update total/new counters on new articles
|
||||
- melib/nntp: implement NNTP posting
|
||||
- configs: throw error on extra unusued conf flags in some imap/nntp
|
||||
- configs: throw error on missing `composing` section with explanation
|
||||
|
||||
Fixed
|
||||
- Fix compilation for netbsd-9.2
|
||||
- conf: fixed some boolean flag values requiring to be string e.g. "true"
|
||||
-- Manos Pitsidianakis <epilys@nessuent.xyz> Wed, 08 Sep 2021 18:14:00 +0200
|
||||
meli (0.7.0-1) buster; urgency=low
|
||||
|
||||
-- Manos Pitsidianakis <epilys@nessuent.xyz> Fri, 03 Sep 2021 18:14:00 +0200
|
||||
meli (0.6.2-1) buster; urgency=low
|
||||
|
||||
Added
|
||||
|
|
|
@ -251,8 +251,12 @@ mail.listing.conversations.from
|
|||
.It
|
||||
mail.listing.conversations.date
|
||||
.It
|
||||
mail.listing.conversations.padding
|
||||
.It
|
||||
mail.listing.conversations.unseen
|
||||
.It
|
||||
mail.listing.conversations.unseen_padding
|
||||
.It
|
||||
mail.listing.conversations.highlighted
|
||||
.It
|
||||
mail.listing.conversations.selected
|
||||
|
|
|
@ -48,8 +48,6 @@ Print documentation page and exit (Piping to a pager is recommended.)
|
|||
Print default theme keys and values in TOML syntax, to be used as a blueprint.
|
||||
.It Cm print-loaded-themes
|
||||
Print all loaded themes in TOML syntax.
|
||||
.It Cm compiled-with
|
||||
Print compile time feature flags of this binary.
|
||||
.It Cm view
|
||||
View mail from input file.
|
||||
.El
|
||||
|
@ -439,8 +437,6 @@ This action is unreversible.
|
|||
.Bl -tag -width 36n
|
||||
.It Cm pipe Ar EXECUTABLE Ar ARGS
|
||||
pipe pager contents to binary
|
||||
.It Cm filter Ar EXECUTABLE Ar ARGS
|
||||
filter and display pager contents through command
|
||||
.It Cm list-post
|
||||
post in list of viewed envelope
|
||||
.It Cm list-unsubscribe
|
||||
|
|
110
docs/meli.conf.5
110
docs/meli.conf.5
|
@ -346,56 +346,6 @@ Example:
|
|||
format = "mbox"
|
||||
mailboxes."Python mailing list" = { path = "~/.mail/python.mbox", subscribe = true, autoload = true }
|
||||
.Ed
|
||||
.Ss NNTP
|
||||
NNTP specific options
|
||||
.Bl -tag -width 36n
|
||||
.It Ic server_hostname Ar String
|
||||
example:
|
||||
.Qq nntp.example.com
|
||||
.It Ic server_username Ar String
|
||||
Server username
|
||||
.It Ic server_password Ar String
|
||||
Server password
|
||||
.It Ic require_auth Ar bool
|
||||
.Pq Em optional
|
||||
require authentication in every case
|
||||
.\" default value
|
||||
.Pq Em true
|
||||
.It Ic use_tls Ar boolean
|
||||
.Pq Em optional
|
||||
Connect with TLS.
|
||||
.\" default value
|
||||
.Pq Em false
|
||||
.It Ic server_port Ar number
|
||||
.Pq Em optional
|
||||
The port to connect to
|
||||
.\" default value
|
||||
.Pq Em 119
|
||||
.It Ic danger_accept_invalid_certs Ar boolean
|
||||
.Pq Em optional
|
||||
Do not validate TLS certificates.
|
||||
.\" default value
|
||||
.Pq Em false
|
||||
.El
|
||||
.Pp
|
||||
You have to explicitly state the groups you want to see in the
|
||||
.Ic mailboxes
|
||||
field.
|
||||
Example:
|
||||
.Bd -literal
|
||||
[accounts.sicpm.mailboxes]
|
||||
"sic.all" = {}
|
||||
.Ed
|
||||
.Pp
|
||||
To submit articles directly to the NNTP server, you must set the special value
|
||||
.Em server_submission
|
||||
in the
|
||||
.Ic send_mail
|
||||
field.
|
||||
Example:
|
||||
.Bd -literal
|
||||
composing.send_mail = "server_submission"
|
||||
.Ed
|
||||
.Ss MAILBOXES
|
||||
.Bl -tag -width 36n
|
||||
.It Ic alias Ar String
|
||||
|
@ -453,20 +403,10 @@ and
|
|||
\&.
|
||||
Example:
|
||||
.Bd -literal
|
||||
[accounts."imap.example.com".mailboxes]
|
||||
"INBOX" = { index_style = "plain" }
|
||||
"INBOX/Lists/devlist" = { autoload = false, pager = { filter = "pygmentize -l diff -f 256"} }
|
||||
.Ed
|
||||
.It Ic sort_order Ar unsigned integer
|
||||
.Pq Em optional
|
||||
Override sort order on the sidebar for this mailbox.
|
||||
Example:
|
||||
.Bd -literal
|
||||
[accounts."imap.example.com".mailboxes]
|
||||
"INBOX" = { index_style = "plain" }
|
||||
"INBOX/Sent" = { sort_order = 0 }
|
||||
"INBOX/Drafts" = { sort_order = 1 }
|
||||
"INBOX/Lists" = { sort_order = 2 }
|
||||
[accounts."imap.example.com".mailboxes."INBOX"]
|
||||
index_style = "plain"
|
||||
[accounts."imap.example.com".mailboxes."INBOX".pager]
|
||||
filter = ""
|
||||
.Ed
|
||||
.El
|
||||
.Sh COMPOSING
|
||||
|
@ -531,11 +471,6 @@ with the replied envelope's date.
|
|||
Whether the strftime call for the attribution string uses the POSIX locale instead of the user's active locale.
|
||||
.\" default value
|
||||
.Pq Em true
|
||||
.It Ic forward_as_attachment Ar boolean or "ask"
|
||||
.Pq Em optional
|
||||
Forward emails as attachment? (Alternative is inline).
|
||||
.\" default value
|
||||
.Pq Em ask
|
||||
.El
|
||||
.Sh SHORTCUTS
|
||||
Shortcuts can take the following values:
|
||||
|
@ -624,14 +559,6 @@ Go to the
|
|||
.Em n Ns
|
||||
th tab
|
||||
.Pq Em cannot be redefined
|
||||
.It Ic info_message_next
|
||||
Show next info message, if any
|
||||
.\" default value
|
||||
.Pq Ql Em M->
|
||||
.It Ic info_message_previous
|
||||
Show previous info message, if any
|
||||
.\" default value
|
||||
.Pq Ql Em M-<
|
||||
.El
|
||||
.sp
|
||||
.Em listing
|
||||
|
@ -767,18 +694,6 @@ View raw envelope source in a pager.
|
|||
Reply to envelope.
|
||||
.\" default value
|
||||
.Pq Em R
|
||||
.It Ic reply_to_author
|
||||
Reply to author.
|
||||
.\" default value
|
||||
.Pq Em Ctrl-r
|
||||
.It Ic reply_to_all
|
||||
Reply to all/Reply to list/Follow up.
|
||||
.\" default value
|
||||
.Pq Em Ctrl-g
|
||||
.It Ic forward
|
||||
Forward email.
|
||||
.\" default value
|
||||
.Pq Em Ctrl-f
|
||||
.It Ic edit
|
||||
Open envelope in composer.
|
||||
.\" default value
|
||||
|
@ -800,11 +715,7 @@ for the mailcap file locations.
|
|||
.\" default value
|
||||
.Pq Em m
|
||||
.It Ic go_to_url
|
||||
Go to url of given index (with the command
|
||||
.Ic url_launcher
|
||||
setting in
|
||||
.Sx PAGER
|
||||
section)
|
||||
Go to url of given index
|
||||
.\" default value
|
||||
.Pq Em g
|
||||
.It Ic toggle_url_mode
|
||||
|
@ -912,17 +823,6 @@ Minimum text width in columns.
|
|||
Choose `text/html` alternative if `text/plain` is empty in `multipart/alternative` attachments.
|
||||
.\" default value
|
||||
.Pq Em true
|
||||
.It Ic show_date_in_my_timezone Ar boolean
|
||||
.Pq Em optional
|
||||
Show Date: in local timezone
|
||||
.\" default value
|
||||
.Pq Em true
|
||||
.It Ic url_launcher Ar String
|
||||
.Pq Em optional
|
||||
A command to launch URLs with.
|
||||
The URL will be given as the first argument of the command.
|
||||
.\" default value
|
||||
.Pq Em xdg-open
|
||||
.El
|
||||
.Sh LISTING
|
||||
.Bl -tag -width 36n
|
||||
|
|
|
@ -14,9 +14,11 @@ color_aliases = { "neon_green" = "#6ef9d4", "darkgrey" = "#4a4a4a", "neon_purp
|
|||
"mail.listing.conversations.date" = { fg = "$neon_purple", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.from" = { fg = "$darkgrey", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.highlighted" = { fg = "theme_default", bg = "Grey58", attrs = "theme_default" }
|
||||
"mail.listing.conversations.padding" = { fg = "Black", bg = "Black", attrs = "theme_default" }
|
||||
"mail.listing.conversations.selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
|
||||
"mail.listing.conversations.subject" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.unseen" = { fg = "Black", bg = "Grey78", attrs = "theme_default" }
|
||||
"mail.listing.conversations.unseen_padding" = { fg = "Black", bg = "Black", attrs = "theme_default" }
|
||||
"mail.listing.plain.even" = { fg = "theme_default", bg = "Grey19", attrs = "theme_default" }
|
||||
"mail.listing.plain.even_highlighted" = { fg = "theme_default", bg = "Grey58", attrs = "theme_default" }
|
||||
"mail.listing.plain.even_selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
|
||||
|
|
|
@ -13,9 +13,11 @@
|
|||
"mail.listing.conversations" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.from" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.highlighted" = { fg = "theme_default", bg = "Grey58", attrs = "theme_default" }
|
||||
"mail.listing.conversations.padding" = { fg = "Grey15", bg = "Grey15", attrs = "theme_default" }
|
||||
"mail.listing.conversations.selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
|
||||
"mail.listing.conversations.subject" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.unseen" = { fg = "Black", bg = "Grey78", attrs = "theme_default" }
|
||||
"mail.listing.conversations.unseen_padding" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.plain.even" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.plain.odd" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.plain.even_unseen" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
|
|
|
@ -16,9 +16,11 @@ color_aliases = { "JewelGreen" = "#157241", "PinkLace" = "#FFD5FD", "TorchRed" =
|
|||
"mail.listing.conversations" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.from" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.highlighted" = { fg = "$JewelGreen", bg = "$SunsetOrange", attrs = "theme_default" }
|
||||
"mail.listing.conversations.padding" = { fg = "$TorchRed", bg = "Grey15", attrs = "theme_default" }
|
||||
"mail.listing.conversations.selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
|
||||
"mail.listing.conversations.subject" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.unseen" = { fg = "Black", bg = "mail.listing.compact.even_unseen", attrs = "theme_default" }
|
||||
"mail.listing.conversations.unseen_padding" = { fg = "$BlueStone", bg = "mail.listing.conversations.unseen", attrs = "theme_default" }
|
||||
"mail.listing.plain.even" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.plain.odd" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.plain.even_unseen" = { fg = "theme_default", bg = "mail.listing.compact.even_unseen", attrs = "theme_default" }
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "melib"
|
||||
version = "0.7.2"
|
||||
version = "0.6.2"
|
||||
authors = ["Manos Pitsidianakis <epilys@nessuent.xyz>"]
|
||||
workspace = ".."
|
||||
edition = "2018"
|
||||
|
|
|
@ -43,6 +43,7 @@ fn main() -> Result<(), std::io::Error> {
|
|||
const EMOJI_DATA_URL: &str =
|
||||
"https://www.unicode.org/Public/UCD/latest/ucd/emoji/emoji-data.txt";
|
||||
|
||||
|
||||
let mod_path = Path::new(MOD_PATH);
|
||||
if mod_path.exists() {
|
||||
eprintln!(
|
||||
|
@ -163,7 +164,7 @@ fn main() -> Result<(), std::io::Error> {
|
|||
|
||||
fn set_general_categories<'u>(codepoints: &mut Vec<Codepoint<'u>>, unicode_data: &'u str) {
|
||||
for line in unicode_data.lines() {
|
||||
let fields = line.trim().split(';').collect::<Vec<_>>();
|
||||
let fields = line.trim().split(";").collect::<Vec<_>>();
|
||||
if fields.len() > FIELD_CATEGORY {
|
||||
for idx in hexrange_to_range(fields[FIELD_CODEPOINT]) {
|
||||
codepoints[idx].category = fields[FIELD_CATEGORY];
|
||||
|
@ -223,7 +224,7 @@ fn main() -> Result<(), std::io::Error> {
|
|||
fn set_emoji_widths(codepoints: &mut Vec<Codepoint<'_>>, emoji_data_lines: &str) {
|
||||
// Read from emoji-data.txt, set codepoint widths
|
||||
for line in emoji_data_lines.lines() {
|
||||
if !line.contains('#') || line.trim().starts_with('#') {
|
||||
if !line.contains("#") || line.trim().starts_with("#") {
|
||||
continue;
|
||||
}
|
||||
let mut fields = line.trim().split('#').collect::<Vec<_>>();
|
||||
|
@ -233,7 +234,7 @@ fn main() -> Result<(), std::io::Error> {
|
|||
let comment = fields.pop().unwrap();
|
||||
let fields = fields.pop().unwrap();
|
||||
|
||||
let hexrange = fields.split(';').next().unwrap();
|
||||
let hexrange = fields.split(";").next().unwrap();
|
||||
|
||||
// In later versions of emoji-data.txt there are some "reserved"
|
||||
// entries that have "NA" instead of a Unicode version number
|
||||
|
@ -245,7 +246,7 @@ fn main() -> Result<(), std::io::Error> {
|
|||
|
||||
use std::str::FromStr;
|
||||
let mut v = comment.trim().split_whitespace().next().unwrap();
|
||||
if v.starts_with('E') {
|
||||
if v.starts_with("E") {
|
||||
v = &v[1..];
|
||||
}
|
||||
if v.as_bytes()
|
||||
|
|
|
@ -105,17 +105,9 @@ impl AddressBook {
|
|||
{
|
||||
let mut ret = AddressBook::new(s.name.clone());
|
||||
if let Some(vcard_path) = s.vcard_folder() {
|
||||
match vcard::load_cards(std::path::Path::new(vcard_path)) {
|
||||
Ok(cards) => {
|
||||
for c in cards {
|
||||
ret.add_card(c);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
crate::log(
|
||||
format!("Could not load vcards from {:?}: {}", vcard_path, err),
|
||||
crate::WARN,
|
||||
);
|
||||
if let Ok(cards) = vcard::load_cards(&std::path::Path::new(vcard_path)) {
|
||||
for c in cards {
|
||||
ret.add_card(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,12 +45,8 @@ impl VCardVersion for VCardVersion3 {}
|
|||
|
||||
pub struct CardDeserializer;
|
||||
|
||||
const HEADER_CRLF: &str = "BEGIN:VCARD\r\n"; //VERSION:4.0\r\n";
|
||||
const FOOTER_CRLF: &str = "END:VCARD\r\n";
|
||||
const HEADER_LF: &str = "BEGIN:VCARD\n"; //VERSION:4.0\n";
|
||||
const FOOTER_LF: &str = "END:VCARD\n";
|
||||
const HEADER: &str = "BEGIN:VCARD"; //VERSION:4.0";
|
||||
const FOOTER: &str = "END:VCARD";
|
||||
static HEADER: &str = "BEGIN:VCARD\r\n"; //VERSION:4.0\r\n";
|
||||
static FOOTER: &str = "END:VCARD\r\n";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VCard<T: VCardVersion>(
|
||||
|
@ -76,14 +72,10 @@ pub struct ContentLine {
|
|||
|
||||
impl CardDeserializer {
|
||||
pub fn from_str(mut input: &str) -> Result<VCard<impl VCardVersion>> {
|
||||
input = if (!input.starts_with(HEADER_CRLF) || !input.ends_with(FOOTER_CRLF))
|
||||
&& (!input.starts_with(HEADER_LF) || !input.ends_with(FOOTER_LF))
|
||||
{
|
||||
input = if !input.starts_with(HEADER) || !input.ends_with(FOOTER) {
|
||||
return Err(MeliError::new(format!("Error while parsing vcard: input does not start or end with correct header and footer. input is:\n{:?}", input)));
|
||||
} else if input.starts_with(HEADER_CRLF) {
|
||||
&input[HEADER_CRLF.len()..input.len() - FOOTER_CRLF.len()]
|
||||
} else {
|
||||
&input[HEADER_LF.len()..input.len() - FOOTER_LF.len()]
|
||||
&input[HEADER.len()..input.len() - FOOTER.len()]
|
||||
};
|
||||
|
||||
let mut ret = HashMap::default();
|
||||
|
@ -277,24 +269,16 @@ pub fn load_cards(p: &std::path::Path) -> Result<Vec<Card>> {
|
|||
use std::io::Read;
|
||||
contents.clear();
|
||||
std::fs::File::open(&f)?.read_to_string(&mut contents)?;
|
||||
match parse_card().parse(contents.as_str()) {
|
||||
Ok((_, c)) => {
|
||||
for s in c {
|
||||
ret.push(
|
||||
CardDeserializer::from_str(s)
|
||||
.and_then(TryInto::try_into)
|
||||
.map(|mut card| {
|
||||
Card::set_external_resource(&mut card, true);
|
||||
is_any_valid = true;
|
||||
card
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
crate::log(
|
||||
format!("Could not parse vcard from {}: {}", f.display(), err),
|
||||
crate::WARN,
|
||||
if let Ok((_, c)) = parse_card().parse(contents.as_str()) {
|
||||
for s in c {
|
||||
ret.push(
|
||||
CardDeserializer::from_str(s)
|
||||
.and_then(TryInto::try_into)
|
||||
.and_then(|mut card| {
|
||||
Card::set_external_resource(&mut card, true);
|
||||
is_any_valid = true;
|
||||
Ok(card)
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -306,16 +290,16 @@ pub fn load_cards(p: &std::path::Path) -> Result<Vec<Card>> {
|
|||
debug!(&c);
|
||||
}
|
||||
}
|
||||
if is_any_valid {
|
||||
if !is_any_valid {
|
||||
ret.into_iter().collect::<Result<Vec<Card>>>()
|
||||
} else {
|
||||
ret.retain(Result::is_ok);
|
||||
ret.into_iter().collect::<Result<Vec<Card>>>()
|
||||
}
|
||||
ret.into_iter().collect::<Result<Vec<Card>>>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_card() {
|
||||
let j = "BEGIN:VCARD\r\nVERSION:4.0\r\nN:Gump;Forrest;;Mr.;\r\nFN:Forrest Gump\r\nORG:Bubba Gump Shrimp Co.\r\nTITLE:Shrimp Man\r\nPHOTO;MEDIATYPE=image/gif:http://www.example.com/dir_photos/my_photo.gif\r\nTEL;TYPE=work,voice;VALUE=uri:tel:+1-111-555-1212\r\nTEL;TYPE=home,voice;VALUE=uri:tel:+1-404-555-1212\r\nADR;TYPE=WORK;PREF=1;LABEL=\"100 Waters Edge\\nBaytown\\, LA 30314\\nUnited States of America\":;;100 Waters Edge;Baytown;LA;30314;United States of America\r\nADR;TYPE=HOME;LABEL=\"42 Plantation St.\\nBaytown\\, LA 30314\\nUnited States of America\":;;42 Plantation St.;Baytown;LA;30314;United States of America\r\nEMAIL:forrestgump@example.com\r\nREV:20080424T195243Z\r\nx-qq:21588891\r\nEND:VCARD\r\n";
|
||||
println!("results = {:#?}", CardDeserializer::from_str(j).unwrap());
|
||||
let j = "BEGIN:VCARD\nVERSION:4.0\nN:Gump;Forrest;;Mr.;\nFN:Forrest Gump\nORG:Bubba Gump Shrimp Co.\nTITLE:Shrimp Man\nPHOTO;MEDIATYPE=image/gif:http://www.example.com/dir_photos/my_photo.gif\nTEL;TYPE=work,voice;VALUE=uri:tel:+1-111-555-1212\nTEL;TYPE=home,voice;VALUE=uri:tel:+1-404-555-1212\nADR;TYPE=WORK;PREF=1;LABEL=\"100 Waters Edge\\nBaytown\\, LA 30314\\nUnited States of America\":;;100 Waters Edge;Baytown;LA;30314;United States of America\nADR;TYPE=HOME;LABEL=\"42 Plantation St.\\nBaytown\\, LA 30314\\nUnited States of America\":;;42 Plantation St.;Baytown;LA;30314;United States of America\nEMAIL:forrestgump@example.com\nREV:20080424T195243Z\nx-qq:21588891\nEND:VCARD\n";
|
||||
println!("results = {:#?}", CardDeserializer::from_str(j).unwrap());
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ pub use self::imap::ImapType;
|
|||
#[cfg(feature = "imap_backend")]
|
||||
pub use self::nntp::NntpType;
|
||||
use crate::conf::AccountSettings;
|
||||
use crate::error::{ErrorKind, MeliError, Result};
|
||||
use crate::error::{MeliError, Result};
|
||||
|
||||
#[cfg(feature = "maildir_backend")]
|
||||
use self::maildir::MaildirType;
|
||||
|
@ -156,11 +156,7 @@ impl Backends {
|
|||
}
|
||||
#[cfg(feature = "notmuch_backend")]
|
||||
{
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let dlpath = "libnotmuch.so.5";
|
||||
#[cfg(target_os = "macos")]
|
||||
let dlpath = "libnotmuch.5.dylib";
|
||||
if libloading::Library::new(dlpath).is_ok() {
|
||||
if libloading::Library::new("libnotmuch.so.5").is_ok() {
|
||||
b.register(
|
||||
"notmuch".to_string(),
|
||||
Backend {
|
||||
|
@ -314,13 +310,26 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
|
|||
Ok(Box::pin(async { Ok(()) }))
|
||||
}
|
||||
|
||||
fn fetch_batch(&mut self, env_hashes: EnvelopeHashBatch) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn load(&mut self, mailbox_hash: MailboxHash) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn fetch(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Result<Vec<Envelope>>> + Send + 'static>>>;
|
||||
|
||||
fn refresh(&mut self, mailbox_hash: MailboxHash) -> ResultFuture<()>;
|
||||
fn watch(&self) -> ResultFuture<()>;
|
||||
|
||||
/// Return a [`Box<dyn BackendWatcher>`](BackendWatcher), to which you can register the
|
||||
/// mailboxes you are interested in for updates and then consume to spawn a watching `Future`.
|
||||
/// The `Future` sends events to the [`BackendEventConsumer`](BackendEventConsumer) supplied to
|
||||
/// the backend in its constructor method.
|
||||
fn watcher(&self) -> Result<Box<dyn BackendWatcher>>;
|
||||
fn mailboxes(&self) -> ResultFuture<HashMap<MailboxHash, Mailbox>>;
|
||||
fn operation(&self, hash: EnvelopeHash) -> Result<Box<dyn BackendOp>>;
|
||||
|
||||
|
@ -360,14 +369,14 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
|
|||
&mut self,
|
||||
_path: String,
|
||||
) -> ResultFuture<(MailboxHash, HashMap<MailboxHash, Mailbox>)> {
|
||||
Err(MeliError::new("Unimplemented.").set_kind(ErrorKind::NotImplemented))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn delete_mailbox(
|
||||
&mut self,
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
|
||||
Err(MeliError::new("Unimplemented.").set_kind(ErrorKind::NotImplemented))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn set_mailbox_subscription(
|
||||
|
@ -375,7 +384,7 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_val: bool,
|
||||
) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Unimplemented.").set_kind(ErrorKind::NotImplemented))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn rename_mailbox(
|
||||
|
@ -383,7 +392,7 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_new_path: String,
|
||||
) -> ResultFuture<Mailbox> {
|
||||
Err(MeliError::new("Unimplemented.").set_kind(ErrorKind::NotImplemented))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn set_mailbox_permissions(
|
||||
|
@ -391,7 +400,7 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_val: MailboxPermissions,
|
||||
) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Unimplemented.").set_kind(ErrorKind::NotImplemented))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn search(
|
||||
|
@ -399,16 +408,7 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
|
|||
_query: crate::search::Query,
|
||||
_mailbox_hash: Option<MailboxHash>,
|
||||
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>> {
|
||||
Err(MeliError::new("Unimplemented.").set_kind(ErrorKind::NotImplemented))
|
||||
}
|
||||
|
||||
fn submit(
|
||||
&self,
|
||||
_bytes: Vec<u8>,
|
||||
_mailbox_hash: Option<MailboxHash>,
|
||||
_flags: Option<Flag>,
|
||||
) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Not supported in this backend.").set_kind(ErrorKind::NotSupported))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -636,7 +636,7 @@ impl EnvelopeHashBatch {
|
|||
#[derive(Default, Clone)]
|
||||
pub struct LazyCountSet {
|
||||
not_yet_seen: usize,
|
||||
set: BTreeSet<EnvelopeHash>,
|
||||
pub set: BTreeSet<EnvelopeHash>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for LazyCountSet {
|
||||
|
@ -724,3 +724,31 @@ impl std::ops::Deref for IsSubscribedFn {
|
|||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Urgency for the events of a single Mailbox.
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum MailboxWatchUrgency {
|
||||
High,
|
||||
Medium,
|
||||
Low,
|
||||
}
|
||||
|
||||
/// Register the mailboxes you are interested in for updates and then consume with `spawn` to spawn
|
||||
/// a watching `Future`. The `Future` sends events to the
|
||||
/// [`BackendEventConsumer`](backends::BackendEventConsumer) supplied to the backend in its constructor
|
||||
/// method.
|
||||
pub trait BackendWatcher: ::std::fmt::Debug + Send + Sync {
|
||||
/// Whether the watcher's `Future` requires blocking I/O.
|
||||
fn is_blocking(&self) -> bool;
|
||||
fn register_mailbox(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
urgency: MailboxWatchUrgency,
|
||||
) -> Result<()>;
|
||||
fn set_polling_period(&mut self, period: Option<std::time::Duration>) -> Result<()>;
|
||||
fn spawn(self: Box<Self>) -> ResultFuture<()>;
|
||||
/// Use the [`Any`](std::any::Any) trait to get the underlying type implementing the
|
||||
/// [`BackendWatcher`](backends::BackendEventConsumer) trait.
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
}
|
||||
|
|
|
@ -370,13 +370,13 @@ impl MailBackend for ImapType {
|
|||
let main_conn = self.connection.clone();
|
||||
let uid_store = self.uid_store.clone();
|
||||
Ok(Box::pin(async move {
|
||||
let inbox = timeout(uid_store.timeout, uid_store.mailboxes.lock())
|
||||
let mut inbox = timeout(uid_store.timeout, uid_store.mailboxes.lock())
|
||||
.await?
|
||||
.get(&mailbox_hash)
|
||||
.map(std::clone::Clone::clone)
|
||||
.unwrap();
|
||||
let mut conn = timeout(uid_store.timeout, main_conn.lock()).await?;
|
||||
watch::examine_updates(inbox, &mut conn, &uid_store).await?;
|
||||
watch::ImapWatcher::examine_updates(&mut inbox, &mut conn, &uid_store).await?;
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
@ -450,68 +450,16 @@ impl MailBackend for ImapType {
|
|||
}))
|
||||
}
|
||||
|
||||
fn watch(&self) -> ResultFuture<()> {
|
||||
fn watcher(&self) -> Result<Box<dyn BackendWatcher>> {
|
||||
let server_conf = self.server_conf.clone();
|
||||
let main_conn = self.connection.clone();
|
||||
let uid_store = self.uid_store.clone();
|
||||
Ok(Box::pin(async move {
|
||||
let has_idle: bool = match server_conf.protocol {
|
||||
ImapProtocol::IMAP {
|
||||
extension_use: ImapExtensionUse { idle, .. },
|
||||
} => {
|
||||
idle && uid_store
|
||||
.capabilities
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"IDLE"))
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
while let Err(err) = if has_idle {
|
||||
idle(ImapWatchKit {
|
||||
conn: ImapConnection::new_connection(&server_conf, uid_store.clone()),
|
||||
main_conn: main_conn.clone(),
|
||||
uid_store: uid_store.clone(),
|
||||
})
|
||||
.await
|
||||
} else {
|
||||
poll_with_examine(ImapWatchKit {
|
||||
conn: ImapConnection::new_connection(&server_conf, uid_store.clone()),
|
||||
main_conn: main_conn.clone(),
|
||||
uid_store: uid_store.clone(),
|
||||
})
|
||||
.await
|
||||
} {
|
||||
let mut main_conn_lck = timeout(uid_store.timeout, main_conn.lock()).await?;
|
||||
if err.kind.is_network() {
|
||||
uid_store.is_online.lock().unwrap().1 = Err(err.clone());
|
||||
} else {
|
||||
return Err(err);
|
||||
}
|
||||
debug!("Watch failure: {}", err.to_string());
|
||||
match timeout(uid_store.timeout, main_conn_lck.connect())
|
||||
.await
|
||||
.and_then(|res| res)
|
||||
{
|
||||
Err(err2) => {
|
||||
debug!("Watch reconnect attempt failed: {}", err2.to_string());
|
||||
}
|
||||
Ok(()) => {
|
||||
debug!("Watch reconnect attempt succesful");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let account_hash = uid_store.account_hash;
|
||||
main_conn_lck.add_refresh_event(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash: 0,
|
||||
kind: RefreshEventKind::Failure(err.clone()),
|
||||
});
|
||||
return Err(err);
|
||||
}
|
||||
debug!("watch future returning");
|
||||
Ok(())
|
||||
Ok(Box::new(ImapWatcher {
|
||||
main_conn,
|
||||
uid_store,
|
||||
mailbox_hashes: BTreeSet::default(),
|
||||
polling_period: std::time::Duration::from_secs(5 * 60),
|
||||
server_conf,
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -1178,20 +1126,20 @@ impl MailBackend for ImapType {
|
|||
keyword => {
|
||||
s.push_str(" KEYWORD ");
|
||||
s.push_str(keyword);
|
||||
s.push(' ');
|
||||
s.push_str(" ");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
And(q1, q2) => {
|
||||
rec(q1, s);
|
||||
s.push(' ');
|
||||
s.push_str(" ");
|
||||
rec(q2, s);
|
||||
}
|
||||
Or(q1, q2) => {
|
||||
s.push_str(" OR ");
|
||||
rec(q1, s);
|
||||
s.push(' ');
|
||||
s.push_str(" ");
|
||||
rec(q2, s);
|
||||
}
|
||||
Not(q) => {
|
||||
|
@ -1433,7 +1381,7 @@ impl ImapType {
|
|||
if !l.starts_with(b"*") {
|
||||
continue;
|
||||
}
|
||||
if let Ok(mut mailbox) = protocol_parser::list_mailbox_result(l).map(|(_, v)| v) {
|
||||
if let Ok(mut mailbox) = protocol_parser::list_mailbox_result(&l).map(|(_, v)| v) {
|
||||
if let Some(parent) = mailbox.parent {
|
||||
if mailboxes.contains_key(&parent) {
|
||||
mailboxes
|
||||
|
@ -1501,40 +1449,9 @@ impl ImapType {
|
|||
}
|
||||
|
||||
pub fn validate_config(s: &AccountSettings) -> Result<()> {
|
||||
let mut keys: HashSet<&'static str> = Default::default();
|
||||
macro_rules! get_conf_val {
|
||||
($s:ident[$var:literal]) => {{
|
||||
keys.insert($var);
|
||||
$s.extra.get($var).ok_or_else(|| {
|
||||
MeliError::new(format!(
|
||||
"Configuration error ({}): IMAP connection requires the field `{}` set",
|
||||
$s.name.as_str(),
|
||||
$var
|
||||
))
|
||||
})
|
||||
}};
|
||||
($s:ident[$var:literal], $default:expr) => {{
|
||||
keys.insert($var);
|
||||
$s.extra
|
||||
.get($var)
|
||||
.map(|v| {
|
||||
<_>::from_str(v).map_err(|e| {
|
||||
MeliError::new(format!(
|
||||
"Configuration error ({}): Invalid value for field `{}`: {}\n{}",
|
||||
$s.name.as_str(),
|
||||
$var,
|
||||
v,
|
||||
e
|
||||
))
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| Ok($default))
|
||||
}};
|
||||
}
|
||||
get_conf_val!(s["server_hostname"])?;
|
||||
get_conf_val!(s["server_username"])?;
|
||||
let use_oauth2: bool = get_conf_val!(s["use_oauth2"], false)?;
|
||||
keys.insert("server_password_command");
|
||||
if !s.extra.contains_key("server_password_command") {
|
||||
if use_oauth2 {
|
||||
return Err(MeliError::new(format!(
|
||||
|
@ -1549,9 +1466,9 @@ impl ImapType {
|
|||
s.name.as_str(),
|
||||
)));
|
||||
}
|
||||
get_conf_val!(s["server_port"], 143)?;
|
||||
let server_port = get_conf_val!(s["server_port"], 143)?;
|
||||
let use_tls = get_conf_val!(s["use_tls"], true)?;
|
||||
let use_starttls = get_conf_val!(s["use_starttls"], false)?;
|
||||
let use_starttls = get_conf_val!(s["use_starttls"], !(server_port == 993))?;
|
||||
if !use_tls && use_starttls {
|
||||
return Err(MeliError::new(format!(
|
||||
"Configuration error ({}): incompatible use_tls and use_starttls values: use_tls = false, use_starttls = true",
|
||||
|
@ -1583,18 +1500,6 @@ impl ImapType {
|
|||
)));
|
||||
}
|
||||
let _timeout = get_conf_val!(s["timeout"], 16_u64)?;
|
||||
let extra_keys = s
|
||||
.extra
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect::<HashSet<&str>>();
|
||||
let diff = extra_keys.difference(&keys).collect::<Vec<&&str>>();
|
||||
if !diff.is_empty() {
|
||||
return Err(MeliError::new(format!(
|
||||
"Configuration error ({}): the following flags are set but are not recognized: {:?}.",
|
||||
s.name.as_str(), diff
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -1785,7 +1690,7 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
|||
ref uid,
|
||||
ref mut envelope,
|
||||
ref mut flags,
|
||||
raw_fetch_value,
|
||||
ref raw_fetch_value,
|
||||
ref references,
|
||||
..
|
||||
} in v.iter_mut()
|
||||
|
|
|
@ -319,7 +319,7 @@ impl ImapStream {
|
|||
.find(|l| l.starts_with(b"* CAPABILITY"))
|
||||
.ok_or_else(|| MeliError::new(""))
|
||||
.and_then(|res| {
|
||||
protocol_parser::capabilities(res)
|
||||
protocol_parser::capabilities(&res)
|
||||
.map_err(|_| MeliError::new(""))
|
||||
.map(|(_, v)| v)
|
||||
});
|
||||
|
@ -392,7 +392,7 @@ impl ImapStream {
|
|||
let mut should_break = false;
|
||||
for l in res.split_rn() {
|
||||
if l.starts_with(b"* CAPABILITY") {
|
||||
capabilities = protocol_parser::capabilities(l)
|
||||
capabilities = protocol_parser::capabilities(&l)
|
||||
.map(|(_, capabilities)| {
|
||||
HashSet::from_iter(capabilities.into_iter().map(|s: &[u8]| s.to_vec()))
|
||||
})
|
||||
|
@ -875,9 +875,9 @@ impl ImapConnection {
|
|||
debug!(
|
||||
"{} select response {}",
|
||||
imap_path,
|
||||
String::from_utf8_lossy(ret)
|
||||
String::from_utf8_lossy(&ret)
|
||||
);
|
||||
let select_response = protocol_parser::select_response(ret).chain_err_summary(|| {
|
||||
let select_response = protocol_parser::select_response(&ret).chain_err_summary(|| {
|
||||
format!("Could not parse select response for mailbox {}", imap_path)
|
||||
})?;
|
||||
{
|
||||
|
@ -958,8 +958,8 @@ impl ImapConnection {
|
|||
.await?;
|
||||
self.read_response(ret, RequiredResponses::EXAMINE_REQUIRED)
|
||||
.await?;
|
||||
debug!("examine response {}", String::from_utf8_lossy(ret));
|
||||
let select_response = protocol_parser::select_response(ret).chain_err_summary(|| {
|
||||
debug!("examine response {}", String::from_utf8_lossy(&ret));
|
||||
let select_response = protocol_parser::select_response(&ret).chain_err_summary(|| {
|
||||
format!("Could not parse select response for mailbox {}", imap_path)
|
||||
})?;
|
||||
self.stream.as_mut()?.current_mailbox = MailboxSelection::Examine(mailbox_hash);
|
||||
|
@ -980,38 +980,45 @@ impl ImapConnection {
|
|||
|
||||
pub async fn unselect(&mut self) -> Result<()> {
|
||||
match self.stream.as_mut()?.current_mailbox.take() {
|
||||
MailboxSelection::Examine(_) | MailboxSelection::Select(_) => {
|
||||
let mut response = Vec::with_capacity(8 * 1024);
|
||||
if self
|
||||
.uid_store
|
||||
.capabilities
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"UNSELECT"))
|
||||
{
|
||||
self.send_command(b"UNSELECT").await?;
|
||||
self.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
} else {
|
||||
/* `RFC3691 - UNSELECT Command` states: "[..] IMAP4 provides this
|
||||
* functionality (via a SELECT command with a nonexistent mailbox name or
|
||||
* reselecting the same mailbox with EXAMINE command)[..]
|
||||
*/
|
||||
let mut nonexistent = "blurdybloop".to_string();
|
||||
MailboxSelection::Examine(_) |
|
||||
MailboxSelection::Select(_) => {
|
||||
let mut response = Vec::with_capacity(8 * 1024);
|
||||
if self
|
||||
.uid_store
|
||||
.capabilities
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"UNSELECT"))
|
||||
{
|
||||
let mailboxes = self.uid_store.mailboxes.lock().await;
|
||||
while mailboxes.values().any(|m| m.imap_path() == nonexistent) {
|
||||
nonexistent.push('p');
|
||||
self.send_command(b"UNSELECT").await?;
|
||||
self.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
} else {
|
||||
/* `RFC3691 - UNSELECT Command` states: "[..] IMAP4 provides this
|
||||
* functionality (via a SELECT command with a nonexistent mailbox name or
|
||||
* reselecting the same mailbox with EXAMINE command)[..]
|
||||
*/
|
||||
let mut nonexistent = "blurdybloop".to_string();
|
||||
{
|
||||
let mailboxes = self.uid_store.mailboxes.lock().await;
|
||||
while mailboxes.values().any(|m| m.imap_path() == nonexistent) {
|
||||
nonexistent.push('p');
|
||||
}
|
||||
}
|
||||
self.send_command(
|
||||
format!(
|
||||
"SELECT \"{}\"",
|
||||
nonexistent
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
self.read_response(&mut response, RequiredResponses::NO_REQUIRED)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
self.send_command(format!("SELECT \"{}\"", nonexistent).as_bytes())
|
||||
.await?;
|
||||
self.read_response(&mut response, RequiredResponses::NO_REQUIRED)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
MailboxSelection::None => {}
|
||||
MailboxSelection::None => {},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -154,7 +154,7 @@ impl BackendOp for ImapOp {
|
|||
.set_summary(format!("message with UID {} was not found?", uid)));
|
||||
}
|
||||
let (_uid, (_flags, _)) = v[0];
|
||||
assert_eq!(_uid, uid);
|
||||
assert_eq!(uid, uid);
|
||||
let mut bytes_cache = uid_store.byte_cache.lock()?;
|
||||
let cache = bytes_cache.entry(uid).or_default();
|
||||
cache.flags = Some(_flags);
|
||||
|
|
|
@ -119,8 +119,8 @@ impl RequiredResponses {
|
|||
}
|
||||
if self.intersects(RequiredResponses::FETCH) {
|
||||
let mut ptr = 0;
|
||||
for (i, l) in line.iter().enumerate() {
|
||||
if !l.is_ascii_digit() {
|
||||
for i in 0..line.len() {
|
||||
if !line[i].is_ascii_digit() {
|
||||
ptr = i;
|
||||
break;
|
||||
}
|
||||
|
@ -257,7 +257,7 @@ pub enum ImapResponse {
|
|||
impl TryFrom<&'_ [u8]> for ImapResponse {
|
||||
type Error = MeliError;
|
||||
fn try_from(val: &'_ [u8]) -> Result<ImapResponse> {
|
||||
let val: &[u8] = val.split_rn().last().unwrap_or_else(|| val.as_ref());
|
||||
let val: &[u8] = val.split_rn().last().unwrap_or(val.as_ref());
|
||||
let mut val = val[val.find(b" ").ok_or_else(|| {
|
||||
MeliError::new(format!(
|
||||
"Expected tagged IMAP response (OK,NO,BAD, etc) but found {:?}",
|
||||
|
@ -594,7 +594,7 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
} else {
|
||||
return debug!(Err(MeliError::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
|
||||
String::from_utf8_lossy(input)
|
||||
String::from_utf8_lossy(&input)
|
||||
))));
|
||||
}
|
||||
} else if input[i..].starts_with(b"FLAGS (") {
|
||||
|
@ -605,7 +605,7 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
} else {
|
||||
return debug!(Err(MeliError::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
|
||||
String::from_utf8_lossy(input)
|
||||
String::from_utf8_lossy(&input)
|
||||
))));
|
||||
}
|
||||
} else if input[i..].starts_with(b"MODSEQ (") {
|
||||
|
@ -621,7 +621,7 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
} else {
|
||||
return debug!(Err(MeliError::new(format!(
|
||||
"Unexpected input while parsing MODSEQ in UID FETCH response. Got: `{:.40}`",
|
||||
String::from_utf8_lossy(input)
|
||||
String::from_utf8_lossy(&input)
|
||||
))));
|
||||
}
|
||||
} else if input[i..].starts_with(b"RFC822 {") {
|
||||
|
@ -640,7 +640,7 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
} else {
|
||||
return debug!(Err(MeliError::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
|
||||
String::from_utf8_lossy(input)
|
||||
String::from_utf8_lossy(&input)
|
||||
))));
|
||||
}
|
||||
} else if input[i..].starts_with(b"ENVELOPE (") {
|
||||
|
@ -676,29 +676,13 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
String::from_utf8_lossy(&input[i..])
|
||||
))));
|
||||
}
|
||||
} else if input[i..].starts_with(b"BODY[HEADER.FIELDS (\"REFERENCES\")] ") {
|
||||
i += b"BODY[HEADER.FIELDS (\"REFERENCES\")] ".len();
|
||||
if let Ok((rest, mut references)) = astring_token(&input[i..]) {
|
||||
if !references.trim().is_empty() {
|
||||
if let Ok((_, (_, v))) = crate::email::parser::headers::header(&references) {
|
||||
references = v;
|
||||
}
|
||||
ret.references = Some(references);
|
||||
}
|
||||
i += input.len() - i - rest.len();
|
||||
} else {
|
||||
return debug!(Err(MeliError::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
))));
|
||||
}
|
||||
} else if input[i..].starts_with(b")\r\n") {
|
||||
i += b")\r\n".len();
|
||||
break;
|
||||
} else {
|
||||
debug!(
|
||||
"Got unexpected token while parsing UID FETCH response:\n`{}`\n",
|
||||
String::from_utf8_lossy(input)
|
||||
String::from_utf8_lossy(&input)
|
||||
);
|
||||
return debug!(Err(MeliError::new(format!(
|
||||
"Got unexpected token while parsing UID FETCH response: `{:.40}`",
|
||||
|
@ -909,7 +893,7 @@ pub fn untagged_responses(input: &[u8]) -> ImapParseResult<Option<UntaggedRespon
|
|||
let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(b"\r\n")(input)?;
|
||||
debug!(
|
||||
"Parse untagged response from {:?}",
|
||||
String::from_utf8_lossy(orig_input)
|
||||
String::from_utf8_lossy(&orig_input)
|
||||
);
|
||||
Ok((
|
||||
input,
|
||||
|
@ -1107,7 +1091,7 @@ pub fn select_response(input: &[u8]) -> Result<SelectResponse> {
|
|||
let (_, highestmodseq) = res?;
|
||||
ret.highestmodseq = Some(
|
||||
std::num::NonZeroU64::new(u64::from_str(&String::from_utf8_lossy(
|
||||
highestmodseq,
|
||||
&highestmodseq,
|
||||
))?)
|
||||
.map(|u| Ok(ModSequence(u)))
|
||||
.unwrap_or(Err(())),
|
||||
|
@ -1115,12 +1099,12 @@ pub fn select_response(input: &[u8]) -> Result<SelectResponse> {
|
|||
} else if l.starts_with(b"* OK [NOMODSEQ") {
|
||||
ret.highestmodseq = Some(Err(()));
|
||||
} else if !l.is_empty() {
|
||||
debug!("select response: {}", String::from_utf8_lossy(l));
|
||||
debug!("select response: {}", String::from_utf8_lossy(&l));
|
||||
}
|
||||
}
|
||||
Ok(ret)
|
||||
} else {
|
||||
let ret = String::from_utf8_lossy(input).to_string();
|
||||
let ret = String::from_utf8_lossy(&input).to_string();
|
||||
debug!("BAD/NO response in select: {}", &ret);
|
||||
Err(MeliError::new(ret))
|
||||
}
|
||||
|
@ -1231,7 +1215,7 @@ pub fn flags(input: &[u8]) -> IResult<&[u8], (Flag, Vec<String>)> {
|
|||
}
|
||||
(true, t) if t.eq_ignore_ascii_case(b"Recent") => { /* ignore */ }
|
||||
(_, f) => {
|
||||
keywords.push(String::from_utf8_lossy(f).into());
|
||||
keywords.push(String::from_utf8_lossy(&f).into());
|
||||
}
|
||||
}
|
||||
input = &input[match_end..];
|
||||
|
@ -1401,7 +1385,7 @@ pub fn envelope_address(input: &[u8]) -> IResult<&[u8], Address> {
|
|||
to_str!(&name),
|
||||
if name.is_empty() { "" } else { " " },
|
||||
to_str!(&mailbox_name),
|
||||
to_str!(host_name)
|
||||
to_str!(&host_name)
|
||||
)
|
||||
.into_bytes()
|
||||
} else {
|
||||
|
@ -1469,7 +1453,7 @@ pub fn quoted(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
|
|||
}
|
||||
|
||||
pub fn quoted_or_nil(input: &[u8]) -> IResult<&[u8], Option<Vec<u8>>> {
|
||||
alt((map(tag("NIL"), |_| None), map(quoted, Some)))(input.ltrim())
|
||||
alt((map(tag("NIL"), |_| None), map(quoted, |v| Some(v))))(input.ltrim())
|
||||
}
|
||||
|
||||
pub fn uid_fetch_envelopes_response(
|
||||
|
@ -1542,7 +1526,7 @@ fn eat_whitespace(mut input: &[u8]) -> IResult<&[u8], ()> {
|
|||
break;
|
||||
}
|
||||
}
|
||||
Ok((input, ()))
|
||||
return Ok((input, ()));
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
|
|
|
@ -23,201 +23,129 @@ use crate::backends::SpecialUsageMailbox;
|
|||
use std::sync::Arc;
|
||||
|
||||
/// Arguments for IMAP watching functions
|
||||
pub struct ImapWatchKit {
|
||||
pub conn: ImapConnection,
|
||||
#[derive(Debug)]
|
||||
pub struct ImapWatcher {
|
||||
pub main_conn: Arc<FutureMutex<ImapConnection>>,
|
||||
pub uid_store: Arc<UIDStore>,
|
||||
pub mailbox_hashes: BTreeSet<MailboxHash>,
|
||||
pub polling_period: std::time::Duration,
|
||||
pub server_conf: ImapServerConf,
|
||||
}
|
||||
|
||||
pub async fn poll_with_examine(kit: ImapWatchKit) -> Result<()> {
|
||||
debug!("poll with examine");
|
||||
let ImapWatchKit {
|
||||
mut conn,
|
||||
main_conn: _,
|
||||
uid_store,
|
||||
} = kit;
|
||||
conn.connect().await?;
|
||||
let mailboxes: HashMap<MailboxHash, ImapMailbox> = {
|
||||
let mailboxes_lck = timeout(uid_store.timeout, uid_store.mailboxes.lock()).await?;
|
||||
mailboxes_lck.clone()
|
||||
};
|
||||
loop {
|
||||
for (_, mailbox) in mailboxes.clone() {
|
||||
examine_updates(mailbox, &mut conn, &uid_store).await?;
|
||||
}
|
||||
//FIXME: make sleep duration configurable
|
||||
smol::Timer::after(std::time::Duration::from_secs(3 * 60)).await;
|
||||
impl BackendWatcher for ImapWatcher {
|
||||
fn is_blocking(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn idle(kit: ImapWatchKit) -> Result<()> {
|
||||
debug!("IDLE");
|
||||
/* IDLE only watches the connection's selected mailbox. We will IDLE on INBOX and every ~5
|
||||
* minutes wake up and poll the others */
|
||||
let ImapWatchKit {
|
||||
mut conn,
|
||||
main_conn,
|
||||
uid_store,
|
||||
} = kit;
|
||||
conn.connect().await?;
|
||||
let mailbox: ImapMailbox = match uid_store
|
||||
.mailboxes
|
||||
.lock()
|
||||
.await
|
||||
.values()
|
||||
.find(|f| f.parent.is_none() && (f.special_usage() == SpecialUsageMailbox::Inbox))
|
||||
.map(std::clone::Clone::clone)
|
||||
{
|
||||
Some(mailbox) => mailbox,
|
||||
None => {
|
||||
return Err(MeliError::new("INBOX mailbox not found in local mailbox index. meli may have not parsed the IMAP mailboxes correctly"));
|
||||
fn register_mailbox(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
_urgency: MailboxWatchUrgency,
|
||||
) -> Result<()> {
|
||||
self.mailbox_hashes.insert(mailbox_hash);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_polling_period(&mut self, period: Option<std::time::Duration>) -> Result<()> {
|
||||
if let Some(period) = period {
|
||||
self.polling_period = period;
|
||||
}
|
||||
};
|
||||
let mailbox_hash = mailbox.hash();
|
||||
let mut response = Vec::with_capacity(8 * 1024);
|
||||
let select_response = conn
|
||||
.examine_mailbox(mailbox_hash, &mut response, true)
|
||||
.await?
|
||||
.unwrap();
|
||||
{
|
||||
let mut uidvalidities = uid_store.uidvalidity.lock().unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
if let Some(v) = uidvalidities.get(&mailbox_hash) {
|
||||
if *v != select_response.uidvalidity {
|
||||
if uid_store.keep_offline_cache {
|
||||
#[cfg(not(feature = "sqlite3"))]
|
||||
let mut cache_handle = super::cache::DefaultCache::get(uid_store.clone())?;
|
||||
#[cfg(feature = "sqlite3")]
|
||||
let mut cache_handle = super::cache::Sqlite3Cache::get(uid_store.clone())?;
|
||||
cache_handle.clear(mailbox_hash, &select_response)?;
|
||||
fn spawn(mut self: Box<Self>) -> ResultFuture<()> {
|
||||
Ok(Box::pin(async move {
|
||||
let has_idle: bool = match self.server_conf.protocol {
|
||||
ImapProtocol::IMAP {
|
||||
extension_use: ImapExtensionUse { idle, .. },
|
||||
} => {
|
||||
idle && self
|
||||
.uid_store
|
||||
.capabilities
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"IDLE"))
|
||||
}
|
||||
conn.add_refresh_event(RefreshEvent {
|
||||
account_hash: uid_store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Rescan,
|
||||
_ => false,
|
||||
};
|
||||
while let Err(err) = if has_idle {
|
||||
self.idle().await
|
||||
} else {
|
||||
self.poll_with_examine().await
|
||||
} {
|
||||
let mut main_conn_lck =
|
||||
timeout(self.uid_store.timeout, self.main_conn.lock()).await?;
|
||||
if err.kind.is_network() {
|
||||
self.uid_store.is_online.lock().unwrap().1 = Err(err.clone());
|
||||
} else {
|
||||
return Err(err);
|
||||
}
|
||||
debug!("Watch failure: {}", err.to_string());
|
||||
match timeout(self.uid_store.timeout, main_conn_lck.connect())
|
||||
.await
|
||||
.and_then(|res| res)
|
||||
{
|
||||
Err(err2) => {
|
||||
debug!("Watch reconnect attempt failed: {}", err2.to_string());
|
||||
}
|
||||
Ok(()) => {
|
||||
debug!("Watch reconnect attempt succesful");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let account_hash = self.uid_store.account_hash;
|
||||
main_conn_lck.add_refresh_event(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash: 0,
|
||||
kind: RefreshEventKind::Failure(err.clone()),
|
||||
});
|
||||
/*
|
||||
uid_store.uid_index.lock().unwrap().clear();
|
||||
uid_store.hash_index.lock().unwrap().clear();
|
||||
uid_store.byte_cache.lock().unwrap().clear();
|
||||
*/
|
||||
return Err(err);
|
||||
}
|
||||
} else {
|
||||
uidvalidities.insert(mailbox_hash, select_response.uidvalidity);
|
||||
}
|
||||
debug!("watch future returning");
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
let mailboxes: HashMap<MailboxHash, ImapMailbox> = {
|
||||
let mailboxes_lck = timeout(uid_store.timeout, uid_store.mailboxes.lock()).await?;
|
||||
mailboxes_lck.clone()
|
||||
};
|
||||
for (h, mailbox) in mailboxes.clone() {
|
||||
if mailbox_hash == h {
|
||||
continue;
|
||||
}
|
||||
examine_updates(mailbox, &mut conn, &uid_store).await?;
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
conn.send_command(b"IDLE").await?;
|
||||
let mut blockn = ImapBlockingConnection::from(conn);
|
||||
let mut watch = std::time::Instant::now();
|
||||
/* duration interval to send heartbeat */
|
||||
const _10_MINS: std::time::Duration = std::time::Duration::from_secs(10 * 60);
|
||||
/* duration interval to check other mailboxes for changes */
|
||||
const _5_MINS: std::time::Duration = std::time::Duration::from_secs(5 * 60);
|
||||
loop {
|
||||
let line = match timeout(Some(_10_MINS), blockn.as_stream()).await {
|
||||
Ok(Some(line)) => line,
|
||||
Ok(None) => {
|
||||
debug!("IDLE connection dropped: {:?}", &blockn.err());
|
||||
blockn.conn.connect().await?;
|
||||
let mut main_conn_lck = timeout(uid_store.timeout, main_conn.lock()).await?;
|
||||
main_conn_lck.connect().await?;
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
/* Timeout */
|
||||
blockn.conn.send_raw(b"DONE").await?;
|
||||
blockn
|
||||
.conn
|
||||
.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
blockn.conn.send_command(b"IDLE").await?;
|
||||
let mut main_conn_lck = timeout(uid_store.timeout, main_conn.lock()).await?;
|
||||
main_conn_lck.connect().await?;
|
||||
continue;
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ImapWatcher {
|
||||
pub async fn idle(&mut self) -> Result<()> {
|
||||
debug!("IDLE");
|
||||
/* IDLE only watches the connection's selected mailbox. We will IDLE on INBOX and every X
|
||||
* minutes wake up and poll the others */
|
||||
let ImapWatcher {
|
||||
ref main_conn,
|
||||
ref uid_store,
|
||||
ref mailbox_hashes,
|
||||
ref polling_period,
|
||||
ref server_conf,
|
||||
..
|
||||
} = self;
|
||||
let mut connection = ImapConnection::new_connection(server_conf, uid_store.clone());
|
||||
connection.connect().await?;
|
||||
let mailbox_hash: MailboxHash = match uid_store
|
||||
.mailboxes
|
||||
.lock()
|
||||
.await
|
||||
.values()
|
||||
.find(|f| f.parent.is_none() && (f.special_usage() == SpecialUsageMailbox::Inbox))
|
||||
.map(|f| f.hash)
|
||||
{
|
||||
Some(h) => h,
|
||||
None => {
|
||||
return Err(MeliError::new("INBOX mailbox not found in local mailbox index. meli may have not parsed the IMAP mailboxes correctly"));
|
||||
}
|
||||
};
|
||||
let now = std::time::Instant::now();
|
||||
if now.duration_since(watch) >= _5_MINS {
|
||||
/* Time to poll all inboxes */
|
||||
let mut conn = timeout(uid_store.timeout, main_conn.lock()).await?;
|
||||
for (_h, mailbox) in mailboxes.clone() {
|
||||
examine_updates(mailbox, &mut conn, &uid_store).await?;
|
||||
}
|
||||
watch = now;
|
||||
}
|
||||
if line
|
||||
.split_rn()
|
||||
.filter(|l| {
|
||||
!l.starts_with(b"+ ")
|
||||
&& !l.starts_with(b"* ok")
|
||||
&& !l.starts_with(b"* ok")
|
||||
&& !l.starts_with(b"* Ok")
|
||||
&& !l.starts_with(b"* OK")
|
||||
})
|
||||
.count()
|
||||
== 0
|
||||
{
|
||||
continue;
|
||||
}
|
||||
{
|
||||
blockn.conn.send_raw(b"DONE").await?;
|
||||
blockn
|
||||
.conn
|
||||
.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
for l in line.split_rn().chain(response.split_rn()) {
|
||||
debug!("process_untagged {:?}", &l);
|
||||
if l.starts_with(b"+ ")
|
||||
|| l.starts_with(b"* ok")
|
||||
|| l.starts_with(b"* ok")
|
||||
|| l.starts_with(b"* Ok")
|
||||
|| l.starts_with(b"* OK")
|
||||
{
|
||||
debug!("ignore continuation mark");
|
||||
continue;
|
||||
}
|
||||
blockn.conn.process_untagged(l).await?;
|
||||
}
|
||||
blockn.conn.send_command(b"IDLE").await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn examine_updates(
|
||||
mailbox: ImapMailbox,
|
||||
conn: &mut ImapConnection,
|
||||
uid_store: &Arc<UIDStore>,
|
||||
) -> Result<()> {
|
||||
if mailbox.no_select {
|
||||
return Ok(());
|
||||
}
|
||||
let mailbox_hash = mailbox.hash();
|
||||
debug!("examining mailbox {} {}", mailbox_hash, mailbox.path());
|
||||
if let Some(new_envelopes) = conn.resync(mailbox_hash).await? {
|
||||
for env in new_envelopes {
|
||||
conn.add_refresh_event(RefreshEvent {
|
||||
mailbox_hash,
|
||||
account_hash: uid_store.account_hash,
|
||||
kind: RefreshEventKind::Create(Box::new(env)),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
#[cfg(not(feature = "sqlite3"))]
|
||||
let mut cache_handle = super::cache::DefaultCache::get(uid_store.clone())?;
|
||||
#[cfg(feature = "sqlite3")]
|
||||
let mut cache_handle = super::cache::Sqlite3Cache::get(uid_store.clone())?;
|
||||
let mut response = Vec::with_capacity(8 * 1024);
|
||||
let select_response = conn
|
||||
let select_response = connection
|
||||
.examine_mailbox(mailbox_hash, &mut response, true)
|
||||
.await?
|
||||
.unwrap();
|
||||
|
@ -227,9 +155,13 @@ pub async fn examine_updates(
|
|||
if let Some(v) = uidvalidities.get(&mailbox_hash) {
|
||||
if *v != select_response.uidvalidity {
|
||||
if uid_store.keep_offline_cache {
|
||||
#[cfg(not(feature = "sqlite3"))]
|
||||
let mut cache_handle = super::cache::DefaultCache::get(uid_store.clone())?;
|
||||
#[cfg(feature = "sqlite3")]
|
||||
let mut cache_handle = super::cache::Sqlite3Cache::get(uid_store.clone())?;
|
||||
cache_handle.clear(mailbox_hash, &select_response)?;
|
||||
}
|
||||
conn.add_refresh_event(RefreshEvent {
|
||||
connection.add_refresh_event(RefreshEvent {
|
||||
account_hash: uid_store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Rescan,
|
||||
|
@ -239,211 +171,384 @@ pub async fn examine_updates(
|
|||
uid_store.hash_index.lock().unwrap().clear();
|
||||
uid_store.byte_cache.lock().unwrap().clear();
|
||||
*/
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
uidvalidities.insert(mailbox_hash, select_response.uidvalidity);
|
||||
}
|
||||
}
|
||||
if mailbox.is_cold() {
|
||||
/* Mailbox hasn't been loaded yet */
|
||||
let has_list_status: bool = conn
|
||||
.uid_store
|
||||
.capabilities
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"LIST-STATUS"));
|
||||
if has_list_status {
|
||||
conn.send_command(
|
||||
format!(
|
||||
"LIST \"{}\" \"\" RETURN (STATUS (MESSAGES UNSEEN))",
|
||||
mailbox.imap_path()
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
conn.read_response(
|
||||
&mut response,
|
||||
RequiredResponses::LIST_REQUIRED | RequiredResponses::STATUS,
|
||||
)
|
||||
.await?;
|
||||
debug!(
|
||||
"list return status out: {}",
|
||||
String::from_utf8_lossy(&response)
|
||||
);
|
||||
for l in response.split_rn() {
|
||||
if !l.starts_with(b"*") {
|
||||
continue;
|
||||
}
|
||||
if let Ok(status) = protocol_parser::status_response(l).map(|(_, v)| v) {
|
||||
if Some(mailbox_hash) == status.mailbox {
|
||||
if let Some(total) = status.messages {
|
||||
if let Ok(mut exists_lck) = mailbox.exists.lock() {
|
||||
exists_lck.clear();
|
||||
exists_lck.set_not_yet_seen(total);
|
||||
}
|
||||
}
|
||||
if let Some(total) = status.unseen {
|
||||
if let Ok(mut unseen_lck) = mailbox.unseen.lock() {
|
||||
unseen_lck.clear();
|
||||
unseen_lck.set_not_yet_seen(total);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
conn.send_command(b"SEARCH UNSEEN").await?;
|
||||
conn.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await?;
|
||||
let unseen_count = protocol_parser::search_results(&response)?.1.len();
|
||||
if let Ok(mut exists_lck) = mailbox.exists.lock() {
|
||||
exists_lck.clear();
|
||||
exists_lck.set_not_yet_seen(select_response.exists);
|
||||
}
|
||||
if let Ok(mut unseen_lck) = mailbox.unseen.lock() {
|
||||
unseen_lck.clear();
|
||||
unseen_lck.set_not_yet_seen(unseen_count);
|
||||
}
|
||||
}
|
||||
mailbox.set_warm(true);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if select_response.recent > 0 {
|
||||
/* UID SEARCH RECENT */
|
||||
conn.send_command(b"UID SEARCH RECENT").await?;
|
||||
conn.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await?;
|
||||
let v = protocol_parser::search_results(response.as_slice()).map(|(_, v)| v)?;
|
||||
if v.is_empty() {
|
||||
debug!(
|
||||
"search response was empty: {}",
|
||||
String::from_utf8_lossy(&response)
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let mut cmd = "UID FETCH ".to_string();
|
||||
cmd.push_str(&v[0].to_string());
|
||||
if v.len() != 1 {
|
||||
for n in v.into_iter().skip(1) {
|
||||
cmd.push(',');
|
||||
cmd.push_str(&n.to_string());
|
||||
}
|
||||
}
|
||||
cmd.push_str(
|
||||
" (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)",
|
||||
);
|
||||
conn.send_command(cmd.as_bytes()).await?;
|
||||
conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await?;
|
||||
} else if select_response.exists > mailbox.exists.lock().unwrap().len() {
|
||||
conn.send_command(
|
||||
format!(
|
||||
"FETCH {}:* (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)",
|
||||
std::cmp::max(mailbox.exists.lock().unwrap().len(), 1)
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await?;
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
debug!(
|
||||
"fetch response is {} bytes and {} lines",
|
||||
response.len(),
|
||||
String::from_utf8_lossy(&response).lines().count()
|
||||
);
|
||||
let (_, mut v, _) = protocol_parser::fetch_responses(&response)?;
|
||||
debug!("responses len is {}", v.len());
|
||||
for FetchResponse {
|
||||
ref uid,
|
||||
ref mut envelope,
|
||||
ref mut flags,
|
||||
ref references,
|
||||
..
|
||||
} in v.iter_mut()
|
||||
{
|
||||
let uid = uid.unwrap();
|
||||
let env = envelope.as_mut().unwrap();
|
||||
env.set_hash(generate_envelope_hash(mailbox.imap_path(), &uid));
|
||||
if let Some(value) = references {
|
||||
env.set_references(value);
|
||||
}
|
||||
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() {
|
||||
mailbox.unseen.lock().unwrap().insert_new(env.hash());
|
||||
}
|
||||
mailbox.exists.lock().unwrap().insert_new(env.hash());
|
||||
for f in keywords {
|
||||
let hash = tag_hash!(f);
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f.to_string());
|
||||
}
|
||||
env.labels_mut().push(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
if uid_store.keep_offline_cache && cache_handle.mailbox_state(mailbox_hash)?.is_some() {
|
||||
cache_handle
|
||||
.insert_envelopes(mailbox_hash, &v)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not save envelopes in cache for mailbox {}",
|
||||
mailbox.imap_path()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
for FetchResponse { uid, envelope, .. } in v {
|
||||
if uid.is_none() || envelope.is_none() {
|
||||
let mailboxes: HashMap<MailboxHash, ImapMailbox> = {
|
||||
let mailboxes_lck = timeout(uid_store.timeout, uid_store.mailboxes.lock()).await?;
|
||||
let mut ret = mailboxes_lck.clone();
|
||||
ret.retain(|k, _| mailbox_hashes.contains(k));
|
||||
ret
|
||||
};
|
||||
for (h, mailbox) in mailboxes.iter() {
|
||||
if mailbox_hash == *h {
|
||||
continue;
|
||||
}
|
||||
let uid = uid.unwrap();
|
||||
if uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains_key(&(mailbox_hash, uid))
|
||||
Self::examine_updates(mailbox, &mut connection, &uid_store).await?;
|
||||
}
|
||||
connection.send_command(b"IDLE").await?;
|
||||
let mut blockn = ImapBlockingConnection::from(connection);
|
||||
let mut watch = std::time::Instant::now();
|
||||
/* duration interval to send heartbeat */
|
||||
const _10_MINS: std::time::Duration = std::time::Duration::from_secs(10 * 60);
|
||||
/* duration interval to check other mailboxes for changes */
|
||||
loop {
|
||||
let line = match timeout(
|
||||
Some(std::cmp::min(*polling_period, _10_MINS)),
|
||||
blockn.as_stream(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(line)) => line,
|
||||
Ok(None) => {
|
||||
debug!("IDLE connection dropped: {:?}", &blockn.err());
|
||||
blockn.conn.connect().await?;
|
||||
let mut main_conn_lck = timeout(uid_store.timeout, main_conn.lock()).await?;
|
||||
main_conn_lck.connect().await?;
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
/* Timeout */
|
||||
blockn.conn.send_raw(b"DONE").await?;
|
||||
blockn
|
||||
.conn
|
||||
.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
blockn.conn.send_command(b"IDLE").await?;
|
||||
let mut main_conn_lck = timeout(uid_store.timeout, main_conn.lock()).await?;
|
||||
main_conn_lck.connect().await?;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let now = std::time::Instant::now();
|
||||
if now.duration_since(watch) >= *polling_period {
|
||||
/* Time to poll all inboxes */
|
||||
let mut conn = timeout(uid_store.timeout, main_conn.lock()).await?;
|
||||
for (_h, mailbox) in mailboxes.iter() {
|
||||
Self::examine_updates(mailbox, &mut conn, &uid_store).await?;
|
||||
}
|
||||
watch = now;
|
||||
}
|
||||
if line
|
||||
.split_rn()
|
||||
.filter(|l| {
|
||||
!l.starts_with(b"+ ")
|
||||
&& !l.starts_with(b"* ok")
|
||||
&& !l.starts_with(b"* ok")
|
||||
&& !l.starts_with(b"* Ok")
|
||||
&& !l.starts_with(b"* OK")
|
||||
})
|
||||
.count()
|
||||
== 0
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let env = envelope.unwrap();
|
||||
debug!(
|
||||
"Create event {} {} {}",
|
||||
env.hash(),
|
||||
env.subject(),
|
||||
mailbox.path(),
|
||||
);
|
||||
uid_store
|
||||
.msn_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.or_default()
|
||||
.push(uid);
|
||||
uid_store
|
||||
.hash_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(env.hash(), (uid, mailbox_hash));
|
||||
uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert((mailbox_hash, uid), env.hash());
|
||||
conn.add_refresh_event(RefreshEvent {
|
||||
account_hash: uid_store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: Create(Box::new(env)),
|
||||
});
|
||||
{
|
||||
blockn.conn.send_raw(b"DONE").await?;
|
||||
blockn
|
||||
.conn
|
||||
.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
for l in line.split_rn().chain(response.split_rn()) {
|
||||
debug!("process_untagged {:?}", String::from_utf8_lossy(&l));
|
||||
if l.starts_with(b"+ ")
|
||||
|| l.starts_with(b"* ok")
|
||||
|| l.starts_with(b"* ok")
|
||||
|| l.starts_with(b"* Ok")
|
||||
|| l.starts_with(b"* OK")
|
||||
{
|
||||
debug!("ignore continuation mark");
|
||||
continue;
|
||||
}
|
||||
blockn.conn.process_untagged(l).await?;
|
||||
}
|
||||
blockn.conn.send_command(b"IDLE").await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
pub async fn poll_with_examine(&mut self) -> Result<()> {
|
||||
debug!("poll with examine");
|
||||
let ImapWatcher {
|
||||
ref mailbox_hashes,
|
||||
ref uid_store,
|
||||
ref polling_period,
|
||||
ref server_conf,
|
||||
..
|
||||
} = self;
|
||||
let mut connection = ImapConnection::new_connection(server_conf, uid_store.clone());
|
||||
connection.connect().await?;
|
||||
let mailboxes: HashMap<MailboxHash, ImapMailbox> = {
|
||||
let mailboxes_lck = timeout(uid_store.timeout, uid_store.mailboxes.lock()).await?;
|
||||
let mut ret = mailboxes_lck.clone();
|
||||
ret.retain(|k, _| mailbox_hashes.contains(k));
|
||||
ret
|
||||
};
|
||||
loop {
|
||||
for (_, mailbox) in mailboxes.iter() {
|
||||
Self::examine_updates(mailbox, &mut connection, &uid_store).await?;
|
||||
}
|
||||
crate::connections::sleep(*polling_period).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn examine_updates(
|
||||
mailbox: &ImapMailbox,
|
||||
conn: &mut ImapConnection,
|
||||
uid_store: &Arc<UIDStore>,
|
||||
) -> Result<()> {
|
||||
if mailbox.no_select {
|
||||
return Ok(());
|
||||
}
|
||||
let mailbox_hash = mailbox.hash();
|
||||
debug!("examining mailbox {} {}", mailbox_hash, mailbox.path());
|
||||
if let Some(new_envelopes) = conn.resync(mailbox_hash).await? {
|
||||
for env in new_envelopes {
|
||||
conn.add_refresh_event(RefreshEvent {
|
||||
mailbox_hash,
|
||||
account_hash: uid_store.account_hash,
|
||||
kind: RefreshEventKind::Create(Box::new(env)),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
#[cfg(not(feature = "sqlite3"))]
|
||||
let mut cache_handle = super::cache::DefaultCache::get(uid_store.clone())?;
|
||||
#[cfg(feature = "sqlite3")]
|
||||
let mut cache_handle = super::cache::Sqlite3Cache::get(uid_store.clone())?;
|
||||
let mut response = Vec::with_capacity(8 * 1024);
|
||||
let select_response = conn
|
||||
.examine_mailbox(mailbox_hash, &mut response, true)
|
||||
.await?
|
||||
.unwrap();
|
||||
{
|
||||
let mut uidvalidities = uid_store.uidvalidity.lock().unwrap();
|
||||
|
||||
if let Some(v) = uidvalidities.get(&mailbox_hash) {
|
||||
if *v != select_response.uidvalidity {
|
||||
if uid_store.keep_offline_cache {
|
||||
cache_handle.clear(mailbox_hash, &select_response)?;
|
||||
}
|
||||
conn.add_refresh_event(RefreshEvent {
|
||||
account_hash: uid_store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Rescan,
|
||||
});
|
||||
/*
|
||||
uid_store.uid_index.lock().unwrap().clear();
|
||||
uid_store.hash_index.lock().unwrap().clear();
|
||||
uid_store.byte_cache.lock().unwrap().clear();
|
||||
*/
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
uidvalidities.insert(mailbox_hash, select_response.uidvalidity);
|
||||
}
|
||||
}
|
||||
if mailbox.is_cold() {
|
||||
/* Mailbox hasn't been loaded yet */
|
||||
let has_list_status: bool = conn
|
||||
.uid_store
|
||||
.capabilities
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"LIST-STATUS"));
|
||||
if has_list_status {
|
||||
conn.send_command(
|
||||
format!(
|
||||
"LIST \"{}\" \"\" RETURN (STATUS (MESSAGES UNSEEN))",
|
||||
mailbox.imap_path()
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
conn.read_response(
|
||||
&mut response,
|
||||
RequiredResponses::LIST_REQUIRED | RequiredResponses::STATUS,
|
||||
)
|
||||
.await?;
|
||||
debug!(
|
||||
"list return status out: {}",
|
||||
String::from_utf8_lossy(&response)
|
||||
);
|
||||
for l in response.split_rn() {
|
||||
if !l.starts_with(b"*") {
|
||||
continue;
|
||||
}
|
||||
if let Ok(status) = protocol_parser::status_response(&l).map(|(_, v)| v) {
|
||||
if Some(mailbox_hash) == status.mailbox {
|
||||
if let Some(total) = status.messages {
|
||||
if let Ok(mut exists_lck) = mailbox.exists.lock() {
|
||||
exists_lck.clear();
|
||||
exists_lck.set_not_yet_seen(total);
|
||||
}
|
||||
}
|
||||
if let Some(total) = status.unseen {
|
||||
if let Ok(mut unseen_lck) = mailbox.unseen.lock() {
|
||||
unseen_lck.clear();
|
||||
unseen_lck.set_not_yet_seen(total);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
conn.send_command(b"SEARCH UNSEEN").await?;
|
||||
conn.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await?;
|
||||
let unseen_count = protocol_parser::search_results(&response)?.1.len();
|
||||
if let Ok(mut exists_lck) = mailbox.exists.lock() {
|
||||
exists_lck.clear();
|
||||
exists_lck.set_not_yet_seen(select_response.exists);
|
||||
}
|
||||
if let Ok(mut unseen_lck) = mailbox.unseen.lock() {
|
||||
unseen_lck.clear();
|
||||
unseen_lck.set_not_yet_seen(unseen_count);
|
||||
}
|
||||
}
|
||||
mailbox.set_warm(true);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if select_response.recent > 0 {
|
||||
/* UID SEARCH RECENT */
|
||||
conn.send_command(b"UID SEARCH RECENT").await?;
|
||||
conn.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await?;
|
||||
let v = protocol_parser::search_results(response.as_slice()).map(|(_, v)| v)?;
|
||||
if v.is_empty() {
|
||||
debug!(
|
||||
"search response was empty: {}",
|
||||
String::from_utf8_lossy(&response)
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let mut cmd = "UID FETCH ".to_string();
|
||||
if v.len() == 1 {
|
||||
cmd.push_str(&v[0].to_string());
|
||||
} else {
|
||||
cmd.push_str(&v[0].to_string());
|
||||
for n in v.into_iter().skip(1) {
|
||||
cmd.push(',');
|
||||
cmd.push_str(&n.to_string());
|
||||
}
|
||||
}
|
||||
cmd.push_str(
|
||||
" (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)",
|
||||
);
|
||||
conn.send_command(cmd.as_bytes()).await?;
|
||||
conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await?;
|
||||
} else if select_response.exists > mailbox.exists.lock().unwrap().len() {
|
||||
conn.send_command(
|
||||
format!(
|
||||
"FETCH {}:* (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)",
|
||||
std::cmp::max(mailbox.exists.lock().unwrap().len(), 1)
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await?;
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
debug!(
|
||||
"fetch response is {} bytes and {} lines",
|
||||
response.len(),
|
||||
String::from_utf8_lossy(&response).lines().count()
|
||||
);
|
||||
let (_, mut v, _) = protocol_parser::fetch_responses(&response)?;
|
||||
debug!("responses len is {}", v.len());
|
||||
for FetchResponse {
|
||||
ref uid,
|
||||
ref mut envelope,
|
||||
ref mut flags,
|
||||
ref references,
|
||||
..
|
||||
} in v.iter_mut()
|
||||
{
|
||||
let uid = uid.unwrap();
|
||||
let env = envelope.as_mut().unwrap();
|
||||
env.set_hash(generate_envelope_hash(&mailbox.imap_path(), &uid));
|
||||
if let Some(value) = references {
|
||||
env.set_references(value);
|
||||
}
|
||||
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() {
|
||||
mailbox.unseen.lock().unwrap().insert_new(env.hash());
|
||||
}
|
||||
mailbox.exists.lock().unwrap().insert_new(env.hash());
|
||||
for f in keywords {
|
||||
let hash = tag_hash!(f);
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f.to_string());
|
||||
}
|
||||
env.labels_mut().push(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
if uid_store.keep_offline_cache {
|
||||
if !cache_handle.mailbox_state(mailbox_hash)?.is_none() {
|
||||
cache_handle
|
||||
.insert_envelopes(mailbox_hash, &v)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not save envelopes in cache for mailbox {}",
|
||||
mailbox.imap_path()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
for FetchResponse { uid, envelope, .. } in v {
|
||||
if uid.is_none() || envelope.is_none() {
|
||||
continue;
|
||||
}
|
||||
let uid = uid.unwrap();
|
||||
if uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains_key(&(mailbox_hash, uid))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let env = envelope.unwrap();
|
||||
debug!(
|
||||
"Create event {} {} {}",
|
||||
env.hash(),
|
||||
env.subject(),
|
||||
mailbox.path(),
|
||||
);
|
||||
uid_store
|
||||
.msn_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.or_default()
|
||||
.push(uid);
|
||||
uid_store
|
||||
.hash_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(env.hash(), (uid, mailbox_hash));
|
||||
uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert((mailbox_hash, uid), env.hash());
|
||||
conn.add_refresh_event(RefreshEvent {
|
||||
account_hash: uid_store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: Create(Box::new(env)),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -90,6 +90,8 @@ use objects::*;
|
|||
pub mod mailbox;
|
||||
use mailbox::*;
|
||||
|
||||
pub mod watch;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct EnvelopeCache {
|
||||
bytes: Option<String>,
|
||||
|
@ -313,6 +315,147 @@ impl MailBackend for JmapType {
|
|||
}))
|
||||
}
|
||||
|
||||
fn load(&mut self, mailbox_hash: MailboxHash) -> ResultFuture<()> {
|
||||
let store = self.store.clone();
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
{
|
||||
crate::connections::sleep(std::time::Duration::from_secs(2)).await;
|
||||
}
|
||||
let mailbox_id = store.mailboxes.read().unwrap()[&mailbox_hash].id.clone();
|
||||
let email_query_call: EmailQuery = EmailQuery::new(
|
||||
Query::new()
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.filter(Some(Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.in_mailbox(Some(mailbox_id))
|
||||
.into(),
|
||||
)))
|
||||
.position(0)
|
||||
.properties(Some(vec![
|
||||
"id".to_string(),
|
||||
"receivedAt".to_string(),
|
||||
"messageId".to_string(),
|
||||
"inReplyTo".to_string(),
|
||||
"hasAttachment".to_string(),
|
||||
"keywords".to_string(),
|
||||
])),
|
||||
)
|
||||
.collapse_threads(false);
|
||||
|
||||
let mut req = Request::new(conn.request_no.clone());
|
||||
let prev_seq = req.add_call(&email_query_call);
|
||||
|
||||
let email_call: EmailGet = EmailGet::new(
|
||||
Get::new()
|
||||
.ids(Some(JmapArgument::reference(
|
||||
prev_seq,
|
||||
EmailQuery::RESULT_FIELD_IDS,
|
||||
)))
|
||||
.account_id(conn.mail_account_id().clone()),
|
||||
);
|
||||
|
||||
req.add_call(&email_call);
|
||||
|
||||
let api_url = conn.session.lock().unwrap().api_url.clone();
|
||||
let mut res = conn
|
||||
.client
|
||||
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
|
||||
.await?;
|
||||
|
||||
let res_text = res.text_async().await?;
|
||||
|
||||
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
|
||||
let e = GetResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
|
||||
let query_response =
|
||||
QueryResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
|
||||
store
|
||||
.mailboxes
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.and_modify(|mbox| {
|
||||
*mbox.email_query_state.lock().unwrap() = Some(query_response.query_state);
|
||||
});
|
||||
let GetResponse::<EmailObject> { list, state, .. } = e;
|
||||
{
|
||||
let (is_empty, is_equal) = {
|
||||
let mailboxes_lck = conn.store.mailboxes.read().unwrap();
|
||||
mailboxes_lck
|
||||
.get(&mailbox_hash)
|
||||
.map(|mbox| {
|
||||
let current_state_lck = mbox.email_state.lock().unwrap();
|
||||
(
|
||||
current_state_lck.is_none(),
|
||||
current_state_lck.as_ref() != Some(&state),
|
||||
)
|
||||
})
|
||||
.unwrap_or((true, true))
|
||||
};
|
||||
if is_empty {
|
||||
let mut mailboxes_lck = conn.store.mailboxes.write().unwrap();
|
||||
debug!("{:?}: inserting state {}", EmailObject::NAME, &state);
|
||||
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
|
||||
*mbox.email_state.lock().unwrap() = Some(state);
|
||||
});
|
||||
} else if !is_equal {
|
||||
conn.email_changes(mailbox_hash).await?;
|
||||
}
|
||||
}
|
||||
let mut total = BTreeSet::default();
|
||||
let mut unread = BTreeSet::default();
|
||||
let new_envelopes: HashMap<EnvelopeHash, Envelope> = list
|
||||
.into_iter(|obj| {
|
||||
let env = store.add_envelope(obj);
|
||||
total.insert(env.hash());
|
||||
if !env.is_seen() {
|
||||
unread.insert(env.hash());
|
||||
}
|
||||
(env.hash(), env)
|
||||
})
|
||||
.collect();
|
||||
let mut mailboxes_lck = store.mailboxes.write().unwrap();
|
||||
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
|
||||
mbox.total_emails.lock().unwrap().insert_existing_set(total);
|
||||
mbox.unread_emails
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert_existing_set(unread);
|
||||
});
|
||||
let keys: BTreeSet<EnvelopeHash> = new_envelopes.keys().cloned().collect();
|
||||
collection.merge(new_envelopes, mailbox_hash, None);
|
||||
let mut envelopes_lck = collection.envelopes.write().unwrap();
|
||||
envelopes_lck.retain(|k, _| !keys.contains(k));
|
||||
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn fetch_batch(&mut self, env_hashes: EnvelopeHashBatch) -> ResultFuture<()> {
|
||||
todo!()
|
||||
/*
|
||||
let store = self.store.clone();
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
//crate::connections::sleep(std::time::Duration::from_secs(2)).await;
|
||||
debug!("fetch_batch {:?}", &env_hashes);
|
||||
let mut envelopes_lck = collection.envelopes.write().unwrap();
|
||||
for env_hash in env_hashes.iter() {
|
||||
if envelopes_lck.contains_key(&env_hash) {
|
||||
continue;
|
||||
}
|
||||
let index_lck = index.write().unwrap();
|
||||
let message = Message::find_message(&database, &index_lck[&env_hash])?;
|
||||
drop(index_lck);
|
||||
let env = message.into_envelope(&index, &collection.tag_index);
|
||||
envelopes_lck.insert(env_hash, env);
|
||||
}
|
||||
debug!("fetch_batch {:?} done", &env_hashes);
|
||||
Ok(())
|
||||
}))
|
||||
*/
|
||||
}
|
||||
|
||||
fn fetch(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
|
@ -341,32 +484,12 @@ impl MailBackend for JmapType {
|
|||
}))
|
||||
}
|
||||
|
||||
fn watch(&self) -> ResultFuture<()> {
|
||||
fn watcher(&self) -> Result<Box<dyn BackendWatcher>> {
|
||||
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;
|
||||
}
|
||||
Ok(Box::new(watch::JmapWatcher {
|
||||
connection,
|
||||
mailbox_hashes: BTreeSet::default(),
|
||||
polling_period: std::time::Duration::from_secs(60),
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* melib - JMAP
|
||||
*
|
||||
* Copyright 2020 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::*;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JmapWatcher {
|
||||
pub mailbox_hashes: BTreeSet<MailboxHash>,
|
||||
pub polling_period: std::time::Duration,
|
||||
pub connection: Arc<FutureMutex<JmapConnection>>,
|
||||
}
|
||||
|
||||
impl BackendWatcher for JmapWatcher {
|
||||
fn is_blocking(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn register_mailbox(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
_urgency: MailboxWatchUrgency,
|
||||
) -> Result<()> {
|
||||
self.mailbox_hashes.insert(mailbox_hash);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_polling_period(&mut self, period: Option<std::time::Duration>) -> Result<()> {
|
||||
if let Some(period) = period {
|
||||
self.polling_period = period;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn(self: Box<Self>) -> ResultFuture<()> {
|
||||
let JmapWatcher {
|
||||
mailbox_hashes,
|
||||
polling_period,
|
||||
connection,
|
||||
} = *self;
|
||||
Ok(Box::pin(async move {
|
||||
{
|
||||
let mut conn = connection.lock().await;
|
||||
conn.connect().await?;
|
||||
}
|
||||
loop {
|
||||
{
|
||||
let conn = connection.lock().await;
|
||||
for &mailbox_hash in &mailbox_hashes {
|
||||
conn.email_changes(mailbox_hash).await?;
|
||||
}
|
||||
}
|
||||
crate::connections::sleep(polling_period).await;
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
|
@ -26,6 +26,8 @@ pub use self::backend::*;
|
|||
mod stream;
|
||||
pub use stream::*;
|
||||
|
||||
pub mod watch;
|
||||
|
||||
use crate::backends::*;
|
||||
use crate::email::Flag;
|
||||
use crate::error::{MeliError, Result};
|
||||
|
|
|
@ -27,20 +27,13 @@ use crate::error::{ErrorKind, MeliError, Result};
|
|||
use crate::shellexpand::ShellExpandTrait;
|
||||
use crate::Collection;
|
||||
use futures::prelude::Stream;
|
||||
|
||||
extern crate notify;
|
||||
use self::notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
|
||||
use std::time::Duration;
|
||||
|
||||
use std::collections::{hash_map::DefaultHasher, HashMap, HashSet};
|
||||
use std::ffi::OsStr;
|
||||
use std::fs;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::{self, Read, Write};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::sync::mpsc::channel;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
@ -339,494 +332,28 @@ impl MailBackend for MaildirType {
|
|||
}))
|
||||
}
|
||||
|
||||
fn watch(&self) -> ResultFuture<()> {
|
||||
let sender = self.event_consumer.clone();
|
||||
let (tx, rx) = channel();
|
||||
let mut watcher = watcher(tx, Duration::from_secs(2)).unwrap();
|
||||
fn watcher(&self) -> Result<Box<dyn BackendWatcher>> {
|
||||
let account_hash = {
|
||||
let mut hasher = DefaultHasher::default();
|
||||
hasher.write(self.name.as_bytes());
|
||||
hasher.finish()
|
||||
};
|
||||
let root_path = self.path.to_path_buf();
|
||||
watcher.watch(&root_path, RecursiveMode::Recursive).unwrap();
|
||||
let cache_dir = xdg::BaseDirectories::with_profile("meli", &self.name).unwrap();
|
||||
debug!("watching {:?}", root_path);
|
||||
let event_consumer = self.event_consumer.clone();
|
||||
let hash_indexes = self.hash_indexes.clone();
|
||||
let mailbox_index = self.mailbox_index.clone();
|
||||
let root_mailbox_hash: MailboxHash = self
|
||||
.mailboxes
|
||||
.values()
|
||||
.find(|m| m.parent.is_none())
|
||||
.map(|m| m.hash())
|
||||
.unwrap();
|
||||
let mailbox_counts = self
|
||||
.mailboxes
|
||||
.iter()
|
||||
.map(|(&k, v)| (k, (v.unseen.clone(), v.total.clone())))
|
||||
.collect::<HashMap<MailboxHash, (Arc<Mutex<usize>>, Arc<Mutex<usize>>)>>();
|
||||
Ok(Box::pin(async move {
|
||||
// Move `watcher` in the closure's scope so that it doesn't get dropped.
|
||||
let _watcher = watcher;
|
||||
let mut buf = Vec::with_capacity(4096);
|
||||
loop {
|
||||
match rx.recv() {
|
||||
/*
|
||||
* Event types:
|
||||
*
|
||||
* pub enum RefreshEventKind {
|
||||
* Update(EnvelopeHash, Envelope), // Old hash, new envelope
|
||||
* Create(Envelope),
|
||||
* Remove(EnvelopeHash),
|
||||
* Rescan,
|
||||
* }
|
||||
*/
|
||||
Ok(event) => match event {
|
||||
/* Create */
|
||||
DebouncedEvent::Create(mut pathbuf) => {
|
||||
debug!("DebouncedEvent::Create(path = {:?}", pathbuf);
|
||||
if path_is_new!(pathbuf) {
|
||||
debug!("path_is_new");
|
||||
/* This creates a Rename event that we will receive later */
|
||||
pathbuf = match move_to_cur(pathbuf) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
debug!("error: {}", e.to_string());
|
||||
continue;
|
||||
}
|
||||
};
|
||||
}
|
||||
let mailbox_hash = get_path_hash!(pathbuf);
|
||||
let file_name = pathbuf
|
||||
.as_path()
|
||||
.strip_prefix(&root_path)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
if let Ok(env) = add_path_to_index(
|
||||
&hash_indexes,
|
||||
mailbox_hash,
|
||||
pathbuf.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
mailbox_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(env.hash(), mailbox_hash);
|
||||
debug!(
|
||||
"Create event {} {} {}",
|
||||
env.hash(),
|
||||
env.subject(),
|
||||
pathbuf.display()
|
||||
);
|
||||
if !env.is_seen() {
|
||||
*mailbox_counts[&mailbox_hash].0.lock().unwrap() += 1;
|
||||
}
|
||||
*mailbox_counts[&mailbox_hash].1.lock().unwrap() += 1;
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: Create(Box::new(env)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
/* Update */
|
||||
DebouncedEvent::NoticeWrite(pathbuf) | DebouncedEvent::Write(pathbuf) => {
|
||||
debug!("DebouncedEvent::Write(path = {:?}", &pathbuf);
|
||||
let mailbox_hash = get_path_hash!(pathbuf);
|
||||
let mut hash_indexes_lock = hash_indexes.lock().unwrap();
|
||||
let index_lock =
|
||||
&mut hash_indexes_lock.entry(mailbox_hash).or_default();
|
||||
let file_name = pathbuf
|
||||
.as_path()
|
||||
.strip_prefix(&root_path)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
/* Linear search in hash_index to find old hash */
|
||||
let old_hash: EnvelopeHash = {
|
||||
if let Some((k, v)) =
|
||||
index_lock.iter_mut().find(|(_, v)| *v.buf == pathbuf)
|
||||
{
|
||||
*v = pathbuf.clone().into();
|
||||
*k
|
||||
} else {
|
||||
drop(hash_indexes_lock);
|
||||
/* Did we just miss a Create event? In any case, create
|
||||
* envelope. */
|
||||
if let Ok(env) = add_path_to_index(
|
||||
&hash_indexes,
|
||||
mailbox_hash,
|
||||
pathbuf.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
mailbox_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(env.hash(), mailbox_hash);
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: Create(Box::new(env)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let new_hash: EnvelopeHash = get_file_hash(pathbuf.as_path());
|
||||
let mut reader = io::BufReader::new(fs::File::open(&pathbuf)?);
|
||||
buf.clear();
|
||||
reader.read_to_end(&mut buf)?;
|
||||
if index_lock.get_mut(&new_hash).is_none() {
|
||||
debug!("write notice");
|
||||
if let Ok(mut env) =
|
||||
Envelope::from_bytes(buf.as_slice(), Some(pathbuf.flags()))
|
||||
{
|
||||
env.set_hash(new_hash);
|
||||
debug!("{}\t{:?}", new_hash, &pathbuf);
|
||||
debug!(
|
||||
"hash {}, path: {:?} couldn't be parsed",
|
||||
new_hash, &pathbuf
|
||||
);
|
||||
index_lock.insert(new_hash, pathbuf.into());
|
||||
|
||||
/* Send Write notice */
|
||||
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: Update(old_hash, Box::new(env)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Remove */
|
||||
DebouncedEvent::NoticeRemove(pathbuf) | DebouncedEvent::Remove(pathbuf) => {
|
||||
debug!("DebouncedEvent::Remove(path = {:?}", pathbuf);
|
||||
let mailbox_hash = get_path_hash!(pathbuf);
|
||||
let mut hash_indexes_lock = hash_indexes.lock().unwrap();
|
||||
let index_lock = hash_indexes_lock.entry(mailbox_hash).or_default();
|
||||
let hash: EnvelopeHash = if let Some((k, _)) =
|
||||
index_lock.iter().find(|(_, v)| *v.buf == pathbuf)
|
||||
{
|
||||
*k
|
||||
} else {
|
||||
debug!("removed but not contained in index");
|
||||
continue;
|
||||
};
|
||||
if let Some(ref modif) = &index_lock[&hash].modified {
|
||||
match modif {
|
||||
PathMod::Path(path) => debug!(
|
||||
"envelope {} has modified path set {}",
|
||||
hash,
|
||||
path.display()
|
||||
),
|
||||
PathMod::Hash(hash) => debug!(
|
||||
"envelope {} has modified path set {}",
|
||||
hash,
|
||||
&index_lock[&hash].buf.display()
|
||||
),
|
||||
}
|
||||
index_lock.entry(hash).and_modify(|e| {
|
||||
e.removed = false;
|
||||
});
|
||||
continue;
|
||||
}
|
||||
{
|
||||
let mut lck = mailbox_counts[&mailbox_hash].1.lock().unwrap();
|
||||
*lck = lck.saturating_sub(1);
|
||||
}
|
||||
if !pathbuf.flags().contains(Flag::SEEN) {
|
||||
let mut lck = mailbox_counts[&mailbox_hash].0.lock().unwrap();
|
||||
*lck = lck.saturating_sub(1);
|
||||
}
|
||||
|
||||
index_lock.entry(hash).and_modify(|e| {
|
||||
e.removed = true;
|
||||
});
|
||||
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: Remove(hash),
|
||||
}),
|
||||
);
|
||||
}
|
||||
/* Envelope hasn't changed */
|
||||
DebouncedEvent::Rename(src, dest) => {
|
||||
debug!("DebouncedEvent::Rename(src = {:?}, dest = {:?})", src, dest);
|
||||
let mailbox_hash = get_path_hash!(src);
|
||||
let dest_mailbox = {
|
||||
let dest_mailbox = get_path_hash!(dest);
|
||||
if dest_mailbox == mailbox_hash {
|
||||
None
|
||||
} else {
|
||||
Some(dest_mailbox)
|
||||
}
|
||||
};
|
||||
let old_hash: EnvelopeHash = get_file_hash(src.as_path());
|
||||
let new_hash: EnvelopeHash = get_file_hash(dest.as_path());
|
||||
|
||||
let mut hash_indexes_lock = hash_indexes.lock().unwrap();
|
||||
let index_lock = hash_indexes_lock.entry(mailbox_hash).or_default();
|
||||
let old_flags = src.flags();
|
||||
let new_flags = dest.flags();
|
||||
let was_seen: bool = old_flags.contains(Flag::SEEN);
|
||||
let is_seen: bool = new_flags.contains(Flag::SEEN);
|
||||
|
||||
if index_lock.contains_key(&old_hash) && !index_lock[&old_hash].removed
|
||||
{
|
||||
debug!("contains_old_key");
|
||||
if let Some(dest_mailbox) = dest_mailbox {
|
||||
index_lock.entry(old_hash).and_modify(|e| {
|
||||
e.removed = true;
|
||||
});
|
||||
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: Remove(old_hash),
|
||||
}),
|
||||
);
|
||||
let file_name = dest
|
||||
.as_path()
|
||||
.strip_prefix(&root_path)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
drop(hash_indexes_lock);
|
||||
if let Ok(env) = add_path_to_index(
|
||||
&hash_indexes,
|
||||
dest_mailbox,
|
||||
dest.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
mailbox_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(env.hash(), dest_mailbox);
|
||||
debug!(
|
||||
"Create event {} {} {}",
|
||||
env.hash(),
|
||||
env.subject(),
|
||||
dest.display()
|
||||
);
|
||||
if !env.is_seen() {
|
||||
*mailbox_counts[&dest_mailbox].0.lock().unwrap() += 1;
|
||||
}
|
||||
*mailbox_counts[&dest_mailbox].1.lock().unwrap() += 1;
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash: dest_mailbox,
|
||||
kind: Create(Box::new(env)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
index_lock.entry(old_hash).and_modify(|e| {
|
||||
debug!(&e.modified);
|
||||
e.modified = Some(PathMod::Hash(new_hash));
|
||||
});
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: Rename(old_hash, new_hash),
|
||||
}),
|
||||
);
|
||||
if !was_seen && is_seen {
|
||||
let mut lck =
|
||||
mailbox_counts[&mailbox_hash].0.lock().unwrap();
|
||||
*lck = lck.saturating_sub(1);
|
||||
} else if was_seen && !is_seen {
|
||||
*mailbox_counts[&mailbox_hash].0.lock().unwrap() += 1;
|
||||
}
|
||||
if old_flags != new_flags {
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: NewFlags(new_hash, (new_flags, vec![])),
|
||||
}),
|
||||
);
|
||||
}
|
||||
mailbox_index.lock().unwrap().insert(new_hash, mailbox_hash);
|
||||
index_lock.insert(new_hash, dest.into());
|
||||
}
|
||||
continue;
|
||||
} else if !index_lock.contains_key(&new_hash)
|
||||
&& index_lock
|
||||
.get(&old_hash)
|
||||
.map(|e| e.removed)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
if index_lock
|
||||
.get(&old_hash)
|
||||
.map(|e| e.removed)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
index_lock.entry(old_hash).and_modify(|e| {
|
||||
e.modified = Some(PathMod::Hash(new_hash));
|
||||
e.removed = false;
|
||||
});
|
||||
debug!("contains_old_key, key was marked as removed (by external source)");
|
||||
} else {
|
||||
debug!("not contains_new_key");
|
||||
}
|
||||
let file_name = dest
|
||||
.as_path()
|
||||
.strip_prefix(&root_path)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
debug!("filename = {:?}", file_name);
|
||||
drop(hash_indexes_lock);
|
||||
if let Ok(env) = add_path_to_index(
|
||||
&hash_indexes,
|
||||
dest_mailbox.unwrap_or(mailbox_hash),
|
||||
dest.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
mailbox_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(env.hash(), dest_mailbox.unwrap_or(mailbox_hash));
|
||||
debug!(
|
||||
"Create event {} {} {}",
|
||||
env.hash(),
|
||||
env.subject(),
|
||||
dest.display()
|
||||
);
|
||||
if !env.is_seen() {
|
||||
*mailbox_counts[&dest_mailbox.unwrap_or(mailbox_hash)]
|
||||
.0
|
||||
.lock()
|
||||
.unwrap() += 1;
|
||||
}
|
||||
*mailbox_counts[&dest_mailbox.unwrap_or(mailbox_hash)]
|
||||
.1
|
||||
.lock()
|
||||
.unwrap() += 1;
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash: dest_mailbox.unwrap_or(mailbox_hash),
|
||||
kind: Create(Box::new(env)),
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
} else {
|
||||
debug!("not valid email");
|
||||
}
|
||||
} else if let Some(dest_mailbox) = dest_mailbox {
|
||||
drop(hash_indexes_lock);
|
||||
let file_name = dest
|
||||
.as_path()
|
||||
.strip_prefix(&root_path)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
if let Ok(env) = add_path_to_index(
|
||||
&hash_indexes,
|
||||
dest_mailbox,
|
||||
dest.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
mailbox_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(env.hash(), dest_mailbox);
|
||||
debug!(
|
||||
"Create event {} {} {}",
|
||||
env.hash(),
|
||||
env.subject(),
|
||||
dest.display()
|
||||
);
|
||||
if !env.is_seen() {
|
||||
*mailbox_counts[&dest_mailbox].0.lock().unwrap() += 1;
|
||||
}
|
||||
*mailbox_counts[&dest_mailbox].1.lock().unwrap() += 1;
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash: dest_mailbox,
|
||||
kind: Create(Box::new(env)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if was_seen && !is_seen {
|
||||
*mailbox_counts[&mailbox_hash].0.lock().unwrap() += 1;
|
||||
}
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: Rename(old_hash, new_hash),
|
||||
}),
|
||||
);
|
||||
debug!("contains_new_key");
|
||||
if old_flags != new_flags {
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: NewFlags(new_hash, (new_flags, vec![])),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* Maybe a re-read should be triggered here just to be safe.
|
||||
(sender)(account_hash, BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash: get_path_hash!(dest),
|
||||
kind: Rescan,
|
||||
}));
|
||||
*/
|
||||
}
|
||||
/* Trigger rescan of mailbox */
|
||||
DebouncedEvent::Rescan => {
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash: root_mailbox_hash,
|
||||
kind: Rescan,
|
||||
}),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Err(e) => debug!("watch error: {:?}", e),
|
||||
}
|
||||
}
|
||||
let mailboxes = self.mailboxes.clone();
|
||||
let root_path = self.path.to_path_buf();
|
||||
Ok(Box::new(super::watch::MaildirWatcher {
|
||||
account_hash,
|
||||
cache_dir,
|
||||
event_consumer,
|
||||
hash_indexes,
|
||||
mailbox_hashes: BTreeSet::default(),
|
||||
mailbox_index,
|
||||
mailboxes,
|
||||
polling_period: std::time::Duration::from_secs(2),
|
||||
root_path,
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -1173,7 +700,7 @@ impl MaildirType {
|
|||
}
|
||||
}
|
||||
Ok(children)
|
||||
}
|
||||
};
|
||||
let root_path = PathBuf::from(settings.root_mailbox()).expand();
|
||||
if !root_path.exists() {
|
||||
return Err(MeliError::new(format!(
|
||||
|
@ -1335,49 +862,3 @@ impl MaildirType {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn add_path_to_index(
|
||||
hash_index: &HashIndexes,
|
||||
mailbox_hash: MailboxHash,
|
||||
path: &Path,
|
||||
cache_dir: &xdg::BaseDirectories,
|
||||
file_name: PathBuf,
|
||||
buf: &mut Vec<u8>,
|
||||
) -> Result<Envelope> {
|
||||
debug!("add_path_to_index path {:?} filename{:?}", path, file_name);
|
||||
let env_hash = get_file_hash(path);
|
||||
{
|
||||
let mut map = hash_index.lock().unwrap();
|
||||
let map = map.entry(mailbox_hash).or_default();
|
||||
map.insert(env_hash, path.to_path_buf().into());
|
||||
debug!(
|
||||
"inserted {} in {} map, len={}",
|
||||
env_hash,
|
||||
mailbox_hash,
|
||||
map.len()
|
||||
);
|
||||
}
|
||||
let mut reader = io::BufReader::new(fs::File::open(&path)?);
|
||||
buf.clear();
|
||||
reader.read_to_end(buf)?;
|
||||
let mut env = Envelope::from_bytes(buf.as_slice(), Some(path.flags()))?;
|
||||
env.set_hash(env_hash);
|
||||
debug!(
|
||||
"add_path_to_index gen {}\t{}",
|
||||
env_hash,
|
||||
file_name.display()
|
||||
);
|
||||
if let Ok(cached) = cache_dir.place_cache_file(file_name) {
|
||||
debug!("putting in cache");
|
||||
/* place result in cache directory */
|
||||
let f = fs::File::create(cached)?;
|
||||
let metadata = f.metadata()?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
permissions.set_mode(0o600); // Read/write for owner only.
|
||||
f.set_permissions(permissions)?;
|
||||
let writer = io::BufWriter::new(f);
|
||||
bincode::Options::serialize_into(bincode::config::DefaultOptions::new(), writer, &env)?;
|
||||
}
|
||||
Ok(env)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,608 @@
|
|||
/*
|
||||
* meli - mailbox module.
|
||||
*
|
||||
* Copyright 2017 - 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::*;
|
||||
use crate::backends::{RefreshEventKind::*, *};
|
||||
use std::collections::BTreeSet;
|
||||
use std::ffi::OsStr;
|
||||
use std::io;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
extern crate notify;
|
||||
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MaildirWatcher {
|
||||
pub account_hash: AccountHash,
|
||||
pub cache_dir: xdg::BaseDirectories,
|
||||
pub event_consumer: BackendEventConsumer,
|
||||
pub hash_indexes: HashIndexes,
|
||||
pub mailbox_hashes: BTreeSet<MailboxHash>,
|
||||
pub mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
|
||||
pub mailboxes: HashMap<MailboxHash, MaildirMailbox>,
|
||||
pub polling_period: std::time::Duration,
|
||||
pub root_path: PathBuf,
|
||||
}
|
||||
|
||||
impl BackendWatcher for MaildirWatcher {
|
||||
fn is_blocking(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn register_mailbox(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
_urgency: MailboxWatchUrgency,
|
||||
) -> Result<()> {
|
||||
self.mailbox_hashes.insert(mailbox_hash);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_polling_period(&mut self, period: Option<std::time::Duration>) -> Result<()> {
|
||||
if let Some(period) = period {
|
||||
self.polling_period = period;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn(self: Box<Self>) -> ResultFuture<()> {
|
||||
let MaildirWatcher {
|
||||
account_hash,
|
||||
cache_dir,
|
||||
event_consumer: sender,
|
||||
hash_indexes,
|
||||
mailbox_hashes: _,
|
||||
mailbox_index,
|
||||
mailboxes,
|
||||
polling_period,
|
||||
root_path,
|
||||
} = *self;
|
||||
Ok(Box::pin(async move {
|
||||
let (tx, rx) = channel();
|
||||
let mut watcher = watcher(tx, polling_period).unwrap();
|
||||
watcher.watch(&root_path, RecursiveMode::Recursive).unwrap();
|
||||
debug!("watching {:?}", root_path);
|
||||
let root_mailbox_hash: MailboxHash = mailboxes
|
||||
.values()
|
||||
.find(|m| m.parent.is_none())
|
||||
.map(|m| m.hash())
|
||||
.unwrap();
|
||||
let mailbox_counts = mailboxes
|
||||
.iter()
|
||||
.map(|(&k, v)| (k, (v.unseen.clone(), v.total.clone())))
|
||||
.collect::<HashMap<MailboxHash, (Arc<Mutex<usize>>, Arc<Mutex<usize>>)>>();
|
||||
let mut buf = Vec::with_capacity(4096);
|
||||
loop {
|
||||
match rx.recv() {
|
||||
/*
|
||||
* Event types:
|
||||
*
|
||||
* pub enum RefreshEventKind {
|
||||
* Update(EnvelopeHash, Envelope), // Old hash, new envelope
|
||||
* Create(Envelope),
|
||||
* Remove(EnvelopeHash),
|
||||
* Rescan,
|
||||
* }
|
||||
*/
|
||||
Ok(event) => match event {
|
||||
/* Create */
|
||||
DebouncedEvent::Create(mut pathbuf) => {
|
||||
debug!("DebouncedEvent::Create(path = {:?}", pathbuf);
|
||||
if path_is_new!(pathbuf) {
|
||||
debug!("path_is_new");
|
||||
/* This creates a Rename event that we will receive later */
|
||||
pathbuf = match move_to_cur(pathbuf) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
debug!("error: {}", e.to_string());
|
||||
continue;
|
||||
}
|
||||
};
|
||||
}
|
||||
let mailbox_hash = get_path_hash!(pathbuf);
|
||||
let file_name = pathbuf
|
||||
.as_path()
|
||||
.strip_prefix(&root_path)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
if let Ok(env) = add_path_to_index(
|
||||
&hash_indexes,
|
||||
mailbox_hash,
|
||||
pathbuf.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
mailbox_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(env.hash(), mailbox_hash);
|
||||
debug!(
|
||||
"Create event {} {} {}",
|
||||
env.hash(),
|
||||
env.subject(),
|
||||
pathbuf.display()
|
||||
);
|
||||
if !env.is_seen() {
|
||||
*mailbox_counts[&mailbox_hash].0.lock().unwrap() += 1;
|
||||
}
|
||||
*mailbox_counts[&mailbox_hash].1.lock().unwrap() += 1;
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: Create(Box::new(env)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
/* Update */
|
||||
DebouncedEvent::NoticeWrite(pathbuf) | DebouncedEvent::Write(pathbuf) => {
|
||||
debug!("DebouncedEvent::Write(path = {:?}", &pathbuf);
|
||||
let mailbox_hash = get_path_hash!(pathbuf);
|
||||
let mut hash_indexes_lock = hash_indexes.lock().unwrap();
|
||||
let index_lock =
|
||||
&mut hash_indexes_lock.entry(mailbox_hash).or_default();
|
||||
let file_name = pathbuf
|
||||
.as_path()
|
||||
.strip_prefix(&root_path)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
/* Linear search in hash_index to find old hash */
|
||||
let old_hash: EnvelopeHash = {
|
||||
if let Some((k, v)) =
|
||||
index_lock.iter_mut().find(|(_, v)| *v.buf == pathbuf)
|
||||
{
|
||||
*v = pathbuf.clone().into();
|
||||
*k
|
||||
} else {
|
||||
drop(hash_indexes_lock);
|
||||
/* Did we just miss a Create event? In any case, create
|
||||
* envelope. */
|
||||
if let Ok(env) = add_path_to_index(
|
||||
&hash_indexes,
|
||||
mailbox_hash,
|
||||
pathbuf.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
mailbox_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(env.hash(), mailbox_hash);
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: Create(Box::new(env)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let new_hash: EnvelopeHash = get_file_hash(pathbuf.as_path());
|
||||
let mut reader = io::BufReader::new(fs::File::open(&pathbuf)?);
|
||||
buf.clear();
|
||||
reader.read_to_end(&mut buf)?;
|
||||
if index_lock.get_mut(&new_hash).is_none() {
|
||||
debug!("write notice");
|
||||
if let Ok(mut env) =
|
||||
Envelope::from_bytes(buf.as_slice(), Some(pathbuf.flags()))
|
||||
{
|
||||
env.set_hash(new_hash);
|
||||
debug!("{}\t{:?}", new_hash, &pathbuf);
|
||||
debug!(
|
||||
"hash {}, path: {:?} couldn't be parsed",
|
||||
new_hash, &pathbuf
|
||||
);
|
||||
index_lock.insert(new_hash, pathbuf.into());
|
||||
|
||||
/* Send Write notice */
|
||||
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: Update(old_hash, Box::new(env)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Remove */
|
||||
DebouncedEvent::NoticeRemove(pathbuf) | DebouncedEvent::Remove(pathbuf) => {
|
||||
debug!("DebouncedEvent::Remove(path = {:?}", pathbuf);
|
||||
let mailbox_hash = get_path_hash!(pathbuf);
|
||||
let mut hash_indexes_lock = hash_indexes.lock().unwrap();
|
||||
let index_lock = hash_indexes_lock.entry(mailbox_hash).or_default();
|
||||
let hash: EnvelopeHash = if let Some((k, _)) =
|
||||
index_lock.iter().find(|(_, v)| *v.buf == pathbuf)
|
||||
{
|
||||
*k
|
||||
} else {
|
||||
debug!("removed but not contained in index");
|
||||
continue;
|
||||
};
|
||||
if let Some(ref modif) = &index_lock[&hash].modified {
|
||||
match modif {
|
||||
PathMod::Path(path) => debug!(
|
||||
"envelope {} has modified path set {}",
|
||||
hash,
|
||||
path.display()
|
||||
),
|
||||
PathMod::Hash(hash) => debug!(
|
||||
"envelope {} has modified path set {}",
|
||||
hash,
|
||||
&index_lock[&hash].buf.display()
|
||||
),
|
||||
}
|
||||
index_lock.entry(hash).and_modify(|e| {
|
||||
e.removed = false;
|
||||
});
|
||||
continue;
|
||||
}
|
||||
{
|
||||
let mut lck = mailbox_counts[&mailbox_hash].1.lock().unwrap();
|
||||
*lck = lck.saturating_sub(1);
|
||||
}
|
||||
if !pathbuf.flags().contains(Flag::SEEN) {
|
||||
let mut lck = mailbox_counts[&mailbox_hash].0.lock().unwrap();
|
||||
*lck = lck.saturating_sub(1);
|
||||
}
|
||||
|
||||
index_lock.entry(hash).and_modify(|e| {
|
||||
e.removed = true;
|
||||
});
|
||||
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: Remove(hash),
|
||||
}),
|
||||
);
|
||||
}
|
||||
/* Envelope hasn't changed */
|
||||
DebouncedEvent::Rename(src, dest) => {
|
||||
debug!("DebouncedEvent::Rename(src = {:?}, dest = {:?})", src, dest);
|
||||
let mailbox_hash = get_path_hash!(src);
|
||||
let dest_mailbox = {
|
||||
let dest_mailbox = get_path_hash!(dest);
|
||||
if dest_mailbox == mailbox_hash {
|
||||
None
|
||||
} else {
|
||||
Some(dest_mailbox)
|
||||
}
|
||||
};
|
||||
let old_hash: EnvelopeHash = get_file_hash(src.as_path());
|
||||
let new_hash: EnvelopeHash = get_file_hash(dest.as_path());
|
||||
|
||||
let mut hash_indexes_lock = hash_indexes.lock().unwrap();
|
||||
let index_lock = hash_indexes_lock.entry(mailbox_hash).or_default();
|
||||
let old_flags = src.flags();
|
||||
let new_flags = dest.flags();
|
||||
let was_seen: bool = old_flags.contains(Flag::SEEN);
|
||||
let is_seen: bool = new_flags.contains(Flag::SEEN);
|
||||
|
||||
if index_lock.contains_key(&old_hash) && !index_lock[&old_hash].removed
|
||||
{
|
||||
debug!("contains_old_key");
|
||||
if let Some(dest_mailbox) = dest_mailbox {
|
||||
index_lock.entry(old_hash).and_modify(|e| {
|
||||
e.removed = true;
|
||||
});
|
||||
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: Remove(old_hash),
|
||||
}),
|
||||
);
|
||||
let file_name = dest
|
||||
.as_path()
|
||||
.strip_prefix(&root_path)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
drop(hash_indexes_lock);
|
||||
if let Ok(env) = add_path_to_index(
|
||||
&hash_indexes,
|
||||
dest_mailbox,
|
||||
dest.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
mailbox_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(env.hash(), dest_mailbox);
|
||||
debug!(
|
||||
"Create event {} {} {}",
|
||||
env.hash(),
|
||||
env.subject(),
|
||||
dest.display()
|
||||
);
|
||||
if !env.is_seen() {
|
||||
*mailbox_counts[&dest_mailbox].0.lock().unwrap() += 1;
|
||||
}
|
||||
*mailbox_counts[&dest_mailbox].1.lock().unwrap() += 1;
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash: dest_mailbox,
|
||||
kind: Create(Box::new(env)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
index_lock.entry(old_hash).and_modify(|e| {
|
||||
debug!(&e.modified);
|
||||
e.modified = Some(PathMod::Hash(new_hash));
|
||||
});
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: Rename(old_hash, new_hash),
|
||||
}),
|
||||
);
|
||||
if !was_seen && is_seen {
|
||||
let mut lck =
|
||||
mailbox_counts[&mailbox_hash].0.lock().unwrap();
|
||||
*lck = lck.saturating_sub(1);
|
||||
} else if was_seen && !is_seen {
|
||||
*mailbox_counts[&mailbox_hash].0.lock().unwrap() += 1;
|
||||
}
|
||||
if old_flags != new_flags {
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: NewFlags(new_hash, (new_flags, vec![])),
|
||||
}),
|
||||
);
|
||||
}
|
||||
mailbox_index.lock().unwrap().insert(new_hash, mailbox_hash);
|
||||
index_lock.insert(new_hash, dest.into());
|
||||
}
|
||||
continue;
|
||||
} else if !index_lock.contains_key(&new_hash)
|
||||
&& index_lock
|
||||
.get(&old_hash)
|
||||
.map(|e| e.removed)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
if index_lock
|
||||
.get(&old_hash)
|
||||
.map(|e| e.removed)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
index_lock.entry(old_hash).and_modify(|e| {
|
||||
e.modified = Some(PathMod::Hash(new_hash));
|
||||
e.removed = false;
|
||||
});
|
||||
debug!("contains_old_key, key was marked as removed (by external source)");
|
||||
} else {
|
||||
debug!("not contains_new_key");
|
||||
}
|
||||
let file_name = dest
|
||||
.as_path()
|
||||
.strip_prefix(&root_path)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
debug!("filename = {:?}", file_name);
|
||||
drop(hash_indexes_lock);
|
||||
if let Ok(env) = add_path_to_index(
|
||||
&hash_indexes,
|
||||
dest_mailbox.unwrap_or(mailbox_hash),
|
||||
dest.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
mailbox_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(env.hash(), dest_mailbox.unwrap_or(mailbox_hash));
|
||||
debug!(
|
||||
"Create event {} {} {}",
|
||||
env.hash(),
|
||||
env.subject(),
|
||||
dest.display()
|
||||
);
|
||||
if !env.is_seen() {
|
||||
*mailbox_counts[&dest_mailbox.unwrap_or(mailbox_hash)]
|
||||
.0
|
||||
.lock()
|
||||
.unwrap() += 1;
|
||||
}
|
||||
*mailbox_counts[&dest_mailbox.unwrap_or(mailbox_hash)]
|
||||
.1
|
||||
.lock()
|
||||
.unwrap() += 1;
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash: dest_mailbox.unwrap_or(mailbox_hash),
|
||||
kind: Create(Box::new(env)),
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
} else {
|
||||
debug!("not valid email");
|
||||
}
|
||||
} else if let Some(dest_mailbox) = dest_mailbox {
|
||||
drop(hash_indexes_lock);
|
||||
let file_name = dest
|
||||
.as_path()
|
||||
.strip_prefix(&root_path)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
if let Ok(env) = add_path_to_index(
|
||||
&hash_indexes,
|
||||
dest_mailbox,
|
||||
dest.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
mailbox_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(env.hash(), dest_mailbox);
|
||||
debug!(
|
||||
"Create event {} {} {}",
|
||||
env.hash(),
|
||||
env.subject(),
|
||||
dest.display()
|
||||
);
|
||||
if !env.is_seen() {
|
||||
*mailbox_counts[&dest_mailbox].0.lock().unwrap() += 1;
|
||||
}
|
||||
*mailbox_counts[&dest_mailbox].1.lock().unwrap() += 1;
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash: dest_mailbox,
|
||||
kind: Create(Box::new(env)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if was_seen && !is_seen {
|
||||
*mailbox_counts[&mailbox_hash].0.lock().unwrap() += 1;
|
||||
}
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: Rename(old_hash, new_hash),
|
||||
}),
|
||||
);
|
||||
debug!("contains_new_key");
|
||||
if old_flags != new_flags {
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: NewFlags(new_hash, (new_flags, vec![])),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* Maybe a re-read should be triggered here just to be safe.
|
||||
(sender)(account_hash, BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash: get_path_hash!(dest),
|
||||
kind: Rescan,
|
||||
}));
|
||||
*/
|
||||
}
|
||||
/* Trigger rescan of mailbox */
|
||||
DebouncedEvent::Rescan => {
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash: root_mailbox_hash,
|
||||
kind: Rescan,
|
||||
}),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Err(e) => debug!("watch error: {:?}", e),
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn add_path_to_index(
|
||||
hash_index: &HashIndexes,
|
||||
mailbox_hash: MailboxHash,
|
||||
path: &Path,
|
||||
cache_dir: &xdg::BaseDirectories,
|
||||
file_name: PathBuf,
|
||||
buf: &mut Vec<u8>,
|
||||
) -> Result<Envelope> {
|
||||
debug!("add_path_to_index path {:?} filename{:?}", path, file_name);
|
||||
let env_hash = get_file_hash(path);
|
||||
{
|
||||
let mut map = hash_index.lock().unwrap();
|
||||
let map = map.entry(mailbox_hash).or_default();
|
||||
map.insert(env_hash, path.to_path_buf().into());
|
||||
debug!(
|
||||
"inserted {} in {} map, len={}",
|
||||
env_hash,
|
||||
mailbox_hash,
|
||||
map.len()
|
||||
);
|
||||
}
|
||||
let mut reader = io::BufReader::new(fs::File::open(&path)?);
|
||||
buf.clear();
|
||||
reader.read_to_end(buf)?;
|
||||
let mut env = Envelope::from_bytes(buf.as_slice(), Some(path.flags()))?;
|
||||
env.set_hash(env_hash);
|
||||
debug!(
|
||||
"add_path_to_index gen {}\t{}",
|
||||
env_hash,
|
||||
file_name.display()
|
||||
);
|
||||
if let Ok(cached) = cache_dir.place_cache_file(file_name) {
|
||||
debug!("putting in cache");
|
||||
/* place result in cache directory */
|
||||
let f = fs::File::create(cached)?;
|
||||
let metadata = f.metadata()?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
permissions.set_mode(0o600); // Read/write for owner only.
|
||||
f.set_permissions(permissions)?;
|
||||
let writer = io::BufWriter::new(f);
|
||||
bincode::Options::serialize_into(bincode::config::DefaultOptions::new(), writer, &env)?;
|
||||
}
|
||||
Ok(env)
|
||||
}
|
|
@ -147,6 +147,8 @@ use std::sync::{Arc, Mutex, RwLock};
|
|||
|
||||
pub mod write;
|
||||
|
||||
pub mod watch;
|
||||
|
||||
pub type Offset = usize;
|
||||
pub type Length = usize;
|
||||
|
||||
|
@ -184,7 +186,7 @@ fn get_rw_lock_blocking(f: &File, path: &Path) -> Result<()> {
|
|||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MboxMailbox {
|
||||
pub struct MboxMailbox {
|
||||
hash: MailboxHash,
|
||||
name: String,
|
||||
path: PathBuf,
|
||||
|
@ -950,157 +952,24 @@ impl MailBackend for MboxType {
|
|||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn watch(&self) -> ResultFuture<()> {
|
||||
let sender = self.event_consumer.clone();
|
||||
let (tx, rx) = channel();
|
||||
let mut watcher = watcher(tx, std::time::Duration::from_secs(10))
|
||||
.map_err(|e| e.to_string())
|
||||
.map_err(MeliError::new)?;
|
||||
for f in self.mailboxes.lock().unwrap().values() {
|
||||
watcher
|
||||
.watch(&f.fs_path, RecursiveMode::Recursive)
|
||||
.map_err(|e| e.to_string())
|
||||
.map_err(MeliError::new)?;
|
||||
debug!("watching {:?}", f.fs_path.as_path());
|
||||
}
|
||||
fn watcher(&self) -> Result<Box<dyn BackendWatcher>> {
|
||||
let account_hash = {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
hasher.write(self.account_name.as_bytes());
|
||||
hasher.finish()
|
||||
};
|
||||
let mailboxes = self.mailboxes.clone();
|
||||
let event_consumer = self.event_consumer.clone();
|
||||
let mailbox_index = self.mailbox_index.clone();
|
||||
let prefer_mbox_type = self.prefer_mbox_type;
|
||||
Ok(Box::pin(async move {
|
||||
loop {
|
||||
match rx.recv() {
|
||||
/*
|
||||
* Event types:
|
||||
*
|
||||
* pub enum RefreshEventKind {
|
||||
* Update(EnvelopeHash, Envelope), // Old hash, new envelope
|
||||
* Create(Envelope),
|
||||
* Remove(EnvelopeHash),
|
||||
* Rescan,
|
||||
* }
|
||||
*/
|
||||
Ok(event) => match event {
|
||||
/* Update */
|
||||
DebouncedEvent::NoticeWrite(pathbuf) | DebouncedEvent::Write(pathbuf) => {
|
||||
let mailbox_hash = get_path_hash!(&pathbuf);
|
||||
let file = match std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(&pathbuf)
|
||||
{
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
get_rw_lock_blocking(&file, &pathbuf)?;
|
||||
let mut mailbox_lock = mailboxes.lock().unwrap();
|
||||
let mut buf_reader = BufReader::new(file);
|
||||
let mut contents = Vec::new();
|
||||
if let Err(e) = buf_reader.read_to_end(&mut contents) {
|
||||
debug!(e);
|
||||
continue;
|
||||
};
|
||||
if contents.starts_with(mailbox_lock[&mailbox_hash].content.as_slice())
|
||||
{
|
||||
if let Ok((_, envelopes)) = mbox_parse(
|
||||
mailbox_lock[&mailbox_hash].index.clone(),
|
||||
&contents,
|
||||
mailbox_lock[&mailbox_hash].content.len(),
|
||||
prefer_mbox_type,
|
||||
) {
|
||||
let mut mailbox_index_lck = mailbox_index.lock().unwrap();
|
||||
for env in envelopes {
|
||||
mailbox_index_lck.insert(env.hash(), mailbox_hash);
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Create(Box::new(env)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Rescan,
|
||||
}),
|
||||
);
|
||||
}
|
||||
mailbox_lock
|
||||
.entry(mailbox_hash)
|
||||
.and_modify(|f| f.content = contents);
|
||||
}
|
||||
/* Remove */
|
||||
DebouncedEvent::NoticeRemove(pathbuf) | DebouncedEvent::Remove(pathbuf) => {
|
||||
if mailboxes
|
||||
.lock()
|
||||
.unwrap()
|
||||
.values()
|
||||
.any(|f| f.fs_path == pathbuf)
|
||||
{
|
||||
let mailbox_hash = get_path_hash!(&pathbuf);
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Failure(MeliError::new(format!(
|
||||
"mbox mailbox {} was removed.",
|
||||
pathbuf.display()
|
||||
))),
|
||||
}),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
DebouncedEvent::Rename(src, dest) => {
|
||||
if mailboxes.lock().unwrap().values().any(|f| f.fs_path == src) {
|
||||
let mailbox_hash = get_path_hash!(&src);
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Failure(MeliError::new(format!(
|
||||
"mbox mailbox {} was renamed to {}.",
|
||||
src.display(),
|
||||
dest.display()
|
||||
))),
|
||||
}),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
/* Trigger rescan of mailboxes */
|
||||
DebouncedEvent::Rescan => {
|
||||
for &mailbox_hash in mailboxes.lock().unwrap().keys() {
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Rescan,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Err(e) => debug!("watch error: {:?}", e),
|
||||
}
|
||||
}
|
||||
let mailboxes = self.mailboxes.clone();
|
||||
let prefer_mbox_type = self.prefer_mbox_type.clone();
|
||||
Ok(Box::new(watch::MboxWatcher {
|
||||
account_hash,
|
||||
event_consumer,
|
||||
mailbox_hashes: BTreeSet::default(),
|
||||
mailbox_index,
|
||||
mailboxes,
|
||||
polling_period: std::time::Duration::from_secs(60),
|
||||
prefer_mbox_type,
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,223 @@
|
|||
/*
|
||||
* meli - mailbox module.
|
||||
*
|
||||
* Copyright 2017 - 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::*;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MboxWatcher {
|
||||
pub account_hash: AccountHash,
|
||||
pub event_consumer: BackendEventConsumer,
|
||||
pub mailbox_hashes: BTreeSet<MailboxHash>,
|
||||
pub mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
|
||||
pub mailboxes: Arc<Mutex<HashMap<MailboxHash, MboxMailbox>>>,
|
||||
pub polling_period: std::time::Duration,
|
||||
pub prefer_mbox_type: Option<MboxFormat>,
|
||||
}
|
||||
|
||||
impl BackendWatcher for MboxWatcher {
|
||||
fn is_blocking(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn register_mailbox(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
_urgency: MailboxWatchUrgency,
|
||||
) -> Result<()> {
|
||||
self.mailbox_hashes.insert(mailbox_hash);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_polling_period(&mut self, period: Option<std::time::Duration>) -> Result<()> {
|
||||
if let Some(period) = period {
|
||||
self.polling_period = period;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn(self: Box<Self>) -> ResultFuture<()> {
|
||||
let MboxWatcher {
|
||||
account_hash,
|
||||
event_consumer: sender,
|
||||
mailbox_hashes,
|
||||
mailbox_index,
|
||||
mailboxes,
|
||||
polling_period,
|
||||
prefer_mbox_type,
|
||||
} = *self;
|
||||
let (tx, rx) = channel();
|
||||
let mut watcher = watcher(tx, polling_period)
|
||||
.map_err(|e| e.to_string())
|
||||
.map_err(MeliError::new)?;
|
||||
for (_, f) in mailboxes
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|(k, _)| mailbox_hashes.contains(k))
|
||||
{
|
||||
watcher
|
||||
.watch(&f.fs_path, RecursiveMode::Recursive)
|
||||
.map_err(|e| e.to_string())
|
||||
.map_err(MeliError::new)?;
|
||||
debug!("watching {:?}", f.fs_path.as_path());
|
||||
}
|
||||
Ok(Box::pin(async move {
|
||||
loop {
|
||||
match rx.recv() {
|
||||
/*
|
||||
* Event types:
|
||||
*
|
||||
* pub enum RefreshEventKind {
|
||||
* Update(EnvelopeHash, Envelope), // Old hash, new envelope
|
||||
* Create(Envelope),
|
||||
* Remove(EnvelopeHash),
|
||||
* Rescan,
|
||||
* }
|
||||
*/
|
||||
Ok(event) => match event {
|
||||
/* Update */
|
||||
DebouncedEvent::NoticeWrite(pathbuf) | DebouncedEvent::Write(pathbuf) => {
|
||||
let mailbox_hash = get_path_hash!(&pathbuf);
|
||||
let file = match std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(&pathbuf)
|
||||
{
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
get_rw_lock_blocking(&file, &pathbuf)?;
|
||||
let mut mailbox_lock = mailboxes.lock().unwrap();
|
||||
let mut buf_reader = BufReader::new(file);
|
||||
let mut contents = Vec::new();
|
||||
if let Err(e) = buf_reader.read_to_end(&mut contents) {
|
||||
debug!(e);
|
||||
continue;
|
||||
};
|
||||
if contents.starts_with(mailbox_lock[&mailbox_hash].content.as_slice())
|
||||
{
|
||||
if let Ok((_, envelopes)) = mbox_parse(
|
||||
mailbox_lock[&mailbox_hash].index.clone(),
|
||||
&contents,
|
||||
mailbox_lock[&mailbox_hash].content.len(),
|
||||
prefer_mbox_type,
|
||||
) {
|
||||
let mut mailbox_index_lck = mailbox_index.lock().unwrap();
|
||||
for env in envelopes {
|
||||
mailbox_index_lck.insert(env.hash(), mailbox_hash);
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Create(Box::new(env)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Rescan,
|
||||
}),
|
||||
);
|
||||
}
|
||||
mailbox_lock
|
||||
.entry(mailbox_hash)
|
||||
.and_modify(|f| f.content = contents);
|
||||
}
|
||||
/* Remove */
|
||||
DebouncedEvent::NoticeRemove(pathbuf) | DebouncedEvent::Remove(pathbuf) => {
|
||||
if mailboxes
|
||||
.lock()
|
||||
.unwrap()
|
||||
.values()
|
||||
.any(|f| f.fs_path == pathbuf)
|
||||
{
|
||||
let mailbox_hash = get_path_hash!(&pathbuf);
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Failure(MeliError::new(format!(
|
||||
"mbox mailbox {} was removed.",
|
||||
pathbuf.display()
|
||||
))),
|
||||
}),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
DebouncedEvent::Rename(src, dest) => {
|
||||
if mailboxes.lock().unwrap().values().any(|f| f.fs_path == src) {
|
||||
let mailbox_hash = get_path_hash!(&src);
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Failure(MeliError::new(format!(
|
||||
"mbox mailbox {} was renamed to {}.",
|
||||
src.display(),
|
||||
dest.display()
|
||||
))),
|
||||
}),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
/* Trigger rescan of mailboxes */
|
||||
DebouncedEvent::Rescan => {
|
||||
for &mailbox_hash in mailboxes.lock().unwrap().keys() {
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Rescan,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Err(e) => debug!("watch error: {:?}", e),
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
|
@ -47,46 +47,10 @@ use std::sync::{Arc, Mutex};
|
|||
use std::time::{Duration, Instant};
|
||||
pub type UID = usize;
|
||||
|
||||
macro_rules! get_conf_val {
|
||||
($s:ident[$var:literal]) => {
|
||||
$s.extra.get($var).ok_or_else(|| {
|
||||
MeliError::new(format!(
|
||||
"Configuration error ({}): NNTP connection requires the field `{}` set",
|
||||
$s.name.as_str(),
|
||||
$var
|
||||
))
|
||||
})
|
||||
};
|
||||
($s:ident[$var:literal], $default:expr) => {
|
||||
$s.extra
|
||||
.get($var)
|
||||
.map(|v| {
|
||||
<_>::from_str(v).map_err(|e| {
|
||||
MeliError::new(format!(
|
||||
"Configuration error ({}) NNTP: Invalid value for field `{}`: {}\n{}",
|
||||
$s.name.as_str(),
|
||||
$var,
|
||||
v,
|
||||
e
|
||||
))
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| Ok($default))
|
||||
};
|
||||
}
|
||||
|
||||
pub static SUPPORTED_CAPABILITIES: &[&str] = &[
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
"COMPRESS DEFLATE",
|
||||
"VERSION 2",
|
||||
"NEWNEWS",
|
||||
"POST",
|
||||
"OVER",
|
||||
"OVER MSGID",
|
||||
"READER",
|
||||
"STARTTLS",
|
||||
"HDR",
|
||||
"AUTHINFO USER",
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -110,7 +74,6 @@ pub struct UIDStore {
|
|||
account_name: Arc<String>,
|
||||
offline_cache: bool,
|
||||
capabilities: Arc<Mutex<Capabilities>>,
|
||||
message_id_index: Arc<Mutex<HashMap<String, EnvelopeHash>>>,
|
||||
hash_index: Arc<Mutex<HashMap<EnvelopeHash, (UID, MailboxHash)>>>,
|
||||
uid_index: Arc<Mutex<HashMap<(MailboxHash, UID), EnvelopeHash>>>,
|
||||
|
||||
|
@ -132,7 +95,6 @@ impl UIDStore {
|
|||
event_consumer,
|
||||
offline_cache: false,
|
||||
capabilities: Default::default(),
|
||||
message_id_index: Default::default(),
|
||||
hash_index: Default::default(),
|
||||
uid_index: Default::default(),
|
||||
mailboxes: Arc::new(FutureMutex::new(Default::default())),
|
||||
|
@ -169,7 +131,6 @@ impl MailBackend for NntpType {
|
|||
)
|
||||
})
|
||||
.collect::<Vec<(String, MailBackendExtensionStatus)>>();
|
||||
let mut supports_submission = false;
|
||||
let NntpExtensionUse {
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
deflate,
|
||||
|
@ -177,10 +138,6 @@ impl MailBackend for NntpType {
|
|||
{
|
||||
for (name, status) in extensions.iter_mut() {
|
||||
match name.as_str() {
|
||||
s if s.eq_ignore_ascii_case("POST") => {
|
||||
supports_submission = true;
|
||||
*status = MailBackendExtensionStatus::Enabled { comment: None };
|
||||
}
|
||||
"COMPRESS DEFLATE" => {
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
{
|
||||
|
@ -214,7 +171,7 @@ impl MailBackend for NntpType {
|
|||
supports_search: false,
|
||||
extensions: Some(extensions),
|
||||
supports_tags: false,
|
||||
supports_submission,
|
||||
supports_submission: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -244,88 +201,8 @@ impl MailBackend for NntpType {
|
|||
}))
|
||||
}
|
||||
|
||||
fn refresh(&mut self, mailbox_hash: MailboxHash) -> ResultFuture<()> {
|
||||
let uid_store = self.uid_store.clone();
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
/* To get updates, either issue NEWNEWS if it's supported by the server, and fallback
|
||||
* to OVER otherwise */
|
||||
let mbox: NntpMailbox = uid_store.mailboxes.lock().await.get(&mailbox_hash).map(std::clone::Clone::clone).ok_or_else(|| MeliError::new(format!("Mailbox with hash {} not found in NNTP connection, this could possibly be a bug or it was deleted.", mailbox_hash)))?;
|
||||
let latest_article: Option<crate::UnixTimestamp> =
|
||||
mbox.latest_article.lock().unwrap().clone();
|
||||
let (over_msgid_support, newnews_support): (bool, bool) = {
|
||||
let caps = uid_store.capabilities.lock().unwrap();
|
||||
|
||||
(
|
||||
caps.iter().any(|c| c.eq_ignore_ascii_case("OVER MSGID")),
|
||||
caps.iter().any(|c| c.eq_ignore_ascii_case("NEWNEWS")),
|
||||
)
|
||||
};
|
||||
let mut res = String::with_capacity(8 * 1024);
|
||||
let mut conn = timeout(Some(Duration::from_secs(60 * 16)), connection.lock()).await?;
|
||||
if let Some(mut latest_article) = latest_article {
|
||||
let timestamp = latest_article - 10 * 60;
|
||||
let datetime_str =
|
||||
crate::datetime::timestamp_to_string(timestamp, Some("%Y%m%d %H%M%S"), true);
|
||||
|
||||
if newnews_support {
|
||||
conn.send_command(
|
||||
format!("NEWNEWS {} {}", &mbox.nntp_path, datetime_str).as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
conn.read_response(&mut res, true, &["230 "]).await?;
|
||||
let message_ids = {
|
||||
let message_id_lck = uid_store.message_id_index.lock().unwrap();
|
||||
res.split_rn()
|
||||
.skip(1)
|
||||
.map(|s| s.trim())
|
||||
.filter(|msg_id| !message_id_lck.contains_key(*msg_id))
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<String>>()
|
||||
};
|
||||
if message_ids.is_empty() || !over_msgid_support {
|
||||
return Ok(());
|
||||
}
|
||||
let mut env_hash_set: BTreeSet<EnvelopeHash> = Default::default();
|
||||
for msg_id in message_ids {
|
||||
conn.send_command(format!("OVER {}", msg_id).as_bytes())
|
||||
.await?;
|
||||
conn.read_response(&mut res, true, &["224 "]).await?;
|
||||
let mut message_id_lck = uid_store.message_id_index.lock().unwrap();
|
||||
let mut hash_index_lck = uid_store.hash_index.lock().unwrap();
|
||||
let mut uid_index_lck = uid_store.uid_index.lock().unwrap();
|
||||
for l in res.split_rn().skip(1) {
|
||||
let (_, (num, env)) = protocol_parser::over_article(l)?;
|
||||
env_hash_set.insert(env.hash());
|
||||
message_id_lck.insert(env.message_id_display().to_string(), env.hash());
|
||||
hash_index_lck.insert(env.hash(), (num, mailbox_hash));
|
||||
uid_index_lck.insert((mailbox_hash, num), env.hash());
|
||||
latest_article = std::cmp::max(latest_article, env.timestamp);
|
||||
(uid_store.event_consumer)(
|
||||
uid_store.account_hash,
|
||||
crate::backends::BackendEvent::Refresh(RefreshEvent {
|
||||
mailbox_hash,
|
||||
account_hash: uid_store.account_hash,
|
||||
kind: RefreshEventKind::Create(Box::new(env)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
{
|
||||
let f = &uid_store.mailboxes.lock().await[&mailbox_hash];
|
||||
*f.latest_article.lock().unwrap() = Some(latest_article);
|
||||
f.exists
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert_existing_set(env_hash_set.clone());
|
||||
f.unseen.lock().unwrap().insert_existing_set(env_hash_set);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
//conn.select_group(mailbox_hash, false, &mut res).await?;
|
||||
Ok(())
|
||||
}))
|
||||
fn refresh(&mut self, _mailbox_hash: MailboxHash) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn mailboxes(&self) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
|
||||
|
@ -362,8 +239,8 @@ impl MailBackend for NntpType {
|
|||
}))
|
||||
}
|
||||
|
||||
fn watch(&self) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Unimplemented.").set_kind(ErrorKind::NotImplemented))
|
||||
fn watcher(&self) -> Result<Box<dyn BackendWatcher>> {
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn operation(&self, env_hash: EnvelopeHash) -> Result<Box<dyn BackendOp>> {
|
||||
|
@ -477,39 +354,6 @@ impl MailBackend for NntpType {
|
|||
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>> {
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn submit(
|
||||
&self,
|
||||
bytes: Vec<u8>,
|
||||
mailbox_hash: Option<MailboxHash>,
|
||||
_flags: Option<Flag>,
|
||||
) -> ResultFuture<()> {
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
match timeout(Some(Duration::from_secs(60 * 16)), connection.lock()).await {
|
||||
Ok(mut conn) => {
|
||||
match &conn.stream {
|
||||
Ok(stream) => {
|
||||
if !stream.supports_submission {
|
||||
return Err(MeliError::new("Server prohibits posting."));
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(err.clone()),
|
||||
}
|
||||
let mut res = String::with_capacity(8 * 1024);
|
||||
if let Some(mailbox_hash) = mailbox_hash {
|
||||
conn.select_group(mailbox_hash, false, &mut res).await?;
|
||||
}
|
||||
conn.send_command(b"POST").await?;
|
||||
conn.read_response(&mut res, false, &["340 "]).await?;
|
||||
conn.send_multiline_data_block(&bytes).await?;
|
||||
conn.read_response(&mut res, false, &["240 "]).await?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl NntpType {
|
||||
|
@ -544,10 +388,10 @@ impl NntpType {
|
|||
*/
|
||||
let server_port = get_conf_val!(s["server_port"], 119)?;
|
||||
let use_tls = get_conf_val!(s["use_tls"], server_port == 563)?;
|
||||
let use_starttls = use_tls && get_conf_val!(s["use_starttls"], false)?;
|
||||
let use_starttls = use_tls && get_conf_val!(s["use_starttls"], !(server_port == 563))?;
|
||||
let danger_accept_invalid_certs: bool =
|
||||
get_conf_val!(s["danger_accept_invalid_certs"], false)?;
|
||||
let require_auth = get_conf_val!(s["require_auth"], false)?;
|
||||
let require_auth = get_conf_val!(s["require_auth"], true)?;
|
||||
let server_conf = NntpServerConf {
|
||||
server_hostname: server_hostname.to_string(),
|
||||
server_username: if require_auth {
|
||||
|
@ -567,7 +411,7 @@ impl NntpType {
|
|||
danger_accept_invalid_certs,
|
||||
extension_use: NntpExtensionUse {
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
deflate: get_conf_val!(s["use_deflate"], false)?,
|
||||
deflate: get_conf_val!(s["use_deflate"], true)?,
|
||||
},
|
||||
};
|
||||
let account_hash = {
|
||||
|
@ -586,7 +430,6 @@ impl NntpType {
|
|||
nntp_path: k.to_string(),
|
||||
high_watermark: Arc::new(Mutex::new(0)),
|
||||
low_watermark: Arc::new(Mutex::new(0)),
|
||||
latest_article: Arc::new(Mutex::new(None)),
|
||||
exists: Default::default(),
|
||||
unseen: Default::default(),
|
||||
},
|
||||
|
@ -655,37 +498,6 @@ impl NntpType {
|
|||
}
|
||||
|
||||
pub fn validate_config(s: &AccountSettings) -> Result<()> {
|
||||
let mut keys: HashSet<&'static str> = Default::default();
|
||||
macro_rules! get_conf_val {
|
||||
($s:ident[$var:literal]) => {{
|
||||
keys.insert($var);
|
||||
$s.extra.get($var).ok_or_else(|| {
|
||||
MeliError::new(format!(
|
||||
"Configuration error ({}): NNTP connection requires the field `{}` set",
|
||||
$s.name.as_str(),
|
||||
$var
|
||||
))
|
||||
})
|
||||
}};
|
||||
($s:ident[$var:literal], $default:expr) => {{
|
||||
keys.insert($var);
|
||||
$s.extra
|
||||
.get($var)
|
||||
.map(|v| {
|
||||
<_>::from_str(v).map_err(|e| {
|
||||
MeliError::new(format!(
|
||||
"Configuration error ({}) NNTP: Invalid value for field `{}`: {}\n{}",
|
||||
$s.name.as_str(),
|
||||
$var,
|
||||
v,
|
||||
e
|
||||
))
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| Ok($default))
|
||||
}};
|
||||
}
|
||||
get_conf_val!(s["require_auth"], false)?;
|
||||
get_conf_val!(s["server_hostname"])?;
|
||||
get_conf_val!(s["server_username"], String::new())?;
|
||||
if !s.extra.contains_key("server_password_command") {
|
||||
|
@ -706,7 +518,7 @@ impl NntpType {
|
|||
)));
|
||||
}
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
get_conf_val!(s["use_deflate"], false)?;
|
||||
get_conf_val!(s["use_deflate"], true)?;
|
||||
#[cfg(not(feature = "deflate_compression"))]
|
||||
if s.extra.contains_key("use_deflate") {
|
||||
return Err(MeliError::new(format!(
|
||||
|
@ -715,18 +527,6 @@ impl NntpType {
|
|||
)));
|
||||
}
|
||||
get_conf_val!(s["danger_accept_invalid_certs"], false)?;
|
||||
let extra_keys = s
|
||||
.extra
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect::<HashSet<&str>>();
|
||||
let diff = extra_keys.difference(&keys).collect::<Vec<&&str>>();
|
||||
if !diff.is_empty() {
|
||||
return Err(MeliError::new(format!(
|
||||
"Configuration error ({}) NNTP: the following flags are set but are not recognized: {:?}.",
|
||||
s.name.as_str(), diff
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -736,7 +536,7 @@ impl NntpType {
|
|||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|c| c.clone())
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
}
|
||||
|
@ -778,9 +578,9 @@ impl FetchState {
|
|||
&uid_store.account_name, path, res
|
||||
)));
|
||||
}
|
||||
let total = usize::from_str(s[1]).unwrap_or(0);
|
||||
let _low = usize::from_str(s[2]).unwrap_or(0);
|
||||
let high = usize::from_str(s[3]).unwrap_or(0);
|
||||
let total = usize::from_str(&s[1]).unwrap_or(0);
|
||||
let _low = usize::from_str(&s[2]).unwrap_or(0);
|
||||
let high = usize::from_str(&s[3]).unwrap_or(0);
|
||||
*high_low_total = Some((high, _low, total));
|
||||
{
|
||||
let f = &uid_store.mailboxes.lock().await[&mailbox_hash];
|
||||
|
@ -792,11 +592,10 @@ impl FetchState {
|
|||
if high <= low {
|
||||
return Ok(None);
|
||||
}
|
||||
const CHUNK_SIZE: usize = 50000;
|
||||
const CHUNK_SIZE: usize = 100;
|
||||
let new_low = std::cmp::max(low, high.saturating_sub(CHUNK_SIZE));
|
||||
high_low_total.as_mut().unwrap().0 = new_low;
|
||||
|
||||
// FIXME: server might not implement OVER capability
|
||||
conn.send_command(format!("OVER {}-{}", new_low, high).as_bytes())
|
||||
.await?;
|
||||
conn.read_response(&mut res, true, command_to_replycodes("OVER"))
|
||||
|
@ -810,28 +609,19 @@ impl FetchState {
|
|||
let mut ret = Vec::with_capacity(high - new_low);
|
||||
//hash_index: Arc<Mutex<HashMap<EnvelopeHash, (UID, MailboxHash)>>>,
|
||||
//uid_index: Arc<Mutex<HashMap<(MailboxHash, UID), EnvelopeHash>>>,
|
||||
let mut latest_article: Option<crate::UnixTimestamp> = None;
|
||||
{
|
||||
let mut message_id_lck = uid_store.message_id_index.lock().unwrap();
|
||||
let mut hash_index_lck = uid_store.hash_index.lock().unwrap();
|
||||
let mut uid_index_lck = uid_store.uid_index.lock().unwrap();
|
||||
for l in res.split_rn().skip(1) {
|
||||
let (_, (num, env)) = protocol_parser::over_article(l)?;
|
||||
message_id_lck.insert(env.message_id_display().to_string(), env.hash());
|
||||
let (_, (num, env)) = protocol_parser::over_article(&l)?;
|
||||
hash_index_lck.insert(env.hash(), (num, mailbox_hash));
|
||||
uid_index_lck.insert((mailbox_hash, num), env.hash());
|
||||
if let Some(ref mut v) = latest_article {
|
||||
*v = std::cmp::max(*v, env.timestamp);
|
||||
} else {
|
||||
latest_article = Some(env.timestamp);
|
||||
}
|
||||
ret.push(env);
|
||||
}
|
||||
}
|
||||
{
|
||||
let hash_set: BTreeSet<EnvelopeHash> = ret.iter().map(|env| env.hash()).collect();
|
||||
let f = &uid_store.mailboxes.lock().await[&mailbox_hash];
|
||||
*f.latest_article.lock().unwrap() = latest_article;
|
||||
f.exists
|
||||
.lock()
|
||||
.unwrap()
|
||||
|
|
|
@ -45,7 +45,7 @@ impl Default for NntpExtensionUse {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
deflate: false,
|
||||
deflate: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,6 @@ pub struct NntpStream {
|
|||
pub stream: AsyncWrapper<Connection>,
|
||||
pub extension_use: NntpExtensionUse,
|
||||
pub current_mailbox: MailboxSelection,
|
||||
pub supports_submission: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
|
@ -101,7 +100,6 @@ impl NntpStream {
|
|||
stream,
|
||||
extension_use: server_conf.extension_use,
|
||||
current_mailbox: MailboxSelection::None,
|
||||
supports_submission: false,
|
||||
};
|
||||
|
||||
if server_conf.use_tls {
|
||||
|
@ -116,8 +114,6 @@ impl NntpStream {
|
|||
if server_conf.use_starttls {
|
||||
ret.read_response(&mut res, false, &["200 ", "201 "])
|
||||
.await?;
|
||||
ret.supports_submission = res.starts_with("200");
|
||||
|
||||
ret.send_command(b"CAPABILITIES").await?;
|
||||
ret.read_response(&mut res, true, command_to_replycodes("CAPABILITIES"))
|
||||
.await?;
|
||||
|
@ -197,6 +193,9 @@ impl NntpStream {
|
|||
.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!(
|
||||
|
@ -217,9 +216,6 @@ impl NntpStream {
|
|||
);
|
||||
}
|
||||
|
||||
ret.read_response(&mut res, false, &["200 ", "201 "])
|
||||
.await?;
|
||||
ret.supports_submission = res.starts_with("200");
|
||||
ret.send_command(b"CAPABILITIES").await?;
|
||||
ret.read_response(&mut res, true, command_to_replycodes("CAPABILITIES"))
|
||||
.await?;
|
||||
|
@ -229,8 +225,7 @@ impl NntpStream {
|
|||
&server_conf.server_hostname, res
|
||||
)));
|
||||
}
|
||||
let capabilities: HashSet<String> =
|
||||
res.lines().skip(1).map(|l| l.trim().to_string()).collect();
|
||||
let capabilities: HashSet<String> = res.lines().skip(1).map(|l| l.to_string()).collect();
|
||||
if !capabilities
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case("VERSION 2"))
|
||||
|
@ -240,12 +235,6 @@ impl NntpStream {
|
|||
&server_conf.server_hostname
|
||||
)));
|
||||
}
|
||||
if !capabilities
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case("POST"))
|
||||
{
|
||||
ret.supports_submission = false;
|
||||
}
|
||||
|
||||
if server_conf.require_auth {
|
||||
if capabilities.iter().any(|c| c.starts_with("AUTHINFO USER")) {
|
||||
|
@ -291,7 +280,6 @@ impl NntpStream {
|
|||
stream,
|
||||
extension_use,
|
||||
current_mailbox,
|
||||
supports_submission,
|
||||
} = ret;
|
||||
let stream = stream.into_inner()?;
|
||||
return Ok((
|
||||
|
@ -300,7 +288,6 @@ impl NntpStream {
|
|||
stream: AsyncWrapper::new(stream.deflate())?,
|
||||
extension_use,
|
||||
current_mailbox,
|
||||
supports_submission,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
@ -377,9 +364,6 @@ impl NntpStream {
|
|||
}
|
||||
|
||||
pub async fn send_command(&mut self, command: &[u8]) -> Result<()> {
|
||||
debug!("sending: {}", unsafe {
|
||||
std::str::from_utf8_unchecked(command)
|
||||
});
|
||||
if let Err(err) = try_await(async move {
|
||||
let command = command.trim();
|
||||
self.stream.write_all(command).await?;
|
||||
|
@ -399,24 +383,13 @@ impl NntpStream {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn send_multiline_data_block(&mut self, data: &[u8]) -> Result<()> {
|
||||
pub async fn send_multiline_data_block(&mut self, data: &str) -> Result<()> {
|
||||
if let Err(err) = try_await(async move {
|
||||
let mut ptr = 0;
|
||||
while let Some(pos) = data[ptr..].find("\n") {
|
||||
let l = &data[ptr..ptr + pos].trim_end();
|
||||
if l.starts_with(b".") {
|
||||
for l in data.lines() {
|
||||
if l.starts_with('.') {
|
||||
self.stream.write_all(b".").await?;
|
||||
}
|
||||
self.stream.write_all(l).await?;
|
||||
self.stream.write_all(b"\r\n").await?;
|
||||
ptr += pos + 1;
|
||||
}
|
||||
let l = &data[ptr..].trim_end();
|
||||
if !l.is_empty() {
|
||||
if l.starts_with(b".") {
|
||||
self.stream.write_all(b".").await?;
|
||||
}
|
||||
self.stream.write_all(l).await?;
|
||||
self.stream.write_all(l.as_bytes()).await?;
|
||||
self.stream.write_all(b"\r\n").await?;
|
||||
}
|
||||
self.stream.write_all(b".\r\n").await?;
|
||||
|
@ -550,7 +523,7 @@ impl NntpConnection {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_multiline_data_block(&mut self, message: &[u8]) -> Result<()> {
|
||||
pub async fn send_multiline_data_block(&mut self, message: &str) -> Result<()> {
|
||||
self.stream
|
||||
.as_mut()?
|
||||
.send_multiline_data_block(message)
|
||||
|
|
|
@ -22,7 +22,6 @@ use crate::backends::{
|
|||
BackendMailbox, LazyCountSet, Mailbox, MailboxHash, MailboxPermissions, SpecialUsageMailbox,
|
||||
};
|
||||
use crate::error::*;
|
||||
use crate::UnixTimestamp;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
|
@ -35,8 +34,6 @@ pub struct NntpMailbox {
|
|||
|
||||
pub exists: Arc<Mutex<LazyCountSet>>,
|
||||
pub unseen: Arc<Mutex<LazyCountSet>>,
|
||||
|
||||
pub latest_article: Arc<Mutex<Option<UnixTimestamp>>>,
|
||||
}
|
||||
|
||||
impl NntpMailbox {
|
||||
|
|
|
@ -67,6 +67,8 @@ pub use tags::*;
|
|||
mod thread;
|
||||
pub use thread::*;
|
||||
|
||||
mod watch;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DbConnection {
|
||||
pub lib: Arc<libloading::Library>,
|
||||
|
@ -232,7 +234,7 @@ unsafe impl Send for NotmuchDb {}
|
|||
unsafe impl Sync for NotmuchDb {}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct NotmuchMailbox {
|
||||
pub struct NotmuchMailbox {
|
||||
hash: MailboxHash,
|
||||
children: Vec<MailboxHash>,
|
||||
parent: Option<MailboxHash>,
|
||||
|
@ -307,11 +309,7 @@ impl NotmuchDb {
|
|||
_is_subscribed: Box<dyn Fn(&str) -> bool>,
|
||||
event_consumer: BackendEventConsumer,
|
||||
) -> Result<Box<dyn MailBackend>> {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let dlpath = "libnotmuch.so.5";
|
||||
#[cfg(target_os = "macos")]
|
||||
let dlpath = "libnotmuch.5.dylib";
|
||||
let lib = Arc::new(libloading::Library::new(dlpath)?);
|
||||
let lib = Arc::new(libloading::Library::new("libnotmuch.so.5")?);
|
||||
let path = Path::new(s.root_mailbox.as_str()).expand();
|
||||
if !path.exists() {
|
||||
return Err(MeliError::new(format!(
|
||||
|
@ -451,6 +449,82 @@ impl MailBackend for NotmuchDb {
|
|||
Ok(Box::pin(async { Ok(()) }))
|
||||
}
|
||||
|
||||
fn load(&mut self, mailbox_hash: MailboxHash) -> ResultFuture<()> {
|
||||
let database = NotmuchDb::new_connection(
|
||||
self.path.as_path(),
|
||||
self.revision_uuid.clone(),
|
||||
self.lib.clone(),
|
||||
false,
|
||||
)?;
|
||||
let mailboxes = self.mailboxes.clone();
|
||||
let index = self.index.clone();
|
||||
let mailbox_index = self.mailbox_index.clone();
|
||||
let collection = self.collection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
{
|
||||
//crate::connections::sleep(std::time::Duration::from_secs(2)).await;
|
||||
}
|
||||
let mailboxes_lck = mailboxes.read().unwrap();
|
||||
let mailbox = mailboxes_lck.get(&mailbox_hash).unwrap();
|
||||
let query: Query = Query::new(&database, mailbox.query_str.as_str())?;
|
||||
let mut unseen_total = 0;
|
||||
let mut mailbox_index_lck = mailbox_index.write().unwrap();
|
||||
let new_envelopes: HashMap<EnvelopeHash, Envelope> = query
|
||||
.search()?
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
let env = m.into_envelope(&index, &collection.tag_index);
|
||||
mailbox_index_lck
|
||||
.entry(env.hash())
|
||||
.or_default()
|
||||
.push(mailbox_hash);
|
||||
if !env.is_seen() {
|
||||
unseen_total += 1;
|
||||
}
|
||||
(env.hash(), env)
|
||||
})
|
||||
.collect();
|
||||
{
|
||||
let mut total_lck = mailbox.total.lock().unwrap();
|
||||
let mut unseen_lck = mailbox.unseen.lock().unwrap();
|
||||
*total_lck = new_envelopes.len();
|
||||
*unseen_lck = unseen_total;
|
||||
}
|
||||
collection.merge(new_envelopes, mailbox_hash, None);
|
||||
let mut envelopes_lck = collection.envelopes.write().unwrap();
|
||||
envelopes_lck.retain(|&k, _| k % 2 == 0);
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn fetch_batch(&mut self, env_hashes: EnvelopeHashBatch) -> ResultFuture<()> {
|
||||
let database = NotmuchDb::new_connection(
|
||||
self.path.as_path(),
|
||||
self.revision_uuid.clone(),
|
||||
self.lib.clone(),
|
||||
false,
|
||||
)?;
|
||||
let index = self.index.clone();
|
||||
let collection = self.collection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
//crate::connections::sleep(std::time::Duration::from_secs(2)).await;
|
||||
debug!("fetch_batch {:?}", &env_hashes);
|
||||
let mut envelopes_lck = collection.envelopes.write().unwrap();
|
||||
for env_hash in env_hashes.iter() {
|
||||
if envelopes_lck.contains_key(&env_hash) {
|
||||
continue;
|
||||
}
|
||||
let index_lck = index.write().unwrap();
|
||||
let message = Message::find_message(&database, &index_lck[&env_hash])?;
|
||||
drop(index_lck);
|
||||
let env = message.into_envelope(&index, &collection.tag_index);
|
||||
envelopes_lck.insert(env_hash, env);
|
||||
}
|
||||
debug!("fetch_batch {:?} done", &env_hashes);
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn fetch(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
|
@ -585,50 +659,29 @@ impl MailBackend for NotmuchDb {
|
|||
}))
|
||||
}
|
||||
|
||||
fn watch(&self) -> ResultFuture<()> {
|
||||
extern crate notify;
|
||||
use notify::{watcher, RecursiveMode, Watcher};
|
||||
|
||||
fn watcher(&self) -> Result<Box<dyn BackendWatcher>> {
|
||||
let account_hash = self.account_hash;
|
||||
let collection = self.collection.clone();
|
||||
let event_consumer = self.event_consumer.clone();
|
||||
let index = self.index.clone();
|
||||
let lib = self.lib.clone();
|
||||
let mailbox_index = self.mailbox_index.clone();
|
||||
let mailboxes = self.mailboxes.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 event_consumer = self.event_consumer.clone();
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut watcher = watcher(tx, std::time::Duration::from_secs(2)).unwrap();
|
||||
watcher.watch(&self.path, RecursiveMode::Recursive).unwrap();
|
||||
Ok(Box::pin(async move {
|
||||
let _watcher = watcher;
|
||||
let rx = rx;
|
||||
loop {
|
||||
let _ = rx.recv().map_err(|err| err.to_string())?;
|
||||
{
|
||||
let mut database = NotmuchDb::new_connection(
|
||||
path.as_path(),
|
||||
revision_uuid.clone(),
|
||||
lib.clone(),
|
||||
false,
|
||||
)?;
|
||||
let new_revision_uuid = database.get_revision_uuid();
|
||||
if new_revision_uuid > *database.revision_uuid.read().unwrap() {
|
||||
database.refresh(
|
||||
mailboxes.clone(),
|
||||
index.clone(),
|
||||
mailbox_index.clone(),
|
||||
collection.tag_index.clone(),
|
||||
account_hash.clone(),
|
||||
event_consumer.clone(),
|
||||
new_revision_uuid,
|
||||
)?;
|
||||
*revision_uuid.write().unwrap() = new_revision_uuid;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Box::new(watch::NotmuchWatcher {
|
||||
account_hash,
|
||||
collection,
|
||||
event_consumer,
|
||||
index,
|
||||
lib,
|
||||
mailbox_hashes: BTreeSet::default(),
|
||||
mailbox_index,
|
||||
mailboxes,
|
||||
path,
|
||||
polling_period: std::time::Duration::from_secs(3),
|
||||
revision_uuid,
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -1055,18 +1108,18 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
|
|||
ret.push_str("tag:attachment");
|
||||
}
|
||||
And(q1, q2) => {
|
||||
ret.push('(');
|
||||
ret.push_str("(");
|
||||
q1.query_to_string(ret);
|
||||
ret.push_str(") AND (");
|
||||
q2.query_to_string(ret);
|
||||
ret.push(')');
|
||||
ret.push_str(")");
|
||||
}
|
||||
Or(q1, q2) => {
|
||||
ret.push('(');
|
||||
ret.push_str("(");
|
||||
q1.query_to_string(ret);
|
||||
ret.push_str(") OR (");
|
||||
q2.query_to_string(ret);
|
||||
ret.push(')');
|
||||
ret.push_str(")");
|
||||
}
|
||||
Not(q) => {
|
||||
ret.push_str("(NOT (");
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
*/
|
||||
|
||||
use super::*;
|
||||
use crate::thread::{ThreadHash, ThreadNode, ThreadNodeHash};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Message<'m> {
|
||||
|
@ -188,22 +187,6 @@ impl<'m> Message<'m> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn into_thread_node(&self) -> (ThreadNodeHash, ThreadNode) {
|
||||
(
|
||||
ThreadNodeHash::from(self.msg_id()),
|
||||
ThreadNode {
|
||||
message: Some(self.env_hash()),
|
||||
parent: None,
|
||||
other_mailbox: false,
|
||||
children: vec![],
|
||||
date: self.date(),
|
||||
show_subject: true,
|
||||
group: ThreadHash::new(),
|
||||
unseen: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn add_tag(&self, tag: &CStr) -> Result<()> {
|
||||
if let Err(err) = unsafe {
|
||||
try_call!(
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* meli - notmuch backend
|
||||
*
|
||||
* Copyright 2019 - 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::*;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NotmuchWatcher {
|
||||
pub account_hash: AccountHash,
|
||||
pub collection: Collection,
|
||||
pub event_consumer: BackendEventConsumer,
|
||||
pub index: Arc<RwLock<HashMap<EnvelopeHash, CString>>>,
|
||||
pub lib: Arc<libloading::Library>,
|
||||
pub mailbox_hashes: BTreeSet<MailboxHash>,
|
||||
pub mailbox_index: Arc<RwLock<HashMap<EnvelopeHash, SmallVec<[MailboxHash; 16]>>>>,
|
||||
pub mailboxes: Arc<RwLock<HashMap<MailboxHash, NotmuchMailbox>>>,
|
||||
pub path: PathBuf,
|
||||
pub polling_period: std::time::Duration,
|
||||
pub revision_uuid: Arc<RwLock<u64>>,
|
||||
}
|
||||
|
||||
impl BackendWatcher for NotmuchWatcher {
|
||||
fn is_blocking(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn register_mailbox(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
_urgency: MailboxWatchUrgency,
|
||||
) -> Result<()> {
|
||||
self.mailbox_hashes.insert(mailbox_hash);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_polling_period(&mut self, period: Option<std::time::Duration>) -> Result<()> {
|
||||
if let Some(period) = period {
|
||||
self.polling_period = period;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn(self: Box<Self>) -> ResultFuture<()> {
|
||||
Ok(Box::pin(async move {
|
||||
extern crate notify;
|
||||
use notify::{watcher, RecursiveMode, Watcher};
|
||||
let NotmuchWatcher {
|
||||
account_hash,
|
||||
collection,
|
||||
event_consumer,
|
||||
index,
|
||||
lib,
|
||||
mailbox_hashes: _,
|
||||
mailbox_index,
|
||||
mailboxes,
|
||||
path,
|
||||
polling_period,
|
||||
revision_uuid,
|
||||
} = *self;
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let mut watcher = watcher(tx, polling_period).unwrap();
|
||||
watcher.watch(&path, RecursiveMode::Recursive).unwrap();
|
||||
loop {
|
||||
let _ = rx.recv().map_err(|err| err.to_string())?;
|
||||
{
|
||||
let mut database = NotmuchDb::new_connection(
|
||||
path.as_path(),
|
||||
revision_uuid.clone(),
|
||||
lib.clone(),
|
||||
false,
|
||||
)?;
|
||||
let new_revision_uuid = database.get_revision_uuid();
|
||||
if new_revision_uuid > *database.revision_uuid.read().unwrap() {
|
||||
database.refresh(
|
||||
mailboxes.clone(),
|
||||
index.clone(),
|
||||
mailbox_index.clone(),
|
||||
collection.tag_index.clone(),
|
||||
account_hash.clone(),
|
||||
event_consumer.clone(),
|
||||
new_revision_uuid,
|
||||
)?;
|
||||
*revision_uuid.write().unwrap() = new_revision_uuid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
|
@ -27,8 +27,37 @@ use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
|||
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
|
||||
pub type EnvelopeRef<'g> = RwRef<'g, EnvelopeHash, Envelope>;
|
||||
pub type EnvelopeRefMut<'g> = RwRefMut<'g, EnvelopeHash, Envelope>;
|
||||
pub struct EnvelopeRef<'g> {
|
||||
guard: RwLockReadGuard<'g, HashMap<EnvelopeHash, Envelope>>,
|
||||
env_hash: EnvelopeHash,
|
||||
}
|
||||
|
||||
impl Deref for EnvelopeRef<'_> {
|
||||
type Target = Envelope;
|
||||
|
||||
fn deref(&self) -> &Envelope {
|
||||
self.guard.get(&self.env_hash).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EnvelopeRefMut<'g> {
|
||||
guard: RwLockWriteGuard<'g, HashMap<EnvelopeHash, Envelope>>,
|
||||
env_hash: EnvelopeHash,
|
||||
}
|
||||
|
||||
impl Deref for EnvelopeRefMut<'_> {
|
||||
type Target = Envelope;
|
||||
|
||||
fn deref(&self) -> &Envelope {
|
||||
self.guard.get(&self.env_hash).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for EnvelopeRefMut<'_> {
|
||||
fn deref_mut(&mut self) -> &mut Envelope {
|
||||
self.guard.get_mut(&self.env_hash).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Collection {
|
||||
|
@ -427,14 +456,14 @@ impl Collection {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_env(&'_ self, hash: EnvelopeHash) -> EnvelopeRef<'_> {
|
||||
pub fn get_env(&'_ self, env_hash: EnvelopeHash) -> EnvelopeRef<'_> {
|
||||
let guard: RwLockReadGuard<'_, _> = self.envelopes.read().unwrap();
|
||||
EnvelopeRef { guard, hash }
|
||||
EnvelopeRef { guard, env_hash }
|
||||
}
|
||||
|
||||
pub fn get_env_mut(&'_ self, hash: EnvelopeHash) -> EnvelopeRefMut<'_> {
|
||||
pub fn get_env_mut(&'_ self, env_hash: EnvelopeHash) -> EnvelopeRefMut<'_> {
|
||||
let guard = self.envelopes.write().unwrap();
|
||||
EnvelopeRefMut { guard, hash }
|
||||
EnvelopeRefMut { guard, env_hash }
|
||||
}
|
||||
|
||||
pub fn get_threads(&'_ self, hash: MailboxHash) -> RwRef<'_, MailboxHash, Threads> {
|
||||
|
@ -478,22 +507,3 @@ impl<K: std::cmp::Eq + std::hash::Hash, V> Deref for RwRef<'_, K, V> {
|
|||
self.guard.get(&self.hash).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RwRefMut<'g, K: std::cmp::Eq + std::hash::Hash, V> {
|
||||
guard: RwLockWriteGuard<'g, HashMap<K, V>>,
|
||||
hash: K,
|
||||
}
|
||||
|
||||
impl<K: std::cmp::Eq + std::hash::Hash, V> DerefMut for RwRefMut<'_, K, V> {
|
||||
fn deref_mut(&mut self) -> &mut V {
|
||||
self.guard.get_mut(&self.hash).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: std::cmp::Eq + std::hash::Hash, V> Deref for RwRefMut<'_, K, V> {
|
||||
type Target = V;
|
||||
|
||||
fn deref(&self) -> &V {
|
||||
self.guard.get(&self.hash).unwrap()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,8 +74,8 @@ impl AccountSettings {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MailboxConf {
|
||||
#[serde(alias = "rename")]
|
||||
pub alias: Option<String>,
|
||||
|
@ -87,8 +87,6 @@ pub struct MailboxConf {
|
|||
pub ignore: ToggleFlag,
|
||||
#[serde(default = "none")]
|
||||
pub usage: Option<SpecialUsageMailbox>,
|
||||
#[serde(default = "none")]
|
||||
pub sort_order: Option<usize>,
|
||||
#[serde(flatten)]
|
||||
pub extra: HashMap<String, String>,
|
||||
}
|
||||
|
@ -101,7 +99,6 @@ impl Default for MailboxConf {
|
|||
subscribe: ToggleFlag::Unset,
|
||||
ignore: ToggleFlag::Unset,
|
||||
usage: None,
|
||||
sort_order: None,
|
||||
extra: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
@ -125,46 +122,6 @@ pub fn none<T>() -> Option<T> {
|
|||
None
|
||||
}
|
||||
|
||||
macro_rules! named_unit_variant {
|
||||
($variant:ident) => {
|
||||
pub mod $variant {
|
||||
/*
|
||||
pub fn serialize<S>(serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(stringify!($variant))
|
||||
}
|
||||
*/
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<(), D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct V;
|
||||
impl<'de> serde::de::Visitor<'de> for V {
|
||||
type Value = ();
|
||||
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.write_str(concat!("\"", stringify!($variant), "\""))
|
||||
}
|
||||
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
|
||||
if value == stringify!($variant) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(E::invalid_value(serde::de::Unexpected::Str(value), &self))
|
||||
}
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_str(V)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mod strings {
|
||||
named_unit_variant!(ask);
|
||||
}
|
||||
|
||||
#[derive(Copy, Debug, Clone, PartialEq)]
|
||||
pub enum ToggleFlag {
|
||||
Unset,
|
||||
|
@ -234,25 +191,17 @@ impl<'de> Deserialize<'de> for ToggleFlag {
|
|||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum InnerToggleFlag {
|
||||
Bool(bool),
|
||||
#[serde(with = "strings::ask")]
|
||||
Ask,
|
||||
}
|
||||
let s = <InnerToggleFlag>::deserialize(deserializer);
|
||||
Ok(
|
||||
match s.map_err(|err| {
|
||||
serde::de::Error::custom(format!(
|
||||
let s = <String>::deserialize(deserializer);
|
||||
Ok(match s? {
|
||||
s if s.eq_ignore_ascii_case("true") => ToggleFlag::True,
|
||||
s if s.eq_ignore_ascii_case("false") => ToggleFlag::False,
|
||||
s if s.eq_ignore_ascii_case("ask") => ToggleFlag::Ask,
|
||||
s => {
|
||||
return Err(serde::de::Error::custom(format!(
|
||||
r#"expected one of "true", "false", "ask", found `{}`"#,
|
||||
err
|
||||
))
|
||||
})? {
|
||||
InnerToggleFlag::Bool(true) => ToggleFlag::True,
|
||||
InnerToggleFlag::Bool(false) => ToggleFlag::False,
|
||||
InnerToggleFlag::Ask => ToggleFlag::Ask,
|
||||
},
|
||||
)
|
||||
s
|
||||
)))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,6 @@ use std::ffi::{CStr, CString};
|
|||
pub type UnixTimestamp = u64;
|
||||
pub const RFC3339_FMT_WITH_TIME: &str = "%Y-%m-%dT%H:%M:%S\0";
|
||||
pub const RFC3339_FMT: &str = "%Y-%m-%d\0";
|
||||
pub const RFC822_DATE: &str = "%a, %d %b %Y %H:%M:%S %z\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";
|
||||
|
@ -73,34 +72,22 @@ extern "C" {
|
|||
fn gettimeofday(tv: *mut libc::timeval, tz: *mut libc::timezone) -> i32;
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "netbsd"))]
|
||||
struct Locale {
|
||||
new_locale: libc::locale_t,
|
||||
old_locale: libc::locale_t,
|
||||
}
|
||||
#[cfg(target_os = "netbsd")]
|
||||
struct Locale {
|
||||
mask: std::os::raw::c_int,
|
||||
old_locale: *const std::os::raw::c_char,
|
||||
}
|
||||
|
||||
impl Drop for Locale {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(not(target_os = "netbsd"))]
|
||||
unsafe {
|
||||
let _ = libc::uselocale(self.old_locale);
|
||||
libc::freelocale(self.new_locale);
|
||||
}
|
||||
#[cfg(target_os = "netbsd")]
|
||||
unsafe {
|
||||
let _ = libc::setlocale(self.mask, self.old_locale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// How to unit test this? Test machine is not guaranteed to have non-english locales.
|
||||
impl Locale {
|
||||
#[cfg(not(target_os = "netbsd"))]
|
||||
fn new(
|
||||
mask: std::os::raw::c_int,
|
||||
locale: *const std::os::raw::c_char,
|
||||
|
@ -120,22 +107,6 @@ impl Locale {
|
|||
old_locale,
|
||||
})
|
||||
}
|
||||
#[cfg(target_os = "netbsd")]
|
||||
fn new(
|
||||
mask: std::os::raw::c_int,
|
||||
locale: *const std::os::raw::c_char,
|
||||
_base: libc::locale_t,
|
||||
) -> Result<Self> {
|
||||
let old_locale = unsafe { libc::setlocale(mask, std::ptr::null_mut()) };
|
||||
if old_locale.is_null() {
|
||||
return Err(nix::Error::last().into());
|
||||
}
|
||||
let new_locale = unsafe { libc::setlocale(mask, locale) };
|
||||
if new_locale.is_null() {
|
||||
return Err(nix::Error::last().into());
|
||||
}
|
||||
Ok(Locale { mask, old_locale })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn timestamp_to_string(timestamp: UnixTimestamp, fmt: Option<&str>, posix: bool) -> String {
|
||||
|
@ -193,7 +164,7 @@ 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;
|
||||
if !(0..12).contains(&month) {
|
||||
if month >= 12 || month < 0 {
|
||||
let mut adj = month / 12;
|
||||
month %= 12;
|
||||
if month < 0 {
|
||||
|
@ -226,7 +197,9 @@ fn year_to_secs(year: i64, is_leap: &mut bool) -> std::result::Result<i64, ()> {
|
|||
} else {
|
||||
*is_leap = false;
|
||||
}
|
||||
return Ok(31536000 * (y - 70) + 86400 * leaps);
|
||||
return Ok((31536000 * (y - 70) + 86400 * leaps)
|
||||
.try_into()
|
||||
.unwrap_or(0));
|
||||
}
|
||||
|
||||
let cycles = (year - 100) / 400;
|
||||
|
|
|
@ -488,14 +488,6 @@ impl Envelope {
|
|||
self.to.as_slice()
|
||||
}
|
||||
|
||||
pub fn cc(&self) -> &[Address] {
|
||||
self.cc.as_slice()
|
||||
}
|
||||
|
||||
pub fn bcc(&self) -> &[Address] {
|
||||
self.bcc.as_slice()
|
||||
}
|
||||
|
||||
pub fn field_to_to_string(&self) -> String {
|
||||
if self.to.is_empty() {
|
||||
self.other_headers
|
||||
|
|
|
@ -323,14 +323,12 @@ impl Hash for Address {
|
|||
impl core::fmt::Display for Address {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
match self {
|
||||
Address::Mailbox(m) if m.display_name.length > 0 => {
|
||||
match m.display_name.display(&m.raw) {
|
||||
d if d.contains(".") || d.contains(",") => {
|
||||
write!(f, "\"{}\" <{}>", d, m.address_spec.display(&m.raw))
|
||||
}
|
||||
d => write!(f, "{} <{}>", d, m.address_spec.display(&m.raw)),
|
||||
}
|
||||
}
|
||||
Address::Mailbox(m) if m.display_name.length > 0 => write!(
|
||||
f,
|
||||
"{} <{}>",
|
||||
m.display_name.display(&m.raw),
|
||||
m.address_spec.display(&m.raw)
|
||||
),
|
||||
Address::Group(g) => {
|
||||
let attachment_strings: Vec<String> =
|
||||
g.mailbox_list.iter().map(|a| format!("{}", a)).collect();
|
||||
|
|
|
@ -48,7 +48,6 @@ pub enum Charset {
|
|||
Windows1253,
|
||||
GBK,
|
||||
GB2312,
|
||||
GB18030,
|
||||
BIG5,
|
||||
ISO2022JP,
|
||||
EUCJP,
|
||||
|
@ -144,9 +143,6 @@ impl<'a> From<&'a [u8]> for Charset {
|
|||
Charset::Windows1253
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"gbk") => Charset::GBK,
|
||||
b if b.eq_ignore_ascii_case(b"gb18030") || b.eq_ignore_ascii_case(b"gb-18030") => {
|
||||
Charset::GB18030
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"gb2312") || b.eq_ignore_ascii_case(b"gb-2312") => {
|
||||
Charset::GB2312
|
||||
}
|
||||
|
@ -188,7 +184,6 @@ impl Display for Charset {
|
|||
Charset::Windows1253 => write!(f, "windows-1253"),
|
||||
Charset::GBK => write!(f, "gbk"),
|
||||
Charset::GB2312 => write!(f, "gb2312"),
|
||||
Charset::GB18030 => write!(f, "gb18030"),
|
||||
Charset::BIG5 => write!(f, "big5"),
|
||||
Charset::ISO2022JP => write!(f, "iso-2022-jp"),
|
||||
Charset::EUCJP => write!(f, "euc-jp"),
|
||||
|
@ -564,12 +559,3 @@ impl From<&[u8]> for ContentDisposition {
|
|||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ContentDispositionKind> for ContentDisposition {
|
||||
fn from(kind: ContentDispositionKind) -> ContentDisposition {
|
||||
ContentDisposition {
|
||||
kind,
|
||||
..ContentDisposition::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -804,7 +804,7 @@ pub fn interpret_format_flowed(_t: &str) -> String {
|
|||
unimplemented!()
|
||||
}
|
||||
|
||||
type Filter<'a> = Box<dyn FnMut(&Attachment, &mut Vec<u8>) + 'a>;
|
||||
type Filter<'a> = Box<dyn FnMut(&Attachment, &mut Vec<u8>) -> () + 'a>;
|
||||
|
||||
fn decode_rec_helper<'a, 'b>(a: &'a Attachment, filter: &mut Option<Filter<'b>>) -> Vec<u8> {
|
||||
match a.content_type {
|
||||
|
|
|
@ -53,11 +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(),
|
||||
Some(crate::datetime::RFC822_DATE),
|
||||
true,
|
||||
),
|
||||
crate::datetime::timestamp_to_string(crate::datetime::now(), None, true),
|
||||
);
|
||||
headers.insert(HeaderName::new_unchecked("From"), "".into());
|
||||
headers.insert(HeaderName::new_unchecked("To"), "".into());
|
||||
|
@ -144,6 +140,22 @@ impl Draft {
|
|||
if let Some(reply_to) = envelope.other_headers().get("Mail-Followup-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::new_unchecked("To"), reply_to.to_string());
|
||||
} else {
|
||||
if let Some(reply_to) = envelope.other_headers().get("Reply-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::new_unchecked("To"), reply_to.to_string());
|
||||
} else {
|
||||
ret.headers_mut().insert(
|
||||
HeaderName::new_unchecked("To"),
|
||||
envelope.field_from_to_string(),
|
||||
);
|
||||
}
|
||||
// FIXME: add To/Cc
|
||||
}
|
||||
} else {
|
||||
if let Some(reply_to) = envelope.other_headers().get("Mail-Reply-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::new_unchecked("To"), reply_to.to_string());
|
||||
} else if let Some(reply_to) = envelope.other_headers().get("Reply-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::new_unchecked("To"), reply_to.to_string());
|
||||
|
@ -153,18 +165,6 @@ impl Draft {
|
|||
envelope.field_from_to_string(),
|
||||
);
|
||||
}
|
||||
// FIXME: add To/Cc
|
||||
} else if let Some(reply_to) = envelope.other_headers().get("Mail-Reply-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::new_unchecked("To"), reply_to.to_string());
|
||||
} else if let Some(reply_to) = envelope.other_headers().get("Reply-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::new_unchecked("To"), reply_to.to_string());
|
||||
} else {
|
||||
ret.headers_mut().insert(
|
||||
HeaderName::new_unchecked("To"),
|
||||
envelope.field_from_to_string(),
|
||||
);
|
||||
}
|
||||
ret.headers_mut().insert(
|
||||
HeaderName::new_unchecked("Cc"),
|
||||
|
|
|
@ -1899,9 +1899,6 @@ pub mod encodings {
|
|||
Charset::GB2312 => {
|
||||
Ok(encoding::codec::simpchinese::GBK_ENCODING.decode(s, DecoderTrap::Strict)?)
|
||||
}
|
||||
Charset::GB18030 => Ok(
|
||||
encoding::codec::simpchinese::GB18030_ENCODING.decode(s, DecoderTrap::Strict)?
|
||||
),
|
||||
Charset::UTF16 => {
|
||||
Ok(encoding::codec::utf_16::UTF_16LE_ENCODING.decode(s, DecoderTrap::Strict)?)
|
||||
}
|
||||
|
@ -2555,12 +2552,6 @@ mod tests {
|
|||
"Re: Climate crisis reality check –\u{a0}EcoHustler",
|
||||
std::str::from_utf8(&phrase(words.as_bytes(), false).unwrap().1).unwrap()
|
||||
);
|
||||
|
||||
let words = r#"=?gb18030?B?zNrRtsbz0rXTys/k19S2r9eqt6LR6dak08q8/g==?="#;
|
||||
assert_eq!(
|
||||
"腾讯企业邮箱自动转发验证邮件",
|
||||
std::str::from_utf8(&phrase(words.as_bytes(), false).unwrap().1).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -42,9 +42,6 @@ pub enum ErrorKind {
|
|||
Bug,
|
||||
Network,
|
||||
Timeout,
|
||||
OSError,
|
||||
NotImplemented,
|
||||
NotSupported,
|
||||
}
|
||||
|
||||
impl fmt::Display for ErrorKind {
|
||||
|
@ -59,9 +56,6 @@ impl fmt::Display for ErrorKind {
|
|||
ErrorKind::Bug => "Bug, please report this!",
|
||||
ErrorKind::Network => "Network",
|
||||
ErrorKind::Timeout => "Timeout",
|
||||
ErrorKind::OSError => "OS Error",
|
||||
ErrorKind::NotImplemented => "Not implemented",
|
||||
ErrorKind::NotSupported => "Not supported",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -214,7 +208,6 @@ impl From<io::Error> for MeliError {
|
|||
MeliError::new(kind.to_string())
|
||||
.set_summary(format!("{:?}", kind.kind()))
|
||||
.set_source(Some(Arc::new(kind)))
|
||||
.set_kind(ErrorKind::OSError)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -181,7 +181,6 @@ pub mod shellexpand {
|
|||
use smallvec::SmallVec;
|
||||
use std::ffi::OsStr;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
#[cfg(not(target_os = "netbsd"))]
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
|
|
|
@ -616,7 +616,7 @@ impl SmtpConnection {
|
|||
let envelope = Envelope::from_bytes(mail.as_bytes(), None)
|
||||
.chain_err_summary(|| "SMTP submission was aborted")?;
|
||||
let tos = tos.unwrap_or_else(|| envelope.to());
|
||||
if tos.is_empty() && envelope.cc().is_empty() && envelope.bcc().is_empty() {
|
||||
if tos.is_empty() {
|
||||
return Err(MeliError::new("SMTP submission was aborted because there was no e-mail address found in the To: header field. Consider adding recipients."));
|
||||
}
|
||||
let mut current_command: SmallVec<[&[u8]; 16]> = SmallVec::new();
|
||||
|
@ -651,11 +651,7 @@ impl SmtpConnection {
|
|||
//return a reply indicating whether the failure is permanent (i.e., will occur again if
|
||||
//the client tries to send the same address again) or temporary (i.e., the address might
|
||||
//be accepted if the client tries again later).
|
||||
for addr in tos
|
||||
.into_iter()
|
||||
.chain(envelope.cc().into_iter())
|
||||
.chain(envelope.bcc().into_iter())
|
||||
{
|
||||
for addr in tos {
|
||||
current_command.clear();
|
||||
current_command.push(b"RCPT TO:<");
|
||||
current_command.push(addr.address_spec_raw().trim());
|
||||
|
|
|
@ -35,11 +35,11 @@ extern crate unicode_segmentation;
|
|||
use self::unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
pub trait TextProcessing: UnicodeSegmentation + CodePointsIter {
|
||||
fn split_graphemes(&self) -> Vec<&str> {
|
||||
fn split_graphemes<'a>(&'a self) -> Vec<&'a str> {
|
||||
UnicodeSegmentation::graphemes(self, true).collect::<Vec<&str>>()
|
||||
}
|
||||
|
||||
fn graphemes_indices(&self) -> Vec<(usize, &str)> {
|
||||
fn graphemes_indices<'a>(&'a self) -> Vec<(usize, &'a str)> {
|
||||
UnicodeSegmentation::grapheme_indices(self, true).collect::<Vec<(usize, &str)>>()
|
||||
}
|
||||
|
||||
|
|
|
@ -128,7 +128,7 @@ trait EvenAfterSpaces {
|
|||
impl EvenAfterSpaces for str {
|
||||
fn even_after_spaces(&self) -> &Self {
|
||||
let mut ret = self;
|
||||
while !ret.is_empty() && get_class!(ret) != SP {
|
||||
while !ret.is_empty() && get_class!(&ret) != SP {
|
||||
ret = &ret[get_base_character!(ret).unwrap().len_utf8()..];
|
||||
}
|
||||
ret
|
||||
|
@ -158,7 +158,7 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
}
|
||||
$last_break = $pos;
|
||||
};
|
||||
}
|
||||
};
|
||||
// After end of text, there are no breaks.
|
||||
if self.pos > self.text.len() {
|
||||
return None;
|
||||
|
@ -173,7 +173,7 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
|
||||
let LineBreakCandidateIter {
|
||||
ref mut iter,
|
||||
text,
|
||||
ref text,
|
||||
ref mut reg_ind_streak,
|
||||
ref mut break_now,
|
||||
ref mut last_break,
|
||||
|
@ -996,8 +996,8 @@ mod alg {
|
|||
let mut p_i = 0;
|
||||
while j > 0 {
|
||||
let mut line = String::new();
|
||||
for word in words.iter().take(j).skip(breaks[j]) {
|
||||
line.push_str(word);
|
||||
for i in breaks[j]..j {
|
||||
line.push_str(words[i]);
|
||||
}
|
||||
lines.push(line);
|
||||
if p_i + 1 < paragraphs {
|
||||
|
@ -1110,7 +1110,7 @@ pub fn split_lines_reflow(text: &str, reflow: Reflow, width: Option<usize>) -> V
|
|||
for (idx, _g) in UnicodeSegmentation::grapheme_indices(line, true) {
|
||||
t[idx] = 1;
|
||||
}
|
||||
Box::new(segment_tree::SegmentTree::new(t))
|
||||
segment_tree::SegmentTree::new(t)
|
||||
};
|
||||
|
||||
let mut prev = 0;
|
||||
|
@ -1246,6 +1246,7 @@ easy to take MORE than nothing.'"#;
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
mod segment_tree {
|
||||
/*! Simple segment tree implementation for maximum in range queries. This is useful if given an
|
||||
* array of numbers you want to get the maximum value inside an interval quickly.
|
||||
|
@ -1342,17 +1343,17 @@ pub struct LineBreakText {
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
enum ReflowState {
|
||||
No {
|
||||
ReflowNo {
|
||||
cur_index: usize,
|
||||
},
|
||||
AllWidth {
|
||||
ReflowAllWidth {
|
||||
width: usize,
|
||||
state: LineBreakTextState,
|
||||
},
|
||||
All {
|
||||
ReflowAll {
|
||||
cur_index: usize,
|
||||
},
|
||||
FormatFlowed {
|
||||
ReflowFormatFlowed {
|
||||
cur_index: usize,
|
||||
},
|
||||
}
|
||||
|
@ -1360,13 +1361,13 @@ enum ReflowState {
|
|||
impl ReflowState {
|
||||
fn new(reflow: Reflow, width: Option<usize>, cur_index: usize) -> ReflowState {
|
||||
match reflow {
|
||||
Reflow::All if width.is_some() => ReflowState::AllWidth {
|
||||
Reflow::All if width.is_some() => ReflowState::ReflowAllWidth {
|
||||
width: width.unwrap(),
|
||||
state: LineBreakTextState::AtLine { cur_index },
|
||||
},
|
||||
Reflow::All => ReflowState::All { cur_index },
|
||||
Reflow::FormatFlowed => ReflowState::FormatFlowed { cur_index },
|
||||
Reflow::No => ReflowState::No { cur_index },
|
||||
Reflow::All => ReflowState::ReflowAll { cur_index },
|
||||
Reflow::FormatFlowed => ReflowState::ReflowFormatFlowed { cur_index },
|
||||
Reflow::No => ReflowState::ReflowNo { cur_index },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1382,7 +1383,7 @@ enum LineBreakTextState {
|
|||
within_line_index: usize,
|
||||
breaks: Vec<(usize, LineBreakCandidate)>,
|
||||
prev_break: usize,
|
||||
segment_tree: Box<segment_tree::SegmentTree>,
|
||||
segment_tree: segment_tree::SegmentTree,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -1436,14 +1437,14 @@ impl LineBreakText {
|
|||
|
||||
pub fn is_finished(&self) -> bool {
|
||||
match self.state {
|
||||
ReflowState::No { cur_index }
|
||||
| ReflowState::All { cur_index }
|
||||
| ReflowState::FormatFlowed { cur_index }
|
||||
| ReflowState::AllWidth {
|
||||
ReflowState::ReflowNo { cur_index }
|
||||
| ReflowState::ReflowAll { cur_index }
|
||||
| ReflowState::ReflowFormatFlowed { cur_index }
|
||||
| ReflowState::ReflowAllWidth {
|
||||
width: _,
|
||||
state: LineBreakTextState::AtLine { cur_index },
|
||||
} => cur_index >= self.text.len(),
|
||||
ReflowState::AllWidth {
|
||||
ReflowState::ReflowAllWidth {
|
||||
width: _,
|
||||
state: LineBreakTextState::WithinLine { .. },
|
||||
} => false,
|
||||
|
@ -1461,7 +1462,7 @@ impl Iterator for LineBreakText {
|
|||
return None;
|
||||
}
|
||||
match self.state {
|
||||
ReflowState::FormatFlowed { ref mut cur_index } => {
|
||||
ReflowState::ReflowFormatFlowed { ref mut cur_index } => {
|
||||
/* rfc3676 - The Text/Plain Format and DelSp Parameters
|
||||
* https://tools.ietf.org/html/rfc3676 */
|
||||
|
||||
|
@ -1575,7 +1576,7 @@ impl Iterator for LineBreakText {
|
|||
}
|
||||
return self.paragraph.pop_front();
|
||||
}
|
||||
ReflowState::AllWidth {
|
||||
ReflowState::ReflowAllWidth {
|
||||
width,
|
||||
ref mut state,
|
||||
} => {
|
||||
|
@ -1624,7 +1625,7 @@ impl Iterator for LineBreakText {
|
|||
{
|
||||
t[idx] = 1;
|
||||
}
|
||||
Box::new(segment_tree::SegmentTree::new(t))
|
||||
segment_tree::SegmentTree::new(t)
|
||||
},
|
||||
};
|
||||
if let LineBreakTextState::WithinLine {
|
||||
|
@ -1740,8 +1741,9 @@ impl Iterator for LineBreakText {
|
|||
};
|
||||
}
|
||||
}
|
||||
ReflowState::No { ref mut cur_index } | ReflowState::All { ref mut cur_index } => {
|
||||
if let Some(line) = self.text[*cur_index..].split('\n').next() {
|
||||
ReflowState::ReflowNo { ref mut cur_index }
|
||||
| ReflowState::ReflowAll { ref mut cur_index } => {
|
||||
for line in self.text[*cur_index..].split('\n') {
|
||||
let ret = line.to_string();
|
||||
*cur_index += line.len() + 2;
|
||||
return Some(ret);
|
||||
|
|
|
@ -178,11 +178,17 @@ pub trait GlobMatch {
|
|||
|
||||
impl GlobMatch for str {
|
||||
fn matches_glob(&self, _pattern: &str) -> bool {
|
||||
let pattern: Vec<&str> = _pattern
|
||||
.strip_suffix('/')
|
||||
.unwrap_or(_pattern)
|
||||
.split_graphemes();
|
||||
let s: Vec<&str> = self.strip_suffix('/').unwrap_or(self).split_graphemes();
|
||||
macro_rules! strip_slash {
|
||||
($v:expr) => {
|
||||
if $v.ends_with("/") {
|
||||
&$v[..$v.len() - 1]
|
||||
} else {
|
||||
$v
|
||||
}
|
||||
};
|
||||
}
|
||||
let pattern: Vec<&str> = strip_slash!(_pattern).split_graphemes();
|
||||
let s: Vec<&str> = strip_slash!(self).split_graphemes();
|
||||
|
||||
// Taken from https://research.swtch.com/glob
|
||||
|
||||
|
|
|
@ -3455,9 +3455,15 @@ pub const LINE_BREAK_RULES: &[(u32, u32, LineBreakClass)] = &[
|
|||
(0x100000, 0x10FFFD, XX),
|
||||
];
|
||||
|
||||
pub const ASCII: &[(u32, u32)] = &[(0x20, 0x7E)];
|
||||
pub const ASCII: &[(u32, u32)] = &[
|
||||
(0x20, 0x7E),
|
||||
];
|
||||
|
||||
pub const PRIVATE: &[(u32, u32)] = &[(0xE000, 0xF8FF), (0xF0000, 0xFFFFD), (0x100000, 0x10FFFD)];
|
||||
pub const PRIVATE: &[(u32, u32)] = &[
|
||||
(0xE000, 0xF8FF),
|
||||
(0xF0000, 0xFFFFD),
|
||||
(0x100000, 0x10FFFD),
|
||||
];
|
||||
|
||||
pub const NONPRINT: &[(u32, u32)] = &[
|
||||
(0x0, 0x1F),
|
||||
|
|
27
src/bin.rs
27
src/bin.rs
|
@ -180,10 +180,6 @@ enum SubCommand {
|
|||
/// print documentation page and exit (Piping to a pager is recommended.).
|
||||
Man(ManOpt),
|
||||
|
||||
#[structopt(display_order = 4)]
|
||||
/// print compile time feature flags of this binary
|
||||
CompiledWith,
|
||||
|
||||
/// View mail from input file.
|
||||
View {
|
||||
#[structopt(value_name = "INPUT", parse(from_os_str))]
|
||||
|
@ -223,7 +219,7 @@ fn run_app(opt: Opt) -> Result<()> {
|
|||
} else {
|
||||
crate::conf::get_config_file()?
|
||||
};
|
||||
conf::FileSettings::validate(config_path, true)?; // TODO: test for tty/interaction
|
||||
conf::FileSettings::validate(config_path)?;
|
||||
return Ok(());
|
||||
}
|
||||
Some(SubCommand::CreateConfig { path }) => {
|
||||
|
@ -294,25 +290,6 @@ fn run_app(opt: Opt) -> Result<()> {
|
|||
Some(SubCommand::Man(_manopt)) => {
|
||||
return Err(MeliError::new("error: this version of meli was not build with embedded documentation. You might have it installed as manpages (eg `man meli`), otherwise check https://meli.delivery"));
|
||||
}
|
||||
Some(SubCommand::CompiledWith) => {
|
||||
#[cfg(feature = "notmuch")]
|
||||
println!("notmuch");
|
||||
#[cfg(feature = "jmap")]
|
||||
println!("jmap");
|
||||
#[cfg(feature = "sqlite3")]
|
||||
println!("sqlite3");
|
||||
#[cfg(feature = "smtp")]
|
||||
println!("smtp");
|
||||
#[cfg(feature = "regexp")]
|
||||
println!("regexp");
|
||||
#[cfg(feature = "dbus-notifications")]
|
||||
println!("dbus-notifications");
|
||||
#[cfg(feature = "cli-docs")]
|
||||
println!("cli-docs");
|
||||
#[cfg(feature = "gpgme")]
|
||||
println!("gpgme");
|
||||
return Ok(());
|
||||
}
|
||||
Some(SubCommand::PrintLoadedThemes) => {
|
||||
let s = conf::FileSettings::new()?;
|
||||
print!("{}", s.terminal.themes.to_string());
|
||||
|
@ -371,7 +348,7 @@ fn run_app(opt: Opt) -> Result<()> {
|
|||
state.register_component(Box::new(components::svg::SVGScreenshotFilter::new()));
|
||||
let window = Box::new(Tabbed::new(
|
||||
vec![
|
||||
Box::new(listing::Listing::new(&mut state.context)),
|
||||
Box::new(listing2::Listing::new(&mut state.context)),
|
||||
Box::new(ContactList::new(&state.context)),
|
||||
],
|
||||
&state.context,
|
||||
|
|
158
src/command.rs
158
src/command.rs
|
@ -108,44 +108,36 @@ impl TokenStream {
|
|||
ptr += 1;
|
||||
}
|
||||
*s = &s[ptr..];
|
||||
//println!("\t before s.is_empty() {:?} {:?}", t, s);
|
||||
if s.is_empty() || &*s == &" " {
|
||||
//println!("{:?} {:?}", t, s);
|
||||
if s.is_empty() {
|
||||
match t.inner() {
|
||||
Literal(lit) => {
|
||||
sugg.insert(format!("{}{}", if s.is_empty() { " " } else { "" }, lit));
|
||||
sugg.insert(format!(" {}", lit));
|
||||
}
|
||||
Alternatives(v) => {
|
||||
for t in v.iter() {
|
||||
//println!("adding empty suggestions for {:?}", t);
|
||||
let mut _s = *s;
|
||||
let mut m = t.matches(&mut _s, sugg);
|
||||
tokens.extend(m.drain(..));
|
||||
t.matches(&mut _s, sugg);
|
||||
}
|
||||
}
|
||||
Seq(_s) => {}
|
||||
RestOfStringValue => {
|
||||
sugg.insert(String::new());
|
||||
}
|
||||
t @ AttachmentIndexValue
|
||||
| t @ MailboxIndexValue
|
||||
| t @ IndexValue
|
||||
| t @ Filepath
|
||||
| t @ AccountName
|
||||
| t @ MailboxPath
|
||||
| t @ QuotedStringValue
|
||||
| t @ AlphanumericStringValue => {
|
||||
let _t = t;
|
||||
//sugg.insert(format!("{}{:?}", if s.is_empty() { " " } else { "" }, t));
|
||||
}
|
||||
RestOfStringValue => {}
|
||||
AttachmentIndexValue
|
||||
| MailboxIndexValue
|
||||
| IndexValue
|
||||
| Filepath
|
||||
| AccountName
|
||||
| MailboxPath
|
||||
| QuotedStringValue
|
||||
| AlphanumericStringValue => {}
|
||||
}
|
||||
tokens.push((*s, *t.inner()));
|
||||
return tokens;
|
||||
}
|
||||
match t.inner() {
|
||||
Literal(lit) => {
|
||||
if lit.starts_with(*s) && lit.len() != s.len() {
|
||||
sugg.insert(lit[s.len()..].to_string());
|
||||
tokens.push((s, *t.inner()));
|
||||
return tokens;
|
||||
} else if s.starts_with(lit) {
|
||||
tokens.push((&s[..lit.len()], *t.inner()));
|
||||
|
@ -167,9 +159,6 @@ impl TokenStream {
|
|||
break;
|
||||
}
|
||||
}
|
||||
if tokens.is_empty() {
|
||||
return tokens;
|
||||
}
|
||||
if !cont {
|
||||
*s = "";
|
||||
}
|
||||
|
@ -393,7 +382,7 @@ define_commands!([
|
|||
},
|
||||
{ tags: ["toggle thread_snooze"],
|
||||
desc: "turn off new notifications for this thread",
|
||||
tokens: &[One(Literal("toggle")), One(Literal("thread_snooze"))],
|
||||
tokens: &[One(Literal("toggle thread_snooze"))],
|
||||
parser: (
|
||||
fn toggle_thread_snooze(input: &[u8]) -> IResult<&[u8], Action> {
|
||||
let (input, _) = tag("toggle")(input)?;
|
||||
|
@ -521,21 +510,6 @@ define_commands!([
|
|||
}
|
||||
)
|
||||
},
|
||||
/* Filter pager contents through binary */
|
||||
{ tags: ["filter "],
|
||||
desc: "filter EXECUTABLE ARGS",
|
||||
tokens: &[One(Literal("filter")), One(Filepath), ZeroOrMore(QuotedStringValue)],
|
||||
parser:(
|
||||
fn filter<'a>(input: &'a [u8]) -> IResult<&'a [u8], Action> {
|
||||
let (input, _) = tag("filter")(input.trim())?;
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, cmd) = map_res(not_line_ending, std::str::from_utf8)(input)?;
|
||||
Ok((input, {
|
||||
View(Filter(cmd.to_string()))
|
||||
}))
|
||||
}
|
||||
)
|
||||
},
|
||||
{ tags: ["add-attachment ", "add-attachment-file-picker "],
|
||||
desc: "add-attachment PATH",
|
||||
tokens: &[One(
|
||||
|
@ -753,17 +727,6 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str
|
|||
}
|
||||
)
|
||||
},
|
||||
{ tags: ["add-addresses-to-contacts "],
|
||||
desc: "add-addresses-to-contacts",
|
||||
tokens: &[One(Literal("add-addresses-to-contacts"))],
|
||||
parser:(
|
||||
fn add_addresses_to_contacts(input: &[u8]) -> IResult<&[u8], Action> {
|
||||
let (input, _) = tag("add-addresses-to-contacts")(input.trim())?;
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, View(AddAddressesToContacts)))
|
||||
}
|
||||
)
|
||||
},
|
||||
{ tags: ["tag", "tag add", "tag remove"],
|
||||
desc: "tag [add/remove], edits message's tags.",
|
||||
tokens: &[One(Literal("tag")), One(Alternatives(&[to_stream!(One(Literal("add"))), to_stream!(One(Literal("remove")))]))],
|
||||
|
@ -922,13 +885,7 @@ fn account_action(input: &[u8]) -> IResult<&[u8], Action> {
|
|||
}
|
||||
|
||||
fn view(input: &[u8]) -> IResult<&[u8], Action> {
|
||||
alt((
|
||||
filter,
|
||||
pipe,
|
||||
save_attachment,
|
||||
export_mail,
|
||||
add_addresses_to_contacts,
|
||||
))(input)
|
||||
alt((pipe, save_attachment, export_mail))(input)
|
||||
}
|
||||
|
||||
pub fn parse_command(input: &[u8]) -> Result<Action, MeliError> {
|
||||
|
@ -958,94 +915,35 @@ pub fn parse_command(input: &[u8]) -> Result<Action, MeliError> {
|
|||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
let mut input = "sort".to_string();
|
||||
macro_rules! match_input {
|
||||
($input:expr) => {{
|
||||
let mut sugg = Default::default();
|
||||
let mut vec = vec![];
|
||||
//print!("{}", $input);
|
||||
for (_tags, _desc, tokens) in COMMAND_COMPLETION.iter() {
|
||||
//println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
|
||||
let m = tokens.matches(&mut $input.as_str(), &mut sugg);
|
||||
if !m.is_empty() {
|
||||
vec.push(tokens);
|
||||
//print!("{:?} ", desc);
|
||||
//println!(" result = {:#?}\n\n", m);
|
||||
}
|
||||
}
|
||||
//println!("suggestions = {:#?}", sugg);
|
||||
sugg.into_iter()
|
||||
.map(|s| format!("{}{}", $input.as_str(), s.as_str()))
|
||||
.collect::<HashSet<String>>()
|
||||
}};
|
||||
}
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["sort date".to_string(), "sort subject".to_string()]).collect(),
|
||||
);
|
||||
input = "so".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["sort".to_string()]).collect(),
|
||||
);
|
||||
input = "so ".to_string();
|
||||
assert_eq!(&match_input!(input), &HashSet::default(),);
|
||||
input = "to".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["toggle".to_string()]).collect(),
|
||||
);
|
||||
input = "toggle ".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter([
|
||||
"toggle mouse".to_string(),
|
||||
"toggle sign".to_string(),
|
||||
"toggle encrypt".to_string(),
|
||||
"toggle thread_snooze".to_string()
|
||||
])
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_parser_interactive() {
|
||||
fn test_parser() {
|
||||
use std::io;
|
||||
let mut input = String::new();
|
||||
loop {
|
||||
input.clear();
|
||||
print!("> ");
|
||||
match io::stdin().read_line(&mut input) {
|
||||
Ok(_n) => {
|
||||
println!("Input is {:?}", input.as_str().trim());
|
||||
let mut sugg = Default::default();
|
||||
let mut vec = vec![];
|
||||
//print!("{}", input);
|
||||
for (_tags, _desc, tokens) in COMMAND_COMPLETION.iter() {
|
||||
//println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
|
||||
for (_tags, desc, tokens) in COMMAND_COMPLETION.iter() {
|
||||
let m = tokens.matches(&mut input.as_str().trim(), &mut sugg);
|
||||
if !m.is_empty() {
|
||||
vec.push(tokens);
|
||||
//print!("{:?} ", desc);
|
||||
//println!(" result = {:#?}\n\n", m);
|
||||
print!("{:?} ", desc);
|
||||
println!(" result = {:#?}\n\n", m);
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"suggestions = {:#?}",
|
||||
sugg.into_iter()
|
||||
.zip(vec.into_iter())
|
||||
.map(|(s, v)| format!(
|
||||
"{}{} {:?}",
|
||||
.map(|s| format!(
|
||||
"{}{}",
|
||||
input.as_str().trim(),
|
||||
if input.trim().is_empty() {
|
||||
s.trim()
|
||||
} else {
|
||||
s.as_str()
|
||||
},
|
||||
v
|
||||
}
|
||||
))
|
||||
.collect::<Vec<String>>()
|
||||
);
|
||||
|
@ -1074,6 +972,16 @@ pub fn command_completion_suggestions(input: &str) -> Vec<String> {
|
|||
}
|
||||
}
|
||||
sugg.into_iter()
|
||||
.map(|s| format!("{}{}", input, s.as_str()))
|
||||
.map(|s| {
|
||||
format!(
|
||||
"{}{}",
|
||||
input.trim(),
|
||||
if input.trim().is_empty() {
|
||||
s.trim()
|
||||
} else {
|
||||
s.as_str()
|
||||
}
|
||||
)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
|
|
|
@ -75,10 +75,8 @@ pub enum MailingListAction {
|
|||
#[derive(Debug)]
|
||||
pub enum ViewAction {
|
||||
Pipe(String, Vec<String>),
|
||||
Filter(String),
|
||||
SaveAttachment(usize, String),
|
||||
ExportMail(String),
|
||||
AddAddressesToContacts,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -132,7 +130,6 @@ pub enum Action {
|
|||
impl Action {
|
||||
pub fn needs_confirmation(&self) -> bool {
|
||||
match self {
|
||||
Action::Listing(ListingAction::Delete) => true,
|
||||
Action::Listing(_) => false,
|
||||
Action::ViewMailbox(_) => false,
|
||||
Action::Sort(_, _) => false,
|
||||
|
|
|
@ -27,6 +27,7 @@ use melib::email::{attachment_types::*, attachments::*};
|
|||
use melib::thread::ThreadNodeHash;
|
||||
|
||||
pub mod listing;
|
||||
pub mod listing2;
|
||||
pub use crate::listing::*;
|
||||
pub mod view;
|
||||
pub use crate::view::*;
|
||||
|
|
|
@ -26,7 +26,7 @@ use melib::Draft;
|
|||
|
||||
use crate::conf::accounts::JobRequest;
|
||||
use crate::jobs::JoinHandle;
|
||||
use crate::terminal::embed::EmbedTerminal;
|
||||
use crate::terminal::embed::EmbedGrid;
|
||||
use indexmap::IndexSet;
|
||||
use nix::sys::wait::WaitStatus;
|
||||
use std::convert::TryInto;
|
||||
|
@ -53,13 +53,13 @@ enum Cursor {
|
|||
|
||||
#[derive(Debug)]
|
||||
enum EmbedStatus {
|
||||
Stopped(Arc<Mutex<EmbedTerminal>>, File),
|
||||
Running(Arc<Mutex<EmbedTerminal>>, File),
|
||||
Stopped(Arc<Mutex<EmbedGrid>>, File),
|
||||
Running(Arc<Mutex<EmbedGrid>>, File),
|
||||
}
|
||||
|
||||
impl std::ops::Deref for EmbedStatus {
|
||||
type Target = Arc<Mutex<EmbedTerminal>>;
|
||||
fn deref(&self) -> &Arc<Mutex<EmbedTerminal>> {
|
||||
type Target = Arc<Mutex<EmbedGrid>>;
|
||||
fn deref(&self) -> &Arc<Mutex<EmbedGrid>> {
|
||||
use EmbedStatus::*;
|
||||
match self {
|
||||
Stopped(ref e, _) | Running(ref e, _) => e,
|
||||
|
@ -68,7 +68,7 @@ impl std::ops::Deref for EmbedStatus {
|
|||
}
|
||||
|
||||
impl std::ops::DerefMut for EmbedStatus {
|
||||
fn deref_mut(&mut self) -> &mut Arc<Mutex<EmbedTerminal>> {
|
||||
fn deref_mut(&mut self) -> &mut Arc<Mutex<EmbedGrid>> {
|
||||
use EmbedStatus::*;
|
||||
match self {
|
||||
Stopped(ref mut e, _) | Running(ref mut e, _) => e,
|
||||
|
@ -408,55 +408,6 @@ impl Composer {
|
|||
Composer::reply_to(coordinates, reply_body, context, true)
|
||||
}
|
||||
|
||||
pub fn forward(
|
||||
coordinates: (AccountHash, MailboxHash, EnvelopeHash),
|
||||
bytes: &[u8],
|
||||
env: &Envelope,
|
||||
as_attachment: bool,
|
||||
context: &mut Context,
|
||||
) -> Self {
|
||||
let mut composer = Composer::with_account(coordinates.0, context);
|
||||
let mut draft: Draft = Draft::default();
|
||||
draft.set_header("Subject", format!("Fwd: {}", env.subject()));
|
||||
let preamble = format!(
|
||||
r#"
|
||||
---------- Forwarded message ---------
|
||||
From: {}
|
||||
Date: {}
|
||||
Subject: {}
|
||||
To: {}
|
||||
|
||||
"#,
|
||||
env.field_from_to_string(),
|
||||
env.date_as_str(),
|
||||
env.subject(),
|
||||
env.field_to_to_string()
|
||||
);
|
||||
if as_attachment {
|
||||
let mut attachment = AttachmentBuilder::new(b"");
|
||||
let mut disposition: ContentDisposition = ContentDispositionKind::Attachment.into();
|
||||
{
|
||||
disposition.filename = Some(format!("{}.eml", env.message_id_raw()));
|
||||
}
|
||||
attachment
|
||||
.set_raw(bytes.to_vec())
|
||||
.set_body_to_raw()
|
||||
.set_content_type(ContentType::MessageRfc822)
|
||||
.set_content_transfer_encoding(ContentTransferEncoding::_8Bit)
|
||||
.set_content_disposition(disposition);
|
||||
draft.attachments.push(attachment);
|
||||
draft.body = preamble;
|
||||
} else {
|
||||
let content_type = ContentType::default();
|
||||
let preamble: AttachmentBuilder =
|
||||
Attachment::new(content_type, Default::default(), preamble.into_bytes()).into();
|
||||
draft.attachments.push(preamble);
|
||||
draft.attachments.push(env.body_bytes(bytes).into());
|
||||
}
|
||||
composer.set_draft(draft);
|
||||
composer
|
||||
}
|
||||
|
||||
pub fn set_draft(&mut self, draft: Draft) {
|
||||
self.draft = draft;
|
||||
self.update_form();
|
||||
|
@ -526,9 +477,6 @@ To: {}
|
|||
hostname.truncate_at_boundary(10);
|
||||
format!("{} [smtp: {}]", acc.name(), hostname)
|
||||
}
|
||||
crate::conf::composing::SendMail::ServerSubmission => {
|
||||
format!("{} [server submission]", acc.name())
|
||||
}
|
||||
};
|
||||
|
||||
(addr, desc)
|
||||
|
@ -843,9 +791,9 @@ impl Component for Composer {
|
|||
clear_area(grid, embed_area, theme_default);
|
||||
copy_area(
|
||||
grid,
|
||||
&guard.grid.buffer(),
|
||||
&guard.grid,
|
||||
embed_area,
|
||||
((0, 0), pos_dec(guard.grid.terminal_size, (1, 1))),
|
||||
((0, 0), pos_dec(guard.terminal_size, (1, 1))),
|
||||
);
|
||||
guard.set_terminal_size((width!(embed_area), height!(embed_area)));
|
||||
context.dirty_areas.push_back(area);
|
||||
|
@ -856,9 +804,9 @@ impl Component for Composer {
|
|||
let guard = embed_pty.lock().unwrap();
|
||||
copy_area(
|
||||
grid,
|
||||
&guard.grid.buffer(),
|
||||
&guard.grid,
|
||||
embed_area,
|
||||
((0, 0), pos_dec(guard.grid.terminal_size, (1, 1))),
|
||||
((0, 0), pos_dec(guard.terminal_size, (1, 1))),
|
||||
);
|
||||
change_colors(grid, embed_area, Color::Byte(8), theme_default.bg);
|
||||
const STOPPED_MESSAGE: &str = "process has stopped, press 'e' to re-activate";
|
||||
|
|
|
@ -28,24 +28,6 @@ use std::collections::{HashMap, HashSet};
|
|||
use std::convert::TryFrom;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
// TODO: emoji_text_presentation_selector should be printed along with the chars before it but not
|
||||
// as a separate Cell
|
||||
//macro_rules! emoji_text_presentation_selector {
|
||||
// () => {
|
||||
// "\u{FE0E}"
|
||||
// };
|
||||
//}
|
||||
//
|
||||
//pub const DEFAULT_ATTACHMENT_FLAG: &str = concat!("📎", emoji_text_presentation_selector!());
|
||||
//pub const DEFAULT_SELECTED_FLAG: &str = concat!("☑️", emoji_text_presentation_selector!());
|
||||
//pub const DEFAULT_UNSEEN_FLAG: &str = concat!("●", emoji_text_presentation_selector!());
|
||||
//pub const DEFAULT_SNOOZED_FLAG: &str = concat!("💤", emoji_text_presentation_selector!());
|
||||
|
||||
pub const DEFAULT_ATTACHMENT_FLAG: &str = "📎";
|
||||
pub const DEFAULT_SELECTED_FLAG: &str = "☑️";
|
||||
pub const DEFAULT_UNSEEN_FLAG: &str = "●";
|
||||
pub const DEFAULT_SNOOZED_FLAG: &str = "💤";
|
||||
|
||||
mod conversations;
|
||||
pub use self::conversations::*;
|
||||
|
||||
|
@ -106,6 +88,8 @@ struct ColorCache {
|
|||
subject: ThemeAttribute,
|
||||
from: ThemeAttribute,
|
||||
date: ThemeAttribute,
|
||||
padding: ThemeAttribute,
|
||||
unseen_padding: ThemeAttribute,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -26,44 +26,6 @@ use std::cmp;
|
|||
use std::convert::TryInto;
|
||||
use std::iter::FromIterator;
|
||||
|
||||
macro_rules! digits_of_num {
|
||||
($num:expr) => {{
|
||||
const GUESS: [usize; 65] = [
|
||||
1, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 8, 8,
|
||||
8, 9, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15,
|
||||
15, 15, 16, 16, 16, 17, 17, 17, 18, 18, 18, 18, 19,
|
||||
];
|
||||
const TENS: [usize; 20] = [
|
||||
1,
|
||||
10,
|
||||
100,
|
||||
1000,
|
||||
10000,
|
||||
100000,
|
||||
1000000,
|
||||
10000000,
|
||||
100000000,
|
||||
1000000000,
|
||||
10000000000,
|
||||
100000000000,
|
||||
1000000000000,
|
||||
10000000000000,
|
||||
100000000000000,
|
||||
1000000000000000,
|
||||
10000000000000000,
|
||||
100000000000000000,
|
||||
1000000000000000000,
|
||||
10000000000000000000,
|
||||
];
|
||||
const SIZE_IN_BITS: usize = std::mem::size_of::<usize>() * 8;
|
||||
|
||||
let leading_zeros = $num.leading_zeros() as usize;
|
||||
let base_two_digits: usize = SIZE_IN_BITS - leading_zeros;
|
||||
let x = GUESS[base_two_digits];
|
||||
x + if $num >= TENS[x] { 1 } else { 0 }
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! address_list {
|
||||
(($name:expr) as comma_sep_list) => {{
|
||||
let mut ret: String =
|
||||
|
@ -318,17 +280,19 @@ impl MailListingTrait for CompactListing {
|
|||
self.order.clear();
|
||||
self.length = 0;
|
||||
let mut rows = Vec::with_capacity(1024);
|
||||
let mut min_width = (0, 0, 0, 0);
|
||||
let mut min_width = (0, 0, 0, 0, 0);
|
||||
let mut row_widths: (
|
||||
SmallVec<[u8; 1024]>,
|
||||
SmallVec<[u8; 1024]>,
|
||||
SmallVec<[u8; 1024]>,
|
||||
SmallVec<[u8; 1024]>,
|
||||
SmallVec<[u8; 1024]>,
|
||||
) = (
|
||||
SmallVec::new(),
|
||||
SmallVec::new(),
|
||||
SmallVec::new(),
|
||||
SmallVec::new(),
|
||||
SmallVec::new(),
|
||||
);
|
||||
|
||||
'items_for_loop: for thread in items {
|
||||
|
@ -378,48 +342,38 @@ impl MailListingTrait for CompactListing {
|
|||
}
|
||||
|
||||
let entry_strings = self.make_entry_string(&root_envelope, context, &threads, thread);
|
||||
row_widths
|
||||
.0
|
||||
.push(digits_of_num!(self.length).try_into().unwrap_or(255));
|
||||
/* date */
|
||||
row_widths.1.push(
|
||||
entry_strings
|
||||
.date
|
||||
.grapheme_width()
|
||||
.try_into()
|
||||
.unwrap_or(255),
|
||||
);
|
||||
/* from */
|
||||
); /* date */
|
||||
row_widths.2.push(
|
||||
entry_strings
|
||||
.from
|
||||
.grapheme_width()
|
||||
.try_into()
|
||||
.unwrap_or(255),
|
||||
);
|
||||
/* subject */
|
||||
); /* from */
|
||||
row_widths.3.push(
|
||||
(entry_strings
|
||||
entry_strings
|
||||
.flag
|
||||
.grapheme_width()
|
||||
.try_into()
|
||||
.unwrap_or(255)
|
||||
+ 1
|
||||
+ entry_strings.subject.grapheme_width()
|
||||
+ 1
|
||||
+ entry_strings.tags.grapheme_width())
|
||||
.try_into()
|
||||
.unwrap_or(255),
|
||||
.unwrap_or(255),
|
||||
); /* flags */
|
||||
row_widths.4.push(
|
||||
(entry_strings.subject.grapheme_width() + 1 + entry_strings.tags.grapheme_width())
|
||||
.try_into()
|
||||
.unwrap_or(255),
|
||||
);
|
||||
min_width.1 = cmp::max(min_width.1, entry_strings.date.grapheme_width()); /* date */
|
||||
min_width.2 = cmp::max(min_width.2, entry_strings.from.grapheme_width()); /* from */
|
||||
min_width.3 = cmp::max(
|
||||
min_width.3,
|
||||
entry_strings.flag.grapheme_width()
|
||||
+ 1
|
||||
+ entry_strings.subject.grapheme_width()
|
||||
+ 1
|
||||
+ entry_strings.tags.grapheme_width(),
|
||||
min_width.3 = cmp::max(min_width.3, entry_strings.flag.grapheme_width()); /* flags */
|
||||
min_width.4 = cmp::max(
|
||||
min_width.4,
|
||||
entry_strings.subject.grapheme_width() + 1 + entry_strings.tags.grapheme_width(),
|
||||
); /* subject */
|
||||
rows.push(((self.length, (thread, root_env_hash)), entry_strings));
|
||||
self.all_threads.insert(thread);
|
||||
|
@ -434,20 +388,21 @@ impl MailListingTrait for CompactListing {
|
|||
/* index column */
|
||||
self.data_columns.columns[0] =
|
||||
CellBuffer::new_with_context(min_width.0, rows.len(), None, context);
|
||||
self.data_columns.segment_tree[0] = row_widths.0.into();
|
||||
|
||||
/* date column */
|
||||
self.data_columns.columns[1] =
|
||||
CellBuffer::new_with_context(min_width.1, rows.len(), None, context);
|
||||
self.data_columns.segment_tree[1] = row_widths.1.into();
|
||||
/* from column */
|
||||
self.data_columns.columns[2] =
|
||||
CellBuffer::new_with_context(min_width.2, rows.len(), None, context);
|
||||
self.data_columns.segment_tree[2] = row_widths.2.into();
|
||||
/* subject column */
|
||||
/* flags column */
|
||||
self.data_columns.columns[3] =
|
||||
CellBuffer::new_with_context(min_width.3, rows.len(), None, context);
|
||||
self.data_columns.segment_tree[3] = row_widths.3.into();
|
||||
/* subject column */
|
||||
self.data_columns.columns[4] =
|
||||
CellBuffer::new_with_context(min_width.4, rows.len(), None, context);
|
||||
self.data_columns.segment_tree[4] = row_widths.4.into();
|
||||
|
||||
self.rows = rows;
|
||||
self.rows_drawn = SegmentTree::from(
|
||||
|
@ -533,16 +488,10 @@ impl ListingTrait for CompactListing {
|
|||
(set_x(upper_left, x), bottom_right),
|
||||
(
|
||||
(0, idx),
|
||||
pos_dec(
|
||||
(
|
||||
self.data_columns.widths[3],
|
||||
self.data_columns.columns[3].size().1,
|
||||
),
|
||||
(1, 1),
|
||||
),
|
||||
pos_dec(self.data_columns.columns[3].size(), (1, 1)),
|
||||
),
|
||||
);
|
||||
for c in grid.row_iter(x..(get_x(bottom_right) + 1), get_y(upper_left)) {
|
||||
for c in grid.row_iter(x..(self.data_columns.widths[3] + x), get_y(upper_left)) {
|
||||
grid[c].set_bg(row_attr.bg).set_attrs(row_attr.attrs);
|
||||
}
|
||||
}
|
||||
|
@ -647,35 +596,40 @@ impl ListingTrait for CompactListing {
|
|||
self.data_columns.widths[0] = self.data_columns.columns[0].size().0;
|
||||
self.data_columns.widths[1] = self.data_columns.columns[1].size().0; /* date*/
|
||||
self.data_columns.widths[2] = self.data_columns.columns[2].size().0; /* from */
|
||||
self.data_columns.widths[3] = self.data_columns.columns[3].size().0; /* subject */
|
||||
self.data_columns.widths[3] = self.data_columns.columns[3].size().0; /* flags */
|
||||
self.data_columns.widths[4] = self.data_columns.columns[4].size().0; /* subject */
|
||||
|
||||
let min_col_width = std::cmp::min(
|
||||
15,
|
||||
std::cmp::min(self.data_columns.widths[3], self.data_columns.widths[2]),
|
||||
std::cmp::min(self.data_columns.widths[4], self.data_columns.widths[2]),
|
||||
);
|
||||
if self.data_columns.widths[0] + self.data_columns.widths[1] + 2 * min_col_width + 4 > width
|
||||
if self.data_columns.widths[0] + self.data_columns.widths[1] + 3 * min_col_width + 8 > width
|
||||
{
|
||||
let remainder = width
|
||||
.saturating_sub(self.data_columns.widths[0])
|
||||
.saturating_sub(self.data_columns.widths[1])
|
||||
.saturating_sub(2 * 2);
|
||||
.saturating_sub(4);
|
||||
self.data_columns.widths[2] = remainder / 6;
|
||||
self.data_columns.widths[4] =
|
||||
((2 * remainder) / 3).saturating_sub(self.data_columns.widths[3]);
|
||||
} else {
|
||||
let remainder = width
|
||||
.saturating_sub(self.data_columns.widths[0])
|
||||
.saturating_sub(self.data_columns.widths[1])
|
||||
.saturating_sub(3 * 2);
|
||||
if min_col_width + self.data_columns.widths[3] > remainder {
|
||||
.saturating_sub(8);
|
||||
if min_col_width + self.data_columns.widths[4] > remainder {
|
||||
self.data_columns.widths[4] =
|
||||
remainder.saturating_sub(min_col_width + self.data_columns.widths[3]);
|
||||
self.data_columns.widths[2] = min_col_width;
|
||||
}
|
||||
}
|
||||
for i in 0..3 {
|
||||
/* Set column widths to their maximum value width in the range
|
||||
for &i in &[2, 4] {
|
||||
/* Set From and Subject column widths to their maximum value width in the range
|
||||
* [top_idx, top_idx + rows]. By using a segment tree the query is O(logn), which is
|
||||
* great!
|
||||
*/
|
||||
self.data_columns.widths[i] =
|
||||
self.data_columns.segment_tree[i].get_max(top_idx, top_idx + rows - 1) as usize;
|
||||
self.data_columns.segment_tree[i].get_max(top_idx, top_idx + rows) as usize;
|
||||
}
|
||||
if self.data_columns.widths.iter().sum::<usize>() > width {
|
||||
let diff = self.data_columns.widths.iter().sum::<usize>() - width;
|
||||
|
@ -686,30 +640,40 @@ impl ListingTrait for CompactListing {
|
|||
15,
|
||||
self.data_columns.widths[2].saturating_sub((2 * diff) / 3),
|
||||
);
|
||||
self.data_columns.widths[4] = std::cmp::max(
|
||||
15,
|
||||
self.data_columns.widths[4].saturating_sub(diff / 3 + diff % 3),
|
||||
);
|
||||
}
|
||||
}
|
||||
clear_area(grid, area, self.color_cache.theme_default);
|
||||
/* Page_no has changed, so draw new page */
|
||||
let mut x = get_x(upper_left);
|
||||
let mut flag_x = 0;
|
||||
for i in 0..4 {
|
||||
let column_width = self.data_columns.widths[i];
|
||||
for i in 0..self.data_columns.columns.len() {
|
||||
let column_width = self.data_columns.columns[i].size().0;
|
||||
if i == 3 {
|
||||
flag_x = x;
|
||||
}
|
||||
if column_width == 0 {
|
||||
if self.data_columns.widths[i] == 0 {
|
||||
continue;
|
||||
}
|
||||
copy_area(
|
||||
grid,
|
||||
&self.data_columns.columns[i],
|
||||
(set_x(upper_left, x), bottom_right),
|
||||
(
|
||||
set_x(upper_left, x),
|
||||
set_x(
|
||||
bottom_right,
|
||||
std::cmp::min(get_x(bottom_right), x + (self.data_columns.widths[i])),
|
||||
),
|
||||
),
|
||||
(
|
||||
(0, top_idx),
|
||||
(column_width.saturating_sub(1), self.length - 1),
|
||||
),
|
||||
);
|
||||
x += column_width + 2; // + SEPARATOR
|
||||
x += self.data_columns.widths[i] + 2; // + SEPARATOR
|
||||
if x > get_x(bottom_right) {
|
||||
break;
|
||||
}
|
||||
|
@ -735,9 +699,26 @@ impl ListingTrait for CompactListing {
|
|||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
);
|
||||
for x in flag_x..get_x(bottom_right) {
|
||||
for x in flag_x
|
||||
..std::cmp::min(
|
||||
get_x(bottom_right),
|
||||
flag_x + 2 + self.data_columns.widths[3],
|
||||
)
|
||||
{
|
||||
grid[(x, get_y(upper_left) + r)].set_bg(row_attr.bg);
|
||||
}
|
||||
change_colors(
|
||||
grid,
|
||||
(
|
||||
(
|
||||
flag_x + 2 + self.data_columns.widths[3],
|
||||
get_y(upper_left) + r,
|
||||
),
|
||||
(get_x(bottom_right), get_y(upper_left) + r),
|
||||
),
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
);
|
||||
}
|
||||
|
||||
self.highlight_line(
|
||||
|
@ -753,7 +734,10 @@ impl ListingTrait for CompactListing {
|
|||
if top_idx + rows > self.length {
|
||||
clear_area(
|
||||
grid,
|
||||
(pos_inc(upper_left, (0, rows - 1)), bottom_right),
|
||||
(
|
||||
pos_inc(upper_left, (0, self.length - top_idx)),
|
||||
bottom_right,
|
||||
),
|
||||
self.color_cache.theme_default,
|
||||
);
|
||||
}
|
||||
|
@ -948,75 +932,30 @@ impl CompactListing {
|
|||
}
|
||||
let mut subject = e.subject().to_string();
|
||||
subject.truncate_at_boundary(150);
|
||||
EntryStrings {
|
||||
date: DateString(ConversationsListing::format_date(context, thread.date())),
|
||||
subject: if thread.len() > 1 {
|
||||
SubjectString(format!("{} ({})", subject, thread.len(),))
|
||||
} else {
|
||||
SubjectString(subject)
|
||||
},
|
||||
flag: FlagString(format!(
|
||||
"{selected}{snoozed}{unseen}{attachments}{whitespace}",
|
||||
selected = if self.selection.get(&hash).cloned().unwrap_or(false) {
|
||||
mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.selected_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_SELECTED_FLAG)
|
||||
} else {
|
||||
""
|
||||
},
|
||||
snoozed = if thread.snoozed() {
|
||||
mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.thread_snoozed_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_SNOOZED_FLAG)
|
||||
} else {
|
||||
""
|
||||
},
|
||||
unseen = if thread.unseen() > 0 {
|
||||
mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.unseen_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_UNSEEN_FLAG)
|
||||
} else {
|
||||
""
|
||||
},
|
||||
attachments = if thread.has_attachments() {
|
||||
mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.attachment_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_ATTACHMENT_FLAG)
|
||||
} else {
|
||||
""
|
||||
},
|
||||
whitespace = if self.selection.get(&hash).cloned().unwrap_or(false)
|
||||
|| thread.unseen() > 0
|
||||
|| thread.snoozed()
|
||||
|| thread.has_attachments()
|
||||
{
|
||||
" "
|
||||
} else {
|
||||
""
|
||||
},
|
||||
)),
|
||||
from: FromString(address_list!((e.from()) as comma_sep_list)),
|
||||
tags: TagString(tags, colors),
|
||||
if thread.len() > 1 {
|
||||
EntryStrings {
|
||||
date: DateString(ConversationsListing::format_date(context, thread.date())),
|
||||
subject: SubjectString(format!("{} ({})", subject, thread.len(),)),
|
||||
flag: FlagString(format!(
|
||||
"{}{}",
|
||||
if thread.has_attachments() { "📎" } else { "" },
|
||||
if thread.snoozed() { "💤" } else { "" }
|
||||
)),
|
||||
from: FromString(address_list!((e.from()) as comma_sep_list)),
|
||||
tags: TagString(tags, colors),
|
||||
}
|
||||
} else {
|
||||
EntryStrings {
|
||||
date: DateString(ConversationsListing::format_date(context, thread.date())),
|
||||
subject: SubjectString(subject),
|
||||
flag: FlagString(format!(
|
||||
"{}{}",
|
||||
if thread.has_attachments() { "📎" } else { "" },
|
||||
if thread.snoozed() { "💤" } else { "" }
|
||||
)),
|
||||
from: FromString(address_list!((e.from()) as comma_sep_list)),
|
||||
tags: TagString(tags, colors),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1041,44 +980,6 @@ impl CompactListing {
|
|||
let threads = account.collection.get_threads(self.cursor_pos.1);
|
||||
let thread = threads.thread_ref(thread_hash);
|
||||
let thread_node_hash = threads.thread_group_iter(thread_hash).next().unwrap().1;
|
||||
|
||||
let selected_flag_len = mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.selected_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_SELECTED_FLAG)
|
||||
.grapheme_width();
|
||||
let thread_snoozed_flag_len = mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.thread_snoozed_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_SNOOZED_FLAG)
|
||||
.grapheme_width();
|
||||
let unseen_flag_len = mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.unseen_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_UNSEEN_FLAG)
|
||||
.grapheme_width();
|
||||
let attachment_flag_len = mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.attachment_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_ATTACHMENT_FLAG)
|
||||
.grapheme_width();
|
||||
|
||||
if let Some(env_hash) = threads.thread_nodes()[&thread_node_hash].message() {
|
||||
if !account.contains_key(env_hash) {
|
||||
/* The envelope has been renamed or removed, so wait for the appropriate event to
|
||||
|
@ -1102,6 +1003,7 @@ impl CompactListing {
|
|||
columns[1].size().0,
|
||||
columns[2].size().0,
|
||||
columns[3].size().0,
|
||||
columns[4].size().0,
|
||||
);
|
||||
let (x, _) = write_string_to_grid(
|
||||
&idx.to_string(),
|
||||
|
@ -1113,7 +1015,7 @@ impl CompactListing {
|
|||
None,
|
||||
);
|
||||
for c in columns[0].row_iter(x..min_width.0, idx) {
|
||||
columns[0][c].set_bg(row_attr.bg).set_ch(' ');
|
||||
columns[0][c].set_bg(row_attr.bg);
|
||||
}
|
||||
let (x, _) = write_string_to_grid(
|
||||
&strings.date,
|
||||
|
@ -1125,7 +1027,7 @@ impl CompactListing {
|
|||
None,
|
||||
);
|
||||
for c in columns[1].row_iter(x..min_width.1, idx) {
|
||||
columns[1][c].set_bg(row_attr.bg).set_ch(' ');
|
||||
columns[1][c].set_bg(row_attr.bg);
|
||||
}
|
||||
let (x, _) = write_string_to_grid(
|
||||
&strings.from,
|
||||
|
@ -1137,7 +1039,7 @@ impl CompactListing {
|
|||
None,
|
||||
);
|
||||
for c in columns[2].row_iter(x..min_width.2, idx) {
|
||||
columns[2][c].set_bg(row_attr.bg).set_ch(' ');
|
||||
columns[2][c].set_bg(row_attr.bg);
|
||||
}
|
||||
let (x, _) = write_string_to_grid(
|
||||
&strings.flag,
|
||||
|
@ -1148,70 +1050,66 @@ impl CompactListing {
|
|||
((0, idx), (min_width.3, idx)),
|
||||
None,
|
||||
);
|
||||
for c in columns[3].row_iter(x..min_width.3, idx) {
|
||||
columns[3][c].set_bg(row_attr.bg);
|
||||
}
|
||||
let (x, _) = write_string_to_grid(
|
||||
&strings.subject,
|
||||
&mut columns[3],
|
||||
&mut columns[4],
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
((x, idx), (min_width.3, idx)),
|
||||
((0, idx), (min_width.4, idx)),
|
||||
None,
|
||||
);
|
||||
columns[3][(x, idx)].set_bg(row_attr.bg).set_ch(' ');
|
||||
let x = {
|
||||
let mut x = x + 1;
|
||||
for (t, &color) in strings.tags.split_whitespace().zip(strings.tags.1.iter()) {
|
||||
let color = color.unwrap_or(self.color_cache.tag_default.bg);
|
||||
let (_x, _) = write_string_to_grid(
|
||||
t,
|
||||
&mut columns[3],
|
||||
&mut columns[4],
|
||||
self.color_cache.tag_default.fg,
|
||||
color,
|
||||
self.color_cache.tag_default.attrs,
|
||||
((x + 1, idx), (min_width.3, idx)),
|
||||
((x + 1, idx), (min_width.4, idx)),
|
||||
None,
|
||||
);
|
||||
for c in columns[3].row_iter(x..(x + 1), idx) {
|
||||
columns[3][c].set_bg(color);
|
||||
for c in columns[4].row_iter(x..(x + 1), idx) {
|
||||
columns[4][c].set_bg(color);
|
||||
}
|
||||
for c in columns[3].row_iter(_x..(_x + 1), idx) {
|
||||
columns[3][c].set_bg(color).set_keep_bg(true);
|
||||
for c in columns[4].row_iter(_x..(_x + 1), idx) {
|
||||
columns[4][c].set_bg(color).set_keep_bg(true);
|
||||
}
|
||||
for c in columns[3].row_iter((x + 1)..(_x + 1), idx) {
|
||||
columns[3][c]
|
||||
for c in columns[4].row_iter((x + 1)..(_x + 1), idx) {
|
||||
columns[4][c]
|
||||
.set_keep_fg(true)
|
||||
.set_keep_bg(true)
|
||||
.set_keep_attrs(true);
|
||||
}
|
||||
for c in columns[3].row_iter(x..(x + 1), idx) {
|
||||
columns[3][c].set_keep_bg(true);
|
||||
for c in columns[4].row_iter(x..(x + 1), idx) {
|
||||
columns[4][c].set_keep_bg(true);
|
||||
}
|
||||
x = _x + 1;
|
||||
columns[3][(x, idx)].set_bg(row_attr.bg).set_ch(' ');
|
||||
}
|
||||
x
|
||||
};
|
||||
for c in columns[3].row_iter(x..min_width.3, idx) {
|
||||
columns[3][c].set_ch(' ').set_bg(row_attr.bg);
|
||||
for c in columns[4].row_iter(x..min_width.4, idx) {
|
||||
columns[4][c].set_ch(' ');
|
||||
columns[4][c].set_bg(row_attr.bg);
|
||||
}
|
||||
/* Set fg color for flags */
|
||||
let mut x = 0;
|
||||
if self.selection.get(&thread_hash).cloned().unwrap_or(false) {
|
||||
x += selected_flag_len;
|
||||
}
|
||||
if thread.snoozed() {
|
||||
for x in x..(x + thread_snoozed_flag_len) {
|
||||
columns[3][(x, idx)].set_fg(self.color_cache.thread_snooze_flag.fg);
|
||||
match (thread.snoozed(), thread.has_attachments()) {
|
||||
(true, true) => {
|
||||
columns[3][(0, idx)].set_fg(self.color_cache.attachment_flag.fg);
|
||||
columns[3][(2, idx)].set_fg(self.color_cache.thread_snooze_flag.fg);
|
||||
}
|
||||
x += thread_snoozed_flag_len;
|
||||
}
|
||||
if thread.unseen() > 0 {
|
||||
x += unseen_flag_len;
|
||||
}
|
||||
if thread.has_attachments() {
|
||||
for x in x..(x + attachment_flag_len) {
|
||||
columns[3][(x, idx)].set_fg(self.color_cache.attachment_flag.fg);
|
||||
(true, false) => {
|
||||
columns[3][(0, idx)].set_fg(self.color_cache.thread_snooze_flag.fg);
|
||||
}
|
||||
(false, true) => {
|
||||
columns[3][(0, idx)].set_fg(self.color_cache.attachment_flag.fg);
|
||||
}
|
||||
(false, false) => {}
|
||||
}
|
||||
*self.rows.get_mut(idx).unwrap() = ((idx, (thread_hash, env_hash)), strings);
|
||||
self.rows_drawn.update(idx, 1);
|
||||
|
@ -1236,48 +1134,12 @@ impl CompactListing {
|
|||
self.data_columns.columns[1].size().0,
|
||||
self.data_columns.columns[2].size().0,
|
||||
self.data_columns.columns[3].size().0,
|
||||
self.data_columns.columns[4].size().0,
|
||||
);
|
||||
let account = &context.accounts[&self.cursor_pos.0];
|
||||
|
||||
let threads = account.collection.get_threads(self.cursor_pos.1);
|
||||
|
||||
let selected_flag_len = mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.selected_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_SELECTED_FLAG)
|
||||
.grapheme_width();
|
||||
let thread_snoozed_flag_len = mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.thread_snoozed_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_SNOOZED_FLAG)
|
||||
.grapheme_width();
|
||||
let unseen_flag_len = mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.unseen_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_UNSEEN_FLAG)
|
||||
.grapheme_width();
|
||||
let attachment_flag_len = mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.attachment_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_ATTACHMENT_FLAG)
|
||||
.grapheme_width();
|
||||
|
||||
for ((idx, (thread_hash, root_env_hash)), strings) in
|
||||
self.rows.iter().skip(start).take(end - start + 1)
|
||||
{
|
||||
|
@ -1361,21 +1223,26 @@ impl CompactListing {
|
|||
((0, idx), (min_width.3, idx)),
|
||||
None,
|
||||
);
|
||||
for x in x..min_width.3 {
|
||||
self.data_columns.columns[3][(x, idx)]
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
}
|
||||
let (x, _) = write_string_to_grid(
|
||||
&strings.subject,
|
||||
&mut self.data_columns.columns[3],
|
||||
&mut self.data_columns.columns[4],
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
((x, idx), (min_width.3, idx)),
|
||||
((0, idx), (min_width.4, idx)),
|
||||
None,
|
||||
);
|
||||
#[cfg(feature = "regexp")]
|
||||
{
|
||||
for text_formatter in crate::conf::text_format_regexps(context, "listing.subject") {
|
||||
let t = self.data_columns.columns[3].insert_tag(text_formatter.tag);
|
||||
let t = self.data_columns.columns[4].insert_tag(text_formatter.tag);
|
||||
for (start, end) in text_formatter.regexp.find_iter(strings.subject.as_str()) {
|
||||
self.data_columns.columns[3].set_tag(t, (start, idx), (end, idx));
|
||||
self.data_columns.columns[4].set_tag(t, (start, idx), (end, idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1385,56 +1252,52 @@ impl CompactListing {
|
|||
let color = color.unwrap_or(self.color_cache.tag_default.bg);
|
||||
let (_x, _) = write_string_to_grid(
|
||||
t,
|
||||
&mut self.data_columns.columns[3],
|
||||
&mut self.data_columns.columns[4],
|
||||
self.color_cache.tag_default.fg,
|
||||
color,
|
||||
self.color_cache.tag_default.attrs,
|
||||
((x + 1, idx), (min_width.3, idx)),
|
||||
((x + 1, idx), (min_width.4, idx)),
|
||||
None,
|
||||
);
|
||||
self.data_columns.columns[3][(x, idx)].set_bg(color);
|
||||
if _x < min_width.3 {
|
||||
self.data_columns.columns[3][(_x, idx)]
|
||||
self.data_columns.columns[4][(x, idx)].set_bg(color);
|
||||
if _x < min_width.4 {
|
||||
self.data_columns.columns[4][(_x, idx)]
|
||||
.set_bg(color)
|
||||
.set_keep_bg(true);
|
||||
}
|
||||
for x in (x + 1).._x {
|
||||
self.data_columns.columns[3][(x, idx)]
|
||||
self.data_columns.columns[4][(x, idx)]
|
||||
.set_keep_fg(true)
|
||||
.set_keep_bg(true)
|
||||
.set_keep_attrs(true);
|
||||
}
|
||||
self.data_columns.columns[3][(x, idx)].set_keep_bg(true);
|
||||
self.data_columns.columns[4][(x, idx)].set_keep_bg(true);
|
||||
x = _x + 1;
|
||||
}
|
||||
x
|
||||
};
|
||||
for x in x..min_width.3 {
|
||||
self.data_columns.columns[3][(x, idx)]
|
||||
for x in x..min_width.4 {
|
||||
self.data_columns.columns[4][(x, idx)]
|
||||
.set_ch(' ')
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
}
|
||||
/* Set fg color for flags */
|
||||
let mut x = 0;
|
||||
if self.selection.get(&thread_hash).cloned().unwrap_or(false) {
|
||||
x += selected_flag_len;
|
||||
}
|
||||
if thread.snoozed() {
|
||||
for x in x..(x + thread_snoozed_flag_len) {
|
||||
self.data_columns.columns[3][(x, idx)]
|
||||
match (thread.snoozed(), thread.has_attachments()) {
|
||||
(true, true) => {
|
||||
self.data_columns.columns[3][(0, idx)]
|
||||
.set_fg(self.color_cache.attachment_flag.fg);
|
||||
self.data_columns.columns[3][(2, idx)]
|
||||
.set_fg(self.color_cache.thread_snooze_flag.fg);
|
||||
}
|
||||
x += thread_snoozed_flag_len;
|
||||
}
|
||||
if thread.unseen() > 0 {
|
||||
x += unseen_flag_len;
|
||||
}
|
||||
if thread.has_attachments() {
|
||||
for x in x..(x + attachment_flag_len) {
|
||||
self.data_columns.columns[3][(x, idx)]
|
||||
(true, false) => {
|
||||
self.data_columns.columns[3][(0, idx)]
|
||||
.set_fg(self.color_cache.thread_snooze_flag.fg);
|
||||
}
|
||||
(false, true) => {
|
||||
self.data_columns.columns[3][(0, idx)]
|
||||
.set_fg(self.color_cache.attachment_flag.fg);
|
||||
}
|
||||
(false, false) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -172,8 +172,13 @@ impl MailListingTrait for ConversationsListing {
|
|||
subject: crate::conf::value(context, "mail.listing.conversations.subject"),
|
||||
from: crate::conf::value(context, "mail.listing.conversations.from"),
|
||||
date: crate::conf::value(context, "mail.listing.conversations.date"),
|
||||
padding: crate::conf::value(context, "mail.listing.conversations.padding"),
|
||||
selected: crate::conf::value(context, "mail.listing.conversations.selected"),
|
||||
unseen: crate::conf::value(context, "mail.listing.conversations.unseen"),
|
||||
unseen_padding: crate::conf::value(
|
||||
context,
|
||||
"mail.listing.conversations.unseen_padding",
|
||||
),
|
||||
highlighted: crate::conf::value(context, "mail.listing.conversations.highlighted"),
|
||||
attachment_flag: crate::conf::value(context, "mail.listing.attachment_flag"),
|
||||
thread_snooze_flag: crate::conf::value(context, "mail.listing.thread_snooze_flag"),
|
||||
|
@ -345,6 +350,8 @@ impl MailListingTrait for ConversationsListing {
|
|||
let width = max_entry_columns;
|
||||
self.content = CellBuffer::new_with_context(width, 4 * rows.len(), None, context);
|
||||
|
||||
let padding_fg = self.color_cache.padding.fg;
|
||||
|
||||
for ((idx, (thread_hash, root_env_hash)), strings) in rows {
|
||||
if !context.accounts[&self.cursor_pos.0].contains_key(root_env_hash) {
|
||||
panic!();
|
||||
|
@ -463,14 +470,12 @@ impl MailListingTrait for ConversationsListing {
|
|||
.set_fg(row_attr.fg)
|
||||
.set_bg(row_attr.bg);
|
||||
}
|
||||
/*
|
||||
for x in 0..width {
|
||||
self.content[(x, 3 * idx + 2)]
|
||||
.set_ch(Self::PADDING_CHAR)
|
||||
.set_fg(row_attr.fg)
|
||||
.set_ch('▓')
|
||||
.set_fg(padding_fg)
|
||||
.set_bg(row_attr.bg);
|
||||
}
|
||||
*/
|
||||
}
|
||||
if self.length == 0 && self.filter_term.is_empty() {
|
||||
let message: String = account[&self.cursor_pos.1].status();
|
||||
|
@ -520,6 +525,12 @@ impl ListingTrait for ConversationsListing {
|
|||
self.selection[&thread_hash]
|
||||
);
|
||||
|
||||
let padding_fg = if thread.unseen() > 0 {
|
||||
self.color_cache.unseen_padding.fg
|
||||
} else {
|
||||
self.color_cache.padding.fg
|
||||
};
|
||||
|
||||
copy_area(
|
||||
grid,
|
||||
&self.content,
|
||||
|
@ -541,12 +552,10 @@ impl ListingTrait for ConversationsListing {
|
|||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
|
||||
/*
|
||||
grid[(x, y + 2)]
|
||||
.set_fg(row_attr.fg)
|
||||
.set_fg(padding_fg)
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
*/
|
||||
}
|
||||
}
|
||||
if width < width!(area) {
|
||||
|
@ -562,12 +571,10 @@ impl ListingTrait for ConversationsListing {
|
|||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
|
||||
/*
|
||||
grid[(x, y + 2)]
|
||||
.set_fg(row_attr.fg)
|
||||
.set_fg(padding_fg)
|
||||
.set_bg(row_attr.bg)
|
||||
.set_attrs(row_attr.attrs);
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -647,7 +654,7 @@ impl ListingTrait for ConversationsListing {
|
|||
}
|
||||
let new_area = (
|
||||
set_y(upper_left, get_y(upper_left) + 3 * (*idx % rows)),
|
||||
set_y(bottom_right, get_y(upper_left) + 3 * (*idx % rows) + 1),
|
||||
set_y(bottom_right, get_y(upper_left) + 3 * (*idx % rows) + 2),
|
||||
);
|
||||
self.highlight_line(grid, new_area, *idx, context);
|
||||
context.dirty_areas.push_back(new_area);
|
||||
|
@ -685,7 +692,7 @@ impl ListingTrait for ConversationsListing {
|
|||
pos_inc(upper_left, (0, 3 * (self.cursor_pos.2 % rows))),
|
||||
set_y(
|
||||
bottom_right,
|
||||
get_y(upper_left) + 3 * (self.cursor_pos.2 % rows) + 1,
|
||||
get_y(upper_left) + 3 * (self.cursor_pos.2 % rows) + 2,
|
||||
),
|
||||
),
|
||||
self.cursor_pos.2,
|
||||
|
@ -709,40 +716,36 @@ impl ListingTrait for ConversationsListing {
|
|||
|
||||
/* fill any remaining columns, if our view is wider than self.content */
|
||||
let width = self.content.size().0;
|
||||
let padding_fg = self.color_cache.padding.fg;
|
||||
|
||||
if width < width!(area) {
|
||||
let y_offset = get_y(upper_left);
|
||||
for y in 0..rows {
|
||||
let fg_color = grid[(get_x(upper_left) + width - 1, y_offset + 3 * y)].fg();
|
||||
let bg_color = grid[(get_x(upper_left) + width - 1, y_offset + 3 * y)].bg();
|
||||
for x in (get_x(upper_left) + width)..=get_x(bottom_right) {
|
||||
grid[(x, y_offset + 3 * y)].set_bg(bg_color);
|
||||
grid[(x, y_offset + 3 * y + 1)]
|
||||
.set_ch('▁')
|
||||
.set_fg(fg_color)
|
||||
.set_fg(self.color_cache.theme_default.fg)
|
||||
.set_bg(bg_color);
|
||||
/*
|
||||
grid[(x, y_offset + 3 * y + 2)]
|
||||
.set_ch(Self::PADDING_CHAR)
|
||||
.set_fg(fg_color)
|
||||
.set_ch('▓')
|
||||
.set_fg(padding_fg)
|
||||
.set_bg(bg_color);
|
||||
*/
|
||||
}
|
||||
}
|
||||
if pad > 0 {
|
||||
let y = 3 * rows;
|
||||
let _fg_color = grid[(get_x(upper_left) + width - 1, y_offset + y)].fg();
|
||||
let bg_color = grid[(get_x(upper_left) + width - 1, y_offset + y)].bg();
|
||||
for x in (get_x(upper_left) + width)..=get_x(bottom_right) {
|
||||
grid[(x, y_offset + y)].set_bg(bg_color);
|
||||
grid[(x, y_offset + y + 1)].set_ch('▁').set_bg(bg_color);
|
||||
grid[(x, y_offset + y + 1)].set_ch('▁');
|
||||
grid[(x, y_offset + y + 1)].set_bg(bg_color);
|
||||
if pad == 2 {
|
||||
/*
|
||||
grid[(x, y_offset + y + 2)]
|
||||
.set_ch(Self::PADDING_CHAR)
|
||||
.set_fg(fg_color)
|
||||
.set_ch('▓')
|
||||
.set_fg(padding_fg)
|
||||
.set_bg(bg_color);
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -867,8 +870,6 @@ impl fmt::Display for ConversationsListing {
|
|||
|
||||
impl ConversationsListing {
|
||||
const DESCRIPTION: &'static str = "conversations listing";
|
||||
//const PADDING_CHAR: char = ' '; //░';
|
||||
|
||||
pub fn new(coordinates: (AccountHash, MailboxHash)) -> Self {
|
||||
ConversationsListing {
|
||||
cursor_pos: (coordinates.0, 1, 0),
|
||||
|
@ -1039,6 +1040,12 @@ impl ConversationsListing {
|
|||
self.selection[&thread_hash]
|
||||
);
|
||||
|
||||
let padding_fg = if thread.unseen() > 0 {
|
||||
self.color_cache.unseen_padding.fg
|
||||
} else {
|
||||
self.color_cache.padding.fg
|
||||
};
|
||||
|
||||
let mut from_address_list = Vec::new();
|
||||
let mut from_address_set: std::collections::HashSet<Vec<u8>> =
|
||||
std::collections::HashSet::new();
|
||||
|
@ -1174,14 +1181,12 @@ impl ConversationsListing {
|
|||
.set_fg(row_attr.fg)
|
||||
.set_bg(row_attr.bg);
|
||||
}
|
||||
/*
|
||||
for c in self.content.row_iter(0..width, 3 * idx + 2) {
|
||||
self.content[c]
|
||||
.set_ch(Self::PADDING_CHAR)
|
||||
.set_fg(row_attr.fg)
|
||||
.set_ch('▓')
|
||||
.set_fg(padding_fg)
|
||||
.set_bg(row_attr.bg);
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1428,7 +1433,7 @@ impl Component for ConversationsListing {
|
|||
if row >= top_idx && row < top_idx + rows {
|
||||
let area = (
|
||||
set_y(upper_left, get_y(upper_left) + (3 * (row % rows))),
|
||||
set_y(bottom_right, get_y(upper_left) + (3 * (row % rows) + 1)),
|
||||
set_y(bottom_right, get_y(upper_left) + (3 * (row % rows) + 2)),
|
||||
);
|
||||
self.highlight_line(grid, area, row, context);
|
||||
context.dirty_areas.push_back(area);
|
||||
|
|
|
@ -384,7 +384,7 @@ impl ListingTrait for PlainListing {
|
|||
pos_dec(self.data_columns.columns[3].size(), (1, 1)),
|
||||
),
|
||||
);
|
||||
for c in grid.row_iter(x..get_x(bottom_right), get_y(upper_left)) {
|
||||
for c in grid.row_iter(x..(x + self.data_columns.widths[3]), get_y(upper_left)) {
|
||||
grid[c].set_bg(row_attr.bg).set_attrs(row_attr.attrs);
|
||||
}
|
||||
}
|
||||
|
@ -482,25 +482,31 @@ impl ListingTrait for PlainListing {
|
|||
self.data_columns.widths[0] = self.data_columns.columns[0].size().0;
|
||||
self.data_columns.widths[1] = self.data_columns.columns[1].size().0; /* date*/
|
||||
self.data_columns.widths[2] = self.data_columns.columns[2].size().0; /* from */
|
||||
self.data_columns.widths[3] = self.data_columns.columns[3].size().0; /* subject */
|
||||
self.data_columns.widths[3] = self.data_columns.columns[3].size().0; /* flags */
|
||||
self.data_columns.widths[4] = self.data_columns.columns[4].size().0; /* subject */
|
||||
|
||||
let min_col_width = std::cmp::min(
|
||||
15,
|
||||
std::cmp::min(self.data_columns.widths[3], self.data_columns.widths[2]),
|
||||
std::cmp::min(self.data_columns.widths[4], self.data_columns.widths[2]),
|
||||
);
|
||||
if self.data_columns.widths[0] + self.data_columns.widths[1] + 2 * min_col_width + 4 > width
|
||||
if self.data_columns.widths[0] + self.data_columns.widths[1] + 3 * min_col_width + 8 > width
|
||||
{
|
||||
let remainder = width
|
||||
.saturating_sub(self.data_columns.widths[0])
|
||||
.saturating_sub(self.data_columns.widths[1])
|
||||
.saturating_sub(2 * 2);
|
||||
.saturating_sub(4);
|
||||
self.data_columns.widths[2] = remainder / 6;
|
||||
self.data_columns.widths[4] =
|
||||
((2 * remainder) / 3).saturating_sub(self.data_columns.widths[3]);
|
||||
} else {
|
||||
let remainder = width
|
||||
.saturating_sub(self.data_columns.widths[0])
|
||||
.saturating_sub(self.data_columns.widths[1])
|
||||
.saturating_sub(3 * 2);
|
||||
if min_col_width + self.data_columns.widths[3] > remainder {
|
||||
.saturating_sub(8);
|
||||
if min_col_width + self.data_columns.widths[4] > remainder {
|
||||
self.data_columns.widths[4] = remainder
|
||||
.saturating_sub(min_col_width)
|
||||
.saturating_sub(self.data_columns.widths[3]);
|
||||
self.data_columns.widths[2] = min_col_width;
|
||||
}
|
||||
}
|
||||
|
@ -508,24 +514,30 @@ impl ListingTrait for PlainListing {
|
|||
/* Page_no has changed, so draw new page */
|
||||
let mut x = get_x(upper_left);
|
||||
let mut flag_x = 0;
|
||||
for i in 0..4 {
|
||||
let column_width = self.data_columns.widths[i];
|
||||
for i in 0..self.data_columns.columns.len() {
|
||||
let column_width = self.data_columns.columns[i].size().0;
|
||||
if i == 3 {
|
||||
flag_x = x;
|
||||
}
|
||||
if column_width == 0 {
|
||||
if self.data_columns.widths[i] == 0 {
|
||||
continue;
|
||||
}
|
||||
copy_area(
|
||||
grid,
|
||||
&self.data_columns.columns[i],
|
||||
(set_x(upper_left, x), bottom_right),
|
||||
(
|
||||
set_x(upper_left, x),
|
||||
set_x(
|
||||
bottom_right,
|
||||
std::cmp::min(get_x(bottom_right), x + (self.data_columns.widths[i])),
|
||||
),
|
||||
),
|
||||
(
|
||||
(0, top_idx),
|
||||
(column_width.saturating_sub(1), self.length - 1),
|
||||
),
|
||||
);
|
||||
x += column_width + 2; // + SEPARATOR
|
||||
x += self.data_columns.widths[i] + 2; // + SEPARATOR
|
||||
if x > get_x(bottom_right) {
|
||||
break;
|
||||
}
|
||||
|
@ -760,53 +772,7 @@ impl PlainListing {
|
|||
EntryStrings {
|
||||
date: DateString(PlainListing::format_date(&e)),
|
||||
subject: SubjectString(subject),
|
||||
flag: FlagString(format!(
|
||||
"{selected}{unseen}{attachments}{whitespace}",
|
||||
selected = if self.selection.get(&e.hash()).cloned().unwrap_or(false) {
|
||||
mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.selected_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_SELECTED_FLAG)
|
||||
} else {
|
||||
""
|
||||
},
|
||||
unseen = if !e.is_seen() {
|
||||
mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.unseen_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_UNSEEN_FLAG)
|
||||
} else {
|
||||
""
|
||||
},
|
||||
attachments = if e.has_attachments() {
|
||||
mailbox_settings!(
|
||||
context[self.cursor_pos.0][&self.cursor_pos.1]
|
||||
.listing
|
||||
.attachment_flag
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(super::DEFAULT_ATTACHMENT_FLAG)
|
||||
} else {
|
||||
""
|
||||
},
|
||||
whitespace = if self.selection.get(&e.hash()).cloned().unwrap_or(false)
|
||||
|| !e.is_seen()
|
||||
|| e.has_attachments()
|
||||
{
|
||||
" "
|
||||
} else {
|
||||
""
|
||||
},
|
||||
)),
|
||||
flag: FlagString(format!("{}", if e.has_attachments() { "📎" } else { "" },)),
|
||||
from: FromString(address_list!((e.from()) as comma_sep_list)),
|
||||
tags: TagString(tags, colors),
|
||||
}
|
||||
|
@ -851,12 +817,10 @@ impl PlainListing {
|
|||
let entry_strings = self.make_entry_string(envelope, context);
|
||||
min_width.1 = cmp::max(min_width.1, entry_strings.date.grapheme_width()); /* date */
|
||||
min_width.2 = cmp::max(min_width.2, entry_strings.from.grapheme_width()); /* from */
|
||||
min_width.3 = cmp::max(
|
||||
min_width.3,
|
||||
entry_strings.flag.grapheme_width()
|
||||
+ entry_strings.subject.grapheme_width()
|
||||
+ 1
|
||||
+ entry_strings.tags.grapheme_width(),
|
||||
min_width.3 = cmp::max(min_width.3, entry_strings.flag.grapheme_width()); /* flags */
|
||||
min_width.4 = cmp::max(
|
||||
min_width.4,
|
||||
entry_strings.subject.grapheme_width() + 1 + entry_strings.tags.grapheme_width(),
|
||||
); /* tags + subject */
|
||||
rows.push(entry_strings);
|
||||
|
||||
|
@ -876,9 +840,12 @@ impl PlainListing {
|
|||
/* from column */
|
||||
self.data_columns.columns[2] =
|
||||
CellBuffer::new_with_context(min_width.2, rows.len(), None, context);
|
||||
/* subject column */
|
||||
/* flags column */
|
||||
self.data_columns.columns[3] =
|
||||
CellBuffer::new_with_context(min_width.3, rows.len(), None, context);
|
||||
/* subject column */
|
||||
self.data_columns.columns[4] =
|
||||
CellBuffer::new_with_context(min_width.4, rows.len(), None, context);
|
||||
|
||||
let iter = if self.filter_term.is_empty() {
|
||||
Box::new(self.local_collection.iter().cloned())
|
||||
|
@ -956,13 +923,16 @@ impl PlainListing {
|
|||
((0, idx), (min_width.3, idx)),
|
||||
None,
|
||||
);
|
||||
for c in columns[3].row_iter(x..min_width.3, idx) {
|
||||
columns[3][c].set_bg(row_attr.bg).set_attrs(row_attr.attrs);
|
||||
}
|
||||
let (x, _) = write_string_to_grid(
|
||||
&strings.subject,
|
||||
&mut columns[3],
|
||||
&mut columns[4],
|
||||
row_attr.fg,
|
||||
row_attr.bg,
|
||||
row_attr.attrs,
|
||||
((x, idx), (min_width.3, idx)),
|
||||
((0, idx), (min_width.4, idx)),
|
||||
None,
|
||||
);
|
||||
let x = {
|
||||
|
@ -971,42 +941,38 @@ impl PlainListing {
|
|||
let color = color.unwrap_or(self.color_cache.tag_default.bg);
|
||||
let (_x, _) = write_string_to_grid(
|
||||
t,
|
||||
&mut columns[3],
|
||||
&mut columns[4],
|
||||
self.color_cache.tag_default.fg,
|
||||
color,
|
||||
self.color_cache.tag_default.attrs,
|
||||
((x + 1, idx), (min_width.3, idx)),
|
||||
((x + 1, idx), (min_width.4, idx)),
|
||||
None,
|
||||
);
|
||||
for c in columns[3].row_iter(x..(x + 1), idx) {
|
||||
columns[3][c].set_bg(color);
|
||||
for c in columns[4].row_iter(x..(x + 1), idx) {
|
||||
columns[4][c].set_bg(color);
|
||||
}
|
||||
for c in columns[3].row_iter(_x..(_x + 1), idx) {
|
||||
columns[3][c].set_bg(color).set_keep_bg(true);
|
||||
for c in columns[4].row_iter(_x..(_x + 1), idx) {
|
||||
columns[4][c].set_bg(color).set_keep_bg(true);
|
||||
}
|
||||
for c in columns[3].row_iter((x + 1)..(_x + 1), idx) {
|
||||
columns[3][c].set_keep_fg(true).set_keep_bg(true);
|
||||
for c in columns[4].row_iter((x + 1)..(_x + 1), idx) {
|
||||
columns[4][c].set_keep_fg(true).set_keep_bg(true);
|
||||
}
|
||||
for c in columns[3].row_iter(x..(x + 1), idx) {
|
||||
columns[3][c].set_keep_bg(true);
|
||||
for c in columns[4].row_iter(x..(x + 1), idx) {
|
||||
columns[4][c].set_keep_bg(true);
|
||||
}
|
||||
x = _x + 1;
|
||||
}
|
||||
x
|
||||
};
|
||||
for c in columns[3].row_iter(x..min_width.3, idx) {
|
||||
columns[3][c].set_bg(row_attr.bg).set_attrs(row_attr.attrs);
|
||||
for c in columns[4].row_iter(x..min_width.4, idx) {
|
||||
columns[4][c].set_bg(row_attr.bg).set_attrs(row_attr.attrs);
|
||||
}
|
||||
/* Set fg color for flags */
|
||||
let mut x = 0;
|
||||
if self.selection.get(&i).cloned().unwrap_or(false) {
|
||||
x += 1;
|
||||
}
|
||||
if !envelope.is_seen() {
|
||||
x += 1;
|
||||
}
|
||||
if envelope.has_attachments() {
|
||||
columns[3][(x, idx)].set_fg(self.color_cache.attachment_flag.fg);
|
||||
if context.accounts[&self.cursor_pos.0]
|
||||
.collection
|
||||
.get_env(i)
|
||||
.has_attachments()
|
||||
{
|
||||
columns[3][(0, idx)].set_fg(Color::Byte(103));
|
||||
}
|
||||
}
|
||||
if self.length == 0 && self.filter_term.is_empty() {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -168,13 +168,11 @@ pub struct MailView {
|
|||
id: ComponentId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub enum PendingReplyAction {
|
||||
Reply,
|
||||
ReplyToAuthor,
|
||||
ReplyToAll,
|
||||
ForwardAttachment,
|
||||
ForwardInline,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -191,7 +189,6 @@ enum MailViewState {
|
|||
},
|
||||
Loaded {
|
||||
bytes: Vec<u8>,
|
||||
env: Envelope,
|
||||
body: Attachment,
|
||||
display: Vec<AttachmentDisplay>,
|
||||
body_text: String,
|
||||
|
@ -313,8 +310,6 @@ impl MailView {
|
|||
.get_env_mut(self.coordinates.2)
|
||||
.populate_headers(&bytes);
|
||||
}
|
||||
let env =
|
||||
account.collection.get_env(self.coordinates.2).clone();
|
||||
let body = AttachmentBuilder::new(&bytes).build();
|
||||
let display = Self::attachment_to(
|
||||
&body,
|
||||
|
@ -330,7 +325,6 @@ impl MailView {
|
|||
self.attachment_displays_to_text(&display, context, true);
|
||||
self.state = MailViewState::Loaded {
|
||||
display,
|
||||
env,
|
||||
body,
|
||||
bytes,
|
||||
body_text,
|
||||
|
@ -394,7 +388,7 @@ impl MailView {
|
|||
}
|
||||
|
||||
fn perform_action(&mut self, action: PendingReplyAction, context: &mut Context) {
|
||||
let (bytes, reply_body, env) = match self.state {
|
||||
let reply_body = match self.state {
|
||||
MailViewState::Init {
|
||||
ref mut pending_action,
|
||||
..
|
||||
|
@ -408,16 +402,9 @@ impl MailView {
|
|||
}
|
||||
return;
|
||||
}
|
||||
MailViewState::Loaded {
|
||||
ref bytes,
|
||||
ref display,
|
||||
ref env,
|
||||
..
|
||||
} => (
|
||||
bytes,
|
||||
self.attachment_displays_to_text(&display, context, false),
|
||||
env,
|
||||
),
|
||||
MailViewState::Loaded { ref display, .. } => {
|
||||
self.attachment_displays_to_text(&display, context, false)
|
||||
}
|
||||
MailViewState::Error { .. } => {
|
||||
return;
|
||||
}
|
||||
|
@ -438,20 +425,6 @@ impl MailView {
|
|||
reply_body,
|
||||
context,
|
||||
)),
|
||||
PendingReplyAction::ForwardAttachment => Box::new(Composer::forward(
|
||||
self.coordinates,
|
||||
bytes,
|
||||
env,
|
||||
true,
|
||||
context,
|
||||
)),
|
||||
PendingReplyAction::ForwardInline => Box::new(Composer::forward(
|
||||
self.coordinates,
|
||||
bytes,
|
||||
env,
|
||||
false,
|
||||
context,
|
||||
)),
|
||||
};
|
||||
|
||||
context
|
||||
|
@ -934,7 +907,7 @@ impl MailView {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
rec(body, context, coordinates, &mut ret, active_jobs);
|
||||
ret
|
||||
}
|
||||
|
@ -1057,41 +1030,6 @@ impl MailView {
|
|||
))));
|
||||
None
|
||||
}
|
||||
|
||||
fn start_contact_selector(&mut self, context: &mut Context) {
|
||||
let account = &context.accounts[&self.coordinates.0];
|
||||
if !account.contains_key(self.coordinates.2) {
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(
|
||||
"Email not found".into(),
|
||||
)));
|
||||
return;
|
||||
}
|
||||
let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
|
||||
|
||||
let mut entries = Vec::new();
|
||||
for addr in envelope.from().iter().chain(envelope.to().iter()) {
|
||||
let mut new_card: Card = Card::new();
|
||||
new_card.set_email(addr.get_email());
|
||||
if let Some(display_name) = addr.get_display_name() {
|
||||
new_card.set_name(display_name);
|
||||
}
|
||||
entries.push((new_card, format!("{}", addr)));
|
||||
}
|
||||
drop(envelope);
|
||||
self.mode = ViewMode::ContactSelector(Selector::new(
|
||||
"select contacts to add",
|
||||
entries,
|
||||
false,
|
||||
Some(Box::new(move |id: ComponentId, results: &[Card]| {
|
||||
Some(UIEvent::FinishedUIDialog(id, Box::new(results.to_vec())))
|
||||
})),
|
||||
context,
|
||||
));
|
||||
self.dirty = true;
|
||||
self.initialised = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for MailView {
|
||||
|
@ -1173,52 +1111,8 @@ impl Component for MailView {
|
|||
})+
|
||||
};
|
||||
}
|
||||
let find_offset = |s: &str| -> (bool, (i64, i64)) {
|
||||
let mut diff = (true, (0, 0));
|
||||
if let Some(pos) = s.as_bytes().iter().position(|b| *b == b'+' || *b == b'-') {
|
||||
let offset = &s[pos..];
|
||||
diff.0 = offset.starts_with('+');
|
||||
if let (Ok(hr_offset), Ok(min_offset)) =
|
||||
(offset[1..3].parse::<i64>(), offset[3..5].parse::<i64>())
|
||||
{
|
||||
diff.1 .0 = hr_offset;
|
||||
diff.1 .1 = min_offset;
|
||||
}
|
||||
}
|
||||
diff
|
||||
};
|
||||
let orig_date = envelope.date_as_str();
|
||||
let date_str: std::borrow::Cow<str> = if mailbox_settings!(
|
||||
context[self.coordinates.0][&self.coordinates.1]
|
||||
.pager
|
||||
.show_date_in_my_timezone
|
||||
)
|
||||
.is_true()
|
||||
{
|
||||
let local_date = melib::datetime::timestamp_to_string(
|
||||
envelope.timestamp,
|
||||
Some(melib::datetime::RFC822_DATE),
|
||||
false,
|
||||
);
|
||||
let orig_offset = find_offset(orig_date);
|
||||
let local_offset = find_offset(&local_date);
|
||||
if orig_offset == local_offset {
|
||||
orig_date.into()
|
||||
} else {
|
||||
format!(
|
||||
"{} [actual timezone: {}{:02}{:02}]",
|
||||
local_date,
|
||||
if orig_offset.0 { '+' } else { '-' },
|
||||
orig_offset.1 .0,
|
||||
orig_offset.1 .1
|
||||
)
|
||||
.into()
|
||||
}
|
||||
} else {
|
||||
orig_date.into()
|
||||
};
|
||||
print_header!(
|
||||
("Date:", date_str),
|
||||
("Date:", envelope.date_as_str()),
|
||||
("From:", envelope.field_from_to_string()),
|
||||
("To:", envelope.field_to_to_string()),
|
||||
);
|
||||
|
@ -1451,26 +1345,12 @@ impl Component for MailView {
|
|||
let colors = crate::conf::value(context, "mail.view.body");
|
||||
self.pager =
|
||||
Pager::from_string(text, Some(context), Some(0), None, colors);
|
||||
if let Some(ref filter) = mailbox_settings!(
|
||||
context[self.coordinates.0][&self.coordinates.1]
|
||||
.pager
|
||||
.filter
|
||||
) {
|
||||
self.pager.filter(filter);
|
||||
}
|
||||
self.subview = None;
|
||||
}
|
||||
} else {
|
||||
text.push_str("Internal error. MailView::open_attachment failed.");
|
||||
let colors = crate::conf::value(context, "mail.view.body");
|
||||
self.pager = Pager::from_string(text, Some(context), Some(0), None, colors);
|
||||
if let Some(ref filter) = mailbox_settings!(
|
||||
context[self.coordinates.0][&self.coordinates.1]
|
||||
.pager
|
||||
.filter
|
||||
) {
|
||||
self.pager.filter(filter);
|
||||
}
|
||||
self.subview = None;
|
||||
}
|
||||
}
|
||||
|
@ -1551,13 +1431,6 @@ impl Component for MailView {
|
|||
};
|
||||
let colors = crate::conf::value(context, "mail.view.body");
|
||||
self.pager = Pager::from_string(text, Some(context), None, None, colors);
|
||||
if let Some(ref filter) = mailbox_settings!(
|
||||
context[self.coordinates.0][&self.coordinates.1]
|
||||
.pager
|
||||
.filter
|
||||
) {
|
||||
self.pager.filter(filter);
|
||||
}
|
||||
}
|
||||
/*
|
||||
ViewMode::Ansi(ref buf) => {
|
||||
|
@ -1609,13 +1482,6 @@ impl Component for MailView {
|
|||
let colors = crate::conf::value(context, "mail.view.body");
|
||||
self.pager =
|
||||
Pager::from_string(text, Some(context), Some(cursor_pos), None, colors);
|
||||
if let Some(ref filter) = mailbox_settings!(
|
||||
context[self.coordinates.0][&self.coordinates.1]
|
||||
.pager
|
||||
.filter
|
||||
) {
|
||||
self.pager.filter(filter);
|
||||
}
|
||||
self.subview = None;
|
||||
}
|
||||
_ => {
|
||||
|
@ -1632,13 +1498,6 @@ impl Component for MailView {
|
|||
let colors = crate::conf::value(context, "mail.view.body");
|
||||
self.pager =
|
||||
Pager::from_string(text, Some(context), Some(cursor_pos), None, colors);
|
||||
if let Some(ref filter) = mailbox_settings!(
|
||||
context[self.coordinates.0][&self.coordinates.1]
|
||||
.pager
|
||||
.filter
|
||||
) {
|
||||
self.pager.filter(filter);
|
||||
}
|
||||
self.subview = None;
|
||||
}
|
||||
};
|
||||
|
@ -1764,10 +1623,6 @@ impl Component for MailView {
|
|||
.get_env_mut(self.coordinates.2)
|
||||
.populate_headers(&bytes);
|
||||
}
|
||||
let env = context.accounts[&self.coordinates.0]
|
||||
.collection
|
||||
.get_env(self.coordinates.2)
|
||||
.clone();
|
||||
let body = AttachmentBuilder::new(&bytes).build();
|
||||
let display = Self::attachment_to(
|
||||
&body,
|
||||
|
@ -1783,7 +1638,6 @@ impl Component for MailView {
|
|||
self.attachment_displays_to_text(&display, context, true);
|
||||
self.state = MailViewState::Loaded {
|
||||
bytes,
|
||||
env,
|
||||
body,
|
||||
display,
|
||||
links: vec![],
|
||||
|
@ -1939,53 +1793,6 @@ impl Component for MailView {
|
|||
self.perform_action(PendingReplyAction::ReplyToAuthor, context);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["forward"]) =>
|
||||
{
|
||||
match mailbox_settings!(
|
||||
context[self.coordinates.0][&self.coordinates.1]
|
||||
.composing
|
||||
.forward_as_attachment
|
||||
) {
|
||||
f if f.is_ask() => {
|
||||
let id = self.id;
|
||||
context.replies.push_back(UIEvent::GlobalUIDialog(Box::new(
|
||||
UIConfirmationDialog::new(
|
||||
"How do you want the email to be forwarded?",
|
||||
vec![
|
||||
(true, "inline".to_string()),
|
||||
(false, "as attachment".to_string()),
|
||||
],
|
||||
true,
|
||||
Some(Box::new(move |_: ComponentId, result: bool| {
|
||||
Some(UIEvent::FinishedUIDialog(
|
||||
id,
|
||||
Box::new(if result {
|
||||
PendingReplyAction::ForwardInline
|
||||
} else {
|
||||
PendingReplyAction::ForwardAttachment
|
||||
}),
|
||||
))
|
||||
})),
|
||||
context,
|
||||
),
|
||||
)));
|
||||
}
|
||||
f if f.is_true() => {
|
||||
self.perform_action(PendingReplyAction::ForwardAttachment, context);
|
||||
}
|
||||
_ => {
|
||||
self.perform_action(PendingReplyAction::ForwardInline, context);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
UIEvent::FinishedUIDialog(id, ref result) if id == self.id() => {
|
||||
if let Some(result) = result.downcast_ref::<PendingReplyAction>() {
|
||||
self.perform_action(*result, context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if shortcut!(key == shortcuts[MailView::DESCRIPTION]["edit"]) =>
|
||||
{
|
||||
|
@ -2057,17 +1864,36 @@ impl Component for MailView {
|
|||
);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Action(View(ViewAction::AddAddressesToContacts)) => {
|
||||
self.start_contact_selector(context);
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if !self.mode.is_contact_selector()
|
||||
&& shortcut!(
|
||||
key == shortcuts[MailView::DESCRIPTION]["add_addresses_to_contacts"]
|
||||
) =>
|
||||
{
|
||||
self.start_contact_selector(context);
|
||||
let account = &context.accounts[&self.coordinates.0];
|
||||
let envelope: EnvelopeRef = account.collection.get_env(self.coordinates.2);
|
||||
|
||||
let mut entries = Vec::new();
|
||||
for addr in envelope.from().iter().chain(envelope.to().iter()) {
|
||||
let mut new_card: Card = Card::new();
|
||||
new_card.set_email(addr.get_email());
|
||||
if let Some(display_name) = addr.get_display_name() {
|
||||
new_card.set_name(display_name);
|
||||
}
|
||||
entries.push((new_card, format!("{}", addr)));
|
||||
}
|
||||
drop(envelope);
|
||||
self.mode = ViewMode::ContactSelector(Selector::new(
|
||||
"select contacts to add",
|
||||
entries,
|
||||
false,
|
||||
Some(Box::new(move |id: ComponentId, results: &[Card]| {
|
||||
Some(UIEvent::FinishedUIDialog(id, Box::new(results.to_vec())))
|
||||
})),
|
||||
context,
|
||||
));
|
||||
self.dirty = true;
|
||||
self.initialised = false;
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
|
||||
|
@ -2306,7 +2132,6 @@ impl Component for MailView {
|
|||
body: _,
|
||||
bytes: _,
|
||||
display: _,
|
||||
env: _,
|
||||
ref body_text,
|
||||
ref links,
|
||||
} => {
|
||||
|
@ -2327,15 +2152,7 @@ impl Component for MailView {
|
|||
}
|
||||
};
|
||||
|
||||
let url_launcher = mailbox_settings!(
|
||||
context[self.coordinates.0][&self.coordinates.1]
|
||||
.pager
|
||||
.url_launcher
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("xdg-open");
|
||||
match Command::new(url_launcher)
|
||||
match Command::new("xdg-open")
|
||||
.arg(url)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
|
@ -2346,7 +2163,7 @@ impl Component for MailView {
|
|||
}
|
||||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::Notification(
|
||||
Some(format!("Failed to launch {:?}", url_launcher)),
|
||||
Some("Failed to launch xdg-open".to_string()),
|
||||
err.to_string(),
|
||||
Some(NotificationType::Error(melib::ErrorKind::External)),
|
||||
));
|
||||
|
@ -2615,15 +2432,7 @@ impl Component for MailView {
|
|||
}
|
||||
}
|
||||
list_management::ListAction::Url(url) => {
|
||||
let url_launcher = mailbox_settings!(
|
||||
context[self.coordinates.0][&self.coordinates.1]
|
||||
.pager
|
||||
.url_launcher
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("xdg-open");
|
||||
match Command::new(url_launcher)
|
||||
match Command::new("xdg-open")
|
||||
.arg(String::from_utf8_lossy(url).into_owned())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
|
@ -2635,8 +2444,8 @@ impl Component for MailView {
|
|||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(format!(
|
||||
"Couldn't launch {:?}: {}",
|
||||
url_launcher, err
|
||||
"Couldn't launch xdg-open: {}",
|
||||
err
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
@ -2648,16 +2457,8 @@ impl Component for MailView {
|
|||
}
|
||||
}
|
||||
MailingListAction::ListArchive if actions.archive.is_some() => {
|
||||
/* open archive url with url_launcher */
|
||||
let url_launcher = mailbox_settings!(
|
||||
context[self.coordinates.0][&self.coordinates.1]
|
||||
.pager
|
||||
.url_launcher
|
||||
)
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("xdg-open");
|
||||
match Command::new(url_launcher)
|
||||
/* open archive url with xdg-open */
|
||||
match Command::new("xdg-open")
|
||||
.arg(actions.archive.unwrap())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
|
@ -2667,8 +2468,8 @@ impl Component for MailView {
|
|||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(format!(
|
||||
"Couldn't launch {:?}: {}",
|
||||
url_launcher, err
|
||||
"Couldn't launch xdg-open: {}",
|
||||
err
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
|
|
@ -528,24 +528,15 @@ impl Component for EnvelopeView {
|
|||
}
|
||||
};
|
||||
|
||||
let url_launcher = context
|
||||
.settings
|
||||
.pager
|
||||
.url_launcher
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("xdg-open");
|
||||
match Command::new(url_launcher)
|
||||
match Command::new("xdg-open")
|
||||
.arg(url)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
{
|
||||
Ok(child) => context.children.push(child),
|
||||
Err(err) => context.replies.push_back(UIEvent::Notification(
|
||||
Some(format!("Failed to launch {:?}", url_launcher)),
|
||||
err.to_string(),
|
||||
Some(NotificationType::Error(melib::ErrorKind::External)),
|
||||
Err(_err) => context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage("Failed to start xdg_open".into()),
|
||||
)),
|
||||
}
|
||||
return true;
|
||||
|
|
|
@ -33,7 +33,6 @@ struct ThreadEntry {
|
|||
dirty: bool,
|
||||
hidden: bool,
|
||||
heading: String,
|
||||
timestamp: UnixTimestamp,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
|
@ -163,26 +162,6 @@ impl ThreadView {
|
|||
}
|
||||
|
||||
fn initiate(&mut self, expanded_hash: Option<ThreadNodeHash>, context: &Context) {
|
||||
#[inline(always)]
|
||||
fn make_entry(
|
||||
i: (usize, ThreadNodeHash, usize),
|
||||
msg_hash: EnvelopeHash,
|
||||
seen: bool,
|
||||
timestamp: UnixTimestamp,
|
||||
) -> ThreadEntry {
|
||||
let (ind, _, _) = i;
|
||||
ThreadEntry {
|
||||
index: i,
|
||||
indentation: ind,
|
||||
msg_hash,
|
||||
seen,
|
||||
dirty: true,
|
||||
hidden: false,
|
||||
heading: String::new(),
|
||||
timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
let account = &context.accounts[&self.coordinates.0];
|
||||
let threads = account.collection.get_threads(self.coordinates.1);
|
||||
|
||||
|
@ -195,13 +174,8 @@ impl ThreadView {
|
|||
for (line, (ind, thread_node_hash)) in thread_iter.enumerate() {
|
||||
let entry = if let Some(msg_hash) = threads.thread_nodes()[&thread_node_hash].message()
|
||||
{
|
||||
let env_ref = account.collection.get_env(msg_hash);
|
||||
make_entry(
|
||||
(ind, thread_node_hash, line),
|
||||
msg_hash,
|
||||
env_ref.is_seen(),
|
||||
env_ref.timestamp,
|
||||
)
|
||||
let seen: bool = account.collection.get_env(msg_hash).is_seen();
|
||||
self.make_entry((ind, thread_node_hash, line), msg_hash, seen)
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
@ -215,13 +189,7 @@ impl ThreadView {
|
|||
}
|
||||
}
|
||||
if expanded_hash.is_none() {
|
||||
self.new_expanded_pos = self
|
||||
.entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.reduce(|a, b| if a.1.timestamp > b.1.timestamp { a } else { b })
|
||||
.map(|el| el.0)
|
||||
.unwrap_or(0);
|
||||
self.new_expanded_pos = self.entries.len().saturating_sub(1);
|
||||
self.expanded_pos = self.new_expanded_pos + 1;
|
||||
}
|
||||
|
||||
|
@ -424,6 +392,24 @@ impl ThreadView {
|
|||
self.visible_entries = vec![(0..self.entries.len()).collect()];
|
||||
}
|
||||
|
||||
fn make_entry(
|
||||
&mut self,
|
||||
i: (usize, ThreadNodeHash, usize),
|
||||
msg_hash: EnvelopeHash,
|
||||
seen: bool,
|
||||
) -> ThreadEntry {
|
||||
let (ind, _, _) = i;
|
||||
ThreadEntry {
|
||||
index: i,
|
||||
indentation: ind,
|
||||
msg_hash,
|
||||
seen,
|
||||
dirty: true,
|
||||
hidden: false,
|
||||
heading: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn highlight_line(
|
||||
&self,
|
||||
grid: &mut CellBuffer,
|
||||
|
|
|
@ -53,7 +53,6 @@ pub struct StatusBar {
|
|||
container: Box<dyn Component>,
|
||||
status: String,
|
||||
status_message: String,
|
||||
substatus_message: String,
|
||||
ex_buffer: Field,
|
||||
ex_buffer_cmd_history_pos: Option<usize>,
|
||||
display_buffer: String,
|
||||
|
@ -97,7 +96,6 @@ impl StatusBar {
|
|||
container,
|
||||
status: String::with_capacity(256),
|
||||
status_message: String::with_capacity(256),
|
||||
substatus_message: String::with_capacity(256),
|
||||
ex_buffer: Field::Text(UText::new(String::with_capacity(256)), None),
|
||||
ex_buffer_cmd_history_pos: None,
|
||||
display_buffer: String::with_capacity(8),
|
||||
|
@ -200,31 +198,6 @@ impl StatusBar {
|
|||
context.dirty_areas.push_back(area);
|
||||
}
|
||||
|
||||
fn update_status(&mut self, context: &Context) {
|
||||
self.status = format!(
|
||||
"{} {}| {}{}{}",
|
||||
self.mode,
|
||||
if self.mouse {
|
||||
context
|
||||
.settings
|
||||
.terminal
|
||||
.mouse_flag
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("🖱️ ")
|
||||
} else {
|
||||
""
|
||||
},
|
||||
&self.status_message,
|
||||
if !self.substatus_message.is_empty() {
|
||||
" | "
|
||||
} else {
|
||||
""
|
||||
},
|
||||
&self.substatus_message,
|
||||
);
|
||||
}
|
||||
|
||||
fn draw_command_bar(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
clear_area(grid, area, crate::conf::value(context, "theme_default"));
|
||||
let (_, y) = write_string_to_grid(
|
||||
|
@ -712,19 +685,42 @@ impl Component for StatusBar {
|
|||
UIEvent::StatusEvent(StatusEvent::UpdateStatus(ref mut s)) => {
|
||||
self.status_message.clear();
|
||||
self.status_message.push_str(s.as_str());
|
||||
self.substatus_message.clear();
|
||||
self.update_status(context);
|
||||
self.dirty = true;
|
||||
}
|
||||
UIEvent::StatusEvent(StatusEvent::UpdateSubStatus(ref mut s)) => {
|
||||
self.substatus_message.clear();
|
||||
self.substatus_message.push_str(s.as_str());
|
||||
self.update_status(context);
|
||||
self.status = format!(
|
||||
"{} {}| {}",
|
||||
self.mode,
|
||||
if self.mouse {
|
||||
context
|
||||
.settings
|
||||
.terminal
|
||||
.mouse_flag
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("🖱️ ")
|
||||
} else {
|
||||
""
|
||||
},
|
||||
&self.status_message,
|
||||
);
|
||||
self.dirty = true;
|
||||
}
|
||||
UIEvent::StatusEvent(StatusEvent::SetMouse(val)) => {
|
||||
self.mouse = *val;
|
||||
self.update_status(context);
|
||||
self.status = format!(
|
||||
"{} {}| {}",
|
||||
self.mode,
|
||||
if self.mouse {
|
||||
context
|
||||
.settings
|
||||
.terminal
|
||||
.mouse_flag
|
||||
.as_ref()
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("🖱️ ")
|
||||
} else {
|
||||
""
|
||||
},
|
||||
&self.status_message,
|
||||
);
|
||||
self.dirty = true;
|
||||
}
|
||||
UIEvent::StatusEvent(StatusEvent::JobCanceled(ref job_id))
|
||||
|
|
|
@ -40,7 +40,6 @@ pub struct Pager {
|
|||
initialised: bool,
|
||||
show_scrollbar: bool,
|
||||
content: CellBuffer,
|
||||
filtered_content: Option<(String, Result<CellBuffer>)>,
|
||||
text_lines: Vec<String>,
|
||||
line_breaker: LineBreakText,
|
||||
movement: Option<PageMovement>,
|
||||
|
@ -111,7 +110,7 @@ impl Pager {
|
|||
}
|
||||
|
||||
pub fn from_string(
|
||||
text: String,
|
||||
mut text: String,
|
||||
context: Option<&Context>,
|
||||
cursor_pos: Option<usize>,
|
||||
mut width: Option<usize>,
|
||||
|
@ -145,7 +144,38 @@ impl Pager {
|
|||
}
|
||||
}
|
||||
|
||||
let mut ret = Pager {
|
||||
if let Some(content) = pager_filter.and_then(|bin| {
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
let mut filter_child = Command::new("sh")
|
||||
.args(&["-c", bin])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("Failed to start pager filter process");
|
||||
{
|
||||
let stdin = filter_child.stdin.as_mut().expect("failed to open stdin");
|
||||
stdin
|
||||
.write_all(text.as_bytes())
|
||||
.expect("Failed to write to stdin");
|
||||
}
|
||||
|
||||
text = String::from_utf8_lossy(
|
||||
&filter_child
|
||||
.wait_with_output()
|
||||
.expect("Failed to wait on filter")
|
||||
.stdout,
|
||||
)
|
||||
.to_string();
|
||||
if text.is_empty() {
|
||||
None
|
||||
} else {
|
||||
crate::terminal::ansi::ansi_to_cellbuffer(&text)
|
||||
}
|
||||
}) {
|
||||
return Pager::from_buf(content, cursor_pos);
|
||||
}
|
||||
Pager {
|
||||
text,
|
||||
text_lines: vec![],
|
||||
reflow,
|
||||
|
@ -156,54 +186,24 @@ impl Pager {
|
|||
initialised: false,
|
||||
dirty: true,
|
||||
id: ComponentId::new_v4(),
|
||||
filtered_content: None,
|
||||
colors,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Some(bin) = pager_filter {
|
||||
ret.filter(bin);
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn filter(&mut self, cmd: &str) {
|
||||
let _f = |bin: &str, text: &str| -> Result<CellBuffer> {
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
let mut filter_child = Command::new("sh")
|
||||
.args(&["-c", bin])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.chain_err_summary(|| "Failed to start pager filter process")?;
|
||||
let stdin = filter_child
|
||||
.stdin
|
||||
.as_mut()
|
||||
.ok_or_else(|| "failed to open stdin")?;
|
||||
stdin
|
||||
.write_all(text.as_bytes())
|
||||
.chain_err_summary(|| "Failed to write to stdin")?;
|
||||
let out = filter_child
|
||||
.wait_with_output()
|
||||
.chain_err_summary(|| "Failed to wait on filter")?
|
||||
.stdout;
|
||||
let mut dev_null = std::fs::File::open("/dev/null")?;
|
||||
let mut embedded = crate::terminal::embed::EmbedGrid::new();
|
||||
embedded.set_terminal_size((80, 20));
|
||||
|
||||
for b in out {
|
||||
embedded.process_byte(&mut dev_null, b);
|
||||
}
|
||||
Ok(std::mem::replace(embedded.buffer_mut(), Default::default()))
|
||||
};
|
||||
let buf = _f(cmd, &self.text);
|
||||
if let Some((width, height)) = buf.as_ref().ok().map(CellBuffer::size) {
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
pub fn from_buf(content: CellBuffer, cursor_pos: Option<usize>) -> Self {
|
||||
let (width, height) = content.size();
|
||||
Pager {
|
||||
text: String::new(),
|
||||
cursor: (0, cursor_pos.unwrap_or(0)),
|
||||
height,
|
||||
width,
|
||||
dirty: true,
|
||||
content,
|
||||
initialised: true,
|
||||
id: ComponentId::new_v4(),
|
||||
..Default::default()
|
||||
}
|
||||
self.filtered_content = Some((cmd.to_string(), buf));
|
||||
}
|
||||
|
||||
pub fn cursor_pos(&self) -> usize {
|
||||
|
@ -219,44 +219,41 @@ impl Pager {
|
|||
if width < self.minimum_width {
|
||||
width = self.minimum_width;
|
||||
}
|
||||
if self.filtered_content.is_none() {
|
||||
if self.line_breaker.width() != Some(width.saturating_sub(4)) {
|
||||
let line_breaker = LineBreakText::new(
|
||||
self.text.clone(),
|
||||
self.reflow,
|
||||
Some(width.saturating_sub(4)),
|
||||
);
|
||||
if self.line_breaker.width() != Some(width.saturating_sub(4)) {
|
||||
let line_breaker = LineBreakText::new(
|
||||
self.text.clone(),
|
||||
self.reflow,
|
||||
Some(width.saturating_sub(4)),
|
||||
);
|
||||
|
||||
self.line_breaker = line_breaker;
|
||||
self.text_lines.clear();
|
||||
};
|
||||
self.height = self.text_lines.len();
|
||||
self.width = width;
|
||||
if let Some(ref mut search) = self.search {
|
||||
use melib::text_processing::search::KMP;
|
||||
search.positions.clear();
|
||||
for (y, l) in self.text_lines.iter().enumerate() {
|
||||
search.positions.extend(
|
||||
l.kmp_search(&search.pattern)
|
||||
.into_iter()
|
||||
.map(|offset| (y, offset)),
|
||||
);
|
||||
}
|
||||
if let Some(pos) = search.positions.get(search.cursor) {
|
||||
if self.cursor.1 > pos.0 || self.cursor.1 + height!(area) < pos.0 {
|
||||
self.cursor.1 = pos.0.saturating_sub(3);
|
||||
}
|
||||
self.line_breaker = line_breaker;
|
||||
self.text_lines.clear();
|
||||
};
|
||||
self.height = self.text_lines.len();
|
||||
self.width = width;
|
||||
if let Some(ref mut search) = self.search {
|
||||
use melib::text_processing::search::KMP;
|
||||
search.positions.clear();
|
||||
for (y, l) in self.text_lines.iter().enumerate() {
|
||||
search.positions.extend(
|
||||
l.kmp_search(&search.pattern)
|
||||
.into_iter()
|
||||
.map(|offset| (y, offset)),
|
||||
);
|
||||
}
|
||||
if let Some(pos) = search.positions.get(search.cursor) {
|
||||
if self.cursor.1 > pos.0 || self.cursor.1 + height!(area) < pos.0 {
|
||||
self.cursor.1 = pos.0.saturating_sub(3);
|
||||
}
|
||||
}
|
||||
self.draw_lines_up_to(
|
||||
grid,
|
||||
area,
|
||||
context,
|
||||
self.cursor.1 + Self::PAGES_AHEAD_TO_RENDER_NO * height!(area),
|
||||
);
|
||||
}
|
||||
self.draw_lines_up_to(
|
||||
grid,
|
||||
area,
|
||||
context,
|
||||
self.cursor.1 + Self::PAGES_AHEAD_TO_RENDER_NO * height!(area),
|
||||
);
|
||||
self.draw_page(grid, area, context);
|
||||
|
||||
self.initialised = true;
|
||||
}
|
||||
|
||||
|
@ -296,47 +293,6 @@ impl Pager {
|
|||
}
|
||||
|
||||
fn draw_page(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
if let Some((ref cmd, ref filtered_content)) = self.filtered_content {
|
||||
match filtered_content {
|
||||
Ok(ref content) => {
|
||||
copy_area(
|
||||
grid,
|
||||
&content,
|
||||
area,
|
||||
(
|
||||
(
|
||||
std::cmp::min(
|
||||
self.cursor.0,
|
||||
content.size().0.saturating_sub(width!(area)),
|
||||
),
|
||||
std::cmp::min(
|
||||
self.cursor.1,
|
||||
content.size().1.saturating_sub(height!(area)),
|
||||
),
|
||||
),
|
||||
pos_dec(content.size(), (1, 1)),
|
||||
),
|
||||
);
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateSubStatus(
|
||||
cmd.to_string(),
|
||||
)));
|
||||
return;
|
||||
}
|
||||
Err(ref err) => {
|
||||
let mut cmd = cmd.as_str();
|
||||
cmd.truncate_at_boundary(4);
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateSubStatus(format!(
|
||||
"{}: {}",
|
||||
cmd, err
|
||||
))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (mut upper_left, bottom_right) = area;
|
||||
for l in self
|
||||
.text_lines
|
||||
|
@ -526,14 +482,7 @@ impl Component for Pager {
|
|||
if cols < 2 || rows < 2 {
|
||||
return;
|
||||
}
|
||||
let (has_more_lines, (width, height)) = if self.filtered_content.is_some() {
|
||||
(false, (self.width, self.height))
|
||||
} else {
|
||||
(
|
||||
!self.line_breaker.is_finished(),
|
||||
(self.line_breaker.width().unwrap_or(cols), self.height),
|
||||
)
|
||||
};
|
||||
let (width, height) = (self.line_breaker.width().unwrap_or(cols), self.height);
|
||||
if self.show_scrollbar && rows < height {
|
||||
cols -= 1;
|
||||
rows -= 1;
|
||||
|
@ -595,7 +544,7 @@ impl Component for Pager {
|
|||
context: ScrollContext {
|
||||
shown_lines,
|
||||
total_lines,
|
||||
has_more_lines,
|
||||
has_more_lines: !self.line_breaker.is_finished(),
|
||||
},
|
||||
},
|
||||
)));
|
||||
|
@ -617,7 +566,11 @@ impl Component for Pager {
|
|||
search.cursor + 1
|
||||
},
|
||||
total_results = search.positions.len(),
|
||||
has_more_lines = if !has_more_lines { "" } else { "(+)" }
|
||||
has_more_lines = if self.line_breaker.is_finished() {
|
||||
""
|
||||
} else {
|
||||
"(+)"
|
||||
}
|
||||
);
|
||||
let mut attribute = crate::conf::value(context, "status.bar");
|
||||
if !context.settings.terminal.use_color() {
|
||||
|
@ -738,12 +691,6 @@ impl Component for Pager {
|
|||
))));
|
||||
return true;
|
||||
}
|
||||
UIEvent::Action(View(Filter(ref cmd))) => {
|
||||
self.filter(cmd);
|
||||
self.initialised = false;
|
||||
self.dirty = true;
|
||||
return true;
|
||||
}
|
||||
UIEvent::Action(Action::Listing(ListingAction::Search(pattern))) => {
|
||||
self.search = Some(SearchPattern {
|
||||
pattern: pattern.to_string(),
|
||||
|
@ -787,17 +734,6 @@ impl Component for Pager {
|
|||
self.dirty = true;
|
||||
return true;
|
||||
}
|
||||
UIEvent::Input(Key::Esc) if self.filtered_content.is_some() => {
|
||||
self.filtered_content = None;
|
||||
self.initialised = false;
|
||||
self.dirty = true;
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateSubStatus(
|
||||
String::new(),
|
||||
)));
|
||||
return true;
|
||||
}
|
||||
UIEvent::Resize => {
|
||||
self.initialised = false;
|
||||
self.dirty = true;
|
||||
|
@ -808,11 +744,6 @@ impl Component for Pager {
|
|||
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
|
||||
ScrollUpdate::End(self.id),
|
||||
)));
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateSubStatus(
|
||||
String::new(),
|
||||
)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
205
src/conf.rs
205
src/conf.rs
|
@ -130,8 +130,8 @@ pub struct MailUIConf {
|
|||
pub pgp: PGPSettingsOverride,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub struct FileMailboxConf {
|
||||
#[serde(flatten)]
|
||||
pub conf_override: MailUIConf,
|
||||
|
@ -293,40 +293,6 @@ pub fn get_config_file() -> Result<PathBuf> {
|
|||
}
|
||||
}
|
||||
|
||||
struct Ask {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl Ask {
|
||||
fn run(self) -> bool {
|
||||
let mut buffer = String::new();
|
||||
let stdin = io::stdin();
|
||||
let mut handle = stdin.lock();
|
||||
|
||||
print!("{} [Y/n] ", &self.message);
|
||||
let _ = io::stdout().flush();
|
||||
loop {
|
||||
buffer.clear();
|
||||
handle
|
||||
.read_line(&mut buffer)
|
||||
.expect("Could not read from stdin.");
|
||||
|
||||
match buffer.trim() {
|
||||
"" | "Y" | "y" | "yes" | "YES" | "Yes" => {
|
||||
return true;
|
||||
}
|
||||
"n" | "N" | "no" | "No" | "NO" => {
|
||||
return false;
|
||||
}
|
||||
_ => {
|
||||
print!("\n{} [Y/n] ", &self.message);
|
||||
let _ = io::stdout().flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileSettings {
|
||||
pub fn new() -> Result<FileSettings> {
|
||||
let config_path = get_config_file()?;
|
||||
|
@ -335,71 +301,45 @@ impl FileSettings {
|
|||
if path_string.is_empty() {
|
||||
return Err(MeliError::new("No configuration found."));
|
||||
}
|
||||
let ask = Ask {
|
||||
message: format!(
|
||||
"No configuration found. Would you like to generate one in {}?",
|
||||
path_string
|
||||
),
|
||||
};
|
||||
if ask.run() {
|
||||
create_config_file(&config_path)?;
|
||||
return Err(MeliError::new(
|
||||
"Edit the sample configuration and relaunch meli.",
|
||||
));
|
||||
}
|
||||
return Err(MeliError::new("No configuration file found."));
|
||||
}
|
||||
println!(
|
||||
"No configuration found. Would you like to generate one in {}? [Y/n]",
|
||||
path_string
|
||||
);
|
||||
let mut buffer = String::new();
|
||||
let stdin = io::stdin();
|
||||
let mut handle = stdin.lock();
|
||||
|
||||
FileSettings::validate(config_path, true)
|
||||
}
|
||||
loop {
|
||||
buffer.clear();
|
||||
handle
|
||||
.read_line(&mut buffer)
|
||||
.expect("Could not read from stdin.");
|
||||
|
||||
pub fn validate(path: PathBuf, interactive: bool) -> Result<Self> {
|
||||
let s = pp::pp(&path)?;
|
||||
let map: toml::map::Map<String, toml::value::Value> = toml::from_str(&s).map_err(|e| {
|
||||
MeliError::new(format!(
|
||||
"{}:\nConfig file is invalid TOML: {}",
|
||||
path.display(),
|
||||
e.to_string()
|
||||
))
|
||||
})?;
|
||||
/*
|
||||
* Check that a global composing option is set and return a user-friendly error message because the
|
||||
* default serde one is confusing.
|
||||
*/
|
||||
if !map.contains_key("composing") {
|
||||
let err_msg = r#"You must set a global `composing` option. If you override `composing` in each account, you can use a dummy global like follows:
|
||||
|
||||
[composing]
|
||||
send_mail = '/bin/false'
|
||||
|
||||
This is required so that you don't accidentally start meli and find out later that you can't send emails."#;
|
||||
if interactive {
|
||||
println!("{}", err_msg);
|
||||
let ask = Ask {
|
||||
message: format!(
|
||||
"Would you like to append this dummy value in your configuration file {} and continue?",
|
||||
path.display()
|
||||
)
|
||||
};
|
||||
if ask.run() {
|
||||
let mut file = OpenOptions::new().append(true).open(&path)?;
|
||||
file.write_all("[composing]\nsend_mail = '/bin/false'\n".as_bytes())
|
||||
.map_err(|err| {
|
||||
MeliError::new(format!(
|
||||
"Could not append to {}: {}",
|
||||
path.display(),
|
||||
err
|
||||
))
|
||||
})?;
|
||||
return FileSettings::validate(path, interactive);
|
||||
match buffer.trim() {
|
||||
"" | "Y" | "y" | "yes" | "YES" | "Yes" => {
|
||||
create_config_file(&config_path)?;
|
||||
return Err(MeliError::new(
|
||||
"Edit the sample configuration and relaunch meli.",
|
||||
));
|
||||
}
|
||||
"n" | "N" | "no" | "No" | "NO" => {
|
||||
return Err(MeliError::new("No configuration file found."));
|
||||
}
|
||||
_ => {
|
||||
println!(
|
||||
"No configuration found. Would you like to generate one in {}? [Y/n]",
|
||||
path_string
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Err(MeliError::new(format!(
|
||||
"{}\n\nEdit the {} and relaunch meli.",
|
||||
if interactive { "" } else { err_msg },
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
|
||||
FileSettings::validate(config_path)
|
||||
}
|
||||
|
||||
pub fn validate(path: PathBuf) -> Result<Self> {
|
||||
let s = pp::pp(&path)?;
|
||||
let mut s: FileSettings = toml::from_str(&s).map_err(|e| {
|
||||
MeliError::new(format!(
|
||||
"{}:\nConfig file contains errors: {}",
|
||||
|
@ -595,10 +535,6 @@ mod default_vals {
|
|||
pub(in crate::conf) fn internal_value_true<T: std::convert::From<super::ToggleFlag>>() -> T {
|
||||
super::ToggleFlag::InternalVal(true).into()
|
||||
}
|
||||
|
||||
pub(in crate::conf) fn ask<T: std::convert::From<super::ToggleFlag>>() -> T {
|
||||
super::ToggleFlag::Ask.into()
|
||||
}
|
||||
}
|
||||
|
||||
mod deserializers {
|
||||
|
@ -1119,74 +1055,3 @@ mod dotaddressable {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_parse() {
|
||||
use std::fmt::Write;
|
||||
use std::fs;
|
||||
use std::io::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
struct ConfigFile {
|
||||
path: PathBuf,
|
||||
file: fs::File,
|
||||
}
|
||||
|
||||
const TEST_CONFIG: &str = r#"
|
||||
[accounts.account-name]
|
||||
root_mailbox = "/path/to/root/mailbox"
|
||||
format = "Maildir"
|
||||
index_style = "Conversations" # or [plain, threaded, compact]
|
||||
identity="email@example.com"
|
||||
display_name = "Name"
|
||||
subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
|
||||
|
||||
# Set mailbox-specific settings
|
||||
[accounts.account-name.mailboxes]
|
||||
"INBOX" = { rename="Inbox" }
|
||||
"drafts" = { rename="Drafts" }
|
||||
"foobar-devel" = { ignore = true } # don't show notifications for this mailbox
|
||||
|
||||
# Setting up an mbox account
|
||||
[accounts.mbox]
|
||||
root_mailbox = "/var/mail/username"
|
||||
format = "mbox"
|
||||
index_style = "Compact"
|
||||
identity="username@hostname.local"
|
||||
"#;
|
||||
impl ConfigFile {
|
||||
fn new() -> std::result::Result<Self, std::io::Error> {
|
||||
let mut f = fs::File::open("/dev/urandom")?;
|
||||
let mut buf = [0u8; 16];
|
||||
f.read_exact(&mut buf)?;
|
||||
let mut filename = String::with_capacity(2 * 16);
|
||||
for byte in &buf {
|
||||
write!(&mut filename, "{:02X}", byte).unwrap();
|
||||
}
|
||||
let mut path = std::env::temp_dir();
|
||||
path.push(&*filename);
|
||||
let mut file = OpenOptions::new()
|
||||
.create_new(true)
|
||||
.append(true)
|
||||
.open(&path)?;
|
||||
file.write_all(TEST_CONFIG.as_bytes())?;
|
||||
Ok(ConfigFile { path, file })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ConfigFile {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_file = ConfigFile::new().unwrap();
|
||||
let err = FileSettings::validate(new_file.path.clone(), false).unwrap_err();
|
||||
assert!(err.details.as_ref().starts_with("You must set a global `composing` option. If you override `composing` in each account, you can use a dummy global like follows"));
|
||||
new_file
|
||||
.file
|
||||
.write_all("[composing]\nsend_mail = '/bin/false'\n".as_bytes())
|
||||
.unwrap();
|
||||
let err = FileSettings::validate(new_file.path.clone(), false).unwrap_err();
|
||||
assert_eq!(err.details.as_ref(), "Configuration error (account-name): root_path `/path/to/root/mailbox` is not a valid directory.");
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ use crate::jobs::{JobExecutor, JobId, JoinHandle};
|
|||
use indexmap::IndexMap;
|
||||
use melib::backends::*;
|
||||
use melib::email::*;
|
||||
use melib::error::{ErrorKind, MeliError, Result};
|
||||
use melib::error::{MeliError, Result};
|
||||
use melib::text_processing::GlobMatch;
|
||||
use melib::thread::{SortField, SortOrder, Threads};
|
||||
use melib::AddressBook;
|
||||
|
@ -163,6 +163,13 @@ pub enum JobRequest {
|
|||
Mailboxes {
|
||||
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
|
||||
},
|
||||
Load {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
FetchBatch {
|
||||
handle: JoinHandle<Result<()>>,
|
||||
},
|
||||
Fetch {
|
||||
mailbox_hash: MailboxHash,
|
||||
handle: JoinHandle<(
|
||||
|
@ -237,6 +244,8 @@ impl Drop for JobRequest {
|
|||
match self {
|
||||
JobRequest::Generic { handle, .. } |
|
||||
JobRequest::IsOnline { handle, .. } |
|
||||
JobRequest::Load { handle, .. } |
|
||||
JobRequest::FetchBatch { handle, .. } |
|
||||
JobRequest::Refresh { handle, .. } |
|
||||
JobRequest::SetFlags { handle, .. } |
|
||||
JobRequest::SaveMessage { handle, .. } |
|
||||
|
@ -275,6 +284,7 @@ impl core::fmt::Debug for JobRequest {
|
|||
match self {
|
||||
JobRequest::Generic { name, .. } => write!(f, "JobRequest::Generic({})", name),
|
||||
JobRequest::Mailboxes { .. } => write!(f, "JobRequest::Mailboxes"),
|
||||
JobRequest::FetchBatch { .. } => write!(f, "JobRequest::FetchBatch",),
|
||||
JobRequest::Fetch { mailbox_hash, .. } => {
|
||||
write!(f, "JobRequest::Fetch({})", mailbox_hash)
|
||||
}
|
||||
|
@ -302,6 +312,9 @@ impl core::fmt::Debug for JobRequest {
|
|||
JobRequest::SendMessageBackground { .. } => {
|
||||
write!(f, "JobRequest::SendMessageBackground")
|
||||
}
|
||||
JobRequest::Load { mailbox_hash, .. } => {
|
||||
write!(f, "JobRequest::Load({})", mailbox_hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -312,6 +325,8 @@ impl core::fmt::Display for JobRequest {
|
|||
JobRequest::Generic { name, .. } => write!(f, "{}", name),
|
||||
JobRequest::Mailboxes { .. } => write!(f, "Get mailbox list"),
|
||||
JobRequest::Fetch { .. } => write!(f, "Mailbox fetch"),
|
||||
JobRequest::FetchBatch { .. } => write!(f, "Fetch envelopes"),
|
||||
JobRequest::Load { .. } => write!(f, "Mailbox load"),
|
||||
JobRequest::IsOnline { .. } => write!(f, "Online status check"),
|
||||
JobRequest::Refresh { .. } => write!(f, "Refresh mailbox"),
|
||||
JobRequest::SetFlags { env_hashes, .. } => write!(
|
||||
|
@ -351,6 +366,15 @@ impl JobRequest {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn is_load(&self, mailbox_hash: MailboxHash) -> bool {
|
||||
match self {
|
||||
JobRequest::Load {
|
||||
mailbox_hash: h, ..
|
||||
} if *h == mailbox_hash => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_fetch(&self, mailbox_hash: MailboxHash) -> bool {
|
||||
match self {
|
||||
JobRequest::Fetch {
|
||||
|
@ -658,8 +682,7 @@ impl Account {
|
|||
{
|
||||
let total = entry.ref_mailbox.count().ok().unwrap_or((0, 0)).1;
|
||||
entry.status = MailboxStatus::Parsing(0, total);
|
||||
if let Ok(mailbox_job) = self.backend.write().unwrap().fetch(*h) {
|
||||
let mailbox_job = mailbox_job.into_future();
|
||||
if let Ok(mailbox_job) = self.backend.write().unwrap().load(*h) {
|
||||
let handle = if self.backend_capabilities.is_async {
|
||||
self.job_executor.spawn_specialized(mailbox_job)
|
||||
} else {
|
||||
|
@ -671,9 +694,10 @@ impl Account {
|
|||
StatusEvent::NewJob(job_id),
|
||||
)))
|
||||
.unwrap();
|
||||
debug!("JobRequest::Load {} {:?}", *h, job_id);
|
||||
self.active_jobs.insert(
|
||||
job_id,
|
||||
JobRequest::Fetch {
|
||||
JobRequest::Load {
|
||||
mailbox_hash: *h,
|
||||
handle,
|
||||
},
|
||||
|
@ -1068,9 +1092,23 @@ impl Account {
|
|||
}
|
||||
|
||||
if !self.active_jobs.values().any(|j| j.is_watch()) {
|
||||
match self.backend.read().unwrap().watch() {
|
||||
Ok(fut) => {
|
||||
let handle = if self.backend_capabilities.is_async {
|
||||
match self
|
||||
.backend
|
||||
.read()
|
||||
.unwrap()
|
||||
.watcher()
|
||||
.and_then(|mut watcher| {
|
||||
for (mailbox_hash, _) in self
|
||||
.mailbox_entries
|
||||
.iter()
|
||||
.filter(|(_, m)| m.conf.mailbox_conf.subscribe.is_true())
|
||||
{
|
||||
watcher.register_mailbox(*mailbox_hash, MailboxWatchUrgency::High)?;
|
||||
}
|
||||
Ok((watcher.is_blocking(), watcher.spawn()?))
|
||||
}) {
|
||||
Ok((is_blocking, fut)) => {
|
||||
let handle = if is_blocking {
|
||||
self.job_executor.spawn_specialized(fut)
|
||||
} else {
|
||||
self.job_executor.spawn_blocking(fut)
|
||||
|
@ -1078,16 +1116,10 @@ impl Account {
|
|||
self.active_jobs
|
||||
.insert(handle.job_id, JobRequest::Watch { handle });
|
||||
}
|
||||
Err(e)
|
||||
if e.kind == ErrorKind::NotSupported || e.kind == ErrorKind::NotImplemented => {
|
||||
}
|
||||
Err(e) => {
|
||||
self.sender
|
||||
.send(ThreadEvent::UIEvent(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(format!(
|
||||
"Account `{}` watch action returned error: {}",
|
||||
&self.name, e
|
||||
)),
|
||||
StatusEvent::DisplayMessage(e.to_string()),
|
||||
)))
|
||||
.unwrap();
|
||||
}
|
||||
|
@ -1127,6 +1159,73 @@ impl Account {
|
|||
self.hash
|
||||
}
|
||||
|
||||
pub fn fetch_batch(&mut self, env_hashes: EnvelopeHashBatch) -> Result<JobId> {
|
||||
debug!("account fetch_batch {:?}", &env_hashes);
|
||||
let job = self.backend.write().unwrap().fetch_batch(env_hashes)?;
|
||||
let handle = if self.backend_capabilities.is_async {
|
||||
self.job_executor.spawn_specialized(job)
|
||||
} else {
|
||||
self.job_executor.spawn_blocking(job)
|
||||
};
|
||||
let job_id = handle.job_id;
|
||||
self.insert_job(handle.job_id, JobRequest::FetchBatch { handle });
|
||||
Ok(job_id)
|
||||
}
|
||||
|
||||
pub fn load2(&mut self, mailbox_hash: MailboxHash) -> Result<Option<JobId>> {
|
||||
debug!("account load2({}", mailbox_hash);
|
||||
match self.mailbox_entries[&mailbox_hash].status {
|
||||
MailboxStatus::Available => Ok(None),
|
||||
MailboxStatus::Failed(ref err) => Err(err.clone()),
|
||||
MailboxStatus::Parsing(_, _) | MailboxStatus::None => {
|
||||
debug!("load2 find: ");
|
||||
if let Some(job_id) = self
|
||||
.active_jobs
|
||||
.iter()
|
||||
.find(|(id, j)| {
|
||||
debug!(id);
|
||||
debug!(j).is_load(mailbox_hash)
|
||||
})
|
||||
.map(|(j, _)| *j)
|
||||
{
|
||||
Ok(Some(job_id))
|
||||
} else {
|
||||
let mailbox_job = self.backend.write().unwrap().load(mailbox_hash);
|
||||
match mailbox_job {
|
||||
Ok(mailbox_job) => {
|
||||
let handle = if self.backend_capabilities.is_async {
|
||||
self.job_executor.spawn_specialized(mailbox_job)
|
||||
} else {
|
||||
self.job_executor.spawn_blocking(mailbox_job)
|
||||
};
|
||||
let job_id = handle.job_id;
|
||||
debug!("JobRequest::Load {} {:?}", mailbox_hash, handle.job_id);
|
||||
self.insert_job(
|
||||
handle.job_id,
|
||||
JobRequest::Load {
|
||||
mailbox_hash,
|
||||
handle,
|
||||
},
|
||||
);
|
||||
Ok(Some(job_id))
|
||||
}
|
||||
Err(err) => {
|
||||
self.mailbox_entries
|
||||
.entry(mailbox_hash)
|
||||
.and_modify(|entry| {
|
||||
entry.status = MailboxStatus::Failed(err.clone());
|
||||
});
|
||||
self.sender
|
||||
.send(ThreadEvent::UIEvent(UIEvent::StartupCheck(mailbox_hash)))
|
||||
.unwrap();
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(&mut self, mailbox_hash: MailboxHash) -> result::Result<(), usize> {
|
||||
if mailbox_hash == 0 {
|
||||
return Err(0);
|
||||
|
@ -1332,25 +1431,6 @@ impl Account {
|
|||
}
|
||||
Ok(Some(handle))
|
||||
}
|
||||
SendMail::ServerSubmission => {
|
||||
if self.backend_capabilities.supports_submission {
|
||||
let job = self.backend.write().unwrap().submit(
|
||||
message.clone().into_bytes(),
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
|
||||
let handle = if self.backend_capabilities.is_async {
|
||||
self.job_executor.spawn_specialized(job)
|
||||
} else {
|
||||
self.job_executor.spawn_blocking(job)
|
||||
};
|
||||
self.insert_job(handle.job_id, JobRequest::SendMessageBackground { handle });
|
||||
return Ok(None);
|
||||
}
|
||||
return Err(MeliError::new("Server does not support submission.")
|
||||
.set_summary("Message not sent."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1358,8 +1438,6 @@ impl Account {
|
|||
&self,
|
||||
send_mail: crate::conf::composing::SendMail,
|
||||
) -> impl FnOnce(Arc<String>) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> + Send {
|
||||
let capabilities = self.backend_capabilities.clone();
|
||||
let backend = self.backend.clone();
|
||||
|message: Arc<String>| -> Pin<Box<dyn Future<Output = Result<()>> + Send>> {
|
||||
Box::pin(async move {
|
||||
use crate::conf::composing::SendMail;
|
||||
|
@ -1413,19 +1491,6 @@ impl Account {
|
|||
.mail_transaction(message.as_str(), None)
|
||||
.await
|
||||
}
|
||||
SendMail::ServerSubmission => {
|
||||
if capabilities.supports_submission {
|
||||
let fut = backend.write().unwrap().submit(
|
||||
message.as_bytes().to_vec(),
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
fut.await?;
|
||||
return Ok(());
|
||||
}
|
||||
return Err(MeliError::new("Server does not support submission.")
|
||||
.set_summary("Message not sent."));
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1663,6 +1728,57 @@ impl Account {
|
|||
}
|
||||
}
|
||||
}
|
||||
JobRequest::Load {
|
||||
mailbox_hash,
|
||||
ref mut handle,
|
||||
..
|
||||
} => {
|
||||
debug!("got mailbox load for {}", mailbox_hash);
|
||||
match handle.chan.try_recv() {
|
||||
Err(_) => {
|
||||
/* canceled */
|
||||
return true;
|
||||
}
|
||||
Ok(None) => {
|
||||
return true;
|
||||
}
|
||||
Ok(Some(Ok(()))) => {
|
||||
self.mailbox_entries
|
||||
.entry(mailbox_hash)
|
||||
.and_modify(|entry| {
|
||||
entry.status = MailboxStatus::Available;
|
||||
});
|
||||
self.sender
|
||||
.send(ThreadEvent::UIEvent(UIEvent::MailboxUpdate((
|
||||
self.hash,
|
||||
mailbox_hash,
|
||||
))))
|
||||
.unwrap();
|
||||
return true;
|
||||
}
|
||||
Ok(Some(Err(err))) => {
|
||||
self.sender
|
||||
.send(ThreadEvent::UIEvent(UIEvent::Notification(
|
||||
Some(format!("{}: could not load mailbox", &self.name)),
|
||||
err.to_string(),
|
||||
Some(crate::types::NotificationType::Error(err.kind)),
|
||||
)))
|
||||
.expect("Could not send event on main channel");
|
||||
self.mailbox_entries
|
||||
.entry(mailbox_hash)
|
||||
.and_modify(|entry| {
|
||||
entry.status = MailboxStatus::Failed(err);
|
||||
});
|
||||
self.sender
|
||||
.send(ThreadEvent::UIEvent(UIEvent::MailboxUpdate((
|
||||
self.hash,
|
||||
mailbox_hash,
|
||||
))))
|
||||
.unwrap();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
JobRequest::Fetch {
|
||||
mailbox_hash,
|
||||
ref mut handle,
|
||||
|
@ -2143,6 +2259,17 @@ impl Account {
|
|||
}
|
||||
}
|
||||
}
|
||||
JobRequest::FetchBatch { ref mut handle } => {
|
||||
if let Ok(Some(Err(err))) = handle.chan.try_recv() {
|
||||
self.sender
|
||||
.send(ThreadEvent::UIEvent(UIEvent::Notification(
|
||||
Some(format!("{}: envelope fetch failed", &self.name)),
|
||||
err.to_string(),
|
||||
Some(crate::types::NotificationType::Error(err.kind)),
|
||||
)))
|
||||
.expect("Could not send event on main channel");
|
||||
}
|
||||
}
|
||||
JobRequest::Watch { ref mut handle } => {
|
||||
debug!("JobRequest::Watch finished??? ");
|
||||
if let Ok(Some(Err(err))) = handle.chan.try_recv() {
|
||||
|
@ -2270,47 +2397,30 @@ fn build_mailboxes_order(
|
|||
}
|
||||
}
|
||||
node
|
||||
}
|
||||
};
|
||||
|
||||
tree.push(rec(*h, &mailbox_entries, 0));
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! mailbox_eq_key {
|
||||
($mailbox:expr) => {{
|
||||
if let Some(sort_order) = $mailbox.conf.mailbox_conf.sort_order {
|
||||
(0, sort_order, $mailbox.ref_mailbox.path())
|
||||
} else {
|
||||
(1, 0, $mailbox.ref_mailbox.path())
|
||||
}
|
||||
}};
|
||||
}
|
||||
tree.sort_unstable_by(|a, b| {
|
||||
if mailbox_entries[&b.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&b.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Greater
|
||||
} else if mailbox_entries[&a.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&a.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Less
|
||||
} else {
|
||||
mailbox_eq_key!(mailbox_entries[&a.hash])
|
||||
.cmp(&mailbox_eq_key!(mailbox_entries[&b.hash]))
|
||||
mailbox_entries[&a.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.cmp(&mailbox_entries[&b.hash].ref_mailbox.path())
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -2319,30 +2429,22 @@ fn build_mailboxes_order(
|
|||
mailboxes_order.push(n.hash);
|
||||
n.children.sort_unstable_by(|a, b| {
|
||||
if mailbox_entries[&b.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&b.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Greater
|
||||
} else if mailbox_entries[&a.hash]
|
||||
.conf
|
||||
.mailbox_conf
|
||||
.sort_order
|
||||
.is_none()
|
||||
&& mailbox_entries[&a.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.eq_ignore_ascii_case("INBOX")
|
||||
{
|
||||
std::cmp::Ordering::Less
|
||||
} else {
|
||||
mailbox_eq_key!(mailbox_entries[&a.hash])
|
||||
.cmp(&mailbox_eq_key!(mailbox_entries[&b.hash]))
|
||||
mailbox_entries[&a.hash]
|
||||
.ref_mailbox
|
||||
.path()
|
||||
.cmp(&mailbox_entries[&b.hash].ref_mailbox.path())
|
||||
}
|
||||
});
|
||||
stack.extend(n.children.iter().rev().map(Some));
|
||||
|
@ -2387,7 +2489,7 @@ fn build_mailboxes_order(
|
|||
iter.peek() != None,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
rec(node, &mailbox_entries, 0, 0, false);
|
||||
}
|
||||
|
|
|
@ -20,8 +20,7 @@
|
|||
*/
|
||||
|
||||
//! Configuration for composing email.
|
||||
use super::default_vals::{ask, false_val, none, true_val};
|
||||
use melib::ToggleFlag;
|
||||
use super::default_vals::{false_val, none, true_val};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Settings for writing and sending new e-mail
|
||||
|
@ -73,10 +72,6 @@ pub struct ComposingSettings {
|
|||
/// Default: true
|
||||
#[serde(default = "true_val")]
|
||||
pub attribution_use_posix_locale: bool,
|
||||
/// Forward emails as attachment? (Alternative is inline)
|
||||
/// Default: ask
|
||||
#[serde(default = "ask", alias = "forward-as-attachment")]
|
||||
pub forward_as_attachment: ToggleFlag,
|
||||
}
|
||||
|
||||
impl Default for ComposingSettings {
|
||||
|
@ -91,55 +86,14 @@ impl Default for ComposingSettings {
|
|||
store_sent_mail: true,
|
||||
attribution_format_string: None,
|
||||
attribution_use_posix_locale: true,
|
||||
forward_as_attachment: ToggleFlag::Ask,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! named_unit_variant {
|
||||
($variant:ident) => {
|
||||
pub mod $variant {
|
||||
pub fn serialize<S>(serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(stringify!($variant))
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<(), D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct V;
|
||||
impl<'de> serde::de::Visitor<'de> for V {
|
||||
type Value = ();
|
||||
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.write_str(concat!("\"", stringify!($variant), "\""))
|
||||
}
|
||||
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
|
||||
if value == stringify!($variant) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(E::invalid_value(serde::de::Unexpected::Str(value), &self))
|
||||
}
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_str(V)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mod strings {
|
||||
named_unit_variant!(server_submission);
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
pub enum SendMail {
|
||||
#[cfg(feature = "smtp")]
|
||||
Smtp(melib::smtp::SmtpServerConf),
|
||||
#[serde(with = "strings::server_submission")]
|
||||
ServerSubmission,
|
||||
ShellCommand(String),
|
||||
}
|
||||
|
|
|
@ -99,26 +99,6 @@ pub struct ListingSettings {
|
|||
///Default: ' '
|
||||
#[serde(default = "default_divider")]
|
||||
pub sidebar_divider: char,
|
||||
|
||||
/// Flag to show if thread entry contains unseen mail.
|
||||
/// Default: "●"
|
||||
#[serde(default)]
|
||||
pub unseen_flag: Option<String>,
|
||||
|
||||
/// Flag to show if thread has been snoozed.
|
||||
/// Default: "💤"
|
||||
#[serde(default)]
|
||||
pub thread_snoozed_flag: Option<String>,
|
||||
|
||||
/// Flag to show if thread entry has been selected.
|
||||
/// Default: "☑️"
|
||||
#[serde(default)]
|
||||
pub selected_flag: Option<String>,
|
||||
|
||||
/// Flag to show if thread entry contains attachments.
|
||||
/// Default: "📎"
|
||||
#[serde(default)]
|
||||
pub attachment_flag: Option<String>,
|
||||
}
|
||||
|
||||
const fn default_divider() -> char {
|
||||
|
@ -139,10 +119,6 @@ impl Default for ListingSettings {
|
|||
sidebar_mailbox_tree_has_sibling_leaf: None,
|
||||
sidebar_mailbox_tree_no_sibling_leaf: None,
|
||||
sidebar_divider: default_divider(),
|
||||
unseen_flag: None,
|
||||
thread_snoozed_flag: None,
|
||||
selected_flag: None,
|
||||
attachment_flag: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -172,10 +148,6 @@ impl DotAddressable for ListingSettings {
|
|||
.sidebar_mailbox_tree_no_sibling_leaf
|
||||
.lookup(field, tail),
|
||||
"sidebar_divider" => self.sidebar_divider.lookup(field, tail),
|
||||
"unseen_flag" => self.unseen_flag.lookup(field, tail),
|
||||
"thread_snoozed_flag" => self.thread_snoozed_flag.lookup(field, tail),
|
||||
"selected_flag" => self.selected_flag.lookup(field, tail),
|
||||
"attachment_flag" => self.attachment_flag.lookup(field, tail),
|
||||
other => Err(MeliError::new(format!(
|
||||
"{} has no field named {}",
|
||||
parent_field, other
|
||||
|
|
|
@ -76,16 +76,6 @@ pub struct PagerSettingsOverride {
|
|||
#[serde(alias = "auto-choose-multipart-alternative")]
|
||||
#[serde(default)]
|
||||
pub auto_choose_multipart_alternative: Option<ToggleFlag>,
|
||||
#[doc = " Show Date: in my timezone"]
|
||||
#[doc = " Default: true"]
|
||||
#[serde(alias = "show-date-in-my-timezone")]
|
||||
#[serde(default)]
|
||||
pub show_date_in_my_timezone: Option<ToggleFlag>,
|
||||
#[doc = " A command to launch URLs with. The URL will be given as the first argument of the command."]
|
||||
#[doc = " Default: None"]
|
||||
#[serde(deserialize_with = "non_empty_string")]
|
||||
#[serde(default)]
|
||||
pub url_launcher: Option<Option<String>>,
|
||||
}
|
||||
impl Default for PagerSettingsOverride {
|
||||
fn default() -> Self {
|
||||
|
@ -100,8 +90,6 @@ impl Default for PagerSettingsOverride {
|
|||
split_long_lines: None,
|
||||
minimum_width: None,
|
||||
auto_choose_multipart_alternative: None,
|
||||
show_date_in_my_timezone: None,
|
||||
url_launcher: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -150,22 +138,6 @@ pub struct ListingSettingsOverride {
|
|||
#[doc = "Default: ' '"]
|
||||
#[serde(default)]
|
||||
pub sidebar_divider: Option<char>,
|
||||
#[doc = " Flag to show if thread entry contains unseen mail."]
|
||||
#[doc = " Default: \"●\""]
|
||||
#[serde(default)]
|
||||
pub unseen_flag: Option<Option<String>>,
|
||||
#[doc = " Flag to show if thread has been snoozed."]
|
||||
#[doc = " Default: \"💤\""]
|
||||
#[serde(default)]
|
||||
pub thread_snoozed_flag: Option<Option<String>>,
|
||||
#[doc = " Flag to show if thread entry has been selected."]
|
||||
#[doc = " Default: \"☑\u{fe0f}\""]
|
||||
#[serde(default)]
|
||||
pub selected_flag: Option<Option<String>>,
|
||||
#[doc = " Flag to show if thread entry contains attachments."]
|
||||
#[doc = " Default: \"📎\""]
|
||||
#[serde(default)]
|
||||
pub attachment_flag: Option<Option<String>>,
|
||||
}
|
||||
impl Default for ListingSettingsOverride {
|
||||
fn default() -> Self {
|
||||
|
@ -181,10 +153,6 @@ impl Default for ListingSettingsOverride {
|
|||
sidebar_mailbox_tree_has_sibling_leaf: None,
|
||||
sidebar_mailbox_tree_no_sibling_leaf: None,
|
||||
sidebar_divider: None,
|
||||
unseen_flag: None,
|
||||
thread_snoozed_flag: None,
|
||||
selected_flag: None,
|
||||
attachment_flag: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -312,11 +280,6 @@ pub struct ComposingSettingsOverride {
|
|||
#[doc = " Default: true"]
|
||||
#[serde(default)]
|
||||
pub attribution_use_posix_locale: Option<bool>,
|
||||
#[doc = " Forward emails as attachment? (Alternative is inline)"]
|
||||
#[doc = " Default: ask"]
|
||||
#[serde(alias = "forward-as-attachment")]
|
||||
#[serde(default)]
|
||||
pub forward_as_attachment: Option<ToggleFlag>,
|
||||
}
|
||||
impl Default for ComposingSettingsOverride {
|
||||
fn default() -> Self {
|
||||
|
@ -330,7 +293,6 @@ impl Default for ComposingSettingsOverride {
|
|||
store_sent_mail: None,
|
||||
attribution_format_string: None,
|
||||
attribution_use_posix_locale: None,
|
||||
forward_as_attachment: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,14 +87,6 @@ pub struct PagerSettings {
|
|||
alias = "auto-choose-multipart-alternative"
|
||||
)]
|
||||
pub auto_choose_multipart_alternative: ToggleFlag,
|
||||
/// Show Date: in my timezone
|
||||
/// Default: true
|
||||
#[serde(default = "internal_value_true", alias = "show-date-in-my-timezone")]
|
||||
pub show_date_in_my_timezone: ToggleFlag,
|
||||
/// A command to launch URLs with. The URL will be given as the first argument of the command.
|
||||
/// Default: None
|
||||
#[serde(default = "none", deserialize_with = "non_empty_string")]
|
||||
pub url_launcher: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for PagerSettings {
|
||||
|
@ -110,8 +102,6 @@ impl Default for PagerSettings {
|
|||
split_long_lines: true,
|
||||
minimum_width: 80,
|
||||
auto_choose_multipart_alternative: ToggleFlag::InternalVal(true),
|
||||
show_date_in_my_timezone: ToggleFlag::InternalVal(true),
|
||||
url_launcher: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -134,8 +124,6 @@ impl DotAddressable for PagerSettings {
|
|||
"auto_choose_multipart_alternative" => {
|
||||
self.auto_choose_multipart_alternative.lookup(field, tail)
|
||||
}
|
||||
"show_date_in_my_timezone" => self.show_date_in_my_timezone.lookup(field, tail),
|
||||
"url_launcher" => self.html_filter.lookup(field, tail),
|
||||
other => Err(MeliError::new(format!(
|
||||
"{} has no field named {}",
|
||||
parent_field, other
|
||||
|
|
|
@ -167,8 +167,8 @@ shortcut_key_values! { "compact-listing",
|
|||
shortcut_key_values! { "listing",
|
||||
/// Shortcut listing for a mail listing.
|
||||
pub struct ListingShortcuts {
|
||||
scroll_up |> "Scroll up list." |> Key::Char('k'),
|
||||
scroll_down |> "Scroll down list." |> Key::Char('j'),
|
||||
scroll_up |> "Scroll up list." |> Key::Up,
|
||||
scroll_down |> "Scroll down list." |> Key::Down,
|
||||
new_mail |> "Start new mail draft in new tab." |> Key::Char('m'),
|
||||
next_account |> "Go to next account." |> Key::Char('h'),
|
||||
next_mailbox |> "Go to next mailbox." |> Key::Char('J'),
|
||||
|
@ -191,8 +191,8 @@ shortcut_key_values! { "listing",
|
|||
shortcut_key_values! { "contact-list",
|
||||
/// Shortcut listing for the contact list view
|
||||
pub struct ContactListShortcuts {
|
||||
scroll_up |> "Scroll up list." |> Key::Char('k'),
|
||||
scroll_down |> "Scroll down list." |> Key::Char('j'),
|
||||
scroll_up |> "Scroll up list." |> Key::Up,
|
||||
scroll_down |> "Scroll down list." |> Key::Down,
|
||||
create_contact |> "Create new contact." |> Key::Char('c'),
|
||||
edit_contact |> "Edit contact under cursor." |> Key::Char('e'),
|
||||
mail_contact |> "Mail contact under cursor." |> Key::Char('m'),
|
||||
|
@ -221,10 +221,8 @@ shortcut_key_values! { "general",
|
|||
next_tab |> "Next tab." |> Key::Char('T'),
|
||||
scroll_right |> "Generic scroll right (catch-all setting)" |> Key::Right,
|
||||
scroll_left |> "Generic scroll left (catch-all setting)" |> Key::Left,
|
||||
scroll_up |> "Generic scroll up (catch-all setting)" |> Key::Char('k'),
|
||||
scroll_down |> "Generic scroll down (catch-all setting)" |> Key::Char('j'),
|
||||
info_message_next |> "Show next info message, if any" |> Key::Alt('>'),
|
||||
info_message_previous |> "Show previous info message, if any" |> Key::Alt('<')
|
||||
scroll_up |> "Generic scroll up (catch-all setting)" |> Key::Up,
|
||||
scroll_down |> "Generic scroll down (catch-all setting)" |> Key::Down
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -232,8 +230,8 @@ shortcut_key_values! { "composing",
|
|||
pub struct ComposingShortcuts {
|
||||
edit_mail |> "Edit mail." |> Key::Char('e'),
|
||||
send_mail |> "Deliver draft to mailer" |> Key::Char('s'),
|
||||
scroll_up |> "Change field focus." |> Key::Char('k'),
|
||||
scroll_down |> "Change field focus." |> Key::Char('j')
|
||||
scroll_up |> "Change field focus." |> Key::Up,
|
||||
scroll_down |> "Change field focus." |> Key::Down
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -247,7 +245,6 @@ shortcut_key_values! { "envelope-view",
|
|||
reply |> "Reply to envelope." |> Key::Char('R'),
|
||||
reply_to_author |> "Reply to author." |> Key::Ctrl('r'),
|
||||
reply_to_all |> "Reply to all/Reply to list/Follow up." |> Key::Ctrl('g'),
|
||||
forward |> "Forward email." |> Key::Ctrl('f'),
|
||||
return_to_normal_view |> "Return to envelope if viewing raw source or attachment." |> Key::Char('r'),
|
||||
toggle_expand_headers |> "Expand extra headers (References and others)." |> Key::Char('h'),
|
||||
toggle_url_mode |> "Toggles url open mode." |> Key::Char('u'),
|
||||
|
@ -257,8 +254,8 @@ shortcut_key_values! { "envelope-view",
|
|||
|
||||
shortcut_key_values! { "thread-view",
|
||||
pub struct ThreadViewShortcuts {
|
||||
scroll_up |> "Scroll up list." |> Key::Char('k'),
|
||||
scroll_down |> "Scroll down list." |> Key::Char('j'),
|
||||
scroll_up |> "Scroll up list." |> Key::Up,
|
||||
scroll_down |> "Scroll down list." |> Key::Down,
|
||||
collapse_subtree |> "collapse thread branches" |> Key::Char('h'),
|
||||
next_page |> "Go to next page." |> Key::PageDown,
|
||||
prev_page |> "Go to previous page." |> Key::PageUp,
|
||||
|
|
|
@ -286,7 +286,9 @@ const DEFAULT_KEYS: &[&str] = &[
|
|||
"mail.listing.conversations.subject",
|
||||
"mail.listing.conversations.from",
|
||||
"mail.listing.conversations.date",
|
||||
"mail.listing.conversations.padding",
|
||||
"mail.listing.conversations.unseen",
|
||||
"mail.listing.conversations.unseen_padding",
|
||||
"mail.listing.conversations.highlighted",
|
||||
"mail.listing.conversations.selected",
|
||||
"mail.view.headers",
|
||||
|
@ -1515,6 +1517,28 @@ impl Default for Themes {
|
|||
fg: Color::Magenta,
|
||||
}
|
||||
);
|
||||
add!(
|
||||
"mail.listing.conversations.padding",
|
||||
dark = {
|
||||
fg: Color::Byte(235),
|
||||
bg: Color::Byte(235),
|
||||
},
|
||||
light = {
|
||||
fg: Color::Byte(254),
|
||||
bg: Color::Byte(254),
|
||||
}
|
||||
);
|
||||
add!(
|
||||
"mail.listing.conversations.unseen_padding",
|
||||
dark = {
|
||||
fg: Color::Byte(235),
|
||||
bg: Color::Byte(235),
|
||||
},
|
||||
light = {
|
||||
fg: Color::Byte(254),
|
||||
bg: Color::Byte(254),
|
||||
}
|
||||
);
|
||||
add!(
|
||||
"mail.listing.conversations.unseen",
|
||||
dark = {
|
||||
|
|
392
src/state.rs
392
src/state.rs
|
@ -33,14 +33,19 @@ use super::*;
|
|||
use melib::backends::{AccountHash, BackendEventConsumer};
|
||||
|
||||
use crate::jobs::JobExecutor;
|
||||
use crate::terminal::screen::Screen;
|
||||
use crossbeam::channel::{unbounded, Receiver, Sender};
|
||||
use indexmap::IndexMap;
|
||||
use smallvec::SmallVec;
|
||||
use std::env;
|
||||
use std::io::Write;
|
||||
use std::os::unix::io::RawFd;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::AlternateScreen;
|
||||
use termion::{clear, cursor};
|
||||
|
||||
pub type StateStdout = termion::screen::AlternateScreen<termion::raw::RawTerminal<std::io::Stdout>>;
|
||||
|
||||
struct InputHandler {
|
||||
pipe: (RawFd, RawFd),
|
||||
|
@ -164,9 +169,16 @@ impl Context {
|
|||
/// A State object to manage and own components and components of the UI. `State` is responsible for
|
||||
/// managing the terminal and interfacing with `melib`
|
||||
pub struct State {
|
||||
screen: Screen,
|
||||
cols: usize,
|
||||
rows: usize,
|
||||
|
||||
grid: CellBuffer,
|
||||
overlay_grid: CellBuffer,
|
||||
draw_rate_limit: RateLimit,
|
||||
stdout: Option<StateStdout>,
|
||||
mouse: bool,
|
||||
child: Option<ForkType>,
|
||||
draw_horizontal_segment_fn: fn(&mut CellBuffer, &mut StateStdout, usize, usize, usize) -> (),
|
||||
pub mode: UIMode,
|
||||
overlay: Vec<Box<dyn Component>>,
|
||||
components: Vec<Box<dyn Component>>,
|
||||
|
@ -191,7 +203,7 @@ struct DisplayMessage {
|
|||
impl Drop for State {
|
||||
fn drop(&mut self) {
|
||||
// When done, restore the defaults to avoid messing with the terminal.
|
||||
self.screen.switch_to_main_screen();
|
||||
self.switch_to_main_screen();
|
||||
use nix::sys::wait::{waitpid, WaitPidFlag};
|
||||
for child in self.context.children.iter_mut() {
|
||||
if let Err(err) = waitpid(
|
||||
|
@ -303,25 +315,23 @@ impl State {
|
|||
let working = Arc::new(());
|
||||
let control = Arc::downgrade(&working);
|
||||
let mut s = State {
|
||||
screen: Screen {
|
||||
cols,
|
||||
rows,
|
||||
grid: CellBuffer::new(cols, rows, Cell::with_char(' ')),
|
||||
overlay_grid: CellBuffer::new(cols, rows, Cell::with_char(' ')),
|
||||
mouse: settings.terminal.use_mouse.is_true(),
|
||||
stdout: None,
|
||||
draw_horizontal_segment_fn: if settings.terminal.use_color() {
|
||||
Screen::draw_horizontal_segment
|
||||
} else {
|
||||
Screen::draw_horizontal_segment_no_color
|
||||
},
|
||||
},
|
||||
cols,
|
||||
rows,
|
||||
grid: CellBuffer::new(cols, rows, Cell::with_char(' ')),
|
||||
overlay_grid: CellBuffer::new(cols, rows, Cell::with_char(' ')),
|
||||
stdout: None,
|
||||
mouse: settings.terminal.use_mouse.is_true(),
|
||||
child: None,
|
||||
mode: UIMode::Normal,
|
||||
components: Vec::with_capacity(8),
|
||||
overlay: Vec::new(),
|
||||
timer,
|
||||
draw_rate_limit: RateLimit::new(1, 3, job_executor.clone()),
|
||||
draw_horizontal_segment_fn: if settings.terminal.use_color() {
|
||||
State::draw_horizontal_segment
|
||||
} else {
|
||||
State::draw_horizontal_segment_no_color
|
||||
},
|
||||
display_messages: SmallVec::new(),
|
||||
display_messages_expiration_start: None,
|
||||
display_messages_pos: 0,
|
||||
|
@ -350,11 +360,11 @@ impl State {
|
|||
},
|
||||
};
|
||||
if s.context.settings.terminal.ascii_drawing {
|
||||
s.screen.grid.set_ascii_drawing(true);
|
||||
s.screen.overlay_grid.set_ascii_drawing(true);
|
||||
s.grid.set_ascii_drawing(true);
|
||||
s.overlay_grid.set_ascii_drawing(true);
|
||||
}
|
||||
|
||||
s.screen.switch_to_alternate_screen(&s.context);
|
||||
s.switch_to_alternate_screen();
|
||||
for i in 0..s.context.accounts.len() {
|
||||
if !s.context.accounts[i].backend_capabilities.is_remote {
|
||||
s.context.accounts[i].watch();
|
||||
|
@ -383,7 +393,7 @@ impl State {
|
|||
.contains_key(&mailbox_hash)
|
||||
{
|
||||
if self.context.accounts[&account_hash]
|
||||
.load(mailbox_hash)
|
||||
.load2(mailbox_hash)
|
||||
.is_err()
|
||||
{
|
||||
self.context.replies.push_back(UIEvent::from(event));
|
||||
|
@ -406,6 +416,78 @@ impl State {
|
|||
}
|
||||
}
|
||||
|
||||
/// Switch back to the terminal's main screen (The command line the user sees before opening
|
||||
/// the application)
|
||||
pub fn switch_to_main_screen(&mut self) {
|
||||
let mouse = self.mouse;
|
||||
write!(
|
||||
self.stdout(),
|
||||
"{}{}{}{}{disable_sgr_mouse}{disable_mouse}",
|
||||
termion::screen::ToMainScreen,
|
||||
cursor::Show,
|
||||
RestoreWindowTitleIconFromStack,
|
||||
BracketModeEnd,
|
||||
disable_sgr_mouse = if mouse { DisableSGRMouse.as_ref() } else { "" },
|
||||
disable_mouse = if mouse { DisableMouse.as_ref() } else { "" },
|
||||
)
|
||||
.unwrap();
|
||||
self.flush();
|
||||
self.stdout = None;
|
||||
}
|
||||
|
||||
pub fn switch_to_alternate_screen(&mut self) {
|
||||
let s = std::io::stdout();
|
||||
|
||||
let mut stdout = AlternateScreen::from(s.into_raw_mode().unwrap());
|
||||
|
||||
write!(
|
||||
&mut stdout,
|
||||
"{save_title_to_stack}{}{}{}{window_title}{}{}{enable_mouse}{enable_sgr_mouse}",
|
||||
termion::screen::ToAlternateScreen,
|
||||
cursor::Hide,
|
||||
clear::All,
|
||||
cursor::Goto(1, 1),
|
||||
BracketModeStart,
|
||||
save_title_to_stack = SaveWindowTitleIconToStack,
|
||||
window_title = if let Some(ref title) = self.context.settings.terminal.window_title {
|
||||
format!("\x1b]2;{}\x07", title)
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
enable_mouse = if self.mouse { EnableMouse.as_ref() } else { "" },
|
||||
enable_sgr_mouse = if self.mouse {
|
||||
EnableSGRMouse.as_ref()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.stdout = Some(stdout);
|
||||
self.flush();
|
||||
}
|
||||
|
||||
pub fn set_mouse(&mut self, value: bool) {
|
||||
if let Some(stdout) = self.stdout.as_mut() {
|
||||
write!(
|
||||
stdout,
|
||||
"{mouse}{sgr_mouse}",
|
||||
mouse = if value {
|
||||
AsRef::<str>::as_ref(&EnableMouse)
|
||||
} else {
|
||||
AsRef::<str>::as_ref(&DisableMouse)
|
||||
},
|
||||
sgr_mouse = if value {
|
||||
AsRef::<str>::as_ref(&EnableSGRMouse)
|
||||
} else {
|
||||
AsRef::<str>::as_ref(&DisableSGRMouse)
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
self.flush();
|
||||
}
|
||||
|
||||
pub fn receiver(&self) -> Receiver<ThreadEvent> {
|
||||
self.context.receiver.clone()
|
||||
}
|
||||
|
@ -420,7 +502,27 @@ impl State {
|
|||
|
||||
/// On `SIGWNICH` the `State` redraws itself according to the new terminal size.
|
||||
pub fn update_size(&mut self) {
|
||||
self.screen.update_size();
|
||||
let termsize = termion::terminal_size().ok();
|
||||
let termcols = termsize.map(|(w, _)| w);
|
||||
let termrows = termsize.map(|(_, h)| h);
|
||||
if termcols.unwrap_or(72) as usize != self.cols
|
||||
|| termrows.unwrap_or(120) as usize != self.rows
|
||||
{
|
||||
debug!(
|
||||
"Size updated, from ({}, {}) -> ({:?}, {:?})",
|
||||
self.cols, self.rows, termcols, termrows
|
||||
);
|
||||
}
|
||||
self.cols = termcols.unwrap_or(72) as usize;
|
||||
self.rows = termrows.unwrap_or(120) as usize;
|
||||
if !self.grid.resize(self.cols, self.rows, None) {
|
||||
panic!(
|
||||
"Terminal size too big: ({} cols, {} rows)",
|
||||
self.cols, self.rows
|
||||
);
|
||||
}
|
||||
let _ = self.overlay_grid.resize(self.cols, self.rows, None);
|
||||
|
||||
self.rcv_event(UIEvent::Resize);
|
||||
self.display_messages_dirty = true;
|
||||
self.display_messages_initialised = false;
|
||||
|
@ -454,10 +556,7 @@ impl State {
|
|||
self.display_messages_expiration_start = None;
|
||||
areas.push((
|
||||
(0, 0),
|
||||
(
|
||||
self.screen.cols.saturating_sub(1),
|
||||
self.screen.rows.saturating_sub(1),
|
||||
),
|
||||
(self.cols.saturating_sub(1), self.rows.saturating_sub(1)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -477,7 +576,7 @@ impl State {
|
|||
}
|
||||
}
|
||||
/* draw each dirty area */
|
||||
let rows = self.screen.rows;
|
||||
let rows = self.rows;
|
||||
for y in 0..rows {
|
||||
let mut segment = None;
|
||||
for ((x_start, y_start), (x_end, y_end)) in &areas {
|
||||
|
@ -485,9 +584,9 @@ impl State {
|
|||
continue;
|
||||
}
|
||||
if let Some((x_start, x_end)) = segment.take() {
|
||||
(self.screen.draw_horizontal_segment_fn)(
|
||||
&mut self.screen.grid,
|
||||
self.screen.stdout.as_mut().unwrap(),
|
||||
(self.draw_horizontal_segment_fn)(
|
||||
&mut self.grid,
|
||||
self.stdout.as_mut().unwrap(),
|
||||
x_start,
|
||||
x_end,
|
||||
y,
|
||||
|
@ -498,9 +597,9 @@ impl State {
|
|||
*s = Some((*x_start, *x_end));
|
||||
}
|
||||
ref mut s @ Some(_) if s.unwrap().1 < *x_start => {
|
||||
(self.screen.draw_horizontal_segment_fn)(
|
||||
&mut self.screen.grid,
|
||||
self.screen.stdout.as_mut().unwrap(),
|
||||
(self.draw_horizontal_segment_fn)(
|
||||
&mut self.grid,
|
||||
self.stdout.as_mut().unwrap(),
|
||||
s.unwrap().0,
|
||||
s.unwrap().1,
|
||||
y,
|
||||
|
@ -508,9 +607,9 @@ impl State {
|
|||
*s = Some((*x_start, *x_end));
|
||||
}
|
||||
ref mut s @ Some(_) if s.unwrap().1 < *x_end => {
|
||||
(self.screen.draw_horizontal_segment_fn)(
|
||||
&mut self.screen.grid,
|
||||
self.screen.stdout.as_mut().unwrap(),
|
||||
(self.draw_horizontal_segment_fn)(
|
||||
&mut self.grid,
|
||||
self.stdout.as_mut().unwrap(),
|
||||
s.unwrap().0,
|
||||
s.unwrap().1,
|
||||
y,
|
||||
|
@ -523,9 +622,9 @@ impl State {
|
|||
}
|
||||
}
|
||||
if let Some((x_start, x_end)) = segment {
|
||||
(self.screen.draw_horizontal_segment_fn)(
|
||||
&mut self.screen.grid,
|
||||
self.screen.stdout.as_mut().unwrap(),
|
||||
(self.draw_horizontal_segment_fn)(
|
||||
&mut self.grid,
|
||||
self.stdout.as_mut().unwrap(),
|
||||
x_start,
|
||||
x_end,
|
||||
y,
|
||||
|
@ -545,9 +644,9 @@ impl State {
|
|||
/* Clear area previously occupied by floating notification box */
|
||||
let displ_area = self.display_messages_area;
|
||||
for y in get_y(upper_left!(displ_area))..=get_y(bottom_right!(displ_area)) {
|
||||
(self.screen.draw_horizontal_segment_fn)(
|
||||
&mut self.screen.grid,
|
||||
self.screen.stdout.as_mut().unwrap(),
|
||||
(self.draw_horizontal_segment_fn)(
|
||||
&mut self.grid,
|
||||
self.stdout.as_mut().unwrap(),
|
||||
get_x(upper_left!(displ_area)),
|
||||
get_x(bottom_right!(displ_area)),
|
||||
y,
|
||||
|
@ -557,7 +656,7 @@ impl State {
|
|||
let noto_colors = crate::conf::value(&self.context, "status.notification");
|
||||
use crate::melib::text_processing::{Reflow, TextProcessing};
|
||||
|
||||
let msg_lines = msg.split_lines_reflow(Reflow::All, Some(self.screen.cols / 3));
|
||||
let msg_lines = msg.split_lines_reflow(Reflow::All, Some(self.cols / 3));
|
||||
let width = msg_lines
|
||||
.iter()
|
||||
.map(|line| line.grapheme_len() + 4)
|
||||
|
@ -567,19 +666,16 @@ impl State {
|
|||
let displ_area = place_in_area(
|
||||
(
|
||||
(0, 0),
|
||||
(
|
||||
self.screen.cols.saturating_sub(1),
|
||||
self.screen.rows.saturating_sub(1),
|
||||
),
|
||||
(self.cols.saturating_sub(1), self.rows.saturating_sub(1)),
|
||||
),
|
||||
(width, std::cmp::min(self.screen.rows, msg_lines.len() + 4)),
|
||||
(width, std::cmp::min(self.rows, msg_lines.len() + 4)),
|
||||
false,
|
||||
false,
|
||||
);
|
||||
let box_displ_area = create_box(&mut self.screen.overlay_grid, displ_area);
|
||||
for row in self.screen.overlay_grid.bounds_iter(box_displ_area) {
|
||||
let box_displ_area = create_box(&mut self.overlay_grid, displ_area);
|
||||
for row in self.overlay_grid.bounds_iter(box_displ_area) {
|
||||
for c in row {
|
||||
self.screen.overlay_grid[c]
|
||||
self.overlay_grid[c]
|
||||
.set_ch(' ')
|
||||
.set_fg(noto_colors.fg)
|
||||
.set_bg(noto_colors.bg)
|
||||
|
@ -592,7 +688,7 @@ impl State {
|
|||
)) {
|
||||
write_string_to_grid(
|
||||
&line,
|
||||
&mut self.screen.overlay_grid,
|
||||
&mut self.overlay_grid,
|
||||
noto_colors.fg,
|
||||
noto_colors.bg,
|
||||
noto_colors.attrs,
|
||||
|
@ -604,32 +700,14 @@ impl State {
|
|||
|
||||
if self.display_messages.len() > 1 {
|
||||
write_string_to_grid(
|
||||
&if self.display_messages_pos == 0 {
|
||||
format!(
|
||||
"Next: {}",
|
||||
self.context.settings.shortcuts.general.info_message_next
|
||||
)
|
||||
if self.display_messages_pos == 0 {
|
||||
"Next: >"
|
||||
} else if self.display_messages_pos + 1 == self.display_messages.len() {
|
||||
format!(
|
||||
"Prev: {}",
|
||||
self.context
|
||||
.settings
|
||||
.shortcuts
|
||||
.general
|
||||
.info_message_previous
|
||||
)
|
||||
"Prev: <"
|
||||
} else {
|
||||
format!(
|
||||
"Prev: {} Next: {}",
|
||||
self.context
|
||||
.settings
|
||||
.shortcuts
|
||||
.general
|
||||
.info_message_previous,
|
||||
self.context.settings.shortcuts.general.info_message_next
|
||||
)
|
||||
"Prev: <, Next: >"
|
||||
},
|
||||
&mut self.screen.overlay_grid,
|
||||
&mut self.overlay_grid,
|
||||
noto_colors.fg,
|
||||
noto_colors.bg,
|
||||
noto_colors.attrs,
|
||||
|
@ -642,9 +720,9 @@ impl State {
|
|||
for y in get_y(upper_left!(self.display_messages_area))
|
||||
..=get_y(bottom_right!(self.display_messages_area))
|
||||
{
|
||||
(self.screen.draw_horizontal_segment_fn)(
|
||||
&mut self.screen.overlay_grid,
|
||||
self.screen.stdout.as_mut().unwrap(),
|
||||
(self.draw_horizontal_segment_fn)(
|
||||
&mut self.overlay_grid,
|
||||
self.stdout.as_mut().unwrap(),
|
||||
get_x(upper_left!(self.display_messages_area)),
|
||||
get_x(bottom_right!(self.display_messages_area)),
|
||||
y,
|
||||
|
@ -656,9 +734,9 @@ impl State {
|
|||
/* Clear area previously occupied by floating notification box */
|
||||
let displ_area = self.display_messages_area;
|
||||
for y in get_y(upper_left!(displ_area))..=get_y(bottom_right!(displ_area)) {
|
||||
(self.screen.draw_horizontal_segment_fn)(
|
||||
&mut self.screen.grid,
|
||||
self.screen.stdout.as_mut().unwrap(),
|
||||
(self.draw_horizontal_segment_fn)(
|
||||
&mut self.grid,
|
||||
self.stdout.as_mut().unwrap(),
|
||||
get_x(upper_left!(displ_area)),
|
||||
get_x(bottom_right!(displ_area)),
|
||||
y,
|
||||
|
@ -670,34 +748,30 @@ impl State {
|
|||
let area = center_area(
|
||||
(
|
||||
(0, 0),
|
||||
(
|
||||
self.screen.cols.saturating_sub(1),
|
||||
self.screen.rows.saturating_sub(1),
|
||||
),
|
||||
(self.cols.saturating_sub(1), self.rows.saturating_sub(1)),
|
||||
),
|
||||
(
|
||||
if self.screen.cols / 3 > 30 {
|
||||
self.screen.cols / 3
|
||||
if self.cols / 3 > 30 {
|
||||
self.cols / 3
|
||||
} else {
|
||||
self.screen.cols
|
||||
self.cols
|
||||
},
|
||||
if self.screen.rows / 5 > 10 {
|
||||
self.screen.rows / 5
|
||||
if self.rows / 5 > 10 {
|
||||
self.rows / 5
|
||||
} else {
|
||||
self.screen.rows
|
||||
self.rows
|
||||
},
|
||||
),
|
||||
);
|
||||
copy_area(&mut self.screen.overlay_grid, &self.screen.grid, area, area);
|
||||
self.overlay.get_mut(0).unwrap().draw(
|
||||
&mut self.screen.overlay_grid,
|
||||
area,
|
||||
&mut self.context,
|
||||
);
|
||||
copy_area(&mut self.overlay_grid, &self.grid, area, area);
|
||||
self.overlay
|
||||
.get_mut(0)
|
||||
.unwrap()
|
||||
.draw(&mut self.overlay_grid, area, &mut self.context);
|
||||
for y in get_y(upper_left!(area))..=get_y(bottom_right!(area)) {
|
||||
(self.screen.draw_horizontal_segment_fn)(
|
||||
&mut self.screen.overlay_grid,
|
||||
self.screen.stdout.as_mut().unwrap(),
|
||||
(self.draw_horizontal_segment_fn)(
|
||||
&mut self.overlay_grid,
|
||||
self.stdout.as_mut().unwrap(),
|
||||
get_x(upper_left!(area)),
|
||||
get_x(bottom_right!(area)),
|
||||
y,
|
||||
|
@ -707,11 +781,76 @@ impl State {
|
|||
self.flush();
|
||||
}
|
||||
|
||||
/// Draw only a specific `area` on the screen.
|
||||
fn draw_horizontal_segment(
|
||||
grid: &mut CellBuffer,
|
||||
stdout: &mut StateStdout,
|
||||
x_start: usize,
|
||||
x_end: usize,
|
||||
y: usize,
|
||||
) {
|
||||
write!(
|
||||
stdout,
|
||||
"{}",
|
||||
cursor::Goto(x_start as u16 + 1, (y + 1) as u16)
|
||||
)
|
||||
.unwrap();
|
||||
let mut current_fg = Color::Default;
|
||||
let mut current_bg = Color::Default;
|
||||
let mut current_attrs = Attr::DEFAULT;
|
||||
write!(stdout, "\x1B[m").unwrap();
|
||||
for x in x_start..=x_end {
|
||||
let c = &grid[(x, y)];
|
||||
if c.attrs() != current_attrs {
|
||||
c.attrs().write(current_attrs, stdout).unwrap();
|
||||
current_attrs = c.attrs();
|
||||
}
|
||||
if c.bg() != current_bg {
|
||||
c.bg().write_bg(stdout).unwrap();
|
||||
current_bg = c.bg();
|
||||
}
|
||||
if c.fg() != current_fg {
|
||||
c.fg().write_fg(stdout).unwrap();
|
||||
current_fg = c.fg();
|
||||
}
|
||||
if !c.empty() {
|
||||
write!(stdout, "{}", c.ch()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_horizontal_segment_no_color(
|
||||
grid: &mut CellBuffer,
|
||||
stdout: &mut StateStdout,
|
||||
x_start: usize,
|
||||
x_end: usize,
|
||||
y: usize,
|
||||
) {
|
||||
write!(
|
||||
stdout,
|
||||
"{}",
|
||||
cursor::Goto(x_start as u16 + 1, (y + 1) as u16)
|
||||
)
|
||||
.unwrap();
|
||||
let mut current_attrs = Attr::DEFAULT;
|
||||
write!(stdout, "\x1B[m").unwrap();
|
||||
for x in x_start..=x_end {
|
||||
let c = &grid[(x, y)];
|
||||
if c.attrs() != current_attrs {
|
||||
c.attrs().write(current_attrs, stdout).unwrap();
|
||||
current_attrs = c.attrs();
|
||||
}
|
||||
if !c.empty() {
|
||||
write!(stdout, "{}", c.ch()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the entire screen from scratch.
|
||||
pub fn render(&mut self) {
|
||||
self.screen.update_size();
|
||||
let cols = self.screen.cols;
|
||||
let rows = self.screen.rows;
|
||||
self.update_size();
|
||||
let cols = self.cols;
|
||||
let rows = self.rows;
|
||||
self.context
|
||||
.dirty_areas
|
||||
.push_back(((0, 0), (cols - 1, rows - 1)));
|
||||
|
@ -722,11 +861,11 @@ impl State {
|
|||
pub fn draw_component(&mut self, idx: usize) {
|
||||
let component = &mut self.components[idx];
|
||||
let upper_left = (0, 0);
|
||||
let bottom_right = (self.screen.cols - 1, self.screen.rows - 1);
|
||||
let bottom_right = (self.cols - 1, self.rows - 1);
|
||||
|
||||
if component.is_dirty() {
|
||||
component.draw(
|
||||
&mut self.screen.grid,
|
||||
&mut self.grid,
|
||||
(upper_left, bottom_right),
|
||||
&mut self.context,
|
||||
);
|
||||
|
@ -888,11 +1027,9 @@ impl State {
|
|||
))));
|
||||
}
|
||||
ToggleMouse => {
|
||||
self.screen.mouse = !self.screen.mouse;
|
||||
self.screen.set_mouse(self.screen.mouse);
|
||||
self.rcv_event(UIEvent::StatusEvent(StatusEvent::SetMouse(
|
||||
self.screen.mouse,
|
||||
)));
|
||||
self.mouse = !self.mouse;
|
||||
self.set_mouse(self.mouse);
|
||||
self.rcv_event(UIEvent::StatusEvent(StatusEvent::SetMouse(self.mouse)));
|
||||
}
|
||||
Quit => {
|
||||
self.context
|
||||
|
@ -982,11 +1119,11 @@ impl State {
|
|||
* Fork has finished in the past.
|
||||
* We're back in the AlternateScreen, but the cursor is reset to Shown, so fix
|
||||
* it.
|
||||
write!(self.screen.stdout(), "{}", cursor::Hide,).unwrap();
|
||||
write!(self.stdout(), "{}", cursor::Hide,).unwrap();
|
||||
self.flush();
|
||||
*/
|
||||
self.screen.switch_to_main_screen();
|
||||
self.screen.switch_to_alternate_screen(&self.context);
|
||||
self.switch_to_main_screen();
|
||||
self.switch_to_alternate_screen();
|
||||
self.context.restore_input();
|
||||
return;
|
||||
}
|
||||
|
@ -1037,15 +1174,7 @@ impl State {
|
|||
self.redraw();
|
||||
return;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if *key
|
||||
== self
|
||||
.context
|
||||
.settings
|
||||
.shortcuts
|
||||
.general
|
||||
.info_message_previous =>
|
||||
{
|
||||
UIEvent::Input(Key::Alt('<')) => {
|
||||
self.display_messages_expiration_start = Some(melib::datetime::now());
|
||||
self.display_messages_active = true;
|
||||
self.display_messages_initialised = false;
|
||||
|
@ -1053,9 +1182,7 @@ impl State {
|
|||
self.display_messages_pos = self.display_messages_pos.saturating_sub(1);
|
||||
return;
|
||||
}
|
||||
UIEvent::Input(ref key)
|
||||
if *key == self.context.settings.shortcuts.general.info_message_next =>
|
||||
{
|
||||
UIEvent::Input(Key::Alt('>')) => {
|
||||
self.display_messages_expiration_start = Some(melib::datetime::now());
|
||||
self.display_messages_active = true;
|
||||
self.display_messages_initialised = false;
|
||||
|
@ -1167,18 +1294,13 @@ impl State {
|
|||
}
|
||||
Some(false)
|
||||
}
|
||||
/// Switch back to the terminal's main screen (The command line the user sees before opening
|
||||
/// the application)
|
||||
pub fn switch_to_main_screen(&mut self) {
|
||||
self.screen.switch_to_main_screen();
|
||||
}
|
||||
|
||||
pub fn switch_to_alternate_screen(&mut self) {
|
||||
self.screen.switch_to_alternate_screen(&mut self.context);
|
||||
}
|
||||
|
||||
fn flush(&mut self) {
|
||||
self.screen.flush();
|
||||
if let Some(s) = self.stdout.as_mut() {
|
||||
s.flush().unwrap();
|
||||
}
|
||||
}
|
||||
fn stdout(&mut self) -> &mut StateStdout {
|
||||
self.stdout.as_mut().unwrap()
|
||||
}
|
||||
|
||||
pub fn check_accounts(&mut self) {
|
||||
|
|
190
src/terminal.rs
190
src/terminal.rs
|
@ -451,193 +451,3 @@ mod braille {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use screen::StateStdout;
|
||||
pub mod screen {
|
||||
use super::*;
|
||||
use cells::CellBuffer;
|
||||
use std::io::Write;
|
||||
use termion::raw::IntoRawMode;
|
||||
use termion::screen::AlternateScreen;
|
||||
use termion::{clear, cursor};
|
||||
pub type StateStdout =
|
||||
termion::screen::AlternateScreen<termion::raw::RawTerminal<std::io::Stdout>>;
|
||||
pub struct Screen {
|
||||
pub cols: usize,
|
||||
pub rows: usize,
|
||||
pub grid: CellBuffer,
|
||||
pub overlay_grid: CellBuffer,
|
||||
pub stdout: Option<StateStdout>,
|
||||
pub mouse: bool,
|
||||
pub draw_horizontal_segment_fn:
|
||||
fn(&mut CellBuffer, &mut StateStdout, usize, usize, usize) -> (),
|
||||
}
|
||||
|
||||
impl Screen {
|
||||
/// Switch back to the terminal's main screen (The command line the user sees before opening
|
||||
/// the application)
|
||||
pub fn switch_to_main_screen(&mut self) {
|
||||
let mouse = self.mouse;
|
||||
write!(
|
||||
self.stdout.as_mut().unwrap(),
|
||||
"{}{}{}{}{disable_sgr_mouse}{disable_mouse}",
|
||||
termion::screen::ToMainScreen,
|
||||
cursor::Show,
|
||||
RestoreWindowTitleIconFromStack,
|
||||
BracketModeEnd,
|
||||
disable_sgr_mouse = if mouse { DisableSGRMouse.as_ref() } else { "" },
|
||||
disable_mouse = if mouse { DisableMouse.as_ref() } else { "" },
|
||||
)
|
||||
.unwrap();
|
||||
self.flush();
|
||||
self.stdout = None;
|
||||
}
|
||||
|
||||
pub fn switch_to_alternate_screen(&mut self, context: &crate::Context) {
|
||||
let s = std::io::stdout();
|
||||
|
||||
let mut stdout = AlternateScreen::from(s.into_raw_mode().unwrap());
|
||||
|
||||
write!(
|
||||
&mut stdout,
|
||||
"{save_title_to_stack}{}{}{}{window_title}{}{}{enable_mouse}{enable_sgr_mouse}",
|
||||
termion::screen::ToAlternateScreen,
|
||||
cursor::Hide,
|
||||
clear::All,
|
||||
cursor::Goto(1, 1),
|
||||
BracketModeStart,
|
||||
save_title_to_stack = SaveWindowTitleIconToStack,
|
||||
window_title = if let Some(ref title) = context.settings.terminal.window_title {
|
||||
format!("\x1b]2;{}\x07", title)
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
enable_mouse = if self.mouse { EnableMouse.as_ref() } else { "" },
|
||||
enable_sgr_mouse = if self.mouse {
|
||||
EnableSGRMouse.as_ref()
|
||||
} else {
|
||||
""
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.stdout = Some(stdout);
|
||||
self.flush();
|
||||
}
|
||||
|
||||
pub fn flush(&mut self) {
|
||||
if let Some(s) = self.stdout.as_mut() {
|
||||
s.flush().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_mouse(&mut self, value: bool) {
|
||||
if let Some(stdout) = self.stdout.as_mut() {
|
||||
write!(
|
||||
stdout,
|
||||
"{mouse}{sgr_mouse}",
|
||||
mouse = if value {
|
||||
AsRef::<str>::as_ref(&EnableMouse)
|
||||
} else {
|
||||
AsRef::<str>::as_ref(&DisableMouse)
|
||||
},
|
||||
sgr_mouse = if value {
|
||||
AsRef::<str>::as_ref(&EnableSGRMouse)
|
||||
} else {
|
||||
AsRef::<str>::as_ref(&DisableSGRMouse)
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
self.flush();
|
||||
}
|
||||
/// On `SIGWNICH` the `State` redraws itself according to the new terminal size.
|
||||
pub fn update_size(&mut self) {
|
||||
let termsize = termion::terminal_size().ok();
|
||||
let termcols = termsize.map(|(w, _)| w);
|
||||
let termrows = termsize.map(|(_, h)| h);
|
||||
if termcols.unwrap_or(72) as usize != self.cols
|
||||
|| termrows.unwrap_or(120) as usize != self.rows
|
||||
{
|
||||
debug!(
|
||||
"Size updated, from ({}, {}) -> ({:?}, {:?})",
|
||||
self.cols, self.rows, termcols, termrows
|
||||
);
|
||||
}
|
||||
self.cols = termcols.unwrap_or(72) as usize;
|
||||
self.rows = termrows.unwrap_or(120) as usize;
|
||||
if !self.grid.resize(self.cols, self.rows, None) {
|
||||
panic!(
|
||||
"Terminal size too big: ({} cols, {} rows)",
|
||||
self.cols, self.rows
|
||||
);
|
||||
}
|
||||
let _ = self.overlay_grid.resize(self.cols, self.rows, None);
|
||||
}
|
||||
|
||||
/// Draw only a specific `area` on the screen.
|
||||
pub fn draw_horizontal_segment(
|
||||
grid: &mut CellBuffer,
|
||||
stdout: &mut StateStdout,
|
||||
x_start: usize,
|
||||
x_end: usize,
|
||||
y: usize,
|
||||
) {
|
||||
write!(
|
||||
stdout,
|
||||
"{}",
|
||||
cursor::Goto(x_start as u16 + 1, (y + 1) as u16)
|
||||
)
|
||||
.unwrap();
|
||||
let mut current_fg = Color::Default;
|
||||
let mut current_bg = Color::Default;
|
||||
let mut current_attrs = Attr::DEFAULT;
|
||||
write!(stdout, "\x1B[m").unwrap();
|
||||
for x in x_start..=x_end {
|
||||
let c = &grid[(x, y)];
|
||||
if c.attrs() != current_attrs {
|
||||
c.attrs().write(current_attrs, stdout).unwrap();
|
||||
current_attrs = c.attrs();
|
||||
}
|
||||
if c.bg() != current_bg {
|
||||
c.bg().write_bg(stdout).unwrap();
|
||||
current_bg = c.bg();
|
||||
}
|
||||
if c.fg() != current_fg {
|
||||
c.fg().write_fg(stdout).unwrap();
|
||||
current_fg = c.fg();
|
||||
}
|
||||
if !c.empty() {
|
||||
write!(stdout, "{}", c.ch()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn draw_horizontal_segment_no_color(
|
||||
grid: &mut CellBuffer,
|
||||
stdout: &mut StateStdout,
|
||||
x_start: usize,
|
||||
x_end: usize,
|
||||
y: usize,
|
||||
) {
|
||||
write!(
|
||||
stdout,
|
||||
"{}",
|
||||
cursor::Goto(x_start as u16 + 1, (y + 1) as u16)
|
||||
)
|
||||
.unwrap();
|
||||
let mut current_attrs = Attr::DEFAULT;
|
||||
write!(stdout, "\x1B[m").unwrap();
|
||||
for x in x_start..=x_end {
|
||||
let c = &grid[(x, y)];
|
||||
if c.attrs() != current_attrs {
|
||||
c.attrs().write(current_attrs, stdout).unwrap();
|
||||
current_attrs = c.attrs();
|
||||
}
|
||||
if !c.empty() {
|
||||
write!(stdout, "{}", c.ch()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1229,6 +1229,380 @@ pub fn clear_area(grid: &mut CellBuffer, area: Area, attributes: crate::conf::Th
|
|||
}
|
||||
}
|
||||
|
||||
pub mod ansi {
|
||||
//! Create a `CellBuffer` from a string slice containing ANSI escape codes.
|
||||
use super::{Attr, Cell, CellBuffer, Color};
|
||||
/// Create a `CellBuffer` from a string slice containing ANSI escape codes.
|
||||
pub fn ansi_to_cellbuffer(s: &str) -> Option<CellBuffer> {
|
||||
let mut bufs: Vec<Vec<Cell>> = Vec::with_capacity(2048);
|
||||
let mut row: Vec<Cell> = Vec::with_capacity(2048);
|
||||
|
||||
enum State {
|
||||
Start,
|
||||
Csi,
|
||||
SetFg,
|
||||
SetBg,
|
||||
}
|
||||
use State::*;
|
||||
|
||||
let mut rows = 0;
|
||||
let mut max_cols = 0;
|
||||
let mut current_fg = Color::Default;
|
||||
let mut current_bg = Color::Default;
|
||||
let mut current_attrs = Attr::DEFAULT;
|
||||
let mut cur_cell;
|
||||
let mut state: State;
|
||||
for l in s.lines() {
|
||||
cur_cell = Cell::default();
|
||||
state = State::Start;
|
||||
let mut chars = l.chars().peekable();
|
||||
if rows > 0 {
|
||||
max_cols = std::cmp::max(row.len(), max_cols);
|
||||
bufs.push(row);
|
||||
row = Vec::with_capacity(2048);
|
||||
}
|
||||
rows += 1;
|
||||
'line_loop: loop {
|
||||
let c = chars.next();
|
||||
if c.is_none() {
|
||||
break 'line_loop;
|
||||
}
|
||||
match (&state, c.unwrap()) {
|
||||
(Start, '\x1b') => {
|
||||
if chars.next() != Some('[') {
|
||||
return None;
|
||||
}
|
||||
state = Csi;
|
||||
}
|
||||
(Start, c) => {
|
||||
cur_cell.set_ch(c);
|
||||
cur_cell.set_fg(current_fg);
|
||||
cur_cell.set_bg(current_bg);
|
||||
cur_cell.set_attrs(current_attrs);
|
||||
row.push(cur_cell);
|
||||
cur_cell = Cell::default();
|
||||
}
|
||||
(Csi, 'm') => {
|
||||
/* Reset styles */
|
||||
current_fg = Color::Default;
|
||||
current_bg = Color::Default;
|
||||
current_attrs = Attr::DEFAULT;
|
||||
state = Start;
|
||||
}
|
||||
(Csi, '0') if chars.peek() == Some(&'0') => {
|
||||
current_attrs = Attr::DEFAULT;
|
||||
chars.next();
|
||||
let next = chars.next();
|
||||
if next == Some('m') {
|
||||
state = Start;
|
||||
} else if next != Some(';') {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
(Csi, c @ '0'..='8') if chars.peek() == Some(&'m') => {
|
||||
chars.next();
|
||||
state = Start;
|
||||
match c {
|
||||
'0' => {
|
||||
//Reset all attributes
|
||||
current_fg = Color::Default;
|
||||
current_bg = Color::Default;
|
||||
current_attrs = Attr::DEFAULT;
|
||||
}
|
||||
'1' => {
|
||||
current_attrs.set(Attr::BOLD, true);
|
||||
}
|
||||
'2' => {
|
||||
current_attrs.set(Attr::DIM, true);
|
||||
}
|
||||
'3' => {
|
||||
current_attrs.set(Attr::ITALICS, true);
|
||||
}
|
||||
'4' => {
|
||||
current_attrs.set(Attr::UNDERLINE, true);
|
||||
}
|
||||
'5' => {
|
||||
current_attrs.set(Attr::BLINK, true);
|
||||
}
|
||||
'7' => {
|
||||
current_attrs.set(Attr::REVERSE, true);
|
||||
}
|
||||
'8' => {
|
||||
current_attrs.set(Attr::HIDDEN, true);
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
(Csi, '0') => {
|
||||
continue;
|
||||
}
|
||||
(Csi, '2') => {
|
||||
match (chars.next(), chars.next()) {
|
||||
(Some('2'), Some('m')) => {
|
||||
current_attrs.set(Attr::BOLD, false);
|
||||
current_attrs.set(Attr::DIM, false);
|
||||
}
|
||||
(Some('3'), Some('m')) => {
|
||||
current_attrs.set(Attr::ITALICS, false);
|
||||
}
|
||||
(Some('4'), Some('m')) => {
|
||||
current_attrs.set(Attr::UNDERLINE, false);
|
||||
}
|
||||
(Some('5'), Some('m')) => {
|
||||
current_attrs.set(Attr::BLINK, false);
|
||||
}
|
||||
(Some('7'), Some('m')) => {
|
||||
current_attrs.set(Attr::REVERSE, false);
|
||||
}
|
||||
(Some('8'), Some('m')) => {
|
||||
current_attrs.set(Attr::HIDDEN, false);
|
||||
}
|
||||
(Some('9'), Some('m')) => { /* Not crossed out */ }
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
(Csi, '3') => {
|
||||
match chars.next() {
|
||||
Some('8') => {
|
||||
/* Set foreground color */
|
||||
if chars.next() == Some(';') {
|
||||
state = SetFg;
|
||||
/* Next arguments are 5;n or 2;r;g;b */
|
||||
continue;
|
||||
}
|
||||
chars.next();
|
||||
return None;
|
||||
}
|
||||
Some('9') => {
|
||||
current_fg = Color::Default;
|
||||
/* default foreground color */
|
||||
let next = chars.next();
|
||||
if next == Some('m') {
|
||||
state = Start;
|
||||
} else if next != Some(';') {
|
||||
return None;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Some(c) if c >= '0' && c < '8' => {
|
||||
current_fg = Color::from_byte(c as u8 - 0x30);
|
||||
if chars.next() != Some('m') {
|
||||
return None;
|
||||
}
|
||||
state = Start;
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
(Csi, '4') => {
|
||||
match chars.next() {
|
||||
Some('8') => {
|
||||
/* Set background color */
|
||||
if chars.next() == Some(';') {
|
||||
state = SetBg;
|
||||
/* Next arguments are 5;n or 2;r;g;b */
|
||||
continue;
|
||||
}
|
||||
return None;
|
||||
}
|
||||
Some('9') => {
|
||||
/* default background color */
|
||||
current_bg = Color::Default;
|
||||
let next = chars.next();
|
||||
if next == Some('m') {
|
||||
state = Start;
|
||||
} else if next != Some(';') {
|
||||
return None;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Some(c) if c >= '0' && c < '8' => {
|
||||
current_bg = Color::from_byte(c as u8 - 0x30);
|
||||
if chars.next() != Some('m') {
|
||||
return None;
|
||||
}
|
||||
state = Start;
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
(Csi, '9') => {
|
||||
match chars.next() {
|
||||
Some('0') => current_fg = Color::Black,
|
||||
Some('1') => current_fg = Color::Red,
|
||||
Some('2') => current_fg = Color::Green,
|
||||
Some('3') => current_fg = Color::Yellow,
|
||||
Some('4') => current_fg = Color::Blue,
|
||||
Some('5') => current_fg = Color::Magenta,
|
||||
Some('6') => current_fg = Color::Cyan,
|
||||
Some('7') => current_fg = Color::White,
|
||||
_ => {}
|
||||
}
|
||||
let next = chars.next();
|
||||
if next != Some('m') {
|
||||
//debug!(next);
|
||||
}
|
||||
state = Start;
|
||||
}
|
||||
(Csi, '1') if chars.peek() == Some(&'0') => {
|
||||
chars.next();
|
||||
match chars.next() {
|
||||
Some('0') => current_bg = Color::Black,
|
||||
Some('1') => current_bg = Color::Red,
|
||||
Some('2') => current_bg = Color::Green,
|
||||
Some('3') => current_bg = Color::Yellow,
|
||||
Some('4') => current_bg = Color::Blue,
|
||||
Some('5') => current_bg = Color::Magenta,
|
||||
Some('6') => current_bg = Color::Cyan,
|
||||
Some('7') => current_bg = Color::White,
|
||||
_ => {}
|
||||
}
|
||||
let next = chars.next();
|
||||
if next != Some('m') {
|
||||
//debug!(next);
|
||||
}
|
||||
|
||||
state = Start;
|
||||
}
|
||||
(SetFg, '5') => {
|
||||
if chars.next() != Some(';') {
|
||||
return None;
|
||||
}
|
||||
let mut accum = 0;
|
||||
while chars.peek().is_some() && chars.peek() != Some(&'m') {
|
||||
let c = chars.next().unwrap();
|
||||
accum *= 10;
|
||||
accum += c as u8 - 0x30;
|
||||
}
|
||||
if chars.next() != Some('m') {
|
||||
return None;
|
||||
}
|
||||
current_fg = Color::from_byte(accum);
|
||||
state = Start;
|
||||
}
|
||||
(SetFg, '2') => {
|
||||
if chars.next() != Some(';') {
|
||||
return None;
|
||||
}
|
||||
let mut rgb_color = Color::Rgb(0, 0, 0);
|
||||
if let Color::Rgb(ref mut r, ref mut g, ref mut b) = rgb_color {
|
||||
'rgb_fg: for val in &mut [r, g, b] {
|
||||
let mut accum = 0;
|
||||
while chars.peek().is_some()
|
||||
&& chars.peek() != Some(&';')
|
||||
&& chars.peek() != Some(&'m')
|
||||
{
|
||||
let c = chars.next().unwrap();
|
||||
accum *= 10;
|
||||
accum += c as u8 - 0x30;
|
||||
}
|
||||
**val = accum;
|
||||
match chars.peek() {
|
||||
Some(&'m') => {
|
||||
break 'rgb_fg;
|
||||
}
|
||||
Some(&';') => {
|
||||
chars.next();
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
if chars.next() != Some('m') {
|
||||
return None;
|
||||
}
|
||||
current_fg = rgb_color;
|
||||
state = Start;
|
||||
}
|
||||
(SetBg, '5') => {
|
||||
if chars.next() != Some(';') {
|
||||
return None;
|
||||
}
|
||||
let mut accum = 0;
|
||||
while chars.peek().is_some() && chars.peek() != Some(&'m') {
|
||||
let c = chars.next().unwrap();
|
||||
accum *= 10;
|
||||
accum += c as u8 - 0x30;
|
||||
}
|
||||
if chars.next() != Some('m') {
|
||||
return None;
|
||||
}
|
||||
current_bg = Color::from_byte(accum);
|
||||
state = Start;
|
||||
}
|
||||
(SetBg, '2') => {
|
||||
if chars.next() != Some(';') {
|
||||
return None;
|
||||
}
|
||||
let mut rgb_color = Color::Rgb(0, 0, 0);
|
||||
if let Color::Rgb(ref mut r, ref mut g, ref mut b) = rgb_color {
|
||||
'rgb_bg: for val in &mut [r, g, b] {
|
||||
let mut accum = 0;
|
||||
while chars.peek().is_some()
|
||||
&& chars.peek() != Some(&';')
|
||||
&& chars.peek() != Some(&'m')
|
||||
{
|
||||
let c = chars.next().unwrap();
|
||||
accum *= 10;
|
||||
accum += c as u8 - 0x30;
|
||||
}
|
||||
**val = accum;
|
||||
match chars.peek() {
|
||||
Some(&'m') => {
|
||||
break 'rgb_bg;
|
||||
}
|
||||
Some(&';') => {
|
||||
chars.next();
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
if chars.next() != Some('m') {
|
||||
return None;
|
||||
}
|
||||
current_bg = rgb_color;
|
||||
state = Start;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
max_cols = std::cmp::max(row.len(), max_cols);
|
||||
bufs.push(row);
|
||||
let mut buf: Vec<Cell> = Vec::with_capacity(max_cols * bufs.len());
|
||||
for l in bufs {
|
||||
let row_len = l.len();
|
||||
buf.extend(l.into_iter());
|
||||
if row_len < max_cols {
|
||||
for _ in row_len..max_cols {
|
||||
buf.push(Cell::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if buf.len() != rows * max_cols {
|
||||
debug!(
|
||||
"BUG: rows: {} cols: {} = {}, but buf.len() = {}",
|
||||
rows,
|
||||
max_cols,
|
||||
rows * max_cols,
|
||||
buf.len()
|
||||
);
|
||||
}
|
||||
Some(CellBuffer {
|
||||
buf,
|
||||
rows,
|
||||
cols: max_cols,
|
||||
default_cell: Cell::default(),
|
||||
growable: false,
|
||||
ascii_drawing: false,
|
||||
tag_table: Default::default(),
|
||||
tag_associations: smallvec::SmallVec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Use `RowIterator` to iterate the cells of a row without the need to do any bounds checking;
|
||||
/// the iterator will simply return `None` when it reaches the end of the row.
|
||||
/// `RowIterator` can be created via the `CellBuffer::row_iter` method and can be returned by
|
||||
|
|
|
@ -37,7 +37,7 @@ use std::os::unix::{
|
|||
|
||||
mod grid;
|
||||
|
||||
pub use grid::{EmbedGrid, EmbedTerminal};
|
||||
pub use grid::EmbedGrid;
|
||||
|
||||
// ioctl request code to "Make the given terminal the controlling terminal of the calling process"
|
||||
use libc::TIOCSCTTY;
|
||||
|
@ -55,11 +55,7 @@ ioctl_write_ptr_bad!(set_window_size, TIOCSWINSZ, Winsize);
|
|||
|
||||
ioctl_none_bad!(set_controlling_terminal, TIOCSCTTY);
|
||||
|
||||
pub fn create_pty(
|
||||
width: usize,
|
||||
height: usize,
|
||||
command: String,
|
||||
) -> Result<Arc<Mutex<EmbedTerminal>>> {
|
||||
pub fn create_pty(width: usize, height: usize, command: String) -> Result<Arc<Mutex<EmbedGrid>>> {
|
||||
// Open a new PTY master
|
||||
let master_fd = posix_openpt(OFlag::O_RDWR)?;
|
||||
|
||||
|
@ -153,7 +149,7 @@ pub fn create_pty(
|
|||
};
|
||||
|
||||
let stdin = unsafe { std::fs::File::from_raw_fd(master_fd.clone().into_raw_fd()) };
|
||||
let mut embed_grid = EmbedTerminal::new(stdin, child_pid);
|
||||
let mut embed_grid = EmbedGrid::new(stdin, child_pid);
|
||||
embed_grid.set_terminal_size((width, height));
|
||||
let grid = Arc::new(Mutex::new(embed_grid));
|
||||
let grid_ = grid.clone();
|
||||
|
@ -168,7 +164,7 @@ pub fn create_pty(
|
|||
Ok(grid)
|
||||
}
|
||||
|
||||
fn forward_pty_translate_escape_codes(pty_fd: std::fs::File, grid: Arc<Mutex<EmbedTerminal>>) {
|
||||
fn forward_pty_translate_escape_codes(pty_fd: std::fs::File, grid: Arc<Mutex<EmbedGrid>>) {
|
||||
let mut bytes_iter = pty_fd.bytes();
|
||||
//debug!("waiting for bytes");
|
||||
while let Some(Ok(byte)) = bytes_iter.next() {
|
||||
|
@ -395,9 +391,6 @@ impl std::fmt::Display for EscCode<'_> {
|
|||
EscCode(CsiQ(ref buf), c) => {
|
||||
write!(f, "ESC[?{}{}\t\tCSI [UNKNOWN]", unsafestr!(buf), *c as char)
|
||||
}
|
||||
EscCode(Normal, c) => {
|
||||
write!(f, "{} as char: {} Normal", c, *c as char)
|
||||
}
|
||||
EscCode(unknown, c) => {
|
||||
write!(f, "{:?}{} [UNKNOWN]", unknown, c)
|
||||
}
|
||||
|
|
|
@ -35,22 +35,18 @@ use nix::sys::wait::{waitpid, WaitPidFlag};
|
|||
* The main process copies the grid whenever the actual terminal is redrawn.
|
||||
**/
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ScreenBuffer {
|
||||
Normal,
|
||||
Alternate,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EmbedGrid {
|
||||
cursor: (usize, usize),
|
||||
/// [top;bottom]
|
||||
scroll_region: ScrollRegion,
|
||||
pub alternate_screen: CellBuffer,
|
||||
pub grid: CellBuffer,
|
||||
pub state: State,
|
||||
pub stdin: std::fs::File,
|
||||
/// Pid of the embed process
|
||||
pub child_pid: nix::unistd::Pid,
|
||||
/// (width, height)
|
||||
pub terminal_size: (usize, usize),
|
||||
initialized: bool,
|
||||
fg_color: Color,
|
||||
bg_color: Color,
|
||||
/// Store the fg/bg color when highlighting the cell where the cursor is so that it can be
|
||||
|
@ -66,35 +62,68 @@ pub struct EmbedGrid {
|
|||
wrap_next: bool,
|
||||
/// Store state in case a multi-byte character is encountered
|
||||
codepoints: CodepointBuf,
|
||||
pub normal_screen: CellBuffer,
|
||||
screen_buffer: ScreenBuffer,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EmbedTerminal {
|
||||
pub grid: EmbedGrid,
|
||||
pub stdin: std::fs::File,
|
||||
/// Pid of the embed process
|
||||
pub child_pid: nix::unistd::Pid,
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum CodepointBuf {
|
||||
None,
|
||||
TwoCodepoints(u8),
|
||||
ThreeCodepoints(u8, Option<u8>),
|
||||
FourCodepoints(u8, Option<u8>, Option<u8>),
|
||||
}
|
||||
|
||||
impl EmbedTerminal {
|
||||
impl EmbedGrid {
|
||||
pub fn new(stdin: std::fs::File, child_pid: nix::unistd::Pid) -> Self {
|
||||
EmbedTerminal {
|
||||
grid: EmbedGrid::new(),
|
||||
EmbedGrid {
|
||||
cursor: (0, 0),
|
||||
scroll_region: ScrollRegion {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
..Default::default()
|
||||
},
|
||||
terminal_size: (0, 0),
|
||||
grid: CellBuffer::default(),
|
||||
state: State::Normal,
|
||||
stdin,
|
||||
child_pid,
|
||||
fg_color: Color::Default,
|
||||
bg_color: Color::Default,
|
||||
prev_fg_color: None,
|
||||
prev_bg_color: None,
|
||||
show_cursor: true,
|
||||
auto_wrap_mode: true,
|
||||
wrap_next: false,
|
||||
origin_mode: false,
|
||||
codepoints: CodepointBuf::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_terminal_size(&mut self, new_val: (usize, usize)) {
|
||||
self.grid.set_terminal_size(new_val);
|
||||
if new_val == self.terminal_size {
|
||||
return;
|
||||
}
|
||||
//debug!("resizing to {:?}", new_val);
|
||||
self.scroll_region.top = 0;
|
||||
self.scroll_region.bottom = new_val.1.saturating_sub(1);
|
||||
|
||||
self.terminal_size = new_val;
|
||||
if !self.grid.resize(new_val.0, new_val.1, None) {
|
||||
panic!(
|
||||
"Terminal size too big: ({} cols, {} rows)",
|
||||
new_val.0, new_val.1
|
||||
);
|
||||
}
|
||||
self.grid.clear(Some(Cell::default()));
|
||||
self.cursor = (0, 0);
|
||||
self.wrap_next = false;
|
||||
let winsize = Winsize {
|
||||
ws_row: <u16>::try_from(new_val.1).unwrap(),
|
||||
ws_col: <u16>::try_from(new_val.0).unwrap(),
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
};
|
||||
|
||||
let master_fd = self.stdin.as_raw_fd();
|
||||
let _ = unsafe { set_window_size(master_fd, &winsize) };
|
||||
let _ = nix::sys::signal::kill(self.child_pid, nix::sys::signal::SIGWINCH);
|
||||
|
@ -115,102 +144,13 @@ impl EmbedTerminal {
|
|||
}
|
||||
|
||||
pub fn process_byte(&mut self, byte: u8) {
|
||||
let Self {
|
||||
ref mut grid,
|
||||
ref mut stdin,
|
||||
child_pid: _,
|
||||
} = self;
|
||||
grid.process_byte(stdin, byte);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum CodepointBuf {
|
||||
None,
|
||||
TwoCodepoints(u8),
|
||||
ThreeCodepoints(u8, Option<u8>),
|
||||
FourCodepoints(u8, Option<u8>, Option<u8>),
|
||||
}
|
||||
|
||||
impl EmbedGrid {
|
||||
pub fn new() -> Self {
|
||||
let mut normal_screen = CellBuffer::default();
|
||||
normal_screen.set_growable(true);
|
||||
EmbedGrid {
|
||||
cursor: (0, 0),
|
||||
scroll_region: ScrollRegion {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
..Default::default()
|
||||
},
|
||||
terminal_size: (0, 0),
|
||||
initialized: false,
|
||||
alternate_screen: CellBuffer::default(),
|
||||
state: State::Normal,
|
||||
fg_color: Color::Default,
|
||||
bg_color: Color::Default,
|
||||
prev_fg_color: None,
|
||||
prev_bg_color: None,
|
||||
show_cursor: true,
|
||||
auto_wrap_mode: true,
|
||||
wrap_next: false,
|
||||
origin_mode: false,
|
||||
codepoints: CodepointBuf::None,
|
||||
normal_screen,
|
||||
screen_buffer: ScreenBuffer::Normal,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer(&self) -> &CellBuffer {
|
||||
match self.screen_buffer {
|
||||
ScreenBuffer::Normal => &self.normal_screen,
|
||||
ScreenBuffer::Alternate => &self.alternate_screen,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_mut(&mut self) -> &mut CellBuffer {
|
||||
match self.screen_buffer {
|
||||
ScreenBuffer::Normal => &mut self.normal_screen,
|
||||
ScreenBuffer::Alternate => &mut self.alternate_screen,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_terminal_size(&mut self, new_val: (usize, usize)) {
|
||||
if new_val == self.terminal_size && self.initialized {
|
||||
return;
|
||||
}
|
||||
self.initialized = true;
|
||||
//debug!("resizing to {:?}", new_val);
|
||||
self.scroll_region.top = 0;
|
||||
self.scroll_region.bottom = new_val.1.saturating_sub(1);
|
||||
|
||||
self.terminal_size = new_val;
|
||||
if !self.alternate_screen.resize(new_val.0, new_val.1, None) {
|
||||
panic!(
|
||||
"Terminal size too big: ({} cols, {} rows)",
|
||||
new_val.0, new_val.1
|
||||
);
|
||||
}
|
||||
self.alternate_screen.clear(Some(Cell::default()));
|
||||
if !self.normal_screen.resize(new_val.0, new_val.1, None) {
|
||||
panic!(
|
||||
"Terminal size too big: ({} cols, {} rows)",
|
||||
new_val.0, new_val.1
|
||||
);
|
||||
}
|
||||
self.normal_screen.clear(Some(Cell::default()));
|
||||
self.cursor = (0, 0);
|
||||
self.wrap_next = false;
|
||||
}
|
||||
|
||||
pub fn process_byte(&mut self, stdin: &mut std::fs::File, byte: u8) {
|
||||
let EmbedGrid {
|
||||
ref mut cursor,
|
||||
ref mut scroll_region,
|
||||
ref mut terminal_size,
|
||||
ref mut alternate_screen,
|
||||
ref terminal_size,
|
||||
ref mut grid,
|
||||
ref mut state,
|
||||
ref mut stdin,
|
||||
ref mut fg_color,
|
||||
ref mut bg_color,
|
||||
ref mut prev_fg_color,
|
||||
|
@ -220,45 +160,15 @@ impl EmbedGrid {
|
|||
ref mut auto_wrap_mode,
|
||||
ref mut wrap_next,
|
||||
ref mut origin_mode,
|
||||
ref mut screen_buffer,
|
||||
ref mut normal_screen,
|
||||
initialized: _,
|
||||
child_pid: _,
|
||||
} = self;
|
||||
let mut grid = normal_screen;
|
||||
|
||||
let is_alternate = match *screen_buffer {
|
||||
ScreenBuffer::Normal => false,
|
||||
_ => {
|
||||
grid = alternate_screen;
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
macro_rules! increase_cursor_y {
|
||||
() => {
|
||||
cursor.1 += 1;
|
||||
if !is_alternate {
|
||||
cursor.0 = 0;
|
||||
if cursor.1 >= terminal_size.1 {
|
||||
if !grid.resize(std::cmp::max(1, grid.cols()), grid.rows() + 2, None) {
|
||||
return;
|
||||
}
|
||||
scroll_region.bottom += 1;
|
||||
terminal_size.1 += 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! increase_cursor_x {
|
||||
() => {
|
||||
if cursor.0 + 1 < terminal_size.0 {
|
||||
cursor.0 += 1;
|
||||
} else if is_alternate && *auto_wrap_mode {
|
||||
} else if *auto_wrap_mode {
|
||||
*wrap_next = true;
|
||||
} else if !is_alternate {
|
||||
cursor.0 = 0;
|
||||
increase_cursor_y!();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -273,14 +183,10 @@ impl EmbedGrid {
|
|||
}
|
||||
macro_rules! cursor_y {
|
||||
() => {
|
||||
if is_alternate {
|
||||
std::cmp::min(
|
||||
cursor.1 + scroll_region.top,
|
||||
terminal_size.1.saturating_sub(1),
|
||||
)
|
||||
} else {
|
||||
cursor.1
|
||||
}
|
||||
std::cmp::min(
|
||||
cursor.1 + scroll_region.top,
|
||||
terminal_size.1.saturating_sub(1),
|
||||
)
|
||||
};
|
||||
}
|
||||
macro_rules! cursor_val {
|
||||
|
@ -368,11 +274,11 @@ impl EmbedGrid {
|
|||
//debug!("setting cell {:?} char '{}'", cursor, c as char);
|
||||
//debug!("newline y-> y+1, cursor was: {:?}", cursor);
|
||||
|
||||
if cursor.1 + 1 < terminal_size.1 || !is_alternate {
|
||||
if cursor.1 == scroll_region.bottom && is_alternate {
|
||||
if cursor.1 + 1 < terminal_size.1 {
|
||||
if cursor.1 == scroll_region.bottom {
|
||||
grid.scroll_up(scroll_region, cursor.1, 1);
|
||||
} else {
|
||||
increase_cursor_y!();
|
||||
cursor.1 += 1;
|
||||
}
|
||||
}
|
||||
*wrap_next = false;
|
||||
|
@ -530,9 +436,6 @@ impl EmbedGrid {
|
|||
grid[cursor_val!()].set_fg(Color::Black);
|
||||
grid[cursor_val!()].set_bg(Color::White);
|
||||
}
|
||||
b"1047" | b"1049" => {
|
||||
*screen_buffer = ScreenBuffer::Alternate;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
@ -560,9 +463,6 @@ impl EmbedGrid {
|
|||
grid[cursor_val!()].set_bg(*bg_color);
|
||||
}
|
||||
}
|
||||
b"1047" | b"1049" => {
|
||||
*screen_buffer = ScreenBuffer::Normal;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
//debug!("{}", EscCode::from((&(*state), byte)));
|
||||
|
|
|
@ -53,7 +53,6 @@ pub enum StatusEvent {
|
|||
BufClear,
|
||||
BufSet(String),
|
||||
UpdateStatus(String),
|
||||
UpdateSubStatus(String),
|
||||
NewJob(JobId),
|
||||
JobFinished(JobId),
|
||||
JobCanceled(JobId),
|
||||
|
@ -286,6 +285,13 @@ pub mod segment_tree {
|
|||
max
|
||||
}
|
||||
|
||||
pub fn get(&self, index: usize) -> u8 {
|
||||
if self.array.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
self.array[index]
|
||||
}
|
||||
|
||||
pub fn update(&mut self, pos: usize, value: u8) {
|
||||
let mut ctr = pos + self.array.len();
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use melib;
|
||||
use melib::email::Draft;
|
||||
|
||||
#[test]
|
||||
|
@ -12,7 +13,7 @@ fn build_draft() {
|
|||
new_draft.set_body("hello world.".to_string());
|
||||
let raw = new_draft.finalise().expect("could not finalise draft");
|
||||
let boundary_def = raw.find("bzz_bzz__bzz__").unwrap();
|
||||
let boundary_end = boundary_def + raw[boundary_def..].find('\"').unwrap();
|
||||
let boundary_end = boundary_def + raw[boundary_def..].find("\"").unwrap();
|
||||
let boundary = raw[boundary_def..boundary_end].to_string();
|
||||
let boundary_str = &boundary["bzz_bzz__bzz__".len()..];
|
||||
|
||||
|
|
Loading…
Reference in New Issue