Compare commits

...

47 Commits

Author SHA1 Message Date
spike 59b95f83d2 fix docs 2022-10-30 13:31:23 +01:00
Manos Pitsidianakis 88a1f0d4bc melib/imap/parser: fix FETCH response parsing bug
Closes #160
Closes #128
2022-10-23 21:05:06 +03:00
Manos Pitsidianakis 64346dd3fe melib/parsec: add map_res, quoted_slice, is_a, alt, take, take_literal 2022-10-22 22:47:14 +03:00
Manos Pitsidianakis 17b42b1a6c melib/parsec: add json deserialization tests 2022-10-22 22:47:10 +03:00
Manos Pitsidianakis 6d20abdde7 melib/gpgme: add #[allow(deref_nullptr)] in bindgen tests 2022-10-22 22:45:15 +03:00
Manos Pitsidianakis 803d3414fd melib/imap/managesieve: implement some rfc5804 commands
Try with managesieve REPL in src/managesieve.rs:

cargo run --bin managesieve-client ~/.config/meli/config.toml
"accountname"

rfc5804 <https://www.rfc-editor.org/rfc/rfc5804.html>
2022-10-22 21:14:53 +03:00
Manos Pitsidianakis 3697b7d960 melib/datetime: don't use LC_ category in place of LC_ masks in libc calls
LC_ masks are bit masks, whereas category values are not.

Concerns #159

[imap] all mail timestamps are zero/epoch #159
https://git.meli.delivery/meli/meli/issues/159
2022-10-17 18:06:58 +03:00
Manos Pitsidianakis dd0baa82e9 Spawn user-given command strings with sh -c ".."
If given string contains arguments, Command::new(string) will fail.

Reported in #159 https://git.meli.delivery/meli/meli/issues/159
2022-10-17 17:40:25 +03:00
Manos Pitsidianakis 0ef4dde939 melib/jmap: wrap serde_json deserialize errors in human readable errors 2022-10-13 10:59:10 +03:00
Manos Pitsidianakis 55ed962425 melib/jmap: use server_url instead of server_hostname + server_port in config 2022-10-13 10:40:48 +03:00
Manos Pitsidianakis 46a038dc68 conf.rs: remove interactive messages when #[cfg(test)] 2022-10-09 20:08:36 +03:00
Manos Pitsidianakis 16646976d7 compose: fix reply subject prefixes stripping original prefix
Unintelligent heuristic but should cover most cases?

Configurable subject response prefix #142
https://git.meli.delivery/meli/meli/issues/142

Closes #142
2022-10-09 18:31:01 +03:00
Manos Pitsidianakis ffb12c6d1a conf.rs: make all public struct fields public 2022-10-09 18:30:22 +03:00
Manos Pitsidianakis 7e09b1807f melib/collection: replace _Ref deref unwraps with expect() 2022-10-09 18:28:41 +03:00
Manos Pitsidianakis 129573e0fd melib/maildir: rename root_path to root_mailbox 2022-10-09 18:28:07 +03:00
Manos Pitsidianakis 0c08cb737c melib/jmap: mark mailboxes as subscribed on personal accounts
The spec https://jmap.io/spec-mail.html#mailboxes says a mailbox property `isSubscribed` should be considered true if the account is marked as `isPersonal`.

Closes #157

JMAP incompatible with Stalwart server #157 https://git.meli.delivery/meli/meli/issues/157
2022-10-04 15:58:36 +03:00
Manos Pitsidianakis 117d7fbe04 melib/jmap/rfc8620.rs: make private fields public 2022-10-04 15:51:43 +03:00
Manos Pitsidianakis 347be54305 melib/error: add NetworkErrorKind enum 2022-10-04 15:49:34 +03:00
Manos Pitsidianakis 7935e49a00 conf/accounts.rs: check properly if mailbox request is an error 2022-10-04 15:42:24 +03:00
Manos Pitsidianakis c54a31f7cc listing/offline.rs: break line for error messages 2022-10-04 15:41:40 +03:00
Manos Pitsidianakis c3fdafde3b Documentation touchups 2022-09-26 18:04:53 +03:00
Manos Pitsidianakis c6bdda03cf melib/backends.rs: fix notmuch error shown on any missing backend 2022-09-24 22:23:43 +03:00
Manos Pitsidianakis e450ad0f9c types.rs: remove unused struct 2022-09-19 22:04:10 +03:00
Manos Pitsidianakis 0ed10711ef notifications: add new_mail_script option
Preferred over `script` option for new email notifications
2022-09-19 21:58:59 +03:00
Manos Pitsidianakis d8d43a16fe HtmlView: add html_open config setting
Add config setting in case xdg query default app for text/html mime type
doesn't yield results.
2022-09-19 21:40:12 +03:00
Manos Pitsidianakis b87d54ea3f melib/backends.rs: impl Into<BTreeSet<EnvelopeHash>> for EnvelopeHashBatch 2022-09-19 15:18:25 +03:00
Manos Pitsidianakis a7a50d3078 src/: Box<_> some large fields in biggest types
As reported by `cargo +nightly typesize`
2022-09-19 15:18:25 +03:00
Manos Pitsidianakis b138d9bc61 melib: fix some clippy lints 2022-09-19 15:18:25 +03:00
Manos Pitsidianakis 787c64c2da conf.rs: remove expect()s from create_config_file()
No reason to expect(), just return the error.
2022-09-13 19:30:20 +03:00
Manos Pitsidianakis 0df46a63ec Show error if sqlite3 search backend is set but doesn't exist
Closes #114
2022-09-11 17:42:22 +03:00
Manos Pitsidianakis 94bd84b45d Fix clippy lints for `meli` crate 2022-09-11 15:19:40 +03:00
Manos Pitsidianakis 388d4e35d6 listing/offline.rs: add in-progress messages while connecting in IMAP 2022-09-11 15:00:30 +03:00
Manos Pitsidianakis 9cbbf71e0f melib/email/attachments: Add DecodeOptions struct for decoding 2022-09-11 01:22:06 +03:00
Manos Pitsidianakis 3688369278 melib/smtp: add smtp test 2022-09-10 21:39:56 +03:00
Manos Pitsidianakis 3c0f5d8274 melib/smtp: add BINARYMIME support to smtp client
Concerns #49

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

IMAP: Lemonade profile tracking issue
2022-09-10 19:02:17 +03:00
Manos Pitsidianakis 8c7b001aa5 listing/conversations.rs: add `thread_subject_pack` command to pack different inner thread subjects in entry title 2022-09-09 02:03:13 +03:00
Manos Pitsidianakis 9dc4d4055c listing: add focus_{left,right} shortcuts to switch focus
This allows you to make the mail entry column occupy the whole screen if
you press focus_right (Right key) twice.
2022-09-07 16:39:15 +03:00
Manos Pitsidianakis 3d92b41075 Add cli-docs feature to the default set 2022-09-06 21:59:30 +03:00
Manos Pitsidianakis 7c7115427d docs/meli.7: complete guide document 2022-09-06 21:41:26 +03:00
Manos Pitsidianakis 5fa4b6260c docs/meli.7: add more screenshots 2022-09-05 19:40:53 +03:00
Manos Pitsidianakis 4a20fc42e1 Update CHANGELOG.md 2022-09-05 17:05:39 +03:00
Manos Pitsidianakis f76f4ea3f7 docs: add meli.7, a general tutorial document
This commit also changes some shortcut names.
2022-09-05 16:25:59 +03:00
Manos Pitsidianakis 2de69d17f1 melib/compose: fix erroneous placement of newlnes for wrap_header_preamble suffix 2022-09-03 17:47:58 +03:00
Manos Pitsidianakis cbe593cf31 mail/compose: add configurable header preample suffix and prefix for editing
This commit adds a new configuration value for the composing section of
settings. Quoting the documentation:

 wrap_header_preamble: Option<(String, String)>
 optional

 Wrap header preample when editing a draft in an editor. This allows you
 to write non-plain text email without the preamble creating syntax
 errors. They are stripped when you return from the editor. The values
 should be a two element array of strings, a prefix and suffix. This can
 be useful when for example you're writing Markdown; you can set the
 value to ["<!--",\ "-->"] which wraps the headers in an HTML comment.
2022-09-02 16:09:45 +03:00
Manos Pitsidianakis a484b397c6 melib/notmuch: show informative error messages if libloading fails
Add instructions on how to solve this, and also a config setting
`library_file_path` to set the path manually if necessary.
2022-09-02 15:17:30 +03:00
Manos Pitsidianakis eb5949dc9b melib/error.rs: switch summary<->details identifiers
They are more intuitive like this.
2022-09-02 12:12:12 +03:00
98 changed files with 6256 additions and 2677 deletions

View File

