Compare commits

..

4 Commits

Author SHA1 Message Date
Manos Pitsidianakis b2ff359eb7 melib/imap: add support for CHILDREN extesion RFC 3348 2022-09-03 23:59:29 +03:00
Manos Pitsidianakis 9fcf88b494 Wrap {AccountHash,MailboxHash} type aliases in New Types wrappers 2022-09-03 23:35:24 +03:00
Manos Pitsidianakis 808bdf75a1 melib/smtp: add BINARYMIME support to smtp client
Concerns #49

IMAP: Lemonade profile tracking issue
2022-09-03 18:17:17 +03:00
Manos Pitsidianakis ed16e29de1 melib/smtp: add 8BITMIME support to smtp client
Concerns #49

IMAP: Lemonade profile tracking issue
2022-09-03 18:02:58 +03:00
98 changed files with 2686 additions and 6012 deletions

View File

@ -7,33 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Added listing configuration setting `thread_subject_pack` (see meli.conf.5)
- Added shortcuts for focusing to sidebar menu and back to the e-mail view (`focus_left` and `focus_right`)
- `f76f4ea3` A new manual page, `meli.7` which contains a general tutorial for using meli.
- `cbe593cf` add configurable header preample suffix and prefix for editing
- `a484b397` Added instructions and information to error shown when libnotmuch could not be found.
- `a484b397` Added configuration setting `library_file_path` to notmuch backend if user wants to specify the library's location manually.
- `aa99b0d7` Implement configurable subject prefix stripping when replying
- `a73885ac` added RGB support to embedded terminal emulator.
- `f4e0970d` added ability to kill embed process with Ctrl-C, or Ctrl-Z and pressing 'q'.
- `9205f3b8` added a per account mail sort order parameter.
- `d921b3c3` implemented sorting with user sort order parameter if defined.
- `dc5afa13` use osascript/applescript for notifications on macos
- `d0de0485` add {in,de}crease_sidebar shortcuts
- `340d6451` add config setting for sidebar ratio
- `36e29cb6` Add configurable mailbox sort order
### Changed
- `f76f4ea3` Shortcut `open_thread` and `exit_thread` renamed to `open_entry` and `exit_entry`.
- `7650805c` Binary size reduced significantly.
### Fixed
- `a42a6ca8` show notifications in terminal if there is no other alternative.
## [alpha-0.7.2] - 2021-10-15
### Added

311
Cargo.lock generated
View File

@ -28,15 +28,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "async-channel"
version = "1.6.1"
@ -162,17 +153,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a"
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi 0.3.9",
]
[[package]]
name = "autocfg"
version = "1.1.0"
@ -220,18 +200,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "bufstream"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8"
[[package]]
name = "bumpalo"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
[[package]]
name = "bytes"
version = "1.2.1"
@ -271,21 +239,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
dependencies = [
"iana-time-zone",
"js-sys",
"num-integer",
"num-traits",
"time 0.1.44",
"wasm-bindgen",
"winapi 0.3.9",
]
[[package]]
name = "clap"
version = "2.34.0"
@ -489,12 +442,6 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "either"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[package]]
name = "encoding"
version = "0.2.33"
@ -795,7 +742,7 @@ checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
dependencies = [
"cfg-if 1.0.0",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi",
]
[[package]]
@ -845,19 +792,6 @@ dependencies = [
"itoa",
]
[[package]]
name = "iana-time-zone"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"js-sys",
"wasm-bindgen",
"winapi 0.3.9",
]
[[package]]
name = "idna"
version = "0.2.3"
@ -962,15 +896,6 @@ dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "kernel32-sys"
version = "0.2.2"
@ -1078,35 +1003,7 @@ dependencies = [
"dirs-next",
"objc-foundation",
"objc_id",
"time 0.3.11",
]
[[package]]
name = "mailin"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d0411d6d3cf6baacae37461dc5b0a32b9c68ae99ddef61bcd88174b8da890a"
dependencies = [
"base64",
"either",
"log",
"nom",
"ternop",
]
[[package]]
name = "mailin-embedded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2f29d14249fb45f7795bc8564175ca7b963254217f24e8cde84ba40d38b58cc"
dependencies = [
"bufstream",
"lazy_static",
"log",
"mailin",
"rustls",
"rustls-pemfile",
"scoped_threadpool",
"time",
]
[[package]]
@ -1177,7 +1074,6 @@ dependencies = [
"isahc",
"libc",
"libloading",
"mailin-embedded",
"native-tls",
"nix",
"nom",
@ -1188,7 +1084,6 @@ dependencies = [
"serde_json",
"smallvec",
"smol",
"stderrlog",
"unicode-segmentation",
"uuid",
"xdg",
@ -1354,25 +1249,6 @@ dependencies = [
"winrt-notification",
]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.13.1"
@ -1661,21 +1537,6 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin",
"untrusted",
"web-sys",
"winapi 0.3.9",
]
[[package]]
name = "rusqlite"
version = "0.28.0"
@ -1690,27 +1551,6 @@ dependencies = [
"smallvec",
]
[[package]]
name = "rustls"
version = "0.20.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033"
dependencies = [
"log",
"ring",
"sct",
"webpki",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55"
dependencies = [
"base64",
]
[[package]]
name = "ryu"
version = "1.0.10"
@ -1736,28 +1576,12 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "scoped_threadpool"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "sct"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "security-framework"
version = "2.6.1"
@ -1891,25 +1715,6 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "stderrlog"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af95cb8a5f79db5b2af2a46f44da7594b5adbcbb65cbf87b8da0959bfdd82460"
dependencies = [
"atty",
"chrono",
"log",
"termcolor",
"thread_local",
]
[[package]]
name = "structopt"
version = "0.3.26"
@ -1986,15 +1791,6 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "termcolor"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
dependencies = [
"winapi-util",
]
[[package]]
name = "termion"
version = "1.5.6"
@ -2007,12 +1803,6 @@ dependencies = [
"redox_termios",
]
[[package]]
name = "ternop"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d4ae32d0a4605a89c28534371b056919c12e7a070ee07505af75130ff030111"
[[package]]
name = "textwrap"
version = "0.11.0"
@ -2051,17 +1841,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "time"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi 0.3.9",
]
[[package]]
name = "time"
version = "0.3.11"
@ -2185,12 +1964,6 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "url"
version = "2.2.2"
@ -2243,92 +2016,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d"
dependencies = [
"cfg-if 1.0.0",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a"
[[package]]
name = "web-sys"
version = "0.3.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webpki"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "wepoll-ffi"
version = "0.1.2"

View File

@ -21,9 +21,9 @@ path = "src/main.rs"
name = "meli"
path = "src/lib.rs"
[[bin]]
name = "managesieve-client"
path = "src/managesieve.rs"
#[[bin]]
#name = "managesieve-meli"
#path = "src/managesieve.rs"
#[[bin]]
#name = "async"
@ -81,7 +81,7 @@ strip = true
members = ["melib", "tools", ]
[features]
default = ["sqlite3", "notmuch", "regexp", "smtp", "dbus-notifications", "gpgme", "cli-docs"]
default = ["sqlite3", "notmuch", "regexp", "smtp", "dbus-notifications", "gpgme"]
notmuch = ["melib/notmuch_backend", ]
jmap = ["melib/jmap_backend",]
sqlite3 = ["melib/sqlite3"]

View File

@ -24,13 +24,11 @@ Official mirrors:
## Documentation
See a comprehensive tour of `meli` in the manual page [`meli(7)`](./docs/meli.7).
See also [Quickstart tutorial](https://meli.delivery/documentation.html#quick-start).
See also the [Quickstart tutorial](https://meli.delivery/documentation.html#quick-start) online.
After installing meli, see `meli(1)`, `meli.conf(5)` and `meli-themes(5)` for documentation. Sample configuration and theme files can be found in the `docs/samples/` subdirectory. Manual pages are also [hosted online](https://meli.delivery/documentation.html "meli documentation").
After installing `meli`, see `meli(1)`, `meli.conf(5)`, `meli(7)` and `meli-themes(5)` for documentation. Sample configuration and theme files can be found in the `docs/samples/` subdirectory. Manual pages are also [hosted online](https://meli.delivery/documentation.html "meli documentation").
`meli` by default looks for a configuration file in this location: `$XDG_CONFIG_HOME/meli/config.toml`
meli by default looks for a configuration file in this location: `$XDG_CONFIG_HOME/meli/config.toml`
You can run meli with arbitrary configuration files by setting the `$MELI_CONFIG`
environment variable to their locations, i.e.:
@ -48,12 +46,12 @@ For a quick start, build and install locally:
Available subcommands for `make` are listed with `make help`. The Makefile *should* be POSIX portable and not require a specific `make` version.
`meli` requires rust 1.39 and rust's package manager, Cargo. Information on how
meli requires rust 1.39 and rust's package manager, Cargo. Information on how
to get it on your system can be found here: <https://doc.rust-lang.org/cargo/getting-started/installation.html>
With Cargo available, the project can be built with `make` and the resulting binary will then be found under `target/release/meli`. Run `make install` to install the binary and man pages. This requires root, so I suggest you override the default paths and install it in your `$HOME`: `make PREFIX=$HOME/.local install`.
You can build and run `meli` with one command: `cargo run --release`.
You can build and run meli with one command: `cargo run --release`.
### Build features
@ -65,8 +63,8 @@ Some functionality is held behind "feature gates", or compile-time flags. The fo
- `jmap` provides support for connecting to a jmap server and use it as a mail backend (off by default)
- `sqlite3` provides support for builting fast search indexes in local sqlite3 databases (on by default)
- `cli-docs` includes the manpage documentation compiled by either `mandoc` or `man` binary to plain text in `meli`'s command line. Embedded documentation can be viewed with the subcommand `meli man [PAGE]`
- `svgscreenshot` provides support for taking screenshots of the current view of `meli` and saving it as SVG files. Its only purpose is taking screenshots for the official `meli` webpage. (off by default)
- `debug-tracing` enables various trace debug logs from various places around the `meli` code base. The trace log is printed in `stderr`. (off by default)
- `svgscreenshot` provides support for taking screenshots of the current view of meli and saving it as SVG files. Its only purpose is taking screenshots for the official meli webpage. (off by default)
- `debug-tracing` enables various trace debug logs from various places around the meli code base. The trace log is printed in `stderr`. (off by default)
### Build Debian package (*deb*)
@ -77,11 +75,11 @@ A `*.deb` package can be built with `make deb-dist`
### Using notmuch
To use the optional notmuch backend feature, you must have `libnotmuch5` installed in your system. In Debian-like systems, install the `libnotmuch5` packages. `meli` detects the library's presence on runtime.
To use the optional notmuch backend feature, you must have `libnotmuch5` installed in your system. In Debian-like systems, install the `libnotmuch5` packages. meli detects the library's presence on runtime.
### Using GPG
To use the optional gpg feature, you must have `libgpgme` installed in your system. In Debian-like systems, install the `libgpgme11` package. `meli` detects the library's presence on runtime.
To use the optional gpg feature, you must have `libgpgme` installed in your system. In Debian-like systems, install the `libgpgme11` package. meli detects the library's presence on runtime.
### Building with JMAP
@ -104,7 +102,7 @@ cargo run
There is a debug/tracing log feature that can be enabled by using the flag
`--feature debug-tracing` after uncommenting the features in `Cargo.toml`. The logs
are printed in stderr, thus you can run `meli` with a redirection (i.e `2> log`)
are printed in stderr, thus you can run meli with a redirection (i.e `2> log`)
Code style follows the default rustfmt profile.

View File

@ -39,37 +39,68 @@ fn main() {
{
use flate2::Compression;
use flate2::GzBuilder;
const MANDOC_OPTS: &[&str] = &["-T", "utf8", "-I", "os=Generated by mandoc(1)"];
const MANDOC_OPTS: &[&'static str] = &["-T", "utf8", "-I", "os=Generated by mandoc(1)"];
use std::env;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
use std::process::Command;
let out_dir = env::var("OUT_DIR").unwrap();
let mut out_dir_path = Path::new(&out_dir).to_path_buf();
out_dir_path.push("meli.txt.gz");
let mut cl = |filepath: &str, output: &str| {
out_dir_path.push(output);
let output = Command::new("mandoc")
.args(MANDOC_OPTS)
.arg(filepath)
.output()
.or_else(|_| Command::new("man").arg("-l").arg(filepath).output())
.unwrap();
let output = Command::new("mandoc")
.args(MANDOC_OPTS)
.arg("docs/meli.1")
.output()
.or_else(|_| Command::new("man").arg("-l").arg("docs/meli.1").output())
.unwrap();
let file = File::create(&out_dir_path).unwrap();
let mut gz = GzBuilder::new()
.comment(output.stdout.len().to_string().into_bytes())
.write(file, Compression::default());
gz.write_all(&output.stdout).unwrap();
gz.finish().unwrap();
out_dir_path.pop();
};
let file = File::create(&out_dir_path).unwrap();
let mut gz = GzBuilder::new()
.comment(output.stdout.len().to_string().into_bytes())
.write(file, Compression::default());
gz.write_all(&output.stdout).unwrap();
gz.finish().unwrap();
out_dir_path.pop();
cl("docs/meli.1", "meli.txt.gz");
cl("docs/meli.conf.5", "meli.conf.txt.gz");
cl("docs/meli-themes.5", "meli-themes.txt.gz");
cl("docs/meli.7", "meli.7.txt.gz");
out_dir_path.push("meli.conf.txt.gz");
let output = Command::new("mandoc")
.args(MANDOC_OPTS)
.arg("docs/meli.conf.5")
.output()
.or_else(|_| {
Command::new("man")
.arg("-l")
.arg("docs/meli.conf.5")
.output()
})
.unwrap();
let file = File::create(&out_dir_path).unwrap();
let mut gz = GzBuilder::new()
.comment(output.stdout.len().to_string().into_bytes())
.write(file, Compression::default());
gz.write_all(&output.stdout).unwrap();
gz.finish().unwrap();
out_dir_path.pop();
out_dir_path.push("meli-themes.txt.gz");
let output = Command::new("mandoc")
.args(MANDOC_OPTS)
.arg("docs/meli-themes.5")
.output()
.or_else(|_| {
Command::new("man")
.arg("-l")
.arg("docs/meli-themes.5")
.output()
})
.unwrap();
let file = File::create(&out_dir_path).unwrap();
let mut gz = GzBuilder::new()
.comment(output.stdout.len().to_string().into_bytes())
.write(file, Compression::default());
gz.write_all(&output.stdout).unwrap();
gz.finish().unwrap();
}
}

View File

@ -101,12 +101,12 @@ Custom themes can be included in your configuration files or be saved independen
directory as TOML files.
To start creating a theme right away, you can begin by editing the default theme keys and values:
.sp
.Dl meli print-default-theme > ~/.config/meli/themes/new_theme.toml
.Dl meli --print-default-theme > ~/.config/meli/themes/new_theme.toml
.sp
.Pa new_theme.toml
will now include all keys and values of the "dark" theme.
.sp
.Dl meli print-loaded-themes
.Dl meli --print-loaded-themes
.sp
will print all loaded themes with the links resolved.
.Sh VALID ATTRIBUTE VALUES

View File

@ -17,29 +17,6 @@
.\" You should have received a copy of the GNU General Public License
.\" along with meli. If not, see <http://www.gnu.org/licenses/>.
.\"
.de Shortcut
.Sm
.Aq \\$1
\
.Po
.Em shortcuts.\\$2\&. Ns
.Em \\$3
.Pc
.Sm
..
.de ShortcutPeriod
.Aq \\$1
.Po
.Em shortcuts.\\$2\&. Ns
.Em \\$3
.Pc Ns
..
.de Command
.Bd -ragged
.Cm \\$*
.Ed
.sp
..
.Dd July 29, 2019
.Dt MELI 1
.Os
@ -66,7 +43,7 @@ if given, or at
.It Cm test-config Op Ar path
Test a configuration file for syntax issues or missing options.
.It Cm man Op Ar page
Print documentation page and exit (Piping to a pager is recommended).
Print documentation page and exit (Piping to a pager is recommended.)
.It Cm print-default-theme
Print default theme keys and values in TOML syntax, to be used as a blueprint.
.It Cm print-loaded-themes
@ -108,12 +85,14 @@ See
for the available configuration options.
.Pp
At any time, you may press
.Shortcut \&? general toggle_help
.Cm \&?
for a searchable list of all available actions and shortcuts, along with every possible setting and command that your version supports.
.Pp
The main visual navigation tool, the left-side sidebar may be toggled with
.ShortcutPeriod ` listing toggle_menu_visibility
\&.
.Cm `
(shortcuts.listing:
.Ic toggle_menu_visibility Ns
).
.Pp
Each mailbox may be viewed in 4 modes:
Plain views each mail individually, Threaded shows their thread relationship visually, Conversations collapses each thread of emails into a single entry, Compact shows one row per thread.
@ -126,22 +105,23 @@ section of your configuration.
See
.Xr meli-themes 5
for complete documentation on user themes.
.Pp
See
.Xr meli 7
for a more detailed tutorial on using
.Nm Ns
\&.
.Sh VIEWING MAIL
Open attachments by typing their index in the attachments list and then
.ShortcutPeriod a envelope_view open_attachment
\&.
.Cm a
.Po
shortcut
.Ic open_attachment
.Pc .
.Nm
will attempt to open text inside its pager, and other content via
.Cm xdg-open Ns
\&.
Press
.Shortcut m envelope_view open_mailcap
.Cm m
.Po
shortcut
.Ic open_mailcap
.Pc
instead to use the mailcap entry for the MIME type of the attachment, if any.
See
.Sx FILES
@ -149,12 +129,12 @@ for the location of the mailcap files and
.Xr mailcap 5
for their syntax.
You can save individual attachments with the
.Command save-attachment Ar INDEX Ar path-to-file
command.
.Em COMMAND
.Cm save-attachment Ar INDEX Ar path-to-file
where
.Ar INDEX
is the attachment's index in the listing.
If the path provided is a directory, the attachment is saved with its filename set to the filename in the attachment, if any.
If the 0th index is provided, the entire message is saved.
If the zeroth index is provided, the entire message is saved.
If the path provided is a directory, the message is saved as an eml file with its filename set to the messages message-id.
.Sh SEARCH
Each e-mail storage backend has a default search method assigned.
@ -183,8 +163,9 @@ To enable sqlite3 indexing for an account set
.Em search_backend
to
.Em sqlite3
in the configuration file and to create the sqlite3 index issue command:
.Command index Ar ACCOUNT_NAME Ns
in the configuration file and to create the sqlite3 index issue command
.Cm index Ar ACCOUNT_NAME Ns \&.
.sp
To search in the message body type your keywords without any special formatting.
To search in specific fields, prepend your search keyword with "field:" like so:
.Pp
@ -253,30 +234,35 @@ Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticable
.Nm
supports tagging in notmuch and IMAP/JMAP backends.
Tags can be searched with the `tags:` or `flags:` prefix in a search query, and can be modified by
.Command tag add TAG
.Cm tag add TAG
and
.Command tag remove TAG
.Cm tag remove TAG
(see
.Xr meli.conf 5 TAGS Ns
, settings
.Ic colors
and
.Ic ignore_tags
for how to set tag colors and tag visibility)
for how to set tag colors and tag visiblity)
.Sh COMPOSING
.Ss Opening the message Composer tab
To create a new mail message, press
.Shortcut m listing new_mail
while viewing a mailbox.
.Cm m
(shortcut
.Ic new_mail Ns
) while viewing a mailbox.
To reply to a mail, press
.ShortcutPeriod R envelope_view reply
\&.
.Cm R
.Po
shortcut
.Ic reply
.Pc .
Both these actions open the mail composer view in a new tab.
.Ss Editing text
.Bl -bullet -compact
.It
Edit the header fields by selecting with the arrow keys and pressing
.Shortcut Enter general focus_in_text_field
.Cm enter
to enter
.Em INSERT
mode and
@ -284,8 +270,10 @@ mode and
key to exit.
.It
At any time you may press
.Shortcut e composing edit_mail Ns
to launch your editor (see
.Cm e
(shortcut
.Ic edit_mail Ns
) to launch your editor (see
.Xr meli.conf 5 COMPOSING Ns
, setting
.Ic editor_command
@ -297,23 +285,19 @@ Your editor can be used in
.Ic embed
to
.Em true
in your composing settings
.Po
You can return to
.Nm
at any time by pressing
.Aq Ctrl-Z
.Pc
in your composing settings.
.It
When launched, your editor captures all input until it exits or stops.
.It
To stop your editor and return to
.Nm
press
.Aq Ctrl-z
and to resume editing press the
press Ctrl-z and to resume editing press the
.Ic edit_mail
command again.
command again
.Po
default
.Em e
.Pc .
.El
.Ss Attachments
Attachments may be handled with the
@ -323,12 +307,14 @@ Attachments may be handled with the
commands (see below).
.Ss Sending
Finally, pressing
.Shortcut s composing send_mail
will send your message according to your settings
.Cm s
(shortcut
.Ic send_mail Ns
) will send your message according to your settings
.Po
see
.Xr meli.conf 5 COMPOSING Ns
, setting name
, setting
.Ic send_mail
.Pc Ns
\&.
@ -377,9 +363,9 @@ is the default mode
commands are issued in
.Em COMMAND
mode, by default started with
.Shortcut \&: general enter_command_mode
.Cm \&:
and exited with
.Aq Esc
.Cm Esc
key.
.It EMBED
is the mode of the embed terminal emulator
@ -599,72 +585,19 @@ Mailcap entries are searched for in the following files, in this order:
.Sh SEE ALSO
.Xr meli.conf 5 ,
.Xr meli-themes 5 ,
.Xr meli 7 ,
.Xr xdg-open 1 ,
.Xr mailcap 5
.Sh CONFORMING TO
.Bl -bullet -compact
.It
XDG Standard
.Lk https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html Ns
\&.
.It
mailcap file, RFC 1524: A User Agent Configuration Mechanism For Multimedia Mail Format Information
.It
RFC 5322: Internet Message Format
.It
RFC 6532: Internationalized Email Headers
.It
RFC 2047: MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text
.It
RFC 3676: The Text/Plain Format and DelSp Parameters
.It
RFC 3156: MIME Security with OpenPGP
.It
RFC 2183: Communicating Presentation Information in Internet Messages: The Content-Disposition Header Field
.It
RFC 2369: The Use of URLs as Meta-Syntax for Core Mail List Commands and their Transport through Message Header Fields
.It
.Li maildir
.Lk https://cr.yp.to/proto/maildir.html Ns
\&.
.It
RFC 5321: Simple Mail Transfer Protocol
.It
RFC 3461: Simple Mail Transfer Protocol (SMTP) Service Extension for Delivery Status Notifications (DSNs)
.It
RFC 4954: SMTP Service Extension for Authentication
.It
RFC 6152: SMTP Service Extension for 8-bit MIME Transport
.It
RFC 4616: The PLAIN Simple Authentication and Security Layer (SASL) Mechanism
.It
RFC 3501: INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1
.It
RFC 3691: Internet Message Access Protocol (IMAP) UNSELECT command
.It
RFC 4549: Synch Ops for Disconnected IMAP4 Clients
.It
RFC 7162: IMAP Extensions: Quick Flag Changes Resynchronization (CONDSTORE) and Quick Mailbox Resynchronization (QRESYNC)
.It
RFC 8620: The JSON Meta Application Protocol (JMAP)
.It
RFC 8621: The JSON Meta Application Protocol (JMAP) for Mail
.It
RFC 3977: Network News Transfer Protocol (NNTP)
.It
RFC 6048: Network News Transfer Protocol (NNTP) Additions to LIST Command
.It
vCard Version 3, RFC 2426: vCard MIME Directory Profile
.It
vCard Version 4, RFC 6350: vCard Format Specification
.It
RFC 6868 Parameter Value Encoding in iCalendar and vCard
.El
.Aq https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html Ns
, maildir
.Aq https://cr.yp.to/proto/maildir.html Ns
, IMAPv4rev1 RFC3501, The JSON Meta Application Protocol (JMAP) RFC8620, The JSON Meta Application Protocol (JMAP) for Mail RFC8621.
.Sh AUTHORS
Copyright 2017-2022
.An Manos Pitsidianakis Mt manos@pitsidianak.is
Copyright 2017-2019
.An Manos Pitsidianakis Aq epilys@nessuent.xyz
Released under the GPL, version 3 or greater.
This software carries no warranty of any kind (See COPYING for full copyright and warranty notices).
This software carries no warranty of any kind.
(See COPYING for full copyright and warranty notices.)
.Pp
.Lk https://meli.delivery
.Aq https://meli.delivery

View File

@ -1,742 +0,0 @@
.\" meli - meli.7
.\"
.\" Copyright 2017-2022 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/>.
.\"
.\".de Hr
.\".Bd -literal -offset center
.\"╌╍─────────────────────────────────────────────────────────╍╌
.\".Ed
.\"..
.de Shortcut
.Sm
.Aq \\$1
\
.Po
.Em shortcuts.\\$2\&. Ns
.Em \\$3
.Pc
.Sm
..
.de ShortcutPeriod
.Aq \\$1
.Po
.Em shortcuts.\\$2\&. Ns
.Em \\$3
.Pc Ns
..
.de Command
.Bd -offset 1n -ragged
.Cm \\$*
.Ed
..
.Dd September 4, 2022
.Dt MELI 7
.Os
.Sh NAME
.Nm meli
.Nd Tutorial for the Meli Mail User Agent
.Sh SYNOPSIS
.Nm
.Op ...
.Sh DESCRIPTION
.Nm
is a terminal mail client aiming for extensive and user-frendly configurability.
.Bd -literal -offset center
^^ .-=-=-=-. ^^
^^ (`-=-=-=-=-`) ^^
(`-=-=-=-=-=-=-`) ^^ ^^
^^ (`-=-=-=-=-=-=-=-`) ^^
( `-=-=-=-(@)-=-=-` ) ^^
(`-=-=-=-=-=-=-=-=-`) ^^
(`-=-=-=-=-=-=-=-=-`) ^^
(`-=-=-=-=-=-=-=-=-`)
^^ (`-=-=-=-=-=-=-=-=-`) ^^
^^ (`-=-=-=-=-=-=-=-`) ^^
(`-=-=-=-=-=-=-`) ^^
^^ (`-=-=-=-=-`)
`-=-=-=-=-` ^^
.Ed
.Sh INTRODUCTION
To quit
.Nm
press
.Shortcut q general quit
at any time.
To go to the next tab on the right, press
.ShortcutPeriod T general next_tab
\&.
.Pp
When launched for the first time,
.Nm
will search for its configuration directory,
.Pa $XDG_CONFIG_HOME/meli/ Ns
\&.
If it doesn't exist, you will be asked if you want to create one and presented with a sample configuration file
.Pq Pa $XDG_CONFIG_HOME/meli/config.toml
that includes the basic settings required for setting up accounts allowing you to copy and edit right away.
See
.Xr meli.conf 5
for the available configuration options.
.Pp
At any time, you may press
.Shortcut \&? general toggle_help
for a searchable list of all available actions and shortcuts, along with every possible setting and command that your version supports.
.Pp
Each time a shortcut is mentioned in this document, you will find a parenthesis next to it with the name of the shortcut setting along with its section in the configuration settings so that you can modify it if you wish.
.Pp
For example, to set the
.Em toggle_help
shortcut mentioned in the previous paragraph, add the following to your configuration:
.Bd -literal -offset center
[shortcuts]
general.toggle_help = 'F1'
.Ed
.sp
Or alternatively:
.Bd -literal -offset center
[shortcuts.general]
toggle_help = 'F1'
.Ed
.Sh INTERACTING WITH Nm
You will be interacting with
.Nm
in four primary ways:
.Bl -column
.It 1.
keyboard shortcuts in
.Sy NORMAL
mode.
.It 2.
commands with arguments in
.Sy COMMAND
mode.
.It 3.
regular text input in text input widgets in
.Sy INSERT
mode.
.It 4.
any kind of input that gets passed directly into an embedded terminal in
.Sy EMBED
mode.
.El
.Sh MODES
.Nm
is a modal application, just like
.Xr vi 1 Ns
\&.
This means that pressing the same keys in different modes would yield different results.
This allows you to separate how the input is interpreted without the need to focus your input with a mouse.
.Bl -tag -width 8n
.It NORMAL
This is the default mode of
.Nm Ns
\&.
All keyboard shortcuts work in this mode.
.It COMMAND
Commands are issued in
.Sy COMMAND
mode, by default started with
.Shortcut \&: general enter_command_mode
and exited with
.Aq Esc
key.
.It EMBED
This is the mode of the embed terminal emulator.
To exit an embedded application, issue
.Aq Ctrl-C
to kill it or
.Aq Ctrl-Z
to stop the program and follow the instructions on
.Nm
to exit.
.It INSERT
This mode is entered when pressing
.Aq Enter
on a cursor selected text input field, and it captures all input as text input.
It is exited with the
.Aq Esc
key.
.El
.Sh ACTIVE SHORTCUTS POPUP
By pressing
.Shortcut \&? general toggle_help
at any time, the shortcuts popup display status gets toggled.
You can find all valid shortcuts for the current UI state you are in.
.Bd -literal -offset center
┌─shortcuts──Press ? to close────────────────────────────────┐
│ ▀│
│ use COMMAND "search" to find shortcuts █│
│ Use Up, Down, Left, Right to scroll. █│
│ █│
│ pager █│
│ █│
│ PageDown page_down █│
│ PageUp page_up │
│ j scroll_down │
│ k scroll_up │
│ │
│ view mail │
│ │
│ c add_addresses_to_contacts │
│ e edit │
│ u toggle_url_mode │
│ a open_attachment │
│ m open_mailcap │
│ R reply │
│ C-r reply_to_author │
│ C-g reply_to_all │
│ C-f forward │
│ M-r view_raw_source │
│ h toggle_expand_headers ▄│
└────────────────────────────────────────────────────────────┘
.Ed
.Bd -ragged -offset 3n
.Em Shows\ active\ shortcuts\ in\ order\ of\ the\ widget\ hierarchy\&.
.Ed
.Sh MAIN VIEW
.Bd -literal -offset center
┌───────────────────────┐
├────┼──────────────────┤
│___ │ ___________ │
│ _ │ _______________ │
│ _ │__________________│
│ _ │ ___________ │
│ │ _____ │
│ │ │
└────┴──────────────────┘
.Ed
.Bd -ragged -offset 3n
.Em The\ main\ view's\ layout\&.
.Ed
.sp
This is the view you will spend more time with in
.Nm Ns
\&.
.Pp
Press
.Shortcut ` listing toggle_menu_visibility
to toggle the sidebars visibility.
.Pp
Press
.Shortcut Left listing focus_right
to switch focus on the sidebar menu.
Press
.Shortcut Right listing focus_left
to switch focus on the e-mail list.
.Pp
On the e-mail list, press
.Shortcut k listing scroll_up
to scroll up, and
.Shortcut j listing scroll_down
to scroll down.
Press
.Shortcut Enter listing open_entry
to open an e-mail entry and
.Shortcut i listing exit_entry
to exit it.
.Bd -ragged
.Sy The sidebar\&.
.Ed
.Bd -literal -offset center
┌─────────────┉┉┉┉┉✂
│ mail▐ contact li✂
│personal account ✂
│ 0 INBOX ✂
│ 1 ┣━Sent ✂
│ 2 ┣━Lists ✂
│ 3 ┃ ┣━meli-dev ✂
│ 4 ┃ ┗━meli ✂
│ 5 ┣━Drafts ✂
│ 6 ┣━Trash ✂
│ 7 ┗━foobar ✂
┇ 8 Trash ✂
✂ ✂ ✂ ✂ ✂ ✂ ✂ ✂ ✂ ✂
.Ed
.sp
Press
.Shortcut k listing scroll_up
to scroll up, and
.Shortcut j listing scroll_down
to scroll down.
.Pp
Press
.Shortcut Enter listing open_mailbox
to open an entry (either a mailbox or an account name).
Entering an account name will show you a page with details about the account and its network connection, depending on the backend.
.Pp
While focused in the sidebar, you can
.Dq collapse
a mailbox tree, if it has children, and you can open it with
.ShortcutPeriod Space listing toggle_mailbox_collapse
\&.
You can have mailbox trees collapsed on startup by default by setting a mailbox's
.Ic collapsed
setting to
.Em true Ns
\&.
See
.Xr meli.conf 5 section MAILBOXES
for details.
.Pp
You can increase the sidebar's width with
.Shortcut Ctrl-p listing increase_sidebar
and decrease with
.ShortcutPeriod Ctrl-o listing decrease_sidebar
\&.
.Bd -ragged
.Sy The status bar.
.Ed
.Bd -literal -offset center
┌────────────────────────────────────────────────────┈┈
│NORMAL | Mailbox: Inbox, Messages: 25772, New: 3006
└────────────────────────────────────────────────────┈┈
.Ed
.Pp
The status bar shows which mode you are, and the status message of the current view.
In the pictured example, it shows the status of a mailbox called
.Dq Inbox
with lots of e-mails.
.Bd -ragged
.Sy The number modifier buffer.
.Ed
.Bd -literal -offset center
┈┈────────────┐
12 │
┈┈────────────┘
.Ed
.Pp
Some commands may accept a number modifier.
.Tg number-modifier
For example, scroll down commands can receive a multiplier
.Em n
to scroll down
.Em n
entries.
Another use of the number buffer is opening URLs inside the pager.
See
.Sx PAGER
for an explanation of interacting with URLs in e-mails.
.Pp
Pressing numbers in
.Sy NORMAL
mode will populate this buffer.
To erase it, press the
.Aq Esc
key.
.Sh MAIL LIST
There are four different list styles:
.Bl -hyphen -compact
.It
.Qq plain
which shows one line per e-mail.
.It
.Qq threaded
which shows a threaded view with drawn tree structure.
.It
.Qq compact
which shows one line per thread which can include multiple e-mails.
.It
.Qq conversations
which shows more than one line per thread which can include multiple e-mails with more details about the thread.
.El
.Bd -ragged
.Sy Plain view\&.
.Ed
.Bd -literal -offset center
│42 Fri, 02 Sep 2022 19:51 xxxxxxxxxxxxx < [PATCH 3/8] │
│43 Fri, 02 Sep 2022 19:51 xxxxxxxxxxxxx < [PATCH 2/8] │
│44 Fri, 02 Sep 2022 19:51 xxxxxxxxxxxxx < [PATCH 1/8] │
|45 Fri, 02 Sep 2022 19:51 xxxxxxxxxxxxx < [PATCH 0/8] |
│46 Fri, 02 Sep 2022 18:18 xxxxxxxx <xxxxx Re: [PATCH 3│
.Ed
.Bd -ragged
.Sy Threaded view\&.
.Ed
.Bd -literal -offset center
│12 9 hours ago xxxxxxxxxxxxxxx [PATCH v3 0│
│13 9 hours ago xxxxxxxxxxxxxxx ├─>[PATCH │
│14 9 hours ago xxxxxxxxxxxxxxx ├─>[PATCH │
|15 9 hours ago xxxxxxxxxxxxxxx ├─>[PATCH |
│16 9 hours ago xxxxxxxxxxxxxxx ├─>[PATCH │
│17 9 hours ago xxxxxxxxxxxxxxx └─>[PATCH │
│18 2022-08-23 01:23:51 xxxxxxxxxxxxxxx [RFC v4 00/│
│19 2022-08-23 01:23:52 xxxxxxxxxxxxxxx ├─>[RFC v4│
|20 2022-08-30 10:30:16 xxxxxxxxxxxxxxx │ └─> |
│21 6 days ago xxxxxxxxxxxxxxx │ └─> │
│22 2022-08-23 01:23:53 xxxxxxxxxxxxxxx ├─>[RFC v4│
.Ed
.Bd -ragged
.Sy Compact view\&.
.Ed
.Bd -literal -offset center
│18 2022-…:38 xxxxxxxxxxxxxxx [PATCH v3 3/3] u…_l() (2) │
|19 2022-…:49 xxxxxxxxxxxxxxx [PATCH v8 0/7] A…e (3) |
│20 2022-…:10 xxxxxxxxxxxxxxx [PATCH v8 2/7] f…s (2) │
│21 2022-…:38 xxxxxxxxxxxxxxx [PATCH v8 3/7] b…s (2) │
│22 2022-…:53 xxxxxxxxxxxxxxx [PATCH v6 00/10] p…g (31) │
.Ed
.Bd -ragged
.Sy Conversations view\&.
.Ed
.Bd -literal -offset center
│[PATCH v2] xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (5) │
|1 day ago▁▁▁▁xxxxxxxxxxxxx <xxxxxxxxxxxxx@xxxxxxxxxx>, xxxxx│
│ |
│[PATCH v2 0/8] xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx│
│1 day ago▁▁▁▁xxxxxxxxxxxxxxx <xxxxxxxxxx@xxxxxxxxxxxxxx>, xx│
| │
│[PATCH 0/2] xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (4) |
│2 days ago▁▁▁▁xxxxxxxxxxxxxxxx <xxxxxxxx@xxxxxxxxxxx>, xxxxx│
│ │
│[PATCH 0/8] xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (12) │
│2 days ago▁▁▁▁xxxxxxxxxxxxx <xxxxxxxx@xxxxxxxxxx>, xxxxxxxxx│
.Ed
.sp
.sp
.Sy Performing actions on entries and/or selections\&.
.Pp
Press
.Shortcut v listing select_entry
to toggle the selection of a single entry.
.Qq select_entry
can be prefixed by a number modifier and affixed by a scrolling motion (up or down) to select multiple entries.
.Tg number-modifier
Simple set operations can be performed on a selection with these shortcut modifiers:
.sp
.Bl -hyphen -compact
.It
Union modifier:
.Shortcut Ctrl-u listing union_modifier
.It
Difference modifier:
.Shortcut Ctrl-d listing diff_modifier
.It
Intersection modifier:
.Shortcut Ctrl-i listing intersection_modifier
.El
.Pp
To set an entry as
.Qq read
\&, use the
.Shortcut n listing set_seen
shortcut.
To set an entry as
.Qq unread
\&, use the command
.Command set unseen
.sp
which also has its complement
.Command set seen
.sp
action.
.Pp
For e-mail backends that support tags
.Po
like
.Qq IMAP
or
.Qq notmuch Ns
.Pc
you can use the following commands on entries and selections to modify them:
.Command tag add TAG
.Command tag remove TAG
.sp
(see
.Xr meli.conf 5 TAGS Ns
, settings
.Ic colors
and
.Ic ignore_tags
for how to set tag colors and tag visibility)
.Sh PAGER
You can open an e-mail entry by pressing
.ShortcutPeriod Enter listing open_entry
\&. This brings up the e-mail view with the e-mail content inside a pager.
.Bd -literal -offset center
┌────────────────────────────────────────────────────────────┐
│Date: Sat, 21 May 2022 16:16:11 +0300 ▀│
│From: Narrator <narrator@example.com> █│
│To: Stanley <427@example.com> █│
│Subject: The e-mail ending █│
│Message-ID: <gambheerata@example.com> █│
│ █│
│The story, and the choices, or what have you, and therefore█│
│by becoming it is! So on and so forth, until inevitably, we │
│all until the end of time. At which time, everything all at │
│once, so now you see? Blah, blah, blah, rah, rah, rah... │
│We've eaten too much and it can't be just yet. No, no! │
│Until two-hundred and forty-five! But the logic of │
│elimination, working backwards, the deduction therefore │
│becomes impossible to manufacture. It went on for nearly │
│ten thousand years, until just yesterday. Here and there, │
│forward and back, and never a moment before lunchtime. It │
│can't be! It's the only thing there is! How many billions │
│left until so much more than forever ago! Which is why I │
│say: │
│ │
│The story, and the choices, or what have you, and therefore │
│by becoming it is! So on and so forth, until inevitably, we▄│
└────────────────────────────────────────────────────────────┘
.Ed
.Bd -ragged -offset 3n
.Em The\ pager\ displaying\ an\ e-mail\&.
.Ed
.Pp
The pager is simple to use.
Scroll with the following:
.Bl -hang -width 27n
.It Go to next pager page
.Shortcut PageDown pager page_down
.It Go to previous pager page
.Shortcut PageUp pager page_up
.It Scroll down pager.
.Shortcut j pager scroll_down
.It Scroll up pager.
.Shortcut k pager scroll_up
.El
.sp
All scrolling shortcuts can be prefixed with a number modifier
.Tg number-modifier
which will act as a multiplier.
.Pp
The pager can enter a special
.Em url
mode which will prefix all detected hyperlinks and e-mail addresses with a number inside square brackets
.ShortcutPeriod u pager toggle_url_mode
\&.
Writing down a chosen number as a number modifier
.Tg number-modifier
and pressing
.Shortcut g envelope_view go_to_url
will attempt to open the link with the system's default open command
.Po
.Xr xdg-open 1
in supported OSes,
and
.Xr open 1
on MacOS
.Pc Ns
\&.
To override with a custom launcher, see
.Qo
.Li pager
.Qc
configuration setting
.Qo
.Li url_launcher
.Qc
.Po
see
.Xr meli.conf 5 PAGER
for more details
.Pc Ns
\&.
.Sh MAIL VIEW
Other things you can do when viewing e-mail:
.Bl -bullet -compact
.It
Most importantly, you can exit the mail view with:
.Shortcut i listing exit_entry
.It
Add addresses from the e-mail headers to contacts:
.Shortcut c envelope_view add_addresses_to_contacts
.It
Open an attachment by entering its index as a number modifier and pressing:
.Tg number-modifier
.Shortcut a envelope_view open_attachment
.It
Open an attachment by its
.Xr mailcap 4
entry by entering its index as a number modifier and pressing:
.Shortcut m envelope_view open_mailcap
.It
Reply to envelope:
.Shortcut R envelope_view reply
.It
Reply to author:
.Shortcut Ctrl-r envelope_view reply_to_author
.It
Reply to all/Reply to list/Follow up:
.Shortcut Ctrl-g envelope_view reply_to_all
.It
Forward email:
.Shortcut Ctrl-f envelope_view forward
.It
Expand extra headers: (References and others)
.Shortcut h envelope_view toggle_expand_headerk
.It
View envelope source in a pager: (toggles between raw and decoded source)
.Shortcut M-r envelope_view view_raw_source
.It
Return to envelope_view if viewing raw source or attachment:
.Shortcut r envelope_view return_to_normal_view
.El
.Sh COMPOSING
To compose an e-mail, you can either start with an empty draft by pressing
.Shortcut m listing new_mail
which opens a composer view in a new tab.
To reply to a specific e-mail, when in envelope view you can select the specific action you want to take:
.sp
.Bl -bullet -compact
.It
Reply to envelope.
.Shortcut R envelope_view reply
.It
Reply to author.
.Shortcut Ctrl-r envelope_view reply_to_author
.It
Reply to all.
.Shortcut Ctrl-g envelope_view reply_to_all
.El
.sp
To launch your editor, press
.ShortcutPeriod e composing edit_mail
\&.
To send your draft, press
.ShortcutPeriod s composing send_mail
\&.
To save the draft without submission, enter the command
.Command close
.sp
and select
.Qq save as draft Ns
\&.
You can return to the draft by going to your
.Qq Drafts
mailbox and selecting
.ShortcutPeriod e envelope_view edit_mail
\&.
.Bd -literal -offset center
┌────────────────────────────────────────────────────────────┐
│ mail▐ contact list ▐ composing ▍███████████████████████│
│ COMPOSING MESSAGE │
│ Date Mon, 05 Sep 2022 17:49:19 +0300 │
│ From myself <myself@example.com>░░░░ │
│ To friend <myfriend@example.com>░░ │
│ Cc ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
│ Bcc ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
│ Subject This is my subject!░░░░░░░░░░░░ │
│ │
│ Hello friend!░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
│ │
│ ☐ don't sign │
│ ☐ don't encrypt │
│ no attachments │
│ │
│NORMAL | Mailbox: Inbox, Messages: 25772, New: 3006 │
└────────────────────────────────────────────────────────────┘
.Ed
.Bd -ragged -offset 3n
.Em The\ lightly\ highlighted\ cells\ represent\ text\ input\ fields\&.
.Ed
.sp
If you enable the embed terminal option, you can launch your terminal editor of choice when you press
.Ic edit_mail Ns
\&.
.Bd -literal -offset center
┌────────────────────────────────────────────────────────────┐
│ mail▐ contact list ▐ composing ▍███████████████████████│
│ ╓COMPOSING MESSAGE┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╖ │
│ ║ p/v/f/h/5/T/m/07f56b6e-ec09-49d9-b8d8-f0c5a81e7826 ║ │
│ ║ 7 Date: Mon, 05 Sep 2022 18:43:10 +0300 ║ │
│ ║ 6 From: Mister Cardholder <mrholder@example.com> ║ │
│ ║ 5 To: ║ │
│ ║ 4 Cc: ║ │
│ ║ 3 Bcc: ║ │
│ ║ 2 Subject: ║ │
│ ║ 1 User-Agent: meli 0.7.2 ║ │
│ ║8 █ ║ │
│ ║~ ║ │
│ ║~ ║ │
│ ║~ ║ │
│ ║~ ║ │
│ ║ N… <6e-ec09-49d9-b8d8-f0c5a81e7826 100% ㏑:8 ℅:1║ │
│ ╚════════════════════════════════════════════════════╝ │
│ │
│ │
│ ☐ don't sign │
│ ☐ don't encrypt │
│ no attachments │
│ │
│EMBED | Mailbox: Inbox, Messages: 25772, New: 3006 │
└────────────────────────────────────────────────────────────┘
.Ed
.Bd -ragged -offset 3n
.Bf -emphasis
.Xr neovim 1 Ns
\ running\ inside\ the\ composing\ tab\&.
.Ef
The\ double\ line\ border\ annotates\ the\ area\ of\ the\ embedded\ terminal,
the\ actual\ embedding\ is\ seamless\&.
.Ed
.Ss composing mail commands
.Bl -tag -width 36n
.It Cm add-attachment Ar PATH
in composer, add
.Ar PATH
as an attachment
.It Cm add-attachment < Ar CMD Ar ARGS
in composer, pipe
.Ar CMD Ar ARGS
output into an attachment
.It Cm add-attachment-file-picker
Launch command defined in the configuration value
.Ic file_picker_command
in
.Xr meli.conf 5 TERMINAL
.It Cm add-attachment-file-picker < Ar CMD Ar ARGS
Launch command
.Ar CMD Ar ARGS Ns
\&.
The command should print file paths in stderr, separated by NULL bytes.
.It Cm remove-attachment Ar INDEX
remove attachment with given index
.It Cm toggle sign
toggle between signing and not signing this message.
If the gpg invocation fails then the mail won't be sent.
See
.Xr meli.conf 5 PGP
for PGP configuration.
.It Cm save-draft
saves a copy of the draft in the Draft folder
.El
.\" TODO add contacts section
.Sh THEMES
See
.Xr meli-themes 5
for documentation on how to theme
.Nm Ns
\&.
.Sh SEE ALSO
.Xr meli 1 ,
.Xr meli.conf 5 ,
.Xr meli-themes 5 ,
.Xr xdg-open 1 ,
.Xr mailcap 5
.Sh AUTHORS
Copyright 2017-2022
.An Manos Pitsidianakis Mt manos@pitsidianak.is
Released under the GPL, version 3 or greater.
This software carries no warranty of any kind.
(See COPYING for full copyright and warranty notices.)
.Pp
.Lk https://meli.delivery
.Lk https://github.com/meli/meli
.Lk https://crates.io/crates/meli

View File

@ -303,15 +303,18 @@ On startup, meli should evaluate this command which if successful must only retu
.Ss JMAP only
JMAP specific options
.Bl -tag -width 36n
.It Ic server_url Ar String
.It Ic server_hostname Ar String
example:
.Qq http://mail.example.com
.Qq http://mail.example.com:8080
.Qq https://mail.example.com
.Qq mail.example.com
.It Ic server_username Ar String
Server username
.It Ic server_password Ar String
Server password
.It Ic server_port Ar number
.Pq Em optional
The port to connect to
.\" default value
.Pq Em 443
.It Ic danger_accept_invalid_certs Ar boolean
.Pq Em optional
Do not validate TLS certificates.
@ -632,12 +635,12 @@ next_tab = 'T'
.Ed
.sp
and for
.Em listing Ns
.Em compact-listing Ns
:
.Bd -literal
[shortcuts.listing]
open_entry = "Enter"
exit_entry = 'i'
[shortcuts.compact-listing]
open_thread = "Enter"
exit_thread = 'i'
.Ed
.sp
.Pp
@ -778,20 +781,16 @@ Decrease sidebar width.
Toggle visibility of side menu in mail list.
.\" default value
.Pq Em `
.It Ic focus_left
Switch focus on the left.
.\" default value
.Pq Em Left
.It Ic focus_right
Switch focus on the right.
.\" default value
.Pq Em Right
.It Ic exit_entry
Exit e-mail entry.
.El
.sp
.Em compact-listing
.Bl -tag -width 36n
.It Ic exit_thread
Exit thread view
.\" default value
.Pq Em i
.It Ic open_entry
Open e-mail entry.
.It Ic open_thread
Open thread.
.\" default value
.Pq Em Enter
.El
@ -987,14 +986,8 @@ Enable notifications.
.Pq Em optional
Script to pass notifications to, with title as 1st arg and body as 2nd
.\" default value
.Pq Em none
.It Ic new_mail_script Ar String
.Pq Em optional
A command to pipe new mail notifications through (preferred over
.Ic script Ns
), with title as 1st arg and body as 2nd.
.\" default value
.Pq Em none
.Pq Em none Ns
\&.
.It Ic xbiff_file_path Ar String
.Pq Em optional
File that gets its size updated when new mail arrives.
@ -1023,11 +1016,6 @@ Always show headers when scrolling.
Pipe html attachments through this filter before display
.\" default value
.Pq Em none
.It Ic html_open Ar String
.Pq Em optional
A command to open html files.
.\" default value
.Pq Em none
.It Ic filter Ar String
.Pq Em optional
A command to pipe mail output through for viewing in pager.
@ -1067,11 +1055,6 @@ The URL will be given as the first argument of the command.
.El
.Sh LISTING
.Bl -tag -width 36n
.It Ic show_menu_scrollbar Ar boolean
.Pq Em optional
Show auto-hiding scrollbar in accounts sidebar menu.
.\" default value
.Pq Em true
.It Ic datetime_fmt Ar String
.Pq Em optional
Datetime formatting passed verbatim to strftime(3).
@ -1119,26 +1102,11 @@ Sets the character to print as the divider between the accounts list and the mes
This is the width of the right container to the entire screen width.
.\" default value
.Pq Em 90
.It Ic unseen_flag Ar Option<String>
Flag to show if thread entry contains unseen mail.
.It Ic show_menu_scrollbar Ar boolean
.Pq Em optional
Show auto-hiding scrollbar in accounts sidebar menu.
.\" default value
.Pq Em "●"
.It Ic thread_snoozed_flag Ar Option<String>
Flag to show if thread has been snoozed.
.\" default value
.Pq Em "💤"
.It Ic selected_flag Ar Option<String>
Flag to show if thread entry has been selected.
.\" default value
.Pq Em "☑️"
.It Ic attachment_flag Ar Option<String>
Flag to show if thread entry contains attachments.
.\" default value
.Pq Em "📎"
.It Ic thread_subject_pack Ar bool
Should threads with differentiating Subjects show a list of those subjects on the entry title?
.\" default value
.Pq Em "true"
.Pq Em true
.El
.Ss Examples of sidebar mailbox tree customization
The default values

View File

@ -77,16 +77,6 @@
### Gmail auto saves sent mail to Sent folder, so don't duplicate the effort:
#composing.store_sent_mail = false
#
##[accounts."jmap account"]
##root_mailbox = "INBOX"
##format = "jmap"
##server_url="http://localhost:8080"
##server_username="user@hostname.local"
##server_password="changeme"
##listing.index_style = "Conversations"
##identity = "user@hostname.local"
##subscribed_mailboxes = ["*", ]
##composing.send_mail = 'server_submission'
#
#[pager]
#filter = "COLUMNS=72 /usr/local/bin/pygmentize -l email"
@ -103,6 +93,10 @@
#[shortcuts.composing]
#edit_mail = 'e'
#
##Thread view defaults:
#[shortcuts.compact-listing]
#exit_thread = 'i'
#
#[shortcuts.contact-list]
#create_contact = 'c'
#edit_contact = 'e'
@ -117,7 +111,6 @@
#next_account = 'h'
#new_mail = 'm'
#set_seen = 'n'
#exit_entry = 'i'
#
##Pager defaults
#

View File

@ -49,10 +49,6 @@ uuid = { version = "^1", features = ["serde", "v4", "v5"] }
xdg = "2.1.0"
xdg-utils = "^0.4.0"
[dev-dependencies]
mailin-embedded = { version = "0.7", features = ["rtls"] }
stderrlog = "^0.5"
[features]
default = ["unicode_algorithms", "imap_backend", "maildir_backend", "mbox_backend", "vcard", "sqlite3", "smtp", "deflate_compression"]

View File

@ -161,7 +161,7 @@ fn main() -> Result<(), std::io::Error> {
}
}
fn set_general_categories<'u>(codepoints: &mut [Codepoint<'u>], unicode_data: &'u str) {
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<_>>();
if fields.len() > FIELD_CATEGORY {
@ -172,7 +172,7 @@ fn main() -> Result<(), std::io::Error> {
}
}
fn set_eaw_widths(codepoints: &mut [Codepoint<'_>], eaw_data_lines: &str) {
fn set_eaw_widths(codepoints: &mut Vec<Codepoint<'_>>, eaw_data_lines: &str) {
// Read from EastAsianWidth.txt, set width values on the codepoints
for line in eaw_data_lines.lines() {
let line = line.trim().split('#').next().unwrap_or(line);
@ -220,8 +220,7 @@ fn main() -> Result<(), std::io::Error> {
}
}
}
fn set_emoji_widths(codepoints: &mut [Codepoint<'_>], emoji_data_lines: &str) {
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('#') {
@ -303,7 +302,7 @@ fn main() -> Result<(), std::io::Error> {
}
}
}
fn set_hardcoded_ranges(codepoints: &mut [Codepoint<'_>]) {
fn set_hardcoded_ranges(codepoints: &mut Vec<Codepoint<'_>>) {
// Mark private use and surrogate codepoints
// Private use can be determined awkwardly from UnicodeData.txt,
// but we just hard-code them.

View File

@ -19,14 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
//! # vCard format
//!
//! This module implements the standards:
//!
//! - Version 3 (read-only) [RFC 2426: vCard MIME Directory Profile](https://datatracker.ietf.org/doc/2426)
//! - Version 4 [RFC 6350: vCard Format Specification](https://datatracker.ietf.org/doc/rfc6350/)
//! - Parameter escaping [RFC 6868 Parameter Value Encoding in iCalendar and vCard](https://datatracker.ietf.org/doc/rfc6868/)
/// Convert VCard strings to meli Cards (contacts).
use super::*;
use crate::error::{MeliError, Result};
use crate::parsec::{match_literal_anycase, one_or_more, peek, prefix, take_until, Parser};
@ -40,12 +33,12 @@ pub trait VCardVersion: core::fmt::Debug {}
pub struct VCardVersionUnknown;
impl VCardVersion for VCardVersionUnknown {}
/// Version 4 <https://tools.ietf.org/html/rfc6350>
/// https://tools.ietf.org/html/rfc6350
#[derive(Debug)]
pub struct VCardVersion4;
impl VCardVersion for VCardVersion4 {}
/// <https://tools.ietf.org/html/rfc2426>
/// https://tools.ietf.org/html/rfc2426
#[derive(Debug)]
pub struct VCardVersion3;
impl VCardVersion for VCardVersion3 {}

View File

@ -57,18 +57,19 @@ use self::maildir::MaildirType;
#[cfg(feature = "mbox_backend")]
use self::mbox::MboxType;
use super::email::{Envelope, EnvelopeHash, Flag};
use futures::stream::Stream;
use std::any::Any;
use std::borrow::Cow;
use std::collections::BTreeSet;
use std::collections::HashMap;
use std::fmt;
use std::fmt::Debug;
use std::future::Future;
use std::ops::Deref;
use std::pin::Pin;
use std::sync::{Arc, RwLock};
use futures::stream::Stream;
use std::future::Future;
use std::pin::Pin;
use std::collections::HashMap;
#[macro_export]
macro_rules! get_path_hash {
($path:expr) => {{
@ -245,7 +246,7 @@ impl Backends {
""
},
key,
if cfg!(feature = "notmuch_backend") && key == "notmuch" {
if cfg!(feature = "notmuch_backend") {
NOTMUCH_ERROR_DETAILS
} else {
""
@ -256,6 +257,27 @@ impl Backends {
}
}
#[derive(
Debug, Serialize, Deserialize, Clone, PartialOrd, Ord, Default, Copy, Hash, PartialEq, Eq,
)]
pub struct AccountHash(pub u64);
#[derive(
Debug, Serialize, Deserialize, Clone, PartialOrd, Ord, Default, Copy, Hash, PartialEq, Eq,
)]
pub struct MailboxHash(pub u64);
impl std::fmt::Display for AccountHash {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "AccountHash({})", self.0)
}
}
impl std::fmt::Display for MailboxHash {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "MailboxHash({})", self.0)
}
}
#[derive(Debug, Clone)]
pub enum BackendEvent {
Notice {
@ -264,9 +286,6 @@ pub enum BackendEvent {
level: crate::LoggingLevel,
},
Refresh(RefreshEvent),
AccountStateChange {
message: Cow<'static, str>,
},
//Job(Box<Future<Output = Result<()>> + Send + 'static>)
}
@ -591,8 +610,6 @@ pub trait BackendMailbox: Debug {
fn count(&self) -> Result<(usize, usize)>;
}
pub type AccountHash = u64;
pub type MailboxHash = u64;
pub type Mailbox = Box<dyn BackendMailbox + Send + Sync>;
impl Clone for Mailbox {
@ -663,12 +680,6 @@ impl std::convert::TryFrom<&[EnvelopeHash]> for EnvelopeHashBatch {
}
}
impl Into<BTreeSet<EnvelopeHash>> for &EnvelopeHashBatch {
fn into(self) -> BTreeSet<EnvelopeHash> {
self.iter().collect::<BTreeSet<EnvelopeHash>>()
}
}
impl EnvelopeHashBatch {
pub fn iter(&self) -> impl std::iter::Iterator<Item = EnvelopeHash> + '_ {
std::iter::once(self.first).chain(self.rest.iter().cloned())
@ -677,10 +688,6 @@ impl EnvelopeHashBatch {
pub fn len(&self) -> usize {
1 + self.rest.len()
}
pub fn to_set(&self) -> BTreeSet<EnvelopeHash> {
self.into()
}
}
#[derive(Default, Clone)]

View File

@ -52,6 +52,7 @@ use futures::stream::Stream;
use std::collections::hash_map::DefaultHasher;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::convert::TryFrom;
use std::convert::TryInto;
use std::hash::Hasher;
use std::pin::Pin;
use std::str::FromStr;
@ -65,6 +66,7 @@ pub type MessageSequenceNumber = ImapNum;
pub static SUPPORTED_CAPABILITIES: &[&str] = &[
"AUTH=OAUTH2",
"CHILDREN",
#[cfg(feature = "deflate_compression")]
"COMPRESS=DEFLATE",
"CONDSTORE",
@ -191,7 +193,7 @@ impl UIDStore {
#[derive(Debug)]
pub struct ImapType {
_is_subscribed: Arc<IsSubscribedFn>,
is_subscribed: Arc<IsSubscribedFn>,
connection: Arc<FutureMutex<ImapConnection>>,
server_conf: ImapServerConf,
uid_store: Arc<UIDStore>,
@ -273,7 +275,7 @@ impl MailBackend for ImapType {
_ => {
if SUPPORTED_CAPABILITIES
.iter()
.any(|c| c.eq_ignore_ascii_case(name.as_str()))
.any(|c| c.eq_ignore_ascii_case(&name.as_str()))
{
*status = MailBackendExtensionStatus::Enabled { comment: None };
}
@ -504,7 +506,7 @@ impl MailBackend for ImapType {
let account_hash = uid_store.account_hash;
main_conn_lck.add_refresh_event(RefreshEvent {
account_hash,
mailbox_hash: 0,
mailbox_hash: MailboxHash(0),
kind: RefreshEventKind::Failure(err.clone()),
});
return Err(err);
@ -937,7 +939,7 @@ impl MailBackend for ImapType {
}
let ret: Result<()> = ImapResponse::try_from(response.as_slice())?.into();
ret?;
let new_hash = get_path_hash!(path.as_str());
let new_hash = MailboxHash(get_path_hash!(path.as_str()));
uid_store.mailboxes.lock().await.clear();
Ok((new_hash, new_mailbox_fut?.await.map_err(|err| MeliError::new(format!("Mailbox create was succesful (returned `{}`) but listing mailboxes afterwards returned `{}`", String::from_utf8_lossy(&response), err)))?))
}))
@ -1074,7 +1076,7 @@ impl MailBackend for ImapType {
.read_response(&mut response, RequiredResponses::empty())
.await?;
}
let new_hash = get_path_hash!(new_path.as_str());
let new_hash = MailboxHash(get_path_hash!(new_path.as_str()));
let ret: Result<()> = ImapResponse::try_from(response.as_slice())?.into();
ret?;
uid_store.mailboxes.lock().await.clear();
@ -1120,32 +1122,32 @@ impl MailBackend for ImapType {
Subject(t) => {
s.push_str(" SUBJECT \"");
s.extend(escape_double_quote(t).chars());
s.push('"');
s.push_str("\"");
}
From(t) => {
s.push_str(" FROM \"");
s.extend(escape_double_quote(t).chars());
s.push('"');
s.push_str("\"");
}
To(t) => {
s.push_str(" TO \"");
s.extend(escape_double_quote(t).chars());
s.push('"');
s.push_str("\"");
}
Cc(t) => {
s.push_str(" CC \"");
s.extend(escape_double_quote(t).chars());
s.push('"');
s.push_str("\"");
}
Bcc(t) => {
s.push_str(" BCC \"");
s.extend(escape_double_quote(t).chars());
s.push('"');
s.push_str("\"");
}
AllText(t) => {
s.push_str(" TEXT \"");
s.extend(escape_double_quote(t).chars());
s.push('"');
s.push_str("\"");
}
Flags(v) => {
for f in v {
@ -1279,7 +1281,7 @@ impl ImapType {
};
let server_port = get_conf_val!(s["server_port"], 143)?;
let use_tls = get_conf_val!(s["use_tls"], true)?;
let use_starttls = use_tls && get_conf_val!(s["use_starttls"], server_port != 993)?;
let use_starttls = use_tls && get_conf_val!(s["use_starttls"], !(server_port == 993))?;
let danger_accept_invalid_certs: bool =
get_conf_val!(s["danger_accept_invalid_certs"], false)?;
#[cfg(feature = "sqlite3")]
@ -1321,7 +1323,7 @@ impl ImapType {
let account_hash = {
let mut hasher = DefaultHasher::new();
hasher.write(s.name.as_bytes());
hasher.finish()
AccountHash(hasher.finish())
};
let account_name = Arc::new(s.name().to_string());
let uid_store: Arc<UIDStore> = Arc::new(UIDStore {
@ -1337,7 +1339,7 @@ impl ImapType {
Ok(Box::new(ImapType {
server_conf,
_is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)),
is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)),
connection: Arc::new(FutureMutex::new(connection)),
uid_store,
}))
@ -1458,7 +1460,7 @@ impl ImapType {
} else {
mailboxes.insert(mailbox.hash, mailbox);
}
} else if let Ok(status) = protocol_parser::status_response(l).map(|(_, v)| v) {
} else if let Ok(status) = protocol_parser::status_response(&l).map(|(_, v)| v) {
if let Some(mailbox_hash) = status.mailbox {
if mailboxes.contains_key(&mailbox_hash) {
let entry = mailboxes.entry(mailbox_hash).or_default();
@ -1474,7 +1476,7 @@ impl ImapType {
debug!("parse error for {:?}", l);
}
}
mailboxes.retain(|_, v| v.hash != 0);
mailboxes.retain(|_, v| v.hash.0 != 0);
conn.send_command(b"LSUB \"\" \"*\"").await?;
conn.read_response(&mut res, RequiredResponses::LSUB_REQUIRED)
.await?;
@ -1483,7 +1485,7 @@ impl ImapType {
if !l.starts_with(b"*") {
continue;
}
if let Ok(subscription) = protocol_parser::list_mailbox_result(l).map(|(_, v)| v) {
if let Ok(subscription) = protocol_parser::list_mailbox_result(&l).map(|(_, v)| v) {
if let Some(f) = mailboxes.get_mut(&subscription.hash()) {
if f.special_usage() == SpecialUsageMailbox::Normal
&& subscription.special_usage() != SpecialUsageMailbox::Normal
@ -1861,7 +1863,7 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
.unwrap()
.entry(mailbox_hash)
.or_default()
.insert(message_sequence_number - 1, uid);
.insert((message_sequence_number - 1).try_into().unwrap(), uid);
uid_store
.hash_index
.lock()

View File

@ -177,8 +177,8 @@ mod sqlite3_m {
.prepare("SELECT MAX(uid) FROM envelopes WHERE mailbox_hash = ?1;")?;
let mut ret: Vec<UID> = stmt
.query_map(sqlite3::params![mailbox_hash as i64], |row| {
row.get(0).map(|i: Sqlite3UID| i as UID)
.query_map(sqlite3::params![mailbox_hash.0 as i64], |row| {
Ok(row.get(0).map(|i: Sqlite3UID| i as UID)?)
})?
.collect::<std::result::Result<_, _>>()?;
Ok(ret.pop().unwrap_or(0))
@ -199,7 +199,7 @@ mod sqlite3_m {
"SELECT uidvalidity, flags, highestmodseq FROM mailbox WHERE mailbox_hash = ?1;",
)?;
let mut ret = stmt.query_map(sqlite3::params![mailbox_hash as i64], |row| {
let mut ret = stmt.query_map(sqlite3::params![mailbox_hash.0 as i64], |row| {
Ok((
row.get(0).map(|u: Sqlite3UID| u as UID)?,
row.get(1)?,
@ -231,7 +231,7 @@ mod sqlite3_m {
.unwrap()
.entry(mailbox_hash)
.and_modify(|entry| *entry = highestmodseq.ok_or(()))
.or_insert_with(|| highestmodseq.ok_or(()));
.or_insert(highestmodseq.ok_or(()));
self.uid_store
.uidvalidity
.lock()
@ -265,7 +265,7 @@ mod sqlite3_m {
self.connection
.execute(
"DELETE FROM mailbox WHERE mailbox_hash = ?1",
sqlite3::params![mailbox_hash as i64],
sqlite3::params![mailbox_hash.0 as i64],
)
.chain_err_summary(|| {
format!(
@ -277,7 +277,7 @@ mod sqlite3_m {
if let Some(Ok(highestmodseq)) = select_response.highestmodseq {
self.connection.execute(
"INSERT OR IGNORE INTO mailbox (uidvalidity, flags, highestmodseq, mailbox_hash) VALUES (?1, ?2, ?3, ?4)",
sqlite3::params![select_response.uidvalidity as Sqlite3UID, select_response.flags.1.iter().map(|s| s.as_str()).collect::<Vec<&str>>().join("\0").as_bytes(), highestmodseq, mailbox_hash as i64],
sqlite3::params![select_response.uidvalidity as Sqlite3UID, select_response.flags.1.iter().map(|s| s.as_str()).collect::<Vec<&str>>().join("\0").as_bytes(), highestmodseq, mailbox_hash.0 as i64],
)
.chain_err_summary(|| {
format!(
@ -292,7 +292,7 @@ mod sqlite3_m {
sqlite3::params![
select_response.uidvalidity as Sqlite3UID,
select_response.flags.1.iter().map(|s| s.as_str()).collect::<Vec<&str>>().join("\0").as_bytes(),
mailbox_hash as i64
mailbox_hash.0 as i64
],
)
.chain_err_summary(|| {
@ -328,7 +328,7 @@ mod sqlite3_m {
.join("\0")
.as_bytes(),
highestmodseq,
mailbox_hash as i64
mailbox_hash.0 as i64
],
)
.chain_err_summary(|| {
@ -350,7 +350,7 @@ mod sqlite3_m {
.collect::<Vec<&str>>()
.join("\0")
.as_bytes(),
mailbox_hash as i64
mailbox_hash.0 as i64
],
)
.chain_err_summary(|| {
@ -374,7 +374,7 @@ mod sqlite3_m {
)?;
let ret: Vec<(UID, Envelope, Option<ModSequence>)> = stmt
.query_map(sqlite3::params![mailbox_hash as i64], |row| {
.query_map(sqlite3::params![mailbox_hash.0 as i64], |row| {
Ok((
row.get(0).map(|i: Sqlite3UID| i as UID)?,
row.get(1)?,
@ -452,7 +452,7 @@ mod sqlite3_m {
max_uid = std::cmp::max(max_uid, *uid);
tx.execute(
"INSERT OR REPLACE INTO envelopes (hash, uid, mailbox_hash, modsequence, envelope) VALUES (?1, ?2, ?3, ?4, ?5)",
sqlite3::params![envelope.hash() as i64, *uid as Sqlite3UID, mailbox_hash as i64, modseq, &envelope],
sqlite3::params![envelope.hash() as i64, *uid as Sqlite3UID, mailbox_hash.0 as i64, modseq, &envelope],
).chain_err_summary(|| format!("Could not insert envelope {} {} in header_cache of account {}", envelope.message_id(), envelope.hash(), uid_store.account_name))?;
}
}
@ -483,10 +483,10 @@ mod sqlite3_m {
for (uid, event) in refresh_events {
match &event.kind {
RefreshEventKind::Remove(env_hash) => {
hash_index_lck.remove(env_hash);
hash_index_lck.remove(&env_hash);
tx.execute(
"DELETE FROM envelopes WHERE mailbox_hash = ?1 AND uid = ?2;",
sqlite3::params![mailbox_hash as i64, *uid as Sqlite3UID],
sqlite3::params![mailbox_hash.0 as i64, *uid as Sqlite3UID],
)
.chain_err_summary(|| {
format!(
@ -502,8 +502,8 @@ mod sqlite3_m {
let mut ret: Vec<Envelope> = stmt
.query_map(
sqlite3::params![mailbox_hash as i64, *uid as Sqlite3UID],
|row| row.get(0),
sqlite3::params![mailbox_hash.0 as i64, *uid as Sqlite3UID],
|row| Ok(row.get(0)?),
)?
.collect::<std::result::Result<_, _>>()?;
if let Some(mut env) = ret.pop() {
@ -512,7 +512,7 @@ mod sqlite3_m {
env.labels_mut().extend(tags.iter().map(|t| tag_hash!(t)));
tx.execute(
"UPDATE envelopes SET envelope = ?1 WHERE mailbox_hash = ?2 AND uid = ?3;",
sqlite3::params![&env, mailbox_hash as i64, *uid as Sqlite3UID],
sqlite3::params![&env, mailbox_hash.0 as i64, *uid as Sqlite3UID],
)
.chain_err_summary(|| {
format!(
@ -556,7 +556,7 @@ mod sqlite3_m {
let x = stmt
.query_map(
sqlite3::params![mailbox_hash as i64, uid as Sqlite3UID],
sqlite3::params![mailbox_hash.0 as i64, uid as Sqlite3UID],
|row| {
Ok((
row.get(0).map(|u: Sqlite3UID| u as UID)?,
@ -575,7 +575,7 @@ mod sqlite3_m {
let x = stmt
.query_map(
sqlite3::params![mailbox_hash as i64, env_hash as i64],
sqlite3::params![mailbox_hash.0 as i64, env_hash as i64],
|row| {
Ok((
row.get(0).map(|u: Sqlite3UID| u as UID)?,
@ -592,12 +592,12 @@ mod sqlite3_m {
return Ok(None);
}
let (uid, inner, modsequence) = ret.pop().unwrap();
Ok(Some(CachedEnvelope {
return Ok(Some(CachedEnvelope {
inner,
uid,
mailbox_hash,
modsequence,
}))
}));
}
fn rfc822(
@ -612,8 +612,8 @@ mod sqlite3_m {
)?;
let x = stmt
.query_map(
sqlite3::params![mailbox_hash as i64, uid as Sqlite3UID],
|row| row.get(0),
sqlite3::params![mailbox_hash.0 as i64, uid as Sqlite3UID],
|row| Ok(row.get(0)?),
)?
.collect::<std::result::Result<_, _>>()?;
x
@ -624,8 +624,8 @@ mod sqlite3_m {
)?;
let x = stmt
.query_map(
sqlite3::params![mailbox_hash as i64, env_hash as i64],
|row| row.get(0),
sqlite3::params![mailbox_hash.0 as i64, env_hash as i64],
|row| Ok(row.get(0)?),
)?
.collect::<std::result::Result<_, _>>()?;
x
@ -655,19 +655,19 @@ pub(super) async fn fetch_cached_envs(state: &mut FetchState) -> Result<Option<V
{
let mut conn = connection.lock().await;
match conn.load_cache(mailbox_hash).await {
None => Ok(None),
None => return Ok(None),
Some(Ok(env_hashes)) => {
let env_lck = uid_store.envelopes.lock().unwrap();
Ok(Some(
return Ok(Some(
env_hashes
.into_iter()
.filter_map(|env_hash| {
env_lck.get(&env_hash).map(|c_env| c_env.inner.clone())
})
.collect::<Vec<Envelope>>(),
))
));
}
Some(Err(err)) => Err(err),
Some(Err(err)) => return Err(err),
}
}
}

View File

@ -44,7 +44,7 @@ use super::{Capabilities, ImapServerConf, UIDStore};
#[derive(Debug, Clone, Copy)]
pub enum SyncPolicy {
None,
///rfc4549 `Synch Ops for Disconnected IMAP4 Clients` <https://tools.ietf.org/html/rfc4549>
///rfc4549 `Synch Ops for Disconnected IMAP4 Clients` https://tools.ietf.org/html/rfc4549
Basic,
///rfc7162 `IMAP Extensions: Quick Flag Changes Resynchronization (CONDSTORE) and Quick Mailbox Resynchronization (QRESYNC)`
Condstore,
@ -115,38 +115,38 @@ pub struct ImapConnection {
impl ImapStream {
pub async fn new_connection(
server_conf: &ImapServerConf,
uid_store: &UIDStore,
) -> Result<(Capabilities, ImapStream)> {
use std::net::TcpStream;
let path = &server_conf.server_hostname;
let cmd_id = 1;
let stream = if server_conf.use_tls {
(uid_store.event_consumer)(
uid_store.account_hash,
crate::backends::BackendEvent::AccountStateChange {
message: "Establishing TLS connection.".into(),
},
);
let mut connector = TlsConnector::builder();
if server_conf.danger_accept_invalid_certs {
connector.danger_accept_invalid_certs(true);
}
let connector = connector
.build()
.chain_err_kind(crate::error::ErrorKind::Network(
crate::error::NetworkErrorKind::InvalidTLSConnection,
))?;
.chain_err_kind(crate::error::ErrorKind::Network)?;
let addr = lookup_ipv4(path, server_conf.server_port)?;
let addr = if let Ok(a) = lookup_ipv4(path, server_conf.server_port) {
a
} else {
return Err(MeliError::new(format!(
"Could not lookup address {}",
&path
)));
};
let mut socket = AsyncWrapper::new(Connection::Tcp(
if let Some(timeout) = server_conf.timeout {
TcpStream::connect_timeout(&addr, timeout)?
TcpStream::connect_timeout(&addr, timeout)
.chain_err_kind(crate::error::ErrorKind::Network)?
} else {
TcpStream::connect(&addr)?
TcpStream::connect(&addr).chain_err_kind(crate::error::ErrorKind::Network)?
},
))?;
))
.chain_err_kind(crate::error::ErrorKind::Network)?;
if server_conf.use_starttls {
let err_fn = || {
if server_conf.server_port == 993 {
@ -160,22 +160,36 @@ impl ImapStream {
ImapProtocol::IMAP { .. } => socket
.write_all(format!("M{} STARTTLS\r\n", cmd_id).as_bytes())
.await
.chain_err_summary(err_fn)?,
.chain_err_summary(err_fn)
.chain_err_kind(crate::error::ErrorKind::Network)?,
ImapProtocol::ManageSieve => {
socket.read(&mut buf).await.chain_err_summary(err_fn)?;
socket
.read(&mut buf)
.await
.chain_err_summary(err_fn)
.chain_err_kind(crate::error::ErrorKind::Network)?;
socket
.write_all(b"STARTTLS\r\n")
.await
.chain_err_summary(err_fn)?;
.chain_err_summary(err_fn)
.chain_err_kind(crate::error::ErrorKind::Network)?;
}
}
socket.flush().await.chain_err_summary(err_fn)?;
socket
.flush()
.await
.chain_err_summary(err_fn)
.chain_err_kind(crate::error::ErrorKind::Network)?;
let mut response = Vec::with_capacity(1024);
let mut broken = false;
let now = Instant::now();
while now.elapsed().as_secs() < 3 {
let len = socket.read(&mut buf).await.chain_err_summary(err_fn)?;
let len = socket
.read(&mut buf)
.await
.chain_err_summary(err_fn)
.chain_err_kind(crate::error::ErrorKind::Network)?;
response.extend_from_slice(&buf[0..len]);
match server_conf.protocol {
ImapProtocol::IMAP { .. } => {
@ -208,7 +222,9 @@ impl ImapStream {
{
// FIXME: This is blocking
let socket = socket.into_inner()?;
let socket = socket
.into_inner()
.chain_err_kind(crate::error::ErrorKind::Network)?;
let mut conn_result = connector.connect(path, socket);
if let Err(native_tls::HandshakeError::WouldBlock(midhandshake_stream)) =
conn_result
@ -224,17 +240,20 @@ impl ImapStream {
midhandshake_stream = Some(stream);
}
p => {
p.chain_err_kind(crate::error::ErrorKind::Network(
crate::error::NetworkErrorKind::InvalidTLSConnection,
))?;
p.chain_err_kind(crate::error::ErrorKind::Network)?;
}
}
}
}
AsyncWrapper::new(Connection::Tls(conn_result.chain_err_summary(|| {
format!("Could not initiate TLS negotiation to {}.", path)
})?))
.chain_err_summary(|| format!("Could not initiate TLS negotiation to {}.", path))?
AsyncWrapper::new(Connection::Tls(
conn_result
.chain_err_summary(|| {
format!("Could not initiate TLS negotiation to {}.", path)
})
.chain_err_kind(crate::error::ErrorKind::Network)?,
))
.chain_err_summary(|| format!("Could not initiate TLS negotiation to {}.", path))
.chain_err_kind(crate::error::ErrorKind::Network)?
}
} else {
let addr = if let Ok(a) = lookup_ipv4(path, server_conf.server_port) {
@ -247,11 +266,13 @@ impl ImapStream {
};
AsyncWrapper::new(Connection::Tcp(
if let Some(timeout) = server_conf.timeout {
TcpStream::connect_timeout(&addr, timeout)?
TcpStream::connect_timeout(&addr, timeout)
.chain_err_kind(crate::error::ErrorKind::Network)?
} else {
TcpStream::connect(&addr)?
TcpStream::connect(&addr).chain_err_kind(crate::error::ErrorKind::Network)?
},
))?
))
.chain_err_kind(crate::error::ErrorKind::Network)?
};
if let Err(err) = stream
.get_ref()
@ -291,12 +312,6 @@ impl ImapStream {
return Ok((Default::default(), ret));
}
(uid_store.event_consumer)(
uid_store.account_hash,
crate::backends::BackendEvent::AccountStateChange {
message: "Negotiating server capabilities.".into(),
},
);
ret.send_command(b"CAPABILITY").await?;
ret.read_response(&mut res).await?;
let capabilities: std::result::Result<Vec<&[u8]>, _> = res
@ -338,12 +353,6 @@ impl ImapStream {
.set_err_kind(crate::error::ErrorKind::Authentication));
}
(uid_store.event_consumer)(
uid_store.account_hash,
crate::backends::BackendEvent::AccountStateChange {
message: "Attempting authentication.".into(),
},
);
match server_conf.protocol {
ImapProtocol::IMAP {
extension_use: ImapExtensionUse { oauth2, .. },
@ -370,7 +379,7 @@ impl ImapStream {
r#"LOGIN "{}" {{{}}}"#,
&server_conf
.server_username
.replace('\\', r#"\\"#)
.replace(r#"\"#, r#"\\"#)
.replace('"', r#"\""#)
.replace('{', r#"\{"#)
.replace('}', r#"\}"#),
@ -479,8 +488,8 @@ impl ImapStream {
last_line_idx += pos + b"\r\n".len();
}
}
Err(err) => {
return Err(MeliError::from(err));
Err(e) => {
return Err(MeliError::from(e).set_err_kind(crate::error::ErrorKind::Network));
}
}
}
@ -496,7 +505,7 @@ impl ImapStream {
}
pub async fn send_command(&mut self, command: &[u8]) -> Result<()> {
_ = timeout(
if let Err(err) = timeout(
self.timeout,
try_await(async move {
let command = command.trim();
@ -530,22 +539,42 @@ impl ImapStream {
Ok(())
}),
)
.await?;
Ok(())
.await
{
Err(err.set_err_kind(crate::error::ErrorKind::Network))
} else {
Ok(())
}
}
pub async fn send_literal(&mut self, data: &[u8]) -> Result<()> {
self.stream.write_all(data).await?;
self.stream.write_all(b"\r\n").await?;
self.stream.flush().await?;
Ok(())
if let Err(err) = try_await(async move {
self.stream.write_all(data).await?;
self.stream.write_all(b"\r\n").await?;
self.stream.flush().await?;
Ok(())
})
.await
{
Err(err.set_err_kind(crate::error::ErrorKind::Network))
} else {
Ok(())
}
}
pub async fn send_raw(&mut self, raw: &[u8]) -> Result<()> {
self.stream.write_all(raw).await?;
self.stream.write_all(b"\r\n").await?;
self.stream.flush().await?;
Ok(())
if let Err(err) = try_await(async move {
self.stream.write_all(raw).await?;
self.stream.write_all(b"\r\n").await?;
self.stream.flush().await?;
Ok(())
})
.await
{
Err(err.set_err_kind(crate::error::ErrorKind::Network))
} else {
Ok(())
}
}
}
@ -572,11 +601,7 @@ impl ImapConnection {
if SystemTime::now().duration_since(time).unwrap_or_default()
>= IMAP_PROTOCOL_TIMEOUT
{
let err = MeliError::new(format!(
"Connection timed out after {} seconds",
IMAP_PROTOCOL_TIMEOUT.as_secs()
))
.set_kind(ErrorKind::Timeout);
let err = MeliError::new("Connection timed out").set_kind(ErrorKind::Timeout);
*status = Err(err.clone());
self.stream = Err(err);
}
@ -599,7 +624,7 @@ impl ImapConnection {
return Ok(());
}
}
let new_stream = ImapStream::new_connection(&self.server_conf, &self.uid_store).await;
let new_stream = ImapStream::new_connection(&self.server_conf).await;
if let Err(err) = new_stream.as_ref() {
self.uid_store.is_online.lock().unwrap().1 = Err(err.clone());
} else {
@ -1104,7 +1129,7 @@ async fn read(
*prev_failure = None;
}
Err(_err) => {
*err = Some(Into::<MeliError>::into(_err));
*err = Some(Into::<MeliError>::into(_err).set_kind(crate::error::ErrorKind::Network));
*break_flag = true;
*prev_failure = Some(SystemTime::now());
}

View File

@ -33,6 +33,7 @@ pub struct ImapMailbox {
pub path: String,
pub name: String,
pub parent: Option<MailboxHash>,
pub has_children: bool,
pub children: Vec<MailboxHash>,
pub separator: u8,
pub usage: Arc<RwLock<SpecialUsageMailbox>>,
@ -47,6 +48,12 @@ pub struct ImapMailbox {
}
impl ImapMailbox {
pub fn set_has_children(&mut self, has_children: bool) -> &mut Self {
self.has_children = has_children;
self
}
pub fn imap_path(&self) -> &str {
&self.imap_path
}
@ -88,6 +95,9 @@ impl BackendMailbox for ImapMailbox {
}
fn children(&self) -> &[MailboxHash] {
if !self.has_children {
return &[];
}
&self.children
}
@ -106,9 +116,11 @@ impl BackendMailbox for ImapMailbox {
fn permissions(&self) -> MailboxPermissions {
*self.permissions.lock().unwrap()
}
fn is_subscribed(&self) -> bool {
self.is_subscribed
}
fn set_is_subscribed(&mut self, new_val: bool) -> Result<()> {
self.is_subscribed = new_val;
Ok(())

View File

@ -21,22 +21,16 @@
use super::{ImapConnection, ImapProtocol, ImapServerConf, UIDStore};
use crate::conf::AccountSettings;
use crate::email::parser::IResult;
use crate::error::{MeliError, Result};
use crate::get_conf_val;
use crate::imap::RequiredResponses;
use nom::{
branch::alt, bytes::complete::tag, combinator::map, multi::separated_list1,
sequence::separated_pair,
branch::alt, bytes::complete::tag, combinator::map, error::Error as NomError, error::ErrorKind,
multi::separated_list1, sequence::separated_pair, IResult,
};
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
pub struct ManageSieveConnection {
pub inner: ImapConnection,
}
pub fn managesieve_capabilities(input: &[u8]) -> Result<Vec<(&[u8], &[u8])>> {
let (_, ret) = separated_list1(
tag(b"\r\n"),
@ -48,225 +42,26 @@ pub fn managesieve_capabilities(input: &[u8]) -> Result<Vec<(&[u8], &[u8])>> {
Ok(ret)
}
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum ManageSieveResponse<'a> {
Ok {
code: Option<&'a [u8]>,
message: Option<&'a [u8]>,
},
NoBye {
code: Option<&'a [u8]>,
message: Option<&'a [u8]>,
},
}
#[test]
fn test_managesieve_capabilities() {
assert_eq!(managesieve_capabilities(b"\"IMPLEMENTATION\" \"Dovecot Pigeonhole\"\r\n\"SIEVE\" \"fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext\"\r\n\"NOTIFY\" \"mailto\"\r\n\"SASL\" \"PLAIN\"\r\n\"STARTTLS\"\r\n\"VERSION\" \"1.0\"\r\n").unwrap(), vec![
(&b"IMPLEMENTATION"[..],&b"Dovecot Pigeonhole"[..]),
(&b"SIEVE"[..],&b"fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext"[..]),
(&b"NOTIFY"[..],&b"mailto"[..]),
(&b"SASL"[..],&b"PLAIN"[..]),
(&b"STARTTLS"[..], &b""[..]),
(&b"VERSION"[..],&b"1.0"[..])]
mod parser {
use super::*;
use nom::bytes::complete::tag;
pub use nom::bytes::complete::{is_not, tag_no_case};
use nom::character::complete::crlf;
use nom::combinator::{iterator, map, opt};
pub use nom::sequence::{delimited, pair, preceded, terminated};
pub fn sieve_name(input: &[u8]) -> IResult<&[u8], &[u8]> {
crate::backends::imap::protocol_parser::string_token(input)
}
// *(sieve-name [SP "ACTIVE"] CRLF)
// response-oknobye
pub fn listscripts(input: &[u8]) -> IResult<&[u8], Vec<(&[u8], bool)>> {
let mut it = iterator(
input,
alt((
terminated(
map(terminated(sieve_name, tag_no_case(b" ACTIVE")), |r| {
(r, true)
}),
crlf,
),
terminated(map(sieve_name, |r| (r, false)), crlf),
)),
);
let parsed = (&mut it).collect::<Vec<(&[u8], bool)>>();
let res: IResult<_, _> = it.finish();
let (rest, _) = res?;
Ok((rest, parsed))
}
// response-getscript = (sieve-script CRLF response-ok) /
// response-nobye
pub fn getscript(input: &[u8]) -> IResult<&[u8], &[u8]> {
sieve_name(input)
}
pub fn response_oknobye(input: &[u8]) -> IResult<&[u8], ManageSieveResponse> {
alt((
map(
terminated(
pair(
preceded(
tag_no_case(b"ok"),
opt(preceded(
tag(b" "),
delimited(tag(b"("), is_not(")"), tag(b")")),
)),
),
opt(preceded(tag(b" "), sieve_name)),
),
crlf,
),
|(code, message)| ManageSieveResponse::Ok { code, message },
),
map(
terminated(
pair(
preceded(
alt((tag_no_case(b"no"), tag_no_case(b"bye"))),
opt(preceded(
tag(b" "),
delimited(tag(b"("), is_not(")"), tag(b")")),
)),
),
opt(preceded(tag(b" "), sieve_name)),
),
crlf,
),
|(code, message)| ManageSieveResponse::NoBye { code, message },
),
))(input)
}
#[test]
fn test_managesieve_listscripts() {
let input_1 = b"\"summer_script\"\r\n\"vacation_script\"\r\n{13}\r\nclever\"script\r\n\"main_script\" ACTIVE\r\nOK";
assert_eq!(
terminated(listscripts, tag_no_case(b"OK"))(input_1),
Ok((
&b""[..],
vec![
(&b"summer_script"[..], false),
(&b"vacation_script"[..], false),
(&b"clever\"script"[..], false),
(&b"main_script"[..], true)
]
))
);
let input_2 = b"\"summer_script\"\r\n\"main_script\" active\r\nok";
assert_eq!(
terminated(listscripts, tag_no_case(b"OK"))(input_2),
Ok((
&b""[..],
vec![(&b"summer_script"[..], false), (&b"main_script"[..], true)]
))
);
let input_3 = b"ok";
assert_eq!(
terminated(listscripts, tag_no_case(b"OK"))(input_3),
Ok((&b""[..], vec![]))
);
}
#[test]
fn test_managesieve_general() {
assert_eq!(managesieve_capabilities(b"\"IMPLEMENTATION\" \"Dovecot Pigeonhole\"\r\n\"SIEVE\" \"fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext\"\r\n\"NOTIFY\" \"mailto\"\r\n\"SASL\" \"PLAIN\"\r\n\"STARTTLS\"\r\n\"VERSION\" \"1.0\"\r\n").unwrap(), vec![
(&b"IMPLEMENTATION"[..],&b"Dovecot Pigeonhole"[..]),
(&b"SIEVE"[..],&b"fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext"[..]),
(&b"NOTIFY"[..],&b"mailto"[..]),
(&b"SASL"[..],&b"PLAIN"[..]),
(&b"STARTTLS"[..], &b""[..]),
(&b"VERSION"[..],&b"1.0"[..])]
);
let response_ok = b"OK (WARNINGS) \"line 8: server redirect action limit is 2, this redirect might be ignored\"\r\n";
assert_eq!(
response_oknobye(response_ok),
Ok((
&b""[..],
ManageSieveResponse::Ok {
code: Some(&b"WARNINGS"[..]),
message: Some(&b"line 8: server redirect action limit is 2, this redirect might be ignored"[..]),
}
))
);
let response_ok = b"OK (WARNINGS)\r\n";
assert_eq!(
response_oknobye(response_ok),
Ok((
&b""[..],
ManageSieveResponse::Ok {
code: Some(&b"WARNINGS"[..]),
message: None,
}
))
);
let response_ok =
b"OK \"line 8: server redirect action limit is 2, this redirect might be ignored\"\r\n";
assert_eq!(
response_oknobye(response_ok),
Ok((
&b""[..],
ManageSieveResponse::Ok {
code: None,
message: Some(&b"line 8: server redirect action limit is 2, this redirect might be ignored"[..]),
}
))
);
let response_ok = b"Ok\r\n";
assert_eq!(
response_oknobye(response_ok),
Ok((
&b""[..],
ManageSieveResponse::Ok {
code: None,
message: None,
}
))
);
let response_nobye = b"No (NONEXISTENT) \"There is no script by that name\"\r\n";
assert_eq!(
response_oknobye(response_nobye),
Ok((
&b""[..],
ManageSieveResponse::NoBye {
code: Some(&b"NONEXISTENT"[..]),
message: Some(&b"There is no script by that name"[..]),
}
))
);
let response_nobye = b"No (NONEXISTENT) {31}\r\nThere is no script by that name\r\n";
assert_eq!(
response_oknobye(response_nobye),
Ok((
&b""[..],
ManageSieveResponse::NoBye {
code: Some(&b"NONEXISTENT"[..]),
message: Some(&b"There is no script by that name"[..]),
}
))
);
let response_nobye = b"No\r\n";
assert_eq!(
response_oknobye(response_nobye),
Ok((
&b""[..],
ManageSieveResponse::NoBye {
code: None,
message: None,
}
))
);
}
);
}
// Return a byte sequence surrounded by "s and decoded if necessary
pub fn quoted_raw(input: &[u8]) -> IResult<&[u8], &[u8]> {
if input.is_empty() || input[0] != b'"' {
return Err(nom::Err::Error((input, "empty").into()));
return Err(nom::Err::Error(NomError {
input,
code: ErrorKind::Tag,
}));
}
let mut i = 1;
@ -277,199 +72,91 @@ pub fn quoted_raw(input: &[u8]) -> IResult<&[u8], &[u8]> {
i += 1;
}
Err(nom::Err::Error((input, "no quotes").into()))
Err(nom::Err::Error(NomError {
input,
code: ErrorKind::Tag,
}))
}
impl ManageSieveConnection {
pub fn new(
account_hash: crate::backends::AccountHash,
account_name: String,
s: &AccountSettings,
event_consumer: crate::backends::BackendEventConsumer,
) -> Result<Self> {
let server_hostname = get_conf_val!(s["server_hostname"])?;
let server_username = get_conf_val!(s["server_username"])?;
let server_password = get_conf_val!(s["server_password"])?;
let server_port = get_conf_val!(s["server_port"], 4190)?;
let danger_accept_invalid_certs: bool =
get_conf_val!(s["danger_accept_invalid_certs"], false)?;
let timeout = get_conf_val!(s["timeout"], 16_u64)?;
let timeout = if timeout == 0 {
None
} else {
Some(std::time::Duration::from_secs(timeout))
};
let server_conf = ImapServerConf {
server_hostname: server_hostname.to_string(),
server_username: server_username.to_string(),
server_password: server_password.to_string(),
server_port,
use_starttls: true,
use_tls: true,
danger_accept_invalid_certs,
protocol: ImapProtocol::ManageSieve,
timeout,
};
let uid_store = Arc::new(UIDStore {
is_online: Arc::new(Mutex::new((
SystemTime::now(),
Err(MeliError::new("Account is uninitialised.")),
))),
..UIDStore::new(
account_hash,
Arc::new(account_name),
event_consumer,
server_conf.timeout,
)
});
Ok(Self {
inner: ImapConnection::new_connection(&server_conf, uid_store),
})
}
pub trait ManageSieve {
fn havespace(&mut self) -> Result<()>;
fn putscript(&mut self) -> Result<()>;
pub async fn havespace(&mut self) -> Result<()> {
fn listscripts(&mut self) -> Result<()>;
fn setactive(&mut self) -> Result<()>;
fn getscript(&mut self) -> Result<()>;
fn deletescript(&mut self) -> Result<()>;
fn renamescript(&mut self) -> Result<()>;
}
pub fn new_managesieve_connection(
account_hash: crate::backends::AccountHash,
account_name: String,
s: &AccountSettings,
event_consumer: crate::backends::BackendEventConsumer,
) -> Result<ImapConnection> {
let server_hostname = get_conf_val!(s["server_hostname"])?;
let server_username = get_conf_val!(s["server_username"])?;
let server_password = get_conf_val!(s["server_password"])?;
let server_port = get_conf_val!(s["server_port"], 4190)?;
let danger_accept_invalid_certs: bool = get_conf_val!(s["danger_accept_invalid_certs"], false)?;
let timeout = get_conf_val!(s["timeout"], 16_u64)?;
let timeout = if timeout == 0 {
None
} else {
Some(std::time::Duration::from_secs(timeout))
};
let server_conf = ImapServerConf {
server_hostname: server_hostname.to_string(),
server_username: server_username.to_string(),
server_password: server_password.to_string(),
server_port,
use_starttls: true,
use_tls: true,
danger_accept_invalid_certs,
protocol: ImapProtocol::ManageSieve,
timeout,
};
let uid_store = Arc::new(UIDStore {
is_online: Arc::new(Mutex::new((
SystemTime::now(),
Err(MeliError::new("Account is uninitialised.")),
))),
..UIDStore::new(
account_hash,
Arc::new(account_name),
event_consumer,
server_conf.timeout,
)
});
Ok(ImapConnection::new_connection(&server_conf, uid_store))
}
impl ManageSieve for ImapConnection {
fn havespace(&mut self) -> Result<()> {
Ok(())
}
fn putscript(&mut self) -> Result<()> {
Ok(())
}
pub async fn putscript(&mut self, script_name: &[u8], script: &[u8]) -> Result<()> {
let mut ret = Vec::new();
self.inner
.send_literal(format!("Putscript {{{len}+}}\r\n", len = script_name.len()).as_bytes())
.await?;
self.inner.send_literal(script_name).await?;
self.inner
.send_literal(format!(" {{{len}+}}\r\n", len = script.len()).as_bytes())
.await?;
self.inner.send_literal(script).await?;
self.inner
.read_response(&mut ret, RequiredResponses::empty())
.await?;
let (_rest, response) = parser::response_oknobye(&ret)?;
match response {
ManageSieveResponse::Ok { .. } => Ok(()),
ManageSieveResponse::NoBye { code, message } => Err(format!(
"Could not upload script: {} {}",
code.map(|b| String::from_utf8_lossy(b)).unwrap_or_default(),
message
.map(|b| String::from_utf8_lossy(b))
.unwrap_or_default()
)
.into()),
}
fn listscripts(&mut self) -> Result<()> {
Ok(())
}
fn setactive(&mut self) -> Result<()> {
Ok(())
}
pub async fn listscripts(&mut self) -> Result<Vec<(Vec<u8>, bool)>> {
let mut ret = Vec::new();
self.inner.send_command(b"Listscripts").await?;
self.inner
.read_response(&mut ret, RequiredResponses::empty())
.await?;
let (_rest, scripts) =
parser::terminated(parser::listscripts, parser::tag_no_case(b"OK"))(&ret)?;
Ok(scripts
.into_iter()
.map(|(n, a)| (n.to_vec(), a))
.collect::<Vec<(Vec<u8>, bool)>>())
fn getscript(&mut self) -> Result<()> {
Ok(())
}
pub async fn checkscript(&mut self, script: &[u8]) -> Result<()> {
let mut ret = Vec::new();
self.inner
.send_literal(format!("Checkscript {{{len}+}}\r\n", len = script.len()).as_bytes())
.await?;
self.inner.send_literal(script).await?;
self.inner
.read_response(&mut ret, RequiredResponses::empty())
.await?;
let (_rest, response) = parser::response_oknobye(&ret)?;
match response {
ManageSieveResponse::Ok { .. } => Ok(()),
ManageSieveResponse::NoBye { code, message } => Err(format!(
"Checkscript reply: {} {}",
code.map(|b| String::from_utf8_lossy(b)).unwrap_or_default(),
message
.map(|b| String::from_utf8_lossy(b))
.unwrap_or_default()
)
.into()),
}
fn deletescript(&mut self) -> Result<()> {
Ok(())
}
pub async fn setactive(&mut self, script_name: &[u8]) -> Result<()> {
let mut ret = Vec::new();
self.inner
.send_literal(format!("Setactive {{{len}+}}\r\n", len = script_name.len()).as_bytes())
.await?;
self.inner.send_literal(script_name).await?;
self.inner
.read_response(&mut ret, RequiredResponses::empty())
.await?;
let (_rest, response) = parser::response_oknobye(&ret)?;
match response {
ManageSieveResponse::Ok { .. } => Ok(()),
ManageSieveResponse::NoBye { code, message } => Err(format!(
"Could not set active script: {} {}",
code.map(|b| String::from_utf8_lossy(b)).unwrap_or_default(),
message
.map(|b| String::from_utf8_lossy(b))
.unwrap_or_default()
)
.into()),
}
}
pub async fn getscript(&mut self, script_name: &[u8]) -> Result<Vec<u8>> {
let mut ret = Vec::new();
self.inner
.send_literal(format!("Getscript {{{len}+}}\r\n", len = script_name.len()).as_bytes())
.await?;
self.inner.send_literal(script_name).await?;
self.inner
.read_response(&mut ret, RequiredResponses::empty())
.await?;
if let Ok((_, ManageSieveResponse::NoBye { code, message })) =
parser::response_oknobye(&ret)
{
return Err(format!(
"Could not set active script: {} {}",
code.map(|b| String::from_utf8_lossy(b)).unwrap_or_default(),
message
.map(|b| String::from_utf8_lossy(b))
.unwrap_or_default()
)
.into());
}
let (_rest, script) =
parser::terminated(parser::getscript, parser::tag_no_case(b"OK"))(&ret)?;
Ok(script.to_vec())
}
pub async fn deletescript(&mut self, script_name: &[u8]) -> Result<()> {
let mut ret = Vec::new();
self.inner
.send_literal(
format!("Deletescript {{{len}+}}\r\n", len = script_name.len()).as_bytes(),
)
.await?;
self.inner.send_literal(script_name).await?;
self.inner
.read_response(&mut ret, RequiredResponses::empty())
.await?;
let (_rest, response) = parser::response_oknobye(&ret)?;
match response {
ManageSieveResponse::Ok { .. } => Ok(()),
ManageSieveResponse::NoBye { code, message } => Err(format!(
"Could not delete script: {} {}",
code.map(|b| String::from_utf8_lossy(b)).unwrap_or_default(),
message
.map(|b| String::from_utf8_lossy(b))
.unwrap_or_default()
)
.into()),
}
}
pub async fn renamescript(&mut self) -> Result<()> {
fn renamescript(&mut self) -> Result<()> {
Ok(())
}
}

View File

@ -150,9 +150,8 @@ fn test_imap_required_responses() {
assert_eq!(v.len(), 1);
}
#[derive(Debug, PartialEq)]
#[derive(Debug)]
pub struct Alert(String);
pub type ImapParseResult<'a, T> = Result<(&'a [u8], T, Option<Alert>)>;
pub struct ImapLineIterator<'a> {
slice: &'a [u8],
@ -258,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);
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 {:?}",
@ -450,6 +449,7 @@ pub fn list_mailbox_result(input: &[u8]) -> IResult<&[u8], ImapMailbox> {
({
let separator: u8 = separator[0];
let mut f = ImapMailbox::default();
f.has_children = true;
f.no_select = false;
f.is_subscribed = false;
@ -476,10 +476,14 @@ pub fn list_mailbox_result(input: &[u8]) -> IResult<&[u8], ImapMailbox> {
let _ = f.set_special_usage(SpecialUsageMailbox::Flagged);
} else if p.eq_ignore_ascii_case(b"\\Archive") {
let _ = f.set_special_usage(SpecialUsageMailbox::Archive);
} else if p.eq_ignore_ascii_case(b"\\HasChildren") {
f.has_children = true;
} else if p.eq_ignore_ascii_case(b"\\HasNoChildren") {
f.has_children = false;
}
}
f.imap_path = path.to_string();
f.hash = get_path_hash!(&f.imap_path);
f.hash = MailboxHash(get_path_hash!(&f.imap_path));
f.path = if separator == b'/' {
f.imap_path.clone()
} else {
@ -487,7 +491,7 @@ pub fn list_mailbox_result(input: &[u8]) -> IResult<&[u8], ImapMailbox> {
};
f.name = if let Some(pos) = f.imap_path.as_bytes().iter().rposition(|&c| c == separator)
{
f.parent = Some(get_path_hash!(&f.imap_path[..pos]));
f.parent = Some(MailboxHash(get_path_hash!(&f.imap_path[..pos])));
f.imap_path[pos + 1..].to_string()
} else {
f.imap_path.clone()
@ -605,8 +609,8 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
i += (input.len() - i - rest.len()) + 1;
} else {
return debug!(Err(MeliError::new(format!(
"Unexpected input while parsing UID FETCH response. Could not parse FLAGS: {:.40}.",
String::from_utf8_lossy(&input[i..])
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
String::from_utf8_lossy(input)
))));
}
} else if input[i..].starts_with(b"MODSEQ (") {
@ -640,8 +644,8 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
i += input.len() - i - rest.len();
} else {
return debug!(Err(MeliError::new(format!(
"Unexpected input while parsing UID FETCH response. Could not parse RFC822: {:.40}",
String::from_utf8_lossy(&input[i..])
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
String::from_utf8_lossy(input)
))));
}
} else if input[i..].starts_with(b"ENVELOPE (") {
@ -651,7 +655,7 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
i += input.len() - i - rest.len();
} else {
return debug!(Err(MeliError::new(format!(
"Unexpected input while parsing UID FETCH response. Could not parse ENVELOPE: {:.40}",
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
String::from_utf8_lossy(&input[i..])
))));
}
@ -665,7 +669,7 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
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) {
if let Ok((_, (_, v))) = crate::email::parser::headers::header(&references) {
references = v;
}
ret.references = Some(references);
@ -673,7 +677,7 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
i += input.len() - i - rest.len();
} else {
return debug!(Err(MeliError::new(format!(
"Unexpected input while parsing UID FETCH response. Could not parse BODY[HEADER.FIELDS (REFERENCES)]: {:.40}",
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
String::from_utf8_lossy(&input[i..])
))));
}
@ -681,7 +685,7 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
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) {
if let Ok((_, (_, v))) = crate::email::parser::headers::header(&references) {
references = v;
}
ret.references = Some(references);
@ -689,7 +693,7 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
i += input.len() - i - rest.len();
} else {
return debug!(Err(MeliError::new(format!(
"Unexpected input while parsing UID FETCH response. Could not parse BODY[HEADER.FIELDS (\"REFERENCES\"): {:.40}",
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
String::from_utf8_lossy(&input[i..])
))));
}
@ -737,9 +741,9 @@ pub fn fetch_responses(mut input: &[u8]) -> ImapParseResult<Vec<FetchResponse<'_
}
Err(err) => {
return Err(MeliError::new(format!(
"Unexpected input while parsing UID FETCH responses: {} `{:.40}`",
err,
String::from_utf8_lossy(input),
"Unexpected input while parsing UID FETCH responses: `{:.40}`, {}",
String::from_utf8_lossy(&input),
err
)));
}
}
@ -750,7 +754,7 @@ pub fn fetch_responses(mut input: &[u8]) -> ImapParseResult<Vec<FetchResponse<'_
} else {
return Err(MeliError::new(format!(
"310Unexpected input while parsing UID FETCH responses: `{:.40}`",
String::from_utf8_lossy(input)
String::from_utf8_lossy(&input)
)));
}
}
@ -924,7 +928,7 @@ pub fn untagged_responses(input: &[u8]) -> ImapParseResult<Option<UntaggedRespon
_ => {
debug!(
"unknown untagged_response: {}",
String::from_utf8_lossy(_tag)
String::from_utf8_lossy(&_tag)
);
None
}
@ -935,7 +939,7 @@ pub fn untagged_responses(input: &[u8]) -> ImapParseResult<Option<UntaggedRespon
}
#[test]
fn test_imap_untagged_responses() {
fn test_untagged_responses() {
use UntaggedResponse::*;
assert_eq!(
untagged_responses(b"* 2 EXISTS\r\n")
@ -978,37 +982,6 @@ fn test_imap_untagged_responses() {
);
}
#[test]
fn test_imap_fetch_response() {
let input: &[u8] = b"* 198 FETCH (UID 7608 FLAGS (\\Seen) ENVELOPE (\"Fri, 24 Jun 2011 10:09:10 +0000\" \"xxxx/xxxx\" ((\"xx@xx.com\" NIL \"xx\" \"xx.com\")) NIL NIL ((\"xx@xx\" NIL \"xx\" \"xx.com\")) ((\"'xx, xx'\" NIL \"xx.xx\" \"xx.com\") (\"xx.xx@xx.com\" NIL \"xx.xx\" \"xx.com\") (\"'xx'\" NIL \"xx.xx\" \"xx.com\") (\"'xx xx'\" NIL \"xx.xx\" \"xx.com\") (\"xx.xx@xx.com\" NIL \"xx.xx\" \"xx.com\")) NIL NIL \"<xx@xx.com>\") BODY[HEADER.FIELDS (REFERENCES)] {2}\r\n\r\nBODYSTRUCTURE ((\"text\" \"html\" (\"charset\" \"us-ascii\") \"<xx@xx>\" NIL \"7BIT\" 17236 232 NIL NIL NIL NIL)(\"image\" \"jpeg\" (\"name\" \"image001.jpg\") \"<image001.jpg@xx.xx>\" \"image001.jpg\" \"base64\" 1918 NIL (\"inline\" (\"filename\" \"image001.jpg\" \"size\" \"1650\" \"creation-date\" \"Sun, 09 Aug 2015 20:56:04 GMT\" \"modification-date\" \"Sun, 14 Aug 2022 22:11:45 GMT\")) NIL NIL) \"related\" (\"boundary\" \"xx--xx\" \"type\" \"text/html\") NIL \"en-US\"))\r\n";
let mut address = SmallVec::new();
address.push(Address::new(None, "xx@xx.com".to_string()));
let mut env = Envelope::new(0);
env.set_subject("xxxx/xxxx".as_bytes().to_vec());
env.set_date("Fri, 24 Jun 2011 10:09:10 +0000".as_bytes());
env.set_from(address.clone());
env.set_to(address);
env.set_message_id("<xx@xx.com>".as_bytes());
assert_eq!(
fetch_response(input).unwrap(),
(
&b""[..],
FetchResponse {
uid: Some(7608),
message_sequence_number: 198,
flags: Some((Flag::SEEN, vec![])),
modseq: None,
body: None,
references: None,
envelope: Some(env),
raw_fetch_value: input,
},
None
)
);
}
pub fn search_results<'a>(input: &'a [u8]) -> IResult<&'a [u8], Vec<ImapNum>> {
alt((
|input: &'a [u8]| -> IResult<&'a [u8], Vec<ImapNum>> {
@ -1159,7 +1132,7 @@ pub fn select_response(input: &[u8]) -> Result<SelectResponse> {
}
#[test]
fn test_imap_select_response() {
fn test_select_response() {
let r = b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\*)] Flags permitted.\r\n* 45 EXISTS\r\n* 0 RECENT\r\n* OK [UNSEEN 16] First unseen.\r\n* OK [UIDVALIDITY 1554422056] UIDs valid\r\n* OK [UIDNEXT 50] Predicted next UID\r\n";
assert_eq!(
@ -1379,12 +1352,6 @@ pub fn envelope(input: &[u8]) -> IResult<&[u8], Envelope> {
))
}
#[test]
fn test_imap_envelope() {
let input: &[u8] = b"(\"Fri, 24 Jun 2011 10:09:10 +0000\" \"xxxx/xxxx\" ((\"xx@xx.com\" NIL \"xx\" \"xx.com\")) NIL NIL ((\"xx@xx\" NIL \"xx\" \"xx.com\")) ((\"'xx, xx'\" NIL \"xx.xx\" \"xx.com\") (\"xx.xx@xx.com\" NIL \"xx.xx\" \"xx.com\") (\"'xx'\" NIL \"xx.xx\" \"xx.com\") (\"'xx xx'\" NIL \"xx.xx\" \"xx.com\") (\"xx.xx@xx.com\" NIL \"xx.xx\" \"xx.com\")) NIL NIL \"<xx@xx.com>\")";
_ = envelope(input).unwrap();
}
/* Helper to build StrBuilder for Address structs */
macro_rules! str_builder {
($offset:expr, $length:expr) => {
@ -1404,7 +1371,7 @@ pub fn envelope_addresses<'a>(
|input: &'a [u8]| -> IResult<&'a [u8], Option<SmallVec<[Address; 1]>>> {
let (input, _) = tag("(")(input)?;
let (input, envelopes) = fold_many1(
delimited(tag("("), envelope_address, alt((tag(") "), tag(")")))),
delimited(tag("("), envelope_address, tag(")")),
SmallVec::new,
|mut acc, item| {
acc.push(item);
@ -1599,7 +1566,9 @@ pub struct StatusResponse {
pub fn status_response(input: &[u8]) -> IResult<&[u8], StatusResponse> {
let (input, _) = tag("* STATUS ")(input)?;
let (input, mailbox) = take_until(" (")(input)?;
let mailbox = mailbox_token(mailbox).map(|(_, m)| get_path_hash!(m)).ok();
let mailbox = mailbox_token(mailbox)
.map(|(_, m)| MailboxHash(get_path_hash!(m)))
.ok();
let (input, _) = tag(" (")(input)?;
let (input, result) = permutation((
opt(preceded(
@ -1655,7 +1624,7 @@ pub fn status_response(input: &[u8]) -> IResult<&[u8], StatusResponse> {
// ; is considered to be INBOX and not an astring.
// ; Refer to section 5.1 for further
// ; semantic details of mailbox names.
pub fn mailbox_token(input: &'_ [u8]) -> IResult<&'_ [u8], std::borrow::Cow<'_, str>> {
pub fn mailbox_token<'i>(input: &'i [u8]) -> IResult<&'i [u8], std::borrow::Cow<'i, str>> {
let (input, astring) = astring_token(input)?;
if astring.eq_ignore_ascii_case(b"INBOX") {
return Ok((input, "INBOX".into()));
@ -1664,12 +1633,12 @@ pub fn mailbox_token(input: &'_ [u8]) -> IResult<&'_ [u8], std::borrow::Cow<'_,
}
// astring = 1*ASTRING-CHAR / string
pub fn astring_token(input: &[u8]) -> IResult<&[u8], &[u8]> {
fn astring_token(input: &[u8]) -> IResult<&[u8], &[u8]> {
alt((string_token, astring_char))(input)
}
// string = quoted / literal
pub fn string_token(input: &[u8]) -> IResult<&[u8], &[u8]> {
fn string_token(input: &[u8]) -> IResult<&[u8], &[u8]> {
if let Ok((r, o)) = literal(input) {
return Ok((r, o));
}

View File

@ -222,7 +222,7 @@ impl ImapConnection {
}
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);
}
@ -317,10 +317,9 @@ impl ImapConnection {
let command = {
let mut iter = v.split(u8::is_ascii_whitespace);
let first = iter.next().unwrap_or(v);
let mut accum = to_str!(first).trim().to_string();
let mut accum = format!("{}", to_str!(first).trim());
for ms in iter {
accum.push(',');
accum.push_str(to_str!(ms).trim());
accum = format!("{},{}", accum, to_str!(ms).trim());
}
format!("UID FETCH {} (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)", accum)
};
@ -353,7 +352,7 @@ impl ImapConnection {
}
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);
}

View File

@ -100,9 +100,10 @@ pub struct EnvelopeCache {
#[derive(Debug, Clone)]
pub struct JmapServerConf {
pub server_url: String,
pub server_hostname: String,
pub server_username: String,
pub server_password: String,
pub server_port: u16,
pub danger_accept_invalid_certs: bool,
pub timeout: Option<Duration>,
}
@ -138,9 +139,10 @@ macro_rules! get_conf_val {
impl JmapServerConf {
pub fn new(s: &AccountSettings) -> Result<Self> {
Ok(JmapServerConf {
server_url: get_conf_val!(s["server_url"])?.to_string(),
server_hostname: get_conf_val!(s["server_hostname"])?.to_string(),
server_username: get_conf_val!(s["server_username"])?.to_string(),
server_password: get_conf_val!(s["server_password"])?.to_string(),
server_port: get_conf_val!(s["server_port"], 443)?,
danger_accept_invalid_certs: get_conf_val!(s["danger_accept_invalid_certs"], false)?,
timeout: get_conf_val!(s["timeout"], 16_u64).map(|t| {
if t == 0 {
@ -335,9 +337,6 @@ impl MailBackend for JmapType {
&store,
mailbox_hash,
).await?;
if res.is_empty() {
return;
}
yield res;
}))
}
@ -453,14 +452,7 @@ impl MailBackend for JmapType {
};
let res_text = res.text().await?;
let upload_response: UploadResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
let upload_response: UploadResponse = serde_json::from_str(&res_text)?;
let mut req = Request::new(conn.request_no.clone());
let creation_id: Id<EmailObject> = "1".to_string().into();
let mut email_imports = HashMap::default();
@ -484,14 +476,7 @@ impl MailBackend for JmapType {
.await?;
let res_text = res.text().await?;
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
let mut v: MethodResponse = serde_json::from_str(&res_text)?;
let m = ImportResponse::try_from(v.method_responses.remove(0)).or_else(|err| {
let ierr: Result<ImportError> =
serde_json::from_str(&res_text).map_err(|err| err.into());
@ -565,14 +550,7 @@ impl MailBackend for JmapType {
.await?;
let res_text = res.text().await?;
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
*store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
let m = QueryResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
let QueryResponse::<EmailObject> { ids, .. } = m;
@ -668,14 +646,7 @@ impl MailBackend for JmapType {
let res_text = res.text().await?;
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
*store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
let m = SetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
if let Some(ids) = m.not_updated {
@ -780,14 +751,7 @@ impl MailBackend for JmapType {
*{"methodResponses":[["Email/set",{"notUpdated":null,"notDestroyed":null,"oldState":"86","newState":"87","accountId":"u148940c7","updated":{"M045926eed54b11423918f392":{"id":"M045926eed54b11423918f392"}},"created":null,"destroyed":null,"notCreated":null},"m3"]],"sessionState":"cyrus-0;p-5;vfs-0"}
*/
//debug!("res_text = {}", &res_text);
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
*store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
let m = SetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
if let Some(ids) = m.not_updated {
@ -937,9 +901,10 @@ impl JmapType {
.unwrap_or_else(|| Ok($default))
};
}
get_conf_val!(s["server_url"])?;
get_conf_val!(s["server_hostname"])?;
get_conf_val!(s["server_username"])?;
get_conf_val!(s["server_password"])?;
get_conf_val!(s["server_port"], 443)?;
get_conf_val!(s["danger_accept_invalid_certs"], false)?;
Ok(())
}

View File

@ -21,7 +21,6 @@
use super::*;
use isahc::config::Configurable;
use std::sync::MutexGuard;
#[derive(Debug)]
pub struct JmapConnection {
@ -57,32 +56,24 @@ impl JmapConnection {
if self.store.online_status.lock().await.1.is_ok() {
return Ok(());
}
let mut jmap_session_resource_url = self.server_conf.server_url.to_string();
let mut jmap_session_resource_url =
if self.server_conf.server_hostname.starts_with("https://") {
self.server_conf.server_hostname.to_string()
} else {
format!("https://{}", &self.server_conf.server_hostname)
};
if self.server_conf.server_port != 443 {
jmap_session_resource_url.push(':');
jmap_session_resource_url.push_str(&self.server_conf.server_port.to_string());
}
jmap_session_resource_url.push_str("/.well-known/jmap");
let mut req = self.client.get_async(&jmap_session_resource_url).await.map_err(|err| {
let err = MeliError::new(format!("Could not connect to JMAP server endpoint for {}. Is your server url setting correct? (i.e. \"jmap.mailserver.org\") (Note: only session resource discovery via /.well-known/jmap is supported. DNS SRV records are not suppported.)\nError connecting to server: {}", &self.server_conf.server_url, &err)).set_source(Some(Arc::new(err)));
//*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
err
})?;
if !req.status().is_success() {
let kind: crate::error::NetworkErrorKind = req.status().into();
let res_text = req.text().await.unwrap_or_default();
let err = MeliError::new(format!(
"Could not connect to JMAP server endpoint for {}. Reply from server: {}",
&self.server_conf.server_url, res_text
))
.set_kind(kind.into());
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
let mut req = self.client.get_async(&jmap_session_resource_url).await?;
let res_text = req.text().await?;
let session: JmapSession = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("Could not connect to JMAP server endpoint for {}. Is your server url setting correct? (i.e. \"jmap.mailserver.org\") (Note: only session resource discovery via /.well-known/jmap is supported. DNS SRV records are not suppported.)\nReply from server: {}", &self.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err)));
let err = MeliError::new(format!("Could not connect to JMAP server endpoint for {}. Is your server hostname setting correct? (i.e. \"jmap.mailserver.org\") (Note: only session resource discovery via /.well-known/jmap is supported. DNS SRV records are not suppported.)\nReply from server: {}", &self.server_conf.server_hostname, &res_text)).set_source(Some(Arc::new(err)));
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
@ -92,7 +83,7 @@ impl JmapConnection {
.capabilities
.contains_key("urn:ietf:params:jmap:core")
{
let err = MeliError::new(format!("Server {} did not return JMAP Core capability (urn:ietf:params:jmap:core). Returned capabilities were: {}", &self.server_conf.server_url, session.capabilities.keys().map(String::as_str).collect::<Vec<&str>>().join(", ")));
let err = MeliError::new(format!("Server {} did not return JMAP Core capability (urn:ietf:params:jmap:core). Returned capabilities were: {}", &self.server_conf.server_hostname, session.capabilities.keys().map(String::as_str).collect::<Vec<&str>>().join(", ")));
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
@ -100,7 +91,7 @@ impl JmapConnection {
.capabilities
.contains_key("urn:ietf:params:jmap:mail")
{
let err = MeliError::new(format!("Server {} does not support JMAP Mail capability (urn:ietf:params:jmap:mail). Returned capabilities were: {}", &self.server_conf.server_url, session.capabilities.keys().map(String::as_str).collect::<Vec<&str>>().join(", ")));
let err = MeliError::new(format!("Server {} does not support JMAP Mail capability (urn:ietf:params:jmap:mail). Returned capabilities were: {}", &self.server_conf.server_hostname, session.capabilities.keys().map(String::as_str).collect::<Vec<&str>>().join(", ")));
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
@ -114,10 +105,6 @@ impl JmapConnection {
self.session.lock().unwrap().primary_accounts["urn:ietf:params:jmap:mail"].clone()
}
pub fn session_guard(&'_ self) -> MutexGuard<'_, JmapSession> {
self.session.lock().unwrap()
}
pub fn add_refresh_event(&self, event: RefreshEvent) {
(self.store.event_consumer)(self.store.account_hash, BackendEvent::Refresh(event));
}
@ -192,14 +179,7 @@ impl JmapConnection {
let res_text = res.text().await?;
debug!(&res_text);
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &self.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
let changes_response =
ChangesResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
if changes_response.new_state == current_state {

View File

@ -95,11 +95,9 @@ impl BackendMailbox for JmapMailbox {
None => SpecialUsageMailbox::Normal,
}
}
fn is_subscribed(&self) -> bool {
self.is_subscribed
}
fn set_is_subscribed(&mut self, new_val: bool) -> Result<()> {
self.is_subscribed = new_val;
// FIXME: jmap subscribe

View File

@ -842,8 +842,7 @@ pub struct EmailQueryChangesResponse {
impl std::convert::TryFrom<&RawValue> for EmailQueryChangesResponse {
type Error = crate::error::MeliError;
fn try_from(t: &RawValue) -> Result<EmailQueryChangesResponse> {
let res: (String, EmailQueryChangesResponse, String) =
serde_json::from_str(t.get()).map_err(|err| crate::error::MeliError::new(format!("BUG: Could not deserialize server JSON response properly, please report this!\nReply from server: {}", &t)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug))?;
let res: (String, EmailQueryChangesResponse, String) = serde_json::from_str(t.get())?;
assert_eq!(&res.0, "Email/queryChanges");
Ok(res.1)
}

View File

@ -184,8 +184,7 @@ pub struct ImportResponse {
impl std::convert::TryFrom<&RawValue> for ImportResponse {
type Error = crate::error::MeliError;
fn try_from(t: &RawValue) -> Result<ImportResponse> {
let res: (String, ImportResponse, String) =
serde_json::from_str(t.get()).map_err(|err| crate::error::MeliError::new(format!("BUG: Could not deserialize server JSON response properly, please report this!\nReply from server: {}", &t)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug))?;
let res: (String, ImportResponse, String) = serde_json::from_str(t.get())?;
assert_eq!(&res.0, &ImportCall::NAME);
Ok(res.1)
}

View File

@ -102,29 +102,12 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
.await?;
let res_text = res.text().await?;
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
*conn.store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
let m = GetResponse::<MailboxObject>::try_from(v.method_responses.remove(0))?;
let GetResponse::<MailboxObject> {
list, account_id, ..
} = m;
// Is account set as `personal`? (`isPersonal` property). Then, even if `isSubscribed` is false
// on a mailbox, it should be regarded as subscribed.
let is_personal: bool = {
let session = conn.session_guard();
session
.accounts
.get(&account_id)
.map(|acc| acc.is_personal)
.unwrap_or(false)
};
*conn.store.account_id.lock().unwrap() = account_id;
let mut ret: HashMap<MailboxHash, JmapMailbox> = list
.into_iter()
@ -158,7 +141,7 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
path: name,
children: Vec::new(),
id,
is_subscribed: is_subscribed || is_personal,
is_subscribed,
my_rights,
parent_id,
parent_hash,
@ -209,14 +192,7 @@ pub async fn get_message_list(
.await?;
let res_text = res.text().await?;
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
*conn.store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
let m = QueryResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
let QueryResponse::<EmailObject> { ids, .. } = m;
@ -289,14 +265,7 @@ pub async fn fetch(
let res_text = res.text().await?;
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
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

View File

@ -227,32 +227,32 @@ impl Object for JmapSession {
#[serde(rename_all = "camelCase")]
pub struct CapabilitiesObject {
#[serde(default)]
pub max_size_upload: u64,
max_size_upload: u64,
#[serde(default)]
pub max_concurrent_upload: u64,
max_concurrent_upload: u64,
#[serde(default)]
pub max_size_request: u64,
max_size_request: u64,
#[serde(default)]
pub max_concurrent_requests: u64,
max_concurrent_requests: u64,
#[serde(default)]
pub max_calls_in_request: u64,
max_calls_in_request: u64,
#[serde(default)]
pub max_objects_in_get: u64,
max_objects_in_get: u64,
#[serde(default)]
pub max_objects_in_set: u64,
max_objects_in_set: u64,
#[serde(default)]
pub collation_algorithms: Vec<String>,
collation_algorithms: Vec<String>,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Account {
pub name: String,
pub is_personal: bool,
pub is_read_only: bool,
pub account_capabilities: HashMap<String, Value>,
name: String,
is_personal: bool,
is_read_only: bool,
account_capabilities: HashMap<String, Value>,
#[serde(flatten)]
pub extra_properties: HashMap<String, Value>,
extra_properties: HashMap<String, Value>,
}
impl Object for Account {
@ -413,8 +413,7 @@ pub struct GetResponse<OBJ: Object> {
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for GetResponse<OBJ> {
type Error = crate::error::MeliError;
fn try_from(t: &RawValue) -> Result<GetResponse<OBJ>, crate::error::MeliError> {
let res: (String, GetResponse<OBJ>, String) =
serde_json::from_str(t.get()).map_err(|err| crate::error::MeliError::new(format!("BUG: Could not deserialize server JSON response properly, please report this!\nReply from server: {}", &t)).set_source(Some(Arc::new(err))).set_kind(crate::error::ErrorKind::Bug))?;
let res: (String, GetResponse<OBJ>, String) = serde_json::from_str(t.get())?;
assert_eq!(&res.0, &format!("{}/get", OBJ::NAME));
Ok(res.1)
}
@ -441,20 +440,20 @@ pub struct Query<F: FilterTrait<OBJ>, OBJ: Object>
where
OBJ: std::fmt::Debug + Serialize,
{
pub account_id: Id<Account>,
pub filter: Option<F>,
pub sort: Option<Comparator<OBJ>>,
account_id: Id<Account>,
filter: Option<F>,
sort: Option<Comparator<OBJ>>,
#[serde(default)]
pub position: u64,
position: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub anchor: Option<String>,
anchor: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "u64_zero")]
pub anchor_offset: u64,
anchor_offset: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u64>,
limit: Option<u64>,
#[serde(default = "bool_false")]
pub calculate_total: bool,
calculate_total: bool,
#[serde(skip)]
_ph: PhantomData<fn() -> OBJ>,
}
@ -518,8 +517,7 @@ pub struct QueryResponse<OBJ: Object> {
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for QueryResponse<OBJ> {
type Error = crate::error::MeliError;
fn try_from(t: &RawValue) -> Result<QueryResponse<OBJ>, crate::error::MeliError> {
let res: (String, QueryResponse<OBJ>, String) =
serde_json::from_str(t.get()).map_err(|err| crate::error::MeliError::new(format!("BUG: Could not deserialize server JSON response properly, please report this!\nReply from server: {}", &t)).set_source(Some(Arc::new(err))).set_kind(crate::error::ErrorKind::Bug))?;
let res: (String, QueryResponse<OBJ>, String) = serde_json::from_str(t.get())?;
assert_eq!(&res.0, &format!("{}/query", OBJ::NAME));
Ok(res.1)
}
@ -653,8 +651,7 @@ pub struct ChangesResponse<OBJ: Object> {
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for ChangesResponse<OBJ> {
type Error = crate::error::MeliError;
fn try_from(t: &RawValue) -> Result<ChangesResponse<OBJ>, crate::error::MeliError> {
let res: (String, ChangesResponse<OBJ>, String) =
serde_json::from_str(t.get()).map_err(|err| crate::error::MeliError::new(format!("BUG: Could not deserialize server JSON response properly, please report this!\nReply from server: {}", &t)).set_source(Some(Arc::new(err))).set_kind(crate::error::ErrorKind::Bug))?;
let res: (String, ChangesResponse<OBJ>, String) = serde_json::from_str(t.get())?;
assert_eq!(&res.0, &format!("{}/changes", OBJ::NAME));
Ok(res.1)
}
@ -852,8 +849,7 @@ pub struct SetResponse<OBJ: Object> {
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for SetResponse<OBJ> {
type Error = crate::error::MeliError;
fn try_from(t: &RawValue) -> Result<SetResponse<OBJ>, crate::error::MeliError> {
let res: (String, SetResponse<OBJ>, String) =
serde_json::from_str(t.get()).map_err(|err| crate::error::MeliError::new(format!("BUG: Could not deserialize server JSON response properly, please report this!\nReply from server: {}", &t)).set_source(Some(Arc::new(err))).set_kind(crate::error::ErrorKind::Bug))?;
let res: (String, SetResponse<OBJ>, String) = serde_json::from_str(t.get())?;
assert_eq!(&res.0, &format!("{}/set", OBJ::NAME));
Ok(res.1)
}

View File

@ -82,7 +82,7 @@ impl MaildirOp {
Ok(if let Some(modif) = &map[&self.hash].modified {
match modif {
PathMod::Path(ref path) => path.clone(),
PathMod::Hash(hash) => map[hash].to_path_buf(),
PathMod::Hash(hash) => map[&hash].to_path_buf(),
}
} else {
map.get(&self.hash).unwrap().to_path_buf()
@ -138,8 +138,11 @@ impl MaildirMailbox {
settings: &AccountSettings,
) -> Result<Self> {
let pathbuf = PathBuf::from(&path);
let mut h = DefaultHasher::new();
pathbuf.hash(&mut h);
let hash = {
let mut h = DefaultHasher::new();
pathbuf.hash(&mut h);
MailboxHash(h.finish())
};
/* Check if mailbox path (Eg `INBOX/Lists/luddites`) is included in the subscribed
* mailboxes in user configuration */
@ -148,7 +151,7 @@ impl MaildirMailbox {
PathBuf::from(&settings.root_mailbox)
.expand()
.parent()
.unwrap_or_else(|| Path::new("/")),
.unwrap_or_else(|| &Path::new("/")),
)
.ok();
@ -159,7 +162,7 @@ impl MaildirMailbox {
};
let ret = MaildirMailbox {
hash: h.finish(),
hash,
name: file_name,
path: fname.unwrap().to_path_buf(),
fs_path: pathbuf,
@ -217,7 +220,7 @@ impl BackendMailbox for MaildirMailbox {
}
fn path(&self) -> &str {
self.path.to_str().unwrap_or_else(|| self.name())
self.path.to_str().unwrap_or(self.name())
}
fn change_name(&mut self, s: &str) {

View File

@ -19,11 +19,6 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
//! # Maildir Backend
//!
//! This module implements a maildir backend according to the maildir specification.
//! <https://cr.yp.to/proto/maildir.html>
use super::{MaildirMailbox, MaildirOp, MaildirPathTrait};
use crate::backends::{RefreshEventKind::*, *};
use crate::conf::AccountSettings;
@ -89,7 +84,7 @@ impl From<PathBuf> for MaildirPath {
#[derive(Debug, Default)]
pub struct HashIndex {
index: HashMap<EnvelopeHash, MaildirPath>,
_hash: MailboxHash,
hash: MailboxHash,
}
impl Deref for HashIndex {
@ -107,7 +102,7 @@ impl DerefMut for HashIndex {
pub type HashIndexes = Arc<Mutex<HashMap<MailboxHash, HashIndex>>>;
/// The maildir backend instance type.
/// Maildir backend https://cr.yp.to/proto/maildir.html
#[derive(Debug)]
pub struct MaildirType {
name: String,
@ -205,7 +200,7 @@ impl MailBackend for MaildirType {
let unseen = mailbox.unseen.clone();
let total = mailbox.total.clone();
let path: PathBuf = mailbox.fs_path().into();
let root_mailbox = self.path.to_path_buf();
let root_path = self.path.to_path_buf();
let map = self.hash_indexes.clone();
let mailbox_index = self.mailbox_index.clone();
super::stream::MaildirStream::new(
@ -214,7 +209,7 @@ impl MailBackend for MaildirType {
unseen,
total,
path,
root_mailbox,
root_path,
map,
mailbox_index,
)
@ -225,13 +220,13 @@ impl MailBackend for MaildirType {
let account_hash = {
let mut hasher = DefaultHasher::default();
hasher.write(self.name.as_bytes());
hasher.finish()
AccountHash(hasher.finish())
};
let sender = self.event_consumer.clone();
let mailbox: &MaildirMailbox = &self.mailboxes[&mailbox_hash];
let path: PathBuf = mailbox.fs_path().into();
let root_mailbox = self.path.to_path_buf();
let root_path = self.path.to_path_buf();
let map = self.hash_indexes.clone();
let mailbox_index = self.mailbox_index.clone();
@ -266,7 +261,7 @@ impl MailBackend for MaildirType {
.lock()
.unwrap()
.insert(env.hash(), mailbox_hash);
let file_name = file.strip_prefix(&root_mailbox).unwrap().to_path_buf();
let file_name = file.strip_prefix(&root_path).unwrap().to_path_buf();
if let Ok(cached) = cache_dir.place_cache_file(file_name) {
/* place result in cache directory */
let f = fs::File::create(cached)?;
@ -332,14 +327,12 @@ impl MailBackend for MaildirType {
let account_hash = {
let mut hasher = DefaultHasher::default();
hasher.write(self.name.as_bytes());
hasher.finish()
AccountHash(hasher.finish())
};
let root_mailbox = self.path.to_path_buf();
watcher
.watch(&root_mailbox, RecursiveMode::Recursive)
.unwrap();
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_mailbox);
debug!("watching {:?}", root_path);
let hash_indexes = self.hash_indexes.clone();
let mailbox_index = self.mailbox_index.clone();
let root_mailbox_hash: MailboxHash = self
@ -384,10 +377,10 @@ impl MailBackend for MaildirType {
}
};
}
let mailbox_hash = get_path_hash!(pathbuf);
let mailbox_hash = MailboxHash(get_path_hash!(pathbuf));
let file_name = pathbuf
.as_path()
.strip_prefix(&root_mailbox)
.strip_prefix(&root_path)
.unwrap()
.to_path_buf();
if let Ok(env) = add_path_to_index(
@ -425,13 +418,13 @@ impl MailBackend for MaildirType {
/* Update */
DebouncedEvent::NoticeWrite(pathbuf) | DebouncedEvent::Write(pathbuf) => {
debug!("DebouncedEvent::Write(path = {:?}", &pathbuf);
let mailbox_hash = get_path_hash!(pathbuf);
let mailbox_hash = MailboxHash(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_mailbox)
.strip_prefix(&root_path)
.unwrap()
.to_path_buf();
/* Linear search in hash_index to find old hash */
@ -502,7 +495,7 @@ impl MailBackend for MaildirType {
/* Remove */
DebouncedEvent::NoticeRemove(pathbuf) | DebouncedEvent::Remove(pathbuf) => {
debug!("DebouncedEvent::Remove(path = {:?}", pathbuf);
let mailbox_hash = get_path_hash!(pathbuf);
let mailbox_hash = MailboxHash(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, _)) =
@ -523,7 +516,7 @@ impl MailBackend for MaildirType {
PathMod::Hash(hash) => debug!(
"envelope {} has modified path set {}",
hash,
&index_lock[hash].buf.display()
&index_lock[&hash].buf.display()
),
}
index_lock.entry(hash).and_modify(|e| {
@ -556,9 +549,9 @@ impl MailBackend for MaildirType {
/* Envelope hasn't changed */
DebouncedEvent::Rename(src, dest) => {
debug!("DebouncedEvent::Rename(src = {:?}, dest = {:?})", src, dest);
let mailbox_hash = get_path_hash!(src);
let mailbox_hash = MailboxHash(get_path_hash!(src));
let dest_mailbox = {
let dest_mailbox = get_path_hash!(dest);
let dest_mailbox = MailboxHash(get_path_hash!(dest));
if dest_mailbox == mailbox_hash {
None
} else {
@ -593,7 +586,7 @@ impl MailBackend for MaildirType {
);
let file_name = dest
.as_path()
.strip_prefix(&root_mailbox)
.strip_prefix(&root_path)
.unwrap()
.to_path_buf();
drop(hash_indexes_lock);
@ -683,7 +676,7 @@ impl MailBackend for MaildirType {
}
let file_name = dest
.as_path()
.strip_prefix(&root_mailbox)
.strip_prefix(&root_path)
.unwrap()
.to_path_buf();
debug!("filename = {:?}", file_name);
@ -732,7 +725,7 @@ impl MailBackend for MaildirType {
drop(hash_indexes_lock);
let file_name = dest
.as_path()
.strip_prefix(&root_mailbox)
.strip_prefix(&root_path)
.unwrap()
.to_path_buf();
if let Ok(env) = add_path_to_index(
@ -794,7 +787,7 @@ impl MailBackend for MaildirType {
/* 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),
mailbox_hash: MailboxHash(get_path_hash!(dest)),
kind: Rescan,
}));
*/
@ -861,7 +854,7 @@ impl MailBackend for MaildirType {
if let Some(modif) = &hash_index[&env_hash].modified {
match modif {
PathMod::Path(ref path) => path.clone(),
PathMod::Hash(hash) => hash_index[hash].to_path_buf(),
PathMod::Hash(hash) => hash_index[&hash].to_path_buf(),
}
} else {
hash_index[&env_hash].to_path_buf()
@ -926,7 +919,7 @@ impl MailBackend for MaildirType {
if let Some(modif) = &hash_index[&env_hash].modified {
match modif {
PathMod::Path(ref path) => path.clone(),
PathMod::Hash(hash) => hash_index[hash].to_path_buf(),
PathMod::Hash(hash) => hash_index[&hash].to_path_buf(),
}
} else {
hash_index[&env_hash].to_path_buf()
@ -966,15 +959,15 @@ impl MailBackend for MaildirType {
if let Some(modif) = &hash_index[&env_hash].modified {
match modif {
PathMod::Path(ref path) => path.clone(),
PathMod::Hash(hash) => hash_index[hash].to_path_buf(),
PathMod::Hash(hash) => hash_index[&hash].to_path_buf(),
}
} else {
hash_index[&env_hash].to_path_buf()
}
};
let filename = path_src.file_name().ok_or_else(|| {
format!("Could not get filename of `{}`", path_src.display(),)
})?;
let filename = path_src
.file_name()
.expect(&format!("Could not get filename of {}", path_src.display()));
dest_path.push(filename);
hash_index.entry(env_hash).or_default().modified =
Some(PathMod::Path(dest_path.clone()));
@ -1018,7 +1011,7 @@ impl MailBackend for MaildirType {
.map(|item| *item.0)
});
let mailbox_hash = get_path_hash!(&path);
let mailbox_hash = MailboxHash(get_path_hash!(&path));
if let Some(parent) = parent {
self.mailboxes
.entry(parent)
@ -1121,7 +1114,7 @@ impl MaildirType {
None,
Vec::new(),
false,
settings,
&settings,
) {
f.children = recurse_mailboxes(mailboxes, settings, &path)?;
for c in &f.children {
@ -1144,7 +1137,7 @@ impl MaildirType {
None,
subdirs,
true,
settings,
&settings,
) {
for c in &f.children {
if let Some(f) = mailboxes.get_mut(c) {
@ -1162,29 +1155,24 @@ impl MaildirType {
}
Ok(children)
}
let root_mailbox = PathBuf::from(settings.root_mailbox()).expand();
if !root_mailbox.exists() {
let root_path = PathBuf::from(settings.root_mailbox()).expand();
if !root_path.exists() {
return Err(MeliError::new(format!(
"Configuration error ({}): root_mailbox `{}` is not a valid directory.",
"Configuration error ({}): root_path `{}` is not a valid directory.",
settings.name(),
settings.root_mailbox.as_str()
)));
} else if !root_mailbox.is_dir() {
} else if !root_path.is_dir() {
return Err(MeliError::new(format!(
"Configuration error ({}): root_mailbox `{}` is not a directory.",
"Configuration error ({}): root_path `{}` is not a directory.",
settings.name(),
settings.root_mailbox.as_str()
)));
}
if let Ok(f) = MaildirMailbox::new(
root_mailbox.to_str().unwrap().to_string(),
root_mailbox
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.to_string(),
root_path.to_str().unwrap().to_string(),
root_path.file_name().unwrap().to_str().unwrap().to_string(),
None,
Vec::with_capacity(0),
false,
@ -1194,7 +1182,7 @@ impl MaildirType {
}
if mailboxes.is_empty() {
let children = recurse_mailboxes(&mut mailboxes, settings, &root_mailbox)?;
let children = recurse_mailboxes(&mut mailboxes, settings, &root_path)?;
for c in &children {
if let Some(f) = mailboxes.get_mut(c) {
f.parent = None;
@ -1202,7 +1190,7 @@ impl MaildirType {
}
} else {
let root_hash = *mailboxes.keys().next().unwrap();
let children = recurse_mailboxes(&mut mailboxes, settings, &root_mailbox)?;
let children = recurse_mailboxes(&mut mailboxes, settings, &root_path)?;
for c in &children {
if let Some(f) = mailboxes.get_mut(c) {
f.parent = Some(root_hash);
@ -1225,7 +1213,7 @@ impl MaildirType {
fh,
HashIndex {
index: HashMap::with_capacity_and_hasher(0, Default::default()),
_hash: fh,
hash: fh,
},
);
}
@ -1236,7 +1224,7 @@ impl MaildirType {
mailbox_index: Default::default(),
event_consumer,
collection: Default::default(),
path: root_mailbox,
path: root_path,
}))
}
@ -1310,16 +1298,16 @@ impl MaildirType {
}
pub fn validate_config(s: &mut AccountSettings) -> Result<()> {
let root_mailbox = PathBuf::from(s.root_mailbox()).expand();
if !root_mailbox.exists() {
let root_path = PathBuf::from(s.root_mailbox()).expand();
if !root_path.exists() {
return Err(MeliError::new(format!(
"Configuration error ({}): root_mailbox `{}` is not a valid directory.",
"Configuration error ({}): root_path `{}` is not a valid directory.",
s.name(),
s.root_mailbox.as_str()
)));
} else if !root_mailbox.is_dir() {
} else if !root_path.is_dir() {
return Err(MeliError::new(format!(
"Configuration error ({}): root_mailbox `{}` is not a directory.",
"Configuration error ({}): root_path `{}` is not a directory.",
s.name(),
s.root_mailbox.as_str()
)));
@ -1331,17 +1319,25 @@ impl MaildirType {
pub fn list_mail_in_maildir_fs(mut path: PathBuf, read_only: bool) -> Result<Vec<PathBuf>> {
let mut files: Vec<PathBuf> = vec![];
path.push("new");
for p in path.read_dir()?.flatten() {
if !read_only {
move_to_cur(p.path()).ok().take();
} else {
files.push(p.path());
for d in path.read_dir()? {
if let Ok(p) = d {
if !read_only {
move_to_cur(p.path()).ok().take();
} else {
files.push(p.path());
}
}
}
path.pop();
path.push("cur");
for e in path.read_dir()?.flatten() {
files.push(e.path());
let iter = path.read_dir()?;
for e in iter {
let e = e.and_then(|x| {
let path = x.path();
Ok(path)
})?;
files.push(e);
}
Ok(files)
}

View File

@ -46,22 +46,29 @@ impl MaildirStream {
unseen: Arc<Mutex<usize>>,
total: Arc<Mutex<usize>>,
mut path: PathBuf,
root_mailbox: PathBuf,
root_path: PathBuf,
map: HashIndexes,
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
) -> Result<Pin<Box<dyn Stream<Item = Result<Vec<Envelope>>> + Send + 'static>>> {
let chunk_size = 2048;
path.push("new");
for p in path.read_dir()?.flatten() {
move_to_cur(p.path()).ok().take();
for d in path.read_dir()? {
if let Ok(p) = d {
move_to_cur(p.path()).ok().take();
}
}
path.pop();
path.push("cur");
let files: Vec<PathBuf> = path
.read_dir()?
.flatten()
.map(|e| e.path())
.collect::<Vec<_>>();
let iter = path.read_dir()?;
let count = path.read_dir()?.count();
let mut files: Vec<PathBuf> = Vec::with_capacity(count);
for e in iter {
let e = e.and_then(|x| {
let path = x.path();
Ok(path)
})?;
files.push(e);
}
let payloads = Box::pin(if !files.is_empty() {
files
.chunks(chunk_size)
@ -73,7 +80,7 @@ impl MaildirStream {
mailbox_hash,
unseen.clone(),
total.clone(),
root_mailbox.clone(),
root_path.clone(),
map.clone(),
mailbox_index.clone(),
)) as Pin<Box<dyn Future<Output = _> + Send + 'static>>
@ -91,7 +98,7 @@ impl MaildirStream {
mailbox_hash: MailboxHash,
unseen: Arc<Mutex<usize>>,
total: Arc<Mutex<usize>>,
root_mailbox: PathBuf,
root_path: PathBuf,
map: HashIndexes,
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
) -> Result<Vec<Envelope>> {
@ -102,7 +109,7 @@ impl MaildirStream {
/* Check if we have a cache file with this email's
* filename */
let file_name = PathBuf::from(&file)
.strip_prefix(&root_mailbox)
.strip_prefix(&root_path)
.unwrap()
.to_path_buf();
if let Some(cached) = cache_dir.find_cache_file(&file_name) {

View File

@ -23,17 +23,16 @@
//!
//! ## Resources
//!
//! [^0]: <https://web.archive.org/web/20160812091518/https://jdebp.eu./FGA/mail-mbox-formats.html>
//! [^1]: <https://wiki2.dovecot.org/MailboxFormat/mbox>
//! [^2]: <https://manpages.debian.org/buster/mutt/mbox.5.en.html>
//! - [0] <https://web.archive.org/web/20160812091518/https://jdebp.eu./FGA/mail-mbox-formats.html>
//! - [1] <https://wiki2.dovecot.org/MailboxFormat/mbox>
//! - [2] <https://manpages.debian.org/buster/mutt/mbox.5.en.html>
//!
//! ## `mbox` format
//!
//! `mbox` describes a family of incompatible legacy formats.
//!
//! "All of the 'mbox' formats store all of the messages in the mailbox in a single file. Delivery appends new messages to the end of the file." [^0]
//! "All of the 'mbox' formats store all of the messages in the mailbox in a single file. Delivery appends new messages to the end of the file." [0]
//!
//! "Each message is preceded by a From_ line and followed by a blank line. A From_ line is a line that begins with the five characters 'F', 'r', 'o', 'm', and ' '." [^0]
//! "Each message is preceded by a From_ line and followed by a blank line. A From_ line is a line that begins with the five characters 'F', 'r', 'o', 'm', and ' '." [0]
//!
//! ## `From ` / postmark line
//!
@ -60,7 +59,7 @@
//!
//! "In order to avoid misinterpretation of lines in message bodies which begin with the four
//! characters 'From', followed by a space character, the mail delivery agent must quote
//! any occurrence of 'From ' at the start of a body line." [^2]
//! any occurrence of 'From ' at the start of a body line." [2]
//!
//! ## Metadata
//!
@ -272,7 +271,7 @@ impl BackendMailbox for MboxMailbox {
/// `BackendOp` implementor for Mbox
#[derive(Debug, Default)]
pub struct MboxOp {
_hash: EnvelopeHash,
hash: EnvelopeHash,
path: PathBuf,
offset: Offset,
length: Length,
@ -280,9 +279,9 @@ pub struct MboxOp {
}
impl MboxOp {
pub fn new(_hash: EnvelopeHash, path: &Path, offset: Offset, length: Length) -> Self {
pub fn new(hash: EnvelopeHash, path: &Path, offset: Offset, length: Length) -> Self {
MboxOp {
_hash,
hash,
path: path.to_path_buf(),
slice: std::cell::RefCell::new(None),
offset,
@ -884,7 +883,7 @@ impl MailBackend for MboxType {
drop(mailboxes_lck);
let mut message_iter = MessageIterator {
index,
input: self.contents.as_slice(),
input: &self.contents.as_slice(),
offset: self.offset,
file_offset: self.file_offset,
format: self.prefer_mbox_type,
@ -982,7 +981,7 @@ impl MailBackend for MboxType {
let account_hash = {
let mut hasher = DefaultHasher::new();
hasher.write(self.account_name.as_bytes());
hasher.finish()
AccountHash(hasher.finish())
};
let mailboxes = self.mailboxes.clone();
let mailbox_index = self.mailbox_index.clone();
@ -1003,7 +1002,7 @@ impl MailBackend for MboxType {
Ok(event) => match event {
/* Update */
DebouncedEvent::NoticeWrite(pathbuf) | DebouncedEvent::Write(pathbuf) => {
let mailbox_hash = get_path_hash!(&pathbuf);
let mailbox_hash = MailboxHash(get_path_hash!(&pathbuf));
let file = match std::fs::OpenOptions::new()
.read(true)
.write(true)
@ -1065,7 +1064,7 @@ impl MailBackend for MboxType {
.values()
.any(|f| f.fs_path == pathbuf)
{
let mailbox_hash = get_path_hash!(&pathbuf);
let mailbox_hash = MailboxHash(get_path_hash!(&pathbuf));
(sender)(
account_hash,
BackendEvent::Refresh(RefreshEvent {
@ -1082,7 +1081,7 @@ impl MailBackend for MboxType {
}
DebouncedEvent::Rename(src, dest) => {
if mailboxes.lock().unwrap().values().any(|f| f.fs_path == src) {
let mailbox_hash = get_path_hash!(&src);
let mailbox_hash = MailboxHash(get_path_hash!(&src));
(sender)(
account_hash,
BackendEvent::Refresh(RefreshEvent {
@ -1266,7 +1265,7 @@ impl MboxType {
.file_name()
.map(|f| f.to_string_lossy().into())
.unwrap_or_default();
let hash = get_path_hash!(&ret.path);
let hash = MailboxHash(get_path_hash!(&ret.path));
let read_only = if let Ok(metadata) = std::fs::metadata(&ret.path) {
metadata.permissions().readonly()
@ -1304,7 +1303,7 @@ impl MboxType {
/* Look for other mailboxes */
for (k, f) in s.mailboxes.iter() {
if let Some(path_str) = f.extra.get("path") {
let hash = get_path_hash!(path_str);
let hash = MailboxHash(get_path_hash!(path_str));
let pathbuf: PathBuf = path_str.into();
if !pathbuf.exists() || pathbuf.is_dir() {
return Err(MeliError::new(format!(

View File

@ -50,7 +50,7 @@ impl MboxFormat {
writer.write_all(&b" "[..])?;
writer.write_all(
crate::datetime::timestamp_to_string(
delivery_date.unwrap_or_else(crate::datetime::now),
delivery_date.unwrap_or_else(|| crate::datetime::now()),
Some(crate::datetime::ASCTIME_FMT),
true,
)

View File

@ -19,13 +19,6 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
//! # NNTP backend / client
//!
//! Implements an NNTP client as specified by [RFC 3977: Network News Transfer Protocol
//! (NNTP)](https://datatracker.ietf.org/doc/html/rfc3977). Also implements [RFC 6048: Network News
//! Transfer Protocol (NNTP) Additions to LIST
//! Command](https://datatracker.ietf.org/doc/html/rfc6048).
use crate::get_conf_val;
use crate::get_path_hash;
use smallvec::SmallVec;
@ -115,6 +108,7 @@ type Capabilities = HashSet<String>;
pub struct UIDStore {
account_hash: AccountHash,
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)>>>,
@ -136,6 +130,7 @@ impl UIDStore {
account_hash,
account_name,
event_consumer,
offline_cache: false,
capabilities: Default::default(),
message_id_index: Default::default(),
hash_index: Default::default(),
@ -152,11 +147,11 @@ impl UIDStore {
#[derive(Debug)]
pub struct NntpType {
_is_subscribed: Arc<IsSubscribedFn>,
is_subscribed: Arc<IsSubscribedFn>,
connection: Arc<FutureMutex<NntpConnection>>,
server_conf: NntpServerConf,
uid_store: Arc<UIDStore>,
_can_create_flags: Arc<Mutex<bool>>,
can_create_flags: Arc<Mutex<bool>>,
}
impl MailBackend for NntpType {
@ -256,7 +251,8 @@ impl MailBackend for NntpType {
/* 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();
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();
@ -577,12 +573,12 @@ impl NntpType {
let account_hash = {
let mut hasher = DefaultHasher::new();
hasher.write(s.name.as_bytes());
hasher.finish()
AccountHash(hasher.finish())
};
let account_name = Arc::new(s.name().to_string());
let mut mailboxes = HashMap::default();
for (k, _f) in s.mailboxes.iter() {
let mailbox_hash = get_path_hash!(&k);
let mailbox_hash = MailboxHash(get_path_hash!(&k));
mailboxes.insert(
mailbox_hash,
NntpMailbox {
@ -603,6 +599,7 @@ impl NntpType {
)));
}
let uid_store: Arc<UIDStore> = Arc::new(UIDStore {
offline_cache: false, //get_conf_val!(s["X_header_caching"], false)?,
mailboxes: Arc::new(FutureMutex::new(mailboxes)),
..UIDStore::new(account_hash, account_name, event_consumer)
});
@ -610,8 +607,8 @@ impl NntpType {
Ok(Box::new(NntpType {
server_conf,
_is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)),
_can_create_flags: Arc::new(Mutex::new(false)),
is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)),
can_create_flags: Arc::new(Mutex::new(false)),
connection: Arc::new(FutureMutex::new(connection)),
uid_store,
}))
@ -648,7 +645,7 @@ impl NntpType {
if s.len() != 3 {
continue;
}
let mailbox_hash = get_path_hash!(&s[0]);
let mailbox_hash = MailboxHash(get_path_hash!(&s[0]));
mailboxes_lck.entry(mailbox_hash).and_modify(|m| {
*m.high_watermark.lock().unwrap() = usize::from_str(s[1]).unwrap_or(0);
*m.low_watermark.lock().unwrap() = usize::from_str(s[2]).unwrap_or(0);
@ -702,7 +699,7 @@ impl NntpType {
let _ = get_conf_val!(s["server_password_command"]);
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 = get_conf_val!(s["use_starttls"], server_port != 563)?;
let use_starttls = get_conf_val!(s["use_starttls"], !(server_port == 563))?;
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",

View File

@ -90,10 +90,11 @@ impl NntpStream {
let stream = {
let addr = lookup_ipv4(path, server_conf.server_port)?;
AsyncWrapper::new(Connection::Tcp(TcpStream::connect_timeout(
&addr,
std::time::Duration::new(16, 0),
)?))?
AsyncWrapper::new(Connection::Tcp(
TcpStream::connect_timeout(&addr, std::time::Duration::new(16, 0))
.chain_err_kind(crate::error::ErrorKind::Network)?,
))
.chain_err_kind(crate::error::ErrorKind::Network)?
};
let mut res = String::with_capacity(8 * 1024);
let mut ret = NntpStream {
@ -108,7 +109,9 @@ impl NntpStream {
if server_conf.danger_accept_invalid_certs {
connector.danger_accept_invalid_certs(true);
}
let connector = connector.build()?;
let connector = connector
.build()
.chain_err_kind(crate::error::ErrorKind::Network)?;
if server_conf.use_starttls {
ret.read_response(&mut res, false, &["200 ", "201 "])
@ -144,8 +147,14 @@ impl NntpStream {
&server_conf.server_hostname
)));
}
ret.stream.write_all(b"STARTTLS\r\n").await?;
ret.stream.flush().await?;
ret.stream
.write_all(b"STARTTLS\r\n")
.await
.chain_err_kind(crate::error::ErrorKind::Network)?;
ret.stream
.flush()
.await
.chain_err_kind(crate::error::ErrorKind::Network)?;
ret.read_response(&mut res, false, command_to_replycodes("STARTTLS"))
.await?;
if !res.starts_with("382 ") {
@ -158,7 +167,10 @@ impl NntpStream {
{
// FIXME: This is blocking
let socket = ret.stream.into_inner()?;
let socket = ret
.stream
.into_inner()
.chain_err_kind(crate::error::ErrorKind::Network)?;
let mut conn_result = connector.connect(path, socket);
if let Err(native_tls::HandshakeError::WouldBlock(midhandshake_stream)) =
conn_result
@ -174,17 +186,16 @@ impl NntpStream {
midhandshake_stream = Some(stream);
}
p => {
p.chain_err_kind(crate::error::ErrorKind::Network(
crate::error::NetworkErrorKind::InvalidTLSConnection,
))?;
p.chain_err_kind(crate::error::ErrorKind::Network)?;
}
}
}
}
ret.stream =
AsyncWrapper::new(Connection::Tls(conn_result?)).chain_err_summary(|| {
format!("Could not initiate TLS negotiation to {}.", path)
})?;
ret.stream = AsyncWrapper::new(Connection::Tls(
conn_result.chain_err_kind(crate::error::ErrorKind::Network)?,
))
.chain_err_summary(|| format!("Could not initiate TLS negotiation to {}.", path))
.chain_err_kind(crate::error::ErrorKind::Network)?;
}
}
//ret.send_command(
@ -356,8 +367,8 @@ impl NntpStream {
last_line_idx += pos + "\r\n".len();
}
}
Err(err) => {
return Err(MeliError::from(err));
Err(e) => {
return Err(MeliError::from(e).set_err_kind(crate::error::ErrorKind::Network));
}
}
}
@ -382,7 +393,7 @@ impl NntpStream {
.await
{
debug!("stream send_command err {:?}", err);
Err(err)
Err(err.set_err_kind(crate::error::ErrorKind::Network))
} else {
Ok(())
}
@ -416,7 +427,7 @@ impl NntpStream {
.await
{
debug!("stream send_multiline_data_block err {:?}", err);
Err(err)
Err(err.set_err_kind(crate::error::ErrorKind::Network))
} else {
Ok(())
}

View File

@ -102,7 +102,7 @@ impl DbConnection {
*self.revision_uuid.read().unwrap(),
new_revision_uuid
);
let query: Query = Query::new(self, &query_str)?;
let query: Query = Query::new(&self, &query_str)?;
let iter = query.search()?;
let mailbox_index_lck = mailbox_index.write().unwrap();
let mailboxes_lck = mailboxes.read().unwrap();
@ -156,7 +156,7 @@ impl DbConnection {
}
drop(query);
index.write().unwrap().retain(|&env_hash, msg_id| {
if Message::find_message(self, msg_id).is_err() {
if Message::find_message(&self, &msg_id).is_err() {
if let Some(mailbox_hashes) = mailbox_index_lck.get(&env_hash) {
for &mailbox_hash in mailbox_hashes {
let m = &mailboxes_lck[&mailbox_hash];
@ -224,7 +224,7 @@ pub struct NotmuchDb {
mailbox_index: Arc<RwLock<HashMap<EnvelopeHash, SmallVec<[MailboxHash; 16]>>>>,
collection: Collection,
path: PathBuf,
_account_name: Arc<String>,
account_name: Arc<String>,
account_hash: AccountHash,
event_consumer: BackendEventConsumer,
save_messages_to: Option<PathBuf>,
@ -367,7 +367,7 @@ impl NotmuchDb {
let hash = {
let mut h = DefaultHasher::new();
k.hash(&mut h);
h.finish()
MailboxHash(h.finish())
};
mailboxes.insert(
hash,
@ -396,7 +396,7 @@ impl NotmuchDb {
let account_hash = {
let mut hasher = DefaultHasher::new();
hasher.write(s.name().as_bytes());
hasher.finish()
AccountHash(hasher.finish())
};
Ok(Box::new(NotmuchDb {
lib,
@ -408,7 +408,7 @@ impl NotmuchDb {
mailboxes: Arc::new(RwLock::new(mailboxes)),
save_messages_to: None,
_account_name: Arc::new(s.name().to_string()),
account_name: Arc::new(s.name().to_string()),
account_hash,
event_consumer,
}))
@ -533,7 +533,7 @@ impl MailBackend for NotmuchDb {
database: Arc<DbConnection>,
index: Arc<RwLock<HashMap<EnvelopeHash, CString>>>,
mailbox_index: Arc<RwLock<HashMap<EnvelopeHash, SmallVec<[MailboxHash; 16]>>>>,
mailboxes: Arc<RwLock<HashMap<u64, NotmuchMailbox>>>,
mailboxes: Arc<RwLock<HashMap<MailboxHash, NotmuchMailbox>>>,
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
iter: std::vec::IntoIter<CString>,
}
@ -694,7 +694,7 @@ impl MailBackend for NotmuchDb {
index.clone(),
mailbox_index.clone(),
collection.tag_index.clone(),
account_hash,
account_hash.clone(),
event_consumer.clone(),
new_revision_uuid,
)?;
@ -728,6 +728,7 @@ impl MailBackend for NotmuchDb {
hash,
index: self.index.clone(),
bytes: None,
collection: self.collection.clone(),
}))
}
@ -931,6 +932,7 @@ impl MailBackend for NotmuchDb {
struct NotmuchOp {
hash: EnvelopeHash,
index: Arc<RwLock<HashMap<EnvelopeHash, CString>>>,
collection: Collection,
database: Arc<DbConnection>,
bytes: Option<Vec<u8>>,
#[allow(dead_code)]
@ -1059,7 +1061,7 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
ret.push(c);
}
}
ret.push('"');
ret.push_str("\"");
}
To(s) | Cc(s) | Bcc(s) => {
ret.push_str("to:\"");
@ -1070,7 +1072,7 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
ret.push(c);
}
}
ret.push('"');
ret.push_str("\"");
}
InReplyTo(_s) | References(_s) | AllAddresses(_s) => {}
/* * * * */
@ -1083,7 +1085,7 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
ret.push(c);
}
}
ret.push('"');
ret.push_str("\"");
}
Subject(s) => {
ret.push_str("subject:\"");
@ -1094,10 +1096,10 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
ret.push(c);
}
}
ret.push('"');
ret.push_str("\"");
}
AllText(s) => {
ret.push('"');
ret.push_str("\"");
for c in s.chars() {
if c == '"' {
ret.push_str("\\\"");
@ -1105,7 +1107,7 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
ret.push(c);
}
}
ret.push('"');
ret.push_str("\"");
}
/* * * * */
Flags(v) => {

View File

@ -16,11 +16,9 @@ pub const _notmuch_status_NOTMUCH_STATUS_OUT_OF_MEMORY: _notmuch_status = 1;
pub const _notmuch_status_NOTMUCH_STATUS_READ_ONLY_DATABASE: _notmuch_status = 2;
/// A Xapian exception occurred.
///
/// ```text
/// @todo We don't really want to expose this lame XAPIAN_EXCEPTION
/// value. Instead we should map to things like DATABASE_LOCKED or
/// whatever.
/// ```
pub const _notmuch_status_NOTMUCH_STATUS_XAPIAN_EXCEPTION: _notmuch_status = 3;
/// An error occurred trying to read or write to a file (this could
/// be file not found, permission denied, etc.)
@ -468,7 +466,6 @@ pub type notmuch_database_get_directory = unsafe extern "C" fn(
///
/// Return value:
///
/// ```text
/// NOTMUCH_STATUS_SUCCESS: Message successfully added to database.
///
/// NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred,
@ -493,7 +490,6 @@ pub type notmuch_database_get_directory = unsafe extern "C" fn(
/// database to use this function.
///
/// @since libnotmuch 5.1 (notmuch 0.26)
/// ```
pub type notmuch_database_index_file = unsafe extern "C" fn(
database: *mut notmuch_database_t,
filename: *const ::std::os::raw::c_char,
@ -505,10 +501,8 @@ extern "C" {
/// Deprecated alias for notmuch_database_index_file called with
/// NULL indexopts.
///
/// ```text
/// @deprecated Deprecated as of libnotmuch 5.1 (notmuch 0.26). Please
/// use notmuch_database_index_file instead.
/// ```
///
pub fn notmuch_database_add_message(
database: *mut notmuch_database_t,
@ -622,7 +616,7 @@ pub type notmuch_database_get_all_tags =
/// completely in the future, but it's likely to be a specialized
/// version of the general Xapian query syntax:
///
/// <https://xapian.org/docs/queryparser.html>
/// https://xapian.org/docs/queryparser.html
///
/// As a special case, passing either a length-zero string, (that is ""),
/// or a string consisting of a single asterisk (that is "*"), will
@ -709,9 +703,7 @@ pub type notmuch_query_get_sort =
/// This exclusion will be ignored if this tag appears explicitly in
/// the query.
///
/// ```text
/// @returns
/// ```
///
/// NOTMUCH_STATUS_SUCCESS: excluded was added successfully.
///
@ -765,9 +757,7 @@ pub type notmuch_query_add_tag_exclude = unsafe extern "C" fn(
/// notmuch_threads_destroy function, but there's no good reason
/// to call it if the query is about to be destroyed).
///
/// ```text
/// @since libnotmuch 5.0 (notmuch 0.25)
/// ```
pub type notmuch_query_search_threads = unsafe extern "C" fn(
query: *mut notmuch_query_t,
out: *mut *mut notmuch_threads_t,
@ -775,9 +765,7 @@ pub type notmuch_query_search_threads = unsafe extern "C" fn(
/// Deprecated alias for notmuch_query_search_threads.
///
/// ```text
/// @deprecated Deprecated as of libnotmuch 5 (notmuch 0.25). Please
/// ```
/// use notmuch_query_search_threads instead.
///
pub type notmuch_query_search_threads_st = unsafe extern "C" fn(
@ -825,9 +813,7 @@ pub type notmuch_query_search_threads_st = unsafe extern "C" fn(
///
/// If a Xapian exception occurs this function will return NULL.
///
/// ```text
/// @since libnotmuch 5 (notmuch 0.25)
/// ```
pub type notmuch_query_search_messages = unsafe extern "C" fn(
query: *mut notmuch_query_t,
out: *mut *mut notmuch_messages_t,
@ -835,9 +821,7 @@ pub type notmuch_query_search_messages = unsafe extern "C" fn(
/// Deprecated alias for notmuch_query_search_messages
///
/// ```text
/// @deprecated Deprecated as of libnotmuch 5 (notmuch 0.25). Please use
/// ```
/// notmuch_query_search_messages instead.
///
pub type notmuch_query_search_messages_st = unsafe extern "C" fn(
@ -903,7 +887,6 @@ pub type notmuch_threads_destroy = unsafe extern "C" fn(threads: *mut notmuch_th
/// This function performs a search and returns the number of matching
/// messages.
///
/// ```text
/// @returns
///
/// NOTMUCH_STATUS_SUCCESS: query completed successfully.
@ -912,7 +895,6 @@ pub type notmuch_threads_destroy = unsafe extern "C" fn(threads: *mut notmuch_th
/// value of *count is not defined.
///
/// @since libnotmuch 5 (notmuch 0.25)
/// ```
pub type notmuch_query_count_messages = unsafe extern "C" fn(
query: *mut notmuch_query_t,
count: *mut ::std::os::raw::c_uint,
@ -920,10 +902,9 @@ pub type notmuch_query_count_messages = unsafe extern "C" fn(
/// Deprecated alias for notmuch_query_count_messages
///
/// ```text
///
/// @deprecated Deprecated since libnotmuch 5.0 (notmuch 0.25). Please
/// use notmuch_query_count_messages instead.
/// ```
pub type notmuch_query_count_messages_st = unsafe extern "C" fn(
query: *mut notmuch_query_t,
count: *mut ::std::os::raw::c_uint,
@ -938,7 +919,6 @@ pub type notmuch_query_count_messages_st = unsafe extern "C" fn(
/// Note that this is a significantly heavier operation than
/// notmuch_query_count_messages{_st}().
///
/// ```text
/// @returns
///
/// NOTMUCH_STATUS_OUT_OF_MEMORY: Memory allocation failed. The value
@ -950,7 +930,6 @@ pub type notmuch_query_count_messages_st = unsafe extern "C" fn(
/// value of *count is not defined.
///
/// @since libnotmuch 5 (notmuch 0.25)
/// ```
pub type notmuch_query_count_threads = unsafe extern "C" fn(
query: *mut notmuch_query_t,
count: *mut ::std::os::raw::c_uint,
@ -958,10 +937,8 @@ pub type notmuch_query_count_threads = unsafe extern "C" fn(
/// Deprecated alias for notmuch_query_count_threads
///
/// ```text
/// @deprecated Deprecated as of libnotmuch 5.0 (notmuch 0.25). Please
/// use notmuch_query_count_threads_st instead.
/// ```
pub type notmuch_query_count_threads_st = unsafe extern "C" fn(
query: *mut notmuch_query_t,
count: *mut ::std::os::raw::c_uint,
@ -987,10 +964,8 @@ pub type notmuch_thread_get_total_messages =
///
/// This sums notmuch_message_count_files over all messages in the
/// thread
/// ```text
/// @returns Non-negative integer
/// @since libnotmuch 5.0 (notmuch 0.25)
/// ```
pub type notmuch_thread_get_total_files =
unsafe extern "C" fn(thread: *mut notmuch_thread_t) -> ::std::os::raw::c_int;
@ -1161,9 +1136,7 @@ pub type notmuch_messages_collect_tags =
/// Get the database associated with this message.
///
/// ```text
/// @since libnotmuch 5.2 (notmuch 0.27)
/// ```
pub type notmuch_message_get_database =
unsafe extern "C" fn(message: *const notmuch_message_t) -> *mut notmuch_database_t;
@ -1215,10 +1188,8 @@ pub type notmuch_message_get_replies =
unsafe extern "C" fn(message: *mut notmuch_message_t) -> *mut notmuch_messages_t;
/// Get the total number of files associated with a message.
/// ```text
/// @returns Non-negative integer
/// @since libnotmuch 5.0 (notmuch 0.25)
/// ```
pub type notmuch_message_count_files =
unsafe extern "C" fn(message: *mut notmuch_message_t) -> ::std::os::raw::c_int;
@ -1479,7 +1450,6 @@ pub type notmuch_message_tags_to_maildir_flags =
/// change tag values. For example, explicitly setting a message to
/// have a given set of tags might look like this:
///
/// ```c
/// notmuch_message_freeze (message);
///
/// notmuch_message_remove_all_tags (message);
@ -1488,7 +1458,6 @@ pub type notmuch_message_tags_to_maildir_flags =
/// notmuch_message_add_tag (message, tags[i]);
///
/// notmuch_message_thaw (message);
/// ```
///
/// With freeze/thaw used like this, the message in the database is
/// guaranteed to have either the full set of original tag values, or
@ -1539,9 +1508,7 @@ pub type notmuch_message_thaw =
/// the messages get reclaimed when the containing query is destroyed.)
pub type notmuch_message_destroy = unsafe extern "C" fn(message: *mut notmuch_message_t);
/// ```text
/// @name Message Properties
/// ```
///
/// This interface provides the ability to attach arbitrary (key,value)
/// string pairs to a message, to remove such pairs, and to iterate
@ -1558,12 +1525,10 @@ pub type notmuch_message_destroy = unsafe extern "C" fn(message: *mut notmuch_me
/// no such key. In the case of multiple values for the given key, the
/// first one is retrieved.
///
/// ```text
/// @returns
/// - NOTMUCH_STATUS_NULL_POINTER: *value* may not be NULL.
/// - NOTMUCH_STATUS_SUCCESS: No error occurred.
/// @since libnotmuch 4.4 (notmuch 0.23)
/// ```
pub type notmuch_message_get_property = unsafe extern "C" fn(
message: *mut notmuch_message_t,
key: *const ::std::os::raw::c_char,
@ -1572,13 +1537,11 @@ pub type notmuch_message_get_property = unsafe extern "C" fn(
/// Add a (key,value) pair to a message
///
/// ```text
/// @returns
/// - NOTMUCH_STATUS_ILLEGAL_ARGUMENT: *key* may not contain an '=' character.
/// - NOTMUCH_STATUS_NULL_POINTER: Neither *key* nor *value* may be NULL.
/// - NOTMUCH_STATUS_SUCCESS: No error occurred.
/// @since libnotmuch 4.4 (notmuch 0.23)
/// ```
pub type notmuch_message_add_property = unsafe extern "C" fn(
message: *mut notmuch_message_t,
key: *const ::std::os::raw::c_char,
@ -1589,13 +1552,11 @@ pub type notmuch_message_add_property = unsafe extern "C" fn(
///
/// It is not an error to remove a non-existant (key,value) pair
///
/// ```text
/// @returns
/// - NOTMUCH_STATUS_ILLEGAL_ARGUMENT: *key* may not contain an '=' character.
/// - NOTMUCH_STATUS_NULL_POINTER: Neither *key* nor *value* may be NULL.
/// - NOTMUCH_STATUS_SUCCESS: No error occurred.
/// @since libnotmuch 4.4 (notmuch 0.23)
/// ```
pub type notmuch_message_remove_property = unsafe extern "C" fn(
message: *mut notmuch_message_t,
key: *const ::std::os::raw::c_char,
@ -1604,7 +1565,6 @@ pub type notmuch_message_remove_property = unsafe extern "C" fn(
/// Remove all (key,value) pairs from the given message.
///
/// ```text
/// @param[in,out] message message to operate on.
/// @param[in] key key to delete properties for. If NULL, delete
/// properties for all keys
@ -1614,7 +1574,6 @@ pub type notmuch_message_remove_property = unsafe extern "C" fn(
/// - NOTMUCH_STATUS_SUCCESS: No error occurred.
///
/// @since libnotmuch 4.4 (notmuch 0.23)
/// ```
pub type notmuch_message_remove_all_properties = unsafe extern "C" fn(
message: *mut notmuch_message_t,
key: *const ::std::os::raw::c_char,
@ -1622,7 +1581,6 @@ pub type notmuch_message_remove_all_properties = unsafe extern "C" fn(
/// Remove all (prefix*,value) pairs from the given message
///
/// ```text
/// @param[in,out] message message to operate on.
/// @param[in] prefix delete properties with keys that start with prefix.
/// If NULL, delete all properties
@ -1632,7 +1590,6 @@ pub type notmuch_message_remove_all_properties = unsafe extern "C" fn(
/// - NOTMUCH_STATUS_SUCCESS: No error occurred.
///
/// @since libnotmuch 5.1 (notmuch 0.26)
/// ```
pub type notmuch_message_remove_all_properties_with_prefix =
unsafe extern "C" fn(
message: *mut notmuch_message_t,
@ -1655,12 +1612,10 @@ extern "C" {
/// as such, will only be valid for as long as the message is valid,
/// (which is until the query from which it derived is destroyed).
///
/// ```text
/// @param[in] message The message to examine
/// @param[in] key key or key prefix
/// @param[in] exact if TRUE, require exact match with key. Otherwise
/// treat as prefix.
/// ```
///
/// Typical usage might be:
///
@ -1680,9 +1635,7 @@ extern "C" {
/// provide a notmuch_message_properities_destroy function, but there's
/// no good reason to call it if the message is about to be destroyed).
///
/// ```text
/// @since libnotmuch 4.4 (notmuch 0.23)
/// ```
pub fn notmuch_message_get_properties(
message: *mut notmuch_message_t,
key: *const ::std::os::raw::c_char,
@ -1691,7 +1644,6 @@ extern "C" {
}
/// Return the number of properties named "key" belonging to the specific message.
///
/// ```text
/// @param[in] message The message to examine
/// @param[in] key key to count
/// @param[out] count The number of matching properties associated with this message.
@ -1701,7 +1653,6 @@ extern "C" {
/// NOTMUCH_STATUS_SUCCESS: successful count, possibly some other error.
///
/// @since libnotmuch 5.2 (notmuch 0.27)
/// ```
pub type notmuch_message_count_properties = unsafe extern "C" fn(
message: *mut notmuch_message_t,
key: *const ::std::os::raw::c_char,
@ -1721,9 +1672,7 @@ pub type notmuch_message_count_properties = unsafe extern "C" fn(
/// code showing how to iterate over a notmuch_message_properties_t
/// object.
///
/// ```text
/// @since libnotmuch 4.4 (notmuch 0.23)
/// ```
pub type notmuch_message_properties_valid =
unsafe extern "C" fn(properties: *mut notmuch_message_properties_t) -> notmuch_bool_t;
@ -1736,9 +1685,7 @@ pub type notmuch_message_properties_valid =
/// See the documentation of notmuch_message_get_properties for example
/// code showing how to iterate over a notmuch_message_properties_t object.
///
/// ```text
/// @since libnotmuch 4.4 (notmuch 0.23)
/// ```
pub type notmuch_message_properties_move_to_next =
unsafe extern "C" fn(properties: *mut notmuch_message_properties_t);
@ -1746,9 +1693,7 @@ pub type notmuch_message_properties_move_to_next =
///
/// this could be useful if iterating for a prefix
///
/// ```text
/// @since libnotmuch 4.4 (notmuch 0.23)
/// ```
pub type notmuch_message_properties_key = unsafe extern "C" fn(
properties: *mut notmuch_message_properties_t,
) -> *const ::std::os::raw::c_char;
@ -1757,9 +1702,7 @@ pub type notmuch_message_properties_key = unsafe extern "C" fn(
///
/// This could be useful if iterating for a prefix.
///
/// ```text
/// @since libnotmuch 4.4 (notmuch 0.23)
/// ```
pub type notmuch_message_properties_value = unsafe extern "C" fn(
properties: *mut notmuch_message_properties_t,
) -> *const ::std::os::raw::c_char;
@ -1770,9 +1713,7 @@ pub type notmuch_message_properties_value = unsafe extern "C" fn(
/// the notmuch_message_properties_t object will be reclaimed when the
/// containing message object is destroyed.
///
/// ```text
/// @since libnotmuch 4.4 (notmuch 0.23)
/// ```
pub type notmuch_message_properties_destroy =
unsafe extern "C" fn(properties: *mut notmuch_message_properties_t);
@ -1880,9 +1821,7 @@ pub type notmuch_directory_get_child_directories =
/// notmuch_directory_t object. Assumes any child directories and files
/// have been deleted by the caller.
///
/// ```text
/// @since libnotmuch 4.3 (notmuch 0.21)
/// ```
pub type notmuch_directory_delete =
unsafe extern "C" fn(directory: *mut notmuch_directory_t) -> notmuch_status_t;
@ -1933,9 +1872,7 @@ pub type notmuch_filenames_destroy = unsafe extern "C" fn(filenames: *mut notmuc
/// set config 'key' to 'value'
///
/// ```text
/// @since libnotmuch 4.4 (notmuch 0.23)
/// ```
pub type notmuch_database_set_config = unsafe extern "C" fn(
db: *mut notmuch_database_t,
key: *const ::std::os::raw::c_char,
@ -1950,9 +1887,7 @@ pub type notmuch_database_set_config = unsafe extern "C" fn(
/// return value is allocated by malloc and should be freed by the
/// caller.
///
/// ```text
/// @since libnotmuch 4.4 (notmuch 0.23)
/// ```
pub type notmuch_database_get_config = unsafe extern "C" fn(
db: *mut notmuch_database_t,
key: *const ::std::os::raw::c_char,
@ -1961,9 +1896,7 @@ pub type notmuch_database_get_config = unsafe extern "C" fn(
/// Create an iterator for all config items with keys matching a given prefix
///
/// ```text
/// @since libnotmuch 4.4 (notmuch 0.23)
/// ```
pub type notmuch_database_get_config_list = unsafe extern "C" fn(
db: *mut notmuch_database_t,
prefix: *const ::std::os::raw::c_char,
@ -1972,9 +1905,7 @@ pub type notmuch_database_get_config_list = unsafe extern "C" fn(
/// Is 'config_list' iterator valid (i.e. _key, _value, _move_to_next can be called).
///
/// ```text
/// @since libnotmuch 4.4 (notmuch 0.23)
/// ```
pub type notmuch_config_list_valid =
unsafe extern "C" fn(config_list: *mut notmuch_config_list_t) -> notmuch_bool_t;
@ -1983,9 +1914,7 @@ pub type notmuch_config_list_valid =
/// return value is owned by the iterator, and will be destroyed by the
/// next call to notmuch_config_list_key or notmuch_config_list_destroy.
///
/// ```text
/// @since libnotmuch 4.4 (notmuch 0.23)
/// ```
pub type notmuch_config_list_key =
unsafe extern "C" fn(config_list: *mut notmuch_config_list_t) -> *const ::std::os::raw::c_char;
@ -1994,25 +1923,19 @@ pub type notmuch_config_list_key =
/// return value is owned by the iterator, and will be destroyed by the
/// next call to notmuch_config_list_value or notmuch config_list_destroy
///
/// ```text
/// @since libnotmuch 4.4 (notmuch 0.23)
/// ```
pub type notmuch_config_list_value =
unsafe extern "C" fn(config_list: *mut notmuch_config_list_t) -> *const ::std::os::raw::c_char;
/// move 'config_list' iterator to the next pair
///
/// ```text
/// @since libnotmuch 4.4 (notmuch 0.23)
/// ```
pub type notmuch_config_list_move_to_next =
unsafe extern "C" fn(config_list: *mut notmuch_config_list_t);
/// free any resources held by 'config_list'
///
/// ```text
/// @since libnotmuch 4.4 (notmuch 0.23)
/// ```
pub type notmuch_config_list_destroy =
unsafe extern "C" fn(config_list: *mut notmuch_config_list_t);
@ -2025,9 +1948,7 @@ pub type notmuch_config_list_destroy =
/// This object represents a set of options on how a message can be
/// added to the index. At the moment it is a featureless stub.
///
/// ```text
/// @since libnotmuch 5.1 (notmuch 0.26)
/// ```
pub type notmuch_database_get_default_indexopts =
unsafe extern "C" fn(db: *mut notmuch_database_t) -> *mut notmuch_indexopts_t;
@ -2046,9 +1967,7 @@ pub type notmuch_decryption_policy_t = u32;
/// message index is adequately protected. DO NOT SET THIS FLAG TO TRUE
/// without considering the security of your index.
///
/// ```text
/// @since libnotmuch 5.1 (notmuch 0.26)
/// ```
pub type notmuch_indexopts_set_decrypt_policy = unsafe extern "C" fn(
indexopts: *mut notmuch_indexopts_t,
decrypt_policy: notmuch_decryption_policy_t,
@ -2057,23 +1976,17 @@ pub type notmuch_indexopts_set_decrypt_policy = unsafe extern "C" fn(
/// Return whether to decrypt encrypted parts while indexing.
/// see notmuch_indexopts_set_decrypt_policy.
///
/// ```text
/// @since libnotmuch 5.1 (notmuch 0.26)
/// ```
pub type notmuch_indexopts_get_decrypt_policy =
unsafe extern "C" fn(indexopts: *const notmuch_indexopts_t) -> notmuch_decryption_policy_t;
/// Destroy a notmuch_indexopts_t object.
///
/// ```text
/// @since libnotmuch 5.1 (notmuch 0.26)
/// ```
pub type notmuch_indexopts_destroy = unsafe extern "C" fn(options: *mut notmuch_indexopts_t);
/// interrogate the library for compile time features
///
/// ```text
/// @since libnotmuch 4.4 (notmuch 0.23)
/// ```
pub type notmuch_built_with =
unsafe extern "C" fn(name: *const ::std::os::raw::c_char) -> notmuch_bool_t;

View File

@ -247,7 +247,7 @@ impl<'m> Message<'m> {
pub fn get_filename(&self) -> &OsStr {
let fs_path = unsafe { call!(self.lib, notmuch_message_get_filename)(self.message) };
let c_str = unsafe { CStr::from_ptr(fs_path) };
OsStr::from_bytes(c_str.to_bytes())
&OsStr::from_bytes(c_str.to_bytes())
}
}

View File

@ -475,7 +475,7 @@ impl<K: std::cmp::Eq + std::hash::Hash, V> Deref for RwRef<'_, K, V> {
type Target = V;
fn deref(&self) -> &V {
self.guard.get(&self.hash).expect("Hash was not found")
self.guard.get(&self.hash).unwrap()
}
}
@ -486,7 +486,7 @@ pub struct RwRefMut<'g, K: std::cmp::Eq + std::hash::Hash, V> {
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).expect("Hash was not found")
self.guard.get_mut(&self.hash).unwrap()
}
}
@ -494,6 +494,6 @@ 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).expect("Hash was not found")
self.guard.get(&self.hash).unwrap()
}
}

View File

@ -202,19 +202,23 @@ impl ToggleFlag {
ToggleFlag::Unset == *self
}
pub fn is_internal(&self) -> bool {
matches!(self, ToggleFlag::InternalVal(_))
if let ToggleFlag::InternalVal(_) = *self {
true
} else {
false
}
}
pub fn is_ask(&self) -> bool {
matches!(self, ToggleFlag::Ask)
*self == ToggleFlag::Ask
}
pub fn is_false(&self) -> bool {
matches!(self, ToggleFlag::False | ToggleFlag::InternalVal(false))
ToggleFlag::False == *self || ToggleFlag::InternalVal(false) == *self
}
pub fn is_true(&self) -> bool {
matches!(self, ToggleFlag::True | ToggleFlag::InternalVal(true))
ToggleFlag::True == *self || ToggleFlag::InternalVal(true) == *self
}
}

View File

@ -271,9 +271,7 @@ pub fn lookup_ipv4(host: &str, port: u16) -> crate::Result<std::net::SocketAddr>
Err(
crate::error::MeliError::new(format!("Could not lookup address {}:{}", host, port))
.set_kind(crate::error::ErrorKind::Network(
crate::error::NetworkErrorKind::HostLookupFailed,
)),
.set_kind(crate::error::ErrorKind::Network),
)
}

View File

@ -41,7 +41,6 @@ use crate::error::{Result, ResultIntoMeliError};
use std::borrow::Cow;
use std::convert::TryInto;
use std::ffi::{CStr, CString};
use std::os::raw::c_int;
pub type UnixTimestamp = u64;
pub const RFC3339_FMT_WITH_TIME: &str = "%Y-%m-%dT%H:%M:%S\0";
@ -75,36 +74,14 @@ extern "C" {
fn gettimeofday(tv: *mut libc::timeval, tz: *mut libc::timezone) -> i32;
}
#[repr(i32)]
#[derive(Copy, Clone)]
#[allow(dead_code)]
enum LocaleCategoryMask {
Time = libc::LC_TIME_MASK,
All = libc::LC_ALL_MASK,
}
#[repr(i32)]
#[derive(Copy, Clone)]
#[allow(dead_code)]
enum LocaleCategory {
Time = libc::LC_TIME,
All = libc::LC_ALL,
}
#[cfg(not(target_os = "netbsd"))]
#[allow(dead_code)]
struct Locale {
mask: LocaleCategoryMask,
category: LocaleCategory,
new_locale: libc::locale_t,
old_locale: libc::locale_t,
}
#[cfg(target_os = "netbsd")]
#[allow(dead_code)]
struct Locale {
mask: LocaleCategoryMask,
category: LocaleCategory,
mask: std::os::raw::c_int,
old_locale: *const std::os::raw::c_char,
}
@ -117,7 +94,7 @@ impl Drop for Locale {
}
#[cfg(target_os = "netbsd")]
unsafe {
let _ = libc::setlocale(self.category as c_int, self.old_locale);
let _ = libc::setlocale(self.mask, self.old_locale);
}
}
}
@ -126,12 +103,11 @@ impl Drop for Locale {
impl Locale {
#[cfg(not(target_os = "netbsd"))]
fn new(
mask: LocaleCategoryMask,
category: LocaleCategory,
mask: std::os::raw::c_int,
locale: *const std::os::raw::c_char,
base: libc::locale_t,
) -> Result<Self> {
let new_locale = unsafe { libc::newlocale(mask as c_int, locale, base) };
let new_locale = unsafe { libc::newlocale(mask, locale, base) };
if new_locale.is_null() {
return Err(nix::Error::last().into());
}
@ -141,32 +117,25 @@ impl Locale {
return Err(nix::Error::last().into());
}
Ok(Locale {
mask,
category,
new_locale,
old_locale,
})
}
#[cfg(target_os = "netbsd")]
fn new(
mask: LocaleCategoryMask,
category: LocaleCategory,
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(category as c_int, std::ptr::null_mut()) };
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(category as c_int, locale) };
let new_locale = unsafe { libc::setlocale(mask, locale) };
if new_locale.is_null() {
return Err(nix::Error::last().into());
}
Ok(Locale {
mask,
category,
old_locale,
})
Ok(Locale { mask, old_locale })
}
}
@ -197,8 +166,7 @@ pub fn timestamp_to_string(timestamp: UnixTimestamp, fmt: Option<&str>, posix: b
let _with_locale: Option<Result<Locale>> = if posix {
Some(
Locale::new(
LocaleCategoryMask::Time,
LocaleCategory::Time,
libc::LC_TIME,
b"C\0".as_ptr() as *const std::os::raw::c_char,
std::ptr::null_mut(),
)
@ -265,7 +233,9 @@ fn year_to_secs(year: i64, is_leap: &mut bool) -> std::result::Result<i64, ()> {
let cycles = (year - 100) / 400;
let centuries;
let mut leaps;
let mut rem = (year - 100) % 400;
let mut rem;
rem = (year - 100) % 400;
if rem == 0 {
*is_leap = true;
@ -336,8 +306,7 @@ where
let fmt = unsafe { CStr::from_bytes_with_nul_unchecked(fmt.as_bytes()) };
let ret = {
let _with_locale = Locale::new(
LocaleCategoryMask::Time,
LocaleCategory::Time,
libc::LC_TIME,
b"C\0".as_ptr() as *const std::os::raw::c_char,
std::ptr::null_mut(),
)
@ -398,8 +367,7 @@ where
let fmt = unsafe { CStr::from_bytes_with_nul_unchecked(fmt.as_bytes()) };
let ret = {
let _with_locale = Locale::new(
LocaleCategoryMask::Time,
LocaleCategory::Time,
libc::LC_TIME,
b"C\0".as_ptr() as *const std::os::raw::c_char,
std::ptr::null_mut(),
)

View File

@ -25,7 +25,7 @@
* # Parsing bytes into an `Envelope`
*
* An [`Envelope`](Envelope) represents the information you can get from an email's headers and body
* structure. Addresses in `To`, `From` fields etc are parsed into [`Address`](crate::email::Address) types.
* structure. Addresses in `To`, `From` fields etc are parsed into [`Address`](email::address::Address) types.
*
* ```
* use melib::{Attachment, Envelope};
@ -391,7 +391,7 @@ impl Envelope {
if let Some(x) = self.in_reply_to.clone() {
self.push_references(x);
}
if let Ok(d) = parser::dates::rfc5322_date(self.date.as_bytes()) {
if let Ok(d) = parser::dates::rfc5322_date(&self.date.as_bytes()) {
self.set_datetime(d);
}
if self.message_id.raw().is_empty() {

View File

@ -112,7 +112,7 @@ impl Address {
}
} else {
MailboxAddress {
raw: address.to_string().into_bytes(),
raw: format!("{}", address).into_bytes(),
display_name: StrBuilder {
offset: 0,
length: 0,
@ -262,7 +262,7 @@ impl Address {
let email = self.get_email();
let (local_part, domain) =
match super::parser::address::addr_spec_raw(email.as_bytes())
.map_err(Into::<MeliError>::into)
.map_err(|err| Into::<MeliError>::into(err))
.and_then(|(_, (l, d))| {
Ok((String::from_utf8(l.into())?, String::from_utf8(d.into())?))
}) {
@ -325,7 +325,7 @@ impl core::fmt::Display for Address {
match self {
Address::Mailbox(m) if m.display_name.length > 0 => {
match m.display_name.display(&m.raw) {
d if d.contains('.') || d.contains(',') => {
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)),
@ -392,7 +392,7 @@ pub trait StrBuild {
}
impl StrBuilder {
pub fn display(&self, s: &[u8]) -> String {
pub fn display<'a>(&self, s: &'a [u8]) -> String {
let offset = self.offset;
let length = self.length;
String::from_utf8_lossy(&s[offset..offset + length]).to_string()

View File

@ -348,7 +348,7 @@ impl PartialEq<&str> for ContentType {
(ContentType::CMSSignature, "application/pkcs7-signature") => true,
(ContentType::MessageRfc822, "message/rfc822") => true,
(ContentType::Other { tag, .. }, _) => {
other.eq_ignore_ascii_case(&String::from_utf8_lossy(tag))
other.eq_ignore_ascii_case(&String::from_utf8_lossy(&tag))
}
(ContentType::OctetStream { .. }, "application/octet-stream") => true,
_ => false,
@ -372,17 +372,22 @@ impl Display for ContentType {
impl ContentType {
pub fn is_text(&self) -> bool {
matches!(self, ContentType::Text { .. })
if let ContentType::Text { .. } = self {
true
} else {
false
}
}
pub fn is_text_html(&self) -> bool {
matches!(
self,
ContentType::Text {
kind: Text::Html,
..
}
)
if let ContentType::Text {
kind: Text::Html, ..
} = self
{
true
} else {
false
}
}
pub fn make_boundary(parts: &[AttachmentBuilder]) -> String {
@ -448,7 +453,11 @@ pub enum Text {
impl Text {
pub fn is_html(&self) -> bool {
matches!(self, Text::Html)
if let Text::Html = self {
true
} else {
false
}
}
}
@ -528,11 +537,11 @@ pub enum ContentDispositionKind {
impl ContentDispositionKind {
pub fn is_inline(&self) -> bool {
matches!(self, ContentDispositionKind::Inline)
*self == ContentDispositionKind::Inline
}
pub fn is_attachment(&self) -> bool {
matches!(self, ContentDispositionKind::Attachment)
*self == ContentDispositionKind::Attachment
}
}

View File

@ -149,7 +149,7 @@ impl AttachmentBuilder {
}
}
if let Some(boundary) = boundary {
let parts = Self::parts(self.body(), boundary);
let parts = Self::parts(self.body(), &boundary);
let boundary = boundary.to_vec();
self.content_type = ContentType::Multipart {
@ -259,7 +259,7 @@ impl AttachmentBuilder {
let mut vec = Vec::with_capacity(attachments.len());
for a in attachments {
let mut builder = AttachmentBuilder::default();
let (headers, body) = match parser::attachments::attachment(a) {
let (headers, body) = match parser::attachments::attachment(&a) {
Ok((_, v)) => v,
Err(_) => {
debug!("error in parsing attachment");
@ -516,19 +516,20 @@ impl Attachment {
.iter()
.find(|(n, _)| n.eq_ignore_ascii_case(b"content-type"))
.and_then(|(_, v)| {
if let Ok((_, (ct, _cst, params))) =
parser::attachments::content_type(v)
{
if ct.eq_ignore_ascii_case(b"multipart") {
let mut boundary = None;
for (n, v) in params {
if n.eq_ignore_ascii_case(b"boundary") {
boundary = Some(v);
break;
match parser::attachments::content_type(v) {
Ok((_, (ct, _cst, params))) => {
if ct.eq_ignore_ascii_case(b"multipart") {
let mut boundary = None;
for (n, v) in params {
if n.eq_ignore_ascii_case(b"boundary") {
boundary = Some(v);
break;
}
}
return boundary;
}
return boundary;
}
_ => {}
}
None
})
@ -553,7 +554,7 @@ impl Attachment {
fn get_text_recursive(&self, text: &mut Vec<u8>) {
match self.content_type {
ContentType::Text { .. } | ContentType::PGPSignature | ContentType::CMSSignature => {
text.extend(self.decode(Default::default()));
text.extend(decode(self, None));
}
ContentType::Multipart {
ref kind,
@ -584,7 +585,6 @@ impl Attachment {
_ => {}
}
}
pub fn text(&self) -> String {
let mut text = Vec::with_capacity(self.body.length);
self.get_text_recursive(&mut text);
@ -594,7 +594,6 @@ impl Attachment {
pub fn mime_type(&self) -> String {
self.content_type.to_string()
}
pub fn attachments(&self) -> Vec<Attachment> {
let mut ret = Vec::new();
fn count_recursive(att: &Attachment, ret: &mut Vec<Attachment>) {
@ -613,26 +612,24 @@ impl Attachment {
}
}
count_recursive(self, &mut ret);
count_recursive(&self, &mut ret);
ret
}
pub fn count_attachments(&self) -> usize {
self.attachments().len()
}
pub fn content_type(&self) -> &ContentType {
&self.content_type
}
pub fn content_transfer_encoding(&self) -> &ContentTransferEncoding {
&self.content_transfer_encoding
}
pub fn is_text(&self) -> bool {
matches!(self.content_type, ContentType::Text { .. })
match self.content_type {
ContentType::Text { .. } => true,
_ => false,
}
}
pub fn is_html(&self) -> bool {
match self.content_type {
ContentType::Text {
@ -653,23 +650,23 @@ impl Attachment {
}
pub fn is_encrypted(&self) -> bool {
matches!(
self.content_type,
match self.content_type {
ContentType::Multipart {
kind: MultipartType::Encrypted,
..
}
)
} => true,
_ => false,
}
}
pub fn is_signed(&self) -> bool {
matches!(
self.content_type,
match self.content_type {
ContentType::Multipart {
kind: MultipartType::Signed,
..
}
)
} => true,
_ => false,
}
}
pub fn into_raw(&self) -> String {
@ -692,13 +689,13 @@ impl Attachment {
for (n, v) in parameters {
ret.push_str("; ");
ret.push_str(&String::from_utf8_lossy(n));
ret.push('=');
ret.push_str("=");
if v.contains(&b' ') {
ret.push('"');
ret.push_str("\"");
}
ret.push_str(&String::from_utf8_lossy(v));
if v.contains(&b' ') {
ret.push('"');
ret.push_str("\"");
}
}
@ -741,7 +738,7 @@ impl Attachment {
} else {
ret.push_str(&format!("Content-Type: {}\r\n\r\n", a.content_type));
}
ret.push_str(BASE64_MIME.encode(a.body()).trim());
ret.push_str(&BASE64_MIME.encode(a.body()).trim());
}
_ => {
ret.push_str(&format!("Content-Type: {}\r\n\r\n", a.content_type));
@ -761,8 +758,11 @@ impl Attachment {
};
for (name, value) in headers {
if name.eq_ignore_ascii_case(b"content-type") {
if let Ok((_, (_, _, params))) = parser::attachments::content_type(value) {
ret = params;
match parser::attachments::content_type(value) {
Ok((_, (_, _, params))) => {
ret = params;
}
_ => {}
}
break;
}
@ -794,133 +794,123 @@ impl Attachment {
.map(|(_, v)| v)
.ok()
.and_then(|n| String::from_utf8(n).ok())
.unwrap_or(s)
.unwrap_or_else(|| s)
})
.map(|n| n.replace(|c| std::path::is_separator(c) || c.is_ascii_control(), "_"))
}
fn decode_rec_helper<'a, 'b>(&'a self, options: &mut DecodeOptions<'b>) -> Vec<u8> {
match self.content_type {
ContentType::Other { .. } => Vec::new(),
ContentType::Text { .. } => self.decode_helper(options),
ContentType::OctetStream { ref name } => name
.clone()
.unwrap_or_else(|| self.mime_type())
.into_bytes(),
ContentType::CMSSignature | ContentType::PGPSignature => Vec::new(),
ContentType::MessageRfc822 => {
if self.content_disposition.kind.is_inline() {
AttachmentBuilder::new(self.body())
.build()
.decode_rec_helper(options)
} else {
b"message/rfc822 attachment".to_vec()
}
}
ContentType::Multipart {
ref kind,
ref parts,
..
} => match kind {
MultipartType::Alternative => {
for a in parts {
if let ContentType::Text {
kind: Text::Plain, ..
} = a.content_type
{
return a.decode_helper(options);
}
}
self.decode_helper(options)
}
MultipartType::Signed => {
let mut vec = Vec::new();
for a in parts {
vec.extend(a.decode_rec_helper(options));
}
vec.extend(self.decode_helper(options));
vec
}
MultipartType::Encrypted => {
let mut vec = Vec::new();
for a in parts {
if a.content_type == "application/octet-stream" {
vec.extend(a.decode_rec_helper(options));
}
}
vec.extend(self.decode_helper(options));
vec
}
_ => {
let mut vec = Vec::new();
for a in parts {
if a.content_disposition.kind.is_inline() {
vec.extend(a.decode_rec_helper(options));
}
}
vec
}
},
}
}
pub fn decode_rec<'a, 'b>(&'a self, mut options: DecodeOptions<'b>) -> Vec<u8> {
self.decode_rec_helper(&mut options)
}
fn decode_helper<'a, 'b>(&'a self, options: &mut DecodeOptions<'b>) -> Vec<u8> {
let charset = options
.force_charset
.unwrap_or_else(|| match self.content_type {
ContentType::Text { charset, .. } => charset,
_ => Default::default(),
});
let bytes = match self.content_transfer_encoding {
ContentTransferEncoding::Base64 => match BASE64_MIME.decode(self.body()) {
Ok(v) => v,
_ => self.body().to_vec(),
},
ContentTransferEncoding::QuotedPrintable => {
parser::encodings::quoted_printable_bytes(self.body())
.unwrap()
.1
}
ContentTransferEncoding::_7Bit
| ContentTransferEncoding::_8Bit
| ContentTransferEncoding::Other { .. } => self.body().to_vec(),
};
let mut ret = if self.content_type.is_text() {
if let Ok(v) = parser::encodings::decode_charset(&bytes, charset) {
v.into_bytes()
} else {
self.body().to_vec()
}
} else {
bytes.to_vec()
};
if let Some(filter) = options.filter.as_mut() {
filter(self, &mut ret);
}
ret
}
pub fn decode<'a, 'b>(&'a self, mut options: DecodeOptions<'b>) -> Vec<u8> {
self.decode_helper(&mut options)
}
}
pub fn interpret_format_flowed(_t: &str) -> String {
unimplemented!()
}
pub type Filter<'a> = Box<dyn FnMut(&Attachment, &mut Vec<u8>) + 'a>;
type Filter<'a> = Box<dyn FnMut(&Attachment, &mut Vec<u8>) + 'a>;
#[derive(Default)]
pub struct DecodeOptions<'att> {
pub filter: Option<Filter<'att>>,
pub force_charset: Option<Charset>,
fn decode_rec_helper<'a, 'b>(a: &'a Attachment, filter: &mut Option<Filter<'b>>) -> Vec<u8> {
match a.content_type {
ContentType::Other { .. } => Vec::new(),
ContentType::Text { .. } => decode_helper(a, filter),
ContentType::OctetStream { ref name } => {
name.clone().unwrap_or_else(|| a.mime_type()).into_bytes()
}
ContentType::CMSSignature | ContentType::PGPSignature => Vec::new(),
ContentType::MessageRfc822 => {
if a.content_disposition.kind.is_inline() {
let b = AttachmentBuilder::new(a.body()).build();
let ret = decode_rec_helper(&b, filter);
ret
} else {
b"message/rfc822 attachment".to_vec()
}
}
ContentType::Multipart {
ref kind,
ref parts,
..
} => match kind {
MultipartType::Alternative => {
for a in parts {
if let ContentType::Text {
kind: Text::Plain, ..
} = a.content_type
{
return decode_helper(a, filter);
}
}
decode_helper(a, filter)
}
MultipartType::Signed => {
let mut vec = Vec::new();
for a in parts {
vec.extend(decode_rec_helper(a, filter));
}
vec.extend(decode_helper(a, filter));
vec
}
MultipartType::Encrypted => {
let mut vec = Vec::new();
for a in parts {
if a.content_type == "application/octet-stream" {
vec.extend(decode_rec_helper(a, filter));
}
}
vec.extend(decode_helper(a, filter));
vec
}
_ => {
let mut vec = Vec::new();
for a in parts {
if a.content_disposition.kind.is_inline() {
vec.extend(decode_rec_helper(a, filter));
}
}
vec
}
},
}
}
pub fn decode_rec<'a, 'b>(a: &'a Attachment, mut filter: Option<Filter<'b>>) -> Vec<u8> {
decode_rec_helper(a, &mut filter)
}
fn decode_helper<'a, 'b>(a: &'a Attachment, filter: &mut Option<Filter<'b>>) -> Vec<u8> {
let charset = match a.content_type {
ContentType::Text { charset: c, .. } => c,
_ => Default::default(),
};
let bytes = match a.content_transfer_encoding {
ContentTransferEncoding::Base64 => match BASE64_MIME.decode(a.body()) {
Ok(v) => v,
_ => a.body().to_vec(),
},
ContentTransferEncoding::QuotedPrintable => {
parser::encodings::quoted_printable_bytes(a.body())
.unwrap()
.1
}
ContentTransferEncoding::_7Bit
| ContentTransferEncoding::_8Bit
| ContentTransferEncoding::Other { .. } => a.body().to_vec(),
};
let mut ret = if a.content_type.is_text() {
if let Ok(v) = parser::encodings::decode_charset(&bytes, charset) {
v.into_bytes()
} else {
a.body().to_vec()
}
} else {
bytes.to_vec()
};
if let Some(filter) = filter {
filter(a, &mut ret);
}
ret
}
pub fn decode<'a, 'b>(a: &'a Attachment, mut filter: Option<Filter<'b>>) -> Vec<u8> {
decode_helper(a, &mut filter)
}

View File

@ -24,7 +24,7 @@ use super::*;
use crate::email::attachment_types::{
Charset, ContentTransferEncoding, ContentType, MultipartType,
};
use crate::email::attachments::AttachmentBuilder;
use crate::email::attachments::{decode, decode_rec, AttachmentBuilder};
use crate::shellexpand::ShellExpandTrait;
use data_encoding::BASE64_MIME;
use std::ffi::OsStr;
@ -92,7 +92,7 @@ impl FromStr for Draft {
}
let body = Envelope::new(0).body_bytes(s.as_bytes());
ret.body = String::from_utf8(body.decode(Default::default()))?;
ret.body = String::from_utf8(decode(&body, None))?;
Ok(ret)
}
@ -101,7 +101,7 @@ impl FromStr for Draft {
impl Draft {
pub fn edit(envelope: &Envelope, bytes: &[u8]) -> Result<Self> {
let mut ret = Draft::default();
for (k, v) in envelope.headers(bytes).unwrap_or_else(|_| Vec::new()) {
for (k, v) in envelope.headers(&bytes).unwrap_or_else(|_| Vec::new()) {
ret.headers.insert(k.try_into()?, v.into());
}
@ -207,7 +207,7 @@ impl Draft {
);
let body = envelope.body_bytes(bytes);
ret.body = {
let reply_body_bytes = body.decode_rec(Default::default());
let reply_body_bytes = decode_rec(&body, None);
let reply_body = String::from_utf8_lossy(&reply_body_bytes);
let lines: Vec<&str> = reply_body.lines().collect();
let mut ret = format!(
@ -257,7 +257,7 @@ impl Draft {
if let Some((pre, _)) = self.wrap_header_preamble.as_ref() {
if !pre.is_empty() {
ret.push_str(pre);
ret.push_str(&pre);
if !pre.ends_with('\n') {
ret.push('\n');
}
@ -273,7 +273,7 @@ impl Draft {
if !post.starts_with('\n') && !ret.ends_with('\n') {
ret.push('\n');
}
ret.push_str(post);
ret.push_str(&post);
ret.push('\n');
}
}
@ -324,11 +324,11 @@ impl Draft {
ret.push_str("\r\n");
}
} else if self.body.is_empty() && self.attachments.len() == 1 {
let attachment = std::mem::take(&mut self.attachments).remove(0);
let attachment = std::mem::replace(&mut self.attachments, Vec::new()).remove(0);
print_attachment(&mut ret, attachment);
} else {
let mut parts = Vec::with_capacity(self.attachments.len() + 1);
let attachments = std::mem::take(&mut self.attachments);
let attachments = std::mem::replace(&mut self.attachments, Vec::new());
if !self.body.is_empty() {
let mut body_attachment = AttachmentBuilder::default();
body_attachment.set_raw(self.body.as_bytes().to_vec());

View File

@ -49,7 +49,7 @@ pub struct ParsingError<I> {
impl core::fmt::Debug for ParsingError<&'_ [u8]> {
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
fmt.debug_struct("ParsingError")
.field("input", &to_str!(self.input))
.field("input", &to_str!(&self.input))
.field("error", &self.error)
.finish()
}
@ -415,22 +415,22 @@ pub mod dates {
accum.extend_from_slice(&day_of_week);
accum.extend_from_slice(b", ");
}
accum.extend_from_slice(day);
accum.extend_from_slice(&day);
accum.extend_from_slice(b" ");
accum.extend_from_slice(month);
accum.extend_from_slice(&month);
accum.extend_from_slice(b" ");
accum.extend_from_slice(year);
accum.extend_from_slice(&year);
accum.extend_from_slice(b" ");
accum.extend_from_slice(hour);
accum.extend_from_slice(&hour);
accum.extend_from_slice(b":");
accum.extend_from_slice(minute);
accum.extend_from_slice(&minute);
if let Some(second) = second {
accum.extend_from_slice(b":");
accum.extend_from_slice(second);
accum.extend_from_slice(&second);
}
accum.extend_from_slice(b" ");
accum.extend_from_slice(sign);
accum.extend_from_slice(zone);
accum.extend_from_slice(&sign);
accum.extend_from_slice(&zone);
match crate::datetime::rfc822_to_timestamp(accum.to_vec()) {
Ok(t) => Ok((input, t)),
Err(_err) => Err(nom::Err::Error(
@ -444,7 +444,6 @@ pub mod dates {
}
///e.g Wed Sep 9 00:27:54 2020
///```text
///day-of-week month day time year
///date-time = [ day-of-week "," ] date time [CFWS]
///date = day month year
@ -453,7 +452,6 @@ pub mod dates {
///hour = 2DIGIT / obs-hour
///minute = 2DIGIT / obs-minute
///second = 2DIGIT / obs-second
///```
pub fn mbox_date_time(input: &[u8]) -> IResult<&[u8], UnixTimestamp> {
let orig_input = input;
let mut accum: SmallVec<[u8; 32]> = SmallVec::new();
@ -474,23 +472,23 @@ pub mod dates {
let (input, year) = year(input)?;
accum.extend_from_slice(&day_of_week);
accum.extend_from_slice(b", ");
accum.extend_from_slice(day);
accum.extend_from_slice(&day);
accum.extend_from_slice(b" ");
accum.extend_from_slice(month);
accum.extend_from_slice(&month);
accum.extend_from_slice(b" ");
accum.extend_from_slice(year);
accum.extend_from_slice(&year);
accum.extend_from_slice(b" ");
accum.extend_from_slice(hour);
accum.extend_from_slice(&hour);
accum.extend_from_slice(b":");
accum.extend_from_slice(minute);
accum.extend_from_slice(&minute);
if let Some(second) = second {
accum.extend_from_slice(b":");
accum.extend_from_slice(second);
accum.extend_from_slice(&second);
}
if let Some((sign, zone)) = zone {
accum.extend_from_slice(b" ");
accum.extend_from_slice(sign);
accum.extend_from_slice(zone);
accum.extend_from_slice(&sign);
accum.extend_from_slice(&zone);
}
match crate::datetime::rfc822_to_timestamp(accum.to_vec()) {
Ok(t) => Ok((input, t)),
@ -573,7 +571,7 @@ pub mod dates {
Ok((rest, ret))
})
.or_else(|_| {
let (rest, ret) = match mbox_date_time(input) {
let (rest, ret) = match mbox_date_time(&input) {
Ok(v) => v,
Err(_) => {
return Err(nom::Err::Error(
@ -863,12 +861,12 @@ pub mod generic {
|input| {
let (input, pr) = many1(terminated(opt(fws), comment))(input)?;
let (input, end) = opt(fws)(input)?;
let mut pr = pr.into_iter().flatten().fold(vec![], |mut acc, x| {
let mut pr = pr.into_iter().filter_map(|s| s).fold(vec![], |mut acc, x| {
acc.extend_from_slice(&x);
acc
});
if pr.is_empty() {
Ok((input, end.unwrap_or_else(|| (&b""[..]).into())))
Ok((input, end.unwrap_or((&b""[..]).into())))
} else {
if let Some(end) = end {
pr.extend_from_slice(&end);
@ -1221,7 +1219,7 @@ pub mod generic {
{
Ok((&input[1..], input[0..1].into()))
} else {
Err(nom::Err::Error((input, "atext(): invalid byte").into()))
return Err(nom::Err::Error((input, "atext(): invalid byte").into()));
}
}
@ -1229,12 +1227,12 @@ pub mod generic {
alt((atext_ascii, utf8_non_ascii))(input)
}
///`dot-atom = [CFWS] dot-atom-text [CFWS]`
///dot-atom = [CFWS] dot-atom-text [CFWS]
pub fn dot_atom(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
let (input, _) = opt(cfws)(input)?;
let (input, ret) = dot_atom_text(input)?;
let (input, _) = opt(cfws)(input)?;
Ok((input, ret))
Ok((input, ret.into()))
}
///```text
@ -2066,7 +2064,7 @@ pub mod encodings {
input,
list.iter()
.fold(SmallVec::with_capacity(list_len), |mut acc, x| {
acc.extend(x.iter().cloned());
acc.extend(x.into_iter().cloned());
acc
}),
))
@ -2115,7 +2113,7 @@ pub mod encodings {
}
let end = input[ptr..].find(b"=?");
let end = end.unwrap_or(input.len() - ptr) + ptr;
let end = end.unwrap_or_else(|| input.len() - ptr) + ptr;
let ascii_s = ptr;
let mut ascii_e = 0;
@ -2380,7 +2378,7 @@ pub mod address {
///`name-addr = [display-name] angle-addr`
pub fn name_addr(input: &[u8]) -> IResult<&[u8], Address> {
let (input, (display_name, angle_addr)) = alt((
pair(map(display_name, Some), angle_addr),
pair(map(display_name, |s| Some(s)), angle_addr),
map(angle_addr, |r| (None, r)),
))(input)?;
Ok((
@ -2477,7 +2475,7 @@ pub mod address {
.trim(),
);
if i != list_len - 1 {
acc.push(' ');
acc.push_str(" ");
i += 1;
}
acc

View File

@ -34,284 +34,6 @@ use std::sync::Arc;
pub type Result<T> = result::Result<T, MeliError>;
#[derive(Debug, Copy, PartialEq, Clone)]
pub enum NetworkErrorKind {
/// Unspecified
None,
/// Name lookup of host failed.
HostLookupFailed,
/// Bad client Certificate
BadClientCertificate,
/// Bad server certificate
BadServerCertificate,
/// Client initialization
ClientInitialization,
/// Connection failed
ConnectionFailed,
/// Invalid content encoding
InvalidContentEncoding,
/// Invalid credentials
InvalidCredentials,
/// Invalid request
InvalidRequest,
/// IO Error
Io,
/// Name resolution
NameResolution,
/// Protocol violation
ProtocolViolation,
/// Request body not rewindable
RequestBodyNotRewindable,
/// Connection (not request) timeout.
Timeout,
/// TooManyRedirects
TooManyRedirects,
/// Invalid TLS connection
InvalidTLSConnection,
/// Equivalent to HTTP status code 400 Bad Request
/// [[RFC7231, Section 6.5.1](https://tools.ietf.org/html/rfc7231#section-6.5.1)]
BadRequest,
/// Equivalent to HTTP status code 401 Unauthorized
/// [[RFC7235, Section 3.1](https://tools.ietf.org/html/rfc7235#section-3.1)]
Unauthorized,
/// Equivalent to HTTP status code 402 Payment Required
/// [[RFC7231, Section 6.5.2](https://tools.ietf.org/html/rfc7231#section-6.5.2)]
PaymentRequired,
/// Equivalent to HTTP status code 403 Forbidden
/// [[RFC7231, Section 6.5.3](https://tools.ietf.org/html/rfc7231#section-6.5.3)]
Forbidden,
/// Equivalent to HTTP status code 404 Not Found
/// [[RFC7231, Section 6.5.4](https://tools.ietf.org/html/rfc7231#section-6.5.4)]
NotFound,
/// Equivalent to HTTP status code 405 Method Not Allowed
/// [[RFC7231, Section 6.5.5](https://tools.ietf.org/html/rfc7231#section-6.5.5)]
MethodNotAllowed,
/// Equivalent to HTTP status code 406 Not Acceptable
/// [[RFC7231, Section 6.5.6](https://tools.ietf.org/html/rfc7231#section-6.5.6)]
NotAcceptable,
/// Equivalent to HTTP status code 407 Proxy Authentication Required
/// [[RFC7235, Section 3.2](https://tools.ietf.org/html/rfc7235#section-3.2)]
ProxyAuthenticationRequired,
/// Equivalent to HTTP status code 408 Request Timeout
/// [[RFC7231, Section 6.5.7](https://tools.ietf.org/html/rfc7231#section-6.5.7)]
RequestTimeout,
/// Equivalent to HTTP status code 409 Conflict
/// [[RFC7231, Section 6.5.8](https://tools.ietf.org/html/rfc7231#section-6.5.8)]
Conflict,
/// Equivalent to HTTP status code 410 Gone
/// [[RFC7231, Section 6.5.9](https://tools.ietf.org/html/rfc7231#section-6.5.9)]
Gone,
/// Equivalent to HTTP status code 411 Length Required
/// [[RFC7231, Section 6.5.10](https://tools.ietf.org/html/rfc7231#section-6.5.10)]
LengthRequired,
/// Equivalent to HTTP status code 412 Precondition Failed
/// [[RFC7232, Section 4.2](https://tools.ietf.org/html/rfc7232#section-4.2)]
PreconditionFailed,
/// Equivalent to HTTP status code 413 Payload Too Large
/// [[RFC7231, Section 6.5.11](https://tools.ietf.org/html/rfc7231#section-6.5.11)]
PayloadTooLarge,
/// Equivalent to HTTP status code 414 URI Too Long
/// [[RFC7231, Section 6.5.12](https://tools.ietf.org/html/rfc7231#section-6.5.12)]
URITooLong,
/// Equivalent to HTTP status code 415 Unsupported Media Type
/// [[RFC7231, Section 6.5.13](https://tools.ietf.org/html/rfc7231#section-6.5.13)]
UnsupportedMediaType,
/// Equivalent to HTTP status code 416 Range Not Satisfiable
/// [[RFC7233, Section 4.4](https://tools.ietf.org/html/rfc7233#section-4.4)]
RangeNotSatisfiable,
/// Equivalent to HTTP status code 417 Expectation Failed
/// [[RFC7231, Section 6.5.14](https://tools.ietf.org/html/rfc7231#section-6.5.14)]
ExpectationFailed,
/// Equivalent to HTTP status code 421 Misdirected Request
/// [RFC7540, Section 9.1.2](http://tools.ietf.org/html/rfc7540#section-9.1.2)
MisdirectedRequest,
/// Equivalent to HTTP status code 422 Unprocessable Entity
/// [[RFC4918](https://tools.ietf.org/html/rfc4918)]
UnprocessableEntity,
/// Equivalent to HTTP status code 423 Locked
/// [[RFC4918](https://tools.ietf.org/html/rfc4918)]
Locked,
/// Equivalent to HTTP status code 424 Failed Dependency
/// [[RFC4918](https://tools.ietf.org/html/rfc4918)]
FailedDependency,
/// Equivalent to HTTP status code 426 Upgrade Required
/// [[RFC7231, Section 6.5.15](https://tools.ietf.org/html/rfc7231#section-6.5.15)]
UpgradeRequired,
/// Equivalent to HTTP status code 428 Precondition Required
/// [[RFC6585](https://tools.ietf.org/html/rfc6585)]
PreconditionRequired,
/// Equivalent to HTTP status code 429 Too Many Requests
/// [[RFC6585](https://tools.ietf.org/html/rfc6585)]
TooManyRequests,
/// Equivalent to HTTP status code 431 Request Header Fields Too Large
/// [[RFC6585](https://tools.ietf.org/html/rfc6585)]
RequestHeaderFieldsTooLarge,
/// Equivalent to HTTP status code 451 Unavailable For Legal Reasons
/// [[RFC7725](http://tools.ietf.org/html/rfc7725)]
UnavailableForLegalReasons,
/// Equivalent to HTTP status code 500 Internal Server Error
/// [[RFC7231, Section 6.6.1](https://tools.ietf.org/html/rfc7231#section-6.6.1)]
InternalServerError,
/// Equivalent to HTTP status code 501 Not Implemented
/// [[RFC7231, Section 6.6.2](https://tools.ietf.org/html/rfc7231#section-6.6.2)]
NotImplemented,
/// Equivalent to HTTP status code 502 Bad Gateway
/// [[RFC7231, Section 6.6.3](https://tools.ietf.org/html/rfc7231#section-6.6.3)]
BadGateway,
/// Equivalent to HTTP status code 503 Service Unavailable
/// [[RFC7231, Section 6.6.4](https://tools.ietf.org/html/rfc7231#section-6.6.4)]
ServiceUnavailable,
/// Equivalent to HTTP status code 504 Gateway Timeout
/// [[RFC7231, Section 6.6.5](https://tools.ietf.org/html/rfc7231#section-6.6.5)]
GatewayTimeout,
/// Equivalent to HTTP status code 505 HTTP Version Not Supported
/// [[RFC7231, Section 6.6.6](https://tools.ietf.org/html/rfc7231#section-6.6.6)]
HTTPVersionNotSupported,
/// Equivalent to HTTP status code 506 Variant Also Negotiates
/// [[RFC2295](https://tools.ietf.org/html/rfc2295)]
VariantAlsoNegotiates,
/// Equivalent to HTTP status code 507 Insufficient Storage
/// [[RFC4918](https://tools.ietf.org/html/rfc4918)]
InsufficientStorage,
/// Equivalent to HTTP status code 508 Loop Detected
/// [[RFC5842](https://tools.ietf.org/html/rfc5842)]
LoopDetected,
/// Equivalent to HTTP status code 510 Not Extended
/// [[RFC2774](https://tools.ietf.org/html/rfc2774)]
NotExtended,
/// Equivalent to HTTP status code 511 Network Authentication Required
/// [[RFC6585](https://tools.ietf.org/html/rfc6585)]
NetworkAuthenticationRequired,
}
impl NetworkErrorKind {
pub fn as_str(&self) -> &'static str {
use NetworkErrorKind::*;
match self {
None => "Network",
HostLookupFailed => "Name lookup of host failed.",
BadClientCertificate => "Bad client Certificate",
BadServerCertificate => "Bad server certificate",
ClientInitialization => "Client initialization",
ConnectionFailed => "Connection failed",
InvalidContentEncoding => "Invalid content encoding",
InvalidCredentials => "Invalid credentials",
InvalidRequest => "Invalid request",
Io => "IO Error",
NameResolution => "Name resolution",
ProtocolViolation => "Protocol violation",
RequestBodyNotRewindable => "Request body not rewindable",
Timeout => "Connection (not request) timeout.",
TooManyRedirects => "TooManyRedirects",
InvalidTLSConnection => "Invalid TLS connection",
BadRequest => "Bad Request",
Unauthorized => "Unauthorized",
PaymentRequired => "Payment Required",
Forbidden => "Forbidden",
NotFound => "Not Found",
MethodNotAllowed => "Method Not Allowed",
NotAcceptable => "Not Acceptable",
ProxyAuthenticationRequired => "Proxy Authentication Required",
RequestTimeout => "Request Timeout",
Conflict => "Conflict",
Gone => "Gone",
LengthRequired => "Length Required",
PreconditionFailed => "Precondition Failed",
PayloadTooLarge => "Payload Too Large",
URITooLong => "URI Too Long",
UnsupportedMediaType => "Unsupported Media Type",
RangeNotSatisfiable => "Range Not Satisfiable",
ExpectationFailed => "Expectation Failed",
MisdirectedRequest => "Misdirected Request",
UnprocessableEntity => "Unprocessable Entity",
Locked => "Locked",
FailedDependency => "Failed Dependency",
UpgradeRequired => "Upgrade Required",
PreconditionRequired => "Precondition Required",
TooManyRequests => "Too Many Requests",
RequestHeaderFieldsTooLarge => "Request Header Fields Too Large",
UnavailableForLegalReasons => "Unavailable For Legal Reasons",
InternalServerError => "Internal Server Error",
NotImplemented => "Not Implemented",
BadGateway => "Bad Gateway",
ServiceUnavailable => "Service Unavailable",
GatewayTimeout => "Gateway Timeout",
HTTPVersionNotSupported => "HTTP Version Not Supported",
VariantAlsoNegotiates => "Variant Also Negotiates",
InsufficientStorage => "Insufficient Storage",
LoopDetected => "Loop Detected",
NotExtended => "Not Extended",
NetworkAuthenticationRequired => "Network Authentication Required",
}
}
}
impl Default for NetworkErrorKind {
fn default() -> Self {
Self::None
}
}
#[cfg(feature = "http")]
impl From<isahc::http::StatusCode> for NetworkErrorKind {
fn from(val: isahc::http::StatusCode) -> Self {
match val {
isahc::http::StatusCode::BAD_REQUEST => Self::BadRequest,
isahc::http::StatusCode::UNAUTHORIZED => Self::Unauthorized,
isahc::http::StatusCode::PAYMENT_REQUIRED => Self::PaymentRequired,
isahc::http::StatusCode::FORBIDDEN => Self::Forbidden,
isahc::http::StatusCode::NOT_FOUND => Self::NotFound,
isahc::http::StatusCode::METHOD_NOT_ALLOWED => Self::MethodNotAllowed,
isahc::http::StatusCode::NOT_ACCEPTABLE => Self::NotAcceptable,
isahc::http::StatusCode::PROXY_AUTHENTICATION_REQUIRED => {
Self::ProxyAuthenticationRequired
}
isahc::http::StatusCode::REQUEST_TIMEOUT => Self::RequestTimeout,
isahc::http::StatusCode::CONFLICT => Self::Conflict,
isahc::http::StatusCode::GONE => Self::Gone,
isahc::http::StatusCode::LENGTH_REQUIRED => Self::LengthRequired,
isahc::http::StatusCode::PRECONDITION_FAILED => Self::PreconditionFailed,
isahc::http::StatusCode::PAYLOAD_TOO_LARGE => Self::PayloadTooLarge,
isahc::http::StatusCode::URI_TOO_LONG => Self::URITooLong,
isahc::http::StatusCode::UNSUPPORTED_MEDIA_TYPE => Self::UnsupportedMediaType,
isahc::http::StatusCode::RANGE_NOT_SATISFIABLE => Self::RangeNotSatisfiable,
isahc::http::StatusCode::EXPECTATION_FAILED => Self::ExpectationFailed,
isahc::http::StatusCode::MISDIRECTED_REQUEST => Self::MisdirectedRequest,
isahc::http::StatusCode::UNPROCESSABLE_ENTITY => Self::UnprocessableEntity,
isahc::http::StatusCode::LOCKED => Self::Locked,
isahc::http::StatusCode::FAILED_DEPENDENCY => Self::FailedDependency,
isahc::http::StatusCode::UPGRADE_REQUIRED => Self::UpgradeRequired,
isahc::http::StatusCode::PRECONDITION_REQUIRED => Self::PreconditionRequired,
isahc::http::StatusCode::TOO_MANY_REQUESTS => Self::TooManyRequests,
isahc::http::StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE => {
Self::RequestHeaderFieldsTooLarge
}
isahc::http::StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS => {
Self::UnavailableForLegalReasons
}
isahc::http::StatusCode::INTERNAL_SERVER_ERROR => Self::InternalServerError,
isahc::http::StatusCode::NOT_IMPLEMENTED => Self::NotImplemented,
isahc::http::StatusCode::BAD_GATEWAY => Self::BadGateway,
isahc::http::StatusCode::SERVICE_UNAVAILABLE => Self::ServiceUnavailable,
isahc::http::StatusCode::GATEWAY_TIMEOUT => Self::GatewayTimeout,
isahc::http::StatusCode::HTTP_VERSION_NOT_SUPPORTED => Self::HTTPVersionNotSupported,
isahc::http::StatusCode::VARIANT_ALSO_NEGOTIATES => Self::VariantAlsoNegotiates,
isahc::http::StatusCode::INSUFFICIENT_STORAGE => Self::InsufficientStorage,
isahc::http::StatusCode::LOOP_DETECTED => Self::LoopDetected,
isahc::http::StatusCode::NOT_EXTENDED => Self::NotExtended,
isahc::http::StatusCode::NETWORK_AUTHENTICATION_REQUIRED => {
Self::NetworkAuthenticationRequired
}
_ => Self::default(),
}
}
}
#[derive(Debug, Copy, PartialEq, Clone)]
pub enum ErrorKind {
None,
@ -319,7 +41,7 @@ pub enum ErrorKind {
Authentication,
Configuration,
Bug,
Network(NetworkErrorKind),
Network,
Timeout,
OSError,
NotImplemented,
@ -336,7 +58,7 @@ impl fmt::Display for ErrorKind {
ErrorKind::External => "External",
ErrorKind::Authentication => "Authentication",
ErrorKind::Bug => "Bug, please report this!",
ErrorKind::Network(ref inner) => inner.as_str(),
ErrorKind::Network => "Network",
ErrorKind::Timeout => "Timeout",
ErrorKind::OSError => "OS Error",
ErrorKind::Configuration => "Configuration",
@ -349,7 +71,7 @@ impl fmt::Display for ErrorKind {
impl ErrorKind {
pub fn is_network(&self) -> bool {
matches!(self, ErrorKind::Network(_))
matches!(self, ErrorKind::Network)
}
pub fn is_timeout(&self) -> bool {
@ -484,7 +206,7 @@ impl MeliError {
impl fmt::Display for MeliError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "{}", self.summary)?;
writeln!(f, "Summary: {}", self.summary)?;
if let Some(details) = self.details.as_ref() {
write!(f, "{}", details)?;
}
@ -557,7 +279,7 @@ impl<T: Sync + Send + 'static + core::fmt::Debug> From<native_tls::HandshakeErro
fn from(kind: native_tls::HandshakeError<T>) -> MeliError {
MeliError::new(kind.to_string())
.set_source(Some(Arc::new(kind)))
.set_kind(ErrorKind::Network(NetworkErrorKind::InvalidTLSConnection))
.set_kind(ErrorKind::Network)
}
}
@ -567,7 +289,7 @@ impl From<native_tls::Error> for MeliError {
fn from(kind: native_tls::Error) -> MeliError {
MeliError::new(kind.to_string())
.set_source(Some(Arc::new(kind)))
.set_kind(ErrorKind::Network(NetworkErrorKind::InvalidTLSConnection))
.set_kind(ErrorKind::Network)
}
}
@ -578,46 +300,11 @@ impl From<std::num::ParseIntError> for MeliError {
}
}
#[cfg(feature = "http")]
impl From<&isahc::error::ErrorKind> for NetworkErrorKind {
#[inline]
fn from(val: &isahc::error::ErrorKind) -> NetworkErrorKind {
use isahc::error::ErrorKind::*;
match val {
BadClientCertificate => NetworkErrorKind::BadClientCertificate,
BadServerCertificate => NetworkErrorKind::BadServerCertificate,
ClientInitialization => NetworkErrorKind::ClientInitialization,
ConnectionFailed => NetworkErrorKind::ConnectionFailed,
InvalidContentEncoding => NetworkErrorKind::InvalidContentEncoding,
InvalidCredentials => NetworkErrorKind::InvalidCredentials,
InvalidRequest => NetworkErrorKind::BadRequest,
Io => NetworkErrorKind::Io,
NameResolution => NetworkErrorKind::HostLookupFailed,
ProtocolViolation => NetworkErrorKind::ProtocolViolation,
RequestBodyNotRewindable => NetworkErrorKind::RequestBodyNotRewindable,
Timeout => NetworkErrorKind::Timeout,
TlsEngine => NetworkErrorKind::InvalidTLSConnection,
TooManyRedirects => NetworkErrorKind::TooManyRedirects,
_ => NetworkErrorKind::None,
}
}
}
impl From<NetworkErrorKind> for ErrorKind {
#[inline]
fn from(kind: NetworkErrorKind) -> ErrorKind {
ErrorKind::Network(kind)
}
}
#[cfg(feature = "http")]
#[cfg(feature = "jmap_backend")]
impl From<isahc::Error> for MeliError {
#[inline]
fn from(val: isahc::Error) -> MeliError {
let kind: NetworkErrorKind = val.kind().into();
MeliError::new(val.to_string())
.set_source(Some(Arc::new(val)))
.set_kind(ErrorKind::Network(kind))
fn from(kind: isahc::Error) -> MeliError {
MeliError::new(kind.to_string()).set_source(Some(Arc::new(kind)))
}
}

File diff suppressed because it is too large Load Diff

View File

@ -111,7 +111,7 @@ impl Read for Data {
if result >= 0 {
Ok(result as usize)
} else {
Err(io::Error::last_os_error())
Err(io::Error::last_os_error().into())
}
}
}
@ -126,7 +126,7 @@ impl Write for Data {
if result >= 0 {
Ok(result as usize)
} else {
Err(io::Error::last_os_error())
Err(io::Error::last_os_error().into())
}
}
@ -149,7 +149,7 @@ impl Seek for Data {
if result >= 0 {
Ok(result as u64)
} else {
Err(io::Error::last_os_error())
Err(io::Error::last_os_error().into())
}
}
}

View File

@ -131,9 +131,9 @@ impl LocateKey {
s if s.eq_ignore_ascii_case("keyserver") => LocateKey::KEYSERVER,
s if s.eq_ignore_ascii_case("keyserver-url") => LocateKey::KEYSERVER_URL,
s if s.eq_ignore_ascii_case("local") => LocateKey::LOCAL,
combination if combination.contains(',') => {
combination if combination.contains(",") => {
let mut ret = LocateKey::NODEFAULT;
for c in combination.trim().split(',') {
for c in combination.trim().split(",") {
ret |= Self::from_string_de::<'de, D, &str>(c.trim())?;
}
ret
@ -157,7 +157,7 @@ impl std::fmt::Display for LocateKey {
($flag:expr, $string:literal) => {{
if self.intersects($flag) {
accum.push_str($string);
accum.push(',');
accum.push_str(",");
}
}};
}
@ -360,7 +360,7 @@ impl Context {
self.set_flag_inner(
auto_key_locate,
CStr::from_bytes_with_nul(accum.as_bytes())
.map_err(|err| format!("Expected `{}`: {}", accum.as_str(), err))?
.expect(accum.as_str())
.as_ptr() as *const _,
)
}
@ -372,7 +372,7 @@ impl Context {
unsafe { CStr::from_ptr(self.get_flag_inner(auto_key_locate)) }.to_string_lossy();
let mut val = LocateKey::NODEFAULT;
if !raw_value.contains("nodefault") {
for mechanism in raw_value.split(',') {
for mechanism in raw_value.split(",") {
match mechanism {
"cert" => val.set(LocateKey::CERT, true),
"pka" => {
@ -538,7 +538,7 @@ impl Context {
};
let _ = rcv.recv().await;
{
let verify_result: gpgme_verify_result_t =
let verify_result =
unsafe { call!(&ctx.lib, gpgme_op_verify_result)(ctx.inner.as_ptr()) };
if verify_result.is_null() {
return Err(MeliError::new(
@ -546,7 +546,7 @@ impl Context {
)
.set_err_kind(ErrorKind::External));
}
unsafe { call!(&ctx.lib, gpgme_free)(verify_result as *mut ::libc::c_void) };
drop(verify_result);
}
let io_state_lck = io_state.lock().unwrap();
let ret = io_state_lck
@ -792,7 +792,7 @@ impl Context {
.chain_err_summary(|| {
"libgpgme error: could not perform seek on signature data object"
})?;
sig.into_bytes()
Ok(sig.into_bytes()?)
})
}
@ -1118,7 +1118,7 @@ impl Context {
cipher
.seek(std::io::SeekFrom::Start(0))
.chain_err_summary(|| "libgpgme error: could not perform seek on plain text")?;
cipher.into_bytes()
Ok(cipher.into_bytes()?)
})
}
}

View File

@ -41,8 +41,8 @@ pub mod dbg {
() => {
eprint!(
"[{}][{:?}] {}:{}_{}: ",
$crate::datetime::timestamp_to_string(
$crate::datetime::now(),
crate::datetime::timestamp_to_string(
crate::datetime::now(),
Some("%Y-%m-%d %T"),
false
),
@ -340,7 +340,7 @@ pub mod shellexpand {
.components()
.last()
.map(|c| c.as_os_str())
.unwrap_or_else(|| OsStr::from_bytes(b""));
.unwrap_or(OsStr::from_bytes(b""));
let prefix = if let Some(p) = self.parent() {
p
} else {
@ -354,33 +354,37 @@ pub mod shellexpand {
}
if let Ok(iter) = std::fs::read_dir(&prefix) {
for entry in iter.flatten() {
if entry.path().as_os_str().as_bytes() != b"."
&& entry.path().as_os_str().as_bytes() != b".."
&& entry
.path()
.as_os_str()
.as_bytes()
.starts_with(_match.as_bytes())
{
if entry.path().is_dir()
&& !entry.path().as_os_str().as_bytes().ends_with(b"/")
for entry in iter {
if let Ok(entry) = entry {
if entry.path().as_os_str().as_bytes() != b"."
&& entry.path().as_os_str().as_bytes() != b".."
&& entry
.path()
.as_os_str()
.as_bytes()
.starts_with(_match.as_bytes())
{
let mut s = unsafe {
String::from_utf8_unchecked(
entry.path().as_os_str().as_bytes()[_match.as_bytes().len()..]
.to_vec(),
)
};
s.push('/');
entries.push(s);
} else {
entries.push(unsafe {
String::from_utf8_unchecked(
entry.path().as_os_str().as_bytes()[_match.as_bytes().len()..]
.to_vec(),
)
});
if entry.path().is_dir()
&& !entry.path().as_os_str().as_bytes().ends_with(b"/")
{
let mut s = unsafe {
String::from_utf8_unchecked(
entry.path().as_os_str().as_bytes()
[_match.as_bytes().len()..]
.to_vec(),
)
};
s.push('/');
entries.push(s);
} else {
entries.push(unsafe {
String::from_utf8_unchecked(
entry.path().as_os_str().as_bytes()
[_match.as_bytes().len()..]
.to_vec(),
)
});
}
}
}
}

View File

@ -93,22 +93,6 @@ where
}
}
pub fn map_res<'a, P, F, E, A, B>(parser: P, map_fn: F) -> impl Parser<'a, B>
where
P: Parser<'a, A>,
F: Fn(A) -> std::result::Result<B, E>,
{
move |input| {
parser.parse(input).and_then(|(next_input, result)| {
if let Ok(res) = map_fn(result) {
Ok((next_input, res))
} else {
Err(next_input)
}
})
}
}
pub fn match_literal<'a>(expected: &'static str) -> impl Parser<'a, ()> {
move |input: &'a str| match input.get(0..expected.len()) {
Some(next) if next == expected => Ok((&input[expected.len()..], ())),
@ -194,24 +178,6 @@ pub fn quoted_string<'a>() -> impl Parser<'a, String> {
)
}
pub fn quoted_slice<'a>() -> impl Parser<'a, &'a str> {
move |input: &'a str| {
if input.is_empty() || !input.starts_with('"') {
return Err(input);
}
let mut i = 1;
while i < input.len() {
if input[i..].starts_with('\"') && !input[i - 1..].starts_with('\\') {
return Ok((&input[i + 1..], &input[1..i]));
}
i += 1;
}
Err(input)
}
}
pub struct BoxedParser<'a, Output> {
parser: Box<dyn Parser<'a, Output> + 'a>,
}
@ -251,8 +217,6 @@ where
right(space0(), left(parser, space0()))
}
pub use whitespace_wrap as ws_eat;
pub fn pair<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, (R1, R2)>
where
P1: Parser<'a, R1>,
@ -326,69 +290,6 @@ pub fn space0<'a>() -> impl Parser<'a, Vec<char>> {
zero_or_more(whitespace_char())
}
pub fn is_a<'a>(slice: &'static [u8]) -> impl Parser<'a, &'a str> {
move |input: &'a str| {
let mut i = 0;
for byte in input.as_bytes().iter() {
if !slice.contains(byte) {
break;
}
i += 1;
}
if i == 0 {
return Err("");
}
let (b, a) = input.split_at(i);
Ok((a, b))
}
}
/// Try alternative parsers in order until one succeeds.
///
/// ```rust
/// # use melib::parsec::{Parser, quoted_slice, match_literal, alt, delimited, prefix};
///
/// let parser = |input| {
/// alt([
/// delimited(
/// match_literal("{"),
/// quoted_slice(),
/// match_literal("}"),
/// ),
/// delimited(
/// match_literal("["),
/// quoted_slice(),
/// match_literal("]"),
/// ),
/// ]).parse(input)
/// };
///
/// let input1: &str = "{\"quoted\"}";
/// let input2: &str = "[\"quoted\"]";
/// assert_eq!(
/// Ok(("", "quoted")),
/// parser.parse(input1)
/// );
///
/// assert_eq!(
/// Ok(("", "quoted")),
/// parser.parse(input2)
/// );
/// ```
pub fn alt<'a, P, A, const N: usize>(parsers: [P; N]) -> impl Parser<'a, A>
where
P: Parser<'a, A>,
{
move |input| {
for parser in parsers.iter() {
if let Ok(res) = parser.parse(input) {
return Ok(res);
}
}
Err(input)
}
}
pub fn and_then<'a, P, F, A, B, NextP>(parser: P, f: F) -> impl Parser<'a, B>
where
P: Parser<'a, A>,
@ -444,198 +345,3 @@ where
Ok((&input[offset..], input))
}
}
pub fn separated_list0<'a, P, A, S, Sep>(
parser: P,
separator: S,
terminated: bool,
) -> impl Parser<'a, Vec<A>>
where
P: Parser<'a, A>,
S: Parser<'a, Sep>,
{
move |mut input| {
let mut result = Vec::new();
let mut prev_sep_result = Ok(());
let mut last_item_input = input;
while let Ok((next_input, next_item)) = parser.parse(input) {
prev_sep_result?;
input = next_input;
last_item_input = next_input;
result.push(next_item);
match separator.parse(input) {
Ok((next_input, _)) => {
input = next_input;
}
Err(err) => {
prev_sep_result = Err(err);
}
}
}
if !terminated {
input = last_item_input;
}
Ok((input, result))
}
}
/// Take `count` bytes
pub fn take<'a>(count: usize) -> impl Parser<'a, &'a str> {
move |i: &'a str| {
if i.len() < count || !i.is_char_boundary(count) {
Err("")
} else {
let (b, a) = i.split_at(count);
Ok((a, b))
}
}
}
/// Take a literal
///
///```rust
/// # use std::str::FromStr;
/// # use melib::parsec::{Parser, delimited, match_literal, map_res, is_a, take_literal};
/// let lit: &str = "{31}\r\nThere is no script by that name\r\n";
/// assert_eq!(
/// take_literal(delimited(
/// match_literal("{"),
/// map_res(is_a(b"0123456789"), |s| usize::from_str(s)),
/// match_literal("}\r\n"),
/// ))
/// .parse(lit),
/// Ok((
/// "\r\n",
/// "There is no script by that name",
/// ))
/// );
///```
pub fn take_literal<'a, P>(parser: P) -> impl Parser<'a, &'a str>
where
P: Parser<'a, usize>,
{
move |input: &'a str| {
let (rest, length) = parser.parse(input)?;
take(length).parse(rest)
}
}
#[cfg(test)]
mod test {
use super::*;
use std::collections::HashMap;
#[test]
fn test_parsec() {
#[derive(Debug, PartialEq)]
enum JsonValue {
JsonString(String),
JsonNumber(f64),
JsonBool(bool),
JsonNull,
JsonObject(HashMap<String, JsonValue>),
JsonArray(Vec<JsonValue>),
}
fn parse_value<'a>() -> impl Parser<'a, JsonValue> {
move |input| {
either(
either(
either(
either(
either(
map(parse_bool(), |b| JsonValue::JsonBool(b)),
map(parse_null(), |()| JsonValue::JsonNull),
),
map(parse_array(), |vec| JsonValue::JsonArray(vec)),
),
map(parse_object(), |obj| JsonValue::JsonObject(obj)),
),
map(parse_number(), |n| JsonValue::JsonNumber(n)),
),
map(quoted_string(), |s| JsonValue::JsonString(s)),
)
.parse(input)
}
}
fn parse_number<'a>() -> impl Parser<'a, f64> {
move |input| {
either(
map(match_literal("TRUE"), |()| 1.0),
map(match_literal("FALSe"), |()| 1.0),
)
.parse(input)
}
}
fn parse_bool<'a>() -> impl Parser<'a, bool> {
move |input| {
ws_eat(either(
map(match_literal("true"), |()| true),
map(match_literal("false"), |()| false),
))
.parse(input)
}
}
fn parse_null<'a>() -> impl Parser<'a, ()> {
move |input| ws_eat(match_literal("null")).parse(input)
}
fn parse_array<'a>() -> impl Parser<'a, Vec<JsonValue>> {
move |input| {
delimited(
ws_eat(match_literal("[")),
separated_list0(parse_value(), ws_eat(match_literal(",")), false),
ws_eat(match_literal("]")),
)
.parse(input)
}
}
fn parse_object<'a>() -> impl Parser<'a, HashMap<String, JsonValue>> {
move |input| {
map(
delimited(
ws_eat(match_literal("{")),
separated_list0(
pair(
suffix(quoted_string(), ws_eat(match_literal(":"))),
parse_value(),
),
ws_eat(match_literal(",")),
false,
),
ws_eat(match_literal("}")),
),
|vec: Vec<(String, JsonValue)>| vec.into_iter().collect(),
)
.parse(input)
}
}
assert_eq!(
Ok(("", JsonValue::JsonString("a".to_string()))),
parse_value().parse(r#""a""#)
);
assert_eq!(
Ok(("", JsonValue::JsonBool(true))),
parse_value().parse(r#"true"#)
);
assert_eq!(
Ok(("", JsonValue::JsonObject(HashMap::default()))),
parse_value().parse(r#"{}"#)
);
println!("{:?}", parse_value().parse(r#"{"a":true}"#));
println!("{:?}", parse_value().parse(r#"{"a":true,"b":false}"#));
println!("{:?}", parse_value().parse(r#"{ "a" : true,"b": false }"#));
println!("{:?}", parse_value().parse(r#"{ "a" : true,"b": false,}"#));
println!("{:?}", parse_value().parse(r#"{"a":false,"b":false,}"#));
// Line:0 Col:18 Error parsing object
// { "a":1, "b" : 2, }
// ^Unexpected ','
}
}

View File

@ -25,9 +25,6 @@
/*!
* SMTP client support
*
* This module implements a client for the SMTP protocol as specified by [RFC 5321 Simple Mail
* Transfer Protocol](https://www.rfc-editor.org/rfc/rfc5321).
*
* The connection and methods are `async` and uses the `smol` runtime.
*# Example
*
@ -158,11 +155,11 @@ pub struct SmtpAuthType {
login: bool,
}
const fn true_val() -> bool {
fn true_val() -> bool {
true
}
const fn false_val() -> bool {
fn false_val() -> bool {
false
}
@ -198,7 +195,6 @@ pub struct SmtpExtensionSupport {
pipelining: bool,
#[serde(default = "crate::conf::true_val")]
chunking: bool,
/// [RFC 6152: SMTP Service Extension for 8-bit MIME Transport](https://www.rfc-editor.org/rfc/rfc6152)
#[serde(default = "crate::conf::true_val")]
_8bitmime: bool,
//Essentially, the PRDR extension to SMTP allows (but does not require) an SMTP server to
@ -268,13 +264,16 @@ impl SmtpConnection {
if danger_accept_invalid_certs {
connector.danger_accept_invalid_certs(true);
}
let connector = connector.build()?;
let connector = connector
.build()
.chain_err_kind(crate::error::ErrorKind::Network)?;
let addr = lookup_ipv4(path, server_conf.port)?;
let mut socket = AsyncWrapper::new(Connection::Tcp(TcpStream::connect_timeout(
&addr,
std::time::Duration::new(4, 0),
)?))?;
let mut socket = AsyncWrapper::new(Connection::Tcp(
TcpStream::connect_timeout(&addr, std::time::Duration::new(4, 0))
.chain_err_kind(crate::error::ErrorKind::Network)?,
))
.chain_err_kind(crate::error::ErrorKind::Network)?;
let pre_ehlo_extensions_reply = read_lines(
&mut socket,
&mut res,
@ -297,7 +296,10 @@ impl SmtpConnection {
return Err(MeliError::new("Please specify what SMTP security transport to use explicitly instead of `auto`."));
}
}
socket.write_all(b"EHLO meli.delivery\r\n").await?;
socket
.write_all(b"EHLO meli.delivery\r\n")
.await
.chain_err_kind(crate::error::ErrorKind::Network)?;
if let SmtpSecurity::StartTLS { .. } = server_conf.security {
let pre_tls_extensions_reply = read_lines(
&mut socket,
@ -308,7 +310,10 @@ impl SmtpConnection {
.await?;
drop(pre_tls_extensions_reply);
//debug!(pre_tls_extensions_reply);
socket.write_all(b"STARTTLS\r\n").await?;
socket
.write_all(b"STARTTLS\r\n")
.await
.chain_err_kind(crate::error::ErrorKind::Network)?;
let _post_starttls_extensions_reply = read_lines(
&mut socket,
&mut res,
@ -320,11 +325,15 @@ impl SmtpConnection {
}
let mut ret = {
let socket = socket.into_inner()?;
let socket = socket
.into_inner()
.chain_err_kind(crate::error::ErrorKind::Network)?;
let _path = path.clone();
socket.set_nonblocking(false)?;
let conn = unblock(move || connector.connect(&_path, socket)).await?;
let conn = unblock(move || connector.connect(&_path, socket))
.await
.chain_err_kind(crate::error::ErrorKind::Network)?;
/*
if let Err(native_tls::HandshakeError::WouldBlock(midhandshake_stream)) =
conn_result
@ -346,17 +355,21 @@ impl SmtpConnection {
}
}
*/
AsyncWrapper::new(Connection::Tls(conn))?
AsyncWrapper::new(Connection::Tls(conn))
.chain_err_kind(crate::error::ErrorKind::Network)?
};
ret.write_all(b"EHLO meli.delivery\r\n").await?;
ret.write_all(b"EHLO meli.delivery\r\n")
.await
.chain_err_kind(crate::error::ErrorKind::Network)?;
ret
}
SmtpSecurity::None => {
let addr = lookup_ipv4(path, server_conf.port)?;
let mut ret = AsyncWrapper::new(Connection::Tcp(TcpStream::connect_timeout(
&addr,
std::time::Duration::new(4, 0),
)?))?;
let mut ret = AsyncWrapper::new(Connection::Tcp(
TcpStream::connect_timeout(&addr, std::time::Duration::new(4, 0))
.chain_err_kind(crate::error::ErrorKind::Network)?,
))
.chain_err_kind(crate::error::ErrorKind::Network)?;
res.clear();
let reply = read_lines(
&mut ret,
@ -374,7 +387,9 @@ impl SmtpConnection {
Reply::new(&res, code)
)));
}
ret.write_all(b"EHLO meli.delivery\r\n").await?;
ret.write_all(b"EHLO meli.delivery\r\n")
.await
.chain_err_kind(crate::error::ErrorKind::Network)?;
ret
}
};
@ -408,10 +423,10 @@ impl SmtpConnection {
ref mut auth_type, ..
} = ret.server_conf.auth
{
if let Some(l) = pre_auth_extensions_reply
for l in pre_auth_extensions_reply
.lines
.iter()
.find(|l| l.starts_with("AUTH"))
.filter(|l| l.starts_with("AUTH"))
{
let l = l["AUTH ".len()..].trim();
for _type in l.split_whitespace() {
@ -421,6 +436,7 @@ impl SmtpConnection {
auth_type.login = true;
}
}
break;
}
}
}
@ -582,10 +598,15 @@ impl SmtpConnection {
// .trim()
//);
for c in command {
self.stream.write_all(c).await?;
self.stream
.write_all(c)
.await
.chain_err_kind(crate::error::ErrorKind::Network)?;
}
self.stream.write_all(b"\r\n").await?;
Ok(())
self.stream
.write_all(b"\r\n")
.await
.chain_err_kind(crate::error::ErrorKind::Network)
}
/// Sends mail
@ -640,9 +661,9 @@ impl SmtpConnection {
//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
.iter()
.chain(envelope.cc().iter())
.chain(envelope.bcc().iter())
.into_iter()
.chain(envelope.cc().into_iter())
.chain(envelope.bcc().into_iter())
{
current_command.clear();
current_command.push(b"RCPT TO:<");
@ -673,7 +694,10 @@ impl SmtpConnection {
let mail_length = format!("{}", mail.as_bytes().len());
self.send_command(&[b"BDAT", mail_length.as_bytes(), b"LAST"])
.await?;
self.stream.write_all(mail.as_bytes()).await?;
self.stream
.write_all(mail.as_bytes())
.await
.chain_err_kind(crate::error::ErrorKind::Network)?;
} else {
//The third step in the procedure is the DATA command
//(or some alternative specified in a service extension).
@ -710,20 +734,35 @@ impl SmtpConnection {
//line.If it is a period, one additional period is inserted at the beginning of the line.
for line in mail.lines() {
if line.starts_with('.') {
self.stream.write_all(b".").await?;
self.stream
.write_all(b".")
.await
.chain_err_kind(crate::error::ErrorKind::Network)?;
}
self.stream.write_all(line.as_bytes()).await?;
self.stream.write_all(b"\r\n").await?;
self.stream
.write_all(line.as_bytes())
.await
.chain_err_kind(crate::error::ErrorKind::Network)?;
self.stream
.write_all(b"\r\n")
.await
.chain_err_kind(crate::error::ErrorKind::Network)?;
}
if !mail.ends_with('\n') {
self.stream.write_all(b".\r\n").await?;
self.stream
.write_all(b".\r\n")
.await
.chain_err_kind(crate::error::ErrorKind::Network)?;
}
//The mail data are terminated by a line containing only a period, that is, the character
//sequence "<CRLF>.<CRLF>", where the first <CRLF> is actually the terminator of the
//previous line (see Section 4.5.2). This is the end of mail data indication.
self.stream.write_all(b".\r\n").await?;
self.stream
.write_all(b".\r\n")
.await
.chain_err_kind(crate::error::ErrorKind::Network)?;
}
//The end of mail data indicator also confirms the mail transaction and tells the SMTP
@ -861,26 +900,11 @@ impl ReplyCode {
fn is_err(&self) -> bool {
use ReplyCode::*;
matches!(
self,
_421 | _450
| _451
| _452
| _455
| _500
| _501
| _502
| _503
| _504
| _535
| _550
| _551
| _552
| _553
| _554
| _555
| _530
)
match self {
_421 | _450 | _451 | _452 | _455 | _500 | _501 | _502 | _503 | _504 | _535 | _550
| _551 | _552 | _553 | _554 | _555 | _530 => true,
_ => false,
}
}
}
@ -999,8 +1023,8 @@ async fn read_lines<'r>(
Ok(b) => {
ret.push_str(unsafe { std::str::from_utf8_unchecked(&buf[0..b]) });
}
Err(err) => {
return Err(MeliError::from(err));
Err(e) => {
return Err(MeliError::from(e).set_kind(crate::error::ErrorKind::Network));
}
}
}
@ -1024,219 +1048,3 @@ async fn read_lines<'r>(
}
Ok(reply)
}
#[cfg(test)]
mod test {
use super::*;
use mailin_embedded::{Handler, Response, Server, SslConfig};
use std::net::IpAddr; //, Ipv4Addr, Ipv6Addr};
use std::sync::{Arc, Mutex};
use std::thread;
const ADDRESS: &str = "127.0.0.1:8825";
#[derive(Debug, Clone)]
enum Message {
Helo,
Mail {
from: String,
},
Rcpt {
from: String,
to: Vec<String>,
},
DataStart {
from: String,
to: Vec<String>,
},
Data {
#[allow(dead_code)]
from: String,
to: Vec<String>,
buf: Vec<u8>,
},
}
#[derive(Debug, Clone)]
struct MyHandler {
mails: Arc<Mutex<Vec<((IpAddr, String), Message)>>>,
stored: Arc<Mutex<Vec<(String, crate::Envelope)>>>,
}
use mailin_embedded::response::{INTERNAL_ERROR, OK};
impl Handler for MyHandler {
fn helo(&mut self, ip: IpAddr, domain: &str) -> Response {
eprintln!("helo ip {:?} domain {:?}", ip, domain);
self.mails
.lock()
.unwrap()
.push(((ip, domain.to_string()), Message::Helo));
OK
}
fn mail(&mut self, ip: IpAddr, domain: &str, from: &str) -> Response {
eprintln!("mail() ip {:?} domain {:?} from {:?}", ip, domain, from);
if let Some((_, message)) = self
.mails
.lock()
.unwrap()
.iter_mut()
.find(|((i, d), _)| (i, d.as_str()) == (&ip, domain))
{
std::dbg!(&message);
if let Message::Helo = message {
*message = Message::Mail {
from: from.to_string(),
};
return OK;
}
}
INTERNAL_ERROR
}
fn rcpt(&mut self, _to: &str) -> Response {
eprintln!("rcpt() to {:?}", _to);
if let Some((_, message)) = self.mails.lock().unwrap().last_mut() {
std::dbg!(&message);
if let Message::Mail { from } = message {
*message = Message::Rcpt {
from: from.clone(),
to: vec![_to.to_string()],
};
return OK;
} else if let Message::Rcpt { to, .. } = message {
to.push(_to.to_string());
return OK;
}
}
INTERNAL_ERROR
}
fn data_start(
&mut self,
_domain: &str,
_from: &str,
_is8bit: bool,
_to: &[String],
) -> Response {
eprintln!(
"data_start() domain {:?} from {:?} is8bit {:?} to {:?}",
_domain, _from, _is8bit, _to
);
if let Some(((_, d), ref mut message)) = self.mails.lock().unwrap().last_mut() {
if d != _domain {
return INTERNAL_ERROR;
}
std::dbg!(&message);
if let Message::Rcpt { from, to } = message {
*message = Message::DataStart {
from: from.to_string(),
to: to.to_vec(),
};
return OK;
}
}
INTERNAL_ERROR
}
fn data(&mut self, _buf: &[u8]) -> std::result::Result<(), std::io::Error> {
if let Some(((_, _), ref mut message)) = self.mails.lock().unwrap().last_mut() {
if let Message::DataStart { from, to } = message {
*message = Message::Data {
from: from.to_string(),
to: to.clone(),
buf: _buf.to_vec(),
};
return Ok(());
} else if let Message::Data { buf, .. } = message {
buf.extend(_buf.into_iter().copied());
return Ok(());
}
}
Ok(())
}
fn data_end(&mut self) -> Response {
eprintln!("datae_nd() ");
if let Some(((_, _), message)) = self.mails.lock().unwrap().pop() {
if let Message::Data { from: _, to, buf } = message {
for to in to {
match crate::Envelope::from_bytes(&buf, None) {
Ok(env) => {
std::dbg!(&env);
std::dbg!(env.other_headers());
self.stored.lock().unwrap().push((to.clone(), env));
}
Err(err) => {
eprintln!("envelope parse error {}", err);
}
}
}
return OK;
}
}
INTERNAL_ERROR
}
}
fn get_smtp_conf() -> SmtpServerConf {
SmtpServerConf {
hostname: "127.0.0.1".into(),
port: 8825,
envelope_from: "foo-chat@example.com".into(),
auth: SmtpAuth::None,
security: SmtpSecurity::None,
extensions: Default::default(),
}
}
#[test]
fn test_smtp() {
stderrlog::new()
.quiet(false)
.verbosity(0)
.show_module_names(true)
.timestamp(stderrlog::Timestamp::Millisecond)
.init()
.unwrap();
let handler = MyHandler {
mails: Arc::new(Mutex::new(vec![])),
stored: Arc::new(Mutex::new(vec![])),
};
let handler2 = handler.clone();
let _smtp_handle = thread::spawn(move || {
let mut server = Server::new(handler2);
server
.with_name("example.com")
.with_ssl(SslConfig::None)
.unwrap()
.with_addr(ADDRESS)
.unwrap();
eprintln!("Running smtp server at {}", ADDRESS);
server.serve().expect("Could not run server");
});
let smtp_server_conf = get_smtp_conf();
let input_str = include_str!("../test_sample_longmessage.eml");
match crate::Envelope::from_bytes(input_str.as_bytes(), None) {
Ok(_envelope) => {}
Err(err) => {
panic!("Could not parse message: {}", err);
}
}
let mut connection =
futures::executor::block_on(SmtpConnection::new_connection(smtp_server_conf)).unwrap();
futures::executor::block_on(connection.mail_transaction(
input_str,
/*tos*/
Some(&[
Address::try_from("foo-chat@example.com").unwrap(),
Address::try_from("webmaster@example.com").unwrap(),
]),
))
.unwrap();
assert_eq!(handler.stored.lock().unwrap().len(), 2);
}
}

View File

@ -34,9 +34,9 @@ pub struct DatabaseDescription {
pub fn db_path(name: &str) -> Result<PathBuf> {
let data_dir =
xdg::BaseDirectories::with_prefix("meli").map_err(|e| MeliError::new(e.to_string()))?;
data_dir
Ok(data_dir
.place_data_file(name)
.map_err(|err| MeliError::new(err.to_string()))
.map_err(|e| MeliError::new(e.to_string()))?)
}
pub fn open_db(db_path: PathBuf) -> Result<Connection> {
@ -149,13 +149,13 @@ impl FromSql for Envelope {
let b: Vec<u8> = FromSql::column_result(value)?;
bincode::Options::deserialize(
Ok(bincode::Options::deserialize(
bincode::Options::with_limit(
bincode::config::DefaultOptions::new(),
2 * u64::try_from(b.len()).map_err(|e| FromSqlError::Other(Box::new(e)))?,
),
&b,
)
.map_err(|e| FromSqlError::Other(Box::new(e)))
.map_err(|e| FromSqlError::Other(Box::new(e)))?)
}
}

View File

@ -972,8 +972,7 @@ mod alg {
);
let x = minima[r - 1 + offset];
let mut for_was_broken = false;
let i_copy = i;
for j in i_copy..(r - 1) {
for j in i..(r - 1) {
let y = cost(j + offset, r - 1 + offset, width, &minima, &offsets);
if y <= x {
n -= j;
@ -1180,8 +1179,8 @@ fn reflow_helper(
let paragraph = paragraph
.trim_start_matches(&quotes)
.replace(&format!("\n{}", &quotes), "")
.replace('\n', "")
.replace('\r', "");
.replace("\n", "")
.replace("\r", "");
if in_paragraph {
if let Some(width) = width {
ret.extend(
@ -1196,7 +1195,7 @@ fn reflow_helper(
ret.push(format!("{}{}", &quotes, &paragraph));
}
} else {
let paragraph = paragraph.replace('\n', "").replace('\r', "");
let paragraph = paragraph.replace("\n", "").replace("\r", "");
if in_paragraph {
if let Some(width) = width {
@ -1574,7 +1573,7 @@ impl Iterator for LineBreakText {
);
self.paragraph = paragraph;
}
self.paragraph.pop_front()
return self.paragraph.pop_front();
}
ReflowState::AllWidth {
width,
@ -1747,7 +1746,7 @@ impl Iterator for LineBreakText {
*cur_index += line.len() + 2;
return Some(ret);
}
None
return None;
}
}
}
@ -1765,8 +1764,8 @@ fn reflow_helper2(
let paragraph = paragraph
.trim_start_matches(&quotes)
.replace(&format!("\n{}", &quotes), "")
.replace('\n', "")
.replace('\r', "");
.replace("\n", "")
.replace("\r", "");
if in_paragraph {
if let Some(width) = width {
ret.extend(
@ -1781,7 +1780,7 @@ fn reflow_helper2(
ret.push_back(format!("{}{}", &quotes, &paragraph));
}
} else {
let paragraph = paragraph.replace('\n', "").replace('\r', "");
let paragraph = paragraph.replace("\n", "").replace("\r", "");
if in_paragraph {
if let Some(width) = width {

View File

@ -77,7 +77,10 @@ impl Truncate for &str {
extern crate unicode_segmentation;
use unicode_segmentation::UnicodeSegmentation;
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(*self, true).nth(skip_len) {
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(*self, true)
.skip(skip_len)
.next()
{
&self[first..]
} else {
self
@ -92,7 +95,10 @@ impl Truncate for &str {
extern crate unicode_segmentation;
use unicode_segmentation::UnicodeSegmentation;
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(*self, true).nth(skip_len) {
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(*self, true)
.skip(skip_len)
.next()
{
*self = &self[first..];
}
}
@ -138,8 +144,9 @@ impl Truncate for String {
extern crate unicode_segmentation;
use unicode_segmentation::UnicodeSegmentation;
if let Some((first, _)) =
UnicodeSegmentation::grapheme_indices(self.as_str(), true).nth(skip_len)
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(self.as_str(), true)
.skip(skip_len)
.next()
{
&self[first..]
} else {
@ -155,8 +162,9 @@ impl Truncate for String {
extern crate unicode_segmentation;
use unicode_segmentation::UnicodeSegmentation;
if let Some((first, _)) =
UnicodeSegmentation::grapheme_indices(self.as_str(), true).nth(skip_len)
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(self.as_str(), true)
.skip(skip_len)
.next()
{
*self = self[first..].to_string();
}

View File

@ -19,7 +19,6 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum LineBreakClass {
BK,

View File

@ -179,9 +179,7 @@ macro_rules! make {
/// use melib::thread::SubjectPrefix;
///
/// let mut subject = "Re: RE: Res: Re: Res: Subject";
/// assert_eq!(subject.strip_prefixes_from_list(<&str>::USUAL_PREFIXES, None), &"Subject");
/// let mut subject = "Re: RE: Res: Re: Res: Subject";
/// assert_eq!(subject.strip_prefixes_from_list(<&str>::USUAL_PREFIXES, Some(1)), &"RE: Res: Re: Res: Subject");
/// assert_eq!(subject.strip_prefixes_from_list(<&str>::USUAL_PREFIXES), &"Subject");
/// ```
pub trait SubjectPrefix {
const USUAL_PREFIXES: &'static [&'static str] = &[
@ -281,7 +279,7 @@ pub trait SubjectPrefix {
];
fn is_a_reply(&self) -> bool;
fn strip_prefixes(&mut self) -> &mut Self;
fn strip_prefixes_from_list(&mut self, list: &[&str], times: Option<u8>) -> &mut Self;
fn strip_prefixes_from_list(&mut self, list: &[&str]) -> &mut Self;
}
impl SubjectPrefix for &[u8] {
@ -337,10 +335,10 @@ impl SubjectPrefix for &[u8] {
self
}
fn strip_prefixes_from_list(&mut self, list: &[&str], mut times: Option<u8>) -> &mut Self {
fn strip_prefixes_from_list(&mut self, list: &[&str]) -> &mut Self {
let result = {
let mut slice = self.trim();
'outer: loop {
loop {
let len = slice.len();
for prefix in list.iter() {
if slice
@ -349,14 +347,10 @@ impl SubjectPrefix for &[u8] {
.unwrap_or(false)
{
slice = &slice[prefix.len()..];
slice = slice.trim();
times = times.map(|u| u.saturating_sub(1));
if times == Some(0) {
break 'outer;
}
}
slice = slice.trim();
}
if slice.len() == len || times == Some(0) {
if slice.len() == len {
break;
}
}
@ -391,15 +385,15 @@ impl SubjectPrefix for &str {
slice = &slice[4..];
continue;
}
if slice.starts_with(' ') || slice.starts_with('\t') || slice.starts_with('\r') {
if slice.starts_with(" ") || slice.starts_with("\t") || slice.starts_with("\r") {
//FIXME just trim whitespace
slice = &slice[1..];
continue;
}
if slice.starts_with('[')
if slice.starts_with("[")
&& !(slice.starts_with("[PATCH") || slice.starts_with("[RFC"))
{
if let Some(pos) = slice.find(']') {
if let Some(pos) = slice.find("]") {
slice = &slice[pos..];
continue;
}
@ -414,10 +408,10 @@ impl SubjectPrefix for &str {
self
}
fn strip_prefixes_from_list(&mut self, list: &[&str], mut times: Option<u8>) -> &mut Self {
fn strip_prefixes_from_list(&mut self, list: &[&str]) -> &mut Self {
let result = {
let mut slice = self.trim();
'outer: loop {
loop {
let len = slice.len();
for prefix in list.iter() {
if slice
@ -426,14 +420,10 @@ impl SubjectPrefix for &str {
.unwrap_or(false)
{
slice = &slice[prefix.len()..];
slice = slice.trim();
times = times.map(|u| u.saturating_sub(1));
if times == Some(0) {
break 'outer;
}
}
slice = slice.trim();
}
if slice.len() == len || times == Some(0) {
if slice.len() == len {
break;
}
}
@ -844,7 +834,6 @@ impl Threads {
}
}
}
for i in 0..self.thread_nodes[&id].children.len() {
let child_hash = self.thread_nodes[&id].children[i];
if let Some(child_env_hash) = self.thread_nodes[&child_hash].message() {
@ -878,6 +867,7 @@ impl Threads {
{
let thread_hash = self.message_ids[message_id];
let node = self.thread_nodes.entry(thread_hash).or_default();
drop(message_id);
drop(envelopes_lck);
envelopes
.write()
@ -888,7 +878,7 @@ impl Threads {
/* If thread node currently has a message from a foreign mailbox and env_hash is
* from current mailbox we want to update it, otherwise return */
if !node.other_mailbox || other_mailbox {
if !(node.other_mailbox && !other_mailbox) {
return false;
}
}
@ -1232,18 +1222,18 @@ impl Threads {
let envelopes = envelopes.read().unwrap();
vec.sort_by(|a, b| match sort {
(SortField::Date, SortOrder::Desc) => {
let a = self.thread_ref(self.thread_nodes[a].group).date();
let b = self.thread_ref(self.thread_nodes[b].group).date();
let a = self.thread_ref(self.thread_nodes[&a].group).date();
let b = self.thread_ref(self.thread_nodes[&b].group).date();
b.cmp(&a)
}
(SortField::Date, SortOrder::Asc) => {
let a = self.thread_ref(self.thread_nodes[a].group).date();
let b = self.thread_ref(self.thread_nodes[b].group).date();
let a = self.thread_ref(self.thread_nodes[&a].group).date();
let b = self.thread_ref(self.thread_nodes[&b].group).date();
a.cmp(&b)
}
(SortField::Subject, SortOrder::Desc) => {
let a = &self.thread_nodes[a].message();
let b = &self.thread_nodes[b].message();
let a = &self.thread_nodes[&a].message();
let b = &self.thread_nodes[&b].message();
match (a, b) {
(Some(_), Some(_)) => {}
@ -1271,8 +1261,8 @@ impl Threads {
}
}
(SortField::Subject, SortOrder::Asc) => {
let a = &self.thread_nodes[a].message();
let b = &self.thread_nodes[b].message();
let a = &self.thread_nodes[&a].message();
let b = &self.thread_nodes[&b].message();
match (a, b) {
(Some(_), Some(_)) => {}
@ -1308,18 +1298,18 @@ impl Threads {
let envelopes = envelopes.read().unwrap();
tree.sort_by(|a, b| match sort {
(SortField::Date, SortOrder::Desc) => {
let a = self.thread_ref(self.thread_nodes[a].group).date();
let b = self.thread_ref(self.thread_nodes[b].group).date();
let a = self.thread_ref(self.thread_nodes[&a].group).date();
let b = self.thread_ref(self.thread_nodes[&b].group).date();
b.cmp(&a)
}
(SortField::Date, SortOrder::Asc) => {
let a = self.thread_ref(self.thread_nodes[a].group).date();
let b = self.thread_ref(self.thread_nodes[b].group).date();
let a = self.thread_ref(self.thread_nodes[&a].group).date();
let b = self.thread_ref(self.thread_nodes[&b].group).date();
a.cmp(&b)
}
(SortField::Subject, SortOrder::Desc) => {
let a = &self.thread_nodes[a].message();
let b = &self.thread_nodes[b].message();
let a = &self.thread_nodes[&a].message();
let b = &self.thread_nodes[&b].message();
match (a, b) {
(Some(_), Some(_)) => {}
@ -1347,8 +1337,8 @@ impl Threads {
}
}
(SortField::Subject, SortOrder::Asc) => {
let a = &self.thread_nodes[a].message();
let b = &self.thread_nodes[b].message();
let a = &self.thread_nodes[&a].message();
let b = &self.thread_nodes[&b].message();
match (a, b) {
(Some(_), Some(_)) => {}

View File

@ -1,65 +0,0 @@
Return-Path: <japoeunp@hotmail.com>
Delivered-To: jonnny@miami-dice.co.uk
Received: from violet.xenserver.co.uk
by violet.xenserver.co.uk with LMTP
id qBHcI7LKml9FxzIAYrQLqw
(envelope-from <japoeunp@hotmail.com>)
for <jonnny@miami-dice.co.uk>; Thu, 29 Oct 2020 13:59:14 +0000
Return-path: <japoeunp@hotmail.com>
Envelope-to: jonnny@miami-dice.co.uk
Delivery-date: Thu, 29 Oct 2020 13:59:14 +0000
Received: from mail-oln040092254105.outbound.protection.outlook.com ([40.92.254.105]:29481 helo=APC01-PU1-obe.outbound.protection.outlook.com)
by violet.xenserver.co.uk with esmtps (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
(Exim 4.93)
(envelope-from <japoeunp@hotmail.com>)
id 1kY8SJ-00DxYw-WD
for jonnny@miami-dice.co.uk; Thu, 29 Oct 2020 13:59:14 +0000
ARC-Seal: i=1; a=rsa-sha256; s=arcselector9901; d=microsoft.com; cv=none;
b=KKU/kthPXLl8CnAmBXXsD1QQWr4evL4ymaLwgHgRi5eSnOe2d2sQxrhcZ1VvLSvW2DQEQoNAm6NUtTC5uRUnBDS0n+g1E5/t1z8oFbzdioCIT6rL77ta3MVcaQ/o+gRa6dIwiNfu8z5GxAujOOu57gCfnCw3/gLeOHH01KtP4ezEB/DvAU9bC8eyso1T7nv+HT0riTjZOywGwDHnVb1aIPPIUiOQrrEi+cfLQRiCer01d94U8Wp+FUECrVYbr4uZGl8mbTwU4oZL1rJ25ubYG54e1ktaPJRa2YEitgJEF5sS8Z503c3RjzzBvvHkc/Kl6ypXcovP9xxeoSrS7YIPKA==
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com;
s=arcselector9901;
h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck;
bh=NUSuxgSF4fNUuTts93/OAIsK9q9w8XhbybHWH/oRmXo=;
b=VU2clBW8reAfnfCef0DeEDlBzcCU2u288YCjTvB0ekvBkJGSdI657WyS8KR7JSy0KcPWRfGbN9GJaETaasoa7bLdfuB6K9foup+vSqlA1witS5JQXQM/vJCKx67DbT8/8emLrKi7yDD2qjtRsb6HfvbwAGGvmPyUeyfTvRv6js+4YUbe5eN6CCdJEploBXDrWjFXHpSCwVCL1oF6rgrJf0+Td+ufX0QEHbOz2uJWj4yz0A8hK2yV+2JDVW7GiBwZMrO4yLNXYck/0HQRyYFe8I86xUBJWp/0IITCTe96x5L/H3lqmGkh4uRt8IsXT/2jBEm5CmXLxJZAMR8RONG9BQ==
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
dkim=none; arc=none
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=hotmail.com;
s=selector1;
h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck;
bh=NUSuxgSF4fNUuTts93/OAIsK9q9w8XhbybHWH/oRmXo=;
b=JRkih9HxwazdzH6MSzSetJMcRwvDr+e97VnoDCQYJf9qQqgtQvzMZR0Z+d2Gu74Ip3ebcvx5oYlOpV15yVZAqUmUeirpF2rdkmMWQiaDQMq9SLiF09eMDkDfEdGLD4V+C36QIISRamgyagIsC72/UB6OyxpXoAjP0SFxbyItvWVgB9EVVsSJLOKXWgRWiYSZxMLye3OQUqdWoiQ9Tw/o8uywLTvcojOizZaS2SrYWajYScBmMiCh58dUarKzrfXmR/WisfBepCf1ia7BKttjalhuJBcMyKfM923X5IbZ+Yw+gVpLtzwGUyPt2cobOAxKna11whmpWdtoBeXRR/hKOg==
Received: from PU1APC01FT013.eop-APC01.prod.protection.outlook.com
(2a01:111:e400:7ebe::45) by
PU1APC01HT068.eop-APC01.prod.protection.outlook.com (2a01:111:e400:7ebe::323)
with Microsoft SMTP Server (version=TLS1_2,
cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.3520.15; Thu, 29 Oct
2020 13:58:16 +0000
Received: from PS1PR0601MB3675.apcprd06.prod.outlook.com
(2a01:111:e400:7ebe::44) by PU1APC01FT013.mail.protection.outlook.com
(2a01:111:e400:7ebe::78) with Microsoft SMTP Server (version=TLS1_2,
cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.3520.15 via Frontend
Transport; Thu, 29 Oct 2020 13:58:16 +0000
Received: from PS1PR0601MB3675.apcprd06.prod.outlook.com
([fe80::65ed:e320:1c31:1695]) by PS1PR0601MB3675.apcprd06.prod.outlook.com
([fe80::65ed:e320:1c31:1695%7]) with mapi id 15.20.3499.027; Thu, 29 Oct 2020
13:58:16 +0000
From: Jamaica Poe <japoeunp@hotmail.com>
To: <foo-chat@example.com>
Subject: thankful that I had the chance to written report, that I could learn
and let alone the chance $4454.32
Thread-Topic: thankful that I had the chance to written report, that I could
learn and let alone the chance $4454.32
Thread-Index: AQHWrfuHFQ6EC5DxDEG0hktDfP8BQg==
Date: Thu, 29 Oct 2020 13:58:16 +0000
Message-ID:
<PS1PR0601MB36750BD00EA89E1482FA98A2D5140@PS1PR0601MB3675.apcprd06.prod.outlook.com>
Accept-Language: en-US
Content-Language: en-US
Content-Type: text/html
Content-Transfer-Encoding: base64
MIME-Version: 1.0
PCFET0NUWVBFPjxodG1sPjxoZWFkPjx0aXRsZT5mb288L3RpdGxlPjwvaGVhZD48Ym9k
eT48dGFibGUgY2xhc3M9ImZvbyI+PHRoZWFkPjx0cj48dGQ+Zm9vPC90ZD48L3RoZWFk
Pjx0Ym9keT48dHI+PHRkPmZvbzE8L3RkPjwvdHI+PC90Ym9keT48L3RhYmxlPjwvYm9k
eT48L2h0bWw+

View File

@ -66,7 +66,7 @@ pub enum PageMovement {
End,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ScrollContext {
shown_lines: usize,
total_lines: usize,

View File

@ -25,7 +25,7 @@ use melib::CardId;
use std::cmp;
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
enum ViewMode {
List,
View(ComponentId),

View File

@ -41,7 +41,7 @@ mod gpg;
mod edit_attachments;
use edit_attachments::*;
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
enum Cursor {
Headers,
Body,
@ -151,7 +151,7 @@ impl Composer {
pager.set_show_scrollbar(true);
Composer {
reply_context: None,
account_hash: 0,
account_hash: AccountHash(0),
cursor: Cursor::Headers,
pager,
draft: Draft::default(),
@ -227,19 +227,19 @@ impl Composer {
)
.as_ref()
.map(|v| v.iter().map(String::as_str).collect::<Vec<&str>>())
.unwrap_or_default();
let subject_stripped = subject.as_ref().strip_prefixes_from_list(
if prefix_list.is_empty() {
.unwrap_or(vec![]);
let subject = subject
.as_ref()
.strip_prefixes_from_list(if prefix_list.is_empty() {
<&str>::USUAL_PREFIXES
} else {
&prefix_list
},
Some(1),
) == &subject.as_ref();
})
.to_string();
let prefix =
account_settings!(context[ret.account_hash].composing.reply_prefix).as_str();
if subject_stripped {
if !subject.starts_with(prefix) {
format!("{prefix} {subject}", prefix = prefix, subject = subject)
} else {
subject.to_string()
@ -1114,7 +1114,7 @@ impl Component for Composer {
Some(Box::new(move |id: ComponentId, results: &[char]| {
Some(UIEvent::FinishedUIDialog(
id,
Box::new(results.first().cloned().unwrap_or('c')),
Box::new(results.get(0).cloned().unwrap_or('c')),
))
})),
context,
@ -2044,7 +2044,7 @@ impl Component for Composer {
Some(Box::new(move |id: ComponentId, results: &[char]| {
Some(UIEvent::FinishedUIDialog(
id,
Box::new(results.first().copied().unwrap_or('n')),
Box::new(results.get(0).copied().unwrap_or('n')),
))
})),
context,
@ -2093,7 +2093,7 @@ impl Component for Composer {
Some(Box::new(move |id: ComponentId, results: &[char]| {
Some(UIEvent::FinishedUIDialog(
id,
Box::new(results.first().copied().unwrap_or('n')),
Box::new(results.get(0).copied().unwrap_or('n')),
))
})),
context,
@ -2390,61 +2390,3 @@ fn attribution_string(
);
melib::datetime::timestamp_to_string(date, Some(fmt.as_str()), posix)
}
#[test]
fn test_compose_reply_subject_prefix() {
let raw_mail = r#"From: "some name" <some@example.com>
To: "me" <myself@example.com>
Cc:
Subject: RE: your e-mail
Message-ID: <h2g7f.z0gy2pgaen5m@example.com>
Content-Type: text/plain
hello world.
"#;
let envelope = Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
let mut context = Context::new_mock();
let account_hash = context.accounts[0].hash();
let mailbox_hash = 0;
let envelope_hash = envelope.hash();
context.accounts[0]
.collection
.insert(envelope, mailbox_hash);
let composer = Composer::reply_to(
(account_hash, mailbox_hash, envelope_hash),
String::new(),
&mut context,
false,
);
assert_eq!(&composer.draft.headers()["Subject"], "RE: your e-mail");
assert_eq!(
&composer.draft.headers()["To"],
r#"some name <some@example.com>"#
);
let raw_mail = r#"From: "some name" <some@example.com>
To: "me" <myself@example.com>
Cc:
Subject: your e-mail
Message-ID: <h2g7f.z0gy2pgaen5m@example.com>
Content-Type: text/plain
hello world.
"#;
let envelope = Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
let envelope_hash = envelope.hash();
context.accounts[0]
.collection
.insert(envelope, mailbox_hash);
let composer = Composer::reply_to(
(account_hash, mailbox_hash, envelope_hash),
String::new(),
&mut context,
false,
);
assert_eq!(&composer.draft.headers()["Subject"], "Re: your e-mail");
assert_eq!(
&composer.draft.headers()["To"],
r#"some name <some@example.com>"#
);
}

View File

@ -21,7 +21,7 @@
use super::*;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum EditAttachmentCursor {
AttachmentNo(usize),
Buttons,

View File

@ -61,14 +61,7 @@ pub use self::plain::*;
mod offline;
pub use self::offline::*;
#[derive(Debug, Copy, Clone)]
pub enum Focus {
None,
Entry,
EntryFullscreen,
}
#[derive(Debug, Copy, PartialEq, Eq, Clone)]
#[derive(Debug, Copy, PartialEq, Clone)]
pub enum Modifier {
SymmetricDifference,
Union,
@ -84,9 +77,9 @@ impl Default for Modifier {
#[derive(Debug, Default, Clone)]
pub struct DataColumns {
pub columns: Box<[CellBuffer; 12]>,
pub columns: [CellBuffer; 12],
pub widths: [usize; 12], // widths of columns calculated in first draw and after size changes
pub segment_tree: Box<[SegmentTree; 12]>,
pub segment_tree: [SegmentTree; 12],
}
#[derive(Debug, Default)]
@ -523,8 +516,6 @@ pub trait ListingTrait: Component {
None
}
fn set_movement(&mut self, mvm: PageMovement);
fn focus(&self) -> Focus;
fn set_focus(&mut self, new_value: Focus, context: &mut Context);
}
#[derive(Debug)]
@ -594,19 +585,19 @@ impl ListingComponent {
}
}
#[derive(PartialEq, Eq, Debug)]
#[derive(PartialEq, Debug)]
enum ListingFocus {
Menu,
Mailbox,
}
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[derive(PartialEq, Copy, Clone, Debug)]
enum MenuEntryCursor {
Status,
Mailbox(usize),
}
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[derive(PartialEq, Copy, Clone, Debug)]
enum ShowMenuScrollbar {
Never,
True,
@ -622,6 +613,7 @@ pub struct Listing {
cursor_pos: (usize, MenuEntryCursor),
menu_cursor_pos: (usize, MenuEntryCursor),
menu_content: CellBuffer,
menu_content_dirty: bool,
menu_scrollbar_show_timer: crate::jobs::Timer,
show_menu_scrollbar: ShowMenuScrollbar,
startup_checks_rate: RateLimit,
@ -664,7 +656,7 @@ impl Component for Listing {
let bottom_right = bottom_right!(area);
let total_cols = get_x(bottom_right) - get_x(upper_left);
let right_component_width = if self.is_menu_visible() {
let right_component_width = if self.menu_visibility {
if self.focus == ListingFocus::Menu {
(self.ratio * total_cols) / 100
} else {
@ -703,10 +695,14 @@ impl Component for Listing {
let account_hash = self.accounts[self.cursor_pos.0].hash;
if right_component_width == total_cols {
if context.is_online(account_hash).is_err()
&& !matches!(self.component, ListingComponent::Offline(_))
{
self.component = Offline(OfflineListing::new((account_hash, 0)));
if context.is_online(account_hash).is_err() {
match self.component {
ListingComponent::Offline(_) => {}
_ => {
self.component =
Offline(OfflineListing::new((account_hash, MailboxHash(0))));
}
}
}
if let Some(s) = self.status.as_mut() {
@ -722,10 +718,14 @@ impl Component for Listing {
(upper_left, (mid.saturating_sub(1), get_y(bottom_right))),
context,
);
if context.is_online(account_hash).is_err()
&& !matches!(self.component, ListingComponent::Offline(_))
{
self.component = Offline(OfflineListing::new((account_hash, 0)));
if context.is_online(account_hash).is_err() {
match self.component {
ListingComponent::Offline(_) => {}
_ => {
self.component =
Offline(OfflineListing::new((account_hash, MailboxHash(0))));
}
}
}
if let Some(s) = self.status.as_mut() {
s.draw(grid, (set_x(upper_left, mid + 1), bottom_right), context);
@ -746,6 +746,7 @@ impl Component for Listing {
*account_settings!(context[account_hash].listing.sidebar_divider);
self.sidebar_divider_theme = conf::value(context, "mail.sidebar_divider");
self.menu_content = CellBuffer::new_with_context(0, 0, None, context);
self.menu_content_dirty = true;
self.set_dirty(true);
}
UIEvent::Timer(n) if *n == self.menu_scrollbar_show_timer.id() => {
@ -753,6 +754,7 @@ impl Component for Listing {
self.show_menu_scrollbar = ShowMenuScrollbar::False;
self.set_dirty(true);
self.menu_content.empty();
self.menu_content_dirty = true;
}
return true;
}
@ -770,7 +772,7 @@ impl Component for Listing {
);
}
}
UIEvent::AccountStatusChange(account_hash, msg) => {
UIEvent::AccountStatusChange(account_hash) => {
let account_index: usize = context
.accounts
.get_index_of(account_hash)
@ -778,48 +780,17 @@ impl Component for Listing {
if self.cursor_pos.0 == account_index {
self.change_account(context);
} else {
let previous_collapsed_mailboxes: BTreeSet<MailboxHash> = self.accounts
[account_index]
.entries
.iter()
.filter_map(|e| {
if e.collapsed {
Some(e.mailbox_hash)
} else {
None
}
})
.collect::<_>();
self.accounts[account_index].entries = context.accounts[&*account_hash]
.list_mailboxes()
.into_iter()
.filter(|mailbox_node| {
context.accounts[&*account_hash][&mailbox_node.hash]
.ref_mailbox
.is_subscribed()
})
.map(|f| MailboxMenuEntry {
depth: f.depth,
indentation: f.indentation,
has_sibling: f.has_sibling,
mailbox_hash: f.hash,
visible: true,
collapsed: if previous_collapsed_mailboxes.is_empty() {
context.accounts[&*account_hash][&f.hash].conf.collapsed
} else {
previous_collapsed_mailboxes.contains(&f.hash)
},
})
.collect::<_>();
self.update_menu_account(account_index, context);
self.set_dirty(true);
self.menu_content.empty();
self.menu_content_dirty = true;
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(match msg {
Some(msg) => format!("{} {}", self.get_status(context), msg),
None => self.get_status(context),
})));
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.get_status(context),
)));
}
return true;
}
UIEvent::MailboxDelete((account_hash, mailbox_hash))
| UIEvent::MailboxCreate((account_hash, mailbox_hash)) => {
@ -828,35 +799,8 @@ impl Component for Listing {
.get_index_of(account_hash)
.expect("Invalid account_hash in UIEventMailbox{Delete,Create}");
self.menu_content.empty();
let previous_collapsed_mailboxes: BTreeSet<MailboxHash> = self.accounts
[account_index]
.entries
.iter()
.filter_map(|e| {
if e.collapsed {
Some(e.mailbox_hash)
} else {
None
}
})
.collect::<_>();
self.accounts[account_index].entries = context.accounts[&*account_hash]
.list_mailboxes()
.into_iter()
.filter(|mailbox_node| {
context.accounts[&*account_hash][&mailbox_node.hash]
.ref_mailbox
.is_subscribed()
})
.map(|f| MailboxMenuEntry {
depth: f.depth,
indentation: f.indentation,
has_sibling: f.has_sibling,
mailbox_hash: f.hash,
visible: true,
collapsed: previous_collapsed_mailboxes.contains(&f.hash),
})
.collect::<_>();
self.menu_content_dirty = true;
self.update_menu_account(account_index, context);
let mut fallback = 0;
if let MenuEntryCursor::Mailbox(ref mut cur) = self.cursor_pos.1 {
*cur = std::cmp::min(
@ -903,6 +847,7 @@ impl Component for Listing {
self.component
.set_coordinates((account_hash, *mailbox_hash));
self.menu_content.empty();
self.menu_content_dirty = true;
self.set_dirty(true);
}
return true;
@ -928,7 +873,7 @@ impl Component for Listing {
if self.focus == ListingFocus::Mailbox {
match *event {
UIEvent::Input(Key::Mouse(MouseEvent::Press(MouseButton::Left, x, _y)))
if self.is_menu_visible() =>
if self.menu_visibility =>
{
match self.menu_width {
WidgetWidth::Hold(wx) | WidgetWidth::Set(wx)
@ -945,7 +890,7 @@ impl Component for Listing {
self.set_dirty(true);
return true;
}
UIEvent::Input(Key::Mouse(MouseEvent::Hold(x, _y))) if self.is_menu_visible() => {
UIEvent::Input(Key::Mouse(MouseEvent::Hold(x, _y))) if self.menu_visibility => {
match self.menu_width {
WidgetWidth::Hold(ref mut hx) => {
*hx = usize::from(x).saturating_sub(1);
@ -955,9 +900,7 @@ impl Component for Listing {
self.set_dirty(true);
return true;
}
UIEvent::Input(Key::Mouse(MouseEvent::Release(x, _y)))
if self.is_menu_visible() =>
{
UIEvent::Input(Key::Mouse(MouseEvent::Release(x, _y))) if self.menu_visibility => {
match self.menu_width {
WidgetWidth::Hold(_) => {
self.menu_width = WidgetWidth::Set(usize::from(x).saturating_sub(1));
@ -967,10 +910,7 @@ impl Component for Listing {
self.set_dirty(true);
return true;
}
UIEvent::Input(ref k)
if self.is_menu_visible()
&& shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) =>
{
UIEvent::Input(Key::Left) if self.menu_visibility => {
self.focus = ListingFocus::Menu;
if self.show_menu_scrollbar != ShowMenuScrollbar::Never {
self.menu_scrollbar_show_timer.rearm();
@ -1341,9 +1281,7 @@ impl Component for Listing {
}
} else if self.focus == ListingFocus::Menu {
match *event {
UIEvent::Input(ref k)
if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"]) =>
{
UIEvent::Input(Key::Right) => {
self.focus = ListingFocus::Mailbox;
context
.replies
@ -1388,6 +1326,7 @@ impl Component for Listing {
target.collapsed = !(target.collapsed);
self.dirty = true;
self.menu_content.empty();
self.menu_content_dirty = true;
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::ScrollUpdate(
@ -1530,6 +1469,7 @@ impl Component for Listing {
self.show_menu_scrollbar = ShowMenuScrollbar::True;
}
self.menu_content.empty();
self.menu_content_dirty = true;
self.set_dirty(true);
return true;
}
@ -1591,6 +1531,7 @@ impl Component for Listing {
self.show_menu_scrollbar = ShowMenuScrollbar::True;
}
self.menu_content.empty();
self.menu_content_dirty = true;
return true;
}
UIEvent::Input(ref k)
@ -1644,6 +1585,7 @@ impl Component for Listing {
self.show_menu_scrollbar = ShowMenuScrollbar::True;
}
self.menu_content.empty();
self.menu_content_dirty = true;
self.set_dirty(true);
return true;
@ -1670,6 +1612,7 @@ impl Component for Listing {
self.dirty = true;
/* clear menu to force redraw */
self.menu_content.empty();
self.menu_content_dirty = true;
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
@ -1820,13 +1763,14 @@ impl Listing {
.collect();
let first_account_hash = account_entries[0].hash;
let mut ret = Listing {
component: Offline(OfflineListing::new((first_account_hash, 0))),
component: Offline(OfflineListing::new((first_account_hash, MailboxHash(0)))),
accounts: account_entries,
status: None,
dirty: true,
cursor_pos: (0, MenuEntryCursor::Mailbox(0)),
menu_cursor_pos: (0, MenuEntryCursor::Mailbox(0)),
menu_content: CellBuffer::new_with_context(0, 0, None, context),
menu_content_dirty: true,
menu_scrollbar_show_timer: context.job_executor.clone().create_timer(
std::time::Duration::from_secs(0),
std::time::Duration::from_millis(1200),
@ -1851,6 +1795,11 @@ impl Listing {
}
fn draw_menu(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if self.menu_content_dirty {
for account_index in 0..self.accounts.len() {
self.update_menu_account(account_index, context);
}
}
clear_area(grid, area, self.theme_default);
let total_height: usize = 3 * (self.accounts.len())
+ self
@ -2293,9 +2242,8 @@ impl Listing {
}
}
fn change_account(&mut self, context: &mut Context) {
let account_hash = context.accounts[self.cursor_pos.0].hash();
let previous_collapsed_mailboxes: BTreeSet<MailboxHash> = self.accounts[self.cursor_pos.0]
fn update_menu_account(&mut self, account_index: usize, context: &mut Context) {
let previous_collapsed_mailboxes: BTreeSet<MailboxHash> = self.accounts[account_index]
.entries
.iter()
.filter_map(|e| {
@ -2306,11 +2254,11 @@ impl Listing {
}
})
.collect::<_>();
self.accounts[self.cursor_pos.0].entries = context.accounts[self.cursor_pos.0]
self.accounts[account_index].entries = context.accounts[account_index]
.list_mailboxes()
.into_iter()
.filter(|mailbox_node| {
context.accounts[self.cursor_pos.0][&mailbox_node.hash]
context.accounts[account_index][&mailbox_node.hash]
.ref_mailbox
.is_subscribed()
})
@ -2321,12 +2269,17 @@ impl Listing {
mailbox_hash: f.hash,
visible: true,
collapsed: if previous_collapsed_mailboxes.is_empty() {
context.accounts[self.cursor_pos.0][&f.hash].conf.collapsed
context.accounts[account_index][&f.hash].conf.collapsed
} else {
previous_collapsed_mailboxes.contains(&f.hash)
},
})
.collect::<_>();
}
fn change_account(&mut self, context: &mut Context) {
self.update_menu_account(self.cursor_pos.0, context);
let account_hash = self.accounts[self.cursor_pos.0].hash;
match self.cursor_pos.1 {
MenuEntryCursor::Mailbox(idx) => {
/* Account might have no mailboxes yet if it's offline */
@ -2342,8 +2295,9 @@ impl Listing {
let index_style =
mailbox_settings!(context[account_hash][mailbox_hash].listing.index_style);
self.component.set_style(*index_style);
} else if !matches!(self.component, ListingComponent::Offline(_)) {
self.component = Offline(OfflineListing::new((account_hash, 0)));
} else {
/* Set to dummy */
self.component = Offline(OfflineListing::new((account_hash, MailboxHash(0))));
}
self.status = None;
context
@ -2378,8 +2332,4 @@ impl Listing {
self.get_status(context),
)));
}
fn is_menu_visible(&self) -> bool {
!matches!(self.component.focus(), Focus::EntryFullscreen) && self.menu_visibility
}
}

View File

@ -187,8 +187,8 @@ pub struct CompactListing {
dirty: bool,
force_draw: bool,
/// If `self.view` exists or not.
focus: Focus,
view: Box<ThreadView>,
unfocused: bool,
view: ThreadView,
row_updates: SmallVec<[ThreadHash; 8]>,
color_cache: ColorCache,
@ -304,10 +304,10 @@ impl MailListingTrait for CompactListing {
if !force && old_cursor_pos == self.new_cursor_pos {
self.view.update(context);
} else if self.unfocused() {
} else if self.unfocused {
let thread = self.get_thread_under_cursor(self.cursor_pos.2);
self.view = Box::new(ThreadView::new(self.new_cursor_pos, thread, None, context));
self.view = ThreadView::new(self.new_cursor_pos, thread, None, context);
}
}
@ -490,8 +490,8 @@ impl ListingTrait for CompactListing {
fn set_coordinates(&mut self, coordinates: (AccountHash, MailboxHash)) {
self.new_cursor_pos = (coordinates.0, coordinates.1, 0);
self.focus = Focus::None;
self.view = Box::new(ThreadView::default());
self.unfocused = false;
self.view = ThreadView::default();
self.filtered_selection.clear();
self.filtered_order.clear();
self.filter_term.clear();
@ -818,7 +818,7 @@ impl ListingTrait for CompactListing {
}
fn unfocused(&self) -> bool {
!matches!(self.focus, Focus::None)
self.unfocused
}
fn set_modifier_active(&mut self, new_val: bool) {
@ -837,33 +837,6 @@ impl ListingTrait for CompactListing {
self.movement = Some(mvm);
self.set_dirty(true);
}
fn set_focus(&mut self, new_value: Focus, context: &mut Context) {
match new_value {
Focus::None => {
self.view
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.dirty = true;
/* If self.row_updates is not empty and we exit a thread, the row_update events
* will be performed but the list will not be drawn. So force a draw in any case.
* */
self.force_draw = true;
}
Focus::Entry => {
self.force_draw = true;
self.dirty = true;
self.view.set_dirty(true);
}
Focus::EntryFullscreen => {
self.view.set_dirty(true);
}
}
self.focus = new_value;
}
fn focus(&self) -> Focus {
self.focus
}
}
impl fmt::Display for CompactListing {
@ -876,7 +849,7 @@ impl CompactListing {
pub const DESCRIPTION: &'static str = "compact listing";
pub fn new(coordinates: (AccountHash, MailboxHash)) -> Box<Self> {
Box::new(CompactListing {
cursor_pos: (coordinates.0, 1, 0),
cursor_pos: (coordinates.0, MailboxHash(0), 0),
new_cursor_pos: (coordinates.0, coordinates.1, 0),
length: 0,
sort: (Default::default(), Default::default()),
@ -890,14 +863,14 @@ impl CompactListing {
filtered_selection: Vec::new(),
filtered_order: HashMap::default(),
selection: HashMap::default(),
focus: Focus::None,
row_updates: SmallVec::new(),
data_columns: DataColumns::default(),
rows_drawn: SegmentTree::default(),
rows: vec![],
dirty: true,
force_draw: true,
view: Box::new(ThreadView::default()),
unfocused: false,
view: ThreadView::default(),
color_cache: ColorCache::default(),
movement: None,
modifier_active: false,
@ -1492,15 +1465,10 @@ impl CompactListing {
impl Component for CompactListing {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if !self.is_dirty() {
return;
}
if matches!(self.focus, Focus::EntryFullscreen) {
return self.view.draw(grid, area, context);
}
if !self.unfocused() {
if !self.unfocused {
if !self.is_dirty() {
return;
}
let mut area = area;
if !self.filter_term.is_empty() {
let (upper_left, bottom_right) = area;
@ -1747,81 +1715,44 @@ impl Component for CompactListing {
}
self.dirty = false;
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
let shortcuts = self.get_shortcuts(context);
match (&event, self.focus) {
(UIEvent::Input(ref k), Focus::Entry)
if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"]) =>
{
self.set_focus(Focus::EntryFullscreen, context);
return true;
}
(UIEvent::Input(ref k), Focus::EntryFullscreen)
if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) =>
{
self.set_focus(Focus::Entry, context);
return true;
}
(UIEvent::Input(ref k), Focus::Entry)
if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) =>
{
self.set_focus(Focus::None, context);
return true;
}
_ => {}
}
if self.unfocused() && self.view.process_event(event, context) {
if self.unfocused && self.view.process_event(event, context) {
return true;
}
let shortcuts = self.get_shortcuts(context);
if self.length > 0 {
match *event {
UIEvent::Input(ref k)
if matches!(self.focus, Focus::None)
&& (shortcut!(k == shortcuts[Listing::DESCRIPTION]["open_entry"])
|| shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"])) =>
if !self.unfocused
&& shortcut!(
k == shortcuts[CompactListing::DESCRIPTION]["open_thread"]
) =>
{
let thread = self.get_thread_under_cursor(self.cursor_pos.2);
self.view = Box::new(ThreadView::new(self.cursor_pos, thread, None, context));
self.set_focus(Focus::Entry, context);
self.view = ThreadView::new(self.cursor_pos, thread, None, context);
self.unfocused = true;
self.dirty = true;
return true;
}
UIEvent::Input(ref k)
if matches!(self.focus, Focus::Entry)
&& shortcut!(k == shortcuts[Listing::DESCRIPTION]["exit_entry"]) =>
if self.unfocused
&& shortcut!(
k == shortcuts[CompactListing::DESCRIPTION]["exit_thread"]
) =>
{
self.set_focus(Focus::None, context);
return true;
}
UIEvent::Input(ref k)
if matches!(self.focus, Focus::None)
&& shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"]) =>
{
self.set_focus(Focus::Entry, context);
return true;
}
UIEvent::Input(ref k)
if !matches!(self.focus, Focus::None)
&& shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) =>
{
match self.focus {
Focus::Entry => {
self.set_focus(Focus::None, context);
}
Focus::EntryFullscreen => {
self.set_focus(Focus::Entry, context);
}
Focus::None => {
unreachable!();
}
}
self.unfocused = false;
self.view
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.dirty = true;
/* If self.row_updates is not empty and we exit a thread, the row_update events
* will be performed but the list will not be drawn. So force a draw in any case.
* */
self.force_draw = true;
return true;
}
UIEvent::Input(ref key)
if !self.unfocused()
if !self.unfocused
&& shortcut!(key == shortcuts[Listing::DESCRIPTION]["select_entry"]) =>
{
if self.modifier_active && self.modifier_command.is_none() {
@ -1835,7 +1766,7 @@ impl Component for CompactListing {
}
UIEvent::Action(ref action) => {
match action {
Action::Sort(field, order) if !self.unfocused() => {
Action::Sort(field, order) if !self.unfocused => {
debug!("Sort {:?} , {:?}", field, order);
self.sort = (*field, *order);
self.sortcmd = true;
@ -1847,13 +1778,13 @@ impl Component for CompactListing {
}
return true;
}
Action::SubSort(field, order) if !self.unfocused() => {
Action::SubSort(field, order) if !self.unfocused => {
debug!("SubSort {:?} , {:?}", field, order);
self.subsort = (*field, *order);
// FIXME: perform subsort.
return true;
}
Action::Listing(ToggleThreadSnooze) if !self.unfocused() => {
Action::Listing(ToggleThreadSnooze) if !self.unfocused => {
let thread = self.get_thread_under_cursor(self.cursor_pos.2);
let account = &mut context.accounts[&self.cursor_pos.0];
account
@ -1944,7 +1875,7 @@ impl Component for CompactListing {
self.dirty = true;
if self.unfocused() {
if self.unfocused {
self.view
.process_event(&mut UIEvent::EnvelopeRename(*old_hash, *new_hash), context);
}
@ -1974,7 +1905,7 @@ impl Component for CompactListing {
self.dirty = true;
if self.unfocused() {
if self.unfocused {
self.view
.process_event(&mut UIEvent::EnvelopeUpdate(*env_hash), context);
}
@ -1986,7 +1917,7 @@ impl Component for CompactListing {
self.dirty = true;
}
UIEvent::Input(Key::Esc)
if !self.unfocused()
if !self.unfocused
&& self.selection.values().cloned().any(std::convert::identity) =>
{
for v in self.selection.values_mut() {
@ -1995,13 +1926,13 @@ impl Component for CompactListing {
self.dirty = true;
return true;
}
UIEvent::Input(Key::Esc) if !self.unfocused() && !self.filter_term.is_empty() => {
UIEvent::Input(Key::Esc) if !self.unfocused && !self.filter_term.is_empty() => {
self.set_coordinates((self.new_cursor_pos.0, self.new_cursor_pos.1));
self.refresh_mailbox(context, false);
self.set_dirty(true);
return true;
}
UIEvent::Action(Action::Listing(Search(ref filter_term))) if !self.unfocused() => {
UIEvent::Action(Action::Listing(Search(ref filter_term))) if !self.unfocused => {
match context.accounts[&self.cursor_pos.0].search(
filter_term,
self.sort,
@ -2023,7 +1954,7 @@ impl Component for CompactListing {
};
self.set_dirty(true);
}
UIEvent::Action(Action::Listing(Select(ref search_term))) if !self.unfocused() => {
UIEvent::Action(Action::Listing(Select(ref search_term))) if !self.unfocused => {
match context.accounts[&self.cursor_pos.0].search(
search_term,
self.sort,
@ -2090,29 +2021,30 @@ impl Component for CompactListing {
}
false
}
fn is_dirty(&self) -> bool {
match self.focus {
Focus::None => self.dirty,
Focus::Entry => self.dirty || self.view.is_dirty(),
Focus::EntryFullscreen => self.view.is_dirty(),
}
self.dirty
|| if self.unfocused {
self.view.is_dirty()
} else {
false
}
}
fn set_dirty(&mut self, value: bool) {
self.dirty = value;
if self.unfocused() {
if self.unfocused {
self.view.set_dirty(value);
}
}
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
let mut map = if self.unfocused() {
let mut map = if self.unfocused {
self.view.get_shortcuts(context)
} else {
ShortcutMaps::default()
};
let config_map = context.settings.shortcuts.compact_listing.key_values();
map.insert(CompactListing::DESCRIPTION, config_map);
let config_map = context.settings.shortcuts.listing.key_values();
map.insert(Listing::DESCRIPTION, config_map);

View File

@ -22,7 +22,6 @@
use super::*;
use crate::components::PageMovement;
use crate::jobs::JoinHandle;
use indexmap::IndexSet;
use std::iter::FromIterator;
macro_rules! row_attr {
@ -115,7 +114,7 @@ pub struct ConversationsListing {
dirty: bool,
force_draw: bool,
/// If `self.view` exists or not.
focus: Focus,
unfocused: bool,
view: ThreadView,
row_updates: SmallVec<[ThreadHash; 8]>,
color_cache: ColorCache,
@ -216,7 +215,7 @@ impl MailListingTrait for ConversationsListing {
if !force && old_cursor_pos == self.new_cursor_pos && old_mailbox_hash == self.cursor_pos.1
{
self.view.update(context);
} else if self.unfocused() {
} else if self.unfocused {
let thread_group = self.get_thread_under_cursor(self.cursor_pos.2);
self.view = ThreadView::new(self.new_cursor_pos, thread_group, None, context);
@ -241,7 +240,6 @@ impl MailListingTrait for ConversationsListing {
}
let mut max_entry_columns = 0;
let mut other_subjects = IndexSet::new();
let mut from_address_list = Vec::new();
let mut from_address_set: std::collections::HashSet<Vec<u8>> =
std::collections::HashSet::new();
@ -275,30 +273,17 @@ impl MailListingTrait for ConversationsListing {
panic!();
}
other_subjects.clear();
from_address_list.clear();
from_address_set.clear();
for (envelope, show_subject) in threads
for envelope in threads
.thread_group_iter(thread)
.filter_map(|(_, h)| {
Some((
threads.thread_nodes()[&h].message()?,
threads.thread_nodes()[&h].show_subject(),
))
})
.map(|(env_hash, show_subject)| {
(
context.accounts[&self.cursor_pos.0]
.collection
.get_env(env_hash),
show_subject,
)
.filter_map(|(_, h)| threads.thread_nodes()[&h].message())
.map(|env_hash| {
context.accounts[&self.cursor_pos.0]
.collection
.get_env(env_hash)
})
{
if show_subject {
other_subjects.insert(envelope.subject().to_string());
}
for addr in envelope.from().iter() {
if from_address_set.contains(addr.address_spec_raw()) {
continue;
@ -328,7 +313,6 @@ impl MailListingTrait for ConversationsListing {
context,
&from_address_list,
&threads,
&other_subjects,
thread,
);
max_entry_columns = std::cmp::max(
@ -368,7 +352,7 @@ impl ListingTrait for ConversationsListing {
fn set_coordinates(&mut self, coordinates: (AccountHash, MailboxHash)) {
self.new_cursor_pos = (coordinates.0, coordinates.1, 0);
self.focus = Focus::None;
self.unfocused = false;
self.view = ThreadView::default();
self.filtered_selection.clear();
self.filtered_order.clear();
@ -553,7 +537,7 @@ impl ListingTrait for ConversationsListing {
}
fn unfocused(&self) -> bool {
!matches!(self.focus, Focus::None)
self.unfocused
}
fn set_modifier_active(&mut self, new_val: bool) {
@ -572,33 +556,6 @@ impl ListingTrait for ConversationsListing {
self.movement = Some(mvm);
self.set_dirty(true);
}
fn set_focus(&mut self, new_value: Focus, context: &mut Context) {
match new_value {
Focus::None => {
self.view
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.dirty = true;
/* If self.row_updates is not empty and we exit a thread, the row_update events
* will be performed but the list will not be drawn. So force a draw in any case.
* */
self.force_draw = true;
}
Focus::Entry => {
self.force_draw = true;
self.dirty = true;
self.view.set_dirty(true);
}
Focus::EntryFullscreen => {
self.view.set_dirty(true);
}
}
self.focus = new_value;
}
fn focus(&self) -> Focus {
self.focus
}
}
impl fmt::Display for ConversationsListing {
@ -608,12 +565,12 @@ impl fmt::Display for ConversationsListing {
}
impl ConversationsListing {
//const DESCRIPTION: &'static str = "conversations listing";
const DESCRIPTION: &'static str = "conversations listing";
//const PADDING_CHAR: char = ' '; //░';
pub fn new(coordinates: (AccountHash, MailboxHash)) -> Box<Self> {
Box::new(Self {
cursor_pos: (coordinates.0, 1, 0),
Box::new(ConversationsListing {
cursor_pos: (coordinates.0, MailboxHash(0), 0),
new_cursor_pos: (coordinates.0, coordinates.1, 0),
length: 0,
sort: (Default::default(), Default::default()),
@ -629,7 +586,7 @@ impl ConversationsListing {
rows: Ok(Vec::with_capacity(1024)),
dirty: true,
force_draw: true,
focus: Focus::None,
unfocused: false,
view: ThreadView::default(),
color_cache: ColorCache::default(),
movement: None,
@ -643,9 +600,8 @@ impl ConversationsListing {
&self,
e: &Envelope,
context: &Context,
from: &[Address],
from: &Vec<Address>,
threads: &Threads,
other_subjects: &IndexSet<String>,
hash: ThreadHash,
) -> EntryStrings {
let thread = threads.thread_ref(hash);
@ -686,24 +642,8 @@ impl ConversationsListing {
tags.pop();
}
}
let mut subject = if *mailbox_settings!(
context[self.cursor_pos.0][&self.cursor_pos.1]
.listing
.thread_subject_pack
) {
other_subjects
.into_iter()
.fold(String::new(), |mut acc, s| {
if !acc.is_empty() {
acc.push_str(", ");
}
acc.push_str(s);
acc
})
} else {
e.subject().to_string()
};
subject.truncate_at_boundary(100);
let mut subject = e.subject().to_string();
subject.truncate_at_boundary(150);
if thread.len() > 1 {
EntryStrings {
date: DateString(ConversationsListing::format_date(context, thread.date())),
@ -788,29 +728,18 @@ impl ConversationsListing {
let idx: usize = self.order[&thread_hash];
let env_hash = threads.thread_nodes()[&thread_node_hash].message().unwrap();
let mut other_subjects = IndexSet::new();
let mut from_address_list = Vec::new();
let mut from_address_set: std::collections::HashSet<Vec<u8>> =
std::collections::HashSet::new();
for (envelope, show_subject) in threads
for envelope in threads
.thread_group_iter(thread_hash)
.filter_map(|(_, h)| {
threads.thread_nodes()[&h]
.message()
.map(|env_hash| (env_hash, threads.thread_nodes()[&h].show_subject()))
})
.map(|(env_hash, show_subject)| {
(
context.accounts[&self.cursor_pos.0]
.collection
.get_env(env_hash),
show_subject,
)
.filter_map(|(_, h)| threads.thread_nodes()[&h].message())
.map(|env_hash| {
context.accounts[&self.cursor_pos.0]
.collection
.get_env(env_hash)
})
{
if show_subject {
other_subjects.insert(envelope.subject().to_string());
}
for addr in envelope.from().iter() {
if from_address_set.contains(addr.address_spec_raw()) {
continue;
@ -825,7 +754,6 @@ impl ConversationsListing {
context,
&from_address_list,
&threads,
&other_subjects,
thread_hash,
);
drop(envelope);
@ -979,11 +907,6 @@ impl Component for ConversationsListing {
if !self.is_dirty() {
return;
}
if matches!(self.focus, Focus::EntryFullscreen) {
return self.view.draw(grid, area, context);
}
let (upper_left, bottom_right) = area;
{
let mut area = area;
@ -1219,7 +1142,7 @@ impl Component for ConversationsListing {
self.draw_list(grid, area, context);
}
}
if matches!(self.focus, Focus::Entry) {
if self.unfocused {
if self.length == 0 && self.dirty {
clear_area(grid, area, self.color_cache.theme_default);
context.dirty_areas.push_back(area);
@ -1234,81 +1157,44 @@ impl Component for ConversationsListing {
}
self.dirty = false;
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
let shortcuts = self.get_shortcuts(context);
match (&event, self.focus) {
(UIEvent::Input(ref k), Focus::Entry)
if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"]) =>
{
self.set_focus(Focus::EntryFullscreen, context);
return true;
}
(UIEvent::Input(ref k), Focus::EntryFullscreen)
if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) =>
{
self.set_focus(Focus::Entry, context);
return true;
}
(UIEvent::Input(ref k), Focus::Entry)
if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) =>
{
self.set_focus(Focus::None, context);
return true;
}
_ => {}
}
if self.unfocused() && self.view.process_event(event, context) {
if self.unfocused && self.view.process_event(event, context) {
return true;
}
let shortcuts = self.get_shortcuts(context);
if self.length > 0 {
match *event {
UIEvent::Input(ref k)
if matches!(self.focus, Focus::None)
&& (shortcut!(k == shortcuts[Listing::DESCRIPTION]["open_entry"])
|| shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"])) =>
if !self.unfocused
&& shortcut!(
k == shortcuts[ConversationsListing::DESCRIPTION]["open_thread"]
) =>
{
let thread = self.get_thread_under_cursor(self.cursor_pos.2);
self.view = ThreadView::new(self.cursor_pos, thread, None, context);
self.set_focus(Focus::Entry, context);
self.unfocused = true;
self.dirty = true;
return true;
}
UIEvent::Input(ref k)
if !matches!(self.focus, Focus::None)
&& shortcut!(k == shortcuts[Listing::DESCRIPTION]["exit_entry"]) =>
if self.unfocused
&& shortcut!(
k == shortcuts[ConversationsListing::DESCRIPTION]["exit_thread"]
) =>
{
self.set_focus(Focus::None, context);
return true;
}
UIEvent::Input(ref k)
if matches!(self.focus, Focus::Entry)
&& shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"]) =>
{
self.set_focus(Focus::EntryFullscreen, context);
return true;
}
UIEvent::Input(ref k)
if !matches!(self.focus, Focus::None)
&& shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) =>
{
match self.focus {
Focus::Entry => {
self.set_focus(Focus::None, context);
}
Focus::EntryFullscreen => {
self.set_focus(Focus::Entry, context);
}
Focus::None => {
unreachable!();
}
}
self.unfocused = false;
self.view
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.dirty = true;
/* If self.row_updates is not empty and we exit a thread, the row_update events
* will be performed but the list will not be drawn. So force a draw in any case.
* */
self.force_draw = true;
return true;
}
UIEvent::Input(ref key)
if !self.unfocused()
if !self.unfocused
&& shortcut!(key == shortcuts[Listing::DESCRIPTION]["select_entry"]) =>
{
if self.modifier_active && self.modifier_command.is_none() {
@ -1339,7 +1225,7 @@ impl Component for ConversationsListing {
self.dirty = true;
if self.unfocused() {
if self.unfocused {
self.view.process_event(
&mut UIEvent::EnvelopeRename(*old_hash, *new_hash),
context,
@ -1371,13 +1257,13 @@ impl Component for ConversationsListing {
self.dirty = true;
if self.unfocused() {
if self.unfocused {
self.view
.process_event(&mut UIEvent::EnvelopeUpdate(*env_hash), context);
}
}
UIEvent::Action(ref action) => match action {
Action::SubSort(field, order) if !self.unfocused() => {
Action::SubSort(field, order) if !self.unfocused => {
debug!("SubSort {:?} , {:?}", field, order);
self.subsort = (*field, *order);
// FIXME subsort
@ -1389,7 +1275,7 @@ impl Component for ConversationsListing {
//}
return true;
}
Action::Sort(field, order) if !self.unfocused() => {
Action::Sort(field, order) if !self.unfocused => {
debug!("Sort {:?} , {:?}", field, order);
// FIXME sort
/*
@ -1409,7 +1295,7 @@ impl Component for ConversationsListing {
*/
return true;
}
Action::Listing(ToggleThreadSnooze) if !self.unfocused() => {
Action::Listing(ToggleThreadSnooze) if !self.unfocused => {
let thread = self.get_thread_under_cursor(self.cursor_pos.2);
let account = &mut context.accounts[&self.cursor_pos.0];
account
@ -1477,7 +1363,7 @@ impl Component for ConversationsListing {
self.dirty = true;
}
UIEvent::Action(ref action) => match action {
Action::Listing(Search(ref filter_term)) if !self.unfocused() => {
Action::Listing(Search(ref filter_term)) if !self.unfocused => {
match context.accounts[&self.cursor_pos.0].search(
filter_term,
self.sort,
@ -1503,7 +1389,7 @@ impl Component for ConversationsListing {
_ => {}
},
UIEvent::Input(Key::Esc)
if !self.unfocused()
if !self.unfocused
&& self.selection.values().cloned().any(std::convert::identity) =>
{
for (k, v) in self.selection.iter_mut() {
@ -1516,7 +1402,7 @@ impl Component for ConversationsListing {
return true;
}
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Char(''))
if !self.unfocused() && !&self.filter_term.is_empty() =>
if !self.unfocused && !&self.filter_term.is_empty() =>
{
self.set_coordinates((self.new_cursor_pos.0, self.new_cursor_pos.1));
self.refresh_mailbox(context, false);
@ -1550,29 +1436,30 @@ impl Component for ConversationsListing {
false
}
fn is_dirty(&self) -> bool {
match self.focus {
Focus::None => self.dirty,
Focus::Entry => self.dirty || self.view.is_dirty(),
Focus::EntryFullscreen => self.view.is_dirty(),
}
self.dirty
|| if self.unfocused {
self.view.is_dirty()
} else {
false
}
}
fn set_dirty(&mut self, value: bool) {
if self.unfocused() {
if self.unfocused {
self.view.set_dirty(value);
}
self.dirty = value;
}
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
let mut map = if self.unfocused() {
let mut map = if self.unfocused {
self.view.get_shortcuts(context)
} else {
ShortcutMaps::default()
};
let config_map = context.settings.shortcuts.compact_listing.key_values();
map.insert(ConversationsListing::DESCRIPTION, config_map);
let config_map = context.settings.shortcuts.listing.key_values();
map.insert(Listing::DESCRIPTION, config_map);

View File

@ -21,14 +21,12 @@
use super::*;
use crate::components::PageMovement;
use std::borrow::Cow;
#[derive(Debug)]
pub struct OfflineListing {
cursor_pos: (AccountHash, MailboxHash),
_row_updates: SmallVec<[ThreadHash; 8]>,
_selection: HashMap<ThreadHash, bool>,
messages: Vec<Cow<'static, str>>,
dirty: bool,
id: ComponentId,
}
@ -86,12 +84,6 @@ impl ListingTrait for OfflineListing {
}
fn set_movement(&mut self, _: PageMovement) {}
fn focus(&self) -> Focus {
Focus::None
}
fn set_focus(&mut self, _new_value: Focus, _context: &mut Context) {}
}
impl fmt::Display for OfflineListing {
@ -106,7 +98,6 @@ impl OfflineListing {
cursor_pos,
_row_updates: SmallVec::new(),
_selection: HashMap::default(),
messages: vec![],
dirty: true,
id: ComponentId::new_v4(),
})
@ -120,50 +111,26 @@ impl Component for OfflineListing {
}
self.dirty = false;
let theme_default = conf::value(context, "theme_default");
let text_unfocused = conf::value(context, "text.unfocused");
let error_message = conf::value(context, "error_message");
clear_area(grid, area, theme_default);
if let Err(err) = context.is_online(self.cursor_pos.0) {
let (x, _) = write_string_to_grid(
"offline: ",
grid,
error_message.fg,
error_message.bg,
error_message.attrs,
conf::value(context, "error_message").fg,
conf::value(context, "error_message").bg,
conf::value(context, "error_message").attrs,
area,
None,
);
write_string_to_grid(
&err.to_string(),
grid,
error_message.fg,
error_message.bg,
error_message.attrs,
Color::Red,
theme_default.bg,
theme_default.attrs,
(set_x(upper_left!(area), x + 1), bottom_right!(area)),
Some(get_x(upper_left!(area))),
None,
);
if let Some(msg) = self.messages.last() {
write_string_to_grid(
msg,
grid,
text_unfocused.fg,
text_unfocused.bg,
Attr::BOLD,
(pos_inc((0, 1), upper_left!(area)), bottom_right!(area)),
None,
);
}
for (i, msg) in self.messages.iter().rev().skip(1).enumerate() {
write_string_to_grid(
msg,
grid,
text_unfocused.fg,
text_unfocused.bg,
text_unfocused.attrs,
(pos_inc((0, 2 + i), upper_left!(area)), bottom_right!(area)),
None,
);
}
} else {
let (_, mut y) = write_string_to_grid(
"loading...",
@ -183,9 +150,9 @@ impl Component for OfflineListing {
write_string_to_grid(
&format!("{}: {:?}", job_id, j),
grid,
text_unfocused.fg,
text_unfocused.bg,
text_unfocused.attrs,
theme_default.fg,
theme_default.bg,
theme_default.attrs,
(set_y(upper_left!(area), y + 1), bottom_right!(area)),
None,
);
@ -194,26 +161,19 @@ impl Component for OfflineListing {
context
.replies
.push_back(UIEvent::AccountStatusChange(self.cursor_pos.0, None));
.push_back(UIEvent::AccountStatusChange(self.cursor_pos.0));
}
context.dirty_areas.push_back(area);
}
fn process_event(&mut self, event: &mut UIEvent, _context: &mut Context) -> bool {
match event {
UIEvent::AccountStatusChange(account_hash, msg)
if *account_hash == self.cursor_pos.0 =>
{
if let Some(msg) = msg.clone() {
self.messages.push(msg);
}
UIEvent::AccountStatusChange(account_hash) if *account_hash == self.cursor_pos.0 => {
self.dirty = true
}
_ => {}
}
false
}
fn is_dirty(&self) -> bool {
self.dirty
}
@ -225,7 +185,6 @@ impl Component for OfflineListing {
fn id(&self) -> ComponentId {
self.id
}
fn set_id(&mut self, id: ComponentId) {
self.id = id;
}

View File

@ -146,7 +146,7 @@ pub struct PlainListing {
dirty: bool,
force_draw: bool,
/// If `self.view` exists or not.
focus: Focus,
unfocused: bool,
view: MailView,
row_updates: SmallVec<[EnvelopeHash; 8]>,
_row_updates: SmallVec<[ThreadHash; 8]>,
@ -296,7 +296,7 @@ impl MailListingTrait for PlainListing {
let temp = (self.new_cursor_pos.0, self.new_cursor_pos.1, env_hash);
if !force && old_cursor_pos == self.new_cursor_pos {
self.view.update(temp, context);
} else if self.unfocused() {
} else if self.unfocused {
self.view = MailView::new(temp, None, None, context);
}
}
@ -335,7 +335,7 @@ impl ListingTrait for PlainListing {
fn set_coordinates(&mut self, coordinates: (AccountHash, MailboxHash)) {
self.new_cursor_pos = (coordinates.0, coordinates.1, 0);
self.focus = Focus::None;
self.unfocused = false;
self.view = MailView::default();
self.filtered_selection.clear();
self.filtered_order.clear();
@ -641,44 +641,13 @@ impl ListingTrait for PlainListing {
}
fn unfocused(&self) -> bool {
!matches!(self.focus, Focus::None)
self.unfocused
}
fn set_movement(&mut self, mvm: PageMovement) {
self.movement = Some(mvm);
self.set_dirty(true);
}
fn set_focus(&mut self, new_value: Focus, context: &mut Context) {
match new_value {
Focus::None => {
self.view
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.dirty = true;
/* If self.row_updates is not empty and we exit a thread, the row_update events
* will be performed but the list will not be drawn. So force a draw in any case.
* */
self.force_draw = true;
}
Focus::Entry => {
let env_hash = self.get_env_under_cursor(self.cursor_pos.2, context);
let temp = (self.cursor_pos.0, self.cursor_pos.1, env_hash);
self.view = MailView::new(temp, None, None, context);
self.force_draw = true;
self.dirty = true;
self.view.set_dirty(true);
}
Focus::EntryFullscreen => {
self.dirty = true;
self.view.set_dirty(true);
}
}
self.focus = new_value;
}
fn focus(&self) -> Focus {
self.focus
}
}
impl fmt::Display for PlainListing {
@ -688,10 +657,10 @@ impl fmt::Display for PlainListing {
}
impl PlainListing {
//const DESCRIPTION: &'static str = "plain listing";
const DESCRIPTION: &'static str = "plain listing";
pub fn new(coordinates: (AccountHash, MailboxHash)) -> Box<Self> {
Box::new(PlainListing {
cursor_pos: (0, 1, 0),
cursor_pos: (AccountHash(0), MailboxHash(0), 0),
new_cursor_pos: (coordinates.0, coordinates.1, 0),
length: 0,
sort: (Default::default(), Default::default()),
@ -711,7 +680,7 @@ impl PlainListing {
data_columns: DataColumns::default(),
dirty: true,
force_draw: true,
focus: Focus::None,
unfocused: false,
view: MailView::default(),
color_cache: ColorCache::default(),
active_jobs: HashMap::default(),
@ -1091,15 +1060,10 @@ impl PlainListing {
impl Component for PlainListing {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if !self.is_dirty() {
return;
}
if matches!(self.focus, Focus::EntryFullscreen) {
return self.view.draw(grid, area, context);
}
if matches!(self.focus, Focus::None) {
if !self.unfocused {
if !self.is_dirty() {
return;
}
let mut area = area;
if !self.filter_term.is_empty() {
let (upper_left, bottom_right) = area;
@ -1165,79 +1129,48 @@ impl Component for PlainListing {
}
self.dirty = false;
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
let shortcuts = self.get_shortcuts(context);
match (&event, self.focus) {
(UIEvent::Input(ref k), Focus::Entry)
if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"]) =>
{
self.set_focus(Focus::EntryFullscreen, context);
return true;
}
(UIEvent::Input(ref k), Focus::EntryFullscreen)
if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) =>
{
self.set_focus(Focus::Entry, context);
return true;
}
(UIEvent::Input(ref k), Focus::Entry)
if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) =>
{
self.set_focus(Focus::None, context);
return true;
}
_ => {}
}
if self.unfocused() && self.view.process_event(event, context) {
if self.unfocused && self.view.process_event(event, context) {
return true;
}
let shortcuts = self.get_shortcuts(context);
if self.length > 0 {
match *event {
UIEvent::Input(ref k)
if matches!(self.focus, Focus::None)
&& (shortcut!(k == shortcuts[Listing::DESCRIPTION]["open_entry"])
|| shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"])) =>
if !self.unfocused
&& shortcut!(k == shortcuts[PlainListing::DESCRIPTION]["open_thread"]) =>
{
self.set_focus(Focus::Entry, context);
let env_hash = self.get_env_under_cursor(self.cursor_pos.2, context);
let temp = (self.cursor_pos.0, self.cursor_pos.1, env_hash);
self.view = MailView::new(temp, None, None, context);
self.unfocused = true;
self.dirty = true;
return true;
}
UIEvent::Input(ref k)
if !matches!(self.focus, Focus::None)
&& shortcut!(k == shortcuts[Listing::DESCRIPTION]["exit_entry"]) =>
if self.unfocused
&& shortcut!(k == shortcuts[PlainListing::DESCRIPTION]["exit_thread"]) =>
{
self.set_focus(Focus::None, context);
return true;
}
UIEvent::Input(ref k)
if !matches!(self.focus, Focus::None)
&& shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) =>
{
match self.focus {
Focus::Entry => {
self.set_focus(Focus::None, context);
}
Focus::EntryFullscreen => {
self.set_focus(Focus::Entry, context);
}
Focus::None => {
unreachable!();
}
}
self.unfocused = false;
self.view
.process_event(&mut UIEvent::VisibilityChange(false), context);
self.dirty = true;
/* If self.row_updates is not empty and we exit a thread, the row_update events
* will be performed but the list will not be drawn. So force a draw in any case.
* */
self.force_draw = true;
return true;
}
UIEvent::Input(ref key)
if !self.unfocused()
if !self.unfocused
&& shortcut!(key == shortcuts[Listing::DESCRIPTION]["select_entry"]) =>
{
let env_hash = self.get_env_under_cursor(self.cursor_pos.2, context);
self.selection.entry(env_hash).and_modify(|e| *e = !*e);
}
UIEvent::Action(ref action) => match action {
Action::SubSort(field, order) if !self.unfocused() => {
Action::SubSort(field, order) if !self.unfocused => {
debug!("SubSort {:?} , {:?}", field, order);
self.subsort = (*field, *order);
//if !self.filtered_selection.is_empty() {
@ -1248,7 +1181,7 @@ impl Component for PlainListing {
//}
return true;
}
Action::Sort(field, order) if !self.unfocused() => {
Action::Sort(field, order) if !self.unfocused => {
debug!("Sort {:?} , {:?}", field, order);
self.sort = (*field, *order);
return true;
@ -1256,7 +1189,7 @@ impl Component for PlainListing {
Action::Listing(a @ ListingAction::SetSeen)
| Action::Listing(a @ ListingAction::SetUnseen)
| Action::Listing(a @ ListingAction::Delete)
if !self.unfocused() =>
if !self.unfocused =>
{
let is_selection_empty =
self.selection.values().cloned().any(std::convert::identity);
@ -1362,7 +1295,7 @@ impl Component for PlainListing {
self.dirty = true;
if self.unfocused() {
if self.unfocused {
self.view
.process_event(&mut UIEvent::EnvelopeRename(*old_hash, *new_hash), context);
}
@ -1381,7 +1314,7 @@ impl Component for PlainListing {
self.row_updates.push(*env_hash);
self.dirty = true;
if self.unfocused() {
if self.unfocused {
self.view
.process_event(&mut UIEvent::EnvelopeUpdate(*env_hash), context);
}
@ -1393,7 +1326,7 @@ impl Component for PlainListing {
self.dirty = true;
}
UIEvent::Input(Key::Esc)
if !self.unfocused()
if !self.unfocused
&& self.selection.values().cloned().any(std::convert::identity) =>
{
for v in self.selection.values_mut() {
@ -1402,13 +1335,13 @@ impl Component for PlainListing {
self.dirty = true;
return true;
}
UIEvent::Input(Key::Esc) if !self.unfocused() && !self.filter_term.is_empty() => {
UIEvent::Input(Key::Esc) if !self.unfocused && !self.filter_term.is_empty() => {
self.set_coordinates((self.new_cursor_pos.0, self.new_cursor_pos.1));
self.set_dirty(true);
self.refresh_mailbox(context, false);
return true;
}
UIEvent::Action(Action::Listing(Search(ref filter_term))) if !self.unfocused() => {
UIEvent::Action(Action::Listing(Search(ref filter_term))) if !self.unfocused => {
match context.accounts[&self.cursor_pos.0].search(
filter_term,
self.sort,
@ -1456,28 +1389,30 @@ impl Component for PlainListing {
}
false
}
fn is_dirty(&self) -> bool {
match self.focus {
Focus::None => self.dirty,
Focus::Entry | Focus::EntryFullscreen => self.view.is_dirty(),
}
self.dirty
|| if self.unfocused {
self.view.is_dirty()
} else {
false
}
}
fn set_dirty(&mut self, value: bool) {
self.dirty = value;
if self.unfocused() {
if self.unfocused {
self.view.set_dirty(value);
}
}
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
let mut map = if self.unfocused() {
let mut map = if self.unfocused {
self.view.get_shortcuts(context)
} else {
ShortcutMaps::default()
};
let config_map = context.settings.shortcuts.compact_listing.key_values();
map.insert(PlainListing::DESCRIPTION, config_map);
let config_map = context.settings.shortcuts.listing.key_values();
map.insert(Listing::DESCRIPTION, config_map);

View File

@ -23,7 +23,6 @@ use super::*;
use crate::components::PageMovement;
use std::cmp;
use std::convert::TryInto;
use std::fmt::Write;
macro_rules! row_attr {
($color_cache:expr, $even: expr, $unseen:expr, $highlighted:expr, $selected:expr $(,)*) => {{
@ -129,9 +128,9 @@ pub struct ThreadListing {
/// If we must redraw on next redraw event
dirty: bool,
/// If `self.view` is focused or not.
focus: Focus,
unfocused: bool,
initialised: bool,
view: Option<Box<MailView>>,
view: Option<MailView>,
movement: Option<PageMovement>,
id: ComponentId,
}
@ -411,10 +410,9 @@ impl ListingTrait for ThreadListing {
fn coordinates(&self) -> (AccountHash, MailboxHash) {
(self.new_cursor_pos.0, self.new_cursor_pos.1)
}
fn set_coordinates(&mut self, coordinates: (AccountHash, MailboxHash)) {
self.new_cursor_pos = (coordinates.0, coordinates.1, 0);
self.focus = Focus::None;
self.unfocused = false;
self.view = None;
self.order.clear();
self.row_updates.clear();
@ -743,55 +741,13 @@ impl ListingTrait for ThreadListing {
}
fn unfocused(&self) -> bool {
!matches!(self.focus, Focus::None)
self.unfocused
}
fn set_movement(&mut self, mvm: PageMovement) {
self.movement = Some(mvm);
self.set_dirty(true);
}
fn set_focus(&mut self, new_value: Focus, context: &mut Context) {
match new_value {
Focus::None => {
self.view = None;
self.dirty = true;
/* If self.row_updates is not empty and we exit a thread, the row_update events
* will be performed but the list will not be drawn. So force a draw in any case.
* */
// self.force_draw = true;
}
Focus::Entry => {
// self.force_draw = true;
self.dirty = true;
let coordinates = (
self.cursor_pos.0,
self.cursor_pos.1,
self.get_env_under_cursor(self.cursor_pos.2, context),
);
if let Some(ref mut v) = self.view {
v.update(coordinates, context);
} else {
self.view = Some(Box::new(MailView::new(coordinates, None, None, context)));
}
if let Some(ref mut s) = self.view {
s.set_dirty(true);
}
}
Focus::EntryFullscreen => {
if let Some(ref mut s) = self.view {
s.set_dirty(true);
}
}
}
self.focus = new_value;
}
fn focus(&self) -> Focus {
self.focus
}
}
impl fmt::Display for ThreadListing {
@ -803,7 +759,7 @@ impl fmt::Display for ThreadListing {
impl ThreadListing {
pub fn new(coordinates: (AccountHash, MailboxHash)) -> Box<Self> {
Box::new(ThreadListing {
cursor_pos: (coordinates.0, 0, 0),
cursor_pos: (coordinates.0, MailboxHash(0), 0),
new_cursor_pos: (coordinates.0, coordinates.1, 0),
length: 0,
sort: (Default::default(), Default::default()),
@ -816,7 +772,7 @@ impl ThreadListing {
selection: HashMap::default(),
order: HashMap::default(),
dirty: true,
focus: Focus::None,
unfocused: false,
view: None,
initialised: false,
movement: None,
@ -884,7 +840,7 @@ impl ThreadListing {
});
*/
if show_subject {
let _ = write!(s, "{:.85}", envelope.subject());
s.push_str(&format!("{:.85}", envelope.subject()));
}
s
}
@ -1136,17 +1092,10 @@ impl Component for ThreadListing {
}
}
*/
if !self.is_dirty() {
return;
}
if matches!(self.focus, Focus::EntryFullscreen) {
if let Some(v) = self.view.as_mut() {
return v.draw(grid, area, context);
if !self.unfocused {
if !self.is_dirty() {
return;
}
}
if !self.unfocused() {
self.dirty = false;
/* Draw the entire list */
self.draw_list(grid, area, context);
@ -1239,7 +1188,7 @@ impl Component for ThreadListing {
if let Some(ref mut v) = self.view {
v.update(coordinates, context);
} else {
self.view = Some(Box::new(MailView::new(coordinates, None, None, context)));
self.view = Some(MailView::new(coordinates, None, None, context));
}
if let Some(v) = self.view.as_mut() {
@ -1249,38 +1198,12 @@ impl Component for ThreadListing {
self.dirty = false;
}
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
let shortcuts = self.get_shortcuts(context);
match (&event, self.focus) {
(UIEvent::Input(ref k), Focus::Entry)
if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"]) =>
{
self.set_focus(Focus::EntryFullscreen, context);
return true;
}
(UIEvent::Input(ref k), Focus::EntryFullscreen)
if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) =>
{
self.set_focus(Focus::Entry, context);
return true;
}
(UIEvent::Input(ref k), Focus::Entry)
if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) =>
{
self.set_focus(Focus::None, context);
return true;
}
_ => {}
}
if let Some(ref mut v) = self.view {
if !matches!(self.focus, Focus::None) && v.process_event(event, context) {
if v.process_event(event, context) {
return true;
}
}
match *event {
UIEvent::ConfigReload { old_settings: _ } => {
self.color_cache = ColorCache {
@ -1315,43 +1238,18 @@ impl Component for ThreadListing {
}
self.set_dirty(true);
}
UIEvent::Input(ref k)
if matches!(self.focus, Focus::None)
&& (shortcut!(k == shortcuts[Listing::DESCRIPTION]["open_entry"])
|| shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"])) =>
{
self.set_focus(Focus::Entry, context);
UIEvent::Input(Key::Char('\n')) if !self.unfocused => {
self.unfocused = true;
self.dirty = true;
return true;
}
UIEvent::Input(ref k)
if !matches!(self.focus, Focus::None)
&& shortcut!(k == shortcuts[Listing::DESCRIPTION]["exit_entry"]) =>
{
self.set_focus(Focus::None, context);
return true;
}
UIEvent::Input(ref k)
if !matches!(self.focus, Focus::Entry)
&& shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"]) =>
{
self.set_focus(Focus::EntryFullscreen, context);
return true;
}
UIEvent::Input(ref k)
if !matches!(self.focus, Focus::None)
&& shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) =>
{
match self.focus {
Focus::Entry => {
self.set_focus(Focus::None, context);
}
Focus::EntryFullscreen => {
self.set_focus(Focus::Entry, context);
}
Focus::None => {
unreachable!();
}
UIEvent::Input(Key::Char('i')) if self.unfocused => {
self.unfocused = false;
if let Some(ref mut s) = self.view {
s.process_event(&mut UIEvent::VisibilityChange(false), context);
}
self.dirty = true;
self.view = None;
return true;
}
UIEvent::MailboxUpdate((ref idxa, ref idxf))
@ -1377,7 +1275,7 @@ impl Component for ThreadListing {
self.dirty = true;
if self.unfocused() {
if self.unfocused {
if let Some(v) = self.view.as_mut() {
v.process_event(
&mut UIEvent::EnvelopeRename(*old_hash, *new_hash),
@ -1403,7 +1301,7 @@ impl Component for ThreadListing {
self.dirty = true;
if self.unfocused() {
if self.unfocused {
if let Some(v) = self.view.as_mut() {
v.process_event(&mut UIEvent::EnvelopeUpdate(*env_hash), context);
}
@ -1430,7 +1328,7 @@ impl Component for ThreadListing {
self.refresh_mailbox(context, false);
return true;
}
Action::Listing(Search(ref filter_term)) if !self.unfocused() => {
Action::Listing(Search(ref filter_term)) if !self.unfocused => {
match context.accounts[&self.cursor_pos.0].search(
filter_term,
self.sort,
@ -1481,36 +1379,20 @@ impl Component for ThreadListing {
}
false
}
fn is_dirty(&self) -> bool {
match self.focus {
Focus::None => self.dirty,
Focus::Entry => self.dirty || self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false),
Focus::EntryFullscreen => self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false),
}
self.dirty || self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
}
fn set_dirty(&mut self, value: bool) {
if let Some(p) = self.view.as_mut() {
p.set_dirty(value);
};
self.dirty = value;
}
fn get_shortcuts(&self, context: &Context) -> ShortcutMaps {
let mut map = if self.unfocused() {
self.view
.as_ref()
.map(|p| p.get_shortcuts(context))
.unwrap_or_default()
} else {
ShortcutMaps::default()
};
let config_map = context.settings.shortcuts.listing.key_values();
map.insert(Listing::DESCRIPTION, config_map);
map
self.view
.as_ref()
.map(|p| p.get_shortcuts(context))
.unwrap_or_default()
}
fn id(&self) -> ComponentId {

View File

@ -27,7 +27,6 @@ use melib::list_management;
use melib::parser::BytesExt;
use smallvec::SmallVec;
use std::collections::HashSet;
use std::fmt::Write as _;
use std::io::Write;
use std::convert::TryFrom;
@ -45,7 +44,7 @@ pub use self::envelope::*;
use linkify::LinkFinder;
use xdg_utils::query_default_app;
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[derive(PartialEq, Copy, Clone, Debug)]
enum Source {
Decoded,
Raw,
@ -531,7 +530,7 @@ impl MailView {
error,
} => {
if show_comments {
let _ = writeln!(acc, "Failed to verify signature: {}.\n", error);
acc.push_str(&format!("Failed to verify signature: {}.\n\n", error));
}
acc.push_str(&self.attachment_displays_to_text(
display,
@ -560,7 +559,7 @@ impl MailView {
}
EncryptedPending { .. } => acc.push_str("Waiting for decryption result."),
EncryptedFailed { inner: _, error } => {
let _ = write!(acc, "Decryption failed: {}.", &error);
acc.push_str(&format!("Decryption failed: {}.", &error))
}
EncryptedSuccess {
inner: _,
@ -734,7 +733,7 @@ impl MailView {
inner: Box::new(a.clone()),
});
} else if a.content_type().is_text_html() {
let bytes = a.decode(Default::default());
let bytes = decode(a, None);
let filter_invocation =
mailbox_settings!(context[coordinates.0][&coordinates.1].pager.html_filter)
.as_ref()
@ -789,7 +788,7 @@ impl MailView {
}
}
} else if a.is_text() {
let bytes = a.decode(Default::default());
let bytes = decode(a, None);
acc.push(AttachmentDisplay::InlineText {
inner: Box::new(a.clone()),
comment: None,
@ -811,7 +810,7 @@ impl MailView {
if let Some(text_attachment_pos) =
parts.iter().position(|a| a.content_type == "text/plain")
{
let bytes = &parts[text_attachment_pos].decode(Default::default());
let bytes = decode(&parts[text_attachment_pos], None);
if bytes.trim().is_empty()
&& mailbox_settings!(
context[coordinates.0][&coordinates.1]
@ -1431,9 +1430,7 @@ impl Component for MailView {
let mut text = "Viewing attachment. Press `r` to return \n".to_string();
if let Some(attachment) = self.open_attachment(aidx, context) {
if attachment.is_html() {
let mut subview = Box::new(HtmlView::new(attachment, context));
subview.set_coordinates(Some(self.coordinates));
self.subview = Some(subview);
self.subview = Some(Box::new(HtmlView::new(attachment, context)));
self.mode = ViewMode::Subview;
} else {
text.push_str(&attachment.text());
@ -1464,9 +1461,7 @@ impl Component for MailView {
}
}
ViewMode::Normal if body.is_html() => {
let mut subview = Box::new(HtmlView::new(body, context));
subview.set_coordinates(Some(self.coordinates));
self.subview = Some(subview);
self.subview = Some(Box::new(HtmlView::new(body, context)));
self.mode = ViewMode::Subview;
}
ViewMode::Normal
@ -1487,7 +1482,7 @@ impl Component for MailView {
_ => false,
} =>
{
let mut subview = Box::new(HtmlView::new(
self.subview = Some(Box::new(HtmlView::new(
body.content_type
.parts()
.unwrap()
@ -1495,9 +1490,7 @@ impl Component for MailView {
.find(|a| a.is_html())
.unwrap_or(body),
context,
));
subview.set_coordinates(Some(self.coordinates));
self.subview = Some(subview);
)));
self.mode = ViewMode::Subview;
self.initialised = false;
}
@ -1659,7 +1652,7 @@ impl Component for MailView {
}
fn process_event(&mut self, mut event: &mut UIEvent, context: &mut Context) -> bool {
if self.coordinates.0 == 0 || self.coordinates.1 == 0 {
if self.coordinates.0 .0 == 0 || self.coordinates.1 .0 == 0 {
return false;
}
let shortcuts = self.get_shortcuts(context);
@ -2218,18 +2211,18 @@ impl Component for MailView {
let filename = attachment.filename();
if let Ok(command) = query_default_app(&attachment_type) {
let p = create_temp_file(
&attachment.decode(Default::default()),
&decode(attachment, None),
filename.as_deref(),
None,
true,
);
let exec_cmd = desktop_exec_to_command(
let (exec_cmd, argument) = desktop_exec_to_command(
&command,
p.path.display().to_string(),
false,
);
match Command::new("sh")
.args(&["-c", &exec_cmd])
match Command::new(&exec_cmd)
.arg(&argument)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
@ -2241,8 +2234,8 @@ impl Component for MailView {
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Failed to start `{}`: {}",
&exec_cmd, err
"Failed to start `{} {}`: {}",
&exec_cmd, &argument, err
)),
));
}
@ -2473,7 +2466,7 @@ impl Component for MailView {
path.push(u.as_hyphenated().to_string());
}
}
match save_attachment(&path, &u.decode(Default::default())) {
match save_attachment(&path, &decode(u, None)) {
Err(err) => {
context.replies.push_back(UIEvent::Notification(
Some(format!("Failed to create file at {}", path.display())),
@ -2790,66 +2783,49 @@ fn save_attachment(path: &std::path::Path, bytes: &[u8]) -> Result<()> {
Ok(())
}
fn desktop_exec_to_command(command: &str, path: String, is_url: bool) -> String {
fn desktop_exec_to_command(command: &str, path: String, is_url: bool) -> (String, String) {
/* Purge unused field codes */
let command = command
.replace("%i", "")
.replace("%c", "")
.replace("%k", "");
if command.contains("%f") {
command.replacen("%f", &path.replace(' ', "\\ "), 1)
} else if command.contains("%F") {
command.replacen("%F", &path.replace(' ', "\\ "), 1)
} else if command.contains("%u") || command.contains("%U") {
let from_pattern = if command.contains("%u") { "%u" } else { "%U" };
if let Some(pos) = command.find("%f").or_else(|| command.find("%F")) {
(command[0..pos].trim().to_string(), path)
} else if let Some(pos) = command.find("%u").or_else(|| command.find("%U")) {
if is_url {
command.replacen(from_pattern, &path, 1)
(command[0..pos].trim().to_string(), path)
} else {
command.replacen(
from_pattern,
&format!("file://{}", path).replace(' ', "\\ "),
1,
(
command[0..pos].trim().to_string(),
format!("file://{}", path),
)
}
} else if is_url {
format!("{} {}", command, path)
} else {
format!("{} {}", command, path.replace(' ', "\\ "))
(command, path)
}
}
/*
#[test]
fn test_desktop_exec() {
assert_eq!(
"ristretto /tmp/file".to_string(),
desktop_exec_to_command("ristretto %F", "/tmp/file".to_string(), false)
);
assert_eq!(
"/usr/lib/firefox-esr/firefox-esr file:///tmp/file".to_string(),
desktop_exec_to_command(
"/usr/lib/firefox-esr/firefox-esr %u",
"/tmp/file".to_string(),
false
)
);
assert_eq!(
"/usr/lib/firefox-esr/firefox-esr www.example.com".to_string(),
desktop_exec_to_command(
"/usr/lib/firefox-esr/firefox-esr %u",
"www.example.com".to_string(),
true
)
);
assert_eq!(
"/usr/bin/vlc --started-from-file www.example.com".to_string(),
desktop_exec_to_command(
"/usr/bin/vlc --started-from-file %U",
"www.example.com".to_string(),
true
)
);
assert_eq!(
"zathura --fork file:///tmp/file".to_string(),
desktop_exec_to_command("zathura --fork %U", "file:///tmp/file".to_string(), true)
);
for cmd in [
"ristretto %F",
"/usr/lib/firefox-esr/firefox-esr %u",
"/usr/bin/vlc --started-from-file %U",
"zathura %U",
]
.iter()
{
println!(
"cmd = {} output = {:?}, is_url = false",
cmd,
desktop_exec_to_command(cmd, "/tmp/file".to_string(), false)
);
println!(
"cmd = {} output = {:?}, is_url = true",
cmd,
desktop_exec_to_command(cmd, "www.example.com".to_string(), true)
);
}
}
*/

View File

@ -25,7 +25,7 @@ use std::process::{Command, Stdio};
use xdg_utils::query_default_app;
#[derive(PartialEq, Eq, Debug)]
#[derive(PartialEq, Debug)]
enum ViewMode {
Normal,
Url,
@ -83,8 +83,9 @@ impl EnvelopeView {
/// Returns the string to be displayed in the Viewer
fn attachment_to_text(&self, body: &Attachment, context: &mut Context) -> String {
let finder = LinkFinder::new();
let body_text = String::from_utf8_lossy(&body.decode_rec(DecodeOptions {
filter: Some(Box::new(|a: &Attachment, v: &mut Vec<u8>| {
let body_text = String::from_utf8_lossy(&decode_rec(
body,
Some(Box::new(|a: &Attachment, v: &mut Vec<u8>| {
if a.content_type().is_text_html() {
let settings = &context.settings;
if let Some(filter_invocation) = settings.pager.html_filter.as_ref() {
@ -122,8 +123,7 @@ impl EnvelopeView {
}
}
})),
..Default::default()
}))
))
.into_owned();
match self.mode {
ViewMode::Normal | ViewMode::Subview => {
@ -134,7 +134,7 @@ impl EnvelopeView {
.iter()
.enumerate()
.fold(t, |mut s, (idx, a)| {
let _ = writeln!(s, "[{}] {}\n", idx, a);
s.push_str(&format!("[{}] {}\n\n", idx, a));
s
});
}
@ -161,7 +161,7 @@ impl EnvelopeView {
.iter()
.enumerate()
.fold(t, |mut s, (idx, a)| {
let _ = writeln!(s, "[{}] {}\n", idx, a);
s.push_str(&format!("[{}] {}\n\n", idx, a));
s
});
}
@ -370,8 +370,7 @@ impl Component for EnvelopeView {
self.mode = ViewMode::Subview;
let colors = crate::conf::value(context, "mail.view.body");
self.subview = Some(Box::new(Pager::from_string(
String::from_utf8_lossy(&u.decode_rec(Default::default()))
.to_string(),
String::from_utf8_lossy(&decode_rec(u, None)).to_string(),
Some(context),
None,
None,
@ -398,18 +397,18 @@ impl Component for EnvelopeView {
let filename = u.filename();
if let Ok(command) = query_default_app(&attachment_type) {
let p = create_temp_file(
&u.decode(Default::default()),
&decode(u, None),
filename.as_deref(),
None,
true,
);
let exec_cmd = super::desktop_exec_to_command(
let (exec_cmd, argument) = super::desktop_exec_to_command(
&command,
p.path.display().to_string(),
false,
);
match Command::new("sh")
.args(&["-c", &exec_cmd])
match Command::new(&exec_cmd)
.arg(&argument)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
@ -421,8 +420,8 @@ impl Component for EnvelopeView {
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Failed to start `{}`: {}",
&exec_cmd, err
"Failed to start `{} {}`: {}",
&exec_cmd, &argument, err
)),
));
}

View File

@ -27,14 +27,13 @@ use std::process::{Command, Stdio};
pub struct HtmlView {
pager: Pager,
bytes: Vec<u8>,
coordinates: Option<(AccountHash, MailboxHash, EnvelopeHash)>,
id: ComponentId,
}
impl HtmlView {
pub fn new(body: &Attachment, context: &mut Context) -> Self {
let id = ComponentId::new_v4();
let bytes: Vec<u8> = body.decode_rec(Default::default());
let bytes: Vec<u8> = decode_rec(body, None);
let settings = &context.settings;
let mut display_text = if let Some(filter_invocation) = settings.pager.html_filter.as_ref()
@ -106,22 +105,13 @@ impl HtmlView {
.iter()
.enumerate()
.fold(display_text, |mut s, (idx, a)| {
let _ = writeln!(s, "[{}] {}\n\n", idx, a);
s.push_str(&format!("[{}] {}\n\n\n", idx, a));
s
});
}
let colors = crate::conf::value(context, "mail.view.body");
let pager = Pager::from_string(display_text, None, None, None, colors);
HtmlView {
pager,
bytes,
id,
coordinates: None,
}
}
pub fn set_coordinates(&mut self, new_value: Option<(AccountHash, MailboxHash, EnvelopeHash)>) {
self.coordinates = new_value;
HtmlView { pager, bytes, id }
}
}
@ -141,20 +131,12 @@ impl Component for HtmlView {
}
if let UIEvent::Input(Key::Char('v')) = event {
let command = if let Some(coordinates) = self.coordinates {
mailbox_settings!(context[coordinates.0][&coordinates.1].pager.html_open)
.as_ref()
.map(|s| s.to_string())
.or_else(|| query_default_app("text/html").ok())
} else {
query_default_app("text/html").ok()
};
if let Some(command) = command {
if let Ok(command) = query_default_app("text/html") {
let p = create_temp_file(&self.bytes, None, None, true);
let exec_cmd =
let (exec_cmd, argument) =
super::desktop_exec_to_command(&command, p.path.display().to_string(), false);
match Command::new("sh")
.args(&["-c", &exec_cmd])
match Command::new(&exec_cmd)
.arg(&argument)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
@ -166,8 +148,8 @@ impl Component for HtmlView {
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Failed to start `{}`: {}",
&exec_cmd, err
"Failed to start `{} {}`: {}",
&exec_cmd, &argument, err
)),
));
}

View File

@ -100,7 +100,7 @@ mod dbus {
| Some(NotificationType::Error(melib::ErrorKind::External)) => {
notification.icon("dialog-error");
}
Some(NotificationType::Error(melib::ErrorKind::Network(_))) => {
Some(NotificationType::Error(melib::ErrorKind::Network)) => {
notification.icon("network-error");
}
Some(NotificationType::Error(melib::ErrorKind::Timeout)) => {
@ -151,14 +151,13 @@ mod dbus {
'"' => ret.push_str("&quot;"),
_ => {
let i = c as u32;
if (0x1..=0x8).contains(&i)
|| (0xb..=0xc).contains(&i)
|| (0xe..=0x1f).contains(&i)
|| (0x7f..=0x84).contains(&i)
|| (0x86..=0x9f).contains(&i)
if (0x1 <= i && i <= 0x8)
|| (0xb <= i && i <= 0xc)
|| (0xe <= i && i <= 0x1f)
|| (0x7f <= i && i <= 0x84)
|| (0x86 <= i && i <= 0x9f)
{
use std::fmt::Write;
let _ = write!(ret, "&#{:x}%{:x};", i, i);
ret.push_str(&format!("&#{:x}%{:x};", i, i));
} else {
ret.push(c);
}
@ -200,14 +199,7 @@ impl Component for NotificationCommand {
}
}
let mut script = context.settings.notifications.script.as_ref();
if *kind == Some(NotificationType::NewMail)
&& context.settings.notifications.new_mail_script.is_some()
{
script = context.settings.notifications.new_mail_script.as_ref();
}
if let Some(ref bin) = script {
if let Some(ref bin) = context.settings.notifications.script {
match Command::new(bin)
.arg(&kind.map(|k| k.to_string()).unwrap_or_default())
.arg(title.as_ref().map(String::as_str).unwrap_or("meli"))

View File

@ -1547,7 +1547,7 @@ impl Component for Tabbed {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq)]
pub struct RawBuffer {
pub buf: CellBuffer,
title: Option<String>,

View File

@ -27,7 +27,7 @@ const OK_LENGTH: usize = "OK".len();
const CANCEL_OFFSET: usize = "OK ".len();
const CANCEL_LENGTH: usize = "Cancel".len();
#[derive(Debug, Copy, PartialEq, Eq, Clone)]
#[derive(Debug, Copy, PartialEq, Clone)]
enum SelectorCursor {
Unfocused,
/// Cursor is at an entry

View File

@ -26,7 +26,7 @@ use std::time::Duration;
type AutoCompleteFn = Box<dyn Fn(&Context, &str) -> Vec<AutoCompleteEntry> + Send + Sync>;
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
enum FormFocus {
Fields,
Buttons,
@ -390,7 +390,7 @@ impl fmt::Display for Field {
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum FormButtonActions {
Accept,
Reset,
@ -870,7 +870,7 @@ where
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[derive(Debug, PartialEq, Clone)]
pub struct AutoCompleteEntry {
pub entry: String,
pub description: String,
@ -911,7 +911,7 @@ impl From<(String, String)> for AutoCompleteEntry {
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[derive(Debug, PartialEq, Clone)]
pub struct AutoComplete {
entries: Vec<AutoCompleteEntry>,
content: CellBuffer,

View File

@ -155,24 +155,24 @@ impl FileMailboxConf {
use crate::conf::deserializers::extra_settings;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FileAccount {
pub root_mailbox: String,
pub format: String,
pub identity: String,
root_mailbox: String,
format: String,
identity: String,
#[serde(default)]
pub extra_identities: Vec<String>,
extra_identities: Vec<String>,
#[serde(default = "none")]
pub display_name: Option<String>,
display_name: Option<String>,
#[serde(default = "false_val")]
pub read_only: bool,
read_only: bool,
#[serde(default)]
pub subscribed_mailboxes: Vec<String>,
subscribed_mailboxes: Vec<String>,
#[serde(default)]
pub mailboxes: IndexMap<String, FileMailboxConf>,
mailboxes: IndexMap<String, FileMailboxConf>,
#[serde(default)]
pub search_backend: SearchBackend,
search_backend: SearchBackend,
#[serde(default)]
pub order: (SortField, SortOrder),
order: (SortField, SortOrder),
#[serde(default = "false_val")]
pub manual_refresh: bool,
#[serde(default = "none")]
@ -345,22 +345,18 @@ impl FileSettings {
if path_string.is_empty() {
return Err(MeliError::new("No configuration found."));
}
#[cfg(not(test))]
let ask = Ask {
message: format!(
"No configuration found. Would you like to generate one in {}?",
path_string
),
};
#[cfg(not(test))]
if ask.run() {
create_config_file(&config_path)?;
return Err(MeliError::new(
"Edit the sample configuration and relaunch meli.",
));
}
#[cfg(test)]
return Ok(FileSettings::default());
return Err(MeliError::new("No configuration file found."));
}
@ -577,7 +573,7 @@ impl Settings {
}
}
#[derive(Copy, Debug, Clone, Hash, PartialEq, Eq)]
#[derive(Copy, Debug, Clone, Hash, PartialEq)]
pub enum IndexStyle {
Plain,
Threaded,
@ -707,7 +703,7 @@ impl Serialize for IndexStyle {
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum SearchBackend {
None,
Auto,
@ -729,12 +725,7 @@ impl<'de> Deserialize<'de> for SearchBackend {
let s = <String>::deserialize(deserializer)?;
match s.as_str() {
#[cfg(feature = "sqlite3")]
sqlite3
if sqlite3.eq_ignore_ascii_case("sqlite3")
|| sqlite3.eq_ignore_ascii_case("sqlite") =>
{
Ok(SearchBackend::Sqlite3)
}
sqlite3 if sqlite3.eq_ignore_ascii_case("sqlite3") => Ok(SearchBackend::Sqlite3),
none if none.eq_ignore_ascii_case("none")
|| none.eq_ignore_ascii_case("nothing")
|| none.is_empty() =>
@ -766,26 +757,15 @@ pub fn create_config_file(p: &Path) -> Result<()> {
.write(true)
.create_new(true)
.open(p)
.chain_err_summary(|| format!("Cannot create configuration file in {}", p.display()))?;
.expect("Could not create config file.");
file.write_all(include_bytes!("../docs/samples/sample-config.toml"))
.and_then(|()| file.flush())
.chain_err_summary(|| format!("Could not write to configuration file {}", p.display()))?;
.expect("Could not write to config file.");
println!("Written example configuration to {}", p.display());
let set_permissions = |file: std::fs::File| -> Result<()> {
let metadata = file.metadata()?;
let mut permissions = metadata.permissions();
let metadata = file.metadata()?;
let mut permissions = metadata.permissions();
permissions.set_mode(0o600); // Read/write for owner only.
file.set_permissions(permissions)?;
Ok(())
};
if let Err(err) = set_permissions(file) {
println!(
"Warning: Could not set permissions of {} to 0o600: {}",
p.display(),
err
);
}
permissions.set_mode(0o600); // Read/write for owner only.
file.set_permissions(permissions)?;
Ok(())
}
@ -924,9 +904,9 @@ mod pp {
#[serde(deny_unknown_fields)]
pub struct LogSettings {
#[serde(default)]
pub log_file: Option<PathBuf>,
log_file: Option<PathBuf>,
#[serde(default)]
pub maximum_level: melib::LoggingLevel,
maximum_level: melib::LoggingLevel,
}
pub use dotaddressable::*;
@ -1265,7 +1245,7 @@ send_mail = '/bin/false'
.write_all("[composing]\nsend_mail = '/bin/false'\n".as_bytes())
.unwrap();
let err = FileSettings::validate(new_file.path.clone(), false, true).unwrap_err();
assert_eq!(err.summary.as_ref(), "Configuration error (account-name): root_mailbox `/path/to/root/mailbox` is not a valid directory.");
assert_eq!(err.summary.as_ref(), "Configuration error (account-name): root_path `/path/to/root/mailbox` is not a valid directory.");
/* Test unrecognised configuration entries error */

View File

@ -489,31 +489,6 @@ impl Account {
.unwrap();
}
}
#[cfg(feature = "sqlite3")]
if settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 {
let db_path = match crate::sqlite3::db_path() {
Err(err) => {
sender
.send(ThreadEvent::UIEvent(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!("Error with setting up an sqlite3 search database for account `{}`: {}", name, err))
)))
.unwrap();
None
}
Ok(path) => Some(path),
};
if let Some(db_path) = db_path {
if !db_path.exists() {
sender
.send(ThreadEvent::UIEvent(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!("An sqlite3 search database for account `{}` seems to be missing, a new one must be created with the `reindex` command.", name))
)))
.unwrap();
}
}
}
Ok(Account {
hash,
name,
@ -644,8 +619,7 @@ impl Account {
acc.push_str(", ");
acc
});
mailbox_comma_sep_list_string
.drain(mailbox_comma_sep_list_string.len().saturating_sub(2)..);
mailbox_comma_sep_list_string.drain(mailbox_comma_sep_list_string.len() - 2..);
melib::log(
format!(
"Account `{}` has the following mailboxes: [{}]",
@ -653,14 +627,6 @@ impl Account {
),
melib::WARN,
);
self.sender
.send(ThreadEvent::UIEvent(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Account `{}` has the following mailboxes: [{}]",
&self.name, mailbox_comma_sep_list_string,
)),
)))
.unwrap();
}
let mut tree: Vec<MailboxNode> = Vec::new();
@ -1146,7 +1112,7 @@ impl Account {
}
pub fn load(&mut self, mailbox_hash: MailboxHash) -> result::Result<(), usize> {
if mailbox_hash == 0 {
if mailbox_hash.0 == 0 {
return Err(0);
}
match self.mailbox_entries[&mailbox_hash].status {
@ -1238,11 +1204,11 @@ impl Account {
),
melib::INFO,
);
Err(MeliError::new(format!(
return Err(MeliError::new(format!(
"Message was stored in {} so that you can restore it manually.",
file.path.display()
))
.set_summary("Could not save in any mailbox"))
.set_summary("Could not save in any mailbox"));
}
}
@ -1643,6 +1609,11 @@ impl Account {
match job {
JobRequest::Mailboxes { ref mut handle } => {
if let Ok(Some(mailboxes)) = handle.chan.try_recv() {
self.sender
.send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange(
self.hash,
)))
.unwrap();
if let Err(err) = mailboxes.and_then(|mailboxes| self.init(mailboxes)) {
if err.kind.is_authentication() {
self.sender
@ -1664,13 +1635,6 @@ impl Account {
};
self.insert_job(handle.job_id, JobRequest::Mailboxes { handle });
};
} else {
self.sender
.send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange(
self.hash,
Some("Loaded mailboxes.".into()),
)))
.unwrap();
}
}
}
@ -1766,7 +1730,7 @@ impl Account {
if let Ok(Some(is_online)) = handle.chan.try_recv() {
self.sender
.send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange(
self.hash, None,
self.hash,
)))
.unwrap();
if is_online.is_ok() {
@ -1821,7 +1785,7 @@ impl Account {
self.is_online = Ok(());
self.sender
.send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange(
self.hash, None,
self.hash,
)))
.unwrap();
}
@ -1841,7 +1805,7 @@ impl Account {
self.is_online = Err(err);
self.sender
.send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange(
self.hash, None,
self.hash,
)))
.unwrap();
}

View File

@ -129,12 +129,6 @@ pub struct ListingSettings {
/// Default: "📎"
#[serde(default)]
pub attachment_flag: Option<String>,
/// Should threads with differentiating Subjects show a list of those subjects on the entry
/// title?
/// Default: "true"
#[serde(default = "true_val")]
pub thread_subject_pack: bool,
}
const fn default_divider() -> char {
@ -164,7 +158,6 @@ impl Default for ListingSettings {
thread_snoozed_flag: None,
selected_flag: None,
attachment_flag: None,
thread_subject_pack: true,
}
}
}
@ -199,7 +192,6 @@ impl DotAddressable for ListingSettings {
"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),
"thread_subject_pack" => self.thread_subject_pack.lookup(field, tail),
other => Err(MeliError::new(format!(
"{} has no field named {}",
parent_field, other

View File

@ -31,26 +31,17 @@ pub struct NotificationsSettings {
/// Default: True
#[serde(default = "true_val")]
pub enable: bool,
/// A command to pipe notifications through.
/// A command to pipe notifications through
/// Default: None
#[serde(default = "none")]
pub script: Option<String>,
/// A command to pipe new mail notifications through (preferred over `script`).
/// Default: None
#[serde(default = "none")]
pub new_mail_script: Option<String>,
/// A file location which has its size changed when new mail arrives (max 128 bytes). Can be
/// used to trigger new mail notifications eg with `xbiff(1)`.
/// used to trigger new mail notifications eg with `xbiff(1)`
/// Default: None
#[serde(default = "none", alias = "xbiff-file-path")]
pub xbiff_file_path: Option<String>,
#[serde(default = "internal_value_false", alias = "play-sound")]
pub play_sound: ToggleFlag,
#[serde(default = "none", alias = "sound-file")]
pub sound_file: Option<String>,
}
@ -60,7 +51,6 @@ impl Default for NotificationsSettings {
Self {
enable: true,
script: None,
new_mail_script: None,
xbiff_file_path: None,
play_sound: ToggleFlag::InternalVal(false),
sound_file: None,
@ -75,7 +65,6 @@ impl DotAddressable for NotificationsSettings {
match *field {
"enable" => self.enable.lookup(field, tail),
"script" => self.script.lookup(field, tail),
"new_mail_script" => self.new_mail_script.lookup(field, tail),
"xbiff_file_path" => self.xbiff_file_path.lookup(field, tail),
"play_sound" => self.play_sound.lookup(field, tail),
"sound_file" => self.sound_file.lookup(field, tail),

View File

@ -88,11 +88,6 @@ pub struct PagerSettingsOverride {
#[serde(deserialize_with = "non_empty_string")]
#[serde(default)]
pub url_launcher: Option<Option<String>>,
#[doc = " A command to open html files."]
#[doc = " Default: None"]
#[serde(deserialize_with = "non_empty_string", alias = "html-open")]
#[serde(default)]
pub html_open: Option<Option<String>>,
}
impl Default for PagerSettingsOverride {
fn default() -> Self {
@ -109,7 +104,6 @@ impl Default for PagerSettingsOverride {
auto_choose_multipart_alternative: None,
show_date_in_my_timezone: None,
url_launcher: None,
html_open: None,
}
}
}
@ -177,11 +171,6 @@ pub struct ListingSettingsOverride {
#[doc = " Default: \"📎\""]
#[serde(default)]
pub attachment_flag: Option<Option<String>>,
#[doc = " Should threads with differentiating Subjects show a list of those subjects on the entry"]
#[doc = " title?"]
#[doc = " Default: \"true\""]
#[serde(default)]
pub thread_subject_pack: Option<bool>,
}
impl Default for ListingSettingsOverride {
fn default() -> Self {
@ -202,7 +191,6 @@ impl Default for ListingSettingsOverride {
thread_snoozed_flag: None,
selected_flag: None,
attachment_flag: None,
thread_subject_pack: None,
}
}
}
@ -214,16 +202,12 @@ pub struct NotificationsSettingsOverride {
#[doc = " Default: True"]
#[serde(default)]
pub enable: Option<bool>,
#[doc = " A command to pipe notifications through."]
#[doc = " A command to pipe notifications through"]
#[doc = " Default: None"]
#[serde(default)]
pub script: Option<Option<String>>,
#[doc = " A command to pipe new mail notifications through (preferred over `script`)."]
#[doc = " Default: None"]
#[serde(default)]
pub new_mail_script: Option<Option<String>>,
#[doc = " A file location which has its size changed when new mail arrives (max 128 bytes). Can be"]
#[doc = " used to trigger new mail notifications eg with `xbiff(1)`."]
#[doc = " used to trigger new mail notifications eg with `xbiff(1)`"]
#[doc = " Default: None"]
#[serde(alias = "xbiff-file-path")]
#[serde(default)]
@ -240,7 +224,6 @@ impl Default for NotificationsSettingsOverride {
NotificationsSettingsOverride {
enable: None,
script: None,
new_mail_script: None,
xbiff_file_path: None,
play_sound: None,
sound_file: None,
@ -257,6 +240,9 @@ pub struct ShortcutsOverride {
pub listing: Option<ListingShortcuts>,
#[serde(default)]
pub composing: Option<ComposingShortcuts>,
#[serde(alias = "compact-listing")]
#[serde(default)]
pub compact_listing: Option<CompactListingShortcuts>,
#[serde(alias = "contact-list")]
#[serde(default)]
pub contact_list: Option<ContactListShortcuts>,
@ -275,6 +261,7 @@ impl Default for ShortcutsOverride {
general: None,
listing: None,
composing: None,
compact_listing: None,
contact_list: None,
envelope_view: None,
thread_view: None,

View File

@ -87,25 +87,14 @@ 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>,
/// A command to open html files.
/// Default: None
#[serde(
default = "none",
deserialize_with = "non_empty_string",
alias = "html-open"
)]
pub html_open: Option<String>,
}
impl Default for PagerSettings {
@ -117,7 +106,6 @@ impl Default for PagerSettings {
pager_ratio: 80,
filter: None,
html_filter: None,
html_open: None,
format_flowed: true,
split_long_lines: true,
minimum_width: 80,
@ -140,7 +128,6 @@ impl DotAddressable for PagerSettings {
"pager_ratio" => self.pager_ratio.lookup(field, tail),
"filter" => self.filter.lookup(field, tail),
"html_filter" => self.html_filter.lookup(field, tail),
"html_open" => self.html_open.lookup(field, tail),
"format_flowed" => self.format_flowed.lookup(field, tail),
"split_long_lines" => self.split_long_lines.lookup(field, tail),
"minimum_width" => self.minimum_width.lookup(field, tail),

View File

@ -43,6 +43,8 @@ pub struct Shortcuts {
pub listing: ListingShortcuts,
#[serde(default)]
pub composing: ComposingShortcuts,
#[serde(default, alias = "compact-listing")]
pub compact_listing: CompactListingShortcuts,
#[serde(default, alias = "contact-list")]
pub contact_list: ContactListShortcuts,
#[serde(default, alias = "envelope-view")]
@ -62,6 +64,9 @@ impl DotAddressable for Shortcuts {
"general" => self.general.lookup(field, tail),
"listing" => self.listing.lookup(field, tail),
"composing" => self.composing.lookup(field, tail),
"compact_listing" | "compact-listing" => {
self.compact_listing.lookup(field, tail)
}
"contact_list" | "contact-list" => self.contact_list.lookup(field, tail),
"envelope_view" | "envelope-view" => self.envelope_view.lookup(field, tail),
"thread_view" | "thread-view" => self.thread_view.lookup(field, tail),
@ -136,6 +141,14 @@ macro_rules! shortcut_key_values {
}
}
shortcut_key_values! { "compact-listing",
/// Shortcut listing for a mail listing in compact mode.
pub struct CompactListingShortcuts {
exit_thread |> "Exit thread view." |> Key::Char('i'),
open_thread |> "Open thread." |> Key::Char('\n')
}
}
shortcut_key_values! { "listing",
/// Shortcut listing for a mail listing.
pub struct ListingShortcuts {
@ -159,11 +172,7 @@ shortcut_key_values! { "listing",
select_entry |> "Select thread entry." |> Key::Char('v'),
increase_sidebar |> "Increase sidebar width." |> Key::Ctrl('p'),
decrease_sidebar |> "Decrease sidebar width." |> Key::Ctrl('o'),
toggle_menu_visibility |> "Toggle visibility of side menu in mail list." |> Key::Char('`'),
focus_left |> "Switch focus on the left." |> Key::Left,
focus_right |> "Switch focus on the right." |> Key::Right,
exit_entry |> "Exit e-mail entry." |> Key::Char('i'),
open_entry |> "Open e-mail entry." |> Key::Char('\n')
toggle_menu_visibility |> "Toggle visibility of side menu in mail list." |> Key::Char('`')
}
}
@ -203,8 +212,7 @@ shortcut_key_values! { "general",
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('<'),
focus_in_text_field |> "Focus on a text field." |> Key::Char('\n')
info_message_previous |> "Show previous info message, if any" |> Key::Alt('<')
}
}

View File

@ -36,7 +36,6 @@ use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use smallvec::SmallVec;
use std::borrow::Cow;
use std::collections::HashSet;
use std::fmt::Write;
#[inline(always)]
pub fn value(context: &Context, key: &'static str) -> ThemeAttribute {
@ -237,10 +236,6 @@ fn unlink_attrs<'k, 't: 'k>(theme: &'t Theme, mut key: &'k str) -> Attr {
const DEFAULT_KEYS: &[&str] = &[
"theme_default",
"text.normal",
"text.unfocused",
"text.error",
"text.highlight",
"error_message",
"email_header",
"highlight",
@ -1241,28 +1236,26 @@ impl Themes {
t => self.other_themes.get(t).unwrap_or(&self.dark),
};
let mut ret = String::new();
let _ = writeln!(ret, "[terminal.themes.{}]", key);
ret.push_str(&format!("[terminal.themes.{}]\n", key));
if unlink {
for k in theme.keys() {
let _ = writeln!(
ret,
"\"{}\" = {{ fg = {}, bg = {}, attrs = {} }}",
ret.push_str(&format!(
"\"{}\" = {{ fg = {}, bg = {}, attrs = {} }}\n",
k,
toml::to_string(&unlink_fg(theme, &ColorField::Fg, k)).unwrap(),
toml::to_string(&unlink_bg(theme, &ColorField::Bg, k)).unwrap(),
toml::to_string(&unlink_attrs(theme, k)).unwrap(),
);
));
}
} else {
for k in theme.keys() {
let _ = writeln!(
ret,
"\"{}\" = {{ fg = {}, bg = {}, attrs = {} }}",
ret.push_str(&format!(
"\"{}\" = {{ fg = {}, bg = {}, attrs = {} }}\n",
k,
toml::to_string(&theme[k].fg).unwrap(),
toml::to_string(&theme[k].bg).unwrap(),
toml::to_string(&theme[k].attrs).unwrap(),
);
));
}
}
ret
@ -1302,22 +1295,11 @@ impl Default for Themes {
light.insert($key.into(), ThemeAttributeInner::default());
dark.insert($key.into(), ThemeAttributeInner::default());
};
($key:literal, $copy_from:literal) => {
light.insert($key.into(), light[$copy_from].clone());
dark.insert($key.into(), dark[$copy_from].clone());
};
}
add!("theme_default", dark = { fg: Color::Default, bg: Color::Default, attrs: Attr::DEFAULT }, light = { fg: Color::Default, bg: Color::Default, attrs: Attr::DEFAULT });
add!("error_message", dark = { fg: Color::Red, bg: "theme_default", attrs: "theme_default" }, light = { fg: Color::Red, bg: "theme_default", attrs: "theme_default" });
add!("error_message", dark = { fg: Color::Byte(243), bg: Color::Default, attrs: Attr::DEFAULT }, light = { fg: Color::Byte(243), bg: Color::Default, attrs: Attr::DEFAULT });
/* text palettes */
add!("text.normal", "theme_default");
add!("text.unfocused", dark = { fg: Color::GREY, bg: "theme_default", attrs: Attr::DIM }, light = { fg: Color::GREY, bg: "theme_default", attrs: Attr::DIM });
add!("text.error", "error_message");
add!("text.highlight", dark = { fg: Color::Blue, bg: "theme_default", attrs: Attr::REVERSE }, light = { fg: Color::Blue, bg: "theme_default", attrs: Attr::REVERSE });
/* rest */
add!("email_header", dark = { fg: Color::Byte(33), bg: Color::Default, attrs: Attr::DEFAULT }, light = { fg: Color::Byte(33), bg: Color::Default, attrs: Attr::DEFAULT });
add!("highlight", dark = { fg: Color::Byte(240), bg: Color::Byte(237), attrs: Attr::BOLD }, light = { fg: Color::Byte(240), bg: Color::Byte(237), attrs: Attr::BOLD });

View File

@ -19,12 +19,11 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
//! # mailcap file - Find mailcap entries to execute attachments.
//!
//! Implements [RFC 1524 A User Agent Configuration Mechanism For Multimedia Mail Format
//! Information](https://www.rfc-editor.org/rfc/inline-errata/rfc1524.html)
/*! Find mailcap entries to execute attachments.
*/
use crate::state::Context;
use crate::types::{create_temp_file, ForkType, UIEvent};
use melib::attachments::decode;
use melib::text_processing::GlobMatch;
use melib::{email::Attachment, MeliError, Result};
use std::collections::HashMap;
@ -160,8 +159,7 @@ impl MailcapEntry {
.map(|arg| match *arg {
"%s" => {
needs_stdin = false;
let _f =
create_temp_file(&a.decode(Default::default()), None, None, true);
let _f = create_temp_file(&decode(a, None), None, None, true);
let p = _f.path().display().to_string();
f = Some(_f);
p
@ -193,11 +191,7 @@ impl MailcapEntry {
.stdout(Stdio::piped())
.spawn()?;
child
.stdin
.as_mut()
.unwrap()
.write_all(&a.decode(Default::default()))?;
child.stdin.as_mut().unwrap().write_all(&decode(a, None))?;
child.wait_with_output()?.stdout
} else {
let child = Command::new("sh")
@ -214,8 +208,7 @@ impl MailcapEntry {
std::borrow::Cow::from("less")
};
let mut pager = Command::new("sh")
.args(["-c", pager_cmd.as_ref()])
let mut pager = Command::new(pager_cmd.as_ref())
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.spawn()?;
@ -228,11 +221,7 @@ impl MailcapEntry {
.stdout(Stdio::inherit())
.spawn()?;
child
.stdin
.as_mut()
.unwrap()
.write_all(&a.decode(Default::default()))?;
child.stdin.as_mut().unwrap().write_all(&decode(a, None))?;
debug!(child.wait_with_output()?.stdout);
} else {
let child = Command::new("sh")

View File

@ -74,10 +74,9 @@ fn notify(
#[cfg(feature = "cli-docs")]
fn parse_manpage(src: &str) -> Result<ManPages> {
match src {
"" | "meli" | "meli.1" | "main" => Ok(ManPages::Main),
"meli.7" | "guide" => Ok(ManPages::Guide),
"meli.conf" | "meli.conf.5" | "conf" | "config" | "configuration" => Ok(ManPages::Conf),
"meli-themes" | "meli-themes.5" | "themes" | "theming" | "theme" => Ok(ManPages::Themes),
"" | "meli" | "main" => Ok(ManPages::Main),
"meli.conf" | "conf" | "config" | "configuration" => Ok(ManPages::Conf),
"meli-themes" | "themes" | "theming" | "theme" => Ok(ManPages::Themes),
_ => Err(MeliError::new(format!(
"Invalid documentation page: {}",
src
@ -95,8 +94,6 @@ enum ManPages {
Conf = 1,
/// meli-themes(5)
Themes = 2,
/// meli(7)
Guide = 3,
}
#[derive(Debug, StructOpt)]
@ -146,7 +143,7 @@ enum SubCommand {
#[derive(Debug, StructOpt)]
struct ManOpt {
#[structopt(default_value = "meli", possible_values=&["meli", "conf", "themes", "meli.7", "guide"], value_name="PAGE", parse(try_from_str = parse_manpage))]
#[structopt(default_value = "meli", possible_values=&["meli", "conf", "themes"], value_name="PAGE", parse(try_from_str = parse_manpage))]
#[cfg(feature = "cli-docs")]
page: ManPages,
/// If true, output text in stdout instead of spawning $PAGER.
@ -199,11 +196,10 @@ fn run_app(opt: Opt) -> Result<()> {
#[cfg(feature = "cli-docs")]
Some(SubCommand::Man(manopt)) => {
let ManOpt { page, no_raw } = manopt;
const MANPAGES: [&[u8]; 4] = [
const MANPAGES: [&[u8]; 3] = [
include_bytes!(concat!(env!("OUT_DIR"), "/meli.txt.gz")),
include_bytes!(concat!(env!("OUT_DIR"), "/meli.conf.txt.gz")),
include_bytes!(concat!(env!("OUT_DIR"), "/meli-themes.txt.gz")),
include_bytes!(concat!(env!("OUT_DIR"), "/meli.7.txt.gz")),
];
use flate2::bufread::GzDecoder;
use std::io::prelude::*;
@ -233,13 +229,12 @@ fn run_app(opt: Opt) -> Result<()> {
}
use std::process::{Command, Stdio};
let mut handle = Command::new("sh")
.arg("-c")
.arg(std::env::var("PAGER").unwrap_or_else(|_| "more".to_string()))
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()?;
let mut handle =
Command::new(std::env::var("PAGER").unwrap_or_else(|_| "more".to_string()))
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()?;
handle.stdin.take().unwrap().write_all(v.as_bytes())?;
handle.wait()?;
@ -247,7 +242,7 @@ fn run_app(opt: Opt) -> Result<()> {
}
#[cfg(not(feature = "cli-docs"))]
Some(SubCommand::Man(_manopt)) => {
return Err(MeliError::new("error: this version of meli was not build with embedded documentation (cargo feature `cli-docs`). You might have it installed as manpages (eg `man meli`), otherwise check https://meli.delivery"));
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")]
@ -319,7 +314,12 @@ fn run_app(opt: Opt) -> Result<()> {
sender,
receiver.clone(),
)?;
state.register_component(Box::new(EnvelopeView::new(wrapper, None, None, 0)));
state.register_component(Box::new(EnvelopeView::new(
wrapper,
None,
None,
AccountHash(0),
)));
} else {
state = State::new(None, sender, receiver.clone())?;
#[cfg(feature = "svgscreenshot")]

View File

@ -24,15 +24,17 @@ extern crate melib;
use melib::*;
use std::collections::VecDeque;
extern crate xdg_utils;
#[macro_use]
extern crate serde_derive;
extern crate linkify;
extern crate uuid;
extern crate serde_json;
extern crate smallvec;
extern crate termion;
use melib::backends::imap::managesieve::ManageSieveConnection;
use melib::backends::imap::managesieve::new_managesieve_connection;
use melib::Result;
#[macro_use]
@ -62,7 +64,7 @@ pub mod sqlite3;
pub mod jobs;
pub mod mailcap;
//pub mod plugins;
pub mod plugins;
use futures::executor::block_on;
@ -82,7 +84,10 @@ fn main() -> Result<()> {
std::process::exit(1);
}
let (config_path, account_name) = (std::mem::take(&mut args[0]), std::mem::take(&mut args[1]));
let (config_path, account_name) = (
std::mem::replace(&mut args[0], String::new()),
std::mem::replace(&mut args[1], String::new()),
);
std::env::set_var("MELI_CONFIG", config_path);
let settings = conf::Settings::new()?;
if !settings.accounts.contains_key(&account_name) {
@ -97,47 +102,12 @@ fn main() -> Result<()> {
);
std::process::exit(1);
}
let account = &settings.accounts[&account_name].account;
let mut conn = ManageSieveConnection::new(
0,
account_name.clone(),
account,
melib::backends::BackendEventConsumer::new(std::sync::Arc::new(|_, _| {})),
)?;
block_on(conn.inner.connect())?;
let mut conn = new_managesieve_connection(&settings.accounts[&account_name].account)?;
block_on(conn.connect())?;
let mut res = String::with_capacity(8 * 1024);
let mut input = String::new();
const AVAILABLE_COMMANDS: &[&str] = &[
"help",
"logout",
"listscripts",
"checkscript",
"putscript",
"setactive",
"getscript",
"deletescript",
];
const COMMANDS_HELP: &[&str] = &[
"help",
"logout",
"listscripts and whether they are active",
"paste a script to check for validity without uploading it",
"upload a script",
"set a script as active",
"get a script by its name",
"delete a script by its name",
];
println!("managesieve shell: use 'help' for available commands");
enum PrevCmd {
None,
Checkscript,
PutscriptName,
PutscriptString(String),
SetActiveName,
GetScriptName,
}
use PrevCmd::*;
let mut prev_cmd: PrevCmd = None;
println!("managesieve shell: use 'logout'");
loop {
use std::io;
use std::io::Write;
@ -146,85 +116,12 @@ fn main() -> Result<()> {
io::stdout().flush().unwrap();
match io::stdin().read_line(&mut input) {
Ok(_) => {
let input = input.trim();
if input.eq_ignore_ascii_case("logout") {
if input.trim().eq_ignore_ascii_case("logout") {
break;
}
if input.eq_ignore_ascii_case("help") {
println!("available commands: [{}]", AVAILABLE_COMMANDS.join(", "));
continue;
}
if input.len() >= "help ".len()
&& input[0.."help ".len()].eq_ignore_ascii_case("help ")
{
if let Some(i) = AVAILABLE_COMMANDS
.iter()
.position(|cmd| cmd.eq_ignore_ascii_case(&input["help ".len()..]))
{
println!("{}", COMMANDS_HELP[i]);
} else {
println!("invalid command `{}`", &input["help ".len()..]);
}
continue;
}
if input.eq_ignore_ascii_case("listscripts") {
let scripts = block_on(conn.listscripts())?;
println!("Got {} scripts:", scripts.len());
for (script, active) in scripts {
println!(
"{}active: {}",
if active { "" } else { "in" },
String::from_utf8_lossy(&script)
);
}
} else if input.eq_ignore_ascii_case("checkscript") {
prev_cmd = Checkscript;
println!("insert file path of script");
} else if input.eq_ignore_ascii_case("putscript") {
prev_cmd = PutscriptName;
println!("Insert script name");
} else if input.eq_ignore_ascii_case("setactive") {
prev_cmd = SetActiveName;
} else if input.eq_ignore_ascii_case("getscript") {
prev_cmd = GetScriptName;
} else if input.eq_ignore_ascii_case("deletescript") {
println!("unimplemented `{}`", input);
} else {
match prev_cmd {
None => println!("invalid command `{}`", input),
Checkscript => {
let content = std::fs::read_to_string(&input).unwrap();
let result = block_on(conn.checkscript(content.as_bytes()));
println!("Got {:?}", result);
prev_cmd = None;
}
PutscriptName => {
prev_cmd = PutscriptString(input.to_string());
println!("insert file path of script");
}
PutscriptString(name) => {
prev_cmd = None;
let content = std::fs::read_to_string(&input).unwrap();
let result =
block_on(conn.putscript(name.as_bytes(), content.as_bytes()));
println!("Got {:?}", result);
}
SetActiveName => {
prev_cmd = None;
let result = block_on(conn.setactive(input.as_bytes()));
println!("Got {:?}", result);
}
GetScriptName => {
prev_cmd = None;
let result = block_on(conn.getscript(input.as_bytes()));
println!("Got {:?}", result);
}
}
}
//block_on(conn.send_command(input.as_bytes()))?;
//block_on(conn.read_lines(&mut res, String::new()))?;
//println!("out: {}", res.trim());
block_on(conn.send_command(input.as_bytes()))?;
block_on(conn.read_lines(&mut res, String::new()))?;
println!("out: {}", res.trim());
}
Err(error) => println!("error: {}", error),
}

View File

@ -36,7 +36,7 @@ pub use rpc::*;
pub const BACKEND_FN: i8 = 0;
pub const BACKEND_OP_FN: i8 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum PluginKind {
LongLived,
Filter,

View File

@ -97,7 +97,7 @@ impl InputHandler {
/// A context container for loaded settings, accounts, UI changes, etc.
pub struct Context {
pub accounts: IndexMap<AccountHash, Account>,
pub settings: Box<Settings>,
pub settings: Settings,
/// Areas of the screen that must be redrawn in the next render
pub dirty_areas: VecDeque<Area>,
@ -145,16 +145,10 @@ impl Context {
}
accounts[account_pos].watch();
replies.push_back(UIEvent::AccountStatusChange(
accounts[account_pos].hash(),
None,
));
replies.push_back(UIEvent::AccountStatusChange(accounts[account_pos].hash()));
}
if ret.is_ok() != was_online {
replies.push_back(UIEvent::AccountStatusChange(
accounts[account_pos].hash(),
None,
));
replies.push_back(UIEvent::AccountStatusChange(accounts[account_pos].hash()));
}
ret
}
@ -163,87 +157,18 @@ impl Context {
let idx = self.accounts.get_index_of(&account_hash).unwrap();
self.is_online_idx(idx)
}
#[cfg(test)]
pub fn new_mock() -> Self {
let (sender, receiver) =
crossbeam::channel::bounded(32 * ::std::mem::size_of::<ThreadEvent>());
let job_executor = Arc::new(JobExecutor::new(sender.clone()));
let input_thread = unbounded();
let input_thread_pipe = nix::unistd::pipe()
.map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync + 'static>)
.unwrap();
let backends = Backends::new();
let settings = Box::new(Settings::new().unwrap());
let accounts = vec![{
let name = "test".to_string();
let mut account_conf = AccountConf::default();
account_conf.conf.format = "maildir".to_string();
account_conf.account.format = "maildir".to_string();
account_conf.account.root_mailbox = "/tmp/".to_string();
let sender = sender.clone();
let account_hash = {
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
let mut hasher = DefaultHasher::new();
hasher.write(name.as_bytes());
hasher.finish()
};
Account::new(
account_hash,
name,
account_conf,
&backends,
job_executor.clone(),
sender.clone(),
BackendEventConsumer::new(Arc::new(
move |account_hash: AccountHash, ev: BackendEvent| {
sender
.send(ThreadEvent::UIEvent(UIEvent::BackendEvent(
account_hash,
ev,
)))
.unwrap();
},
)),
)
.unwrap()
}];
let accounts = accounts.into_iter().map(|acc| (acc.hash(), acc)).collect();
let working = Arc::new(());
let control = Arc::downgrade(&working);
Context {
accounts,
settings,
dirty_areas: VecDeque::with_capacity(0),
replies: VecDeque::with_capacity(0),
temp_files: Vec::new(),
job_executor,
children: vec![],
input_thread: InputHandler {
pipe: input_thread_pipe,
rx: input_thread.1,
tx: input_thread.0,
control,
state_tx: sender.clone(),
},
sender,
receiver,
}
}
}
/// 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: Box<Screen>,
screen: Screen,
draw_rate_limit: RateLimit,
child: Option<ForkType>,
pub mode: UIMode,
overlay: Vec<Box<dyn Component>>,
components: Vec<Box<dyn Component>>,
pub context: Box<Context>,
pub context: Context,
timer: thread::JoinHandle<()>,
display_messages: SmallVec<[DisplayMessage; 8]>,
@ -297,11 +222,11 @@ impl State {
let input_thread_pipe = nix::unistd::pipe()
.map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync + 'static>)?;
let backends = Backends::new();
let settings = Box::new(if let Some(settings) = settings {
let settings = if let Some(settings) = settings {
settings
} else {
Settings::new()?
});
};
/*
let mut plugin_manager = PluginManager::new();
for (_, p) in settings.plugins.clone() {
@ -333,7 +258,7 @@ impl State {
use std::hash::Hasher;
let mut hasher = DefaultHasher::new();
hasher.write(n.as_bytes());
hasher.finish()
AccountHash(hasher.finish())
};
Account::new(
account_hash,
@ -376,7 +301,7 @@ impl State {
let working = Arc::new(());
let control = Arc::downgrade(&working);
let mut s = State {
screen: Box::new(Screen {
screen: Screen {
cols,
rows,
grid: CellBuffer::new(cols, rows, Cell::with_char(' ')),
@ -388,7 +313,7 @@ impl State {
} else {
Screen::draw_horizontal_segment_no_color
},
}),
},
child: None,
mode: UIMode::Normal,
components: Vec::with_capacity(8),
@ -402,7 +327,7 @@ impl State {
display_messages_dirty: false,
display_messages_initialised: false,
display_messages_area: ((0, 0), (0, 0)),
context: Box::new(Context {
context: Context {
accounts,
settings,
dirty_areas: VecDeque::with_capacity(5),
@ -420,7 +345,7 @@ impl State {
},
sender,
receiver,
}),
},
};
if s.context.settings.terminal.ascii_drawing {
s.screen.grid.set_ascii_drawing(true);
@ -464,7 +389,7 @@ impl State {
}
let Context {
ref mut accounts, ..
} = &mut *self.context;
} = &mut self.context;
if let Some(notification) = accounts[&account_hash].reload(event, mailbox_hash) {
if let UIEvent::Notification(_, _, _) = notification {
@ -1017,10 +942,10 @@ impl State {
if toml::Value::try_from(&new_settings) == toml::Value::try_from(&self.context.settings) {
return Err("No changes detected.".into());
}
Ok(Box::new(new_settings))
Ok(new_settings)
}) {
Ok(new_settings) => {
let old_settings = std::mem::replace(&mut self.context.settings, new_settings);
let old_settings = Box::new(std::mem::replace(&mut self.context.settings, new_settings));
self.context.replies.push_back(UIEvent::ConfigReload {
old_settings
});
@ -1090,10 +1015,6 @@ impl State {
)));
return;
}
UIEvent::BackendEvent(account_hash, BackendEvent::AccountStateChange { message }) => {
self.rcv_event(UIEvent::AccountStatusChange(account_hash, Some(message)));
return;
}
UIEvent::BackendEvent(_, BackendEvent::Refresh(refresh_event)) => {
self.refresh_event(refresh_event);
return;

View File

@ -463,7 +463,6 @@ pub mod screen {
use termion::{clear, cursor};
pub type StateStdout =
termion::screen::AlternateScreen<termion::raw::RawTerminal<BufWriter<std::io::Stdout>>>;
pub struct Screen {
pub cols: usize,
pub rows: usize,

View File

@ -34,7 +34,7 @@ use termion::color::{AnsiValue, Rgb as TermionRgb};
///
/// # Examples
///
/// ```no_run
/// ```
/// use meli::Color;
///
/// // The default color.
@ -456,7 +456,7 @@ impl<'de> Deserialize<'de> for Color {
#[test]
fn test_color_de() {
#[derive(Debug, Deserialize, PartialEq, Eq)]
#[derive(Debug, Deserialize, PartialEq)]
struct V {
k: Color,
}
@ -754,267 +754,3 @@ impl Serialize for Color {
}
}
}
pub use aliases::*;
pub mod aliases {
use super::Color;
impl Color {
pub const BLACK: Color = Color::Black;
pub const MAROON: Color = Color::Byte(1);
pub const GREEN: Color = Color::Green;
pub const OLIVE: Color = Color::Byte(3);
pub const NAVY: Color = Color::Byte(4);
pub const PURPLE: Color = Color::Magenta;
pub const TEAL: Color = Color::Cyan;
pub const SILVER: Color = Color::Byte(7);
pub const GREY: Color = Color::Byte(8);
pub const RED: Color = Color::Byte(9);
pub const LIME: Color = Color::Byte(10);
pub const YELLOW: Color = Color::Byte(11);
pub const BLUE: Color = Color::Byte(12);
pub const FUCHSIA: Color = Color::Byte(13);
pub const AQUA: Color = Color::Byte(14);
pub const WHITE: Color = Color::Byte(15);
pub const GREY0: Color = Color::Byte(16);
pub const NAVYBLUE: Color = Color::Byte(17);
pub const DARKBLUE: Color = Color::Byte(18);
pub const BLUE3: Color = Color::Byte(19);
pub const BLUE3_: Color = Color::Byte(20);
pub const BLUE1: Color = Color::Byte(21);
pub const DARKGREEN: Color = Color::Byte(22);
pub const DEEPSKYBLUE4: Color = Color::Byte(23);
pub const DEEPSKYBLUE4_: Color = Color::Byte(24);
pub const DEEPSKYBLUE4__: Color = Color::Byte(25);
pub const DODGERBLUE3: Color = Color::Byte(26);
pub const DODGERBLUE2: Color = Color::Byte(27);
pub const GREEN4: Color = Color::Byte(28);
pub const SPRINGGREEN4: Color = Color::Byte(29);
pub const TURQUOISE4: Color = Color::Byte(30);
pub const DEEPSKYBLUE3: Color = Color::Byte(31);
pub const DEEPSKYBLUE3_: Color = Color::Byte(32);
pub const DODGERBLUE1: Color = Color::Byte(33);
pub const GREEN3: Color = Color::Byte(34);
pub const SPRINGGREEN3: Color = Color::Byte(35);
pub const DARKCYAN: Color = Color::Byte(36);
pub const LIGHTSEAGREEN: Color = Color::Byte(37);
pub const DEEPSKYBLUE2: Color = Color::Byte(38);
pub const DEEPSKYBLUE1: Color = Color::Byte(39);
pub const GREEN3_: Color = Color::Byte(40);
pub const SPRINGGREEN3_: Color = Color::Byte(41);
pub const SPRINGGREEN2: Color = Color::Byte(42);
pub const CYAN3: Color = Color::Byte(43);
pub const DARKTURQUOISE: Color = Color::Byte(44);
pub const TURQUOISE2: Color = Color::Byte(45);
pub const GREEN1: Color = Color::Byte(46);
pub const SPRINGGREEN2_: Color = Color::Byte(47);
pub const SPRINGGREEN1: Color = Color::Byte(48);
pub const MEDIUMSPRINGGREEN: Color = Color::Byte(49);
pub const CYAN2: Color = Color::Byte(50);
pub const CYAN1: Color = Color::Byte(51);
pub const DARKRED: Color = Color::Byte(52);
pub const DEEPPINK4: Color = Color::Byte(53);
pub const PURPLE4: Color = Color::Byte(54);
pub const PURPLE4_: Color = Color::Byte(55);
pub const PURPLE3: Color = Color::Byte(56);
pub const BLUEVIOLET: Color = Color::Byte(57);
pub const ORANGE4: Color = Color::Byte(58);
pub const GREY37: Color = Color::Byte(59);
pub const MEDIUMPURPLE4: Color = Color::Byte(60);
pub const SLATEBLUE3: Color = Color::Byte(61);
pub const SLATEBLUE3_: Color = Color::Byte(62);
pub const ROYALBLUE1: Color = Color::Byte(63);
pub const CHARTREUSE4: Color = Color::Byte(64);
pub const DARKSEAGREEN4: Color = Color::Byte(65);
pub const PALETURQUOISE4: Color = Color::Byte(66);
pub const STEELBLUE: Color = Color::Byte(67);
pub const STEELBLUE3: Color = Color::Byte(68);
pub const CORNFLOWERBLUE: Color = Color::Byte(69);
pub const CHARTREUSE3: Color = Color::Byte(70);
pub const DARKSEAGREEN4_: Color = Color::Byte(71);
pub const CADETBLUE: Color = Color::Byte(72);
pub const CADETBLUE_: Color = Color::Byte(73);
pub const SKYBLUE3: Color = Color::Byte(74);
pub const STEELBLUE1: Color = Color::Byte(75);
pub const CHARTREUSE3_: Color = Color::Byte(76);
pub const PALEGREEN3: Color = Color::Byte(77);
pub const SEAGREEN3: Color = Color::Byte(78);
pub const AQUAMARINE3: Color = Color::Byte(79);
pub const MEDIUMTURQUOISE: Color = Color::Byte(80);
pub const STEELBLUE1_: Color = Color::Byte(81);
pub const CHARTREUSE2: Color = Color::Byte(82);
pub const SEAGREEN2: Color = Color::Byte(83);
pub const SEAGREEN1: Color = Color::Byte(84);
pub const SEAGREEN1_: Color = Color::Byte(85);
pub const AQUAMARINE1: Color = Color::Byte(86);
pub const DARKSLATEGRAY2: Color = Color::Byte(87);
pub const DARKRED_: Color = Color::Byte(88);
pub const DEEPPINK4_: Color = Color::Byte(89);
pub const DARKMAGENTA: Color = Color::Byte(90);
pub const DARKMAGENTA_: Color = Color::Byte(91);
pub const DARKVIOLET: Color = Color::Byte(92);
pub const PURPLE_: Color = Color::Byte(93);
pub const ORANGE4_: Color = Color::Byte(94);
pub const LIGHTPINK4: Color = Color::Byte(95);
pub const PLUM4: Color = Color::Byte(96);
pub const MEDIUMPURPLE3: Color = Color::Byte(97);
pub const MEDIUMPURPLE3_: Color = Color::Byte(98);
pub const SLATEBLUE1: Color = Color::Byte(99);
pub const YELLOW4: Color = Color::Byte(100);
pub const WHEAT4: Color = Color::Byte(101);
pub const GREY53: Color = Color::Byte(102);
pub const LIGHTSLATEGREY: Color = Color::Byte(103);
pub const MEDIUMPURPLE: Color = Color::Byte(104);
pub const LIGHTSLATEBLUE: Color = Color::Byte(105);
pub const YELLOW4_: Color = Color::Byte(106);
pub const DARKOLIVEGREEN3: Color = Color::Byte(107);
pub const DARKSEAGREEN: Color = Color::Byte(108);
pub const LIGHTSKYBLUE3: Color = Color::Byte(109);
pub const LIGHTSKYBLUE3_: Color = Color::Byte(110);
pub const SKYBLUE2: Color = Color::Byte(111);
pub const CHARTREUSE2_: Color = Color::Byte(112);
pub const DARKOLIVEGREEN3_: Color = Color::Byte(113);
pub const PALEGREEN3_: Color = Color::Byte(114);
pub const DARKSEAGREEN3: Color = Color::Byte(115);
pub const DARKSLATEGRAY3: Color = Color::Byte(116);
pub const SKYBLUE1: Color = Color::Byte(117);
pub const CHARTREUSE1: Color = Color::Byte(118);
pub const LIGHTGREEN: Color = Color::Byte(119);
pub const LIGHTGREEN_: Color = Color::Byte(120);
pub const PALEGREEN1: Color = Color::Byte(121);
pub const AQUAMARINE1_: Color = Color::Byte(122);
pub const DARKSLATEGRAY1: Color = Color::Byte(123);
pub const RED3: Color = Color::Byte(124);
pub const DEEPPINK4__: Color = Color::Byte(125);
pub const MEDIUMVIOLETRED: Color = Color::Byte(126);
pub const MAGENTA3: Color = Color::Byte(127);
pub const DARKVIOLET_: Color = Color::Byte(128);
pub const PURPLE__: Color = Color::Byte(129);
pub const DARKORANGE3: Color = Color::Byte(130);
pub const INDIANRED: Color = Color::Byte(131);
pub const HOTPINK3: Color = Color::Byte(132);
pub const MEDIUMORCHID3: Color = Color::Byte(133);
pub const MEDIUMORCHID: Color = Color::Byte(134);
pub const MEDIUMPURPLE2: Color = Color::Byte(135);
pub const DARKGOLDENROD: Color = Color::Byte(136);
pub const LIGHTSALMON3: Color = Color::Byte(137);
pub const ROSYBROWN: Color = Color::Byte(138);
pub const GREY63: Color = Color::Byte(139);
pub const MEDIUMPURPLE2_: Color = Color::Byte(140);
pub const MEDIUMPURPLE1: Color = Color::Byte(141);
pub const GOLD3: Color = Color::Byte(142);
pub const DARKKHAKI: Color = Color::Byte(143);
pub const NAVAJOWHITE3: Color = Color::Byte(144);
pub const GREY69: Color = Color::Byte(145);
pub const LIGHTSTEELBLUE3: Color = Color::Byte(146);
pub const LIGHTSTEELBLUE: Color = Color::Byte(147);
pub const YELLOW3: Color = Color::Byte(148);
pub const DARKOLIVEGREEN3__: Color = Color::Byte(149);
pub const DARKSEAGREEN3_: Color = Color::Byte(150);
pub const DARKSEAGREEN2: Color = Color::Byte(151);
pub const LIGHTCYAN3: Color = Color::Byte(152);
pub const LIGHTSKYBLUE1: Color = Color::Byte(153);
pub const GREENYELLOW: Color = Color::Byte(154);
pub const DARKOLIVEGREEN2: Color = Color::Byte(155);
pub const PALEGREEN1_: Color = Color::Byte(156);
pub const DARKSEAGREEN2_: Color = Color::Byte(157);
pub const DARKSEAGREEN1: Color = Color::Byte(158);
pub const PALETURQUOISE1: Color = Color::Byte(159);
pub const RED3_: Color = Color::Byte(160);
pub const DEEPPINK3: Color = Color::Byte(161);
pub const DEEPPINK3_: Color = Color::Byte(162);
pub const MAGENTA3_: Color = Color::Byte(163);
pub const MAGENTA3__: Color = Color::Byte(164);
pub const MAGENTA2: Color = Color::Byte(165);
pub const DARKORANGE3_: Color = Color::Byte(166);
pub const INDIANRED_: Color = Color::Byte(167);
pub const HOTPINK3_: Color = Color::Byte(168);
pub const HOTPINK2: Color = Color::Byte(169);
pub const ORCHID: Color = Color::Byte(170);
pub const MEDIUMORCHID1: Color = Color::Byte(171);
pub const ORANGE3: Color = Color::Byte(172);
pub const LIGHTSALMON3_: Color = Color::Byte(173);
pub const LIGHTPINK3: Color = Color::Byte(174);
pub const PINK3: Color = Color::Byte(175);
pub const PLUM3: Color = Color::Byte(176);
pub const VIOLET: Color = Color::Byte(177);
pub const GOLD3_: Color = Color::Byte(178);
pub const LIGHTGOLDENROD3: Color = Color::Byte(179);
pub const TAN: Color = Color::Byte(180);
pub const MISTYROSE3: Color = Color::Byte(181);
pub const THISTLE3: Color = Color::Byte(182);
pub const PLUM2: Color = Color::Byte(183);
pub const YELLOW3_: Color = Color::Byte(184);
pub const KHAKI3: Color = Color::Byte(185);
pub const LIGHTGOLDENROD2: Color = Color::Byte(186);
pub const LIGHTYELLOW3: Color = Color::Byte(187);
pub const GREY84: Color = Color::Byte(188);
pub const LIGHTSTEELBLUE1: Color = Color::Byte(189);
pub const YELLOW2: Color = Color::Byte(190);
pub const DARKOLIVEGREEN1: Color = Color::Byte(191);
pub const DARKOLIVEGREEN1_: Color = Color::Byte(192);
pub const DARKSEAGREEN1_: Color = Color::Byte(193);
pub const HONEYDEW2: Color = Color::Byte(194);
pub const LIGHTCYAN1: Color = Color::Byte(195);
pub const RED1: Color = Color::Byte(196);
pub const DEEPPINK2: Color = Color::Byte(197);
pub const DEEPPINK1: Color = Color::Byte(198);
pub const DEEPPINK1_: Color = Color::Byte(199);
pub const MAGENTA2_: Color = Color::Byte(200);
pub const MAGENTA1: Color = Color::Byte(201);
pub const ORANGERED1: Color = Color::Byte(202);
pub const INDIANRED1: Color = Color::Byte(203);
pub const INDIANRED1_: Color = Color::Byte(204);
pub const HOTPINK: Color = Color::Byte(205);
pub const HOTPINK_: Color = Color::Byte(206);
pub const MEDIUMORCHID1_: Color = Color::Byte(207);
pub const DARKORANGE: Color = Color::Byte(208);
pub const SALMON1: Color = Color::Byte(209);
pub const LIGHTCORAL: Color = Color::Byte(210);
pub const PALEVIOLETRED1: Color = Color::Byte(211);
pub const ORCHID2: Color = Color::Byte(212);
pub const ORCHID1: Color = Color::Byte(213);
pub const ORANGE1: Color = Color::Byte(214);
pub const SANDYBROWN: Color = Color::Byte(215);
pub const LIGHTSALMON1: Color = Color::Byte(216);
pub const LIGHTPINK1: Color = Color::Byte(217);
pub const PINK1: Color = Color::Byte(218);
pub const PLUM1: Color = Color::Byte(219);
pub const GOLD1: Color = Color::Byte(220);
pub const LIGHTGOLDENROD2_: Color = Color::Byte(221);
pub const LIGHTGOLDENROD2__: Color = Color::Byte(222);
pub const NAVAJOWHITE1: Color = Color::Byte(223);
pub const MISTYROSE1: Color = Color::Byte(224);
pub const THISTLE1: Color = Color::Byte(225);
pub const YELLOW1: Color = Color::Byte(226);
pub const LIGHTGOLDENROD1: Color = Color::Byte(227);
pub const KHAKI1: Color = Color::Byte(228);
pub const WHEAT1: Color = Color::Byte(229);
pub const CORNSILK1: Color = Color::Byte(230);
pub const GREY100: Color = Color::Byte(231);
pub const GREY3: Color = Color::Byte(232);
pub const GREY7: Color = Color::Byte(233);
pub const GREY11: Color = Color::Byte(234);
pub const GREY15: Color = Color::Byte(235);
pub const GREY19: Color = Color::Byte(236);
pub const GREY23: Color = Color::Byte(237);
pub const GREY27: Color = Color::Byte(238);
pub const GREY30: Color = Color::Byte(239);
pub const GREY35: Color = Color::Byte(240);
pub const GREY39: Color = Color::Byte(241);
pub const GREY42: Color = Color::Byte(242);
pub const GREY46: Color = Color::Byte(243);
pub const GREY50: Color = Color::Byte(244);
pub const GREY54: Color = Color::Byte(245);
pub const GREY58: Color = Color::Byte(246);
pub const GREY62: Color = Color::Byte(247);
pub const GREY66: Color = Color::Byte(248);
pub const GREY70: Color = Color::Byte(249);
pub const GREY74: Color = Color::Byte(250);
pub const GREY78: Color = Color::Byte(251);
pub const GREY82: Color = Color::Byte(252);
pub const GREY85: Color = Color::Byte(253);
pub const GREY89: Color = Color::Byte(254);
pub const GREY93: Color = Color::Byte(255);
}
}

View File

@ -25,6 +25,15 @@ use melib::error::{MeliError, Result};
use melib::text_processing::wcwidth;
use nix::sys::wait::WaitStatus;
use nix::sys::wait::{waitpid, WaitPidFlag};
/**
* `EmbedGrid` manages the terminal grid state of the embed process.
*
* The embed process sends bytes to the master end (see super mod) and interprets them in a state
* machine stored in `State`. Escape codes are translated as changes to the grid, eg changes in a
* cell's colors.
*
* The main process copies the grid whenever the actual terminal is redrawn.
**/
#[derive(Debug)]
enum ScreenBuffer {
@ -32,17 +41,10 @@ enum ScreenBuffer {
Alternate,
}
/// `EmbedGrid` manages the terminal grid state of the embed process.
///
/// The embed process sends bytes to the master end (see super mod) and interprets them in a state
/// machine stored in `State`. Escape codes are translated as changes to the grid, eg changes in a
/// cell's colors.
///
/// The main process copies the grid whenever the actual terminal is redrawn.
#[derive(Debug)]
pub struct EmbedGrid {
cursor: (usize, usize),
/// `[top;bottom]`
/// [top;bottom]
scroll_region: ScrollRegion,
pub alternate_screen: CellBuffer,
pub state: State,
@ -177,7 +179,7 @@ impl EmbedTerminal {
}
}
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
enum CodepointBuf {
None,
TwoCodepoints(u8),

View File

@ -139,7 +139,7 @@ impl PartialEq<Key> for &Key {
}
}
#[derive(PartialEq, Eq)]
#[derive(PartialEq)]
/// Keep track of whether we're accepting normal user input or a pasted string.
enum InputMode {
Normal,
@ -359,7 +359,7 @@ impl Serialize for Key {
#[test]
fn test_key_serde() {
#[derive(Debug, Deserialize, PartialEq, Eq)]
#[derive(Debug, Deserialize, PartialEq)]
struct V {
k: Key,
}

View File

@ -170,7 +170,7 @@ pub fn center_area(area: Area, (width, height): (usize, usize)) -> Area {
)
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum Alignment {
/// Stretch to fill all space if possible, center if no meaningful way to stretch.
Fill,

View File

@ -21,7 +21,7 @@
use melib::text_processing::TextProcessing;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq)]
pub struct UText {
content: String,
cursor_pos: usize,

View File

@ -39,7 +39,6 @@ use super::command::Action;
use super::jobs::{JobExecutor, JobId};
use super::terminal::*;
use crate::components::{Component, ComponentId, ScrollUpdate};
use std::borrow::Cow;
use std::sync::Arc;
use melib::backends::{AccountHash, BackendEvent, MailboxHash};
@ -135,7 +134,7 @@ pub enum UIEvent {
MailboxUpdate((AccountHash, MailboxHash)), // (account_idx, mailbox_idx)
MailboxDelete((AccountHash, MailboxHash)),
MailboxCreate((AccountHash, MailboxHash)),
AccountStatusChange(AccountHash, Option<Cow<'static, str>>),
AccountStatusChange(AccountHash),
ComponentKill(Uuid),
BackendEvent(AccountHash, BackendEvent),
StartupCheck(MailboxHash),
@ -169,7 +168,7 @@ impl From<RefreshEvent> for UIEvent {
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum UIMode {
Normal,
Insert,
@ -195,6 +194,14 @@ impl fmt::Display for UIMode {
}
}
/// An event notification that is passed to Entities for handling.
pub struct Notification {
_title: String,
_content: String,
_timestamp: std::time::Instant,
}
pub 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.