36 Commits

Author SHA1 Message Date
Manos Pitsidianakis 15ca25af73
Bump version to 0.7.2 1 week ago
Manos Pitsidianakis 37d0846195
melib/email/address: quote display_name if it contains "," 1 week ago
Manos Pitsidianakis ffc498a5d0
melib/smtp: fix Cc and Bcc ignored when sending mail 1 week ago
Manos Pitsidianakis d25eb00a11
command: improve(?) command completion and add test 2 weeks ago
Manos Pitsidianakis 240374950a
melib/email/address: quote display_name if it contains "." 3 weeks ago
Manos Pitsidianakis 505adca54d
Add forward mail option 3 weeks ago
Manos Pitsidianakis e090c31f96
state: Move grid to Screen struct under terminal mod 1 month ago
Manos Pitsidianakis 20feb50475
view/thread: open the latest email in the thread by default 1 month ago
Manos Pitsidianakis f975e1004c
Add url_launcher config setting 1 month ago
Manos Pitsidianakis b88c3c573d
Add add_addresses_to_contacts command 1 month ago
Manos Pitsidianakis 32901f57d2
Add show_date_in_my_timezone pager config flag 1 month ago
Manos Pitsidianakis d1712557cb
docs: add pager filter documentation 1 month ago
Manos Pitsidianakis a977351f0a
mail/view: respect per-folder/account pager filter override 1 month ago
Manos Pitsidianakis e7b9d2963c
pager: add filter command, esc to clear filter 1 month ago
Manos Pitsidianakis 25579d8807
terminal/cells: remove ansi module 1 month ago
Manos Pitsidianakis 22fb2ed46c
Implement pager filter through EmbedGrid 1 month ago
Manos Pitsidianakis 733de5a5fb
Fix some clippy suggestions 1 month ago
Manos Pitsidianakis 592339bdca
embed: split EmbedGrid to EmbedTerminal and EmbedGrid 1 month ago
Manos Pitsidianakis ae8c2addab
Show compile time features in with command argument 2 months ago
Manos Pitsidianakis bc08bf1d13
Bump version to 0.7.1 2 months ago
Manos Pitsidianakis 7533df86e0
Fix compilation for netbsd-9.2 2 months ago
Manos Pitsidianakis 526a246430
melib/nntp: update total/new counters on new articles 2 months ago
Alex.F 69916f267b
add 'GB18030' charset 7 months ago
Manos Pitsidianakis 13c5798c7b
conf/shortcuts.rs: add info_message_{next,previous} 2 months ago
Manos Pitsidianakis 07e166e1fb
melib/error: Add kinds: NotImplemented, NotSupported, OSError 2 months ago
Manos Pitsidianakis 72a2ba20dc
conf/accounts.rs: print info when displaying watch error 2 months ago
Manos Pitsidianakis c8da6d2049
melib/nntp: implement refresh 2 months ago
Manos Pitsidianakis 90042379a6
melib/{imap,nntp}: throw error on extra unusued conf flags 2 months ago
Manos Pitsidianakis f40ae9e11b
Change all Down/Up shortcuts to j/k 2 months ago
Manos Pitsidianakis 09f3edba76
config: show explanation if `composing` field missing 2 months ago
Manos Pitsidianakis 09dc0a2409
melib/conf: deserialize ToggleFlag from bool & string 2 months ago
Manos Pitsidianakis 3bc187c570
melib/collections: add RwRef{,Mut} structs 2 months ago
Manos Pitsidianakis 05393d8caa
listing/conversations: highlight two rows instead of three 2 months ago
Manos Pitsidianakis b49d965695
Fix unused var etc warnings 2 months ago
Manos Pitsidianakis 6235164df2
melib/nntp: increase chunk size 2 months ago
Manos Pitsidianakis 521f634e7b
melib/nntp: implement NNTP posting 2 months ago
  1. 38
      CHANGELOG.md
  2. 6
      Cargo.lock
  3. 4
      Cargo.toml
  4. 34
      debian/changelog
  5. 4
      docs/meli.1
  6. 99
      docs/meli.conf.5
  7. 2
      melib/Cargo.toml
  8. 9
      melib/build.rs
  9. 2
      melib/src/addressbook.rs
  10. 10
      melib/src/addressbook/vcard.rs
  11. 23
      melib/src/backends.rs
  12. 57
      melib/src/backends/imap.rs
  13. 81
      melib/src/backends/imap/connection.rs
  14. 2
      melib/src/backends/imap/operations.rs
  15. 32
      melib/src/backends/imap/protocol_parser.rs
  16. 30
      melib/src/backends/imap/watch.rs
  17. 2
      melib/src/backends/maildir/backend.rs
  18. 238
      melib/src/backends/nntp.rs
  19. 47
      melib/src/backends/nntp/connection.rs
  20. 3
      melib/src/backends/nntp/mailbox.rs
  21. 8
      melib/src/backends/notmuch.rs
  22. 60
      melib/src/collection.rs
  23. 72
      melib/src/conf.rs
  24. 34
      melib/src/datetime.rs
  25. 8
      melib/src/email.rs
  26. 14
      melib/src/email/address.rs
  27. 14
      melib/src/email/attachment_types.rs
  28. 2
      melib/src/email/attachments.rs
  29. 28
      melib/src/email/compose.rs
  30. 9
      melib/src/email/parser.rs
  31. 7
      melib/src/error.rs
  32. 1
      melib/src/lib.rs
  33. 8
      melib/src/smtp.rs
  34. 4
      melib/src/text_processing/grapheme_clusters.rs
  35. 52
      melib/src/text_processing/line_break.rs
  36. 16
      melib/src/text_processing/mod.rs
  37. 10
      melib/src/text_processing/tables.rs
  38. 25
      src/bin.rs
  39. 158
      src/command.rs
  40. 2
      src/command/actions.rs
  41. 72
      src/components/mail/compose.rs
  42. 22
      src/components/mail/listing/conversations.rs
  43. 279
      src/components/mail/view.rs
  44. 15
      src/components/mail/view/envelope.rs
  45. 56
      src/components/mail/view/thread.rs
  46. 68
      src/components/utilities.rs
  47. 233
      src/components/utilities/pager.rs
  48. 201
      src/conf.rs
  49. 48
      src/conf/accounts.rs
  50. 48
      src/conf/composing.rs
  51. 18
      src/conf/overrides.rs
  52. 12
      src/conf/pager.rs
  53. 23
      src/conf/shortcuts.rs
  54. 390
      src/state.rs
  55. 190
      src/terminal.rs
  56. 374
      src/terminal/cells.rs
  57. 15
      src/terminal/embed.rs
  58. 196
      src/terminal/embed/grid.rs
  59. 1
      src/types.rs
  60. 3
      tests/generating_email.rs