@ -7,6 +7,33 @@ 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,6 +28,15 @@ 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"
@ -153,6 +162,17 @@ 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"
@ -200,6 +220,18 @@ 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"
@ -239,6 +271,21 @@ 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"
@ -442,6 +489,12 @@ 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"
@ -742,7 +795,7 @@ checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
dependencies = [
"cfg-if 1.0.0",
"libc",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
@ -792,6 +845,19 @@ 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"
@ -896,6 +962,15 @@ 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"
@ -1003,7 +1078,35 @@ dependencies = [
"dirs-next",
"objc-foundation",
"objc_id",
"time",
"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",
]
[[package]]
@ -1074,6 +1177,7 @@ dependencies = [
"isahc",
"libc",
"libloading",
"mailin-embedded",
"native-tls",
"nix",
"nom",
@ -1084,6 +1188,7 @@ dependencies = [
"serde_json",
"smallvec",
"smol",
"stderrlog",
"unicode-segmentation",
"uuid",
"xdg",
@ -1249,6 +1354,25 @@ 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"
@ -1537,6 +1661,21 @@ 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"
@ -1551,6 +1690,27 @@ 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"
@ -1576,12 +1736,28 @@ 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"
@ -1715,6 +1891,25 @@ 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"
@ -1791,6 +1986,15 @@ 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"
@ -1803,6 +2007,12 @@ 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"
@ -1841,6 +2051,17 @@ 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"
@ -1964,6 +2185,12 @@ 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"
@ -2016,12 +2243,92 @@ 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-meli"
#path = "src/managesieve.rs"
[[bin]]
name = "managesieve-client"
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"]
default = ["sqlite3", "notmuch", "regexp", "smtp", "dbus-notifications", "gpgme", "cli-docs"]
notmuch = ["melib/notmuch_backend", ]
jmap = ["melib/jmap_backend",]
sqlite3 = ["melib/sqlite3"]

View File

@ -24,11 +24,13 @@ Official mirrors:
## Documentation
See also [Quickstart tutorial](https://meli.delivery/documentation.html#quick-start).
See a comprehensive tour of `meli` in the manual page [`meli(7)`](./docs/meli.7).
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").
See also the [Quickstart tutorial](https://meli.delivery/documentation.html#quick-start) online.
meli by default looks for a configuration file in this location: `$XDG_CONFIG_HOME/meli/config.toml`
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`
You can run meli with arbitrary configuration files by setting the `$MELI_CONFIG`
environment variable to their locations, i.e.:
@ -46,12 +48,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
@ -63,8 +65,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*)
@ -75,11 +77,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
@ -102,7 +104,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,68 +39,37 @@ fn main() {
{
use flate2::Compression;
use flate2::GzBuilder;
const MANDOC_OPTS: &[&'static str] = &["-T", "utf8", "-I", "os=Generated by mandoc(1)"];
const MANDOC_OPTS: &[&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 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 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 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();
};
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();
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");
}
}

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,6 +17,29 @@
.\" 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
@ -43,7 +66,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
@ -85,14 +108,12 @@ See
for the available configuration options.
.Pp
At any time, you may press
.Cm \&?
.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
The main visual navigation tool, the left-side sidebar may be toggled with
.Cm `
(shortcuts.listing:
.Ic toggle_menu_visibility Ns
).
.ShortcutPeriod ` listing toggle_menu_visibility
\&.
.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.
@ -105,23 +126,22 @@ 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
.Cm a
.Po
shortcut
.Ic open_attachment
.Pc .
.ShortcutPeriod a envelope_view open_attachment
\&.
.Nm
will attempt to open text inside its pager, and other content via
.Cm xdg-open Ns
\&.
Press
.Cm m
.Po
shortcut
.Ic open_mailcap
.Pc
.Shortcut m envelope_view open_mailcap
instead to use the mailcap entry for the MIME type of the attachment, if any.
See
.Sx FILES
@ -129,12 +149,12 @@ for the location of the mailcap files and
.Xr mailcap 5
for their syntax.
You can save individual attachments with the
.Em COMMAND
.Cm save-attachment Ar INDEX Ar path-to-file
where
.Command save-attachment Ar INDEX Ar path-to-file
command.
.Ar INDEX
is the attachment's index in the listing.
If the zeroth index is provided, the entire message is saved.
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 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.
@ -163,9 +183,8 @@ 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
.Cm index Ar ACCOUNT_NAME Ns \&.
.sp
in the configuration file and to create the sqlite3 index issue command:
.Command index Ar ACCOUNT_NAME Ns
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
@ -234,35 +253,30 @@ 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
.Cm tag add TAG
.Command tag add TAG
and
.Cm tag remove TAG
.Command 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 visiblity)
for how to set tag colors and tag visibility)
.Sh COMPOSING
.Ss Opening the message Composer tab
To create a new mail message, press
.Cm m
(shortcut
.Ic new_mail Ns
) while viewing a mailbox.
.Shortcut m listing new_mail
while viewing a mailbox.
To reply to a mail, press
.Cm R
.Po
shortcut
.Ic reply
.Pc .
.ShortcutPeriod R envelope_view reply
\&.
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
.Cm enter
.Shortcut Enter general focus_in_text_field
to enter
.Em INSERT
mode and
@ -270,10 +284,8 @@ mode and
key to exit.
.It
At any time you may press
.Cm e
(shortcut
.Ic edit_mail Ns
) to launch your editor (see
.Shortcut e composing edit_mail Ns
to launch your editor (see
.Xr meli.conf 5 COMPOSING Ns
, setting
.Ic editor_command
@ -285,19 +297,23 @@ Your editor can be used in
.Ic embed
to
.Em true
in your composing settings.
in your composing settings
.Po
You can return to
.Nm
at any time by pressing
.Aq Ctrl-Z
.Pc
.It
When launched, your editor captures all input until it exits or stops.
.It
To stop your editor and return to
.Nm
press Ctrl-z and to resume editing press the
press
.Aq Ctrl-z
and to resume editing press the
.Ic edit_mail
command again
.Po
default
.Em e
.Pc .
command again.
.El
.Ss Attachments
Attachments may be handled with the
@ -307,14 +323,12 @@ Attachments may be handled with the
commands (see below).
.Ss Sending
Finally, pressing
.Cm s
(shortcut
.Ic send_mail Ns
) will send your message according to your settings
.Shortcut s composing send_mail
will send your message according to your settings
.Po
see
.Xr meli.conf 5 COMPOSING Ns
, setting
, setting name
.Ic send_mail
.Pc Ns
\&.
@ -363,9 +377,9 @@ is the default mode
commands are issued in
.Em COMMAND
mode, by default started with
.Cm \&:
.Shortcut \&: general enter_command_mode
and exited with
.Cm Esc
.Aq Esc
key.
.It EMBED
is the mode of the embed terminal emulator
@ -585,19 +599,72 @@ 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
.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.
.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
.Sh AUTHORS
Copyright 2017-2019
.An Manos Pitsidianakis Aq epilys@nessuent.xyz
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.)
This software carries no warranty of any kind (See COPYING for full copyright and warranty notices).
.Pp
.Aq https://meli.delivery
.Lk https://meli.delivery

742
docs/meli.7 100644
View File

@ -0,0 +1,742 @@
.\" 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

@ -182,7 +182,11 @@ Its format is described below in
\&.
.El
.Ss notmuch only
.Ic root_mailbox
notmuch is supported by loading the dynamic library libnotmuch.
If its location is missing from your library paths, you must add it yourself.
Alternatively, you can specify its path by using a setting.
.Bl -tag -width 36n
.It Ic root_mailbox
points to the directory which contains the
.Pa .notmuch/
subdirectory.
@ -192,10 +196,15 @@ You must explicitly state the mailboxes you want in the
field and set the
.Ar query
property to each of them.
.It Ic library_file_path Ar Path
Use an arbitrary location of libnotmuch by specifying its full filesystem path.
.Pq Em optional
.El
Example:
.Bd -literal
[accounts.notmuch]
format = "notmuch"
#library_file_path = "/opt/homebrew/lib/libnotmuch.5.dylib"
\&...
[accounts.notmuch.mailboxes]
"INBOX" = { query="tag:inbox", subscribe = true }
@ -294,18 +303,15 @@ 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_hostname Ar String
.It Ic server_url Ar String
example:
.Qq mail.example.com
.Qq http://mail.example.com
.Qq http://mail.example.com:8080
.Qq https://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.
@ -503,7 +509,21 @@ Add meli User-Agent header in new drafts
.\" default value
.Pq Em true
.It Ic default_header_values Ar hash table String[String]
.Pq Em optional
Default header values used when creating a new draft.
.\" default value
.Pq Em []
.It Ic wrap_header_preamble Ar Option<(String, String)>
.Pq Em optional
Wrap header preample when editing a draft in an editor.
This allows you to write non-plain text email without the preamble creating syntax errors.
They are stripped when you return from the editor.
The values should be a two element array of strings, a prefix and suffix.
This can be useful when for example you're writing Markdown; you can set the value to
.Em ["<!--",\ "-->"]
which wraps the headers in an HTML comment.
.\" default value
.Pq Em None
.It Ic store_sent_mail Ar boolean
.Pq Em optional
Store sent mail after successful submission.
@ -612,12 +632,12 @@ next_tab = 'T'
.Ed
.sp
and for
.Em compact-listing Ns
.Em listing Ns
:
.Bd -literal
[shortcuts.compact-listing]
open_thread = "Enter"
exit_thread = 'i'
[shortcuts.listing]
open_entry = "Enter"
exit_entry = 'i'
.Ed
.sp
.Pp
@ -758,16 +778,20 @@ Decrease sidebar width.
Toggle visibility of side menu in mail list.
.\" default value
.Pq Em `
.El
.sp
.Em compact-listing
.Bl -tag -width 36n
.It Ic exit_thread
Exit thread view
.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.
.\" default value
.Pq Em i
.It Ic open_thread
Open thread.
.It Ic open_entry
Open e-mail entry.
.\" default value
.Pq Em Enter
.El
@ -963,8 +987,14 @@ Enable notifications.
.Pq Em optional
Script to pass notifications to, with title as 1st arg and body as 2nd
.\" default value
.Pq Em none Ns
\&.
.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
.It Ic xbiff_file_path Ar String
.Pq Em optional
File that gets its size updated when new mail arrives.
@ -993,6 +1023,11 @@ 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.
@ -1032,6 +1067,11 @@ 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).
@ -1079,11 +1119,26 @@ 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 show_menu_scrollbar Ar boolean
.Pq Em optional
Show auto-hiding scrollbar in accounts sidebar menu.
.It Ic unseen_flag Ar Option<String>
Flag to show if thread entry contains unseen mail.
.\" default value
.Pq Em true
.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"
.El
.Ss Examples of sidebar mailbox tree customization
The default values

View File

@ -77,6 +77,16 @@
### 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"
@ -93,10 +103,6 @@
#[shortcuts.composing]
#edit_mail = 'e'
#
##Thread view defaults:
#[shortcuts.compact-listing]
#exit_thread = 'i'
#
#[shortcuts.contact-list]
#create_contact = 'c'
#edit_contact = 'e'
@ -111,6 +117,7 @@
#next_account = 'h'
#new_mail = 'm'
#set_seen = 'n'
#exit_entry = 'i'
#
##Pager defaults
#

View File

@ -49,6 +49,10 @@ 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 Vec<Codepoint<'u>>, unicode_data: &'u str) {
fn set_general_categories<'u>(codepoints: &mut [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 Vec<Codepoint<'_>>, eaw_data_lines: &str) {
fn set_eaw_widths(codepoints: &mut [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,7 +220,8 @@ fn main() -> Result<(), std::io::Error> {
}
}
}
fn set_emoji_widths(codepoints: &mut Vec<Codepoint<'_>>, emoji_data_lines: &str) {
fn set_emoji_widths(codepoints: &mut [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('#') {
@ -302,7 +303,7 @@ fn main() -> Result<(), std::io::Error> {
}
}
}
fn set_hardcoded_ranges(codepoints: &mut Vec<Codepoint<'_>>) {
fn set_hardcoded_ranges(codepoints: &mut [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,7 +19,14 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
/// Convert VCard strings to meli Cards (contacts).
//! # 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/)
use super::*;
use crate::error::{MeliError, Result};
use crate::parsec::{match_literal_anycase, one_or_more, peek, prefix, take_until, Parser};
@ -33,12 +40,12 @@ pub trait VCardVersion: core::fmt::Debug {}
pub struct VCardVersionUnknown;
impl VCardVersion for VCardVersionUnknown {}
/// https://tools.ietf.org/html/rfc6350
/// Version 4 <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,17 @@ 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::ops::Deref;
use std::sync::{Arc, RwLock};
use futures::stream::Stream;
use std::future::Future;
use std::ops::Deref;
use std::pin::Pin;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
#[macro_export]
macro_rules! get_path_hash {
@ -108,10 +107,45 @@ impl Default for Backends {
#[cfg(feature = "notmuch_backend")]
pub const NOTMUCH_ERROR_MSG: &str =
"libnotmuch5 was not found in your system. Make sure it is installed and in the library paths.\n";
"libnotmuch5 was not found in your system. Make sure it is installed and in the library paths. For a custom file path, use `library_file_path` setting in your notmuch account.\n";
#[cfg(not(feature = "notmuch_backend"))]
pub const NOTMUCH_ERROR_MSG: &str = "this version of meli is not compiled with notmuch support. Use an appropriate version and make sure libnotmuch5 is installed and in the library paths.\n";
#[cfg(not(feature = "notmuch_backend"))]
pub const NOTMUCH_ERROR_DETAILS: &str = "";
#[cfg(all(feature = "notmuch_backend", target_os = "unix"))]
pub const NOTMUCH_ERROR_DETAILS: &str = r#"If you have installed the library manually, try setting the `LD_LIBRARY_PATH` environment variable to its `lib` directory. Otherwise, set it to the location of libnotmuch.5.so. Example:
LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/path/to/notmuch/lib" meli
or, put this in your shell init script (.bashenv, .zshenv, .bashrc, .zshrc, .profile):
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/path/to/notmuch/lib"
You can also set any location by specifying the library file path with the configuration flag `library_file_path`."#;
#[cfg(all(feature = "notmuch_backend", target_os = "macos"))]
pub const NOTMUCH_ERROR_DETAILS: &str = r#"If you have installed the library via homebrew, try setting the `DYLD_LIBRARY_PATH` environment variable to its `lib` directory. Otherwise, set it to the location of libnotmuch.5.dylib. Example:
DYLD_LIBRARY_PATH="$(brew --prefix)/lib" meli
or, put this in your shell init script (.bashenv, .zshenv, .bashrc, .zshrc, .profile):
export DYLD_LIBRARY_PATH="$(brew --prefix)/lib"
Make sure to append to DYLD_LIBRARY_PATH if it's not empty, by prepending a colon to the libnotmuch5.dylib location:
export DYLD_LIBRARY_PATH="$DYLD_LIBRARY_PATH:$(brew --prefix)/lib"
You can also set any location by specifying the library file path with the configuration flag `library_file_path`."#;
#[cfg(all(
feature = "notmuch_backend",
not(any(target_os = "unix", target_os = "macos"))
))]
pub const NOTMUCH_ERROR_DETAILS: &str = r#"If notmuch is installed but the library isn't found, consult your system's documentation on how to make dynamic libraries discoverable."#;
impl Backends {
pub fn new() -> Self {
let mut b = Backends {
@ -156,19 +190,13 @@ impl Backends {
}
#[cfg(feature = "notmuch_backend")]
{
#[cfg(not(target_os = "macos"))]
let dlpath = "libnotmuch.so.5";
#[cfg(target_os = "macos")]
let dlpath = "libnotmuch.5.dylib";
if unsafe { libloading::Library::new(dlpath) }.is_ok() {
b.register(
"notmuch".to_string(),
Backend {
create_fn: Box::new(|| Box::new(|f, i, ev| NotmuchDb::new(f, i, ev))),
validate_conf_fn: Box::new(NotmuchDb::validate_config),
},
);
}
b.register(
"notmuch".to_string(),
Backend {
create_fn: Box::new(|| Box::new(|f, i, ev| NotmuchDb::new(f, i, ev))),
validate_conf_fn: Box::new(NotmuchDb::validate_config),
},
);
}
#[cfg(feature = "jmap_backend")]
{
@ -187,6 +215,10 @@ impl Backends {
if !self.map.contains_key(key) {
if key == "notmuch" {
eprint!("{}", NOTMUCH_ERROR_MSG);
#[cfg(feature = "notmuch_backend")]
{
eprint!("{}", NOTMUCH_ERROR_DETAILS);
}
}
panic!("{} is not a valid mail backend", key);
}
@ -206,13 +238,18 @@ impl Backends {
.get(key)
.ok_or_else(|| {
MeliError::new(format!(
"{}{} is not a valid mail backend",
"{}{} is not a valid mail backend. {}",
if key == "notmuch" {
NOTMUCH_ERROR_MSG
} else {
""
},
key
key,
if cfg!(feature = "notmuch_backend") && key == "notmuch" {
NOTMUCH_ERROR_DETAILS
} else {
""
},
))
})?
.validate_conf_fn)(s)
@ -222,19 +259,22 @@ impl Backends {
#[derive(Debug, Clone)]
pub enum BackendEvent {
Notice {
description: Option<String>,
content: String,
description: String,
content: Option<String>,
level: crate::LoggingLevel,
},
Refresh(RefreshEvent),
AccountStateChange {
message: Cow<'static, str>,
},
//Job(Box<Future<Output = Result<()>> + Send + 'static>)
}
impl From<MeliError> for BackendEvent {
fn from(val: MeliError) -> BackendEvent {
BackendEvent::Notice {
description: val.summary.as_ref().map(|s| s.to_string()),
content: val.to_string(),
description: val.summary.to_string(),
content: Some(val.to_string()),
level: crate::LoggingLevel::ERROR,
}
}
@ -623,6 +663,12 @@ 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())
@ -631,6 +677,10 @@ 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,7 +52,6 @@ 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;
@ -192,7 +191,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>,
@ -274,7 +273,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 };
}
@ -1121,32 +1120,32 @@ impl MailBackend for ImapType {
Subject(t) => {
s.push_str(" SUBJECT \"");
s.extend(escape_double_quote(t).chars());
s.push_str("\"");
s.push('"');
}
From(t) => {
s.push_str(" FROM \"");
s.extend(escape_double_quote(t).chars());
s.push_str("\"");
s.push('"');
}
To(t) => {
s.push_str(" TO \"");
s.extend(escape_double_quote(t).chars());
s.push_str("\"");
s.push('"');
}
Cc(t) => {
s.push_str(" CC \"");
s.extend(escape_double_quote(t).chars());
s.push_str("\"");
s.push('"');
}
Bcc(t) => {
s.push_str(" BCC \"");
s.extend(escape_double_quote(t).chars());
s.push_str("\"");
s.push('"');
}
AllText(t) => {
s.push_str(" TEXT \"");
s.extend(escape_double_quote(t).chars());
s.push_str("\"");
s.push('"');
}
Flags(v) => {
for f in v {
@ -1280,7 +1279,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")]
@ -1338,7 +1337,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,
}))
@ -1459,7 +1458,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();
@ -1484,7 +1483,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
@ -1862,7 +1861,7 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
.unwrap()
.entry(mailbox_hash)
.or_default()
.insert((message_sequence_number - 1).try_into().unwrap(), uid);
.insert(message_sequence_number - 1, uid);
uid_store
.hash_index
.lock()

View File

@ -178,7 +178,7 @@ mod sqlite3_m {
let mut ret: Vec<UID> = stmt
.query_map(sqlite3::params![mailbox_hash as i64], |row| {
Ok(row.get(0).map(|i: Sqlite3UID| i as UID)?)
row.get(0).map(|i: Sqlite3UID| i as UID)
})?
.collect::<std::result::Result<_, _>>()?;
Ok(ret.pop().unwrap_or(0))
@ -231,7 +231,7 @@ mod sqlite3_m {
.unwrap()
.entry(mailbox_hash)
.and_modify(|entry| *entry = highestmodseq.ok_or(()))
.or_insert(highestmodseq.ok_or(()));
.or_insert_with(|| highestmodseq.ok_or(()));
self.uid_store
.uidvalidity
.lock()
@ -483,7 +483,7 @@ 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],
@ -503,7 +503,7 @@ mod sqlite3_m {
let mut ret: Vec<Envelope> = stmt
.query_map(
sqlite3::params![mailbox_hash as i64, *uid as Sqlite3UID],
|row| Ok(row.get(0)?),
|row| row.get(0),
)?
.collect::<std::result::Result<_, _>>()?;
if let Some(mut env) = ret.pop() {
@ -592,12 +592,12 @@ mod sqlite3_m {
return Ok(None);
}
let (uid, inner, modsequence) = ret.pop().unwrap();
return Ok(Some(CachedEnvelope {
Ok(Some(CachedEnvelope {
inner,
uid,
mailbox_hash,
modsequence,
}));
}))
}
fn rfc822(
@ -613,7 +613,7 @@ mod sqlite3_m {
let x = stmt
.query_map(
sqlite3::params![mailbox_hash as i64, uid as Sqlite3UID],
|row| Ok(row.get(0)?),
|row| row.get(0),
)?
.collect::<std::result::Result<_, _>>()?;
x
@ -625,7 +625,7 @@ mod sqlite3_m {
let x = stmt
.query_map(
sqlite3::params![mailbox_hash as i64, env_hash as i64],
|row| Ok(row.get(0)?),
|row| 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 => return Ok(None),
None => Ok(None),
Some(Ok(env_hashes)) => {
let env_lck = uid_store.envelopes.lock().unwrap();
return Ok(Some(
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)) => return Err(err),
Some(Err(err)) => 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)?;
.chain_err_kind(crate::error::ErrorKind::Network(
crate::error::NetworkErrorKind::InvalidTLSConnection,
))?;
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 addr = lookup_ipv4(path, server_conf.server_port)?;
let mut socket = AsyncWrapper::new(Connection::Tcp(
if let Some(timeout) = server_conf.timeout {
TcpStream::connect_timeout(&addr, timeout)
.chain_err_kind(crate::error::ErrorKind::Network)?
TcpStream::connect_timeout(&addr, timeout)?
} else {
TcpStream::connect(&addr).chain_err_kind(crate::error::ErrorKind::Network)?
TcpStream::connect(&addr)?
},
))
.chain_err_kind(crate::error::ErrorKind::Network)?;
))?;
if server_conf.use_starttls {
let err_fn = || {
if server_conf.server_port == 993 {
@ -160,36 +160,22 @@ impl ImapStream {
ImapProtocol::IMAP { .. } => socket
.write_all(format!("M{} STARTTLS\r\n", cmd_id).as_bytes())
.await
.chain_err_summary(err_fn)
.chain_err_kind(crate::error::ErrorKind::Network)?,
.chain_err_summary(err_fn)?,
ImapProtocol::ManageSieve => {
socket
.read(&mut buf)
.await
.chain_err_summary(err_fn)
.chain_err_kind(crate::error::ErrorKind::Network)?;
socket.read(&mut buf).await.chain_err_summary(err_fn)?;
socket
.write_all(b"STARTTLS\r\n")
.await
.chain_err_summary(err_fn)
.chain_err_kind(crate::error::ErrorKind::Network)?;
.chain_err_summary(err_fn)?;
}
}
socket
.flush()
.await
.chain_err_summary(err_fn)
.chain_err_kind(crate::error::ErrorKind::Network)?;
socket.flush().await.chain_err_summary(err_fn)?;
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)
.chain_err_kind(crate::error::ErrorKind::Network)?;
let len = socket.read(&mut buf).await.chain_err_summary(err_fn)?;
response.extend_from_slice(&buf[0..len]);
match server_conf.protocol {
ImapProtocol::IMAP { .. } => {
@ -222,9 +208,7 @@ impl ImapStream {
{
// FIXME: This is blocking
let socket = socket
.into_inner()
.chain_err_kind(crate::error::ErrorKind::Network)?;
let socket = socket.into_inner()?;
let mut conn_result = connector.connect(path, socket);
if let Err(native_tls::HandshakeError::WouldBlock(midhandshake_stream)) =
conn_result
@ -240,20 +224,17 @@ impl ImapStream {
midhandshake_stream = Some(stream);
}
p => {
p.chain_err_kind(crate::error::ErrorKind::Network)?;
p.chain_err_kind(crate::error::ErrorKind::Network(
crate::error::NetworkErrorKind::InvalidTLSConnection,
))?;
}
}
}
}
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)?
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))?
}
} else {
let addr = if let Ok(a) = lookup_ipv4(path, server_conf.server_port) {
@ -266,13 +247,11 @@ impl ImapStream {
};
AsyncWrapper::new(Connection::Tcp(
if let Some(timeout) = server_conf.timeout {
TcpStream::connect_timeout(&addr, timeout)
.chain_err_kind(crate::error::ErrorKind::Network)?
TcpStream::connect_timeout(&addr, timeout)?
} else {
TcpStream::connect(&addr).chain_err_kind(crate::error::ErrorKind::Network)?
TcpStream::connect(&addr)?
},
))
.chain_err_kind(crate::error::ErrorKind::Network)?
))?
};
if let Err(err) = stream
.get_ref()
@ -312,6 +291,12 @@ 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
@ -353,6 +338,12 @@ 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, .. },
@ -379,7 +370,7 @@ impl ImapStream {
r#"LOGIN "{}" {{{}}}"#,
&server_conf
.server_username
.replace(r#"\"#, r#"\\"#)
.replace('\\', r#"\\"#)
.replace('"', r#"\""#)
.replace('{', r#"\{"#)
.replace('}', r#"\}"#),
@ -488,8 +479,8 @@ impl ImapStream {
last_line_idx += pos + b"\r\n".len();
}
}
Err(e) => {
return Err(MeliError::from(e).set_err_kind(crate::error::ErrorKind::Network));
Err(err) => {
return Err(MeliError::from(err));
}
}
}
@ -505,7 +496,7 @@ impl ImapStream {
}
pub async fn send_command(&mut self, command: &[u8]) -> Result<()> {
if let Err(err) = timeout(
_ = timeout(
self.timeout,
try_await(async move {
let command = command.trim();
@ -539,42 +530,22 @@ impl ImapStream {
Ok(())
}),
)
.await
{
Err(err.set_err_kind(crate::error::ErrorKind::Network))
} else {
Ok(())
}
.await?;
Ok(())
}
pub async fn send_literal(&mut self, data: &[u8]) -> Result<()> {
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(())
}
self.stream.write_all(data).await?;
self.stream.write_all(b"\r\n").await?;
self.stream.flush().await?;
Ok(())
}
pub async fn send_raw(&mut self, raw: &[u8]) -> Result<()> {
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(())
}
self.stream.write_all(raw).await?;
self.stream.write_all(b"\r\n").await?;
self.stream.flush().await?;
Ok(())
}
}
@ -601,7 +572,11 @@ impl ImapConnection {
if SystemTime::now().duration_since(time).unwrap_or_default()
>= IMAP_PROTOCOL_TIMEOUT
{
let err = MeliError::new("Connection timed out").set_kind(ErrorKind::Timeout);
let err = MeliError::new(format!(
"Connection timed out after {} seconds",
IMAP_PROTOCOL_TIMEOUT.as_secs()
))
.set_kind(ErrorKind::Timeout);
*status = Err(err.clone());
self.stream = Err(err);
}
@ -624,7 +599,7 @@ impl ImapConnection {
return Ok(());
}
}
let new_stream = ImapStream::new_connection(&self.server_conf).await;
let new_stream = ImapStream::new_connection(&self.server_conf, &self.uid_store).await;
if let Err(err) = new_stream.as_ref() {
self.uid_store.is_online.lock().unwrap().1 = Err(err.clone());
} else {
@ -746,8 +721,8 @@ impl ImapConnection {
(self.uid_store.event_consumer)(
self.uid_store.account_hash,
crate::backends::BackendEvent::Notice {
description: None,
content: response_code.to_string(),
description: response_code.to_string(),
content: None,
level: crate::logging::LoggingLevel::ERROR,
},
);
@ -763,8 +738,8 @@ impl ImapConnection {
(self.uid_store.event_consumer)(
self.uid_store.account_hash,
crate::backends::BackendEvent::Notice {
description: None,
content: response_code.to_string(),
description: response_code.to_string(),
content: None,
level: crate::logging::LoggingLevel::ERROR,
},
);
@ -1129,7 +1104,7 @@ async fn read(
*prev_failure = None;
}
Err(_err) => {
*err = Some(Into::<MeliError>::into(_err).set_kind(crate::error::ErrorKind::Network));
*err = Some(Into::<MeliError>::into(_err));
*break_flag = true;
*prev_failure = Some(SystemTime::now());
}

View File

@ -21,16 +21,22 @@
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, error::Error as NomError, error::ErrorKind,
multi::separated_list1, sequence::separated_pair, IResult,
branch::alt, bytes::complete::tag, combinator::map, multi::separated_list1,
sequence::separated_pair,
};
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"),
@ -42,26 +48,225 @@ pub fn managesieve_capabilities(input: &[u8]) -> Result<Vec<(&[u8], &[u8])>> {
Ok(ret)
}
#[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"[..])]
#[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]>,
},
}
);
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(NomError {
input,
code: ErrorKind::Tag,
}));
return Err(nom::Err::Error((input, "empty").into()));
}
let mut i = 1;
@ -72,91 +277,199 @@ pub fn quoted_raw(input: &[u8]) -> IResult<&[u8], &[u8]> {
i += 1;
}
Err(nom::Err::Error(NomError {
input,
code: ErrorKind::Tag,
}))
Err(nom::Err::Error((input, "no quotes").into()))
}
pub trait ManageSieve {
fn havespace(&mut self) -> Result<()>;
fn putscript(&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(())
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),
})
}
fn putscript(&mut self) -> Result<()> {
pub async fn havespace(&mut self) -> Result<()> {
Ok(())
}
fn listscripts(&mut self) -> Result<()> {
Ok(())
}
fn setactive(&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 getscript(&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 deletescript(&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 renamescript(&mut self) -> Result<()> {
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<()> {
Ok(())
}
}

View File

@ -150,8 +150,9 @@ fn test_imap_required_responses() {
assert_eq!(v.len(), 1);
}
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub struct Alert(String);
pub type ImapParseResult<'a, T> = Result<(&'a [u8], T, Option<Alert>)>;
pub struct ImapLineIterator<'a> {
slice: &'a [u8],
@ -257,7 +258,7 @@ pub enum ImapResponse {
impl TryFrom<&'_ [u8]> for ImapResponse {
type Error = MeliError;
fn try_from(val: &'_ [u8]) -> Result<ImapResponse> {
let val: &[u8] = val.split_rn().last().unwrap_or_else(|| val.as_ref());
let val: &[u8] = val.split_rn().last().unwrap_or(val);
let mut val = val[val.find(b" ").ok_or_else(|| {
MeliError::new(format!(
"Expected tagged IMAP response (OK,NO,BAD, etc) but found {:?}",
@ -604,8 +605,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. Got: `{:.40}`",
String::from_utf8_lossy(input)
"Unexpected input while parsing UID FETCH response. Could not parse FLAGS: {:.40}.",
String::from_utf8_lossy(&input[i..])
))));
}
} else if input[i..].starts_with(b"MODSEQ (") {
@ -639,8 +640,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. Got: `{:.40}`",
String::from_utf8_lossy(input)
"Unexpected input while parsing UID FETCH response. Could not parse RFC822: {:.40}",
String::from_utf8_lossy(&input[i..])
))));
}
} else if input[i..].starts_with(b"ENVELOPE (") {
@ -650,7 +651,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. Got: `{:.40}`",
"Unexpected input while parsing UID FETCH response. Could not parse ENVELOPE: {:.40}",
String::from_utf8_lossy(&input[i..])
))));
}
@ -664,7 +665,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);
@ -672,7 +673,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. Got: `{:.40}`",
"Unexpected input while parsing UID FETCH response. Could not parse BODY[HEADER.FIELDS (REFERENCES)]: {:.40}",
String::from_utf8_lossy(&input[i..])
))));
}
@ -680,7 +681,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);
@ -688,7 +689,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. Got: `{:.40}`",
"Unexpected input while parsing UID FETCH response. Could not parse BODY[HEADER.FIELDS (\"REFERENCES\"): {:.40}",
String::from_utf8_lossy(&input[i..])
))));
}
@ -736,9 +737,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}`, {}",
String::from_utf8_lossy(&input),
err
"Unexpected input while parsing UID FETCH responses: {} `{:.40}`",
err,
String::from_utf8_lossy(input),
)));
}
}
@ -749,7 +750,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)
)));
}
}
@ -923,7 +924,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
}
@ -934,7 +935,7 @@ pub fn untagged_responses(input: &[u8]) -> ImapParseResult<Option<UntaggedRespon
}
#[test]
fn test_untagged_responses() {
fn test_imap_untagged_responses() {
use UntaggedResponse::*;
assert_eq!(
untagged_responses(b"* 2 EXISTS\r\n")
@ -977,6 +978,37 @@ fn test_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>> {
@ -1127,7 +1159,7 @@ pub fn select_response(input: &[u8]) -> Result<SelectResponse> {
}
#[test]
fn test_select_response() {
fn test_imap_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!(
@ -1347,6 +1379,12 @@ 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) => {
@ -1366,7 +1404,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, tag(")")),
delimited(tag("("), envelope_address, alt((tag(") "), tag(")")))),
SmallVec::new,
|mut acc, item| {
acc.push(item);
@ -1617,7 +1655,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<'i>(input: &'i [u8]) -> IResult<&'i [u8], std::borrow::Cow<'i, str>> {
pub fn mailbox_token(input: &'_ [u8]) -> IResult<&'_ [u8], std::borrow::Cow<'_, str>> {
let (input, astring) = astring_token(input)?;
if astring.eq_ignore_ascii_case(b"INBOX") {
return Ok((input, "INBOX".into()));
@ -1626,12 +1664,12 @@ pub fn mailbox_token<'i>(input: &'i [u8]) -> IResult<&'i [u8], std::borrow::Cow<
}
// astring = 1*ASTRING-CHAR / string
fn astring_token(input: &[u8]) -> IResult<&[u8], &[u8]> {
pub fn astring_token(input: &[u8]) -> IResult<&[u8], &[u8]> {
alt((string_token, astring_char))(input)
}
// string = quoted / literal
fn string_token(input: &[u8]) -> IResult<&[u8], &[u8]> {
pub 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,9 +317,10 @@ impl ImapConnection {
let command = {
let mut iter = v.split(u8::is_ascii_whitespace);
let first = iter.next().unwrap_or(v);
let mut accum = format!("{}", to_str!(first).trim());
let mut accum = to_str!(first).trim().to_string();
for ms in iter {
accum = format!("{},{}", accum, to_str!(ms).trim());
accum.push(',');
accum.push_str(to_str!(ms).trim());
}
format!("UID FETCH {} (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)", accum)
};
@ -352,7 +353,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,10 +100,9 @@ pub struct EnvelopeCache {
#[derive(Debug, Clone)]
pub struct JmapServerConf {
pub server_hostname: String,
pub server_url: String,
pub server_username: String,
pub server_password: String,
pub server_port: u16,
pub danger_accept_invalid_certs: bool,
pub timeout: Option<Duration>,
}
@ -139,10 +138,9 @@ macro_rules! get_conf_val {
impl JmapServerConf {
pub fn new(s: &AccountSettings) -> Result<Self> {
Ok(JmapServerConf {
server_hostname: get_conf_val!(s["server_hostname"])?.to_string(),
server_url: get_conf_val!(s["server_url"])?.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 {
@ -337,6 +335,9 @@ impl MailBackend for JmapType {
&store,
mailbox_hash,
).await?;
if res.is_empty() {
return;
}
yield res;
}))
}
@ -452,7 +453,14 @@ impl MailBackend for JmapType {
};
let res_text = res.text().await?;
let upload_response: UploadResponse = serde_json::from_str(&res_text)?;
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 mut req = Request::new(conn.request_no.clone());
let creation_id: Id<EmailObject> = "1".to_string().into();
let mut email_imports = HashMap::default();
@ -476,7 +484,14 @@ impl MailBackend for JmapType {
.await?;
let res_text = res.text().await?;
let mut v: MethodResponse = serde_json::from_str(&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 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());
@ -550,7 +565,14 @@ impl MailBackend for JmapType {
.await?;
let res_text = res.text().await?;
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
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,
};
*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;
@ -646,7 +668,14 @@ impl MailBackend for JmapType {
let res_text = res.text().await?;
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
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,
};
*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 {
@ -751,7 +780,14 @@ 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 = serde_json::from_str(&res_text).unwrap();
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,
};
*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 {
@ -901,10 +937,9 @@ impl JmapType {
.unwrap_or_else(|| Ok($default))
};
}
get_conf_val!(s["server_hostname"])?;
get_conf_val!(s["server_url"])?;
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,6 +21,7 @@
use super::*;
use isahc::config::Configurable;
use std::sync::MutexGuard;
#[derive(Debug)]
pub struct JmapConnection {
@ -56,24 +57,32 @@ impl JmapConnection {
if self.store.online_status.lock().await.1.is_ok() {
return Ok(());
}
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());
}
let mut jmap_session_resource_url = self.server_conf.server_url.to_string();
jmap_session_resource_url.push_str("/.well-known/jmap");
let mut req = self.client.get_async(&jmap_session_resource_url).await?;
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 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 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)));
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)));
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
@ -83,7 +92,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_hostname, 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_url, 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);
}
@ -91,7 +100,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_hostname, 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_url, 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);
}
@ -105,6 +114,10 @@ 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));
}
@ -179,7 +192,14 @@ impl JmapConnection {
let res_text = res.text().await?;
debug!(&res_text);
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
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 changes_response =
ChangesResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
if changes_response.new_state == current_state {

View File

@ -95,9 +95,11 @@ 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,7 +842,8 @@ 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())?;
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))?;
assert_eq!(&res.0, "Email/queryChanges");
Ok(res.1)
}

View File

@ -184,7 +184,8 @@ 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())?;
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))?;
assert_eq!(&res.0, &ImportCall::NAME);
Ok(res.1)
}

View File

@ -102,12 +102,29 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
.await?;
let res_text = res.text().await?;
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
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,
};
*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()
@ -141,7 +158,7 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
path: name,
children: Vec::new(),
id,
is_subscribed,
is_subscribed: is_subscribed || is_personal,
my_rights,
parent_id,
parent_hash,
@ -192,7 +209,14 @@ pub async fn get_message_list(
.await?;
let res_text = res.text().await?;
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
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,
};
*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;
@ -265,7 +289,14 @@ pub async fn fetch(
let res_text = res.text().await?;
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
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 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)]
max_size_upload: u64,
pub max_size_upload: u64,
#[serde(default)]
max_concurrent_upload: u64,
pub max_concurrent_upload: u64,
#[serde(default)]
max_size_request: u64,
pub max_size_request: u64,
#[serde(default)]
max_concurrent_requests: u64,
pub max_concurrent_requests: u64,
#[serde(default)]
max_calls_in_request: u64,
pub max_calls_in_request: u64,
#[serde(default)]
max_objects_in_get: u64,
pub max_objects_in_get: u64,
#[serde(default)]
max_objects_in_set: u64,
pub max_objects_in_set: u64,
#[serde(default)]
collation_algorithms: Vec<String>,
pub collation_algorithms: Vec<String>,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Account {
name: String,
is_personal: bool,
is_read_only: bool,
account_capabilities: HashMap<String, Value>,
pub name: String,
pub is_personal: bool,
pub is_read_only: bool,
pub account_capabilities: HashMap<String, Value>,
#[serde(flatten)]
extra_properties: HashMap<String, Value>,
pub extra_properties: HashMap<String, Value>,
}
impl Object for Account {
@ -413,7 +413,8 @@ 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())?;
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))?;
assert_eq!(&res.0, &format!("{}/get", OBJ::NAME));
Ok(res.1)
}
@ -440,20 +441,20 @@ pub struct Query<F: FilterTrait<OBJ>, OBJ: Object>
where
OBJ: std::fmt::Debug + Serialize,
{
account_id: Id<Account>,
filter: Option<F>,
sort: Option<Comparator<OBJ>>,
pub account_id: Id<Account>,
pub filter: Option<F>,
pub sort: Option<Comparator<OBJ>>,
#[serde(default)]
position: u64,
pub position: u64,
#[serde(skip_serializing_if = "Option::is_none")]
anchor: Option<String>,
pub anchor: Option<String>,
#[serde(default)]
#[serde(skip_serializing_if = "u64_zero")]
anchor_offset: u64,
pub anchor_offset: u64,
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<u64>,
pub limit: Option<u64>,
#[serde(default = "bool_false")]
calculate_total: bool,
pub calculate_total: bool,
#[serde(skip)]
_ph: PhantomData<fn() -> OBJ>,
}
@ -517,7 +518,8 @@ 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())?;
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))?;
assert_eq!(&res.0, &format!("{}/query", OBJ::NAME));
Ok(res.1)
}
@ -651,7 +653,8 @@ 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())?;
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))?;
assert_eq!(&res.0, &format!("{}/changes", OBJ::NAME));
Ok(res.1)
}
@ -849,7 +852,8 @@ 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())?;
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))?;
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()
@ -148,7 +148,7 @@ impl MaildirMailbox {
PathBuf::from(&settings.root_mailbox)
.expand()
.parent()
.unwrap_or_else(|| &Path::new("/")),
.unwrap_or_else(|| Path::new("/")),
)
.ok();
@ -217,7 +217,7 @@ impl BackendMailbox for MaildirMailbox {
}
fn path(&self) -> &str {
self.path.to_str().unwrap_or(self.name())
self.path.to_str().unwrap_or_else(|| self.name())
}
fn change_name(&mut self, s: &str) {

View File

@ -19,6 +19,11 @@
* 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;
@ -84,7 +89,7 @@ impl From<PathBuf> for MaildirPath {
#[derive(Debug, Default)]
pub struct HashIndex {
index: HashMap<EnvelopeHash, MaildirPath>,
hash: MailboxHash,
_hash: MailboxHash,
}
impl Deref for HashIndex {
@ -102,7 +107,7 @@ impl DerefMut for HashIndex {
pub type HashIndexes = Arc<Mutex<HashMap<MailboxHash, HashIndex>>>;
/// Maildir backend https://cr.yp.to/proto/maildir.html
/// The maildir backend instance type.
#[derive(Debug)]
pub struct MaildirType {
name: String,
@ -200,7 +205,7 @@ impl MailBackend for MaildirType {
let unseen = mailbox.unseen.clone();
let total = mailbox.total.clone();
let path: PathBuf = mailbox.fs_path().into();
let root_path = self.path.to_path_buf();
let root_mailbox = self.path.to_path_buf();
let map = self.hash_indexes.clone();
let mailbox_index = self.mailbox_index.clone();
super::stream::MaildirStream::new(
@ -209,7 +214,7 @@ impl MailBackend for MaildirType {
unseen,
total,
path,
root_path,
root_mailbox,
map,
mailbox_index,
)
@ -226,7 +231,7 @@ impl MailBackend for MaildirType {
let mailbox: &MaildirMailbox = &self.mailboxes[&mailbox_hash];
let path: PathBuf = mailbox.fs_path().into();
let root_path = self.path.to_path_buf();
let root_mailbox = self.path.to_path_buf();
let map = self.hash_indexes.clone();
let mailbox_index = self.mailbox_index.clone();
@ -261,7 +266,7 @@ impl MailBackend for MaildirType {
.lock()
.unwrap()
.insert(env.hash(), mailbox_hash);
let file_name = file.strip_prefix(&root_path).unwrap().to_path_buf();
let file_name = file.strip_prefix(&root_mailbox).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)?;
@ -329,10 +334,12 @@ impl MailBackend for MaildirType {
hasher.write(self.name.as_bytes());
hasher.finish()
};
let root_path = self.path.to_path_buf();
watcher.watch(&root_path, RecursiveMode::Recursive).unwrap();
let root_mailbox = self.path.to_path_buf();
watcher
.watch(&root_mailbox, RecursiveMode::Recursive)
.unwrap();
let cache_dir = xdg::BaseDirectories::with_profile("meli", &self.name).unwrap();
debug!("watching {:?}", root_path);
debug!("watching {:?}", root_mailbox);
let hash_indexes = self.hash_indexes.clone();
let mailbox_index = self.mailbox_index.clone();
let root_mailbox_hash: MailboxHash = self
@ -380,7 +387,7 @@ impl MailBackend for MaildirType {
let mailbox_hash = get_path_hash!(pathbuf);
let file_name = pathbuf
.as_path()
.strip_prefix(&root_path)
.strip_prefix(&root_mailbox)
.unwrap()
.to_path_buf();
if let Ok(env) = add_path_to_index(
@ -424,7 +431,7 @@ impl MailBackend for MaildirType {
&mut hash_indexes_lock.entry(mailbox_hash).or_default();
let file_name = pathbuf
.as_path()
.strip_prefix(&root_path)
.strip_prefix(&root_mailbox)
.unwrap()
.to_path_buf();
/* Linear search in hash_index to find old hash */
@ -516,7 +523,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| {
@ -586,7 +593,7 @@ impl MailBackend for MaildirType {
);
let file_name = dest
.as_path()
.strip_prefix(&root_path)
.strip_prefix(&root_mailbox)
.unwrap()
.to_path_buf();
drop(hash_indexes_lock);
@ -676,7 +683,7 @@ impl MailBackend for MaildirType {
}
let file_name = dest
.as_path()
.strip_prefix(&root_path)
.strip_prefix(&root_mailbox)
.unwrap()
.to_path_buf();
debug!("filename = {:?}", file_name);
@ -725,7 +732,7 @@ impl MailBackend for MaildirType {
drop(hash_indexes_lock);
let file_name = dest
.as_path()
.strip_prefix(&root_path)
.strip_prefix(&root_mailbox)
.unwrap()
.to_path_buf();
if let Ok(env) = add_path_to_index(
@ -854,7 +861,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()
@ -919,7 +926,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()
@ -959,15 +966,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()
.expect(&format!("Could not get filename of {}", path_src.display()));
let filename = path_src.file_name().ok_or_else(|| {
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()));
@ -1114,7 +1121,7 @@ impl MaildirType {
None,
Vec::new(),
false,
&settings,
settings,
) {
f.children = recurse_mailboxes(mailboxes, settings, &path)?;
for c in &f.children {
@ -1137,7 +1144,7 @@ impl MaildirType {
None,
subdirs,
true,
&settings,
settings,
) {
for c in &f.children {
if let Some(f) = mailboxes.get_mut(c) {
@ -1155,24 +1162,29 @@ impl MaildirType {
}
Ok(children)
}
let root_path = PathBuf::from(settings.root_mailbox()).expand();
if !root_path.exists() {
let root_mailbox = PathBuf::from(settings.root_mailbox()).expand();
if !root_mailbox.exists() {
return Err(MeliError::new(format!(
"Configuration error ({}): root_path `{}` is not a valid directory.",
"Configuration error ({}): root_mailbox `{}` is not a valid directory.",
settings.name(),
settings.root_mailbox.as_str()
)));
} else if !root_path.is_dir() {
} else if !root_mailbox.is_dir() {
return Err(MeliError::new(format!(
"Configuration error ({}): root_path `{}` is not a directory.",
"Configuration error ({}): root_mailbox `{}` is not a directory.",
settings.name(),
settings.root_mailbox.as_str()
)));
}
if let Ok(f) = MaildirMailbox::new(
root_path.to_str().unwrap().to_string(),
root_path.file_name().unwrap().to_str().unwrap().to_string(),
root_mailbox.to_str().unwrap().to_string(),
root_mailbox
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.to_string(),
None,
Vec::with_capacity(0),
false,
@ -1182,7 +1194,7 @@ impl MaildirType {
}
if mailboxes.is_empty() {
let children = recurse_mailboxes(&mut mailboxes, settings, &root_path)?;
let children = recurse_mailboxes(&mut mailboxes, settings, &root_mailbox)?;
for c in &children {
if let Some(f) = mailboxes.get_mut(c) {
f.parent = None;
@ -1190,7 +1202,7 @@ impl MaildirType {
}
} else {
let root_hash = *mailboxes.keys().next().unwrap();
let children = recurse_mailboxes(&mut mailboxes, settings, &root_path)?;
let children = recurse_mailboxes(&mut mailboxes, settings, &root_mailbox)?;
for c in &children {
if let Some(f) = mailboxes.get_mut(c) {
f.parent = Some(root_hash);
@ -1213,7 +1225,7 @@ impl MaildirType {
fh,
HashIndex {
index: HashMap::with_capacity_and_hasher(0, Default::default()),
hash: fh,
_hash: fh,
},
);
}
@ -1224,7 +1236,7 @@ impl MaildirType {
mailbox_index: Default::default(),
event_consumer,
collection: Default::default(),
path: root_path,
path: root_mailbox,
}))
}
@ -1298,16 +1310,16 @@ impl MaildirType {
}
pub fn validate_config(s: &mut AccountSettings) -> Result<()> {
let root_path = PathBuf::from(s.root_mailbox()).expand();
if !root_path.exists() {
let root_mailbox = PathBuf::from(s.root_mailbox()).expand();
if !root_mailbox.exists() {
return Err(MeliError::new(format!(
"Configuration error ({}): root_path `{}` is not a valid directory.",
"Configuration error ({}): root_mailbox `{}` is not a valid directory.",
s.name(),
s.root_mailbox.as_str()
)));
} else if !root_path.is_dir() {
} else if !root_mailbox.is_dir() {
return Err(MeliError::new(format!(
"Configuration error ({}): root_path `{}` is not a directory.",
"Configuration error ({}): root_mailbox `{}` is not a directory.",
s.name(),
s.root_mailbox.as_str()
)));
@ -1319,25 +1331,17 @@ 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 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());
}
for p in path.read_dir()?.flatten() {
if !read_only {
move_to_cur(p.path()).ok().take();
} else {
files.push(p.path());
}
}
path.pop();
path.push("cur");
let iter = path.read_dir()?;
for e in iter {
let e = e.and_then(|x| {
let path = x.path();
Ok(path)
})?;
files.push(e);
for e in path.read_dir()?.flatten() {
files.push(e.path());
}
Ok(files)
}

View File

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

View File

@ -23,16 +23,17 @@
//!
//! ## 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
//!
@ -59,7 +60,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
//!
@ -271,7 +272,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,
@ -279,9 +280,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,
@ -883,7 +884,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,

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,6 +19,13 @@
* 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;
@ -108,7 +115,6 @@ 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)>>>,
@ -130,7 +136,6 @@ impl UIDStore {
account_hash,
account_name,
event_consumer,
offline_cache: false,
capabilities: Default::default(),
message_id_index: Default::default(),
hash_index: Default::default(),
@ -147,11 +152,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 {
@ -251,8 +256,7 @@ 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().clone();
let latest_article: Option<crate::UnixTimestamp> = *mbox.latest_article.lock().unwrap();
let (over_msgid_support, newnews_support): (bool, bool) = {
let caps = uid_store.capabilities.lock().unwrap();
@ -599,7 +603,6 @@ 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)
});
@ -607,8 +610,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,
}))
@ -699,7 +702,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,11 +90,10 @@ 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))
.chain_err_kind(crate::error::ErrorKind::Network)?,
))
.chain_err_kind(crate::error::ErrorKind::Network)?
AsyncWrapper::new(Connection::Tcp(TcpStream::connect_timeout(
&addr,
std::time::Duration::new(16, 0),
)?))?
};
let mut res = String::with_capacity(8 * 1024);
let mut ret = NntpStream {
@ -109,9 +108,7 @@ impl NntpStream {
if server_conf.danger_accept_invalid_certs {
connector.danger_accept_invalid_certs(true);
}
let connector = connector
.build()
.chain_err_kind(crate::error::ErrorKind::Network)?;
let connector = connector.build()?;
if server_conf.use_starttls {
ret.read_response(&mut res, false, &["200 ", "201 "])
@ -147,14 +144,8 @@ impl NntpStream {
&server_conf.server_hostname
)));
}
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.stream.write_all(b"STARTTLS\r\n").await?;
ret.stream.flush().await?;
ret.read_response(&mut res, false, command_to_replycodes("STARTTLS"))
.await?;
if !res.starts_with("382 ") {
@ -167,10 +158,7 @@ impl NntpStream {
{
// FIXME: This is blocking
let socket = ret
.stream
.into_inner()
.chain_err_kind(crate::error::ErrorKind::Network)?;
let socket = ret.stream.into_inner()?;
let mut conn_result = connector.connect(path, socket);
if let Err(native_tls::HandshakeError::WouldBlock(midhandshake_stream)) =
conn_result
@ -186,16 +174,17 @@ impl NntpStream {
midhandshake_stream = Some(stream);
}
p => {
p.chain_err_kind(crate::error::ErrorKind::Network)?;
p.chain_err_kind(crate::error::ErrorKind::Network(
crate::error::NetworkErrorKind::InvalidTLSConnection,
))?;
}
}
}
}
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.stream =
AsyncWrapper::new(Connection::Tls(conn_result?)).chain_err_summary(|| {
format!("Could not initiate TLS negotiation to {}.", path)
})?;
}
}
//ret.send_command(
@ -367,8 +356,8 @@ impl NntpStream {
last_line_idx += pos + "\r\n".len();
}
}
Err(e) => {
return Err(MeliError::from(e).set_err_kind(crate::error::ErrorKind::Network));
Err(err) => {
return Err(MeliError::from(err));
}
}
}
@ -393,7 +382,7 @@ impl NntpStream {
.await
{
debug!("stream send_command err {:?}", err);
Err(err.set_err_kind(crate::error::ErrorKind::Network))
Err(err)
} else {
Ok(())
}
@ -427,7 +416,7 @@ impl NntpStream {
.await
{
debug!("stream send_multiline_data_block err {:?}", err);
Err(err.set_err_kind(crate::error::ErrorKind::Network))
Err(err)
} 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>,
@ -310,10 +310,30 @@ impl NotmuchDb {
event_consumer: BackendEventConsumer,
) -> Result<Box<dyn MailBackend>> {
#[cfg(not(target_os = "macos"))]
let dlpath = "libnotmuch.so.5";
let mut dlpath = "libnotmuch.so.5";
#[cfg(target_os = "macos")]
let dlpath = "libnotmuch.5.dylib";
let lib = Arc::new(unsafe { libloading::Library::new(dlpath)? });
let mut dlpath = "libnotmuch.5.dylib";
let mut custom_dlpath = false;
if let Some(lib_path) = s.extra.get("library_file_path") {
dlpath = lib_path.as_str();
custom_dlpath = true;
}
let lib = Arc::new(unsafe {
match libloading::Library::new(dlpath) {
Ok(l) => l,
Err(err) => {
if custom_dlpath {
return Err(MeliError::new(format!("Notmuch `library_file_path` setting value `{}` for account {} does not exist or is a directory or not a valid library file.",dlpath, s.name()))
.set_kind(ErrorKind::Configuration)
.set_source(Some(Arc::new(err))));
} else {
return Err(MeliError::new("Could not load libnotmuch!")
.set_details(super::NOTMUCH_ERROR_DETAILS)
.set_source(Some(Arc::new(err))));
}
}
}
});
let mut path = Path::new(s.root_mailbox.as_str()).expand();
if !path.exists() {
return Err(MeliError::new(format!(
@ -388,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,
}))
@ -423,6 +443,15 @@ impl NotmuchDb {
path.pop();
let account_name = s.name().to_string();
if let Some(lib_path) = s.extra.remove("library_file_path") {
if !Path::new(&lib_path).exists() || Path::new(&lib_path).is_dir() {
return Err(MeliError::new(format!(
"Notmuch `library_file_path` setting value `{}` for account {} does not exist or is a directory.",
&lib_path,
s.name()
)).set_kind(ErrorKind::Configuration));
}
}
for (k, f) in s.mailboxes.iter_mut() {
if f.extra.remove("query").is_none() {
return Err(MeliError::new(format!(
@ -665,7 +694,7 @@ impl MailBackend for NotmuchDb {
index.clone(),
mailbox_index.clone(),
collection.tag_index.clone(),
account_hash.clone(),
account_hash,
event_consumer.clone(),
new_revision_uuid,
)?;
@ -699,7 +728,6 @@ impl MailBackend for NotmuchDb {
hash,
index: self.index.clone(),
bytes: None,
collection: self.collection.clone(),
}))
}
@ -903,7 +931,6 @@ 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)]
@ -1032,7 +1059,7 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
ret.push(c);
}
}
ret.push_str("\"");
ret.push('"');
}
To(s) | Cc(s) | Bcc(s) => {
ret.push_str("to:\"");
@ -1043,7 +1070,7 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
ret.push(c);
}
}
ret.push_str("\"");
ret.push('"');
}
InReplyTo(_s) | References(_s) | AllAddresses(_s) => {}
/* * * * */
@ -1056,7 +1083,7 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
ret.push(c);
}
}
ret.push_str("\"");
ret.push('"');
}
Subject(s) => {
ret.push_str("subject:\"");
@ -1067,10 +1094,10 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
ret.push(c);
}
}
ret.push_str("\"");
ret.push('"');
}
AllText(s) => {
ret.push_str("\"");
ret.push('"');
for c in s.chars() {
if c == '"' {
ret.push_str("\\\"");
@ -1078,7 +1105,7 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
ret.push(c);
}
}
ret.push_str("\"");
ret.push('"');
}
/* * * * */
Flags(v) => {

View File

@ -16,9 +16,11 @@ 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.)
@ -466,6 +468,7 @@ 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,
@ -490,6 +493,7 @@ 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,
@ -501,8 +505,10 @@ 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,
@ -616,7 +622,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
@ -703,7 +709,9 @@ 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.
///
@ -757,7 +765,9 @@ 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,
@ -765,7 +775,9 @@ 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(
@ -813,7 +825,9 @@ 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,
@ -821,7 +835,9 @@ 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(
@ -887,6 +903,7 @@ 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.
@ -895,6 +912,7 @@ 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,
@ -902,9 +920,10 @@ 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,
@ -919,6 +938,7 @@ 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
@ -930,6 +950,7 @@ 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,
@ -937,8 +958,10 @@ 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,
@ -964,8 +987,10 @@ 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;
@ -1136,7 +1161,9 @@ 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;
@ -1188,8 +1215,10 @@ 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;
@ -1450,6 +1479,7 @@ 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);
@ -1458,6 +1488,7 @@ 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
@ -1508,7 +1539,9 @@ 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
@ -1525,10 +1558,12 @@ 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,
@ -1537,11 +1572,13 @@ 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,
@ -1552,11 +1589,13 @@ 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,
@ -1565,6 +1604,7 @@ 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
@ -1574,6 +1614,7 @@ 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,
@ -1581,6 +1622,7 @@ 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
@ -1590,6 +1632,7 @@ 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,
@ -1612,10 +1655,12 @@ 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:
///
@ -1635,7 +1680,9 @@ 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,
@ -1644,6 +1691,7 @@ 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.
@ -1653,6 +1701,7 @@ 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,
@ -1672,7 +1721,9 @@ 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;
@ -1685,7 +1736,9 @@ 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);
@ -1693,7 +1746,9 @@ 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;
@ -1702,7 +1757,9 @@ 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;
@ -1713,7 +1770,9 @@ 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);
@ -1821,7 +1880,9 @@ 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;
@ -1872,7 +1933,9 @@ 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,
@ -1887,7 +1950,9 @@ 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,
@ -1896,7 +1961,9 @@ 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,
@ -1905,7 +1972,9 @@ 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;
@ -1914,7 +1983,9 @@ 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;
@ -1923,19 +1994,25 @@ 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);
@ -1948,7 +2025,9 @@ 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;
@ -1967,7 +2046,9 @@ 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,
@ -1976,17 +2057,23 @@ 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).unwrap()
self.guard.get(&self.hash).expect("Hash was not found")
}
}
@ -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).unwrap()
self.guard.get_mut(&self.hash).expect("Hash was not found")
}
}
@ -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).unwrap()
self.guard.get(&self.hash).expect("Hash was not found")
}
}

View File

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

View File

@ -271,7 +271,9 @@ 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),
.set_kind(crate::error::ErrorKind::Network(
crate::error::NetworkErrorKind::HostLookupFailed,
)),
)
}

View File

@ -41,6 +41,7 @@ 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";
@ -74,14 +75,36 @@ 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: std::os::raw::c_int,
mask: LocaleCategoryMask,
category: LocaleCategory,
old_locale: *const std::os::raw::c_char,
}
@ -94,7 +117,7 @@ impl Drop for Locale {
}
#[cfg(target_os = "netbsd")]
unsafe {
let _ = libc::setlocale(self.mask, self.old_locale);
let _ = libc::setlocale(self.category as c_int, self.old_locale);
}
}
}
@ -103,11 +126,12 @@ impl Drop for Locale {
impl Locale {
#[cfg(not(target_os = "netbsd"))]
fn new(
mask: std::os::raw::c_int,
mask: LocaleCategoryMask,
category: LocaleCategory,
locale: *const std::os::raw::c_char,
base: libc::locale_t,
) -> Result<Self> {
let new_locale = unsafe { libc::newlocale(mask, locale, base) };
let new_locale = unsafe { libc::newlocale(mask as c_int, locale, base) };
if new_locale.is_null() {
return Err(nix::Error::last().into());
}
@ -117,25 +141,32 @@ impl Locale {
return Err(nix::Error::last().into());
}
Ok(Locale {
mask,
category,
new_locale,
old_locale,
})
}
#[cfg(target_os = "netbsd")]
fn new(
mask: std::os::raw::c_int,
mask: LocaleCategoryMask,
category: LocaleCategory,
locale: *const std::os::raw::c_char,
_base: libc::locale_t,
) -> Result<Self> {
let old_locale = unsafe { libc::setlocale(mask, std::ptr::null_mut()) };
let old_locale = unsafe { libc::setlocale(category as c_int, std::ptr::null_mut()) };
if old_locale.is_null() {
return Err(nix::Error::last().into());
}
let new_locale = unsafe { libc::setlocale(mask, locale) };
let new_locale = unsafe { libc::setlocale(category as c_int, locale) };
if new_locale.is_null() {
return Err(nix::Error::last().into());
}
Ok(Locale { mask, old_locale })
Ok(Locale {
mask,
category,
old_locale,
})
}
}
@ -166,7 +197,8 @@ pub fn timestamp_to_string(timestamp: UnixTimestamp, fmt: Option<&str>, posix: b
let _with_locale: Option<Result<Locale>> = if posix {
Some(
Locale::new(
libc::LC_TIME,
LocaleCategoryMask::Time,
LocaleCategory::Time,
b"C\0".as_ptr() as *const std::os::raw::c_char,
std::ptr::null_mut(),
)
@ -233,9 +265,7 @@ 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;
rem = (year - 100) % 400;
let mut rem = (year - 100) % 400;
if rem == 0 {
*is_leap = true;
@ -306,7 +336,8 @@ where
let fmt = unsafe { CStr::from_bytes_with_nul_unchecked(fmt.as_bytes()) };
let ret = {
let _with_locale = Locale::new(
libc::LC_TIME,
LocaleCategoryMask::Time,
LocaleCategory::Time,
b"C\0".as_ptr() as *const std::os::raw::c_char,
std::ptr::null_mut(),
)
@ -367,7 +398,8 @@ where
let fmt = unsafe { CStr::from_bytes_with_nul_unchecked(fmt.as_bytes()) };
let ret = {
let _with_locale = Locale::new(
libc::LC_TIME,
LocaleCategoryMask::Time,
LocaleCategory::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`](email::address::Address) types.
* structure. Addresses in `To`, `From` fields etc are parsed into [`Address`](crate::email::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: format!("{}", address).into_bytes(),
raw: address.to_string().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(|err| Into::<MeliError>::into(err))
.map_err(Into::<MeliError>::into)
.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<'a>(&self, s: &'a [u8]) -> String {
pub fn display(&self, s: &[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,22 +372,17 @@ impl Display for ContentType {
impl ContentType {
pub fn is_text(&self) -> bool {
if let ContentType::Text { .. } = self {
true
} else {
false
}
matches!(self, ContentType::Text { .. })
}
pub fn is_text_html(&self) -> bool {
if let ContentType::Text {
kind: Text::Html, ..
} = self
{
true
} else {
false
}
matches!(
self,
ContentType::Text {
kind: Text::Html,
..
}
)
}
pub fn make_boundary(parts: &[AttachmentBuilder]) -> String {
@ -453,11 +448,7 @@ pub enum Text {
impl Text {
pub fn is_html(&self) -> bool {
if let Text::Html = self {
true
} else {
false
}
matches!(self, Text::Html)
}
}
@ -537,11 +528,11 @@ pub enum ContentDispositionKind {
impl ContentDispositionKind {
pub fn is_inline(&self) -> bool {
*self == ContentDispositionKind::Inline
matches!(self, ContentDispositionKind::Inline)
}
pub fn is_attachment(&self) -> bool {
*self == ContentDispositionKind::Attachment
matches!(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,20 +516,19 @@ impl Attachment {
.iter()
.find(|(n, _)| n.eq_ignore_ascii_case(b"content-type"))
.and_then(|(_, v)| {
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;
}
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;
}
return boundary;
}
return boundary;
}
_ => {}
}
None
})
@ -554,7 +553,7 @@ impl Attachment {
fn get_text_recursive(&self, text: &mut Vec<u8>) {
match self.content_type {
ContentType::Text { .. } | ContentType::PGPSignature | ContentType::CMSSignature => {
text.extend(decode(self, None));
text.extend(self.decode(Default::default()));
}
ContentType::Multipart {
ref kind,
@ -585,6 +584,7 @@ impl Attachment {
_ => {}
}
}
pub fn text(&self) -> String {
let mut text = Vec::with_capacity(self.body.length);
self.get_text_recursive(&mut text);
@ -594,6 +594,7 @@ 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>) {
@ -612,24 +613,26 @@ 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 {
match self.content_type {
ContentType::Text { .. } => true,
_ => false,
}
matches!(self.content_type, ContentType::Text { .. })
}
pub fn is_html(&self) -> bool {
match self.content_type {
ContentType::Text {
@ -650,23 +653,23 @@ impl Attachment {
}
pub fn is_encrypted(&self) -> bool {
match self.content_type {
matches!(
self.content_type,
ContentType::Multipart {
kind: MultipartType::Encrypted,
..
} => true,
_ => false,
}
}
)
}
pub fn is_signed(&self) -> bool {
match self.content_type {
matches!(
self.content_type,
ContentType::Multipart {
kind: MultipartType::Signed,
..
} => true,
_ => false,
}
}
)
}
pub fn into_raw(&self) -> String {
@ -689,13 +692,13 @@ impl Attachment {
for (n, v) in parameters {
ret.push_str("; ");
ret.push_str(&String::from_utf8_lossy(n));
ret.push_str("=");
ret.push('=');
if v.contains(&b' ') {
ret.push_str("\"");
ret.push('"');
}
ret.push_str(&String::from_utf8_lossy(v));
if v.contains(&b' ') {
ret.push_str("\"");
ret.push('"');
}
}
@ -738,7 +741,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));
@ -758,11 +761,8 @@ impl Attachment {
};
for (name, value) in headers {
if name.eq_ignore_ascii_case(b"content-type") {
match parser::attachments::content_type(value) {
Ok((_, (_, _, params))) => {
ret = params;
}
_ => {}
if let Ok((_, (_, _, params))) = parser::attachments::content_type(value) {
ret = params;
}
break;
}
@ -794,123 +794,133 @@ impl Attachment {
.map(|(_, v)| v)
.ok()
.and_then(|n| String::from_utf8(n).ok())
.unwrap_or_else(|| s)
.unwrap_or(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!()
}
type Filter<'a> = Box<dyn FnMut(&Attachment, &mut Vec<u8>) + 'a>;
pub type Filter<'a> = Box<dyn FnMut(&Attachment, &mut Vec<u8>) + 'a>;
fn decode_rec_helper<'a, 'b>(a: &'a Attachment, filter: &mut Option<Filter<'b>>) -> Vec<u8> {
match a.content_type {
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)
#[derive(Default)]
pub struct DecodeOptions<'att> {
pub filter: Option<Filter<'att>>,
pub force_charset: Option<Charset>,
}

View File

@ -24,13 +24,13 @@ use super::*;
use crate::email::attachment_types::{
Charset, ContentTransferEncoding, ContentType, MultipartType,
};
use crate::email::attachments::{decode, decode_rec, AttachmentBuilder};
use crate::email::attachments::AttachmentBuilder;
use crate::shellexpand::ShellExpandTrait;
use data_encoding::BASE64_MIME;
use std::ffi::OsStr;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::str;
use std::str::FromStr;
use xdg_utils::query_mime_info;
pub mod mime;
@ -44,6 +44,7 @@ use super::parser;
pub struct Draft {
pub headers: HeaderMap,
pub body: String,
pub wrap_header_preamble: Option<(String, String)>,
pub attachments: Vec<AttachmentBuilder>,
}
@ -68,13 +69,14 @@ impl Default for Draft {
Draft {
headers,
body: String::new(),
wrap_header_preamble: None,
attachments: Vec::new(),
}
}
}
impl str::FromStr for Draft {
impl FromStr for Draft {
type Err = MeliError;
fn from_str(s: &str) -> Result<Self> {
if s.is_empty() {
@ -90,7 +92,7 @@ impl str::FromStr for Draft {
}
let body = Envelope::new(0).body_bytes(s.as_bytes());
ret.body = String::from_utf8(decode(&body, None))?;
ret.body = String::from_utf8(body.decode(Default::default()))?;
Ok(ret)
}
@ -99,7 +101,7 @@ impl str::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());
}
@ -114,6 +116,39 @@ impl Draft {
self
}
pub fn set_wrap_header_preamble(&mut self, value: Option<(String, String)>) -> &mut Self {
self.wrap_header_preamble = value;
self
}
pub fn update(&mut self, value: &str) -> Result<bool> {
let mut value: std::borrow::Cow<'_, str> = value.into();
if let Some((pre, post)) = self.wrap_header_preamble.as_ref() {
let mut s = value.as_ref();
s = s.strip_prefix(pre).unwrap_or(s);
s = s.strip_prefix('\n').unwrap_or(s);
if let Some(pos) = s.find(post) {
let mut headers = &s[..pos];
headers = headers.strip_suffix(post).unwrap_or(headers);
if headers.ends_with('\n') {
headers = &headers[..headers.len() - 1];
}
value = format!(
"{headers}{body}",
headers = headers,
body = &s[pos + post.len()..]
)
.into();
}
}
let new = Draft::from_str(value.as_ref())?;
let changes: bool = self.headers != new.headers || self.body != new.body;
self.headers = new.headers;
self.body = new.body;
Ok(changes)
}
pub fn new_reply(envelope: &Envelope, bytes: &[u8], reply_to_all: bool) -> Self {
let mut ret = Draft::default();
ret.headers_mut().insert(
@ -172,7 +207,7 @@ impl Draft {
);
let body = envelope.body_bytes(bytes);
ret.body = {
let reply_body_bytes = decode_rec(&body, None);
let reply_body_bytes = body.decode_rec(Default::default());
let reply_body = String::from_utf8_lossy(&reply_body_bytes);
let lines: Vec<&str> = reply_body.lines().collect();
let mut ret = format!(
@ -217,17 +252,36 @@ impl Draft {
self
}
pub fn to_string(&self) -> Result<String> {
pub fn to_edit_string(&self) -> String {
let mut ret = String::new();
if let Some((pre, _)) = self.wrap_header_preamble.as_ref() {
if !pre.is_empty() {
ret.push_str(pre);
if !pre.ends_with('\n') {
ret.push('\n');
}
}
}
for (k, v) in self.headers.deref() {
ret.push_str(&format!("{}: {}\n", k, v));
}
if let Some((_, post)) = self.wrap_header_preamble.as_ref() {
if !post.is_empty() {
if !post.starts_with('\n') && !ret.ends_with('\n') {
ret.push('\n');
}
ret.push_str(post);
ret.push('\n');
}
}
ret.push('\n');
ret.push_str(&self.body);
Ok(ret)
ret
}
pub fn finalise(mut self) -> Result<String> {
@ -270,11 +324,11 @@ impl Draft {
ret.push_str("\r\n");
}
} else if self.body.is_empty() && self.attachments.len() == 1 {
let attachment = std::mem::replace(&mut self.attachments, Vec::new()).remove(0);
let attachment = std::mem::take(&mut self.attachments).remove(0);
print_attachment(&mut ret, attachment);
} else {
let mut parts = Vec::with_capacity(self.attachments.len() + 1);
let attachments = std::mem::replace(&mut self.attachments, Vec::new());
let attachments = std::mem::take(&mut self.attachments);
if !self.body.is_empty() {
let mut body_attachment = AttachmentBuilder::default();
body_attachment.set_raw(self.body.as_bytes().to_vec());
@ -415,27 +469,69 @@ mod tests {
use std::str::FromStr;
#[test]
fn test_new() {
fn test_new_draft() {
let mut default = Draft::default();
assert_eq!(
Draft::from_str(&default.to_string().unwrap()).unwrap(),
default
);
assert_eq!(Draft::from_str(&default.to_edit_string()).unwrap(), default);
default.set_body("αδφαφσαφασ".to_string());
assert_eq!(
Draft::from_str(&default.to_string().unwrap()).unwrap(),
default
);
assert_eq!(Draft::from_str(&default.to_edit_string()).unwrap(), default);
default.set_body("ascii only".to_string());
assert_eq!(
Draft::from_str(&default.to_string().unwrap()).unwrap(),
default
);
assert_eq!(Draft::from_str(&default.to_edit_string()).unwrap(), default);
}
#[test]
fn test_draft_update() {
let mut default = Draft::default();
default
.set_wrap_header_preamble(Some(("<!--".to_string(), "-->".to_string())))
.set_body("αδφαφσαφασ".to_string())
.set_header("Subject", "test_update()".into())
.set_header("Date", "Sun, 16 Jun 2013 17:56:45 +0200".into());
let original = default.clone();
let s = default.to_edit_string();
assert_eq!(s, "<!--\nDate: Sun, 16 Jun 2013 17:56:45 +0200\nFrom: \nTo: \nCc: \nBcc: \nSubject: test_update()\n-->\n\nαδφαφσαφασ");
assert!(!default.update(&s).unwrap());
assert_eq!(&original, &default);
default.set_wrap_header_preamble(Some(("".to_string(), "".to_string())));
let original = default.clone();
let s = default.to_edit_string();
assert_eq!(s, "Date: Sun, 16 Jun 2013 17:56:45 +0200\nFrom: \nTo: \nCc: \nBcc: \nSubject: test_update()\n\nαδφαφσαφασ");
assert!(!default.update(&s).unwrap());
assert_eq!(&original, &default);
default.set_wrap_header_preamble(None);
let original = default.clone();
let s = default.to_edit_string();
assert_eq!(s, "Date: Sun, 16 Jun 2013 17:56:45 +0200\nFrom: \nTo: \nCc: \nBcc: \nSubject: test_update()\n\nαδφαφσαφασ");
assert!(!default.update(&s).unwrap());
assert_eq!(&original, &default);
default.set_wrap_header_preamble(Some((
"{-\n\n\n===========".to_string(),
"</mixed>".to_string(),
)));
let original = default.clone();
let s = default.to_edit_string();
assert_eq!(s, "{-\n\n\n===========\nDate: Sun, 16 Jun 2013 17:56:45 +0200\nFrom: \nTo: \nCc: \nBcc: \nSubject: test_update()\n</mixed>\n\nαδφαφσαφασ");
assert!(!default.update(&s).unwrap());
assert_eq!(&original, &default);
default
.set_body(
"hellohello<!--\n<!--\n<--hellohello\nhellohello-->\n-->\n-->hello\n".to_string(),
)
.set_wrap_header_preamble(Some(("<!--".to_string(), "-->".to_string())));
let original = default.clone();
let s = default.to_edit_string();
assert_eq!(s, "<!--\nDate: Sun, 16 Jun 2013 17:56:45 +0200\nFrom: \nTo: \nCc: \nBcc: \nSubject: test_update()\n-->\n\nhellohello<!--\n<!--\n<--hellohello\nhellohello-->\n-->\n-->hello\n");
assert!(!default.update(&s).unwrap());
assert_eq!(&original, &default);
}
/*
#[test]
fn test_attachments() {
/*
let mut default = Draft::default();
default.set_body("αδφαφσαφασ".to_string());
@ -453,8 +549,8 @@ mod tests {
.set_content_transfer_encoding(ContentTransferEncoding::Base64);
default.attachments_mut().push(attachment);
println!("{}", default.finalise().unwrap());
*/
}
*/
}
/// Reads file from given path, and returns an 'application/octet-stream' AttachmentBuilder object

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,6 +444,7 @@ 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
@ -452,6 +453,7 @@ 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();
@ -472,23 +474,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)),
@ -571,7 +573,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(
@ -861,12 +863,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().filter_map(|s| s).fold(vec![], |mut acc, x| {
let mut pr = pr.into_iter().flatten().fold(vec![], |mut acc, x| {
acc.extend_from_slice(&x);
acc
});
if pr.is_empty() {
Ok((input, end.unwrap_or((&b""[..]).into())))
Ok((input, end.unwrap_or_else(|| (&b""[..]).into())))
} else {
if let Some(end) = end {
pr.extend_from_slice(&end);
@ -1219,7 +1221,7 @@ pub mod generic {
{
Ok((&input[1..], input[0..1].into()))
} else {
return Err(nom::Err::Error((input, "atext(): invalid byte").into()));
Err(nom::Err::Error((input, "atext(): invalid byte").into()))
}
}
@ -1227,12 +1229,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.into()))
Ok((input, ret))
}
///```text
@ -2064,7 +2066,7 @@ pub mod encodings {
input,
list.iter()
.fold(SmallVec::with_capacity(list_len), |mut acc, x| {
acc.extend(x.into_iter().cloned());
acc.extend(x.iter().cloned());
acc
}),
))
@ -2113,7 +2115,7 @@ pub mod encodings {
}
let end = input[ptr..].find(b"=?");
let end = end.unwrap_or_else(|| input.len() - ptr) + ptr;
let end = end.unwrap_or(input.len() - ptr) + ptr;
let ascii_s = ptr;
let mut ascii_e = 0;
@ -2378,7 +2380,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, |s| Some(s)), angle_addr),
pair(map(display_name, Some), angle_addr),
map(angle_addr, |r| (None, r)),
))(input)?;
Ok((
@ -2475,7 +2477,7 @@ pub mod address {
.trim(),
);
if i != list_len - 1 {
acc.push_str(" ");
acc.push(' ');
i += 1;
}
acc

View File

@ -34,6 +34,284 @@ 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,
@ -41,7 +319,7 @@ pub enum ErrorKind {
Authentication,
Configuration,
Bug,
Network,
Network(NetworkErrorKind),
Timeout,
OSError,
NotImplemented,
@ -58,7 +336,7 @@ impl fmt::Display for ErrorKind {
ErrorKind::External => "External",
ErrorKind::Authentication => "Authentication",
ErrorKind::Bug => "Bug, please report this!",
ErrorKind::Network => "Network",
ErrorKind::Network(ref inner) => inner.as_str(),
ErrorKind::Timeout => "Timeout",
ErrorKind::OSError => "OS Error",
ErrorKind::Configuration => "Configuration",
@ -71,7 +349,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 {
@ -85,14 +363,18 @@ impl ErrorKind {
#[derive(Debug, Clone)]
pub struct MeliError {
pub summary: Option<Cow<'static, str>>,
pub details: Cow<'static, str>,
pub summary: Cow<'static, str>,
pub details: Option<Cow<'static, str>>,
pub source: Option<std::sync::Arc<dyn Error + Send + Sync + 'static>>,
pub kind: ErrorKind,
}
pub trait IntoMeliError {
fn set_err_summary<M>(self, msg: M) -> MeliError
where
M: Into<Cow<'static, str>>;
fn set_err_details<M>(self, msg: M) -> MeliError
where
M: Into<Cow<'static, str>>;
fn set_err_kind(self, kind: ErrorKind) -> MeliError;
@ -117,6 +399,15 @@ impl<I: Into<MeliError>> IntoMeliError for I {
err.set_summary(msg)
}
#[inline]
fn set_err_details<M>(self, msg: M) -> MeliError
where
M: Into<Cow<'static, str>>,
{
let err: MeliError = self.into();
err.set_details(msg)
}
#[inline]
fn set_err_kind(self, kind: ErrorKind) -> MeliError {
let err: MeliError = self.into();
@ -146,21 +437,33 @@ impl MeliError {
M: Into<Cow<'static, str>>,
{
MeliError {
summary: None,
details: msg.into(),
summary: msg.into(),
details: None,
source: None,
kind: ErrorKind::None,
}
}
pub fn set_details<M>(mut self, details: M) -> MeliError
where
M: Into<Cow<'static, str>>,
{
if let Some(old_details) = self.details.as_ref() {
self.details = Some(format!("{}. {}", old_details, details.into()).into());
} else {
self.details = Some(details.into());
}
self
}
pub fn set_summary<M>(mut self, summary: M) -> MeliError
where
M: Into<Cow<'static, str>>,
{
if let Some(old_summary) = self.summary.take() {
self.summary = Some(format!("{}. {}", old_summary, summary.into()).into());
if self.summary.is_empty() {
self.summary = summary.into();
} else {
self.summary = Some(summary.into());
self.summary = format!("{}. {}", self.summary, summary.into()).into();
}
self
}
@ -181,10 +484,10 @@ impl MeliError {
impl fmt::Display for MeliError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(summary) = self.summary.as_ref() {
writeln!(f, "Summary: {}", summary)?;
writeln!(f, "{}", self.summary)?;
if let Some(details) = self.details.as_ref() {
write!(f, "{}", details)?;
}
write!(f, "{}", self.details)?;
if let Some(source) = self.source.as_ref() {
write!(f, "\nCaused by: {}", source)?;
}
@ -205,7 +508,7 @@ impl From<io::Error> for MeliError {
#[inline]
fn from(kind: io::Error) -> MeliError {
MeliError::new(kind.to_string())
.set_summary(format!("{:?}", kind.kind()))
.set_details(kind.kind().to_string())
.set_source(Some(Arc::new(kind)))
.set_kind(ErrorKind::OSError)
}
@ -214,21 +517,21 @@ impl From<io::Error> for MeliError {
impl<'a> From<Cow<'a, str>> for MeliError {
#[inline]
fn from(kind: Cow<'_, str>) -> MeliError {
MeliError::new(format!("{:?}", kind))
MeliError::new(kind.to_string())
}
}
impl From<string::FromUtf8Error> for MeliError {
#[inline]
fn from(kind: string::FromUtf8Error) -> MeliError {
MeliError::new(format!("{:?}", kind)).set_source(Some(Arc::new(kind)))
MeliError::new(kind.to_string()).set_source(Some(Arc::new(kind)))
}
}
impl From<str::Utf8Error> for MeliError {
#[inline]
fn from(kind: str::Utf8Error) -> MeliError {
MeliError::new(format!("{:?}", kind)).set_source(Some(Arc::new(kind)))
MeliError::new(kind.to_string()).set_source(Some(Arc::new(kind)))
}
}
//use std::option;
@ -242,7 +545,7 @@ impl From<str::Utf8Error> for MeliError {
impl<T> From<std::sync::PoisonError<T>> for MeliError {
#[inline]
fn from(kind: std::sync::PoisonError<T>) -> MeliError {
MeliError::new(format!("{}", kind))
MeliError::new(kind.to_string()).set_kind(ErrorKind::Bug)
}
}
@ -252,7 +555,9 @@ impl<T: Sync + Send + 'static + core::fmt::Debug> From<native_tls::HandshakeErro
{
#[inline]
fn from(kind: native_tls::HandshakeError<T>) -> MeliError {
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
MeliError::new(kind.to_string())
.set_source(Some(Arc::new(kind)))
.set_kind(ErrorKind::Network(NetworkErrorKind::InvalidTLSConnection))
}
}
@ -260,22 +565,59 @@ impl<T: Sync + Send + 'static + core::fmt::Debug> From<native_tls::HandshakeErro
impl From<native_tls::Error> for MeliError {
#[inline]
fn from(kind: native_tls::Error) -> MeliError {
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
MeliError::new(kind.to_string())
.set_source(Some(Arc::new(kind)))
.set_kind(ErrorKind::Network(NetworkErrorKind::InvalidTLSConnection))
}
}
impl From<std::num::ParseIntError> for MeliError {
#[inline]
fn from(kind: std::num::ParseIntError) -> MeliError {
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
MeliError::new(kind.to_string()).set_source(Some(Arc::new(kind)))
}
}
#[cfg(feature = "jmap_backend")]
#[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")]
impl From<isahc::Error> for MeliError {
#[inline]
fn from(kind: isahc::Error) -> MeliError {
MeliError::new(kind.to_string()).set_source(Some(Arc::new(kind)))
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))
}
}
@ -283,35 +625,35 @@ impl From<isahc::Error> for MeliError {
impl From<serde_json::error::Error> for MeliError {
#[inline]
fn from(kind: serde_json::error::Error) -> MeliError {
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
MeliError::new(kind.to_string()).set_source(Some(Arc::new(kind)))
}
}
impl From<Box<dyn Error + Sync + Send + 'static>> for MeliError {
#[inline]
fn from(kind: Box<dyn Error + Sync + Send + 'static>) -> MeliError {
MeliError::new(format!("{}", kind)).set_source(Some(kind.into()))
MeliError::new(kind.to_string()).set_source(Some(kind.into()))
}
}
impl From<std::ffi::NulError> for MeliError {
#[inline]
fn from(kind: std::ffi::NulError) -> MeliError {
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
MeliError::new(kind.to_string()).set_source(Some(Arc::new(kind)))
}
}
impl From<Box<bincode::ErrorKind>> for MeliError {
#[inline]
fn from(kind: Box<bincode::ErrorKind>) -> MeliError {
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
MeliError::new(kind.to_string()).set_source(Some(Arc::new(kind)))
}
}
impl From<nix::Error> for MeliError {
#[inline]
fn from(kind: nix::Error) -> MeliError {
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
MeliError::new(kind.to_string()).set_source(Some(Arc::new(kind)))
}
}
@ -319,14 +661,14 @@ impl From<nix::Error> for MeliError {
impl From<rusqlite::Error> for MeliError {
#[inline]
fn from(kind: rusqlite::Error) -> MeliError {
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
MeliError::new(kind.to_string()).set_source(Some(Arc::new(kind)))
}
}
impl From<libloading::Error> for MeliError {
#[inline]
fn from(kind: libloading::Error) -> MeliError {
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
MeliError::new(kind.to_string()).set_source(Some(Arc::new(kind)))
}
}
@ -347,16 +689,14 @@ impl From<String> for MeliError {
impl From<nom::Err<(&[u8], nom::error::ErrorKind)>> for MeliError {
#[inline]
fn from(kind: nom::Err<(&[u8], nom::error::ErrorKind)>) -> MeliError {
MeliError::new("Parsing error")
.set_source(Some(Arc::new(MeliError::new(format!("{}", kind)))))
MeliError::new("Parsing error").set_source(Some(Arc::new(MeliError::new(kind.to_string()))))
}
}
impl From<nom::Err<(&str, nom::error::ErrorKind)>> for MeliError {
#[inline]
fn from(kind: nom::Err<(&str, nom::error::ErrorKind)>) -> MeliError {
MeliError::new("Parsing error")
.set_source(Some(Arc::new(MeliError::new(format!("{}", kind)))))
MeliError::new("Parsing error").set_details(kind.to_string())
}
}

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

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_str(",");
accum.push(',');
}
}};
}
@ -360,7 +360,7 @@ impl Context {
self.set_flag_inner(
auto_key_locate,
CStr::from_bytes_with_nul(accum.as_bytes())
.expect(accum.as_str())
.map_err(|err| format!("Expected `{}`: {}", accum.as_str(), err))?
.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 =
let verify_result: gpgme_verify_result_t =
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));
}
drop(verify_result);
unsafe { call!(&ctx.lib, gpgme_free)(verify_result as *mut ::libc::c_void) };
}
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"
})?;
Ok(sig.into_bytes()?)
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")?;
Ok(cipher.into_bytes()?)
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(OsStr::from_bytes(b""));
.unwrap_or_else(|| OsStr::from_bytes(b""));
let prefix = if let Some(p) = self.parent() {
p
} else {
@ -354,37 +354,33 @@ pub mod shellexpand {
}
if let Ok(iter) = std::fs::read_dir(&prefix) {
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())
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"/")
{
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(),
)
});
}
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,6 +93,22 @@ 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()..], ())),
@ -178,6 +194,24 @@ 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>,
}
@ -217,6 +251,8 @@ 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>,
@ -290,6 +326,69 @@ 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>,
@ -345,3 +444,198 @@ 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,6 +25,9 @@
/*!
* 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
*
@ -155,11 +158,11 @@ pub struct SmtpAuthType {
login: bool,
}
fn true_val() -> bool {
const fn true_val() -> bool {
true
}
fn false_val() -> bool {
const fn false_val() -> bool {
false
}
@ -195,6 +198,9 @@ 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
//issue multiple responses after a message has been transferred, by mutual consent of the
//client and server. SMTP clients that support the PRDR extension then use the expanded
@ -202,7 +208,7 @@ pub struct SmtpExtensionSupport {
//envelope exchange.
#[serde(default = "crate::conf::true_val")]
prdr: bool,
#[serde(default = "crate::conf::false_val")]
#[serde(default = "crate::conf::true_val")]
binarymime: bool,
//Resources:
//- http://www.postfix.org/SMTPUTF8_README.html
@ -224,7 +230,8 @@ impl Default for SmtpExtensionSupport {
pipelining: true,
chunking: true,
prdr: true,
binarymime: false,
_8bitmime: true,
binarymime: true,
smtputf8: true,
auth: true,
dsn_notify: Some("FAILURE".into()),
@ -261,16 +268,13 @@ impl SmtpConnection {
if danger_accept_invalid_certs {
connector.danger_accept_invalid_certs(true);
}
let connector = connector
.build()
.chain_err_kind(crate::error::ErrorKind::Network)?;
let connector = connector.build()?;
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))
.chain_err_kind(crate::error::ErrorKind::Network)?,
))
.chain_err_kind(crate::error::ErrorKind::Network)?;
let mut socket = AsyncWrapper::new(Connection::Tcp(TcpStream::connect_timeout(
&addr,
std::time::Duration::new(4, 0),
)?))?;
let pre_ehlo_extensions_reply = read_lines(
&mut socket,
&mut res,
@ -293,10 +297,7 @@ 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
.chain_err_kind(crate::error::ErrorKind::Network)?;
socket.write_all(b"EHLO meli.delivery\r\n").await?;
if let SmtpSecurity::StartTLS { .. } = server_conf.security {
let pre_tls_extensions_reply = read_lines(
&mut socket,
@ -307,10 +308,7 @@ impl SmtpConnection {
.await?;
drop(pre_tls_extensions_reply);
//debug!(pre_tls_extensions_reply);
socket
.write_all(b"STARTTLS\r\n")
.await
.chain_err_kind(crate::error::ErrorKind::Network)?;
socket.write_all(b"STARTTLS\r\n").await?;
let _post_starttls_extensions_reply = read_lines(
&mut socket,
&mut res,
@ -322,15 +320,11 @@ impl SmtpConnection {
}
let mut ret = {
let socket = socket
.into_inner()
.chain_err_kind(crate::error::ErrorKind::Network)?;
let socket = socket.into_inner()?;
let _path = path.clone();
socket.set_nonblocking(false)?;
let conn = unblock(move || connector.connect(&_path, socket))
.await
.chain_err_kind(crate::error::ErrorKind::Network)?;
let conn = unblock(move || connector.connect(&_path, socket)).await?;
/*
if let Err(native_tls::HandshakeError::WouldBlock(midhandshake_stream)) =
conn_result
@ -352,21 +346,17 @@ impl SmtpConnection {
}
}
*/
AsyncWrapper::new(Connection::Tls(conn))
.chain_err_kind(crate::error::ErrorKind::Network)?
AsyncWrapper::new(Connection::Tls(conn))?
};
ret.write_all(b"EHLO meli.delivery\r\n")
.await
.chain_err_kind(crate::error::ErrorKind::Network)?;
ret.write_all(b"EHLO meli.delivery\r\n").await?;
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))
.chain_err_kind(crate::error::ErrorKind::Network)?,
))
.chain_err_kind(crate::error::ErrorKind::Network)?;
let mut ret = AsyncWrapper::new(Connection::Tcp(TcpStream::connect_timeout(
&addr,
std::time::Duration::new(4, 0),
)?))?;
res.clear();
let reply = read_lines(
&mut ret,
@ -384,9 +374,7 @@ impl SmtpConnection {
Reply::new(&res, code)
)));
}
ret.write_all(b"EHLO meli.delivery\r\n")
.await
.chain_err_kind(crate::error::ErrorKind::Network)?;
ret.write_all(b"EHLO meli.delivery\r\n").await?;
ret
}
};
@ -420,10 +408,10 @@ impl SmtpConnection {
ref mut auth_type, ..
} = ret.server_conf.auth
{
for l in pre_auth_extensions_reply
if let Some(l) = pre_auth_extensions_reply
.lines
.iter()
.filter(|l| l.starts_with("AUTH"))
.find(|l| l.starts_with("AUTH"))
{
let l = l["AUTH ".len()..].trim();
for _type in l.split_whitespace() {
@ -433,7 +421,6 @@ impl SmtpConnection {
auth_type.login = true;
}
}
break;
}
}
}
@ -561,6 +548,7 @@ impl SmtpConnection {
self.server_conf.extensions.pipelining &= reply.lines.contains(&"PIPELINING");
self.server_conf.extensions.chunking &= reply.lines.contains(&"CHUNKING");
self.server_conf.extensions.prdr &= reply.lines.contains(&"PRDR");
self.server_conf.extensions._8bitmime &= reply.lines.contains(&"8BITMIME");
self.server_conf.extensions.binarymime &= reply.lines.contains(&"BINARYMIME");
self.server_conf.extensions.smtputf8 &= reply.lines.contains(&"SMTPUTF8");
if !reply.lines.contains(&"DSN") {
@ -594,15 +582,10 @@ impl SmtpConnection {
// .trim()
//);
for c in command {
self.stream
.write_all(c)
.await
.chain_err_kind(crate::error::ErrorKind::Network)?;
self.stream.write_all(c).await?;
}
self.stream
.write_all(b"\r\n")
.await
.chain_err_kind(crate::error::ErrorKind::Network)
self.stream.write_all(b"\r\n").await?;
Ok(())
}
/// Sends mail
@ -637,6 +620,11 @@ impl SmtpConnection {
if self.server_conf.extensions.prdr {
current_command.push(b" PRDR");
}
if self.server_conf.extensions.binarymime {
current_command.push(b" BODY=BINARYMIME");
} else if self.server_conf.extensions._8bitmime {
current_command.push(b" BODY=8BITMIME");
}
self.send_command(&current_command).await?;
current_command.clear();
if !self.server_conf.extensions.pipelining {
@ -652,9 +640,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
.into_iter()
.chain(envelope.cc().into_iter())
.chain(envelope.bcc().into_iter())
.iter()
.chain(envelope.cc().iter())
.chain(envelope.bcc().iter())
{
current_command.clear();
current_command.push(b"RCPT TO:<");
@ -681,70 +669,62 @@ impl SmtpConnection {
//permitted on either side of the colon following FROM in the MAIL command or TO in the
//RCPT command. The syntax is exactly as given above.
//The third step in the procedure is the DATA command
//(or some alternative specified in a service extension).
//DATA <CRLF>
self.send_command(&[b"DATA"]).await?;
//Client SMTP implementations that employ pipelining MUST check ALL statuses associated
//with each command in a group. For example, if none of the RCPT TO recipient addresses
//were accepted the client must then check the response to the DATA command -- the client
//cannot assume that the DATA command will be rejected just because none of the RCPT TO
//commands worked. If the DATA command was properly rejected the client SMTP can just
//issue RSET, but if the DATA command was accepted the client SMTP should send a single
//dot.
let mut _all_error = self.server_conf.extensions.pipelining;
let mut _any_error = false;
let mut ignore_mailfrom = true;
for expected_reply_code in pipelining_queue {
let reply = self.read_lines(&mut res, expected_reply_code).await?;
if !ignore_mailfrom {
_all_error &= reply.code.is_err();
_any_error |= reply.code.is_err();
if self.server_conf.extensions.binarymime {
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?;
} else {
//The third step in the procedure is the DATA command
//(or some alternative specified in a service extension).
//DATA <CRLF>
self.send_command(&[b"DATA"]).await?;
//Client SMTP implementations that employ pipelining MUST check ALL statuses associated
//with each command in a group. For example, if none of the RCPT TO recipient addresses
//were accepted the client must then check the response to the DATA command -- the client
//cannot assume that the DATA command will be rejected just because none of the RCPT TO
//commands worked. If the DATA command was properly rejected the client SMTP can just
//issue RSET, but if the DATA command was accepted the client SMTP should send a single
//dot.
let mut _all_error = self.server_conf.extensions.pipelining;
let mut _any_error = false;
let mut ignore_mailfrom = true;
for expected_reply_code in pipelining_queue {
let reply = self.read_lines(&mut res, expected_reply_code).await?;
if !ignore_mailfrom {
_all_error &= reply.code.is_err();
_any_error |= reply.code.is_err();
}
ignore_mailfrom = false;
pipelining_results.push(reply.into());
}
ignore_mailfrom = false;
pipelining_results.push(reply.into());
}
//If accepted, the SMTP server returns a 354 Intermediate reply and considers all
//succeeding lines up to but not including the end of mail data indicator to be the
//message text. When the end of text is successfully received and stored, the
//SMTP-receiver sends a "250 OK" reply.
self.read_lines(&mut res, Some((ReplyCode::_354, &[])))
.await?;
//If accepted, the SMTP server returns a 354 Intermediate reply and considers all
//succeeding lines up to but not including the end of mail data indicator to be the
//message text. When the end of text is successfully received and stored, the
//SMTP-receiver sends a "250 OK" reply.
self.read_lines(&mut res, Some((ReplyCode::_354, &[])))
.await?;
//Before sending a line of mail text, the SMTP client checks the first character of the
//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
.chain_err_kind(crate::error::ErrorKind::Network)?;
//Before sending a line of mail text, the SMTP client checks the first character of the
//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(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
.chain_err_kind(crate::error::ErrorKind::Network)?;
}
if !mail.ends_with('\n') {
self.stream.write_all(b".\r\n").await?;
}
//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
.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?;
}
//The end of mail data indicator also confirms the mail transaction and tells the SMTP
//server to now process the stored recipients and mail data. If accepted, the SMTP
@ -881,11 +861,26 @@ impl ReplyCode {
fn is_err(&self) -> bool {
use ReplyCode::*;
match self {
_421 | _450 | _451 | _452 | _455 | _500 | _501 | _502 | _503 | _504 | _535 | _550
| _551 | _552 | _553 | _554 | _555 | _530 => true,
_ => false,
}
matches!(
self,
_421 | _450
| _451
| _452
| _455
| _500
| _501
| _502
| _503
| _504
| _535
| _550
| _551
| _552
| _553
| _554
| _555
| _530
)
}
}
@ -1004,8 +999,8 @@ async fn read_lines<'r>(
Ok(b) => {
ret.push_str(unsafe { std::str::from_utf8_unchecked(&buf[0..b]) });
}
Err(e) => {
return Err(MeliError::from(e).set_kind(crate::error::ErrorKind::Network));
Err(err) => {
return Err(MeliError::from(err));
}
}
}
@ -1029,3 +1024,219 @@ 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()))?;
Ok(data_dir
data_dir
.place_data_file(name)
.map_err(|e| MeliError::new(e.to_string()))?)
.map_err(|err| MeliError::new(err.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)?;
Ok(bincode::Options::deserialize(
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,7 +972,8 @@ mod alg {
);
let x = minima[r - 1 + offset];
let mut for_was_broken = false;
for j in i..(r - 1) {
let i_copy = i;
for j in i_copy..(r - 1) {
let y = cost(j + offset, r - 1 + offset, width, &minima, &offsets);
if y <= x {
n -= j;
@ -1179,8 +1180,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(
@ -1195,7 +1196,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 {
@ -1573,7 +1574,7 @@ impl Iterator for LineBreakText {
);
self.paragraph = paragraph;
}
return self.paragraph.pop_front();
self.paragraph.pop_front()
}
ReflowState::AllWidth {
width,
@ -1746,7 +1747,7 @@ impl Iterator for LineBreakText {
*cur_index += line.len() + 2;
return Some(ret);
}
return None;
None
}
}
}
@ -1764,8 +1765,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(
@ -1780,7 +1781,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,10 +77,7 @@ impl Truncate for &str {
extern crate unicode_segmentation;
use unicode_segmentation::UnicodeSegmentation;
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(*self, true)
.skip(skip_len)
.next()
{
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(*self, true).nth(skip_len) {
&self[first..]
} else {
self
@ -95,10 +92,7 @@ impl Truncate for &str {
extern crate unicode_segmentation;
use unicode_segmentation::UnicodeSegmentation;
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(*self, true)
.skip(skip_len)
.next()
{
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(*self, true).nth(skip_len) {
*self = &self[first..];
}
}
@ -144,9 +138,8 @@ impl Truncate for String {
extern crate unicode_segmentation;
use unicode_segmentation::UnicodeSegmentation;
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(self.as_str(), true)
.skip(skip_len)
.next()
if let Some((first, _)) =
UnicodeSegmentation::grapheme_indices(self.as_str(), true).nth(skip_len)
{
&self[first..]
} else {
@ -162,9 +155,8 @@ impl Truncate for String {
extern crate unicode_segmentation;
use unicode_segmentation::UnicodeSegmentation;
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(self.as_str(), true)
.skip(skip_len)
.next()
if let Some((first, _)) =
UnicodeSegmentation::grapheme_indices(self.as_str(), true).nth(skip_len)
{
*self = self[first..].to_string();
}

View File

@ -19,6 +19,7 @@
* 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,7 +179,9 @@ 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), &"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");
/// ```
pub trait SubjectPrefix {
const USUAL_PREFIXES: &'static [&'static str] = &[
@ -279,7 +281,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]) -> &mut Self;
fn strip_prefixes_from_list(&mut self, list: &[&str], times: Option<u8>) -> &mut Self;
}
impl SubjectPrefix for &[u8] {
@ -335,10 +337,10 @@ impl SubjectPrefix for &[u8] {
self
}
fn strip_prefixes_from_list(&mut self, list: &[&str]) -> &mut Self {
fn strip_prefixes_from_list(&mut self, list: &[&str], mut times: Option<u8>) -> &mut Self {
let result = {
let mut slice = self.trim();
loop {
'outer: loop {
let len = slice.len();
for prefix in list.iter() {
if slice
@ -347,10 +349,14 @@ 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 {
if slice.len() == len || times == Some(0) {
break;
}
}
@ -385,15 +391,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;
}
@ -408,10 +414,10 @@ impl SubjectPrefix for &str {
self
}
fn strip_prefixes_from_list(&mut self, list: &[&str]) -> &mut Self {
fn strip_prefixes_from_list(&mut self, list: &[&str], mut times: Option<u8>) -> &mut Self {
let result = {
let mut slice = self.trim();
loop {
'outer: loop {
let len = slice.len();
for prefix in list.iter() {
if slice
@ -420,10 +426,14 @@ 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 {
if slice.len() == len || times == Some(0) {
break;
}
}
@ -834,6 +844,7 @@ 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() {
@ -867,7 +878,6 @@ 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()
@ -878,7 +888,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;
}
}
@ -1222,18 +1232,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(_)) => {}
@ -1261,8 +1271,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(_)) => {}
@ -1298,18 +1308,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(_)) => {}
@ -1337,8 +1347,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

@ -0,0 +1,65 @@
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)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScrollContext {
shown_lines: usize,
total_lines: usize,

View File

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

View File

@ -33,7 +33,6 @@ use std::convert::TryInto;
use std::future::Future;
use std::pin::Pin;
use std::process::{Command, Stdio};
use std::str::FromStr;
use std::sync::{Arc, Mutex};
#[cfg(feature = "gpgme")]
@ -42,7 +41,7 @@ mod gpg;
mod edit_attachments;
use edit_attachments::*;
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Eq)]
enum Cursor {
Headers,
Body,
@ -228,19 +227,19 @@ impl Composer {
)
.as_ref()
.map(|v| v.iter().map(String::as_str).collect::<Vec<&str>>())
.unwrap_or(vec![]);
let subject = subject
.as_ref()
.strip_prefixes_from_list(if prefix_list.is_empty() {
.unwrap_or_default();
let subject_stripped = subject.as_ref().strip_prefixes_from_list(
if prefix_list.is_empty() {
<&str>::USUAL_PREFIXES
} else {
&prefix_list
})
.to_string();
},
Some(1),
) == &subject.as_ref();
let prefix =
account_settings!(context[ret.account_hash].composing.reply_prefix).as_str();
if !subject.starts_with(prefix) {
if subject_stripped {
format!("{prefix} {subject}", prefix = prefix, subject = subject)
} else {
subject.to_string()
@ -725,13 +724,9 @@ To: {}
fn update_from_file(&mut self, file: File, context: &mut Context) -> bool {
let result = file.read_to_string();
match Draft::from_str(result.as_str()) {
Ok(mut new_draft) => {
std::mem::swap(self.draft.attachments_mut(), new_draft.attachments_mut());
if self.draft != new_draft {
self.has_changes = true;
}
self.draft = new_draft;
match self.draft.update(result.as_str()) {
Ok(has_changes) => {
self.has_changes = has_changes;
true
}
Err(err) => {
@ -1119,7 +1114,7 @@ impl Component for Composer {
Some(Box::new(move |id: ComponentId, results: &[char]| {
Some(UIEvent::FinishedUIDialog(
id,
Box::new(results.get(0).cloned().unwrap_or('c')),
Box::new(results.first().cloned().unwrap_or('c')),
))
})),
context,
@ -1697,8 +1692,13 @@ impl Component for Composer {
};
/* update Draft's headers based on form values */
self.update_draft();
self.draft.set_wrap_header_preamble(
account_settings!(context[self.account_hash].composing.wrap_header_preamble)
.clone(),
);
let f = create_temp_file(
self.draft.to_string().unwrap().as_str().as_bytes(),
self.draft.to_edit_string().as_str().as_bytes(),
None,
None,
true,
@ -1766,13 +1766,9 @@ impl Component for Composer {
}
context.replies.push_back(UIEvent::Fork(ForkType::Finished));
let result = f.read_to_string();
match Draft::from_str(result.as_str()) {
Ok(mut new_draft) => {
std::mem::swap(self.draft.attachments_mut(), new_draft.attachments_mut());
if self.draft != new_draft {
self.has_changes = true;
}
self.draft = new_draft;
match self.draft.update(result.as_str()) {
Ok(has_changes) => {
self.has_changes = has_changes;
}
Err(err) => {
context.replies.push_back(UIEvent::Notification(
@ -2048,7 +2044,7 @@ impl Component for Composer {
Some(Box::new(move |id: ComponentId, results: &[char]| {
Some(UIEvent::FinishedUIDialog(
id,
Box::new(results.get(0).copied().unwrap_or('n')),
Box::new(results.first().copied().unwrap_or('n')),
))
})),
context,
@ -2097,7 +2093,7 @@ impl Component for Composer {
Some(Box::new(move |id: ComponentId, results: &[char]| {
Some(UIEvent::FinishedUIDialog(
id,
Box::new(results.get(0).copied().unwrap_or('n')),
Box::new(results.first().copied().unwrap_or('n')),
))
})),
context,
@ -2229,8 +2225,8 @@ pub fn save_draft(
..
}) => {
context.replies.push_back(UIEvent::Notification(
summary.map(|s| s.into()),
details.into(),
details.map(|s| s.into()),
summary.to_string(),
Some(NotificationType::Error(kind)),
));
}
@ -2394,3 +2390,61 @@ 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)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum EditAttachmentCursor {
AttachmentNo(usize),
Buttons,

View File

@ -61,7 +61,14 @@ pub use self::plain::*;
mod offline;
pub use self::offline::*;
#[derive(Debug, Copy, PartialEq, Clone)]
#[derive(Debug, Copy, Clone)]
pub enum Focus {
None,
Entry,
EntryFullscreen,
}
#[derive(Debug, Copy, PartialEq, Eq, Clone)]
pub enum Modifier {
SymmetricDifference,
Union,
@ -77,9 +84,9 @@ impl Default for Modifier {
#[derive(Debug, Default, Clone)]
pub struct DataColumns {
pub columns: [CellBuffer; 12],
pub columns: Box<[CellBuffer; 12]>,
pub widths: [usize; 12], // widths of columns calculated in first draw and after size changes
pub segment_tree: [SegmentTree; 12],
pub segment_tree: Box<[SegmentTree; 12]>,
}
#[derive(Debug, Default)]
@ -516,6 +523,8 @@ 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)]
@ -585,19 +594,19 @@ impl ListingComponent {
}
}
#[derive(PartialEq, Debug)]
#[derive(PartialEq, Eq, Debug)]
enum ListingFocus {
Menu,
Mailbox,
}
#[derive(PartialEq, Copy, Clone, Debug)]
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
enum MenuEntryCursor {
Status,
Mailbox(usize),
}
#[derive(PartialEq, Copy, Clone, Debug)]
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
enum ShowMenuScrollbar {
Never,
True,
@ -655,7 +664,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.menu_visibility {
let right_component_width = if self.is_menu_visible() {
if self.focus == ListingFocus::Menu {
(self.ratio * total_cols) / 100
} else {
@ -694,13 +703,10 @@ 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() {
match self.component {
ListingComponent::Offline(_) => {}
_ => {
self.component = Offline(OfflineListing::new((account_hash, 0)));
}
}
if context.is_online(account_hash).is_err()
&& !matches!(self.component, ListingComponent::Offline(_))
{
self.component = Offline(OfflineListing::new((account_hash, 0)));
}
if let Some(s) = self.status.as_mut() {
@ -716,13 +722,10 @@ impl Component for Listing {
(upper_left, (mid.saturating_sub(1), get_y(bottom_right))),
context,
);
if context.is_online(account_hash).is_err() {
match self.component {
ListingComponent::Offline(_) => {}
_ => {
self.component = Offline(OfflineListing::new((account_hash, 0)));
}
}
if context.is_online(account_hash).is_err()
&& !matches!(self.component, ListingComponent::Offline(_))
{
self.component = Offline(OfflineListing::new((account_hash, 0)));
}
if let Some(s) = self.status.as_mut() {
s.draw(grid, (set_x(upper_left, mid + 1), bottom_right), context);
@ -767,7 +770,7 @@ impl Component for Listing {
);
}
}
UIEvent::AccountStatusChange(account_hash) => {
UIEvent::AccountStatusChange(account_hash, msg) => {
let account_index: usize = context
.accounts
.get_index_of(account_hash)
@ -812,11 +815,11 @@ impl Component for Listing {
self.menu_content.empty();
context
.replies
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(
self.get_status(context),
)));
.push_back(UIEvent::StatusEvent(StatusEvent::UpdateStatus(match msg {
Some(msg) => format!("{} {}", self.get_status(context), msg),
None => self.get_status(context),
})));
}
return true;
}
UIEvent::MailboxDelete((account_hash, mailbox_hash))
| UIEvent::MailboxCreate((account_hash, mailbox_hash)) => {
@ -925,7 +928,7 @@ impl Component for Listing {
if self.focus == ListingFocus::Mailbox {
match *event {
UIEvent::Input(Key::Mouse(MouseEvent::Press(MouseButton::Left, x, _y)))
if self.menu_visibility =>
if self.is_menu_visible() =>
{
match self.menu_width {
WidgetWidth::Hold(wx) | WidgetWidth::Set(wx)
@ -942,7 +945,7 @@ impl Component for Listing {
self.set_dirty(true);
return true;
}
UIEvent::Input(Key::Mouse(MouseEvent::Hold(x, _y))) if self.menu_visibility => {
UIEvent::Input(Key::Mouse(MouseEvent::Hold(x, _y))) if self.is_menu_visible() => {
match self.menu_width {
WidgetWidth::Hold(ref mut hx) => {
*hx = usize::from(x).saturating_sub(1);
@ -952,7 +955,9 @@ impl Component for Listing {
self.set_dirty(true);
return true;
}
UIEvent::Input(Key::Mouse(MouseEvent::Release(x, _y))) if self.menu_visibility => {
UIEvent::Input(Key::Mouse(MouseEvent::Release(x, _y)))
if self.is_menu_visible() =>
{
match self.menu_width {
WidgetWidth::Hold(_) => {
self.menu_width = WidgetWidth::Set(usize::from(x).saturating_sub(1));
@ -962,7 +967,10 @@ impl Component for Listing {
self.set_dirty(true);
return true;
}
UIEvent::Input(Key::Left) if self.menu_visibility => {
UIEvent::Input(ref k)
if self.is_menu_visible()
&& shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_left"]) =>
{
self.focus = ListingFocus::Menu;
if self.show_menu_scrollbar != ShowMenuScrollbar::Never {
self.menu_scrollbar_show_timer.rearm();
@ -1333,7 +1341,9 @@ impl Component for Listing {
}
} else if self.focus == ListingFocus::Menu {
match *event {
UIEvent::Input(Key::Right) => {
UIEvent::Input(ref k)
if shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"]) =>
{
self.focus = ListingFocus::Mailbox;
context
.replies
@ -2332,8 +2342,7 @@ impl Listing {
let index_style =
mailbox_settings!(context[account_hash][mailbox_hash].listing.index_style);
self.component.set_style(*index_style);
} else {
/* Set to dummy */
} else if !matches!(self.component, ListingComponent::Offline(_)) {
self.component = Offline(OfflineListing::new((account_hash, 0)));
}
self.status = None;
@ -2369,4 +2378,8 @@ 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.
unfocused: bool,
view: ThreadView,
focus: Focus,
view: Box<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 = ThreadView::new(self.new_cursor_pos, thread, None, context);
self.view = Box::new(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.unfocused = false;
self.view = ThreadView::default();
self.focus = Focus::None;
self.view = Box::new(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 {
self.unfocused
!matches!(self.focus, Focus::None)
}
fn set_modifier_active(&mut self, new_val: bool) {
@ -837,6 +837,33 @@ 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 {
@ -863,14 +890,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,
unfocused: false,
view: ThreadView::default(),
view: Box::new(ThreadView::default()),
color_cache: ColorCache::default(),
movement: None,
modifier_active: false,
@ -1465,10 +1492,15 @@ impl CompactListing {
impl Component for CompactListing {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if !self.unfocused {
if !self.is_dirty() {
return;
}
if !self.is_dirty() {
return;
}
if matches!(self.focus, Focus::EntryFullscreen) {
return self.view.draw(grid, area, context);
}
if !self.unfocused() {
let mut area = area;
if !self.filter_term.is_empty() {
let (upper_left, bottom_right) = area;
@ -1715,44 +1747,81 @@ impl Component for CompactListing {
}
self.dirty = false;
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
if self.unfocused && self.view.process_event(event, context) {
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) {
return true;
}
let shortcuts = self.get_shortcuts(context);
if self.length > 0 {
match *event {
UIEvent::Input(ref k)
if !self.unfocused
&& shortcut!(
k == shortcuts[CompactListing::DESCRIPTION]["open_thread"]
) =>
if matches!(self.focus, Focus::None)
&& (shortcut!(k == shortcuts[Listing::DESCRIPTION]["open_entry"])
|| shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"])) =>
{
let thread = self.get_thread_under_cursor(self.cursor_pos.2);
self.view = ThreadView::new(self.cursor_pos, thread, None, context);
self.unfocused = true;
self.dirty = true;
self.view = Box::new(ThreadView::new(self.cursor_pos, thread, None, context));
self.set_focus(Focus::Entry, context);
return true;
}
UIEvent::Input(ref k)
if self.unfocused
&& shortcut!(
k == shortcuts[CompactListing::DESCRIPTION]["exit_thread"]
) =>
if matches!(self.focus, Focus::Entry)
&& shortcut!(k == shortcuts[Listing::DESCRIPTION]["exit_entry"]) =>
{
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;
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!();
}
}
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() {
@ -1766,7 +1835,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;
@ -1778,13 +1847,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
@ -1875,7 +1944,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);
}
@ -1905,7 +1974,7 @@ impl Component for CompactListing {
self.dirty = true;
if self.unfocused {
if self.unfocused() {
self.view
.process_event(&mut UIEvent::EnvelopeUpdate(*env_hash), context);
}
@ -1917,7 +1986,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() {
@ -1926,13 +1995,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,
@ -1954,7 +2023,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,
@ -2021,30 +2090,29 @@ impl Component for CompactListing {
}
false
}
fn is_dirty(&self) -> bool {
self.dirty
|| if self.unfocused {
self.view.is_dirty()
} else {
false
}
match self.focus {
Focus::None => self.dirty,
Focus::Entry => self.dirty || self.view.is_dirty(),
Focus::EntryFullscreen => self.view.is_dirty(),
}
}
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,6 +22,7 @@
use super::*;
use crate::components::PageMovement;
use crate::jobs::JoinHandle;
use indexmap::IndexSet;
use std::iter::FromIterator;
macro_rules! row_attr {
@ -114,7 +115,7 @@ pub struct ConversationsListing {
dirty: bool,
force_draw: bool,
/// If `self.view` exists or not.
unfocused: bool,
focus: Focus,
view: ThreadView,
row_updates: SmallVec<[ThreadHash; 8]>,
color_cache: ColorCache,
@ -215,7 +216,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);
@ -240,6 +241,7 @@ 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();
@ -273,17 +275,30 @@ impl MailListingTrait for ConversationsListing {
panic!();
}
other_subjects.clear();
from_address_list.clear();
from_address_set.clear();
for envelope in threads
for (envelope, show_subject) in threads
.thread_group_iter(thread)
.filter_map(|(_, h)| threads.thread_nodes()[&h].message())
.map(|env_hash| {
context.accounts[&self.cursor_pos.0]
.collection
.get_env(env_hash)
.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,
)
})
{
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;
@ -313,6 +328,7 @@ impl MailListingTrait for ConversationsListing {
context,
&from_address_list,
&threads,
&other_subjects,
thread,
);
max_entry_columns = std::cmp::max(
@ -352,7 +368,7 @@ impl ListingTrait for ConversationsListing {
fn set_coordinates(&mut self, coordinates: (AccountHash, MailboxHash)) {
self.new_cursor_pos = (coordinates.0, coordinates.1, 0);
self.unfocused = false;
self.focus = Focus::None;
self.view = ThreadView::default();
self.filtered_selection.clear();
self.filtered_order.clear();
@ -537,7 +553,7 @@ impl ListingTrait for ConversationsListing {
}
fn unfocused(&self) -> bool {
self.unfocused
!matches!(self.focus, Focus::None)
}
fn set_modifier_active(&mut self, new_val: bool) {
@ -556,6 +572,33 @@ 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 {
@ -565,11 +608,11 @@ 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(ConversationsListing {
Box::new(Self {
cursor_pos: (coordinates.0, 1, 0),
new_cursor_pos: (coordinates.0, coordinates.1, 0),
length: 0,
@ -586,7 +629,7 @@ impl ConversationsListing {
rows: Ok(Vec::with_capacity(1024)),
dirty: true,
force_draw: true,
unfocused: false,
focus: Focus::None,
view: ThreadView::default(),
color_cache: ColorCache::default(),
movement: None,
@ -600,8 +643,9 @@ impl ConversationsListing {
&self,
e: &Envelope,
context: &Context,
from: &Vec<Address>,
from: &[Address],
threads: &Threads,
other_subjects: &IndexSet<String>,
hash: ThreadHash,
) -> EntryStrings {
let thread = threads.thread_ref(hash);
@ -642,8 +686,24 @@ impl ConversationsListing {
tags.pop();
}
}
let mut subject = e.subject().to_string();
subject.truncate_at_boundary(150);
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);
if thread.len() > 1 {
EntryStrings {
date: DateString(ConversationsListing::format_date(context, thread.date())),
@ -728,18 +788,29 @@ 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 in threads
for (envelope, show_subject) in threads
.thread_group_iter(thread_hash)
.filter_map(|(_, h)| threads.thread_nodes()[&h].message())
.map(|env_hash| {
context.accounts[&self.cursor_pos.0]
.collection
.get_env(env_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,
)
})
{
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;
@ -754,6 +825,7 @@ impl ConversationsListing {
context,
&from_address_list,
&threads,
&other_subjects,
thread_hash,
);
drop(envelope);
@ -907,6 +979,11 @@ 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;
@ -1142,7 +1219,7 @@ impl Component for ConversationsListing {
self.draw_list(grid, area, context);
}
}
if self.unfocused {
if matches!(self.focus, Focus::Entry) {
if self.length == 0 && self.dirty {
clear_area(grid, area, self.color_cache.theme_default);
context.dirty_areas.push_back(area);
@ -1157,44 +1234,81 @@ impl Component for ConversationsListing {
}
self.dirty = false;
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
if self.unfocused && self.view.process_event(event, context) {
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) {
return true;
}
let shortcuts = self.get_shortcuts(context);
if self.length > 0 {
match *event {
UIEvent::Input(ref k)
if !self.unfocused
&& shortcut!(
k == shortcuts[ConversationsListing::DESCRIPTION]["open_thread"]
) =>
if matches!(self.focus, Focus::None)
&& (shortcut!(k == shortcuts[Listing::DESCRIPTION]["open_entry"])
|| shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"])) =>
{
let thread = self.get_thread_under_cursor(self.cursor_pos.2);
self.view = ThreadView::new(self.cursor_pos, thread, None, context);
self.unfocused = true;
self.dirty = true;
self.set_focus(Focus::Entry, context);
return true;
}
UIEvent::Input(ref k)
if self.unfocused
&& shortcut!(
k == shortcuts[ConversationsListing::DESCRIPTION]["exit_thread"]
) =>
if !matches!(self.focus, Focus::None)
&& shortcut!(k == shortcuts[Listing::DESCRIPTION]["exit_entry"]) =>
{
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;
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!();
}
}
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() {
@ -1225,7 +1339,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,
@ -1257,13 +1371,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
@ -1275,7 +1389,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
/*
@ -1295,7 +1409,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
@ -1363,7 +1477,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,
@ -1389,7 +1503,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() {
@ -1402,7 +1516,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);
@ -1436,30 +1550,29 @@ impl Component for ConversationsListing {
false
}
fn is_dirty(&self) -> bool {
self.dirty
|| if self.unfocused {
self.view.is_dirty()
} else {
false
}
match self.focus {
Focus::None => self.dirty,
Focus::Entry => self.dirty || self.view.is_dirty(),
Focus::EntryFullscreen => self.view.is_dirty(),
}
}
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,12 +21,14 @@
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,
}
@ -84,6 +86,12 @@ 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 {
@ -98,6 +106,7 @@ impl OfflineListing {
cursor_pos,
_row_updates: SmallVec::new(),
_selection: HashMap::default(),
messages: vec![],
dirty: true,
id: ComponentId::new_v4(),
})
@ -111,26 +120,50 @@ 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,
conf::value(context, "error_message").fg,
conf::value(context, "error_message").bg,
conf::value(context, "error_message").attrs,
error_message.fg,
error_message.bg,
error_message.attrs,
area,
None,
);
write_string_to_grid(
&err.to_string(),
grid,
Color::Red,
theme_default.bg,
theme_default.attrs,
error_message.fg,
error_message.bg,
error_message.attrs,
(set_x(upper_left!(area), x + 1), bottom_right!(area)),
None,
Some(get_x(upper_left!(area))),
);
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...",
@ -150,9 +183,9 @@ impl Component for OfflineListing {
write_string_to_grid(
&format!("{}: {:?}", job_id, j),
grid,
theme_default.fg,
theme_default.bg,
theme_default.attrs,
text_unfocused.fg,
text_unfocused.bg,
text_unfocused.attrs,
(set_y(upper_left!(area), y + 1), bottom_right!(area)),
None,
);
@ -161,19 +194,26 @@ impl Component for OfflineListing {
context
.replies
.push_back(UIEvent::AccountStatusChange(self.cursor_pos.0));
.push_back(UIEvent::AccountStatusChange(self.cursor_pos.0, None));
}
context.dirty_areas.push_back(area);
}
fn process_event(&mut self, event: &mut UIEvent, _context: &mut Context) -> bool {
match event {
UIEvent::AccountStatusChange(account_hash) if *account_hash == self.cursor_pos.0 => {
UIEvent::AccountStatusChange(account_hash, msg)
if *account_hash == self.cursor_pos.0 =>
{
if let Some(msg) = msg.clone() {
self.messages.push(msg);
}
self.dirty = true
}
_ => {}
}
false
}
fn is_dirty(&self) -> bool {
self.dirty
}
@ -185,6 +225,7 @@ 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.
unfocused: bool,
focus: Focus,
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.unfocused = false;
self.focus = Focus::None;
self.view = MailView::default();
self.filtered_selection.clear();
self.filtered_order.clear();
@ -641,13 +641,44 @@ impl ListingTrait for PlainListing {
}
fn unfocused(&self) -> bool {
self.unfocused
!matches!(self.focus, Focus::None)
}
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 {
@ -657,7 +688,7 @@ 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),
@ -680,7 +711,7 @@ impl PlainListing {
data_columns: DataColumns::default(),
dirty: true,
force_draw: true,
unfocused: false,
focus: Focus::None,
view: MailView::default(),
color_cache: ColorCache::default(),
active_jobs: HashMap::default(),
@ -1060,10 +1091,15 @@ impl PlainListing {
impl Component for PlainListing {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
if !self.unfocused {
if !self.is_dirty() {
return;
}
if !self.is_dirty() {
return;
}
if matches!(self.focus, Focus::EntryFullscreen) {
return self.view.draw(grid, area, context);
}
if matches!(self.focus, Focus::None) {
let mut area = area;
if !self.filter_term.is_empty() {
let (upper_left, bottom_right) = area;
@ -1129,48 +1165,79 @@ impl Component for PlainListing {
}
self.dirty = false;
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
if self.unfocused && self.view.process_event(event, context) {
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) {
return true;
}
let shortcuts = self.get_shortcuts(context);
if self.length > 0 {
match *event {
UIEvent::Input(ref k)
if !self.unfocused
&& shortcut!(k == shortcuts[PlainListing::DESCRIPTION]["open_thread"]) =>
if matches!(self.focus, Focus::None)
&& (shortcut!(k == shortcuts[Listing::DESCRIPTION]["open_entry"])
|| shortcut!(k == shortcuts[Listing::DESCRIPTION]["focus_right"])) =>
{
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;
self.set_focus(Focus::Entry, context);
return true;
}
UIEvent::Input(ref k)
if self.unfocused
&& shortcut!(k == shortcuts[PlainListing::DESCRIPTION]["exit_thread"]) =>
if !matches!(self.focus, Focus::None)
&& shortcut!(k == shortcuts[Listing::DESCRIPTION]["exit_entry"]) =>
{
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;
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!();
}
}
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() {
@ -1181,7 +1248,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;
@ -1189,7 +1256,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);
@ -1295,7 +1362,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);
}
@ -1314,7 +1381,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);
}
@ -1326,7 +1393,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() {
@ -1335,13 +1402,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,
@ -1389,30 +1456,28 @@ impl Component for PlainListing {
}
false
}
fn is_dirty(&self) -> bool {
self.dirty
|| if self.unfocused {
self.view.is_dirty()
} else {
false
}
match self.focus {
Focus::None => self.dirty,
Focus::Entry | Focus::EntryFullscreen => self.view.is_dirty(),
}
}
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,6 +23,7 @@ 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 $(,)*) => {{
@ -128,9 +129,9 @@ pub struct ThreadListing {
/// If we must redraw on next redraw event
dirty: bool,
/// If `self.view` is focused or not.
unfocused: bool,
focus: Focus,
initialised: bool,
view: Option<MailView>,
view: Option<Box<MailView>>,
movement: Option<PageMovement>,
id: ComponentId,
}
@ -410,9 +411,10 @@ 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.unfocused = false;
self.focus = Focus::None;
self.view = None;
self.order.clear();
self.row_updates.clear();
@ -741,13 +743,55 @@ impl ListingTrait for ThreadListing {
}
fn unfocused(&self) -> bool {
self.unfocused
!matches!(self.focus, Focus::None)
}
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 {
@ -772,7 +816,7 @@ impl ThreadListing {
selection: HashMap::default(),
order: HashMap::default(),
dirty: true,
unfocused: false,
focus: Focus::None,
view: None,
initialised: false,
movement: None,
@ -840,7 +884,7 @@ impl ThreadListing {
});
*/
if show_subject {
s.push_str(&format!("{:.85}", envelope.subject()));
let _ = write!(s, "{:.85}", envelope.subject());
}
s
}
@ -1092,10 +1136,17 @@ impl Component for ThreadListing {
}
}
*/
if !self.unfocused {
if !self.is_dirty() {
return;
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() {
self.dirty = false;
/* Draw the entire list */
self.draw_list(grid, area, context);
@ -1188,7 +1239,7 @@ impl Component for ThreadListing {
if let Some(ref mut v) = self.view {
v.update(coordinates, context);
} else {
self.view = Some(MailView::new(coordinates, None, None, context));
self.view = Some(Box::new(MailView::new(coordinates, None, None, context)));
}
if let Some(v) = self.view.as_mut() {
@ -1198,12 +1249,38 @@ 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 v.process_event(event, context) {
if !matches!(self.focus, Focus::None) && v.process_event(event, context) {
return true;
}
}
match *event {
UIEvent::ConfigReload { old_settings: _ } => {
self.color_cache = ColorCache {
@ -1238,18 +1315,43 @@ impl Component for ThreadListing {
}
self.set_dirty(true);
}
UIEvent::Input(Key::Char('\n')) if !self.unfocused => {
self.unfocused = true;
self.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);
return true;
}
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);
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!();
}
}
self.dirty = true;
self.view = None;
return true;
}
UIEvent::MailboxUpdate((ref idxa, ref idxf))
@ -1275,7 +1377,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),
@ -1301,7 +1403,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);
}
@ -1328,7 +1430,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,
@ -1379,20 +1481,36 @@ impl Component for ThreadListing {
}
false
}
fn is_dirty(&self) -> bool {
self.dirty || self.view.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
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),
}
}
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 {
self.view
.as_ref()
.map(|p| p.get_shortcuts(context))
.unwrap_or_default()
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
}
fn id(&self) -> ComponentId {

View File

@ -27,6 +27,7 @@ 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;
@ -44,7 +45,7 @@ pub use self::envelope::*;
use linkify::LinkFinder;
use xdg_utils::query_default_app;
#[derive(PartialEq, Copy, Clone, Debug)]
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
enum Source {
Decoded,
Raw,
@ -530,7 +531,7 @@ impl MailView {
error,
} => {
if show_comments {
acc.push_str(&format!("Failed to verify signature: {}.\n\n", error));
let _ = writeln!(acc, "Failed to verify signature: {}.\n", error);
}
acc.push_str(&self.attachment_displays_to_text(
display,
@ -559,7 +560,7 @@ impl MailView {
}
EncryptedPending { .. } => acc.push_str("Waiting for decryption result."),
EncryptedFailed { inner: _, error } => {
acc.push_str(&format!("Decryption failed: {}.", &error))
let _ = write!(acc, "Decryption failed: {}.", &error);
}
EncryptedSuccess {
inner: _,
@ -733,7 +734,7 @@ impl MailView {
inner: Box::new(a.clone()),
});
} else if a.content_type().is_text_html() {
let bytes = decode(a, None);
let bytes = a.decode(Default::default());
let filter_invocation =
mailbox_settings!(context[coordinates.0][&coordinates.1].pager.html_filter)
.as_ref()
@ -788,7 +789,7 @@ impl MailView {
}
}
} else if a.is_text() {
let bytes = decode(a, None);
let bytes = a.decode(Default::default());
acc.push(AttachmentDisplay::InlineText {
inner: Box::new(a.clone()),
comment: None,
@ -810,7 +811,7 @@ impl MailView {
if let Some(text_attachment_pos) =
parts.iter().position(|a| a.content_type == "text/plain")
{
let bytes = decode(&parts[text_attachment_pos], None);
let bytes = &parts[text_attachment_pos].decode(Default::default());
if bytes.trim().is_empty()
&& mailbox_settings!(
context[coordinates.0][&coordinates.1]
@ -1430,7 +1431,9 @@ 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() {
self.subview = Some(Box::new(HtmlView::new(attachment, context)));
let mut subview = Box::new(HtmlView::new(attachment, context));
subview.set_coordinates(Some(self.coordinates));
self.subview = Some(subview);
self.mode = ViewMode::Subview;
} else {
text.push_str(&attachment.text());
@ -1461,7 +1464,9 @@ impl Component for MailView {
}
}
ViewMode::Normal if body.is_html() => {
self.subview = Some(Box::new(HtmlView::new(body, context)));
let mut subview = Box::new(HtmlView::new(body, context));
subview.set_coordinates(Some(self.coordinates));
self.subview = Some(subview);
self.mode = ViewMode::Subview;
}
ViewMode::Normal
@ -1482,7 +1487,7 @@ impl Component for MailView {
_ => false,
} =>
{
self.subview = Some(Box::new(HtmlView::new(
let mut subview = Box::new(HtmlView::new(
body.content_type
.parts()
.unwrap()
@ -1490,7 +1495,9 @@ 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;
}
@ -2211,18 +2218,18 @@ impl Component for MailView {
let filename = attachment.filename();
if let Ok(command) = query_default_app(&attachment_type) {
let p = create_temp_file(
&decode(attachment, None),
&attachment.decode(Default::default()),
filename.as_deref(),
None,
true,
);
let (exec_cmd, argument) = desktop_exec_to_command(
let exec_cmd = desktop_exec_to_command(
&command,
p.path.display().to_string(),
false,
);
match Command::new(&exec_cmd)
.arg(&argument)
match Command::new("sh")
.args(&["-c", &exec_cmd])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
@ -2234,8 +2241,8 @@ impl Component for MailView {
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Failed to start `{} {}`: {}",
&exec_cmd, &argument, err
"Failed to start `{}`: {}",
&exec_cmd, err
)),
));
}
@ -2466,7 +2473,7 @@ impl Component for MailView {
path.push(u.as_hyphenated().to_string());
}
}
match save_attachment(&path, &decode(u, None)) {
match save_attachment(&path, &u.decode(Default::default())) {
Err(err) => {
context.replies.push_back(UIEvent::Notification(
Some(format!("Failed to create file at {}", path.display())),
@ -2783,49 +2790,66 @@ fn save_attachment(path: &std::path::Path, bytes: &[u8]) -> Result<()> {
Ok(())
}
fn desktop_exec_to_command(command: &str, path: String, is_url: bool) -> (String, String) {
fn desktop_exec_to_command(command: &str, path: String, is_url: bool) -> String {
/* Purge unused field codes */
let command = command
.replace("%i", "")
.replace("%c", "")
.replace("%k", "");
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 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 is_url {
(command[0..pos].trim().to_string(), path)
command.replacen(from_pattern, &path, 1)
} else {
(
command[0..pos].trim().to_string(),
format!("file://{}", path),
command.replacen(
from_pattern,
&format!("file://{}", path).replace(' ', "\\ "),
1,
)
}
} else if is_url {
format!("{} {}", command, path)
} else {
(command, path)
format!("{} {}", command, path.replace(' ', "\\ "))
}
}
/*
#[test]
fn test_desktop_exec() {
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)
);
}
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)
);
}
*/

View File

@ -25,7 +25,7 @@ use std::process::{Command, Stdio};
use xdg_utils::query_default_app;
#[derive(PartialEq, Debug)]
#[derive(PartialEq, Eq, Debug)]
enum ViewMode {
Normal,
Url,
@ -83,9 +83,8 @@ 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(&decode_rec(
body,
Some(Box::new(|a: &Attachment, v: &mut Vec<u8>| {
let body_text = String::from_utf8_lossy(&body.decode_rec(DecodeOptions {
filter: 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() {
@ -123,7 +122,8 @@ 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)| {
s.push_str(&format!("[{}] {}\n\n", idx, a));
let _ = writeln!(s, "[{}] {}\n", idx, a);
s
});
}
@ -161,7 +161,7 @@ impl EnvelopeView {
.iter()
.enumerate()
.fold(t, |mut s, (idx, a)| {
s.push_str(&format!("[{}] {}\n\n", idx, a));
let _ = writeln!(s, "[{}] {}\n", idx, a);
s
});
}
@ -370,7 +370,8 @@ 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(&decode_rec(u, None)).to_string(),
String::from_utf8_lossy(&u.decode_rec(Default::default()))
.to_string(),
Some(context),
None,
None,
@ -397,18 +398,18 @@ impl Component for EnvelopeView {
let filename = u.filename();
if let Ok(command) = query_default_app(&attachment_type) {
let p = create_temp_file(
&decode(u, None),
&u.decode(Default::default()),
filename.as_deref(),
None,
true,
);
let (exec_cmd, argument) = super::desktop_exec_to_command(
let exec_cmd = super::desktop_exec_to_command(
&command,
p.path.display().to_string(),
false,
);
match Command::new(&exec_cmd)
.arg(&argument)
match Command::new("sh")
.args(&["-c", &exec_cmd])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
@ -420,8 +421,8 @@ impl Component for EnvelopeView {
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Failed to start `{} {}`: {}",
&exec_cmd, &argument, err
"Failed to start `{}`: {}",
&exec_cmd, err
)),
));
}

View File

@ -27,13 +27,14 @@ 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> = decode_rec(body, None);
let bytes: Vec<u8> = body.decode_rec(Default::default());
let settings = &context.settings;
let mut display_text = if let Some(filter_invocation) = settings.pager.html_filter.as_ref()
@ -105,13 +106,22 @@ impl HtmlView {
.iter()
.enumerate()
.fold(display_text, |mut s, (idx, a)| {
s.push_str(&format!("[{}] {}\n\n\n", idx, a));
let _ = writeln!(s, "[{}] {}\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 }
HtmlView {
pager,
bytes,
id,
coordinates: None,
}
}
pub fn set_coordinates(&mut self, new_value: Option<(AccountHash, MailboxHash, EnvelopeHash)>) {
self.coordinates = new_value;
}
}
@ -131,12 +141,20 @@ impl Component for HtmlView {
}
if let UIEvent::Input(Key::Char('v')) = event {
if let Ok(command) = query_default_app("text/html") {
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 {
let p = create_temp_file(&self.bytes, None, None, true);
let (exec_cmd, argument) =
let exec_cmd =
super::desktop_exec_to_command(&command, p.path.display().to_string(), false);
match Command::new(&exec_cmd)
.arg(&argument)
match Command::new("sh")
.args(&["-c", &exec_cmd])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
@ -148,8 +166,8 @@ impl Component for HtmlView {
Err(err) => {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(format!(
"Failed to start `{} {}`: {}",
&exec_cmd, &argument, err
"Failed to start `{}`: {}",
&exec_cmd, 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,13 +151,14 @@ mod dbus {
'"' => ret.push_str("&quot;"),
_ => {
let i = c as u32;
if (0x1 <= i && i <= 0x8)
|| (0xb <= i && i <= 0xc)
|| (0xe <= i && i <= 0x1f)
|| (0x7f <= i && i <= 0x84)
|| (0x86 <= i && i <= 0x9f)
if (0x1..=0x8).contains(&i)
|| (0xb..=0xc).contains(&i)
|| (0xe..=0x1f).contains(&i)
|| (0x7f..=0x84).contains(&i)
|| (0x86..=0x9f).contains(&i)
{
ret.push_str(&format!("&#{:x}%{:x};", i, i));
use std::fmt::Write;
let _ = write!(ret, "&#{:x}%{:x};", i, i);
} else {
ret.push(c);
}
@ -199,7 +200,14 @@ impl Component for NotificationCommand {
}
}
if let Some(ref bin) = context.settings.notifications.script {
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 {
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)]
#[derive(Debug, Clone, PartialEq, Eq)]
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, Clone)]
#[derive(Debug, Copy, PartialEq, Eq, 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)]
#[derive(Debug, PartialEq, Eq)]
enum FormFocus {
Fields,
Buttons,
@ -390,7 +390,7 @@ impl fmt::Display for Field {
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum FormButtonActions {
Accept,
Reset,
@ -870,7 +870,7 @@ where
}
}
#[derive(Debug, PartialEq, Clone)]
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct AutoCompleteEntry {
pub entry: String,
pub description: String,
@ -911,7 +911,7 @@ impl From<(String, String)> for AutoCompleteEntry {
}
}
#[derive(Debug, PartialEq, Clone)]
#[derive(Debug, PartialEq, Eq, 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 {
root_mailbox: String,
format: String,
identity: String,
pub root_mailbox: String,
pub format: String,
pub identity: String,
#[serde(default)]
extra_identities: Vec<String>,
pub extra_identities: Vec<String>,
#[serde(default = "none")]
display_name: Option<String>,
pub display_name: Option<String>,
#[serde(default = "false_val")]
read_only: bool,
pub read_only: bool,
#[serde(default)]
subscribed_mailboxes: Vec<String>,
pub subscribed_mailboxes: Vec<String>,
#[serde(default)]
mailboxes: IndexMap<String, FileMailboxConf>,
pub mailboxes: IndexMap<String, FileMailboxConf>,
#[serde(default)]
search_backend: SearchBackend,
pub search_backend: SearchBackend,
#[serde(default)]
order: (SortField, SortOrder),
pub order: (SortField, SortOrder),
#[serde(default = "false_val")]
pub manual_refresh: bool,
#[serde(default = "none")]
@ -345,18 +345,22 @@ 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."));
}
@ -573,7 +577,7 @@ impl Settings {
}
}
#[derive(Copy, Debug, Clone, Hash, PartialEq)]
#[derive(Copy, Debug, Clone, Hash, PartialEq, Eq)]
pub enum IndexStyle {
Plain,
Threaded,
@ -703,7 +707,7 @@ impl Serialize for IndexStyle {
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum SearchBackend {
None,
Auto,
@ -725,7 +729,12 @@ 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") => Ok(SearchBackend::Sqlite3),
sqlite3
if sqlite3.eq_ignore_ascii_case("sqlite3")
|| sqlite3.eq_ignore_ascii_case("sqlite") =>
{
Ok(SearchBackend::Sqlite3)
}
none if none.eq_ignore_ascii_case("none")
|| none.eq_ignore_ascii_case("nothing")
|| none.is_empty() =>
@ -757,15 +766,26 @@ pub fn create_config_file(p: &Path) -> Result<()> {
.write(true)
.create_new(true)
.open(p)
.expect("Could not create config file.");
.chain_err_summary(|| format!("Cannot create configuration file in {}", p.display()))?;
file.write_all(include_bytes!("../docs/samples/sample-config.toml"))
.expect("Could not write to config file.");
.and_then(|()| file.flush())
.chain_err_summary(|| format!("Could not write to configuration file {}", p.display()))?;
println!("Written example configuration to {}", p.display());
let metadata = file.metadata()?;
let mut permissions = metadata.permissions();
let set_permissions = |file: std::fs::File| -> Result<()> {
let metadata = file.metadata()?;
let mut permissions = metadata.permissions();
permissions.set_mode(0o600); // Read/write for owner only.
file.set_permissions(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
);
}
Ok(())
}
@ -904,9 +924,9 @@ mod pp {
#[serde(deny_unknown_fields)]
pub struct LogSettings {
#[serde(default)]
log_file: Option<PathBuf>,
pub log_file: Option<PathBuf>,
#[serde(default)]
maximum_level: melib::LoggingLevel,
pub maximum_level: melib::LoggingLevel,
}
pub use dotaddressable::*;
@ -1239,20 +1259,20 @@ send_mail = '/bin/false'
let mut new_file = ConfigFile::new(TEST_CONFIG).unwrap();
let err = FileSettings::validate(new_file.path.clone(), false, true).unwrap_err();
assert!(err.details.as_ref().starts_with("You must set a global `composing` option. If you override `composing` in each account, you can use a dummy global like follows"));
assert!(err.summary.as_ref().starts_with("You must set a global `composing` option. If you override `composing` in each account, you can use a dummy global like follows"));
new_file
.file
.write_all("[composing]\nsend_mail = '/bin/false'\n".as_bytes())
.unwrap();
let err = FileSettings::validate(new_file.path.clone(), false, true).unwrap_err();
assert_eq!(err.details.as_ref(), "Configuration error (account-name): root_path `/path/to/root/mailbox` is not a valid directory.");
assert_eq!(err.summary.as_ref(), "Configuration error (account-name): root_mailbox `/path/to/root/mailbox` is not a valid directory.");
/* Test unrecognised configuration entries error */
let new_file = ConfigFile::new(EXTRA_CONFIG).unwrap();
let err = FileSettings::validate(new_file.path.clone(), false, true).unwrap_err();
assert_eq!(
err.details.as_ref(),
err.summary.as_ref(),
"Unrecognised configuration values: {\"index_style\": \"Compact\"}"
);

View File

@ -489,6 +489,31 @@ 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,
@ -619,7 +644,8 @@ impl Account {
acc.push_str(", ");
acc
});
mailbox_comma_sep_list_string.drain(mailbox_comma_sep_list_string.len() - 2..);
mailbox_comma_sep_list_string
.drain(mailbox_comma_sep_list_string.len().saturating_sub(2)..);
melib::log(
format!(
"Account `{}` has the following mailboxes: [{}]",
@ -627,6 +653,14 @@ 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();
@ -1204,11 +1238,11 @@ impl Account {
),
melib::INFO,
);
return Err(MeliError::new(format!(
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"))
}
}
@ -1609,11 +1643,6 @@ 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
@ -1635,6 +1664,13 @@ 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();
}
}
}
@ -1730,7 +1766,7 @@ impl Account {
if let Ok(Some(is_online)) = handle.chan.try_recv() {
self.sender
.send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange(
self.hash,
self.hash, None,
)))
.unwrap();
if is_online.is_ok() {
@ -1785,7 +1821,7 @@ impl Account {
self.is_online = Ok(());
self.sender
.send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange(
self.hash,
self.hash, None,
)))
.unwrap();
}
@ -1805,7 +1841,7 @@ impl Account {
self.is_online = Err(err);
self.sender
.send(ThreadEvent::UIEvent(UIEvent::AccountStatusChange(
self.hash,
self.hash, None,
)))
.unwrap();
}

View File

@ -54,6 +54,12 @@ pub struct ComposingSettings {
/// Default: empty
#[serde(default, alias = "default-header-values")]
pub default_header_values: HashMap<String, String>,
/// Wrap header preample when editing a draft in an editor. This allows you to write non-plain
/// text email without the preamble creating syntax errors. They are stripped when you return
/// from the editor. The values should be a two element array of strings, a prefix and suffix.
/// Default: None
#[serde(default, alias = "wrap-header-preample")]
pub wrap_header_preamble: Option<(String, String)>,
/// Store sent mail after successful submission. This setting is meant to be disabled for
/// non-standard behaviour in gmail, which auto-saves sent mail on its own.
/// Default: true
@ -96,6 +102,7 @@ impl Default for ComposingSettings {
insert_user_agent: true,
default_header_values: HashMap::default(),
store_sent_mail: true,
wrap_header_preamble: None,
attribution_format_string: None,
attribution_use_posix_locale: true,
forward_as_attachment: ToggleFlag::Ask,

View File

@ -129,6 +129,12 @@ 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 {
@ -158,6 +164,7 @@ impl Default for ListingSettings {
thread_snoozed_flag: None,
selected_flag: None,
attachment_flag: None,
thread_subject_pack: true,
}
}
}
@ -192,6 +199,7 @@ 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,17 +31,26 @@ 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>,
}
@ -51,6 +60,7 @@ 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,
@ -65,6 +75,7 @@ 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,6 +88,11 @@ 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 {
@ -104,6 +109,7 @@ impl Default for PagerSettingsOverride {
auto_choose_multipart_alternative: None,
show_date_in_my_timezone: None,
url_launcher: None,
html_open: None,
}
}
}
@ -171,6 +177,11 @@ 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 {
@ -191,6 +202,7 @@ impl Default for ListingSettingsOverride {
thread_snoozed_flag: None,
selected_flag: None,
attachment_flag: None,
thread_subject_pack: None,
}
}
}
@ -202,12 +214,16 @@ 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)]
@ -224,6 +240,7 @@ impl Default for NotificationsSettingsOverride {
NotificationsSettingsOverride {
enable: None,
script: None,
new_mail_script: None,
xbiff_file_path: None,
play_sound: None,
sound_file: None,
@ -240,9 +257,6 @@ 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>,
@ -261,7 +275,6 @@ impl Default for ShortcutsOverride {
general: None,
listing: None,
composing: None,
compact_listing: None,
contact_list: None,
envelope_view: None,
thread_view: None,
@ -299,6 +312,13 @@ pub struct ComposingSettingsOverride {
#[serde(alias = "default-header-values")]
#[serde(default)]
pub default_header_values: Option<HashMap<String, String>>,
#[doc = " Wrap header preample when editing a draft in an editor. This allows you to write non-plain"]
#[doc = " text email without the preamble creating syntax errors. They are stripped when you return"]
#[doc = " from the editor. The values should be a two element array of strings, a prefix and suffix."]
#[doc = " Default: None"]
#[serde(alias = "wrap-header-preample")]
#[serde(default)]
pub wrap_header_preamble: Option<Option<(String, String)>>,
#[doc = " Store sent mail after successful submission. This setting is meant to be disabled for"]
#[doc = " non-standard behaviour in gmail, which auto-saves sent mail on its own."]
#[doc = " Default: true"]
@ -342,6 +362,7 @@ impl Default for ComposingSettingsOverride {
format_flowed: None,
insert_user_agent: None,
default_header_values: None,
wrap_header_preamble: None,
store_sent_mail: None,
attribution_format_string: None,
attribution_use_posix_locale: None,

View File

@ -87,14 +87,25 @@ 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 {
@ -106,6 +117,7 @@ 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,
@ -128,6 +140,7 @@ 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,8 +43,6 @@ 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")]
@ -64,9 +62,6 @@ 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),
@ -141,14 +136,6 @@ 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 {
@ -172,7 +159,11 @@ 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('`')
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')
}
}
@ -212,7 +203,8 @@ 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('<')
info_message_previous |> "Show previous info message, if any" |> Key::Alt('<'),
focus_in_text_field |> "Focus on a text field." |> Key::Char('\n')
}
}

View File

@ -36,6 +36,7 @@ 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 {
@ -236,6 +237,10 @@ 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",
@ -1236,26 +1241,28 @@ impl Themes {
t => self.other_themes.get(t).unwrap_or(&self.dark),
};
let mut ret = String::new();
ret.push_str(&format!("[terminal.themes.{}]\n", key));
let _ = writeln!(ret, "[terminal.themes.{}]", key);
if unlink {
for k in theme.keys() {
ret.push_str(&format!(
"\"{}\" = {{ fg = {}, bg = {}, attrs = {} }}\n",
let _ = writeln!(
ret,
"\"{}\" = {{ fg = {}, bg = {}, attrs = {} }}",
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() {
ret.push_str(&format!(
"\"{}\" = {{ fg = {}, bg = {}, attrs = {} }}\n",
let _ = writeln!(
ret,
"\"{}\" = {{ fg = {}, bg = {}, attrs = {} }}",
k,
toml::to_string(&theme[k].fg).unwrap(),
toml::to_string(&theme[k].bg).unwrap(),
toml::to_string(&theme[k].attrs).unwrap(),
));
);
}
}
ret
@ -1295,11 +1302,22 @@ 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::Byte(243), bg: Color::Default, attrs: Attr::DEFAULT }, light = { fg: Color::Byte(243), 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" });
/* 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,11 +19,12 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
/*! Find mailcap entries to execute attachments.
*/
//! # 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)
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;
@ -159,7 +160,8 @@ impl MailcapEntry {
.map(|arg| match *arg {
"%s" => {
needs_stdin = false;
let _f = create_temp_file(&decode(a, None), None, None, true);
let _f =
create_temp_file(&a.decode(Default::default()), None, None, true);
let p = _f.path().display().to_string();
f = Some(_f);
p
@ -191,7 +193,11 @@ impl MailcapEntry {
.stdout(Stdio::piped())
.spawn()?;
child.stdin.as_mut().unwrap().write_all(&decode(a, None))?;
child
.stdin
.as_mut()
.unwrap()
.write_all(&a.decode(Default::default()))?;
child.wait_with_output()?.stdout
} else {
let child = Command::new("sh")
@ -208,7 +214,8 @@ impl MailcapEntry {
std::borrow::Cow::from("less")
};
let mut pager = Command::new(pager_cmd.as_ref())
let mut pager = Command::new("sh")
.args(["-c", pager_cmd.as_ref()])
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.spawn()?;
@ -221,7 +228,11 @@ impl MailcapEntry {
.stdout(Stdio::inherit())
.spawn()?;
child.stdin.as_mut().unwrap().write_all(&decode(a, None))?;
child
.stdin
.as_mut()
.unwrap()
.write_all(&a.decode(Default::default()))?;
debug!(child.wait_with_output()?.stdout);
} else {
let child = Command::new("sh")

View File

@ -74,9 +74,10 @@ fn notify(
#[cfg(feature = "cli-docs")]
fn parse_manpage(src: &str) -> Result<ManPages> {
match src {
"" | "meli" | "main" => Ok(ManPages::Main),
"meli.conf" | "conf" | "config" | "configuration" => Ok(ManPages::Conf),
"meli-themes" | "themes" | "theming" | "theme" => Ok(ManPages::Themes),
"" | "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),
_ => Err(MeliError::new(format!(
"Invalid documentation page: {}",
src
@ -94,6 +95,8 @@ enum ManPages {
Conf = 1,
/// meli-themes(5)
Themes = 2,
/// meli(7)
Guide = 3,
}
#[derive(Debug, StructOpt)]
@ -143,7 +146,7 @@ enum SubCommand {
#[derive(Debug, StructOpt)]
struct ManOpt {
#[structopt(default_value = "meli", possible_values=&["meli", "conf", "themes"], value_name="PAGE", parse(try_from_str = parse_manpage))]
#[structopt(default_value = "meli", possible_values=&["meli", "conf", "themes", "meli.7", "guide"], 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.
@ -196,10 +199,11 @@ fn run_app(opt: Opt) -> Result<()> {
#[cfg(feature = "cli-docs")]
Some(SubCommand::Man(manopt)) => {
let ManOpt { page, no_raw } = manopt;
const MANPAGES: [&[u8]; 3] = [
const MANPAGES: [&[u8]; 4] = [
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::*;
@ -229,12 +233,13 @@ fn run_app(opt: Opt) -> Result<()> {
}
use std::process::{Command, Stdio};
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()?;
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()?;
handle.stdin.take().unwrap().write_all(v.as_bytes())?;
handle.wait()?;
@ -242,7 +247,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. 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 (cargo feature `cli-docs`). You might have it installed as manpages (eg `man meli`), otherwise check https://meli.delivery"));
}
Some(SubCommand::CompiledWith) => {
#[cfg(feature = "notmuch")]

View File

@ -24,17 +24,15 @@ 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::new_managesieve_connection;
use melib::backends::imap::managesieve::ManageSieveConnection;
use melib::Result;
#[macro_use]
@ -64,7 +62,7 @@ pub mod sqlite3;
pub mod jobs;
pub mod mailcap;
pub mod plugins;
//pub mod plugins;
use futures::executor::block_on;
@ -84,10 +82,7 @@ fn main() -> Result<()> {
std::process::exit(1);
}
let (config_path, account_name) = (
std::mem::replace(&mut args[0], String::new()),
std::mem::replace(&mut args[1], String::new()),
);
let (config_path, account_name) = (std::mem::take(&mut args[0]), std::mem::take(&mut args[1]));
std::env::set_var("MELI_CONFIG", config_path);
let settings = conf::Settings::new()?;
if !settings.accounts.contains_key(&account_name) {
@ -102,12 +97,47 @@ fn main() -> Result<()> {
);
std::process::exit(1);
}
let mut conn = new_managesieve_connection(&settings.accounts[&account_name].account)?;
block_on(conn.connect())?;
let mut res = String::with_capacity(8 * 1024);
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 input = String::new();
println!("managesieve shell: use 'logout'");
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;
loop {
use std::io;
use std::io::Write;
@ -116,12 +146,85 @@ fn main() -> Result<()> {
io::stdout().flush().unwrap();
match io::stdin().read_line(&mut input) {
Ok(_) => {
if input.trim().eq_ignore_ascii_case("logout") {
let input = input.trim();
if input.eq_ignore_ascii_case("logout") {
break;
}
block_on(conn.send_command(input.as_bytes()))?;
block_on(conn.read_lines(&mut res, String::new()))?;
println!("out: {}", res.trim());
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());
}
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, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, 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: Settings,
pub settings: Box<Settings>,
/// Areas of the screen that must be redrawn in the next render
pub dirty_areas: VecDeque<Area>,
@ -145,10 +145,16 @@ impl Context {
}
accounts[account_pos].watch();
replies.push_back(UIEvent::AccountStatusChange(accounts[account_pos].hash()));
replies.push_back(UIEvent::AccountStatusChange(
accounts[account_pos].hash(),
None,
));
}
if ret.is_ok() != was_online {
replies.push_back(UIEvent::AccountStatusChange(accounts[account_pos].hash()));
replies.push_back(UIEvent::AccountStatusChange(
accounts[account_pos].hash(),
None,
));
}
ret
}
@ -157,18 +163,87 @@ 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: Screen,
screen: Box<Screen>,
draw_rate_limit: RateLimit,
child: Option<ForkType>,
pub mode: UIMode,
overlay: Vec<Box<dyn Component>>,
components: Vec<Box<dyn Component>>,
pub context: Context,
pub context: Box<Context>,
timer: thread::JoinHandle<()>,
display_messages: SmallVec<[DisplayMessage; 8]>,
@ -222,11 +297,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 = if let Some(settings) = settings {
let settings = Box::new(if let Some(settings) = settings {
settings
} else {
Settings::new()?
};
});
/*
let mut plugin_manager = PluginManager::new();
for (_, p) in settings.plugins.clone() {
@ -301,7 +376,7 @@ impl State {
let working = Arc::new(());
let control = Arc::downgrade(&working);
let mut s = State {
screen: Screen {
screen: Box::new(Screen {
cols,
rows,
grid: CellBuffer::new(cols, rows, Cell::with_char(' ')),
@ -313,7 +388,7 @@ impl State {
} else {
Screen::draw_horizontal_segment_no_color
},
},
}),
child: None,
mode: UIMode::Normal,
components: Vec::with_capacity(8),
@ -327,7 +402,7 @@ impl State {
display_messages_dirty: false,
display_messages_initialised: false,
display_messages_area: ((0, 0), (0, 0)),
context: Context {
context: Box::new(Context {
accounts,
settings,
dirty_areas: VecDeque::with_capacity(5),
@ -345,7 +420,7 @@ impl State {
},
sender,
receiver,
},
}),
};
if s.context.settings.terminal.ascii_drawing {
s.screen.grid.set_ascii_drawing(true);
@ -389,7 +464,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 {
@ -942,10 +1017,10 @@ impl State {
if toml::Value::try_from(&new_settings) == toml::Value::try_from(&self.context.settings) {
return Err("No changes detected.".into());
}
Ok(new_settings)
Ok(Box::new(new_settings))
}) {
Ok(new_settings) => {
let old_settings = Box::new(std::mem::replace(&mut self.context.settings, new_settings));
let old_settings = std::mem::replace(&mut self.context.settings, new_settings);
self.context.replies.push_back(UIEvent::ConfigReload {
old_settings
});
@ -1004,17 +1079,21 @@ impl State {
format!(
"{}: {}{}{}",
self.context.accounts[&account_hash].name(),
description.as_ref().map(|s| s.as_str()).unwrap_or(""),
if description.is_some() { ": " } else { "" },
content.as_str()
description.as_str(),
if content.is_some() { ": " } else { "" },
content.as_ref().map(|s| s.as_str()).unwrap_or("")
),
level,
);
self.rcv_event(UIEvent::StatusEvent(StatusEvent::DisplayMessage(
content.to_string(),
description.to_string(),
)));
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,6 +463,7 @@ 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)]
#[derive(Debug, Deserialize, PartialEq, Eq)]
struct V {
k: Color,
}
@ -754,3 +754,267 @@ 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,15 +25,6 @@ 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 {
@ -41,10 +32,17 @@ 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,
@ -179,7 +177,7 @@ impl EmbedTerminal {
}
}
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Eq)]
enum CodepointBuf {
None,
TwoCodepoints(u8),

View File

@ -139,7 +139,7 @@ impl PartialEq<Key> for &Key {
}
}
#[derive(PartialEq)]
#[derive(PartialEq, Eq)]
/// 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)]
#[derive(Debug, Deserialize, PartialEq, Eq)]
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)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
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)]
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct UText {
content: String,
cursor_pos: usize,

View File

@ -39,6 +39,7 @@ 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};
@ -134,7 +135,7 @@ pub enum UIEvent {
MailboxUpdate((AccountHash, MailboxHash)), // (account_idx, mailbox_idx)
MailboxDelete((AccountHash, MailboxHash)),
MailboxCreate((AccountHash, MailboxHash)),
AccountStatusChange(AccountHash),
AccountStatusChange(AccountHash, Option<Cow<'static, str>>),
ComponentKill(Uuid),
BackendEvent(AccountHash, BackendEvent),
StartupCheck(MailboxHash),
@ -168,7 +169,7 @@ impl From<RefreshEvent> for UIEvent {
}
}
#[derive(Debug, PartialEq, Copy, Clone)]
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum UIMode {
Normal,
Insert,
@ -194,14 +195,6 @@ 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.