Compare commits

..

2 Commits

Author SHA1 Message Date
Manos Pitsidianakis 77e4488637
lazy_fetch WIP 2021-01-15 19:13:34 +02:00
Manos Pitsidianakis 819d993f11
melib/backends: replace watch() with watcher(), BackendWatcher trait
```
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.
```

Watching mailboxes for updates is more flexible now that you can
explicitly register mailboxes and set polling period.
2021-01-15 19:12:09 +02:00
80 changed files with 7645 additions and 4133 deletions

View File

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

12
Cargo.lock generated
View File

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

View File

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

View File

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

37
debian/changelog vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
[package]
name = "melib"
version = "0.7.2"
version = "0.6.2"
authors = ["Manos Pitsidianakis <epilys@nessuent.xyz>"]
workspace = ".."
edition = "2018"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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::*;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()..];