38
CHANGELOG.md

@ -7,6 +7,42 @@ 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
@ -132,3 +168,5 @@ Notable changes:
[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

6
Cargo.lock

@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "adler"
version = "0.2.3"
@ -1096,7 +1098,7 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
[[package]]
name = "meli"
version = "0.7.0"
version = "0.7.2"
dependencies = [
"async-task 3.0.0",
"bincode",
@ -1133,7 +1135,7 @@ dependencies = [
[[package]]
name = "melib"
version = "0.7.0"
version = "0.7.2"
dependencies = [
"async-stream",
"base64 0.12.3",

4
Cargo.toml

@ -1,6 +1,6 @@
[package]
name = "meli"
version = "0.7.0"
version = "0.7.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.0" }
melib = { path = "melib", version = "0.7.2" }
serde = "1.0.71"
serde_derive = "1.0.71"

34
debian/changelog

@ -1,3 +1,37 @@
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

4
docs/meli.1

@ -48,6 +48,8 @@ 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
@ -437,6 +439,8 @@ 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

99
docs/meli.conf.5

@ -346,6 +346,56 @@ 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
@ -403,10 +453,9 @@ and
\&.
Example:
.Bd -literal
[accounts."imap.example.com".mailboxes."INBOX"]
index_style = "plain"
[accounts."imap.example.com".mailboxes."INBOX".pager]
filter = ""
[accounts."imap.example.com".mailboxes]
"INBOX" = { index_style = "plain" }
"INBOX/Lists/devlist" = { autoload = false, pager = { filter = "pygmentize -l diff -f 256"} }
.Ed
.El
.Sh COMPOSING
@ -471,6 +520,11 @@ 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:
@ -559,6 +613,14 @@ 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
@ -694,6 +756,18 @@ 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
@ -715,7 +789,11 @@ for the mailcap file locations.
.\" default value
.Pq Em m
.It Ic go_to_url
Go to url of given index
Go to url of given index (with the command
.Ic url_launcher
setting in
.Sx PAGER
section)
.\" default value
.Pq Em g
.It Ic toggle_url_mode
@ -823,6 +901,17 @@ 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

2
melib/Cargo.toml

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

9
melib/build.rs

@ -43,7 +43,6 @@ 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!(
@ -164,7 +163,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];
@ -224,7 +223,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<_>>();
@ -234,7 +233,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
@ -246,7 +245,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()

2
melib/src/addressbook.rs

@ -105,7 +105,7 @@ impl AddressBook {
{
let mut ret = AddressBook::new(s.name.clone());
if let Some(vcard_path) = s.vcard_folder() {
if let Ok(cards) = vcard::load_cards(&std::path::Path::new(vcard_path)) {
if let Ok(cards) = vcard::load_cards(std::path::Path::new(vcard_path)) {
for c in cards {
ret.add_card(c);
}

10
melib/src/addressbook/vcard.rs

@ -274,10 +274,10 @@ pub fn load_cards(p: &std::path::Path) -> Result<Vec<Card>> {
ret.push(
CardDeserializer::from_str(s)
.and_then(TryInto::try_into)
.and_then(|mut card| {
.map(|mut card| {
Card::set_external_resource(&mut card, true);
is_any_valid = true;
Ok(card)
card
}),
);
}
@ -290,12 +290,10 @@ pub fn load_cards(p: &std::path::Path) -> Result<Vec<Card>> {
debug!(&c);
}
}
if !is_any_valid {
ret.into_iter().collect::<Result<Vec<Card>>>()
} else {
if is_any_valid {
ret.retain(Result::is_ok);
ret.into_iter().collect::<Result<Vec<Card>>>()
}
ret.into_iter().collect::<Result<Vec<Card>>>()
}
#[test]

23
melib/src/backends.rs

@ -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::{MeliError, Result};
use crate::error::{ErrorKind, MeliError, Result};
#[cfg(feature = "maildir_backend")]
use self::maildir::MaildirType;
@ -360,14 +360,14 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
&mut self,
_path: String,
) -> ResultFuture<(MailboxHash, HashMap<MailboxHash, Mailbox>)> {
Err(MeliError::new("Unimplemented."))
Err(MeliError::new("Unimplemented.").set_kind(ErrorKind::NotImplemented))
}
fn delete_mailbox(
&mut self,
_mailbox_hash: MailboxHash,
) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
Err(MeliError::new("Unimplemented."))
Err(MeliError::new("Unimplemented.").set_kind(ErrorKind::NotImplemented))
}
fn set_mailbox_subscription(
@ -375,7 +375,7 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
_mailbox_hash: MailboxHash,
_val: bool,
) -> ResultFuture<()> {
Err(MeliError::new("Unimplemented."))
Err(MeliError::new("Unimplemented.").set_kind(ErrorKind::NotImplemented))
}
fn rename_mailbox(
@ -383,7 +383,7 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
_mailbox_hash: MailboxHash,
_new_path: String,
) -> ResultFuture<Mailbox> {
Err(MeliError::new("Unimplemented."))
Err(MeliError::new("Unimplemented.").set_kind(ErrorKind::NotImplemented))
}
fn set_mailbox_permissions(
@ -391,7 +391,7 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
_mailbox_hash: MailboxHash,
_val: MailboxPermissions,
) -> ResultFuture<()> {
Err(MeliError::new("Unimplemented."))
Err(MeliError::new("Unimplemented.").set_kind(ErrorKind::NotImplemented))
}
fn search(
@ -399,7 +399,16 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
_query: crate::search::Query,
_mailbox_hash: Option<MailboxHash>,
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>> {
Err(MeliError::new("Unimplemented."))
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))
}
}

57
melib/src/backends/imap.rs

@ -1178,20 +1178,20 @@ impl MailBackend for ImapType {
keyword => {
s.push_str(" KEYWORD ");
s.push_str(keyword);
s.push_str(" ");
s.push(' ');
}
}
}
}
And(q1, q2) => {
rec(q1, s);
s.push_str(" ");
s.push(' ');
rec(q2, s);
}
Or(q1, q2) => {
s.push_str(" OR ");
rec(q1, s);
s.push_str(" ");
s.push(' ');
rec(q2, s);
}
Not(q) => {
@ -1433,7 +1433,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,9 +1501,40 @@ 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!(
@ -1518,9 +1549,9 @@ impl ImapType {
s.name.as_str(),
)));
}
let server_port = get_conf_val!(s["server_port"], 143)?;
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"], !(server_port == 993))?;
let use_starttls = get_conf_val!(s["use_starttls"], false)?;
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",
@ -1552,6 +1583,18 @@ 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(())
}
@ -1742,7 +1785,7 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
ref uid,
ref mut envelope,
ref mut flags,
ref raw_fetch_value,
raw_fetch_value,
ref references,
..
} in v.iter_mut()

81
melib/src/backends/imap/connection.rs

@ -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()))
})
@ -588,7 +588,9 @@ impl ImapConnection {
pub fn connect<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async move {
if let (time, ref mut status @ Ok(())) = *self.uid_store.is_online.lock().unwrap() {
if SystemTime::now().duration_since(time).unwrap_or_default() >= IMAP_PROTOCOL_TIMEOUT {
if SystemTime::now().duration_since(time).unwrap_or_default()
>= IMAP_PROTOCOL_TIMEOUT
{
let err = MeliError::new("Connection timed out").set_kind(ErrorKind::Timeout);
*status = Err(err.clone());
self.stream = Err(err);
@ -873,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)
})?;
{
@ -956,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);
@ -978,45 +980,38 @@ 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"))
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();
{
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?;
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?;
}
MailboxSelection::None => {},
}
MailboxSelection::None => {}
}
Ok(())
}

2
melib/src/backends/imap/operations.rs

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

32
melib/src/backends/imap/protocol_parser.rs

@ -119,8 +119,8 @@ impl RequiredResponses {
}
if self.intersects(RequiredResponses::FETCH) {
let mut ptr = 0;
for i in 0..line.len() {
if !line[i].is_ascii_digit() {
for (i, l) in line.iter().enumerate() {
if !l.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(val.as_ref());
let val: &[u8] = val.split_rn().last().unwrap_or_else(|| 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 (") {
@ -682,7 +682,7 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
} 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}`",
@ -893,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,
@ -1091,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(())),
@ -1099,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))
}
@ -1215,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..];
@ -1385,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 {
@ -1453,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, |v| Some(v))))(input.ltrim())
alt((map(tag("NIL"), |_| None), map(quoted, Some)))(input.ltrim())
}
pub fn uid_fetch_envelopes_response(
@ -1526,7 +1526,7 @@ fn eat_whitespace(mut input: &[u8]) -> IResult<&[u8], ()> {
break;
}
}
return Ok((input, ()));
Ok((input, ()))
}
#[derive(Debug, Default, Clone)]

30
melib/src/backends/imap/watch.rs

@ -276,7 +276,7 @@ pub async fn examine_updates(
if !l.starts_with(b"*") {
continue;
}
if let Ok(status) = protocol_parser::status_response(&l).map(|(_, v)| v) {
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() {
@ -326,10 +326,8 @@ pub async fn examine_updates(
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());
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());
@ -372,7 +370,7 @@ pub async fn examine_updates(
{
let uid = uid.unwrap();
let env = envelope.as_mut().unwrap();
env.set_hash(generate_envelope_hash(&mailbox.imap_path(), &uid));
env.set_hash(generate_envelope_hash(mailbox.imap_path(), &uid));
if let Some(value) = references {
env.set_references(value);
}
@ -392,17 +390,15 @@ pub async fn examine_updates(
}
}
}
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()
)
})?;
}
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 {

2
melib/src/backends/maildir/backend.rs

@ -1173,7 +1173,7 @@ impl MaildirType {
}
}
Ok(children)
};
}
let root_path = PathBuf::from(settings.root_mailbox()).expand();
if !root_path.exists() {
return Err(MeliError::new(format!(

238
melib/src/backends/nntp.rs

@ -47,10 +47,46 @@ 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)]
@ -74,6 +110,7 @@ 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>>>,
@ -95,6 +132,7 @@ 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())),
@ -131,6 +169,7 @@ impl MailBackend for NntpType {
)
})
.collect::<Vec<(String, MailBackendExtensionStatus)>>();
let mut supports_submission = false;
let NntpExtensionUse {
#[cfg(feature = "deflate_compression")]
deflate,
@ -138,6 +177,10 @@ 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")]
{
@ -171,7 +214,7 @@ impl MailBackend for NntpType {
supports_search: false,
extensions: Some(extensions),
supports_tags: false,
supports_submission: false,
supports_submission,
}
}
@ -201,8 +244,88 @@ impl MailBackend for NntpType {
}))
}
fn refresh(&mut self, _mailbox_hash: MailboxHash) -> ResultFuture<()> {
Err(MeliError::new("Unimplemented."))
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 mailboxes(&self) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
@ -240,7 +363,7 @@ impl MailBackend for NntpType {
}
fn watch(&self) -> ResultFuture<()> {
Err(MeliError::new("Unimplemented."))
Err(MeliError::new("Unimplemented.").set_kind(ErrorKind::NotImplemented))
}
fn operation(&self, env_hash: EnvelopeHash) -> Result<Box<dyn BackendOp>> {
@ -354,6 +477,39 @@ 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) => {