Compare commits
1 Commits
master
...
jmap-event
Author | SHA1 | Date |
---|---|---|
Manos Pitsidianakis | a36a444b2c |
|
@ -15,6 +15,3 @@ debian/debhelper-build-stamp
|
|||
debian/files
|
||||
debian/meli.substvars
|
||||
debian/meli/
|
||||
|
||||
# CLion IDE
|
||||
.idea
|
116
CHANGELOG.md
116
CHANGELOG.md
|
@ -8,118 +8,9 @@ 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
|
||||
- `7606317f` melib/notmuch: add support for virtual mailbox hierarchy
|
||||
Add optional `parent` property to notmuch mailbox configuration.
|
||||
- `d9c07def` Add command to select charset encoding for email
|
||||
Open dialog to select charset with `d`.
|
||||
- `d679a744` melib/jmap: Implement Bearer token authentication
|
||||
Fastmail now uses an API token in a http header for authentication.
|
||||
This can be used either as a server_password or provided by a
|
||||
`server_password_command` like oauth2.
|
||||
- `47e6d5d9` add edit-config CLI subcommand that opens config files on `EDITOR`
|
||||
- `8c671935` Add compose (pre-submission) hooks for validation/linting
|
||||
compose-hooks run before submitting an e-mail.
|
||||
They perform draft validation and/or transformations.
|
||||
If a hook encounters an error or warning, it will show up as a notification.
|
||||
The currently available hooks are:
|
||||
- `past-date-warn`
|
||||
Warn if Date header value is far in the past or future.
|
||||
- `important-header-warn`
|
||||
Warn if important headers (From, Date, To, Cc, Bcc) are missing or invalid.
|
||||
- `missing-attachment-warn`
|
||||
Warn if Subject, draft body mention attachments but they are missing.
|
||||
- `empty-draft-warn`
|
||||
Warn if draft has no subject and no body.
|
||||
|
||||
They can be disabled with `[composing.disabled_compose_hooks]` setting.
|
||||
|
||||
### 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
|
||||
|
||||
- Add forward mail option
|
||||
- Add url_launcher config setting
|
||||
- Add add_addresses_to_contacts command
|
||||
- Add show_date_in_my_timezone pager config flag
|
||||
- docs: add pager filter documentation
|
||||
- mail/view: respect per-folder/account pager filter override
|
||||
- pager: add filter command, esc to clear filter
|
||||
- Show compile time features in with command argument
|
||||
|
||||
### Fixed
|
||||
|
||||
- melib/email/address: quote display_name if it contains ","
|
||||
- melib/smtp: fix Cc and Bcc ignored when sending mail
|
||||
- melib/email/address: quote display_name if it contains "."
|
||||
|
||||
## [alpha-0.7.1] - 2021-09-08
|
||||
|
||||
### Added
|
||||
|
||||
- Change all Down/Up shortcuts to j/k
|
||||
- add 'GB18030' charset
|
||||
- melib/nntp: implement refresh
|
||||
- melib/nntp: update total/new counters on new articles
|
||||
- melib/nntp: implement NNTP posting
|
||||
- configs: throw error on extra unusued conf flags in some imap/nntp
|
||||
- configs: throw error on missing `composing` section with explanation
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix compilation for netbsd-9.2
|
||||
- conf: fixed some boolean flag values requiring to be string e.g. "true"
|
||||
|
||||
## [alpha-0.7.0] - 2021-09-03
|
||||
|
||||
### Added
|
||||
|
||||
Notable changes:
|
||||
|
||||
- add import command to import email from files into accounts
|
||||
- add add-attachment-file-picker command and `file_picker_command` setting to
|
||||
- Add import command to import email from files into accounts
|
||||
- Add add-attachment-file-picker command and `file_picker_command` setting to
|
||||
use external commands to choose files when composing new mail
|
||||
- ask confirm for delete
|
||||
- add export-mbox command
|
||||
- add export-mail command
|
||||
- add TLS support with nntp
|
||||
- add JMAP watch with polling
|
||||
- add reload-config command
|
||||
- add import-mail command
|
||||
- imap: implement gmail XOAUTH2 authentication method
|
||||
- imap: implement OAUTH2 authentication
|
||||
- compose: treat inline message/rfc822 as attachments
|
||||
- add gpg support via libgpgme
|
||||
|
||||
### Fixed
|
||||
|
||||
- Loading notmuch library on macos
|
||||
- Limit dbus dependency to target_os = "linux"
|
||||
- IMAP, notmuch, mbox backends: various performance fixes
|
||||
|
||||
## [alpha-0.6.2] - 2020-09-24
|
||||
|
||||
|
@ -218,6 +109,3 @@ Notable changes:
|
|||
[alpha-0.6.0]: https://github.com/meli/meli/releases/tag/alpha-0.6.0
|
||||
[alpha-0.6.1]: https://github.com/meli/meli/releases/tag/alpha-0.6.1
|
||||
[alpha-0.6.2]: https://github.com/meli/meli/releases/tag/alpha-0.6.2
|
||||
[alpha-0.7.0]: https://github.com/meli/meli/releases/tag/alpha-0.7.0
|
||||
[alpha-0.7.1]: https://github.com/meli/meli/releases/tag/alpha-0.7.1
|
||||
[alpha-0.7.2]: https://github.com/meli/meli/releases/tag/alpha-0.7.2
|
||||
|
|
File diff suppressed because it is too large
Load Diff
81
Cargo.toml
81
Cargo.toml
|
@ -1,9 +1,8 @@
|
|||
[package]
|
||||
name = "meli"
|
||||
version = "0.7.2"
|
||||
version = "0.6.2"
|
||||
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
|
||||
edition = "2018"
|
||||
rust-version = "1.65.0"
|
||||
|
||||
license = "GPL-3.0-or-later"
|
||||
readme = "README.md"
|
||||
|
@ -16,72 +15,66 @@ default-run = "meli"
|
|||
|
||||
[[bin]]
|
||||
name = "meli"
|
||||
path = "src/main.rs"
|
||||
path = "src/bin.rs"
|
||||
|
||||
[lib]
|
||||
name = "meli"
|
||||
path = "src/lib.rs"
|
||||
#[[bin]]
|
||||
#name = "managesieve-meli"
|
||||
#path = "src/managesieve.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "managesieve-client"
|
||||
path = "src/managesieve.rs"
|
||||
required-features = ["melib/imap_backend"]
|
||||
#[[bin]]
|
||||
#name = "async"
|
||||
#path = "src/async.rs"
|
||||
|
||||
[dependencies]
|
||||
async-task = "^4.2.0"
|
||||
bitflags = "1.0"
|
||||
crossbeam = { version = "^0.8" }
|
||||
flate2 = { version = "1", optional = true }
|
||||
futures = "0.3.5"
|
||||
indexmap = { version = "^1.6", features = ["serde-1", ] }
|
||||
libc = { version = "0.2.125", default-features = false, features = ["extra_traits",] }
|
||||
linkify = { version = "^0.8", default-features = false }
|
||||
melib = { path = "melib", version = "0.7.2" }
|
||||
nix = { version = "^0.24", default-features = false }
|
||||
notify = { version = "4.0.1", default-features = false } # >:c
|
||||
num_cpus = "1.12.0"
|
||||
pcre2 = { version = "0.2.3", optional = true }
|
||||
xdg = "2.1.0"
|
||||
crossbeam = "0.7.2"
|
||||
signal-hook = "0.1.12"
|
||||
signal-hook-registry = "1.2.0"
|
||||
nix = "0.17.0"
|
||||
melib = { path = "melib", version = "0.6.2" }
|
||||
|
||||
serde = "1.0.71"
|
||||
serde_derive = "1.0.71"
|
||||
serde_json = "1.0"
|
||||
signal-hook = { version = "^0.3", default-features = false }
|
||||
signal-hook-registry = { version = "1.2.0", default-features = false }
|
||||
smallvec = { version = "^1.5.0", features = ["serde", ] }
|
||||
structopt = { version = "0.3.14", default-features = false }
|
||||
svg_crate = { version = "^0.13", optional = true, package = "svg" }
|
||||
termion = { version = "1.5.1", default-features = false }
|
||||
toml = { version = "0.5.6", default-features = false, features = ["preserve_order", ] }
|
||||
toml = { version = "0.5.6", features = ["preserve_order", ] }
|
||||
indexmap = { version = "^1.5", features = ["serde-1", ] }
|
||||
linkify = "0.4.0"
|
||||
notify = "4.0.1" # >:c
|
||||
termion = "1.5.1"
|
||||
bincode = "^1.3.0"
|
||||
uuid = { version = "0.8.1", features = ["serde", "v4"] }
|
||||
unicode-segmentation = "1.2.1" # >:c
|
||||
xdg = "2.1.0"
|
||||
|
||||
libc = {version = "0.2.59", features = ["extra_traits",]}
|
||||
smallvec = { version = "^1.5.0", features = ["serde", ] }
|
||||
bitflags = "1.0"
|
||||
pcre2 = { version = "0.2.3", optional = true }
|
||||
structopt = { version = "0.3.14", default-features = false }
|
||||
svg_crate = { version = "0.8.0", optional = true, package = "svg" }
|
||||
futures = "0.3.5"
|
||||
async-task = "3.0.0"
|
||||
num_cpus = "1.12.0"
|
||||
flate2 = { version = "1.0.16", optional = true }
|
||||
|
||||
[target.'cfg(target_os="linux")'.dependencies]
|
||||
notify-rust = { version = "^4", default-features = false, features = ["dbus", ], optional = true }
|
||||
notify-rust = { version = "^4", optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
flate2 = { version = "1", optional = true }
|
||||
proc-macro2 = "1.0.37"
|
||||
syn = { version = "1.0.31", features = [] }
|
||||
quote = "^1.0"
|
||||
regex = "1"
|
||||
syn = { version = "1", features = [] }
|
||||
|
||||
[dev-dependencies]
|
||||
flate2 = { version = "1" }
|
||||
regex = "1"
|
||||
tempfile = "3.3"
|
||||
proc-macro2 = "1.0.18"
|
||||
flate2 = { version = "1.0.16", optional = true }
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
opt-level = "s"
|
||||
debug = false
|
||||
strip = true
|
||||
|
||||
[workspace]
|
||||
members = ["melib", "tools", ]
|
||||
|
||||
[features]
|
||||
default = ["sqlite3", "notmuch", "regexp", "smtp", "dbus-notifications", "gpgme", "cli-docs"]
|
||||
default = ["sqlite3", "notmuch", "regexp", "smtp", "dbus-notifications", "gpgme"]
|
||||
notmuch = ["melib/notmuch_backend", ]
|
||||
jmap = ["melib/jmap_backend",]
|
||||
sqlite3 = ["melib/sqlite3"]
|
||||
|
|
40
Makefile
40
Makefile
|
@ -16,14 +16,6 @@
|
|||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
.POSIX:
|
||||
.SUFFIXES:
|
||||
CARGO_TARGET_DIR ?= target
|
||||
MIN_RUSTC ?= 1.65.0
|
||||
CARGO_BIN ?= cargo
|
||||
CARGO_ARGS ?=
|
||||
CARGO_SORT_BIN = cargo-sort
|
||||
PRINTF = /usr/bin/printf
|
||||
|
||||
# Options
|
||||
PREFIX ?= /usr/local
|
||||
|
@ -31,6 +23,11 @@ EXPANDED_PREFIX := `cd ${PREFIX} && pwd -P`
|
|||
BINDIR ?= ${EXPANDED_PREFIX}/bin
|
||||
MANDIR ?= ${EXPANDED_PREFIX}/share/man
|
||||
|
||||
CARGO_TARGET_DIR ?= target
|
||||
MIN_RUSTC ?= 1.39.0
|
||||
CARGO_BIN ?= cargo
|
||||
CARGO_ARGS ?=
|
||||
|
||||
# Installation parameters
|
||||
DOCS_SUBDIR ?= docs/
|
||||
MANPAGES ?= meli.1 meli.conf.5 meli-themes.5
|
||||
|
@ -49,11 +46,11 @@ CARGO_COLOR ?= `[ -z $${NO_COLOR+x} ] && echo "" || echo "--color=never "`
|
|||
RED ?= `[ -z $${NO_COLOR+x} ] && ([ -z $${TERM} ] && echo "" || tput setaf 1) || echo ""`
|
||||
GREEN ?= `[ -z $${NO_COLOR+x} ] && ([ -z $${TERM} ] && echo "" || tput setaf 2) || echo ""`
|
||||
|
||||
.PHONY: meli
|
||||
.POSIX:
|
||||
.SUFFIXES:
|
||||
meli: check-deps
|
||||
@${CARGO_BIN} build ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --release --bin meli
|
||||
@${CARGO_BIN} build ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --release
|
||||
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo "For a quick start, build and install locally:\n ${BOLD}${GREEN}PREFIX=~/.local make install${ANSI_RESET}\n"
|
||||
@echo "Available subcommands:"
|
||||
|
@ -95,20 +92,7 @@ help:
|
|||
|
||||
.PHONY: check
|
||||
check:
|
||||
@${CARGO_BIN} check ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --tests --examples --benches --bins
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
@$(CARGO_BIN) +nightly fmt --all || $(CARGO_BIN) fmt --all
|
||||
@OUT=$$($(CARGO_SORT_BIN) -w 2>&1) || $(PRINTF) "WARN: %s cargo-sort failed or binary not found in PATH.\n" "$$OUT"
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@$(CARGO_BIN) clippy --no-deps --all-features --all --tests --examples --benches --bins
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all --tests --examples --benches --bins
|
||||
@${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --workspace
|
||||
|
||||
.PHONY: check-deps
|
||||
check-deps:
|
||||
|
@ -142,9 +126,9 @@ install-doc:
|
|||
MANPAGEPATH=${DESTDIR}${MANDIR}/man$${SECTION}/$${MANPAGE}.gz; \
|
||||
echo " * installing $${MANPAGE} → ${GREEN}$${MANPAGEPATH}${ANSI_RESET}"; \
|
||||
gzip -n < ${DOCS_SUBDIR}$${MANPAGE} > $${MANPAGEPATH} \
|
||||
; done ; \
|
||||
; done ; \
|
||||
(case ":${MANPATHS}:" in \
|
||||
*:${DESTDIR}${MANDIR}:*) echo -n "";; \
|
||||
*:${DESTDIR}${MANDIR}:*) echo -n "";; \
|
||||
*) echo "\n${RED}${BOLD}WARNING${ANSI_RESET}: ${UNDERLINE}Path ${DESTDIR}${MANDIR} is not contained in your MANPATH variable or the output of \`manpath\` command.${ANSI_RESET} \`man\` might fail finding the installed manpages. Consider adding it if necessary.\nMANPATH variable / output of \`manpath\`: ${MANPATHS}" ;; \
|
||||
esac) ; \
|
||||
else echo "NO_MAN is defined, so no documentation is going to be installed." ; fi)
|
||||
|
@ -154,7 +138,7 @@ install-bin: meli
|
|||
@mkdir -p $(DESTDIR)${BINDIR}
|
||||
@echo " - ${BOLD}Installing binary to ${ANSI_RESET}${GREEN}${DESTDIR}${BINDIR}/meli${ANSI_RESET}"
|
||||
@case ":${PATH}:" in \
|
||||
*:${DESTDIR}${BINDIR}:*) echo -n "";; \
|
||||
*:${DESTDIR}${BINDIR}:*) echo -n "";; \
|
||||
*) echo "\n${RED}${BOLD}WARNING${ANSI_RESET}: ${UNDERLINE}Path ${DESTDIR}${BINDIR} is not contained in your PATH variable.${ANSI_RESET} Consider adding it if necessary.\nPATH variable: ${PATH}";; \
|
||||
esac
|
||||
@mkdir -p $(DESTDIR)${BINDIR}
|
||||
|
|
29
README.md
29
README.md
|
@ -3,7 +3,7 @@
|
|||
**BSD/Linux terminal email client with support for multiple accounts and Maildir / mbox / notmuch / IMAP / JMAP.**
|
||||
|
||||
Community links:
|
||||
[mailing lists](https://lists.meli.delivery/) | `#meli` on OFTC IRC | Report bugs and/or feature requests in [meli's issue tracker](https://git.meli.delivery/meli/meli/issues "meli gitea issue tracker")
|
||||
[mailing lists](https://lists.meli.delivery/) | `#meli` on freenode IRC | Report bugs and/or feature requests in [meli's issue tracker](https://git.meli.delivery/meli/meli/issues "meli gitea issue tracker")
|
||||
|
||||
| | | |
|
||||
:---:|:---:|:---:
|
||||
|
@ -24,13 +24,11 @@ Official mirrors:
|
|||
|
||||
## Documentation
|
||||
|
||||
See a comprehensive tour of `meli` in the manual page [`meli(7)`](./docs/meli.7).
|
||||
See also [Quickstart tutorial](https://meli.delivery/documentation.html#quick-start).
|
||||
|
||||
See also the [Quickstart tutorial](https://meli.delivery/documentation.html#quick-start) online.
|
||||
After installing meli, see `meli(1)`, `meli.conf(5)` and `meli-themes(5)` for documentation. Sample configuration and theme files can be found in the `docs/samples/` subdirectory. Manual pages are also [hosted online](https://meli.delivery/documentation.html "meli documentation").
|
||||
|
||||
After installing `meli`, see `meli(1)`, `meli.conf(5)`, `meli(7)` and `meli-themes(5)` for documentation. Sample configuration and theme files can be found in the `docs/samples/` subdirectory. Manual pages are also [hosted online](https://meli.delivery/documentation.html "meli documentation").
|
||||
|
||||
`meli` by default looks for a configuration file in this location: `$XDG_CONFIG_HOME/meli/config.toml`
|
||||
meli by default looks for a configuration file in this location: `$XDG_CONFIG_HOME/meli/config.toml`
|
||||
|
||||
You can run meli with arbitrary configuration files by setting the `$MELI_CONFIG`
|
||||
environment variable to their locations, i.e.:
|
||||
|
@ -48,12 +46,12 @@ For a quick start, build and install locally:
|
|||
|
||||
Available subcommands for `make` are listed with `make help`. The Makefile *should* be POSIX portable and not require a specific `make` version.
|
||||
|
||||
`meli` requires rust 1.65 and rust's package manager, Cargo. Information on how
|
||||
meli requires rust 1.39 and rust's package manager, Cargo. Information on how
|
||||
to get it on your system can be found here: <https://doc.rust-lang.org/cargo/getting-started/installation.html>
|
||||
|
||||
With Cargo available, the project can be built with `make` and the resulting binary will then be found under `target/release/meli`. Run `make install` to install the binary and man pages. This requires root, so I suggest you override the default paths and install it in your `$HOME`: `make PREFIX=$HOME/.local install`.
|
||||
|
||||
You can build and run `meli` with one command: `cargo run --release`.
|
||||
You can build and run meli with one command: `cargo run --release`.
|
||||
|
||||
### Build features
|
||||
|
||||
|
@ -65,8 +63,8 @@ Some functionality is held behind "feature gates", or compile-time flags. The fo
|
|||
- `jmap` provides support for connecting to a jmap server and use it as a mail backend (off by default)
|
||||
- `sqlite3` provides support for builting fast search indexes in local sqlite3 databases (on by default)
|
||||
- `cli-docs` includes the manpage documentation compiled by either `mandoc` or `man` binary to plain text in `meli`'s command line. Embedded documentation can be viewed with the subcommand `meli man [PAGE]`
|
||||
- `svgscreenshot` provides support for taking screenshots of the current view of `meli` and saving it as SVG files. Its only purpose is taking screenshots for the official `meli` webpage. (off by default)
|
||||
- `debug-tracing` enables various trace debug logs from various places around the `meli` code base. The trace log is printed in `stderr`. (off by default)
|
||||
- `svgscreenshot` provides support for taking screenshots of the current view of meli and saving it as SVG files. Its only purpose is taking screenshots for the official meli webpage. (off by default)
|
||||
- `debug-tracing` enables various trace debug logs from various places around the meli code base. The trace log is printed in `stderr`. (off by default)
|
||||
|
||||
### Build Debian package (*deb*)
|
||||
|
||||
|
@ -77,11 +75,11 @@ A `*.deb` package can be built with `make deb-dist`
|
|||
|
||||
### Using notmuch
|
||||
|
||||
To use the optional notmuch backend feature, you must have `libnotmuch5` installed in your system. In Debian-like systems, install the `libnotmuch5` packages. `meli` detects the library's presence on runtime.
|
||||
To use the optional notmuch backend feature, you must have `libnotmuch5` installed in your system. In Debian-like systems, install the `libnotmuch5` packages. meli detects the library's presence on runtime.
|
||||
|
||||
### Using GPG
|
||||
|
||||
To use the optional gpg feature, you must have `libgpgme` installed in your system. In Debian-like systems, install the `libgpgme11` package. `meli` detects the library's presence on runtime.
|
||||
To use the optional gpg feature, you must have `libgpgme` installed in your system. In Debian-like systems, install the `libgpgme11` package. meli detects the library's presence on runtime.
|
||||
|
||||
### Building with JMAP
|
||||
|
||||
|
@ -93,11 +91,6 @@ MELI_FEATURES="jmap" make
|
|||
|
||||
or if building directly with cargo, use the flag `--features="jmap"'.
|
||||
|
||||
### HTML Rendering
|
||||
|
||||
HTML rendering is achieved using [w3m](https://github.com/tats/w3m) by default.
|
||||
You can use the `pager.html_filter` setting to override this (for more details you can consult [`meli.conf(5)`](./docs/meli.conf.5)).
|
||||
|
||||
# Development
|
||||
|
||||
Development builds can be built and/or run with
|
||||
|
@ -109,7 +102,7 @@ cargo run
|
|||
|
||||
There is a debug/tracing log feature that can be enabled by using the flag
|
||||
`--feature debug-tracing` after uncommenting the features in `Cargo.toml`. The logs
|
||||
are printed in stderr, thus you can run `meli` with a redirection (i.e `2> log`)
|
||||
are printed in stderr, thus you can run meli with a redirection (i.e `2> log`)
|
||||
|
||||
Code style follows the default rustfmt profile.
|
||||
|
||||
|
|
90
build.rs
90
build.rs
|
@ -26,7 +26,6 @@ mod config_macros;
|
|||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=src/conf/.rebuild.overrides.rs");
|
||||
config_macros::override_derive(&[
|
||||
("src/conf/pager.rs", "PagerSettings"),
|
||||
("src/conf/listing.rs", "ListingSettings"),
|
||||
|
@ -38,39 +37,70 @@ fn main() {
|
|||
]);
|
||||
#[cfg(feature = "cli-docs")]
|
||||
{
|
||||
use flate2::{Compression, GzBuilder};
|
||||
const MANDOC_OPTS: &[&str] = &["-T", "utf8", "-I", "os=Generated by mandoc(1)"];
|
||||
use std::{env, fs::File, io::prelude::*, path::Path, process::Command};
|
||||
|
||||
use flate2::Compression;
|
||||
use flate2::GzBuilder;
|
||||
const MANDOC_OPTS: &[&'static str] = &["-T", "utf8", "-I", "os=Generated by mandoc(1)"];
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let mut out_dir_path = Path::new(&out_dir).to_path_buf();
|
||||
out_dir_path.push("meli.txt.gz");
|
||||
|
||||
let mut cl = |filepath: &str, output: &str| {
|
||||
out_dir_path.push(output);
|
||||
let output = Command::new("mandoc")
|
||||
.args(MANDOC_OPTS)
|
||||
.arg(filepath)
|
||||
.output()
|
||||
.or_else(|_| Command::new("man").arg("-l").arg(filepath).output())
|
||||
.expect(
|
||||
"could not execute `mandoc` or `man`. If the binaries are not available in \
|
||||
the PATH, disable `cli-docs` feature to be able to continue compilation.",
|
||||
);
|
||||
let output = Command::new("mandoc")
|
||||
.args(MANDOC_OPTS)
|
||||
.arg("docs/meli.1")
|
||||
.output()
|
||||
.or_else(|_| Command::new("man").arg("-l").arg("docs/meli.1").output())
|
||||
.unwrap();
|
||||
|
||||
let file = File::create(&out_dir_path).unwrap_or_else(|err| {
|
||||
panic!("Could not create file {}: {}", out_dir_path.display(), err)
|
||||
});
|
||||
let mut gz = GzBuilder::new()
|
||||
.comment(output.stdout.len().to_string().into_bytes())
|
||||
.write(file, Compression::default());
|
||||
gz.write_all(&output.stdout).unwrap();
|
||||
gz.finish().unwrap();
|
||||
out_dir_path.pop();
|
||||
};
|
||||
let file = File::create(&out_dir_path).unwrap();
|
||||
let mut gz = GzBuilder::new()
|
||||
.comment(output.stdout.len().to_string().into_bytes())
|
||||
.write(file, Compression::default());
|
||||
gz.write_all(&output.stdout).unwrap();
|
||||
gz.finish().unwrap();
|
||||
out_dir_path.pop();
|
||||
|
||||
cl("docs/meli.1", "meli.txt.gz");
|
||||
cl("docs/meli.conf.5", "meli.conf.txt.gz");
|
||||
cl("docs/meli-themes.5", "meli-themes.txt.gz");
|
||||
cl("docs/meli.7", "meli.7.txt.gz");
|
||||
out_dir_path.push("meli.conf.txt.gz");
|
||||
let output = Command::new("mandoc")
|
||||
.args(MANDOC_OPTS)
|
||||
.arg("docs/meli.conf.5")
|
||||
.output()
|
||||
.or_else(|_| {
|
||||
Command::new("man")
|
||||
.arg("-l")
|
||||
.arg("docs/meli.conf.5")
|
||||
.output()
|
||||
})
|
||||
.unwrap();
|
||||
let file = File::create(&out_dir_path).unwrap();
|
||||
let mut gz = GzBuilder::new()
|
||||
.comment(output.stdout.len().to_string().into_bytes())
|
||||
.write(file, Compression::default());
|
||||
gz.write_all(&output.stdout).unwrap();
|
||||
gz.finish().unwrap();
|
||||
out_dir_path.pop();
|
||||
|
||||
out_dir_path.push("meli-themes.txt.gz");
|
||||
let output = Command::new("mandoc")
|
||||
.args(MANDOC_OPTS)
|
||||
.arg("docs/meli-themes.5")
|
||||
.output()
|
||||
.or_else(|_| {
|
||||
Command::new("man")
|
||||
.arg("-l")
|
||||
.arg("docs/meli-themes.5")
|
||||
.output()
|
||||
})
|
||||
.unwrap();
|
||||
let file = File::create(&out_dir_path).unwrap();
|
||||
let mut gz = GzBuilder::new()
|
||||
.comment(output.stdout.len().to_string().into_bytes())
|
||||
.write(file, Compression::default());
|
||||
gz.write_all(&output.stdout).unwrap();
|
||||
gz.finish().unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,21 +19,17 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
io::prelude::*,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use quote::{format_ident, quote};
|
||||
use regex::Regex;
|
||||
|
||||
// Write ConfigStructOverride to overrides.rs
|
||||
pub fn override_derive(filenames: &[(&str, &str)]) {
|
||||
let mut output_file =
|
||||
File::create("src/conf/overrides.rs").expect("Unable to open output file");
|
||||
let mut output_string = r##"// @generated
|
||||
/*
|
||||
let mut output_string = r##"/*
|
||||
* meli - conf/overrides.rs
|
||||
*
|
||||
* Copyright 2020 Manos Pitsidianakis
|
||||
|
@ -54,23 +50,15 @@ pub fn override_derive(filenames: &[(&str, &str)]) {
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#![allow(clippy::derivable_impls)]
|
||||
|
||||
//! This module is automatically generated by config_macros.rs.
|
||||
|
||||
//! This module is automatically generated by build.rs.
|
||||
use super::*;
|
||||
use melib::HeaderName;
|
||||
|
||||
"##
|
||||
.to_string();
|
||||
|
||||
let cfg_attr_default_attr_regex = Regex::new(r"\s*default\s*[,]").unwrap();
|
||||
let cfg_attr_default_val_attr_regex = Regex::new(r#"\s*default\s*=\s*"[^"]*"\s*,\s*"#).unwrap();
|
||||
let cfg_attr_feature_regex = Regex::new(r"[(](?:not[(]\s*)?feature").unwrap();
|
||||
|
||||
'file_loop: for (filename, ident) in filenames {
|
||||
println!("cargo:rerun-if-changed={}", filename);
|
||||
let mut file = File::open(filename)
|
||||
let mut file = File::open(&filename)
|
||||
.unwrap_or_else(|err| panic!("Unable to open file `{}` {}", filename, err));
|
||||
|
||||
let mut src = String::new();
|
||||
|
@ -116,23 +104,10 @@ use melib::HeaderName;
|
|||
.iter()
|
||||
.filter_map(|f| {
|
||||
let mut new_attr = f.clone();
|
||||
if let proc_macro2::TokenTree::Group(g) =
|
||||
if let quote::__private::TokenTree::Group(g) =
|
||||
f.tokens.clone().into_iter().next().unwrap()
|
||||
{
|
||||
let mut attr_inner_value = f.tokens.to_string();
|
||||
if cfg_attr_feature_regex.is_match(&attr_inner_value) {
|
||||
attr_inner_value = cfg_attr_default_val_attr_regex
|
||||
.replace_all(&attr_inner_value, "")
|
||||
.to_string();
|
||||
if attr_inner_value.contains("default") {
|
||||
attr_inner_value = cfg_attr_default_attr_regex
|
||||
.replace_all(&attr_inner_value, "")
|
||||
.to_string();
|
||||
}
|
||||
let new_toks: proc_macro2::TokenStream =
|
||||
attr_inner_value.parse().unwrap();
|
||||
new_attr.tokens = quote! { #new_toks };
|
||||
}
|
||||
let attr_inner_value = f.tokens.to_string();
|
||||
if !attr_inner_value.starts_with("( default")
|
||||
&& !attr_inner_value.starts_with("( default =")
|
||||
&& !attr_inner_value.starts_with("(default")
|
||||
|
@ -173,9 +148,7 @@ use melib::HeaderName;
|
|||
#[serde(default)]
|
||||
pub #ident : Option<#ty>
|
||||
};
|
||||
if !field_idents.contains(&ident) {
|
||||
field_idents.push(ident);
|
||||
}
|
||||
field_idents.push(ident);
|
||||
field_tokentrees.push(t);
|
||||
}
|
||||
//let fields = &s.fields;
|
||||
|
|
|
@ -1,40 +1,3 @@
|
|||
meli (0.7.2-1) bullseye; urgency=low
|
||||
Added
|
||||
|
||||
- Add forward mail option
|
||||
- Add url_launcher config setting
|
||||
- Add add_addresses_to_contacts command
|
||||
- Add show_date_in_my_timezone pager config flag
|
||||
- docs: add pager filter documentation
|
||||
- mail/view: respect per-folder/account pager filter override
|
||||
- pager: add filter command, esc to clear filter
|
||||
- Show compile time features in with command argument
|
||||
|
||||
Fixed
|
||||
|
||||
- melib/email/address: quote display_name if it contains ","
|
||||
- melib/smtp: fix Cc and Bcc ignored when sending mail
|
||||
- melib/email/address: quote display_name if it contains "."
|
||||
|
||||
-- Manos Pitsidianakis <epilys@nessuent.xyz> Fri, 15 Oct 2021 12:34:00 +0200
|
||||
meli (0.7.1-1) bullseye; urgency=low
|
||||
|
||||
Added
|
||||
- Change all Down/Up shortcuts to j/k
|
||||
- add 'GB18030' charset
|
||||
- melib/nntp: implement refresh
|
||||
- melib/nntp: update total/new counters on new articles
|
||||
- melib/nntp: implement NNTP posting
|
||||
- configs: throw error on extra unusued conf flags in some imap/nntp
|
||||
- configs: throw error on missing `composing` section with explanation
|
||||
|
||||
Fixed
|
||||
- Fix compilation for netbsd-9.2
|
||||
- conf: fixed some boolean flag values requiring to be string e.g. "true"
|
||||
-- Manos Pitsidianakis <epilys@nessuent.xyz> Wed, 08 Sep 2021 18:14:00 +0200
|
||||
meli (0.7.0-1) buster; urgency=low
|
||||
|
||||
-- Manos Pitsidianakis <epilys@nessuent.xyz> Fri, 03 Sep 2021 18:14:00 +0200
|
||||
meli (0.6.2-1) buster; urgency=low
|
||||
|
||||
Added
|
||||
|
|
|
@ -2,7 +2,7 @@ Source: meli
|
|||
Section: mail
|
||||
Priority: optional
|
||||
Maintainer: Manos Pitsidianakis <epilys@nessuent.xyz>
|
||||
Build-Depends: debhelper (>=11~), mandoc (>=1.14.4-1), quilt, libsqlite3-dev
|
||||
Build-Depends: debhelper (>=11~), mandoc (>=1.14.4-1)
|
||||
Standards-Version: 4.1.4
|
||||
Homepage: https://meli.delivery
|
||||
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
Description: Fix PREFIX env var in Makefile for use in Debian
|
||||
Author: Manos Pitsidianakis <epilys@nessuent.xyz>
|
||||
Last-Update: 2023-03-06
|
||||
Index: meli/Makefile
|
||||
===================================================================
|
||||
--- meli.orig/Makefile
|
||||
+++ meli/Makefile
|
||||
@@ -20,7 +20,7 @@
|
||||
.SUFFIXES:
|
||||
Last-Update: 2020-01-30
|
||||
--- a/Makefile
|
||||
+++ b/Makefile
|
||||
@@ -18,7 +18,7 @@
|
||||
# along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Options
|
||||
-PREFIX ?= /usr/local
|
||||
|
|
|
@ -1,122 +0,0 @@
|
|||
# Using other apps with `meli`
|
||||
|
||||
## Sending mail with a command line tool
|
||||
|
||||
`composing.send_mail` can use either settings for an SMTP server or a shell
|
||||
command to which it pipes new mail to.
|
||||
|
||||
### `msmtp` and `send_mail`
|
||||
|
||||
[`msmtp`][msmtp] is a command line SMTP client that can be configured to work
|
||||
with many SMTP servers. It supports queuing and other small useful features.
|
||||
See [the documentation](https://marlam.de/msmtp/msmtp.html).
|
||||
|
||||
```toml
|
||||
[composing]
|
||||
send_mail = 'msmtp --logfile=/home/user/.mail/msmtp.log --read-recipients
|
||||
--read-envelope-from'
|
||||
```
|
||||
[msmtp]: https://marlam.de/msmtp/
|
||||
|
||||
## Editor
|
||||
|
||||
Any editor you specify in `composing.editor_cmd` will be invoked with the
|
||||
e-mail draft file path appended as an argument to it. For example, if your
|
||||
setting is `editor_cmd = 'nano'`, `meli` will execute `nano /tmp/meli/...`.
|
||||
|
||||
### Configuration
|
||||
|
||||
#### `vim` / `neovim` command
|
||||
|
||||
The following command setting in your `meli` configuration file makes editing
|
||||
start at the first empty line, that is, after the e-mail headers. This allows
|
||||
you to start writing the e-mail body right away after opening the editor from
|
||||
`meli`.
|
||||
|
||||
```toml
|
||||
[composing]
|
||||
editor_cmd = '~/.local/bin/vim +/^$'
|
||||
```
|
||||
|
||||
In `vim`, the `+` argument positions the cursor at the first file argument. `/`
|
||||
specifies a pattern position instead of a line number. `^` specifies the start
|
||||
of a line, and `$` the end of the line. The pattern altogether matches an empty
|
||||
line, which will be after the e-mail headers.
|
||||
|
||||
### Composing with `format=flowed`
|
||||
|
||||
`format=flowed` is a proposed IETF standard[^formatflowed] that lets you
|
||||
preserve the structure of paragraphs by disambiguating a *hard* and a *soft*
|
||||
line break. A line break that is preceded by a space character is *soft* and
|
||||
does not terminate the paragraph, while a line break without a space is a
|
||||
*hard* one and creates a new paragraph. This allows text to be re-flowed in
|
||||
e-mail clients at different display widths and font sizes without messing up
|
||||
the author's formatting.
|
||||
|
||||
#### `vim` / `neovim` and `format=flowed`
|
||||
|
||||
Create a `mail.vim` file type plugin in:
|
||||
|
||||
- `$HOME/.vim/after/ftplugin/mail.vim` for vim
|
||||
- `$HOME/.config/nvim/after/ftplugin/mail.vim` for neovim
|
||||
|
||||
```vim
|
||||
setlocal nomodeline
|
||||
setlocal textwidth=72
|
||||
setlocal formatoptions=aqtw2r
|
||||
setlocal nojoinspaces
|
||||
setlocal nosmartindent
|
||||
setlocal comments+=nb:>
|
||||
match ErrorMsg '\s\+$'
|
||||
```
|
||||
|
||||
Also, don't forget that you can easily quote stuff with `MailQuote`.
|
||||
From `:help ft-mail-plugin`:
|
||||
|
||||
> Local mappings:
|
||||
> `<LocalLeader>q` or `\\MailQuote`
|
||||
> Quotes the text selected in Visual mode, or from the cursor position
|
||||
> to the end of the file in Normal mode.
|
||||
> This means "> " is inserted in each line.
|
||||
|
||||
See the accompanying [`mail.vim`](./mail.vim) for comments for each setting.
|
||||
|
||||
## `xbiff`
|
||||
|
||||
[`xbiff(1)`][xbiff] manual page says:[^xbiffmanpage]
|
||||
|
||||
> The `xbiff` program displays a little image of a mailbox. When there is no
|
||||
> mail, the flag on the mailbox is down. When mail arrives, the flag goes up
|
||||
> and the mailbox beeps.
|
||||
|
||||
This tool is very outdated, but some users might still have use for it.
|
||||
Therefore `meli` provides support (also, it's easy to support this feature).
|
||||
|
||||
Specify a file path in `notifications.xbiff_file_path` and `meli` will write to
|
||||
it when new mail arrives. This file can the be used as input to `xbiff`.
|
||||
|
||||
```toml
|
||||
[notifications]
|
||||
xbiff_file_path = "/tmp/xbiff"
|
||||
```
|
||||
|
||||
[xbiff]: https://en.wikipedia.org/wiki/Xbiff
|
||||
[^xbiffmanpage]: https://www.x.org/releases/X11R7.0/doc/html/xbiff.1.html
|
||||
|
||||
## Viewing HTML e-mail
|
||||
|
||||
By default `meli` tries to render HTML e-mail with `w3m`. You can override this
|
||||
by setting the `pager.html_filter` setting. The default setting corresponds to:
|
||||
|
||||
```toml
|
||||
[pager]
|
||||
html_filter = "w3m -I utf-8 -T text/html"
|
||||
```
|
||||
|
||||
The HTML of the e-mail is piped into `html_filter`'s standard input.
|
||||
|
||||
## Externally refreshing e-mail accounts
|
||||
|
||||
If your account's syncing is handled by an external tool, you can use the
|
||||
refresh shortcuts within `meli` to call this tool with
|
||||
`accounts.refresh_command`.
|
|
@ -1,87 +0,0 @@
|
|||
" Place this plugin in
|
||||
"
|
||||
" `$HOME/.vim/after/ftplugin/mail.vim` for vim
|
||||
" `$HOME/.config/nvim/after/ftplugin/mail.vim` for neovim
|
||||
|
||||
" Don't use modelines in e-mail messages
|
||||
setlocal nomodeline
|
||||
setlocal textwidth=72
|
||||
|
||||
" *fo-a*
|
||||
" a Automatic formatting of paragraphs.
|
||||
" Every time text is inserted or deleted the paragraph will be reformatted.
|
||||
" *fo-w*
|
||||
" w Trailing white space indicates a paragraph continues in the next line.
|
||||
" A line that ends in a non-white character ends a paragraph.
|
||||
" *fo-q*
|
||||
" q Allow formatting of comments with "gq".
|
||||
" *fo-t*
|
||||
" t Auto-wrap text using textwidth
|
||||
" *fo-r*
|
||||
" r Automatically insert the current comment leader after hitting <Enter> in
|
||||
" Insert mode.
|
||||
" *fo-c*
|
||||
" c Auto-wrap comments using textwidth, inserting the current comment leader
|
||||
" automatically.
|
||||
" *fo-2*
|
||||
" 2 When formatting text, use the indent of the second line of a paragraph for
|
||||
" the rest of the paragraph, instead of the indent of the first line.
|
||||
" This supports paragraphs in which the first line has a different indent than
|
||||
" the rest.
|
||||
" Note that 'autoindent' must be set too.
|
||||
" Example:
|
||||
" first line of a paragraph
|
||||
" second line of the same paragraph
|
||||
" third line.
|
||||
" This also works inside comments, ignoring the comment leader.
|
||||
setlocal formatoptions=aqtw2r
|
||||
|
||||
" Disable adding two spaces after '.', '?' and '!' with a join command.
|
||||
setlocal nojoinspaces
|
||||
|
||||
" Disable smartident (meant for source code)
|
||||
setlocal nosmartindent
|
||||
|
||||
" *'comments'* *'com'* *E524* *E525*
|
||||
" A comma-separated list of strings that can start a comment line.
|
||||
" See |format-comments|.
|
||||
" See |option-backslash| about using backslashes to insert a space.
|
||||
"
|
||||
"
|
||||
" The 'comments' option is a comma-separated list of parts.
|
||||
" Each part defines a type of comment string.
|
||||
" A part consists of: {flags}:{string}
|
||||
"
|
||||
" {string} is the literal text that must appear.
|
||||
"
|
||||
" {flags}:
|
||||
" n Nested comment.
|
||||
" Nesting with mixed parts is allowed.
|
||||
" If 'comments' is "n:),n:>" a line starting with "> ) >" is a comment.
|
||||
"
|
||||
" b Blank (<Space>, <Tab> or <EOL>) required after {string}.
|
||||
setlocal comments+=nb:>
|
||||
|
||||
" Highlight trailing whitespace as errors.
|
||||
match ErrorMsg '\s\+$'
|
||||
|
||||
" MAIL *mail.vim* *ft-mail.vim*
|
||||
" By default mail.vim synchronises syntax to 100 lines before the first
|
||||
" displayed line.
|
||||
" If you have a slow machine, and generally deal with emails with short
|
||||
" headers, you can change this to a smaller value:
|
||||
|
||||
let mail_minlines = 30
|
||||
|
||||
|
||||
" *no_mail_maps* *g:no_mail_maps*
|
||||
" Disable defining mappings for a specific filetype by setting a variable,
|
||||
" which contains the name of the filetype.
|
||||
" For the "mail" filetype this would be:
|
||||
let no_mail_maps = 1
|
||||
|
||||
" Local mappings:
|
||||
" <LocalLeader>q or \\MailQuote
|
||||
" Quotes the text selected in Visual mode, or from the cursor position
|
||||
" to the end of the file in Normal mode.
|
||||
" This means "> " is inserted in each line.
|
|
@ -17,14 +17,14 @@
|
|||
.\" You should have received a copy of the GNU General Public License
|
||||
.\" along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
.\"
|
||||
.Dd November 11, 2022
|
||||
.Dd January 23, 2020
|
||||
.Dt MELI-THEMES 5
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm meli-themes
|
||||
.Nd themes for the
|
||||
.Xr meli 1
|
||||
terminal e-mail client
|
||||
.Nm meli
|
||||
mail client
|
||||
.Sh SYNOPSIS
|
||||
.Nm meli
|
||||
comes with two themes,
|
||||
|
@ -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
|
||||
|
@ -171,18 +171,8 @@ In this mode, cursor locations (i.e., currently selected entries/items) will use
|
|||
.It
|
||||
theme_default
|
||||
.It
|
||||
error_message
|
||||
.It
|
||||
highlight
|
||||
.It
|
||||
status.bar
|
||||
.It
|
||||
status.command_bar
|
||||
.It
|
||||
status.history
|
||||
.It
|
||||
status.history.hints
|
||||
.It
|
||||
status.notification
|
||||
.It
|
||||
tab.focused
|
||||
|
@ -261,8 +251,12 @@ mail.listing.conversations.from
|
|||
.It
|
||||
mail.listing.conversations.date
|
||||
.It
|
||||
mail.listing.conversations.padding
|
||||
.It
|
||||
mail.listing.conversations.unseen
|
||||
.It
|
||||
mail.listing.conversations.unseen_padding
|
||||
.It
|
||||
mail.listing.conversations.highlighted
|
||||
.It
|
||||
mail.listing.conversations.selected
|
||||
|
|
228
docs/meli.1
228
docs/meli.1
|
@ -17,37 +17,12 @@
|
|||
.\" 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 November 11, 2022
|
||||
.Dd July 29, 2019
|
||||
.Dt MELI 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm meli
|
||||
.Nd terminal e-mail client
|
||||
.Em μέλι
|
||||
is the Greek word for honey
|
||||
.Nd Meli Mail User Agent. meli is the Greek word for honey
|
||||
.Sh SYNOPSIS
|
||||
.Nm
|
||||
.Op Fl -help | h
|
||||
|
@ -65,22 +40,14 @@ Create configuration file in
|
|||
.Pa path
|
||||
if given, or at
|
||||
.Pa $XDG_CONFIG_HOME/meli/config.toml
|
||||
.It Cm edit-config
|
||||
Edit configuration files with
|
||||
.Ev EDITOR
|
||||
or
|
||||
.Ev VISUAL Ns
|
||||
\&.
|
||||
.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
|
||||
Print all loaded themes in TOML syntax.
|
||||
.It Cm compiled-with
|
||||
Print compile time feature flags of this binary.
|
||||
.It Cm view
|
||||
View mail from input file.
|
||||
.El
|
||||
|
@ -116,12 +83,14 @@ See
|
|||
for the available configuration options.
|
||||
.Pp
|
||||
At any time, you may press
|
||||
.Shortcut \&? general toggle_help
|
||||
.Cm \&?
|
||||
for a searchable list of all available actions and shortcuts, along with every possible setting and command that your version supports.
|
||||
.Pp
|
||||
The main visual navigation tool, the left-side sidebar may be toggled with
|
||||
.ShortcutPeriod ` listing toggle_menu_visibility
|
||||
\&.
|
||||
.Cm `
|
||||
(shortcuts.listing:
|
||||
.Ic toggle_menu_visibility Ns
|
||||
).
|
||||
.Pp
|
||||
Each mailbox may be viewed in 4 modes:
|
||||
Plain views each mail individually, Threaded shows their thread relationship visually, Conversations collapses each thread of emails into a single entry, Compact shows one row per thread.
|
||||
|
@ -134,22 +103,23 @@ section of your configuration.
|
|||
See
|
||||
.Xr meli-themes 5
|
||||
for complete documentation on user themes.
|
||||
.Pp
|
||||
See
|
||||
.Xr meli 7
|
||||
for a more detailed tutorial on using
|
||||
.Nm Ns
|
||||
\&.
|
||||
.Sh VIEWING MAIL
|
||||
Open attachments by typing their index in the attachments list and then
|
||||
.ShortcutPeriod a envelope_view open_attachment
|
||||
\&.
|
||||
.Cm a
|
||||
.Po
|
||||
shortcut
|
||||
.Ic open_attachment
|
||||
.Pc .
|
||||
.Nm
|
||||
will attempt to open text inside its pager, and other content via
|
||||
.Cm xdg-open Ns
|
||||
\&.
|
||||
Press
|
||||
.Shortcut m envelope_view open_mailcap
|
||||
.Cm m
|
||||
.Po
|
||||
shortcut
|
||||
.Ic open_mailcap
|
||||
.Pc
|
||||
instead to use the mailcap entry for the MIME type of the attachment, if any.
|
||||
See
|
||||
.Sx FILES
|
||||
|
@ -157,12 +127,12 @@ for the location of the mailcap files and
|
|||
.Xr mailcap 5
|
||||
for their syntax.
|
||||
You can save individual attachments with the
|
||||
.Command save-attachment Ar INDEX Ar path-to-file
|
||||
command.
|
||||
.Em COMMAND
|
||||
.Cm save-attachment Ar INDEX Ar path-to-file
|
||||
where
|
||||
.Ar INDEX
|
||||
is the attachment's index in the listing.
|
||||
If the path provided is a directory, the attachment is saved with its filename set to the filename in the attachment, if any.
|
||||
If the 0th index is provided, the entire message is saved.
|
||||
If the zeroth index is provided, the entire message is saved.
|
||||
If the path provided is a directory, the message is saved as an eml file with its filename set to the messages message-id.
|
||||
.Sh SEARCH
|
||||
Each e-mail storage backend has a default search method assigned.
|
||||
|
@ -191,8 +161,9 @@ To enable sqlite3 indexing for an account set
|
|||
.Em search_backend
|
||||
to
|
||||
.Em sqlite3
|
||||
in the configuration file and to create the sqlite3 index issue command:
|
||||
.Command index Ar ACCOUNT_NAME Ns
|
||||
in the configuration file and to create the sqlite3 index issue command
|
||||
.Cm index Ar ACCOUNT_NAME Ns \&.
|
||||
.sp
|
||||
To search in the message body type your keywords without any special formatting.
|
||||
To search in specific fields, prepend your search keyword with "field:" like so:
|
||||
.Pp
|
||||
|
@ -200,7 +171,7 @@ To search in specific fields, prepend your search keyword with "field:" like so:
|
|||
.Pp
|
||||
.D1 not ((from:unrealistic and (to:complex or not "query")) or flags:seen,draft)
|
||||
.Pp
|
||||
.D1 alladdresses:mailing@example.com and cc:me@example.com
|
||||
.D1 alladdresses:mailing@list.tld and cc:me@domain.tld
|
||||
.Pp
|
||||
Boolean operators are
|
||||
.Em or Ns
|
||||
|
@ -261,30 +232,35 @@ Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticable
|
|||
.Nm
|
||||
supports tagging in notmuch and IMAP/JMAP backends.
|
||||
Tags can be searched with the `tags:` or `flags:` prefix in a search query, and can be modified by
|
||||
.Command tag add TAG
|
||||
.Cm tag add TAG
|
||||
and
|
||||
.Command tag remove TAG
|
||||
.Cm tag remove TAG
|
||||
(see
|
||||
.Xr meli.conf 5 TAGS Ns
|
||||
, settings
|
||||
.Ic colors
|
||||
and
|
||||
.Ic ignore_tags
|
||||
for how to set tag colors and tag visibility)
|
||||
for how to set tag colors and tag visiblity)
|
||||
.Sh COMPOSING
|
||||
.Ss Opening the message Composer tab
|
||||
To create a new mail message, press
|
||||
.Shortcut m listing new_mail
|
||||
while viewing a mailbox.
|
||||
.Cm m
|
||||
(shortcut
|
||||
.Ic new_mail Ns
|
||||
) while viewing a mailbox.
|
||||
To reply to a mail, press
|
||||
.ShortcutPeriod R envelope_view reply
|
||||
\&.
|
||||
.Cm R
|
||||
.Po
|
||||
shortcut
|
||||
.Ic reply
|
||||
.Pc .
|
||||
Both these actions open the mail composer view in a new tab.
|
||||
.Ss Editing text
|
||||
.Bl -bullet -compact
|
||||
.It
|
||||
Edit the header fields by selecting with the arrow keys and pressing
|
||||
.Shortcut Enter general focus_in_text_field
|
||||
.Cm enter
|
||||
to enter
|
||||
.Em INSERT
|
||||
mode and
|
||||
|
@ -292,8 +268,10 @@ mode and
|
|||
key to exit.
|
||||
.It
|
||||
At any time you may press
|
||||
.Shortcut e composing edit Ns
|
||||
to launch your editor (see
|
||||
.Cm e
|
||||
(shortcut
|
||||
.Ic edit_mail Ns
|
||||
) to launch your editor (see
|
||||
.Xr meli.conf 5 COMPOSING Ns
|
||||
, setting
|
||||
.Ic editor_command
|
||||
|
@ -305,23 +283,19 @@ Your editor can be used in
|
|||
.Ic embed
|
||||
to
|
||||
.Em true
|
||||
in your composing settings
|
||||
.Po
|
||||
You can return to
|
||||
.Nm
|
||||
at any time by pressing
|
||||
.Aq Ctrl-Z
|
||||
.Pc
|
||||
in your composing settings.
|
||||
.It
|
||||
When launched, your editor captures all input until it exits or stops.
|
||||
.It
|
||||
To stop your editor and return to
|
||||
.Nm
|
||||
press
|
||||
.Aq Ctrl-z
|
||||
and to resume editing press the
|
||||
.Ic edit
|
||||
command again.
|
||||
press Ctrl-z and to resume editing press the
|
||||
.Ic edit_mail
|
||||
command again
|
||||
.Po
|
||||
default
|
||||
.Em e
|
||||
.Pc .
|
||||
.El
|
||||
.Ss Attachments
|
||||
Attachments may be handled with the
|
||||
|
@ -331,12 +305,14 @@ Attachments may be handled with the
|
|||
commands (see below).
|
||||
.Ss Sending
|
||||
Finally, pressing
|
||||
.Shortcut s composing send_mail
|
||||
will send your message according to your settings
|
||||
.Cm s
|
||||
(shortcut
|
||||
.Ic send_mail Ns
|
||||
) will send your message according to your settings
|
||||
.Po
|
||||
see
|
||||
.Xr meli.conf 5 COMPOSING Ns
|
||||
, setting name
|
||||
, setting
|
||||
.Ic send_mail
|
||||
.Pc Ns
|
||||
\&.
|
||||
|
@ -353,11 +329,11 @@ To save your draft without sending it, issue
|
|||
and select 'save as draft'.
|
||||
.sp
|
||||
To open a draft for further editing, select your draft in the mail listing and press
|
||||
.Ic edit Ns
|
||||
.Ic edit_mail Ns
|
||||
\&.
|
||||
.Sh CONTACTS
|
||||
.Nm
|
||||
supports three kinds of contact backends:
|
||||
supports two kinds of contact backends:
|
||||
.sp
|
||||
.Bl -enum -compact -offset indent
|
||||
.It
|
||||
|
@ -372,11 +348,6 @@ The path defined as
|
|||
.Ic vcard_folder
|
||||
can hold multiple vCards per file.
|
||||
They are loaded read only.
|
||||
.It
|
||||
a
|
||||
.Xr mutt 1
|
||||
compatible alias file in the option
|
||||
.Ic mutt_alias_file
|
||||
.El
|
||||
.sp
|
||||
See
|
||||
|
@ -390,9 +361,9 @@ is the default mode
|
|||
commands are issued in
|
||||
.Em COMMAND
|
||||
mode, by default started with
|
||||
.Shortcut \&: general enter_command_mode
|
||||
.Cm \&:
|
||||
and exited with
|
||||
.Aq Esc
|
||||
.Cm Esc
|
||||
key.
|
||||
.It EMBED
|
||||
is the mode of the embed terminal emulator
|
||||
|
@ -447,8 +418,6 @@ Copy or move to other mailbox.
|
|||
Copy or move to another account's mailbox.
|
||||
.It Cm delete
|
||||
Delete selected threads.
|
||||
.It Cm export-mbox Ar FILEPATH
|
||||
Export selected threads to mboxcl2 file.
|
||||
.It Cm create-mailbox Ar ACCOUNT Ar MAILBOX_PATH
|
||||
create mailbox with given path.
|
||||
Be careful with backends and separator sensitivity (eg IMAP)
|
||||
|
@ -466,8 +435,6 @@ This action is unreversible.
|
|||
.Bl -tag -width 36n
|
||||
.It Cm pipe Ar EXECUTABLE Ar ARGS
|
||||
pipe pager contents to binary
|
||||
.It Cm filter Ar EXECUTABLE Ar ARGS
|
||||
filter and display pager contents through command
|
||||
.It Cm list-post
|
||||
post in list of viewed envelope
|
||||
.It Cm list-unsubscribe
|
||||
|
@ -478,10 +445,6 @@ open list archive with
|
|||
.El
|
||||
.Ss composing mail commands
|
||||
.Bl -tag -width 36n
|
||||
.It Cm mailto Ar MAILTO_ADDRESS
|
||||
Opens a composer tab with initial values parsed from the
|
||||
.Li mailto:
|
||||
address.
|
||||
.It Cm add-attachment Ar PATH
|
||||
in composer, add
|
||||
.Ar PATH
|
||||
|
@ -616,72 +579,19 @@ Mailcap entries are searched for in the following files, in this order:
|
|||
.Sh SEE ALSO
|
||||
.Xr meli.conf 5 ,
|
||||
.Xr meli-themes 5 ,
|
||||
.Xr meli 7 ,
|
||||
.Xr xdg-open 1 ,
|
||||
.Xr mailcap 5
|
||||
.Sh CONFORMING TO
|
||||
.Bl -bullet -compact
|
||||
.It
|
||||
XDG Standard
|
||||
.Lk https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html Ns
|
||||
\&.
|
||||
.It
|
||||
mailcap file, RFC 1524: A User Agent Configuration Mechanism For Multimedia Mail Format Information
|
||||
.It
|
||||
RFC 5322: Internet Message Format
|
||||
.It
|
||||
RFC 6532: Internationalized Email Headers
|
||||
.It
|
||||
RFC 2047: MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text
|
||||
.It
|
||||
RFC 3676: The Text/Plain Format and DelSp Parameters
|
||||
.It
|
||||
RFC 3156: MIME Security with OpenPGP
|
||||
.It
|
||||
RFC 2183: Communicating Presentation Information in Internet Messages: The Content-Disposition Header Field
|
||||
.It
|
||||
RFC 2369: The Use of URLs as Meta-Syntax for Core Mail List Commands and their Transport through Message Header Fields
|
||||
.It
|
||||
.Li maildir
|
||||
.Lk https://cr.yp.to/proto/maildir.html Ns
|
||||
\&.
|
||||
.It
|
||||
RFC 5321: Simple Mail Transfer Protocol
|
||||
.It
|
||||
RFC 3461: Simple Mail Transfer Protocol (SMTP) Service Extension for Delivery Status Notifications (DSNs)
|
||||
.It
|
||||
RFC 4954: SMTP Service Extension for Authentication
|
||||
.It
|
||||
RFC 6152: SMTP Service Extension for 8-bit MIME Transport
|
||||
.It
|
||||
RFC 4616: The PLAIN Simple Authentication and Security Layer (SASL) Mechanism
|
||||
.It
|
||||
RFC 3501: INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1
|
||||
.It
|
||||
RFC 3691: Internet Message Access Protocol (IMAP) UNSELECT command
|
||||
.It
|
||||
RFC 4549: Synch Ops for Disconnected IMAP4 Clients
|
||||
.It
|
||||
RFC 7162: IMAP Extensions: Quick Flag Changes Resynchronization (CONDSTORE) and Quick Mailbox Resynchronization (QRESYNC)
|
||||
.It
|
||||
RFC 8620: The JSON Meta Application Protocol (JMAP)
|
||||
.It
|
||||
RFC 8621: The JSON Meta Application Protocol (JMAP) for Mail
|
||||
.It
|
||||
RFC 3977: Network News Transfer Protocol (NNTP)
|
||||
.It
|
||||
RFC 6048: Network News Transfer Protocol (NNTP) Additions to LIST Command
|
||||
.It
|
||||
vCard Version 3, RFC 2426: vCard MIME Directory Profile
|
||||
.It
|
||||
vCard Version 4, RFC 6350: vCard Format Specification
|
||||
.It
|
||||
RFC 6868 Parameter Value Encoding in iCalendar and vCard
|
||||
.El
|
||||
.Aq https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html Ns
|
||||
, maildir
|
||||
.Aq https://cr.yp.to/proto/maildir.html Ns
|
||||
, IMAPv4rev1 RFC3501, The JSON Meta Application Protocol (JMAP) RFC8620, The JSON Meta Application Protocol (JMAP) for Mail RFC8621.
|
||||
.Sh AUTHORS
|
||||
Copyright 2017-2022
|
||||
.An Manos Pitsidianakis Mt manos@pitsidianak.is
|
||||
Copyright 2017-2019
|
||||
.An Manos Pitsidianakis Aq epilys@nessuent.xyz
|
||||
Released under the GPL, version 3 or greater.
|
||||
This software carries no warranty of any kind (See COPYING for full copyright and warranty notices).
|
||||
This software carries no warranty of any kind.
|
||||
(See COPYING for full copyright and warranty notices.)
|
||||
.Pp
|
||||
.Lk https://meli.delivery
|
||||
.Aq https://meli.delivery
|
||||
|
|
742
docs/meli.7
742
docs/meli.7
|
@ -1,742 +0,0 @@
|
|||
.\" meli - meli.7
|
||||
.\"
|
||||
.\" Copyright 2017-2022 Manos Pitsidianakis
|
||||
.\"
|
||||
.\" This file is part of meli.
|
||||
.\"
|
||||
.\" meli is free software: you can redistribute it and/or modify
|
||||
.\" it under the terms of the GNU General Public License as published by
|
||||
.\" the Free Software Foundation, either version 3 of the License, or
|
||||
.\" (at your option) any later version.
|
||||
.\"
|
||||
.\" meli is distributed in the hope that it will be useful,
|
||||
.\" but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
.\" GNU General Public License for more details.
|
||||
.\"
|
||||
.\" You should have received a copy of the GNU General Public License
|
||||
.\" along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
.\"
|
||||
.\".de Hr
|
||||
.\".Bd -literal -offset center
|
||||
.\"╌╍─────────────────────────────────────────────────────────╍╌
|
||||
.\".Ed
|
||||
.\"..
|
||||
.de Shortcut
|
||||
.Sm
|
||||
.Aq \\$1
|
||||
\
|
||||
.Po
|
||||
.Em shortcuts.\\$2\&. Ns
|
||||
.Em \\$3
|
||||
.Pc
|
||||
.Sm
|
||||
..
|
||||
.de ShortcutPeriod
|
||||
.Aq \\$1
|
||||
.Po
|
||||
.Em shortcuts.\\$2\&. Ns
|
||||
.Em \\$3
|
||||
.Pc Ns
|
||||
..
|
||||
.de Command
|
||||
.Bd -offset 1n -ragged
|
||||
.Cm \\$*
|
||||
.Ed
|
||||
..
|
||||
.Dd November 11, 2022
|
||||
.Dt MELI 7
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm meli
|
||||
.Nd Tutorial for the meli terminal e-mail client
|
||||
.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.
|
||||
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
|
||||
.Pp
|
||||
To go to the next tab on the right, press
|
||||
.ShortcutPeriod T general next_tab
|
||||
\&.
|
||||
.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
|
||||
\&.
|
||||
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
|
||||
\&.
|
||||
.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 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
|
755
docs/meli.conf.5
755
docs/meli.conf.5
File diff suppressed because it is too large
Load Diff
|
@ -11,8 +11,8 @@
|
|||
#[accounts.account-name]
|
||||
#root_mailbox = "/path/to/root/mailbox"
|
||||
#format = "Maildir"
|
||||
#listing.index_style = "Conversations" # or [plain, threaded, compact]
|
||||
#identity="email@example.com"
|
||||
#index_style = "Conversations" # or [plain, threaded, compact]
|
||||
#identity="email@address.tld"
|
||||
#display_name = "Name"
|
||||
#subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
|
||||
#
|
||||
|
@ -26,21 +26,21 @@
|
|||
#[accounts.mbox]
|
||||
#root_mailbox = "/var/mail/username"
|
||||
#format = "mbox"
|
||||
#listing.index_style = "Compact"
|
||||
#index_style = "Compact"
|
||||
#identity="username@hostname.local"
|
||||
#
|
||||
## Setting up an IMAP account
|
||||
#[accounts."imap"]
|
||||
#root_mailbox = "INBOX"
|
||||
#format = "imap"
|
||||
#server_hostname="mail.example.com"
|
||||
#server_hostname="mail.server.tld"
|
||||
#server_password="pha2hiLohs2eeeish2phaii1We3ood4chakaiv0hien2ahie3m"
|
||||
#server_username="username@example.com"
|
||||
##server_port="993" # imaps
|
||||
#server_username="username@server.tld"
|
||||
#server_port="993" # imaps
|
||||
#server_port="143" # STARTTLS
|
||||
#use_starttls=true #optional
|
||||
#listing.index_style = "Conversations"
|
||||
#identity = "username@example.com"
|
||||
#index_style = "Conversations"
|
||||
#identity = "username@server.tld"
|
||||
#display_name = "Name Name"
|
||||
### match every mailbox:
|
||||
#subscribed_mailboxes = ["*" ]
|
||||
|
@ -48,18 +48,18 @@
|
|||
##subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
|
||||
#
|
||||
## Setting up an account for an already existing notmuch database
|
||||
##[accounts.notmuch]
|
||||
##root_mailbox = "/path/to/folder" # where .notmuch/ directory is located
|
||||
##format = "notmuch"
|
||||
##listing.index_style = "conversations"
|
||||
##identity="username@example.com"
|
||||
##display_name = "Name Name"
|
||||
## # notmuch mailboxes are virtual, they are defined by their alias and the notmuch query that corresponds to their content.
|
||||
## [accounts.notmuch.mailboxes]
|
||||
## "INBOX" = { query="tag:inbox", subscribe = true }
|
||||
## "Drafts" = { query="tag:draft", subscribe = true }
|
||||
## "Sent" = { query="from:username@example.com from:username2@example.com", subscribe = true }
|
||||
##
|
||||
#[accounts.notmuch]
|
||||
#root_mailbox = "/path/to/folder" # where .notmuch/ directory is located
|
||||
#format = "notmuch"
|
||||
#index_style = "conversations"
|
||||
#identity="username@server.tld"
|
||||
#display_name = "Name Name"
|
||||
# # notmuch mailboxes are virtual, they are defined by their alias and the notmuch query that corresponds to their content.
|
||||
# [accounts.notmuch.mailboxes]
|
||||
# "INBOX" = { query="tag:inbox", subscribe = true }
|
||||
# "Drafts" = { query="tag:draft", subscribe = true }
|
||||
# "Sent" = { query="from:username@server.tld from:username2@server.tld", subscribe = true }
|
||||
#
|
||||
## Setting up a Gmail account
|
||||
#[accounts."gmail"]
|
||||
#root_mailbox = '[Gmail]'
|
||||
|
@ -68,8 +68,8 @@
|
|||
#server_password="password"
|
||||
#server_username="username@gmail.com"
|
||||
#server_port="993"
|
||||
#listing.index_style = "Conversations"
|
||||
#identity = "username@gmail.com"
|
||||
#index_style = "Conversations"
|
||||
#identity = "username@server.tld"
|
||||
#display_name = "Name Name"
|
||||
### match every mailbox:
|
||||
#subscribed_mailboxes = ["*" ]
|
||||
|
@ -77,21 +77,11 @@
|
|||
### 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"
|
||||
#pager_context = 0 # default, optional
|
||||
#sticky_headers = true # default, optional
|
||||
#headers_sticky = true # default, optional
|
||||
#
|
||||
#[notifications]
|
||||
#script = "notify-send"
|
||||
|
@ -101,7 +91,11 @@
|
|||
#
|
||||
###shortcuts
|
||||
#[shortcuts.composing]
|
||||
#edit = 'e'
|
||||
#edit_mail = 'e'
|
||||
#
|
||||
##Thread view defaults:
|
||||
#[shortcuts.compact-listing]
|
||||
#exit_thread = 'i'
|
||||
#
|
||||
#[shortcuts.contact-list]
|
||||
#create_contact = 'c'
|
||||
|
@ -117,7 +111,6 @@
|
|||
#next_account = 'h'
|
||||
#new_mail = 'm'
|
||||
#set_seen = 'n'
|
||||
#exit_entry = 'i'
|
||||
#
|
||||
##Pager defaults
|
||||
#
|
||||
|
@ -130,7 +123,7 @@
|
|||
#[composing]
|
||||
##required for sending e-mail
|
||||
#send_mail = 'msmtp --read-recipients --read-envelope-from'
|
||||
##send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
|
||||
##send_mail = { hostname = "smtp.mail.tld", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
|
||||
#editor_command = 'vim +/^$' # optional, by default $EDITOR is used.
|
||||
#
|
||||
#
|
||||
|
|
|
@ -14,9 +14,11 @@ color_aliases = { "neon_green" = "#6ef9d4", "darkgrey" = "#4a4a4a", "neon_purp
|
|||
"mail.listing.conversations.date" = { fg = "$neon_purple", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.from" = { fg = "$darkgrey", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.highlighted" = { fg = "theme_default", bg = "Grey58", attrs = "theme_default" }
|
||||
"mail.listing.conversations.padding" = { fg = "Black", bg = "Black", attrs = "theme_default" }
|
||||
"mail.listing.conversations.selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
|
||||
"mail.listing.conversations.subject" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.unseen" = { fg = "Black", bg = "Grey78", attrs = "theme_default" }
|
||||
"mail.listing.conversations.unseen_padding" = { fg = "Black", bg = "Black", attrs = "theme_default" }
|
||||
"mail.listing.plain.even" = { fg = "theme_default", bg = "Grey19", attrs = "theme_default" }
|
||||
"mail.listing.plain.even_highlighted" = { fg = "theme_default", bg = "Grey58", attrs = "theme_default" }
|
||||
"mail.listing.plain.even_selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
|
||||
|
|
|
@ -13,9 +13,11 @@
|
|||
"mail.listing.conversations" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.from" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.highlighted" = { fg = "theme_default", bg = "Grey58", attrs = "theme_default" }
|
||||
"mail.listing.conversations.padding" = { fg = "Grey15", bg = "Grey15", attrs = "theme_default" }
|
||||
"mail.listing.conversations.selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
|
||||
"mail.listing.conversations.subject" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.unseen" = { fg = "Black", bg = "Grey78", attrs = "theme_default" }
|
||||
"mail.listing.conversations.unseen_padding" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.plain.even" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.plain.odd" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.plain.even_unseen" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
|
|
|
@ -16,9 +16,11 @@ color_aliases = { "JewelGreen" = "#157241", "PinkLace" = "#FFD5FD", "TorchRed" =
|
|||
"mail.listing.conversations" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.from" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.highlighted" = { fg = "$JewelGreen", bg = "$SunsetOrange", attrs = "theme_default" }
|
||||
"mail.listing.conversations.padding" = { fg = "$TorchRed", bg = "Grey15", attrs = "theme_default" }
|
||||
"mail.listing.conversations.selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
|
||||
"mail.listing.conversations.subject" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.conversations.unseen" = { fg = "Black", bg = "mail.listing.compact.even_unseen", attrs = "theme_default" }
|
||||
"mail.listing.conversations.unseen_padding" = { fg = "$BlueStone", bg = "mail.listing.conversations.unseen", attrs = "theme_default" }
|
||||
"mail.listing.plain.even" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.plain.odd" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.plain.even_unseen" = { fg = "theme_default", bg = "mail.listing.compact.even_unseen", attrs = "theme_default" }
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,17 +1,16 @@
|
|||
[package]
|
||||
name = "melib"
|
||||
version = "0.7.2"
|
||||
version = "0.6.2"
|
||||
authors = ["Manos Pitsidianakis <epilys@nessuent.xyz>"]
|
||||
workspace = ".."
|
||||
edition = "2018"
|
||||
build = "build.rs"
|
||||
rust-version = "1.65.0"
|
||||
|
||||
homepage = "https://meli.delivery"
|
||||
repository = "https://git.meli.delivery/meli/meli.git"
|
||||
description = "mail library"
|
||||
keywords = ["mail", "mua", "maildir", "imap", "jmap"]
|
||||
categories = ["email", "parser-implementations"]
|
||||
categories = [ "email", "parser-implementations"]
|
||||
license = "GPL-3.0-or-later"
|
||||
readme = "README.md"
|
||||
|
||||
|
@ -20,65 +19,46 @@ name = "melib"
|
|||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
async-stream = "^0.3"
|
||||
base64 = { version = "^0.13", optional = true }
|
||||
bitflags = "1.0"
|
||||
data-encoding = { version = "2.1.1" }
|
||||
encoding = { version = "0.2.33", default-features = false }
|
||||
encoding_rs = { version = "^0.8" }
|
||||
flate2 = { version = "1.0.16", optional = true }
|
||||
futures = "0.3.5"
|
||||
data-encoding = "2.1.1"
|
||||
encoding = "0.2.33"
|
||||
nom = { version = "5.1.1" }
|
||||
|
||||
indexmap = { version = "^1.5", default-features = false, features = ["serde-1", ] }
|
||||
isahc = { version = "^1.7.2", optional = true, default-features = false, features = ["http2", "json", "text-decoding"] }
|
||||
libc = { version = "0.2.125", features = ["extra_traits",] }
|
||||
|
||||
libloading = "^0.7"
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
native-tls = { version = "0.2.3", default-features = false, optional = true }
|
||||
nix = "^0.24"
|
||||
nom = { version = "7" }
|
||||
indexmap = { version = "^1.5", features = ["serde-1", ] }
|
||||
notify = { version = "4.0.15", optional = true }
|
||||
regex = { version = "1" }
|
||||
rusqlite = { version = "^0.28", default-features = false, optional = true }
|
||||
xdg = "2.1.0"
|
||||
native-tls = { version ="0.2.3", optional=true }
|
||||
serde = { version = "1.0.71", features = ["rc", ] }
|
||||
serde_derive = "1.0.71"
|
||||
serde_json = { version = "1.0", features = ["raw_value",] }
|
||||
bincode = "^1.3.0"
|
||||
uuid = { version = "0.8.1", features = ["serde", "v4", "v5"] }
|
||||
|
||||
unicode-segmentation = { version = "1.2.1", optional = true }
|
||||
libc = {version = "0.2.59", features = ["extra_traits",]}
|
||||
isahc = { version = "0.9.7", optional = true, default-features = false, features = ["http2", "json", "text-decoding"]}
|
||||
serde_json = { version = "1.0", optional = true, features = ["raw_value",] }
|
||||
smallvec = { version = "^1.5.0", features = ["serde", ] }
|
||||
nix = "0.17.0"
|
||||
rusqlite = {version = "0.24.0", optional = true }
|
||||
|
||||
libloading = "0.6.2"
|
||||
futures = "0.3.5"
|
||||
smol = "1.0.0"
|
||||
|
||||
unicode-segmentation = { version = "1.2.1", default-features = false, optional = true }
|
||||
uuid = { version = "^1", features = ["serde", "v4", "v5"] }
|
||||
xdg = "2.1.0"
|
||||
async-stream = "0.2.1"
|
||||
base64 = { version = "0.12.3", optional = true }
|
||||
flate2 = { version = "1.0.16", optional = true }
|
||||
xdg-utils = "^0.4.0"
|
||||
|
||||
[dependencies.imap-codec]
|
||||
version = "0.10.0"
|
||||
features = [
|
||||
"ext_condstore_qresync",
|
||||
"ext_enable",
|
||||
"ext_idle",
|
||||
"ext_literal",
|
||||
"ext_move",
|
||||
"ext_sasl_ir",
|
||||
"ext_unselect"
|
||||
]
|
||||
optional = true
|
||||
|
||||
[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"]
|
||||
|
||||
debug-tracing = []
|
||||
deflate_compression = ["flate2", "imap-codec/ext_compress"]
|
||||
deflate_compression = ["flate2", ]
|
||||
gpgme = []
|
||||
http = ["isahc"]
|
||||
http-static = ["isahc", "isahc/static-curl"]
|
||||
imap_backend = ["imap-codec", "tls"]
|
||||
jmap_backend = ["http"]
|
||||
imap_backend = ["tls"]
|
||||
jmap_backend = ["http", "serde_json"]
|
||||
maildir_backend = ["notify"]
|
||||
mbox_backend = ["notify"]
|
||||
notmuch_backend = []
|
||||
|
|
|
@ -29,11 +29,11 @@ and body structure. Addresses in `To`, `From` fields etc are parsed into
|
|||
```rust
|
||||
use melib::{Attachment, Envelope};
|
||||
|
||||
let raw_mail = r#"From: "some name" <some@example.com>
|
||||
To: "me" <myself@example.com>
|
||||
let raw_mail = r#"From: "some name" <some@address.com>
|
||||
To: "me" <myself@i.tld>
|
||||
Cc:
|
||||
Subject: =?utf-8?Q?gratuitously_encoded_subject?=
|
||||
Message-ID: <h2g7f.z0gy2pgaen5m@example.com>
|
||||
Message-ID: <h2g7f.z0gy2pgaen5m@address.com>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed; charset="utf-8";
|
||||
boundary="bzz_bzz__bzz__"
|
||||
|
@ -74,7 +74,7 @@ ouiijDaaCCGQRgrpH3q4QYYXWDihxBE+7KCDDjnUIEVAADs=
|
|||
|
||||
let envelope = Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
|
||||
assert_eq!(envelope.subject().as_ref(), "gratuitously encoded subject");
|
||||
assert_eq!(envelope.message_id_display().as_ref(), "<h2g7f.z0gy2pgaen5m@example.com>");
|
||||
assert_eq!(envelope.message_id_display().as_ref(), "<h2g7f.z0gy2pgaen5m@address.com>");
|
||||
|
||||
let body = envelope.body_bytes(raw_mail.as_bytes());
|
||||
assert_eq!(body.content_type().to_string().as_str(), "multipart/mixed");
|
||||
|
|
|
@ -19,8 +19,6 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#![allow(clippy::needless_range_loop)]
|
||||
|
||||
#[cfg(feature = "unicode_algorithms")]
|
||||
include!("src/text_processing/types.rs");
|
||||
|
||||
|
@ -31,12 +29,11 @@ fn main() -> Result<(), std::io::Error> {
|
|||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed={}", MOD_PATH);
|
||||
/* Line break tables */
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{prelude::*, BufReader},
|
||||
path::Path,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::io::BufReader;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
const LINE_BREAK_TABLE_URL: &str =
|
||||
"http://www.unicode.org/Public/UCD/latest/ucd/LineBreak.txt";
|
||||
/* Grapheme width tables */
|
||||
|
@ -46,6 +43,7 @@ fn main() -> Result<(), std::io::Error> {
|
|||
const EMOJI_DATA_URL: &str =
|
||||
"https://www.unicode.org/Public/UCD/latest/ucd/emoji/emoji-data.txt";
|
||||
|
||||
|
||||
let mod_path = Path::new(MOD_PATH);
|
||||
if mod_path.exists() {
|
||||
eprintln!(
|
||||
|
@ -55,7 +53,7 @@ fn main() -> Result<(), std::io::Error> {
|
|||
std::process::exit(0);
|
||||
}
|
||||
let mut child = Command::new("curl")
|
||||
.args(["-o", "-", LINE_BREAK_TABLE_URL])
|
||||
.args(&["-o", "-", LINE_BREAK_TABLE_URL])
|
||||
.stdout(Stdio::piped())
|
||||
.stdin(Stdio::null())
|
||||
.stderr(Stdio::inherit())
|
||||
|
@ -72,8 +70,7 @@ fn main() -> Result<(), std::io::Error> {
|
|||
let tokens: &str = line.split_whitespace().next().unwrap();
|
||||
|
||||
let semicolon_idx: usize = tokens.chars().position(|c| c == ';').unwrap();
|
||||
/* LineBreak.txt list is ascii encoded so we can assume each char takes one
|
||||
* byte: */
|
||||
/* LineBreak.txt list is ascii encoded so we can assume each char takes one byte: */
|
||||
let chars_str: &str = &tokens[..semicolon_idx];
|
||||
|
||||
let mut codepoint_iter = chars_str.split("..");
|
||||
|
@ -91,21 +88,21 @@ fn main() -> Result<(), std::io::Error> {
|
|||
child.wait()?;
|
||||
|
||||
let child = Command::new("curl")
|
||||
.args(["-o", "-", UNICODE_DATA_URL])
|
||||
.args(&["-o", "-", UNICODE_DATA_URL])
|
||||
.stdout(Stdio::piped())
|
||||
.output()?;
|
||||
|
||||
let unicode_data = String::from_utf8_lossy(&child.stdout);
|
||||
|
||||
let child = Command::new("curl")
|
||||
.args(["-o", "-", EAW_URL])
|
||||
.args(&["-o", "-", EAW_URL])
|
||||
.stdout(Stdio::piped())
|
||||
.output()?;
|
||||
|
||||
let eaw_data = String::from_utf8_lossy(&child.stdout);
|
||||
|
||||
let child = Command::new("curl")
|
||||
.args(["-o", "-", EMOJI_DATA_URL])
|
||||
.args(&["-o", "-", EMOJI_DATA_URL])
|
||||
.stdout(Stdio::piped())
|
||||
.output()?;
|
||||
|
||||
|
@ -165,9 +162,9 @@ fn main() -> Result<(), std::io::Error> {
|
|||
}
|
||||
}
|
||||
|
||||
fn set_general_categories<'u>(codepoints: &mut [Codepoint<'u>], unicode_data: &'u str) {
|
||||
fn set_general_categories<'u>(codepoints: &mut Vec<Codepoint<'u>>, unicode_data: &'u str) {
|
||||
for line in unicode_data.lines() {
|
||||
let fields = line.trim().split(';').collect::<Vec<_>>();
|
||||
let fields = line.trim().split(";").collect::<Vec<_>>();
|
||||
if fields.len() > FIELD_CATEGORY {
|
||||
for idx in hexrange_to_range(fields[FIELD_CODEPOINT]) {
|
||||
codepoints[idx].category = fields[FIELD_CATEGORY];
|
||||
|
@ -176,7 +173,7 @@ fn main() -> Result<(), std::io::Error> {
|
|||
}
|
||||
}
|
||||
|
||||
fn set_eaw_widths(codepoints: &mut [Codepoint<'_>], eaw_data_lines: &str) {
|
||||
fn set_eaw_widths(codepoints: &mut Vec<Codepoint<'_>>, eaw_data_lines: &str) {
|
||||
// Read from EastAsianWidth.txt, set width values on the codepoints
|
||||
for line in eaw_data_lines.lines() {
|
||||
let line = line.trim().split('#').next().unwrap_or(line);
|
||||
|
@ -202,13 +199,13 @@ fn main() -> Result<(), std::io::Error> {
|
|||
}
|
||||
// Apply the following special cases:
|
||||
// - The unassigned code points in the following blocks default to "W":
|
||||
// - CJK Unified Ideographs Extension A: U+3400..U+4DBF
|
||||
// - CJK Unified Ideographs: U+4E00..U+9FFF
|
||||
// - CJK Compatibility Ideographs: U+F900..U+FAFF
|
||||
// - All undesignated code points in Planes 2 and 3, whether inside or outside
|
||||
// of allocated blocks, default to "W":
|
||||
// - Plane 2: U+20000..U+2FFFD
|
||||
// - Plane 3: U+30000..U+3FFFD
|
||||
// CJK Unified Ideographs Extension A: U+3400..U+4DBF
|
||||
// CJK Unified Ideographs: U+4E00..U+9FFF
|
||||
// CJK Compatibility Ideographs: U+F900..U+FAFF
|
||||
// - All undesignated code points in Planes 2 and 3, whether inside or
|
||||
// outside of allocated blocks, default to "W":
|
||||
// Plane 2: U+20000..U+2FFFD
|
||||
// Plane 3: U+30000..U+3FFFD
|
||||
const WIDE_RANGES: [(usize, usize); 5] = [
|
||||
(0x3400, 0x4DBF),
|
||||
(0x4E00, 0x9FFF),
|
||||
|
@ -224,11 +221,10 @@ fn main() -> Result<(), std::io::Error> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_emoji_widths(codepoints: &mut [Codepoint<'_>], emoji_data_lines: &str) {
|
||||
fn set_emoji_widths(codepoints: &mut Vec<Codepoint<'_>>, emoji_data_lines: &str) {
|
||||
// Read from emoji-data.txt, set codepoint widths
|
||||
for line in emoji_data_lines.lines() {
|
||||
if !line.contains('#') || line.trim().starts_with('#') {
|
||||
if !line.contains("#") || line.trim().starts_with("#") {
|
||||
continue;
|
||||
}
|
||||
let mut fields = line.trim().split('#').collect::<Vec<_>>();
|
||||
|
@ -238,7 +234,7 @@ fn main() -> Result<(), std::io::Error> {
|
|||
let comment = fields.pop().unwrap();
|
||||
let fields = fields.pop().unwrap();
|
||||
|
||||
let hexrange = fields.split(';').next().unwrap();
|
||||
let hexrange = fields.split(";").next().unwrap();
|
||||
|
||||
// In later versions of emoji-data.txt there are some "reserved"
|
||||
// entries that have "NA" instead of a Unicode version number
|
||||
|
@ -249,12 +245,12 @@ fn main() -> Result<(), std::io::Error> {
|
|||
}
|
||||
|
||||
use std::str::FromStr;
|
||||
let mut v = comment.split_whitespace().next().unwrap();
|
||||
if v.starts_with('E') {
|
||||
let mut v = comment.trim().split_whitespace().next().unwrap();
|
||||
if v.starts_with("E") {
|
||||
v = &v[1..];
|
||||
}
|
||||
if v.as_bytes()
|
||||
.first()
|
||||
.get(0)
|
||||
.map(|c| !c.is_ascii_digit())
|
||||
.unwrap_or(true)
|
||||
{
|
||||
|
@ -307,7 +303,7 @@ fn main() -> Result<(), std::io::Error> {
|
|||
}
|
||||
}
|
||||
}
|
||||
fn set_hardcoded_ranges(codepoints: &mut [Codepoint<'_>]) {
|
||||
fn set_hardcoded_ranges(codepoints: &mut Vec<Codepoint<'_>>) {
|
||||
// Mark private use and surrogate codepoints
|
||||
// Private use can be determined awkwardly from UnicodeData.txt,
|
||||
// but we just hard-code them.
|
||||
|
@ -329,7 +325,7 @@ fn main() -> Result<(), std::io::Error> {
|
|||
}
|
||||
}
|
||||
|
||||
let mut file = File::create(mod_path)?;
|
||||
let mut file = File::create(&mod_path)?;
|
||||
file.write_all(
|
||||
br#"/*
|
||||
* meli - text_processing crate.
|
||||
|
|
|
@ -22,16 +22,11 @@
|
|||
#[cfg(feature = "vcard")]
|
||||
pub mod vcard;
|
||||
|
||||
pub mod mutt;
|
||||
|
||||
use std::{collections::HashMap, ops::Deref};
|
||||
|
||||
use crate::datetime::{self, UnixTimestamp};
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::utils::{
|
||||
datetime::{self, UnixTimestamp},
|
||||
parsec::Parser,
|
||||
};
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(Hash, Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
|
||||
#[serde(from = "String")]
|
||||
|
@ -41,42 +36,27 @@ pub enum CardId {
|
|||
Hash(u64),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CardId {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
impl Into<String> for CardId {
|
||||
fn into(self) -> String {
|
||||
match self {
|
||||
Self::Uuid(u) => u.as_hyphenated().fmt(fmt),
|
||||
Self::Hash(u) => u.fmt(fmt),
|
||||
CardId::Uuid(u) => u.to_string(),
|
||||
CardId::Hash(u) => u.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CardId> for String {
|
||||
fn from(val: CardId) -> Self {
|
||||
val.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for CardId {
|
||||
fn from(s: String) -> Self {
|
||||
use std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
if let Ok(u) = Uuid::parse_str(s.as_str()) {
|
||||
Self::Uuid(u)
|
||||
} else if let Ok(num) = u64::from_str(s.trim()) {
|
||||
Self::Hash(num)
|
||||
fn from(s: String) -> CardId {
|
||||
if let Ok(u) = uuid::Uuid::parse_str(s.as_str()) {
|
||||
CardId::Uuid(u)
|
||||
} else {
|
||||
let mut hasher = DefaultHasher::default();
|
||||
s.hash(&mut hasher);
|
||||
Self::Hash(hasher.finish())
|
||||
use std::str::FromStr;
|
||||
CardId::Hash(u64::from_str(&s).unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct AddressBook {
|
||||
display_name: String,
|
||||
created: UnixTimestamp,
|
||||
|
@ -84,7 +64,7 @@ pub struct AddressBook {
|
|||
pub cards: HashMap<CardId, Card>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Card {
|
||||
id: CardId,
|
||||
title: String,
|
||||
|
@ -102,14 +82,13 @@ pub struct Card {
|
|||
last_edited: UnixTimestamp,
|
||||
extra_properties: HashMap<String, String>,
|
||||
|
||||
/// If true, we can't make any changes because we do not manage this
|
||||
/// resource.
|
||||
/// If true, we can't make any changes because we do not manage this resource.
|
||||
external_resource: bool,
|
||||
}
|
||||
|
||||
impl AddressBook {
|
||||
pub fn new(display_name: String) -> Self {
|
||||
Self {
|
||||
pub fn new(display_name: String) -> AddressBook {
|
||||
AddressBook {
|
||||
display_name,
|
||||
created: datetime::now(),
|
||||
last_edited: datetime::now(),
|
||||
|
@ -117,46 +96,23 @@ impl AddressBook {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn with_account(s: &crate::conf::AccountSettings) -> Self {
|
||||
let mut ret = Self::new(s.name.clone());
|
||||
if let Some(mutt_alias_file) = s.extra.get("mutt_alias_file").map(String::as_str) {
|
||||
match std::fs::read_to_string(std::path::Path::new(mutt_alias_file))
|
||||
.map_err(|err| err.to_string())
|
||||
.and_then(|contents| {
|
||||
contents
|
||||
.lines()
|
||||
.map(|line| mutt::parse_mutt_contact().parse(line).map(|(_, c)| c))
|
||||
.collect::<Result<Vec<Card>, &str>>()
|
||||
.map_err(|err| err.to_string())
|
||||
}) {
|
||||
Ok(cards) => {
|
||||
for c in cards {
|
||||
ret.add_card(c);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"Could not load mutt alias file {:?}: {}",
|
||||
mutt_alias_file,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
pub fn with_account(s: &crate::conf::AccountSettings) -> AddressBook {
|
||||
#[cfg(not(feature = "vcard"))]
|
||||
{
|
||||
AddressBook::new(s.name.clone())
|
||||
}
|
||||
#[cfg(feature = "vcard")]
|
||||
if let Some(vcard_path) = s.vcard_folder() {
|
||||
match vcard::load_cards(std::path::Path::new(vcard_path)) {
|
||||
Ok(cards) => {
|
||||
{
|
||||
let mut ret = AddressBook::new(s.name.clone());
|
||||
if let Some(vcard_path) = s.vcard_folder() {
|
||||
if let Ok(cards) = vcard::load_cards(&std::path::Path::new(vcard_path)) {
|
||||
for c in cards {
|
||||
ret.add_card(c);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("Could not load vcards from {:?}: {}", vcard_path, err);
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn add_card(&mut self, card: Card) {
|
||||
|
@ -186,8 +142,8 @@ impl Deref for AddressBook {
|
|||
}
|
||||
|
||||
impl Card {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
pub fn new() -> Card {
|
||||
Card {
|
||||
id: CardId::Uuid(Uuid::new_v4()),
|
||||
title: String::new(),
|
||||
name: String::new(),
|
||||
|
@ -236,57 +192,39 @@ impl Card {
|
|||
self.key.as_str()
|
||||
}
|
||||
pub fn last_edited(&self) -> String {
|
||||
datetime::timestamp_to_string(self.last_edited, None, false)
|
||||
datetime::timestamp_to_string(self.last_edited, None)
|
||||
}
|
||||
|
||||
pub fn set_id(&mut self, new_val: CardId) -> &mut Self {
|
||||
pub fn set_id(&mut self, new_val: CardId) {
|
||||
self.id = new_val;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_title(&mut self, new: String) -> &mut Self {
|
||||
pub fn set_title(&mut self, new: String) {
|
||||
self.title = new;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_name(&mut self, new: String) -> &mut Self {
|
||||
pub fn set_name(&mut self, new: String) {
|
||||
self.name = new;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_additionalname(&mut self, new: String) -> &mut Self {
|
||||
pub fn set_additionalname(&mut self, new: String) {
|
||||
self.additionalname = new;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_name_prefix(&mut self, new: String) -> &mut Self {
|
||||
pub fn set_name_prefix(&mut self, new: String) {
|
||||
self.name_prefix = new;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_name_suffix(&mut self, new: String) -> &mut Self {
|
||||
pub fn set_name_suffix(&mut self, new: String) {
|
||||
self.name_suffix = new;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_email(&mut self, new: String) -> &mut Self {
|
||||
pub fn set_email(&mut self, new: String) {
|
||||
self.email = new;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_url(&mut self, new: String) -> &mut Self {
|
||||
pub fn set_url(&mut self, new: String) {
|
||||
self.url = new;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_key(&mut self, new: String) -> &mut Self {
|
||||
pub fn set_key(&mut self, new: String) {
|
||||
self.key = new;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_extra_property(&mut self, key: &str, value: String) -> &mut Self {
|
||||
pub fn set_extra_property(&mut self, key: &str, value: String) {
|
||||
self.extra_properties.insert(key.to_string(), value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn extra_property(&self, key: &str) -> Option<&str> {
|
||||
|
@ -297,9 +235,8 @@ impl Card {
|
|||
&self.extra_properties
|
||||
}
|
||||
|
||||
pub fn set_external_resource(&mut self, new_val: bool) -> &mut Self {
|
||||
pub fn set_external_resource(&mut self, new_val: bool) {
|
||||
self.external_resource = new_val;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn external_resource(&self) -> bool {
|
||||
|
@ -308,8 +245,8 @@ impl Card {
|
|||
}
|
||||
|
||||
impl From<HashMap<String, String>> for Card {
|
||||
fn from(mut map: HashMap<String, String>) -> Self {
|
||||
let mut card = Self::new();
|
||||
fn from(mut map: HashMap<String, String>) -> Card {
|
||||
let mut card = Card::new();
|
||||
if let Some(val) = map.remove("TITLE") {
|
||||
card.title = val;
|
||||
}
|
||||
|
|
|
@ -1,106 +0,0 @@
|
|||
/*
|
||||
* meli - addressbook module
|
||||
*
|
||||
* Copyright 2019 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/>.
|
||||
*/
|
||||
|
||||
//! # Mutt contact formats
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use super::*;
|
||||
use crate::utils::parsec::{is_not, map_res, match_literal_anycase, prefix, Parser};
|
||||
|
||||
//alias <nickname> [ <long name> ] <address>
|
||||
// From mutt doc:
|
||||
//
|
||||
// ```text
|
||||
// Since the name can consist of several whitespace-separated words, the
|
||||
// last word is considered the address, and it can be optionally enclosed
|
||||
// between angle brackets.
|
||||
// For example: alias mumon My dear pupil Mumon foobar@example.com
|
||||
// will be parsed in this way:
|
||||
//
|
||||
// alias mumon My dear pupil Mumon foobar@example.com
|
||||
// ^ ^ ^
|
||||
// nickname long name email address
|
||||
// The nickname (or alias) will be used to select a corresponding long name
|
||||
// and email address when specifying the To field of an outgoing message,
|
||||
// e.g. when using the function in the browser or index context.
|
||||
// The long name is optional, so you can specify an alias command in this
|
||||
// way:
|
||||
//
|
||||
// alias mumon foobar@example.com
|
||||
// ^ ^
|
||||
// nickname email address
|
||||
// ```
|
||||
pub fn parse_mutt_contact<'a>() -> impl Parser<'a, Card> {
|
||||
move |input| {
|
||||
map_res(
|
||||
prefix(match_literal_anycase("alias "), is_not(b"\r\n")),
|
||||
|l| {
|
||||
let mut tokens = l.split_whitespace().collect::<VecDeque<&str>>();
|
||||
|
||||
let mut ret = Card::new();
|
||||
let title = tokens.pop_front().ok_or(l)?.to_string();
|
||||
let mut email = tokens.pop_back().ok_or(l)?.to_string();
|
||||
if email.starts_with('<') && email.ends_with('>') {
|
||||
email.pop();
|
||||
email.remove(0);
|
||||
}
|
||||
let mut name = tokens.into_iter().fold(String::new(), |mut acc, el| {
|
||||
acc.push_str(el);
|
||||
acc.push(' ');
|
||||
acc
|
||||
});
|
||||
name.pop();
|
||||
if name.trim().is_empty() {
|
||||
name = title.clone();
|
||||
}
|
||||
ret.set_title(title).set_email(email).set_name(name);
|
||||
Ok::<Card, &'a str>(ret)
|
||||
},
|
||||
)
|
||||
.parse(input)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mutt_contacts() {
|
||||
let a = "alias mumon My dear pupil Mumon foobar@example.com";
|
||||
let b = "alias mumon foobar@example.com";
|
||||
let c = "alias <nickname> <long name> <address>";
|
||||
|
||||
let (other, a_card) = parse_mutt_contact().parse(a).unwrap();
|
||||
assert!(other.is_empty());
|
||||
assert_eq!(a_card.name(), "My dear pupil Mumon");
|
||||
assert_eq!(a_card.title(), "mumon");
|
||||
assert_eq!(a_card.email(), "foobar@example.com");
|
||||
|
||||
let (other, b_card) = parse_mutt_contact().parse(b).unwrap();
|
||||
assert!(other.is_empty());
|
||||
assert_eq!(b_card.name(), "mumon");
|
||||
assert_eq!(b_card.title(), "mumon");
|
||||
assert_eq!(b_card.email(), "foobar@example.com");
|
||||
|
||||
let (other, c_card) = parse_mutt_contact().parse(c).unwrap();
|
||||
assert!(other.is_empty());
|
||||
assert_eq!(c_card.name(), "<long name>");
|
||||
assert_eq!(c_card.title(), "<nickname>");
|
||||
assert_eq!(c_card.email(), "address");
|
||||
}
|
|
@ -19,21 +19,12 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! # vCard format
|
||||
//!
|
||||
//! This module implements the standards:
|
||||
//!
|
||||
//! - Version 3 (read-only) [RFC 2426: vCard MIME Directory Profile](https://datatracker.ietf.org/doc/2426)
|
||||
//! - Version 4 [RFC 6350: vCard Format Specification](https://datatracker.ietf.org/doc/rfc6350/)
|
||||
//! - Parameter escaping [RFC 6868 Parameter Value Encoding in iCalendar and vCard](https://datatracker.ietf.org/doc/rfc6868/)
|
||||
|
||||
use std::{collections::HashMap, convert::TryInto};
|
||||
|
||||
/// Convert VCard strings to meli Cards (contacts).
|
||||
use super::*;
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
utils::parsec::{match_literal_anycase, one_or_more, peek, prefix, take_until, Parser},
|
||||
};
|
||||
use crate::error::{MeliError, Result};
|
||||
use crate::parsec::{match_literal_anycase, one_or_more, peek, prefix, take_until, Parser};
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
|
||||
/* Supported vcard versions */
|
||||
pub trait VCardVersion: core::fmt::Debug {}
|
||||
|
@ -42,24 +33,20 @@ pub trait VCardVersion: core::fmt::Debug {}
|
|||
pub struct VCardVersionUnknown;
|
||||
impl VCardVersion for VCardVersionUnknown {}
|
||||
|
||||
/// Version 4 <https://tools.ietf.org/html/rfc6350>
|
||||
/// https://tools.ietf.org/html/rfc6350
|
||||
#[derive(Debug)]
|
||||
pub struct VCardVersion4;
|
||||
impl VCardVersion for VCardVersion4 {}
|
||||
|
||||
/// <https://tools.ietf.org/html/rfc2426>
|
||||
/// https://tools.ietf.org/html/rfc2426
|
||||
#[derive(Debug)]
|
||||
pub struct VCardVersion3;
|
||||
impl VCardVersion for VCardVersion3 {}
|
||||
|
||||
pub struct CardDeserializer;
|
||||
|
||||
const HEADER_CRLF: &str = "BEGIN:VCARD\r\n"; //VERSION:4.0\r\n";
|
||||
const FOOTER_CRLF: &str = "END:VCARD\r\n";
|
||||
const HEADER_LF: &str = "BEGIN:VCARD\n"; //VERSION:4.0\n";
|
||||
const FOOTER_LF: &str = "END:VCARD\n";
|
||||
const HEADER: &str = "BEGIN:VCARD"; //VERSION:4.0";
|
||||
const FOOTER: &str = "END:VCARD";
|
||||
static HEADER: &str = "BEGIN:VCARD\r\n"; //VERSION:4.0\r\n";
|
||||
static FOOTER: &str = "END:VCARD\r\n";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct VCard<T: VCardVersion>(
|
||||
|
@ -84,19 +71,11 @@ pub struct ContentLine {
|
|||
}
|
||||
|
||||
impl CardDeserializer {
|
||||
pub fn try_from_str(mut input: &str) -> Result<VCard<impl VCardVersion>> {
|
||||
input = if (!input.starts_with(HEADER_CRLF) || !input.ends_with(FOOTER_CRLF))
|
||||
&& (!input.starts_with(HEADER_LF) || !input.ends_with(FOOTER_LF))
|
||||
{
|
||||
return Err(Error::new(format!(
|
||||
"Error while parsing vcard: input does not start or end with correct header and \
|
||||
footer. input is:\n{:?}",
|
||||
input
|
||||
)));
|
||||
} else if input.starts_with(HEADER_CRLF) {
|
||||
&input[HEADER_CRLF.len()..input.len() - FOOTER_CRLF.len()]
|
||||
pub fn from_str(mut input: &str) -> Result<VCard<impl VCardVersion>> {
|
||||
input = if !input.starts_with(HEADER) || !input.ends_with(FOOTER) {
|
||||
return Err(MeliError::new(format!("Error while parsing vcard: input does not start or end with correct header and footer. input is:\n{:?}", input)));
|
||||
} else {
|
||||
&input[HEADER_LF.len()..input.len() - FOOTER_LF.len()]
|
||||
&input[HEADER.len()..input.len() - FOOTER.len()]
|
||||
};
|
||||
|
||||
let mut ret = HashMap::default();
|
||||
|
@ -153,13 +132,13 @@ impl CardDeserializer {
|
|||
}
|
||||
}
|
||||
if !has_colon {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"Error while parsing vcard: error at line {}, no colon. {:?}",
|
||||
l, el
|
||||
)));
|
||||
}
|
||||
if name.is_empty() {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"Error while parsing vcard: error at line {}, no name for content line. {:?}",
|
||||
l, el
|
||||
)));
|
||||
|
@ -172,7 +151,7 @@ impl CardDeserializer {
|
|||
}
|
||||
|
||||
impl<V: VCardVersion> TryInto<Card> for VCard<V> {
|
||||
type Error = crate::error::Error;
|
||||
type Error = crate::error::MeliError;
|
||||
|
||||
fn try_into(mut self) -> crate::error::Result<Card> {
|
||||
let mut card = Card::new();
|
||||
|
@ -193,7 +172,7 @@ impl<V: VCardVersion> TryInto<Card> for VCard<V> {
|
|||
if let Some(val) = self.0.remove("FN") {
|
||||
card.set_name(val.value);
|
||||
} else {
|
||||
return Err(Error::new("FN entry missing in VCard."));
|
||||
return Err(MeliError::new("FN entry missing in VCard."));
|
||||
}
|
||||
if let Some(val) = self.0.remove("NICKNAME") {
|
||||
card.set_additionalname(val.value);
|
||||
|
@ -222,9 +201,8 @@ impl<V: VCardVersion> TryInto<Card> for VCard<V> {
|
|||
T102200Z
|
||||
T102200-0800
|
||||
*/
|
||||
card.birthday =
|
||||
crate::utils::datetime::timestamp_from_string(val.value.as_str(), "%Y%m%d\0")
|
||||
.unwrap_or_default();
|
||||
card.birthday = crate::datetime::timestamp_from_string(val.value.as_str(), "%Y%m%d")
|
||||
.unwrap_or_default();
|
||||
}
|
||||
if let Some(val) = self.0.remove("EMAIL") {
|
||||
card.set_email(val.value);
|
||||
|
@ -270,7 +248,7 @@ fn test_load_cards() {
|
|||
for s in parse_card().parse(contents.as_str()).unwrap().1 {
|
||||
println!("");
|
||||
println!("{}", s);
|
||||
println!("{:?}", CardDeserializer::try_from_str(s));
|
||||
println!("{:?}", CardDeserializer::from_str(s));
|
||||
println!("");
|
||||
}
|
||||
*/
|
||||
|
@ -291,22 +269,17 @@ pub fn load_cards(p: &std::path::Path) -> Result<Vec<Card>> {
|
|||
use std::io::Read;
|
||||
contents.clear();
|
||||
std::fs::File::open(&f)?.read_to_string(&mut contents)?;
|
||||
match parse_card().parse(contents.as_str()) {
|
||||
Ok((_, c)) => {
|
||||
for s in c {
|
||||
ret.push(
|
||||
CardDeserializer::try_from_str(s)
|
||||
.and_then(TryInto::try_into)
|
||||
.map(|mut card| {
|
||||
Card::set_external_resource(&mut card, true);
|
||||
is_any_valid = true;
|
||||
card
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("Could not parse vcard from {}: {}", f.display(), err);
|
||||
if let Ok((_, c)) = parse_card().parse(contents.as_str()) {
|
||||
for s in c {
|
||||
ret.push(
|
||||
CardDeserializer::from_str(s)
|
||||
.and_then(TryInto::try_into)
|
||||
.and_then(|mut card| {
|
||||
Card::set_external_resource(&mut card, true);
|
||||
is_any_valid = true;
|
||||
Ok(card)
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -317,22 +290,16 @@ pub fn load_cards(p: &std::path::Path) -> Result<Vec<Card>> {
|
|||
debug!(&c);
|
||||
}
|
||||
}
|
||||
if is_any_valid {
|
||||
if !is_any_valid {
|
||||
ret.into_iter().collect::<Result<Vec<Card>>>()
|
||||
} else {
|
||||
ret.retain(Result::is_ok);
|
||||
ret.into_iter().collect::<Result<Vec<Card>>>()
|
||||
}
|
||||
ret.into_iter().collect::<Result<Vec<Card>>>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_card() {
|
||||
let j = "BEGIN:VCARD\r\nVERSION:4.0\r\nN:Gump;Forrest;;Mr.;\r\nFN:Forrest Gump\r\nORG:Bubba Gump Shrimp Co.\r\nTITLE:Shrimp Man\r\nPHOTO;MEDIATYPE=image/gif:http://www.example.com/dir_photos/my_photo.gif\r\nTEL;TYPE=work,voice;VALUE=uri:tel:+1-111-555-1212\r\nTEL;TYPE=home,voice;VALUE=uri:tel:+1-404-555-1212\r\nADR;TYPE=WORK;PREF=1;LABEL=\"100 Waters Edge\\nBaytown\\, LA 30314\\nUnited States of America\":;;100 Waters Edge;Baytown;LA;30314;United States of America\r\nADR;TYPE=HOME;LABEL=\"42 Plantation St.\\nBaytown\\, LA 30314\\nUnited States of America\":;;42 Plantation St.;Baytown;LA;30314;United States of America\r\nEMAIL:forrestgump@example.com\r\nREV:20080424T195243Z\r\nx-qq:21588891\r\nEND:VCARD\r\n";
|
||||
println!(
|
||||
"results = {:#?}",
|
||||
CardDeserializer::try_from_str(j).unwrap()
|
||||
);
|
||||
let j = "BEGIN:VCARD\nVERSION:4.0\nN:Gump;Forrest;;Mr.;\nFN:Forrest Gump\nORG:Bubba Gump Shrimp Co.\nTITLE:Shrimp Man\nPHOTO;MEDIATYPE=image/gif:http://www.example.com/dir_photos/my_photo.gif\nTEL;TYPE=work,voice;VALUE=uri:tel:+1-111-555-1212\nTEL;TYPE=home,voice;VALUE=uri:tel:+1-404-555-1212\nADR;TYPE=WORK;PREF=1;LABEL=\"100 Waters Edge\\nBaytown\\, LA 30314\\nUnited States of America\":;;100 Waters Edge;Baytown;LA;30314;United States of America\nADR;TYPE=HOME;LABEL=\"42 Plantation St.\\nBaytown\\, LA 30314\\nUnited States of America\":;;42 Plantation St.;Baytown;LA;30314;United States of America\nEMAIL:forrestgump@example.com\nREV:20080424T195243Z\nx-qq:21588891\nEND:VCARD\n";
|
||||
println!(
|
||||
"results = {:#?}",
|
||||
CardDeserializer::try_from_str(j).unwrap()
|
||||
);
|
||||
println!("results = {:#?}", CardDeserializer::from_str(j).unwrap());
|
||||
}
|
||||
|
|
|
@ -19,8 +19,17 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
pub mod utf7;
|
||||
use smallvec::SmallVec;
|
||||
#[macro_export]
|
||||
macro_rules! tag_hash {
|
||||
($tag:ident) => {{
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::Hasher;
|
||||
let mut hasher = DefaultHasher::new();
|
||||
hasher.write($tag.as_bytes());
|
||||
hasher.finish()
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap_backend")]
|
||||
pub mod imap;
|
||||
|
@ -36,42 +45,36 @@ pub mod jmap;
|
|||
pub mod maildir;
|
||||
#[cfg(feature = "mbox_backend")]
|
||||
pub mod mbox;
|
||||
use std::{
|
||||
any::Any,
|
||||
borrow::Cow,
|
||||
collections::{BTreeSet, HashMap},
|
||||
fmt,
|
||||
fmt::Debug,
|
||||
future::Future,
|
||||
ops::Deref,
|
||||
pin::Pin,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use futures::stream::Stream;
|
||||
|
||||
#[cfg(feature = "imap_backend")]
|
||||
pub use self::imap::ImapType;
|
||||
#[cfg(feature = "imap_backend")]
|
||||
pub use self::nntp::NntpType;
|
||||
use crate::conf::AccountSettings;
|
||||
use crate::error::{MeliError, Result};
|
||||
|
||||
#[cfg(feature = "maildir_backend")]
|
||||
use self::maildir::MaildirType;
|
||||
#[cfg(feature = "mbox_backend")]
|
||||
use self::mbox::MboxType;
|
||||
#[cfg(feature = "imap_backend")]
|
||||
pub use self::nntp::NntpType;
|
||||
use super::email::{Envelope, EnvelopeHash, Flag};
|
||||
use crate::{
|
||||
conf::AccountSettings,
|
||||
error::{Error, ErrorKind, Result},
|
||||
LogLevel,
|
||||
};
|
||||
use std::any::Any;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt;
|
||||
use std::fmt::Debug;
|
||||
use std::ops::Deref;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
pub use futures::stream::Stream;
|
||||
use std::future::Future;
|
||||
pub use std::pin::Pin;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! get_path_hash {
|
||||
($path:expr) => {{
|
||||
use std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut hasher = DefaultHasher::new();
|
||||
$path.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
|
@ -86,8 +89,6 @@ pub type BackendCreator = Box<
|
|||
) -> Result<Box<dyn MailBackend>>,
|
||||
>;
|
||||
|
||||
pub type BackendValidateConfigFn = Box<dyn Fn(&mut AccountSettings) -> Result<()>>;
|
||||
|
||||
/// A hashmap containing all available mail backends.
|
||||
/// An abstraction over any available backends.
|
||||
pub struct Backends {
|
||||
|
@ -96,62 +97,24 @@ pub struct Backends {
|
|||
|
||||
pub struct Backend {
|
||||
pub create_fn: Box<dyn Fn() -> BackendCreator>,
|
||||
pub validate_conf_fn: BackendValidateConfigFn,
|
||||
pub validate_conf_fn: Box<dyn Fn(&AccountSettings) -> Result<()>>,
|
||||
}
|
||||
|
||||
impl Default for Backends {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
Backends::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[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. For a custom file path, \
|
||||
use `library_file_path` setting in your notmuch account.\n";
|
||||
pub const NOTMUCH_ERROR_MSG: &str =
|
||||
"libnotmuch5 was not found in your system. Make sure it is installed and in the library paths.\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."#;
|
||||
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";
|
||||
|
||||
impl Backends {
|
||||
pub fn new() -> Self {
|
||||
let mut b = Self {
|
||||
let mut b = Backends {
|
||||
map: HashMap::with_capacity_and_hasher(1, Default::default()),
|
||||
};
|
||||
#[cfg(feature = "maildir_backend")]
|
||||
|
@ -193,13 +156,15 @@ impl Backends {
|
|||
}
|
||||
#[cfg(feature = "notmuch_backend")]
|
||||
{
|
||||
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),
|
||||
},
|
||||
);
|
||||
if libloading::Library::new("libnotmuch.so.5").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),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "jmap_backend")]
|
||||
{
|
||||
|
@ -218,10 +183,6 @@ 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);
|
||||
}
|
||||
|
@ -235,24 +196,19 @@ impl Backends {
|
|||
self.map.insert(key, backend);
|
||||
}
|
||||
|
||||
pub fn validate_config(&self, key: &str, s: &mut AccountSettings) -> Result<()> {
|
||||
pub fn validate_config(&self, key: &str, s: &AccountSettings) -> Result<()> {
|
||||
(self
|
||||
.map
|
||||
.get(key)
|
||||
.ok_or_else(|| {
|
||||
Error::new(format!(
|
||||
"{}{} is not a valid mail backend. {}",
|
||||
MeliError::new(format!(
|
||||
"{}{} is not a valid mail backend",
|
||||
if key == "notmuch" {
|
||||
NOTMUCH_ERROR_MSG
|
||||
} else {
|
||||
""
|
||||
},
|
||||
key,
|
||||
if cfg!(feature = "notmuch_backend") && key == "notmuch" {
|
||||
NOTMUCH_ERROR_DETAILS
|
||||
} else {
|
||||
""
|
||||
},
|
||||
key
|
||||
))
|
||||
})?
|
||||
.validate_conf_fn)(s)
|
||||
|
@ -262,23 +218,20 @@ impl Backends {
|
|||
#[derive(Debug, Clone)]
|
||||
pub enum BackendEvent {
|
||||
Notice {
|
||||
description: String,
|
||||
content: Option<String>,
|
||||
level: LogLevel,
|
||||
description: Option<String>,
|
||||
content: String,
|
||||
level: crate::LoggingLevel,
|
||||
},
|
||||
Refresh(RefreshEvent),
|
||||
AccountStateChange {
|
||||
message: Cow<'static, str>,
|
||||
},
|
||||
//Job(Box<Future<Output = Result<()>> + Send + 'static>)
|
||||
}
|
||||
|
||||
impl From<Error> for BackendEvent {
|
||||
fn from(val: Error) -> Self {
|
||||
Self::Notice {
|
||||
description: val.summary.to_string(),
|
||||
content: Some(val.to_string()),
|
||||
level: LogLevel::ERROR,
|
||||
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(),
|
||||
level: crate::LoggingLevel::ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -292,15 +245,7 @@ pub enum RefreshEventKind {
|
|||
Remove(EnvelopeHash),
|
||||
NewFlags(EnvelopeHash, (Flag, Vec<String>)),
|
||||
Rescan,
|
||||
Failure(Error),
|
||||
MailboxCreate(Mailbox),
|
||||
MailboxDelete(MailboxHash),
|
||||
MailboxRename {
|
||||
old_mailbox_hash: MailboxHash,
|
||||
new_mailbox: Mailbox,
|
||||
},
|
||||
MailboxSubscribe(MailboxHash),
|
||||
MailboxUnsubscribe(MailboxHash),
|
||||
Failure(MeliError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -312,10 +257,9 @@ pub struct RefreshEvent {
|
|||
|
||||
#[derive(Clone)]
|
||||
pub struct BackendEventConsumer(Arc<dyn Fn(AccountHash, BackendEvent) + Send + Sync>);
|
||||
|
||||
impl BackendEventConsumer {
|
||||
pub fn new(b: Arc<dyn Fn(AccountHash, BackendEvent) + Send + Sync>) -> Self {
|
||||
Self(b)
|
||||
BackendEventConsumer(b)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -358,7 +302,6 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
|
|||
Ok(Box::pin(async { Ok(()) }))
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn fetch(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
|
@ -397,63 +340,66 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
|
|||
mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<()>;
|
||||
|
||||
fn collection(&self) -> crate::Collection;
|
||||
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
|
||||
None
|
||||
}
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
|
||||
fn create_mailbox(
|
||||
&mut self,
|
||||
path: String,
|
||||
) -> ResultFuture<(MailboxHash, HashMap<MailboxHash, Mailbox>)>;
|
||||
_path: String,
|
||||
) -> ResultFuture<(MailboxHash, HashMap<MailboxHash, Mailbox>)> {
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn delete_mailbox(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<HashMap<MailboxHash, Mailbox>>;
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn set_mailbox_subscription(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
val: bool,
|
||||
) -> ResultFuture<()>;
|
||||
_mailbox_hash: MailboxHash,
|
||||
_val: bool,
|
||||
) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn rename_mailbox(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
new_path: String,
|
||||
) -> ResultFuture<Mailbox>;
|
||||
_mailbox_hash: MailboxHash,
|
||||
_new_path: String,
|
||||
) -> ResultFuture<Mailbox> {
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn set_mailbox_permissions(
|
||||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
val: MailboxPermissions,
|
||||
) -> ResultFuture<()>;
|
||||
_mailbox_hash: MailboxHash,
|
||||
_val: MailboxPermissions,
|
||||
) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn search(
|
||||
&self,
|
||||
query: crate::search::Query,
|
||||
mailbox_hash: Option<MailboxHash>,
|
||||
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>>;
|
||||
|
||||
fn submit(
|
||||
&self,
|
||||
_bytes: Vec<u8>,
|
||||
_query: crate::search::Query,
|
||||
_mailbox_hash: Option<MailboxHash>,
|
||||
_flags: Option<Flag>,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new("Submission not supported in this backend.")
|
||||
.set_kind(ErrorKind::NotSupported))
|
||||
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>> {
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
}
|
||||
|
||||
/// A `BackendOp` manages common operations for the various mail backends. They
|
||||
/// only live for the duration of the operation. They are generated by the
|
||||
/// `operation` method of `Mailbackend` trait.
|
||||
/// A `BackendOp` manages common operations for the various mail backends. They only live for the
|
||||
/// duration of the operation. They are generated by the `operation` method of `Mailbackend` trait.
|
||||
///
|
||||
/// # Motivation
|
||||
///
|
||||
/// We need a way to do various operations on individual mails regardless of
|
||||
/// what backend they come from (eg local or imap).
|
||||
/// We need a way to do various operations on individual mails regardless of what backend they come
|
||||
/// from (eg local or imap).
|
||||
///
|
||||
/// # Creation
|
||||
/// ```ignore
|
||||
|
@ -489,17 +435,16 @@ pub trait BackendOp: ::std::fmt::Debug + ::std::marker::Send {
|
|||
|
||||
/// Wrapper for BackendOps that are to be set read-only.
|
||||
///
|
||||
/// Warning: Backend implementations may still cause side-effects (for example
|
||||
/// IMAP can set the Seen flag when fetching an envelope)
|
||||
/// Warning: Backend implementations may still cause side-effects (for example IMAP can set the
|
||||
/// Seen flag when fetching an envelope)
|
||||
#[derive(Debug)]
|
||||
pub struct ReadOnlyOp {
|
||||
op: Box<dyn BackendOp>,
|
||||
}
|
||||
|
||||
impl ReadOnlyOp {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(op: Box<dyn BackendOp>) -> Box<dyn BackendOp> {
|
||||
Box::new(Self { op })
|
||||
Box::new(ReadOnlyOp { op })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -512,9 +457,8 @@ impl BackendOp for ReadOnlyOp {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Copy, Hash, Eq, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Copy, Hash, Eq, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum SpecialUsageMailbox {
|
||||
#[default]
|
||||
Normal,
|
||||
Inbox,
|
||||
Archive,
|
||||
|
@ -545,32 +489,38 @@ impl std::fmt::Display for SpecialUsageMailbox {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for SpecialUsageMailbox {
|
||||
fn default() -> Self {
|
||||
SpecialUsageMailbox::Normal
|
||||
}
|
||||
}
|
||||
|
||||
impl SpecialUsageMailbox {
|
||||
pub fn detect_usage(name: &str) -> Option<Self> {
|
||||
pub fn detect_usage(name: &str) -> Option<SpecialUsageMailbox> {
|
||||
if name.eq_ignore_ascii_case("inbox") {
|
||||
Some(Self::Inbox)
|
||||
Some(SpecialUsageMailbox::Inbox)
|
||||
} else if name.eq_ignore_ascii_case("archive") {
|
||||
Some(Self::Archive)
|
||||
Some(SpecialUsageMailbox::Archive)
|
||||
} else if name.eq_ignore_ascii_case("drafts") {
|
||||
Some(Self::Drafts)
|
||||
Some(SpecialUsageMailbox::Drafts)
|
||||
} else if name.eq_ignore_ascii_case("junk") || name.eq_ignore_ascii_case("spam") {
|
||||
Some(Self::Junk)
|
||||
Some(SpecialUsageMailbox::Junk)
|
||||
} else if name.eq_ignore_ascii_case("sent") {
|
||||
Some(Self::Sent)
|
||||
Some(SpecialUsageMailbox::Sent)
|
||||
} else if name.eq_ignore_ascii_case("trash") {
|
||||
Some(Self::Trash)
|
||||
Some(SpecialUsageMailbox::Trash)
|
||||
} else {
|
||||
Some(Self::Normal)
|
||||
Some(SpecialUsageMailbox::Normal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait BackendMailbox: Debug {
|
||||
fn hash(&self) -> MailboxHash;
|
||||
/// Final component of `path`.
|
||||
fn name(&self) -> &str;
|
||||
/// Path of mailbox within the mailbox hierarchy, with `/` as separator.
|
||||
fn path(&self) -> &str;
|
||||
fn change_name(&mut self, new_name: &str);
|
||||
fn clone(&self) -> Mailbox;
|
||||
fn children(&self) -> &[MailboxHash];
|
||||
fn parent(&self) -> Option<MailboxHash>;
|
||||
|
@ -582,10 +532,8 @@ pub trait BackendMailbox: Debug {
|
|||
fn count(&self) -> Result<(usize, usize)>;
|
||||
}
|
||||
|
||||
crate::declare_u64_hash!(AccountHash);
|
||||
crate::declare_u64_hash!(MailboxHash);
|
||||
crate::declare_u64_hash!(TagHash);
|
||||
|
||||
pub type AccountHash = u64;
|
||||
pub type MailboxHash = u64;
|
||||
pub type Mailbox = Box<dyn BackendMailbox + Send + Sync>;
|
||||
|
||||
impl Clone for Mailbox {
|
||||
|
@ -608,7 +556,7 @@ pub struct MailboxPermissions {
|
|||
|
||||
impl Default for MailboxPermissions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
MailboxPermissions {
|
||||
create_messages: false,
|
||||
remove_messages: false,
|
||||
set_flags: false,
|
||||
|
@ -627,7 +575,7 @@ impl std::fmt::Display for MailboxPermissions {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct EnvelopeHashBatch {
|
||||
pub first: EnvelopeHash,
|
||||
pub rest: SmallVec<[EnvelopeHash; 64]>,
|
||||
|
@ -635,7 +583,7 @@ pub struct EnvelopeHashBatch {
|
|||
|
||||
impl From<EnvelopeHash> for EnvelopeHashBatch {
|
||||
fn from(value: EnvelopeHash) -> Self {
|
||||
Self {
|
||||
EnvelopeHashBatch {
|
||||
first: value,
|
||||
rest: SmallVec::new(),
|
||||
}
|
||||
|
@ -649,35 +597,21 @@ impl std::convert::TryFrom<&[EnvelopeHash]> for EnvelopeHashBatch {
|
|||
if value.is_empty() {
|
||||
return Err(());
|
||||
}
|
||||
Ok(Self {
|
||||
Ok(EnvelopeHashBatch {
|
||||
first: value[0],
|
||||
rest: value[1..].iter().cloned().collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&EnvelopeHashBatch> for BTreeSet<EnvelopeHash> {
|
||||
fn from(val: &EnvelopeHashBatch) -> Self {
|
||||
val.iter().collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl EnvelopeHashBatch {
|
||||
pub fn iter(&self) -> impl std::iter::Iterator<Item = EnvelopeHash> + '_ {
|
||||
std::iter::once(self.first).chain(self.rest.iter().cloned())
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
1 + self.rest.len()
|
||||
}
|
||||
|
||||
pub fn to_set(&self) -> BTreeSet<EnvelopeHash> {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
|
@ -719,10 +653,6 @@ impl LazyCountSet {
|
|||
self.not_yet_seen = self.not_yet_seen.saturating_sub(self.set.len() - old_len);
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.set.len() + self.not_yet_seen
|
||||
|
@ -754,10 +684,10 @@ fn test_lazy_count_set() {
|
|||
new.set_not_yet_seen(10);
|
||||
assert_eq!(new.len(), 10);
|
||||
for i in 0..10 {
|
||||
assert!(new.insert_existing(EnvelopeHash(i)));
|
||||
assert!(new.insert_existing(i));
|
||||
}
|
||||
assert_eq!(new.len(), 10);
|
||||
assert!(!new.insert_existing(EnvelopeHash(10)));
|
||||
assert!(!new.insert_existing(10));
|
||||
assert_eq!(new.len(), 10);
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -21,22 +21,21 @@
|
|||
|
||||
use super::*;
|
||||
mod sync;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use crate::{
|
||||
backends::MailboxHash,
|
||||
email::{Envelope, EnvelopeHash},
|
||||
error::*,
|
||||
};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[derive(Debug, PartialEq, Hash, Eq, Ord, PartialOrd, Copy, Clone)]
|
||||
pub struct ModSequence(pub std::num::NonZeroU64);
|
||||
|
||||
impl TryFrom<i64> for ModSequence {
|
||||
type Error = ();
|
||||
fn try_from(val: i64) -> std::result::Result<Self, ()> {
|
||||
fn try_from(val: i64) -> std::result::Result<ModSequence, ()> {
|
||||
std::num::NonZeroU64::new(val as u64)
|
||||
.map(|u| Ok(Self(u)))
|
||||
.map(|u| Ok(ModSequence(u)))
|
||||
.unwrap_or(Err(()))
|
||||
}
|
||||
}
|
||||
|
@ -94,29 +93,22 @@ pub trait ImapCache: Send + core::fmt::Debug {
|
|||
) -> Result<Option<Vec<u8>>>;
|
||||
}
|
||||
|
||||
pub trait ImapCacheReset: Send + core::fmt::Debug {
|
||||
fn reset_db(uid_store: &UIDStore) -> Result<()>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite3")]
|
||||
pub use sqlite3_m::*;
|
||||
|
||||
#[cfg(feature = "sqlite3")]
|
||||
mod sqlite3_m {
|
||||
use super::*;
|
||||
use crate::utils::sqlite3::{
|
||||
self,
|
||||
rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput},
|
||||
Connection, DatabaseDescription,
|
||||
use crate::sqlite3::rusqlite::types::{
|
||||
FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput,
|
||||
};
|
||||
use crate::sqlite3::{self, DatabaseDescription};
|
||||
|
||||
type Sqlite3UID = i32;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Sqlite3Cache {
|
||||
connection: Connection,
|
||||
connection: crate::sqlite3::Connection,
|
||||
loaded_mailboxes: BTreeSet<MailboxHash>,
|
||||
uid_store: Arc<UIDStore>,
|
||||
}
|
||||
|
@ -148,7 +140,7 @@ mod sqlite3_m {
|
|||
CREATE INDEX IF NOT EXISTS envelope_idx ON envelopes(hash);
|
||||
CREATE INDEX IF NOT EXISTS mailbox_idx ON mailbox(mailbox_hash);",
|
||||
),
|
||||
version: 3,
|
||||
version: 2,
|
||||
};
|
||||
|
||||
impl ToSql for ModSequence {
|
||||
|
@ -163,7 +155,7 @@ mod sqlite3_m {
|
|||
if i == 0 {
|
||||
return Err(FromSqlError::OutOfRange(0));
|
||||
}
|
||||
Ok(Self::try_from(i).unwrap())
|
||||
Ok(ModSequence::try_from(i).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -172,7 +164,7 @@ mod sqlite3_m {
|
|||
Ok(Box::new(Self {
|
||||
connection: sqlite3::open_or_create_db(
|
||||
&DB_DESCRIPTION,
|
||||
Some(&uid_store.account_name),
|
||||
Some(uid_store.account_name.as_str()),
|
||||
)?,
|
||||
loaded_mailboxes: BTreeSet::default(),
|
||||
uid_store,
|
||||
|
@ -185,23 +177,17 @@ mod sqlite3_m {
|
|||
.prepare("SELECT MAX(uid) FROM envelopes WHERE mailbox_hash = ?1;")?;
|
||||
|
||||
let mut ret: Vec<UID> = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash], |row| {
|
||||
row.get(0).map(|i: Sqlite3UID| i as UID)
|
||||
.query_map(sqlite3::params![mailbox_hash as i64], |row| {
|
||||
Ok(row.get(0).map(|i: Sqlite3UID| i as UID)?)
|
||||
})?
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
Ok(ret.pop().unwrap_or(0))
|
||||
}
|
||||
}
|
||||
|
||||
impl ImapCacheReset for Sqlite3Cache {
|
||||
fn reset_db(uid_store: &UIDStore) -> Result<()> {
|
||||
sqlite3::reset_db(&DB_DESCRIPTION, Some(&uid_store.account_name))
|
||||
}
|
||||
}
|
||||
|
||||
impl ImapCache for Sqlite3Cache {
|
||||
fn reset(&mut self) -> Result<()> {
|
||||
Self::reset_db(&self.uid_store)
|
||||
sqlite3::reset_db(&DB_DESCRIPTION, Some(self.uid_store.account_name.as_str()))
|
||||
}
|
||||
|
||||
fn mailbox_state(&mut self, mailbox_hash: MailboxHash) -> Result<Option<()>> {
|
||||
|
@ -213,7 +199,7 @@ mod sqlite3_m {
|
|||
"SELECT uidvalidity, flags, highestmodseq FROM mailbox WHERE mailbox_hash = ?1;",
|
||||
)?;
|
||||
|
||||
let mut ret = stmt.query_map(sqlite3::params![mailbox_hash], |row| {
|
||||
let mut ret = stmt.query_map(sqlite3::params![mailbox_hash as i64], |row| {
|
||||
Ok((
|
||||
row.get(0).map(|u: Sqlite3UID| u as UID)?,
|
||||
row.get(1)?,
|
||||
|
@ -245,7 +231,7 @@ mod sqlite3_m {
|
|||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.and_modify(|entry| *entry = highestmodseq.ok_or(()))
|
||||
.or_insert_with(|| highestmodseq.ok_or(()));
|
||||
.or_insert(highestmodseq.ok_or(()));
|
||||
self.uid_store
|
||||
.uidvalidity
|
||||
.lock()
|
||||
|
@ -253,10 +239,13 @@ mod sqlite3_m {
|
|||
.entry(mailbox_hash)
|
||||
.and_modify(|entry| *entry = uidvalidity)
|
||||
.or_insert(uidvalidity);
|
||||
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
|
||||
let mut tag_lck = self.uid_store.tag_index.write().unwrap();
|
||||
for f in to_str!(&flags).split('\0') {
|
||||
let hash = TagHash::from_bytes(f.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| f.to_string());
|
||||
let hash = tag_hash!(f);
|
||||
//debug!("hash {} flag {}", hash, &f);
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f.to_string());
|
||||
}
|
||||
}
|
||||
self.loaded_mailboxes.insert(mailbox_hash);
|
||||
Ok(Some(()))
|
||||
|
@ -276,7 +265,7 @@ mod sqlite3_m {
|
|||
self.connection
|
||||
.execute(
|
||||
"DELETE FROM mailbox WHERE mailbox_hash = ?1",
|
||||
sqlite3::params![mailbox_hash],
|
||||
sqlite3::params![mailbox_hash as i64],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
|
@ -286,46 +275,24 @@ mod sqlite3_m {
|
|||
})?;
|
||||
|
||||
if let Some(Ok(highestmodseq)) = select_response.highestmodseq {
|
||||
self.connection
|
||||
.execute(
|
||||
"INSERT OR IGNORE INTO mailbox (uidvalidity, flags, highestmodseq, \
|
||||
mailbox_hash) VALUES (?1, ?2, ?3, ?4)",
|
||||
sqlite3::params![
|
||||
select_response.uidvalidity as Sqlite3UID,
|
||||
select_response
|
||||
.flags
|
||||
.1
|
||||
.iter()
|
||||
.map(|s| s.as_str())
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\0")
|
||||
.as_bytes(),
|
||||
highestmodseq,
|
||||
mailbox_hash
|
||||
],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not insert uidvalidity {} in header_cache of account {}",
|
||||
select_response.uidvalidity, self.uid_store.account_name
|
||||
)
|
||||
})?;
|
||||
self.connection.execute(
|
||||
"INSERT OR IGNORE INTO mailbox (uidvalidity, flags, highestmodseq, mailbox_hash) VALUES (?1, ?2, ?3, ?4)",
|
||||
sqlite3::params![select_response.uidvalidity as Sqlite3UID, select_response.flags.1.iter().map(|s| s.as_str()).collect::<Vec<&str>>().join("\0").as_bytes(), highestmodseq, mailbox_hash as i64],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not insert uidvalidity {} in header_cache of account {}",
|
||||
select_response.uidvalidity, self.uid_store.account_name
|
||||
)
|
||||
})?;
|
||||
} else {
|
||||
self.connection
|
||||
.execute(
|
||||
"INSERT OR IGNORE INTO mailbox (uidvalidity, flags, mailbox_hash) VALUES \
|
||||
(?1, ?2, ?3)",
|
||||
"INSERT OR IGNORE INTO mailbox (uidvalidity, flags, mailbox_hash) VALUES (?1, ?2, ?3)",
|
||||
sqlite3::params![
|
||||
select_response.uidvalidity as Sqlite3UID,
|
||||
select_response
|
||||
.flags
|
||||
.1
|
||||
.iter()
|
||||
.map(|s| s.as_str())
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\0")
|
||||
.as_bytes(),
|
||||
mailbox_hash
|
||||
select_response.flags.1.iter().map(|s| s.as_str()).collect::<Vec<&str>>().join("\0").as_bytes(),
|
||||
mailbox_hash as i64
|
||||
],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
|
@ -361,7 +328,7 @@ mod sqlite3_m {
|
|||
.join("\0")
|
||||
.as_bytes(),
|
||||
highestmodseq,
|
||||
mailbox_hash
|
||||
mailbox_hash as i64
|
||||
],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
|
@ -383,7 +350,7 @@ mod sqlite3_m {
|
|||
.collect::<Vec<&str>>()
|
||||
.join("\0")
|
||||
.as_bytes(),
|
||||
mailbox_hash
|
||||
mailbox_hash as i64
|
||||
],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
|
@ -402,32 +369,19 @@ mod sqlite3_m {
|
|||
return Ok(None);
|
||||
}
|
||||
|
||||
let ret: Vec<(UID, Envelope, Option<ModSequence>)> = match {
|
||||
let mut stmt = self.connection.prepare(
|
||||
"SELECT uid, envelope, modsequence FROM envelopes WHERE mailbox_hash = ?1;",
|
||||
)?;
|
||||
let mut stmt = self.connection.prepare(
|
||||
"SELECT uid, envelope, modsequence FROM envelopes WHERE mailbox_hash = ?1;",
|
||||
)?;
|
||||
|
||||
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
|
||||
// for the temporary to live long enough
|
||||
let x = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash], |row| {
|
||||
Ok((
|
||||
row.get(0).map(|i: Sqlite3UID| i as UID)?,
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
))
|
||||
})?
|
||||
.collect::<std::result::Result<_, _>>();
|
||||
x
|
||||
} {
|
||||
Err(err) if matches!(&err, rusqlite::Error::FromSqlConversionFailure(_, _, _)) => {
|
||||
drop(err);
|
||||
self.reset()?;
|
||||
return Ok(None);
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
Ok(v) => v,
|
||||
};
|
||||
let ret: Vec<(UID, Envelope, Option<ModSequence>)> = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash as i64], |row| {
|
||||
Ok((
|
||||
row.get(0).map(|i: Sqlite3UID| i as UID)?,
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
))
|
||||
})?
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
let mut max_uid = 0;
|
||||
let mut env_lck = self.uid_store.envelopes.lock().unwrap();
|
||||
let mut hash_index_lck = self.uid_store.hash_index.lock().unwrap();
|
||||
|
@ -475,7 +429,7 @@ mod sqlite3_m {
|
|||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if self.mailbox_state(mailbox_hash)?.is_none() {
|
||||
return Err(Error::new("Mailbox is not in cache").set_kind(ErrorKind::Bug));
|
||||
return Err(MeliError::new("Mailbox is not in cache").set_kind(ErrorKind::Bug));
|
||||
}
|
||||
let Self {
|
||||
ref mut connection,
|
||||
|
@ -497,24 +451,9 @@ mod sqlite3_m {
|
|||
{
|
||||
max_uid = std::cmp::max(max_uid, *uid);
|
||||
tx.execute(
|
||||
"INSERT OR REPLACE INTO envelopes (hash, uid, mailbox_hash, modsequence, \
|
||||
envelope) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
sqlite3::params![
|
||||
envelope.hash(),
|
||||
*uid as Sqlite3UID,
|
||||
mailbox_hash,
|
||||
modseq,
|
||||
&envelope
|
||||
],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not insert envelope {} {} in header_cache of account {}",
|
||||
envelope.message_id(),
|
||||
envelope.hash(),
|
||||
uid_store.account_name
|
||||
)
|
||||
})?;
|
||||
"INSERT OR REPLACE INTO envelopes (hash, uid, mailbox_hash, modsequence, envelope) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
sqlite3::params![envelope.hash() as i64, *uid as Sqlite3UID, mailbox_hash as i64, modseq, &envelope],
|
||||
).chain_err_summary(|| format!("Could not insert envelope {} {} in header_cache of account {}", envelope.message_id(), envelope.hash(), uid_store.account_name))?;
|
||||
}
|
||||
}
|
||||
tx.commit()?;
|
||||
|
@ -532,7 +471,7 @@ mod sqlite3_m {
|
|||
refresh_events: &[(UID, RefreshEvent)],
|
||||
) -> Result<()> {
|
||||
if self.mailbox_state(mailbox_hash)?.is_none() {
|
||||
return Err(Error::new("Mailbox is not in cache").set_kind(ErrorKind::Bug));
|
||||
return Err(MeliError::new("Mailbox is not in cache").set_kind(ErrorKind::Bug));
|
||||
}
|
||||
let Self {
|
||||
ref mut connection,
|
||||
|
@ -544,10 +483,10 @@ mod sqlite3_m {
|
|||
for (uid, event) in refresh_events {
|
||||
match &event.kind {
|
||||
RefreshEventKind::Remove(env_hash) => {
|
||||
hash_index_lck.remove(env_hash);
|
||||
hash_index_lck.remove(&env_hash);
|
||||
tx.execute(
|
||||
"DELETE FROM envelopes WHERE mailbox_hash = ?1 AND uid = ?2;",
|
||||
sqlite3::params![mailbox_hash, *uid as Sqlite3UID],
|
||||
sqlite3::params![mailbox_hash as i64, *uid as Sqlite3UID],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
|
@ -562,27 +501,25 @@ mod sqlite3_m {
|
|||
)?;
|
||||
|
||||
let mut ret: Vec<Envelope> = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash, *uid as Sqlite3UID], |row| {
|
||||
row.get(0)
|
||||
})?
|
||||
.query_map(
|
||||
sqlite3::params![mailbox_hash as i64, *uid as Sqlite3UID],
|
||||
|row| Ok(row.get(0)?),
|
||||
)?
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
if let Some(mut env) = ret.pop() {
|
||||
env.set_flags(*flags);
|
||||
env.tags_mut().clear();
|
||||
env.tags_mut()
|
||||
.extend(tags.iter().map(|t| TagHash::from_bytes(t.as_bytes())));
|
||||
env.labels_mut().clear();
|
||||
env.labels_mut().extend(tags.iter().map(|t| tag_hash!(t)));
|
||||
tx.execute(
|
||||
"UPDATE envelopes SET envelope = ?1 WHERE mailbox_hash = ?2 AND \
|
||||
uid = ?3;",
|
||||
sqlite3::params![&env, mailbox_hash, *uid as Sqlite3UID],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not update envelope {} uid {} from mailbox {} account \
|
||||
{}",
|
||||
env_hash, *uid, mailbox_hash, uid_store.account_name
|
||||
)
|
||||
})?;
|
||||
"UPDATE envelopes SET envelope = ?1 WHERE mailbox_hash = ?2 AND uid = ?3;",
|
||||
sqlite3::params![&env, mailbox_hash as i64, *uid as Sqlite3UID],
|
||||
)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not update envelope {} uid {} from mailbox {} account {}",
|
||||
env_hash, *uid, mailbox_hash, uid_store.account_name
|
||||
)
|
||||
})?;
|
||||
uid_store
|
||||
.envelopes
|
||||
.lock()
|
||||
|
@ -614,39 +551,39 @@ mod sqlite3_m {
|
|||
let mut ret: Vec<(UID, Envelope, Option<ModSequence>)> = match identifier {
|
||||
Ok(uid) => {
|
||||
let mut stmt = self.connection.prepare(
|
||||
"SELECT uid, envelope, modsequence FROM envelopes WHERE mailbox_hash = ?1 \
|
||||
AND uid = ?2;",
|
||||
)?;
|
||||
"SELECT uid, envelope, modsequence FROM envelopes WHERE mailbox_hash = ?1 AND uid = ?2;",
|
||||
)?;
|
||||
|
||||
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
|
||||
// for the temporary to live long enough
|
||||
let x = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash, uid as Sqlite3UID], |row| {
|
||||
Ok((
|
||||
row.get(0).map(|u: Sqlite3UID| u as UID)?,
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
))
|
||||
})?
|
||||
.query_map(
|
||||
sqlite3::params![mailbox_hash as i64, uid as Sqlite3UID],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get(0).map(|u: Sqlite3UID| u as UID)?,
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
))
|
||||
},
|
||||
)?
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
x
|
||||
}
|
||||
Err(env_hash) => {
|
||||
let mut stmt = self.connection.prepare(
|
||||
"SELECT uid, envelope, modsequence FROM envelopes WHERE mailbox_hash = ?1 \
|
||||
AND hash = ?2;",
|
||||
)?;
|
||||
"SELECT uid, envelope, modsequence FROM envelopes WHERE mailbox_hash = ?1 AND hash = ?2;",
|
||||
)?;
|
||||
|
||||
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
|
||||
// for the temporary to live long enough
|
||||
let x = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash, env_hash], |row| {
|
||||
Ok((
|
||||
row.get(0).map(|u: Sqlite3UID| u as UID)?,
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
))
|
||||
})?
|
||||
.query_map(
|
||||
sqlite3::params![mailbox_hash as i64, env_hash as i64],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get(0).map(|u: Sqlite3UID| u as UID)?,
|
||||
row.get(1)?,
|
||||
row.get(2)?,
|
||||
))
|
||||
},
|
||||
)?
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
x
|
||||
}
|
||||
|
@ -655,12 +592,12 @@ mod sqlite3_m {
|
|||
return Ok(None);
|
||||
}
|
||||
let (uid, inner, modsequence) = ret.pop().unwrap();
|
||||
Ok(Some(CachedEnvelope {
|
||||
return Ok(Some(CachedEnvelope {
|
||||
inner,
|
||||
uid,
|
||||
mailbox_hash,
|
||||
modsequence,
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
fn rfc822(
|
||||
|
@ -673,12 +610,11 @@ mod sqlite3_m {
|
|||
let mut stmt = self.connection.prepare(
|
||||
"SELECT rfc822 FROM envelopes WHERE mailbox_hash = ?1 AND uid = ?2;",
|
||||
)?;
|
||||
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
|
||||
// for the temporary to live long enough
|
||||
let x = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash, uid as Sqlite3UID], |row| {
|
||||
row.get(0)
|
||||
})?
|
||||
.query_map(
|
||||
sqlite3::params![mailbox_hash as i64, uid as Sqlite3UID],
|
||||
|row| Ok(row.get(0)?),
|
||||
)?
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
x
|
||||
}
|
||||
|
@ -686,10 +622,11 @@ mod sqlite3_m {
|
|||
let mut stmt = self.connection.prepare(
|
||||
"SELECT rfc822 FROM envelopes WHERE mailbox_hash = ?1 AND hash = ?2;",
|
||||
)?;
|
||||
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
|
||||
// for the temporary to live long enough
|
||||
let x = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash, env_hash], |row| row.get(0))?
|
||||
.query_map(
|
||||
sqlite3::params![mailbox_hash as i64, env_hash as i64],
|
||||
|row| Ok(row.get(0)?),
|
||||
)?
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
x
|
||||
}
|
||||
|
@ -718,19 +655,19 @@ pub(super) async fn fetch_cached_envs(state: &mut FetchState) -> Result<Option<V
|
|||
{
|
||||
let mut conn = connection.lock().await;
|
||||
match conn.load_cache(mailbox_hash).await {
|
||||
None => Ok(None),
|
||||
None => return Ok(None),
|
||||
Some(Ok(env_hashes)) => {
|
||||
let env_lck = uid_store.envelopes.lock().unwrap();
|
||||
Ok(Some(
|
||||
return Ok(Some(
|
||||
env_hashes
|
||||
.into_iter()
|
||||
.filter_map(|env_hash| {
|
||||
env_lck.get(&env_hash).map(|c_env| c_env.inner.clone())
|
||||
})
|
||||
.collect::<Vec<Envelope>>(),
|
||||
))
|
||||
));
|
||||
}
|
||||
Some(Err(err)) => Err(err),
|
||||
Some(Err(err)) => return Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -750,19 +687,13 @@ mod default_m {
|
|||
}
|
||||
}
|
||||
|
||||
impl ImapCacheReset for DefaultCache {
|
||||
fn reset_db(uid_store: &UIDStore) -> Result<()> {
|
||||
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
}
|
||||
}
|
||||
|
||||
impl ImapCache for DefaultCache {
|
||||
fn reset(&mut self) -> Result<()> {
|
||||
DefaultCache::reset_db(&self.uid_store)
|
||||
Err(MeliError::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
}
|
||||
|
||||
fn mailbox_state(&mut self, _mailbox_hash: MailboxHash) -> Result<Option<()>> {
|
||||
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
Err(MeliError::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
}
|
||||
|
||||
fn clear(
|
||||
|
@ -770,11 +701,11 @@ mod default_m {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_select_response: &SelectResponse,
|
||||
) -> Result<()> {
|
||||
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
Err(MeliError::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
}
|
||||
|
||||
fn envelopes(&mut self, _mailbox_hash: MailboxHash) -> Result<Option<Vec<EnvelopeHash>>> {
|
||||
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
Err(MeliError::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
}
|
||||
|
||||
fn insert_envelopes(
|
||||
|
@ -782,7 +713,7 @@ mod default_m {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_fetches: &[FetchResponse<'_>],
|
||||
) -> Result<()> {
|
||||
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
Err(MeliError::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
}
|
||||
|
||||
fn update_mailbox(
|
||||
|
@ -790,7 +721,7 @@ mod default_m {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_select_response: &SelectResponse,
|
||||
) -> Result<()> {
|
||||
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
Err(MeliError::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
}
|
||||
|
||||
fn update(
|
||||
|
@ -798,7 +729,7 @@ mod default_m {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_refresh_events: &[(UID, RefreshEvent)],
|
||||
) -> Result<()> {
|
||||
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
Err(MeliError::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
}
|
||||
|
||||
fn find_envelope(
|
||||
|
@ -806,7 +737,7 @@ mod default_m {
|
|||
_identifier: std::result::Result<UID, EnvelopeHash>,
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> Result<Option<CachedEnvelope>> {
|
||||
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
Err(MeliError::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
}
|
||||
|
||||
fn rfc822(
|
||||
|
@ -814,7 +745,7 @@ mod default_m {
|
|||
_identifier: std::result::Result<UID, EnvelopeHash>,
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
Err(MeliError::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,20 +19,13 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use imap_codec::{
|
||||
fetch::{MacroOrMessageDataItemNames, MessageDataItemName},
|
||||
search::SearchKey,
|
||||
sequence::SequenceSet,
|
||||
status::StatusDataItemName,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
impl ImapConnection {
|
||||
pub async fn resync(&mut self, mailbox_hash: MailboxHash) -> Result<Option<Vec<Envelope>>> {
|
||||
debug!("resync mailbox_hash {}", mailbox_hash);
|
||||
debug!(&self.sync_policy);
|
||||
if matches!(self.sync_policy, SyncPolicy::None) {
|
||||
if let SyncPolicy::None = self.sync_policy {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
|
@ -107,13 +100,13 @@ impl ImapConnection {
|
|||
.unwrap()
|
||||
.get(&mailbox_hash)
|
||||
.cloned();
|
||||
// 3. tag2 UID FETCH 1:<lastseenuid> FLAGS
|
||||
//if cached_uidvalidity.is_none() || cached_max_uid.is_none() {
|
||||
// return Ok(None);
|
||||
//}
|
||||
// 3. tag2 UID FETCH 1:<lastseenuid> FLAGS
|
||||
if cached_uidvalidity.is_none() || cached_max_uid.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let current_uidvalidity: UID = cached_uidvalidity.unwrap_or(1);
|
||||
let max_uid: UID = cached_max_uid.unwrap_or(1);
|
||||
let current_uidvalidity: UID = cached_uidvalidity.unwrap();
|
||||
let max_uid: UID = cached_max_uid.unwrap();
|
||||
let (mailbox_path, mailbox_exists, unseen) = {
|
||||
let f = &self.uid_store.mailboxes.lock().await[&mailbox_hash];
|
||||
(
|
||||
|
@ -134,12 +127,14 @@ impl ImapConnection {
|
|||
}
|
||||
cache_handle.update_mailbox(mailbox_hash, &select_response)?;
|
||||
|
||||
// 2. tag1 UID FETCH <lastseenuid+1>:* <descriptors>
|
||||
self.send_command(CommandBody::fetch(
|
||||
max_uid + 1..,
|
||||
common_attributes(),
|
||||
true,
|
||||
)?)
|
||||
// 2. tag1 UID FETCH <lastseenuid+1>:* <descriptors>
|
||||
self.send_command(
|
||||
format!(
|
||||
"UID FETCH {}:* (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)",
|
||||
max_uid + 1
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await?;
|
||||
|
@ -162,18 +157,32 @@ impl ImapConnection {
|
|||
let env = envelope.as_mut().unwrap();
|
||||
env.set_hash(generate_envelope_hash(&mailbox_path, &uid));
|
||||
if let Some(value) = references {
|
||||
let parse_result = crate::email::parser::address::msg_id_list(value);
|
||||
if let Ok((_, value)) = parse_result {
|
||||
let prev_val = env.references.take();
|
||||
for v in value {
|
||||
env.push_references(v);
|
||||
}
|
||||
if let Some(prev) = prev_val {
|
||||
for v in prev.refs {
|
||||
env.push_references(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
env.set_references(value);
|
||||
}
|
||||
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
|
||||
let mut tag_lck = self.uid_store.tag_index.write().unwrap();
|
||||
if let Some((flags, keywords)) = flags {
|
||||
env.set_flags(*flags);
|
||||
if !env.is_seen() {
|
||||
new_unseen.insert(env.hash());
|
||||
}
|
||||
for f in keywords {
|
||||
let hash = TagHash::from_bytes(f.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| f.to_string());
|
||||
env.tags_mut().push(hash);
|
||||
let hash = tag_hash!(f);
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f.to_string());
|
||||
}
|
||||
env.labels_mut().push(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -230,25 +239,18 @@ impl ImapConnection {
|
|||
unseen_lck.insert_set(new_unseen);
|
||||
}
|
||||
mailbox_exists.lock().unwrap().insert_set(payload_hash_set);
|
||||
// 3. tag2 UID FETCH 1:<lastseenuid> FLAGS
|
||||
let sequence_set = if max_uid == 0 {
|
||||
SequenceSet::from(..)
|
||||
// 3. tag2 UID FETCH 1:<lastseenuid> FLAGS
|
||||
if max_uid == 0 {
|
||||
self.send_command("UID FETCH 1:* FLAGS".as_bytes()).await?;
|
||||
} else {
|
||||
SequenceSet::try_from(..=max_uid)?
|
||||
};
|
||||
self.send_command(CommandBody::Fetch {
|
||||
sequence_set,
|
||||
macro_or_item_names: MacroOrMessageDataItemNames::MessageDataItemNames(vec![
|
||||
MessageDataItemName::Flags,
|
||||
]),
|
||||
uid: true,
|
||||
})
|
||||
.await?;
|
||||
self.send_command(format!("UID FETCH 1:{} FLAGS", max_uid).as_bytes())
|
||||
.await?;
|
||||
}
|
||||
self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await?;
|
||||
// 1) update cached flags for old messages;
|
||||
// 2) find out which old messages got expunged; and
|
||||
// 3) build a mapping between message numbers and UIDs (for old messages).
|
||||
//1) update cached flags for old messages;
|
||||
//2) find out which old messages got expunged; and
|
||||
//3) build a mapping between message numbers and UIDs (for old messages).
|
||||
let mut valid_envs = BTreeSet::default();
|
||||
let mut env_lck = self.uid_store.envelopes.lock().unwrap();
|
||||
let (_, v, _) = protocol_parser::fetch_responses(&response)?;
|
||||
|
@ -262,19 +264,19 @@ impl ImapConnection {
|
|||
}
|
||||
let (flags, tags) = flags.unwrap();
|
||||
if env_lck[&env_hash].inner.flags() != flags
|
||||
|| env_lck[&env_hash].inner.tags()
|
||||
|| env_lck[&env_hash].inner.labels()
|
||||
!= &tags
|
||||
.iter()
|
||||
.map(|t| TagHash::from_bytes(t.as_bytes()))
|
||||
.collect::<SmallVec<[TagHash; 8]>>()
|
||||
.map(|t| tag_hash!(t))
|
||||
.collect::<SmallVec<[u64; 8]>>()
|
||||
{
|
||||
env_lck.entry(env_hash).and_modify(|entry| {
|
||||
entry.inner.set_flags(flags);
|
||||
entry.inner.tags_mut().clear();
|
||||
entry.inner.labels_mut().clear();
|
||||
entry
|
||||
.inner
|
||||
.tags_mut()
|
||||
.extend(tags.iter().map(|t| TagHash::from_bytes(t.as_bytes())));
|
||||
.labels_mut()
|
||||
.extend(tags.iter().map(|t| tag_hash!(t)));
|
||||
});
|
||||
refresh_events.push((
|
||||
uid,
|
||||
|
@ -385,9 +387,9 @@ impl ImapConnection {
|
|||
// client MUST
|
||||
// * empty the local cache of that mailbox;
|
||||
// * "forget" the cached HIGHESTMODSEQ value for the mailbox;
|
||||
// * remove any pending "actions" that refer to UIDs in that mailbox (note
|
||||
// that this doesn't affect actions performed on client-generated fake UIDs;
|
||||
// see Section 5); and
|
||||
// * remove any pending "actions" that refer to UIDs in that
|
||||
// mailbox (note that this doesn't affect actions performed on
|
||||
// client-generated fake UIDs; see Section 5); and
|
||||
// * skip steps 1b and 2-II;
|
||||
cache_handle.clear(mailbox_hash, &select_response)?;
|
||||
return Ok(None);
|
||||
|
@ -408,9 +410,9 @@ impl ImapConnection {
|
|||
let new_highestmodseq = select_response.highestmodseq.unwrap().unwrap();
|
||||
let mut refresh_events = vec![];
|
||||
// 1b) Check the mailbox HIGHESTMODSEQ.
|
||||
// If the cached value is the same as the one returned by the server, skip
|
||||
// fetching message flags on step 2-II, i.e., the client only has to
|
||||
// find out which messages got expunged.
|
||||
// If the cached value is the same as the one returned by the server, skip fetching
|
||||
// message flags on step 2-II, i.e., the client only has to find out which messages got
|
||||
// expunged.
|
||||
if cached_highestmodseq != new_highestmodseq {
|
||||
/* Cache is synced, only figure out which messages got expunged */
|
||||
|
||||
|
@ -422,12 +424,10 @@ impl ImapConnection {
|
|||
// "FETCH 1:* (FLAGS) (CHANGEDSINCE <cached-value>)" or
|
||||
// "SEARCH MODSEQ <cached-value>".
|
||||
|
||||
// 2. tag1 UID FETCH <lastseenuid+1>:* <descriptors>
|
||||
// TODO(#222): imap-codec does not support "CONDSTORE/QRESYNC" currently.
|
||||
self.send_command_raw(
|
||||
// 2. tag1 UID FETCH <lastseenuid+1>:* <descriptors>
|
||||
self.send_command(
|
||||
format!(
|
||||
"UID FETCH {}:* (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] \
|
||||
BODYSTRUCTURE) (CHANGEDSINCE {})",
|
||||
"UID FETCH {}:* (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE) (CHANGEDSINCE {})",
|
||||
cached_max_uid + 1,
|
||||
cached_highestmodseq,
|
||||
)
|
||||
|
@ -455,18 +455,32 @@ impl ImapConnection {
|
|||
let env = envelope.as_mut().unwrap();
|
||||
env.set_hash(generate_envelope_hash(&mailbox_path, &uid));
|
||||
if let Some(value) = references {
|
||||
let parse_result = crate::email::parser::address::msg_id_list(value);
|
||||
if let Ok((_, value)) = parse_result {
|
||||
let prev_val = env.references.take();
|
||||
for v in value {
|
||||
env.push_references(v);
|
||||
}
|
||||
if let Some(prev) = prev_val {
|
||||
for v in prev.refs {
|
||||
env.push_references(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
env.set_references(value);
|
||||
}
|
||||
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
|
||||
let mut tag_lck = self.uid_store.tag_index.write().unwrap();
|
||||
if let Some((flags, keywords)) = flags {
|
||||
env.set_flags(*flags);
|
||||
if !env.is_seen() {
|
||||
new_unseen.insert(env.hash());
|
||||
}
|
||||
for f in keywords {
|
||||
let hash = TagHash::from_bytes(f.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| f.to_string());
|
||||
env.tags_mut().push(hash);
|
||||
let hash = tag_hash!(f);
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f.to_string());
|
||||
}
|
||||
env.labels_mut().push(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -517,10 +531,9 @@ impl ImapConnection {
|
|||
unseen_lck.insert_set(new_unseen);
|
||||
}
|
||||
mailbox_exists.lock().unwrap().insert_set(payload_hash_set);
|
||||
// 3. tag2 UID FETCH 1:<lastseenuid> FLAGS
|
||||
// 3. tag2 UID FETCH 1:<lastseenuid> FLAGS
|
||||
if cached_max_uid == 0 {
|
||||
// TODO(#222): imap-codec does not support "CONDSTORE/QRESYNC" currently.
|
||||
self.send_command_raw(
|
||||
self.send_command(
|
||||
format!(
|
||||
"UID FETCH 1:* FLAGS (CHANGEDSINCE {})",
|
||||
cached_highestmodseq
|
||||
|
@ -529,8 +542,7 @@ impl ImapConnection {
|
|||
)
|
||||
.await?;
|
||||
} else {
|
||||
// TODO(#222): imap-codec does not support "CONDSTORE/QRESYNC" currently.
|
||||
self.send_command_raw(
|
||||
self.send_command(
|
||||
format!(
|
||||
"UID FETCH 1:{} FLAGS (CHANGEDSINCE {})",
|
||||
cached_max_uid, cached_highestmodseq
|
||||
|
@ -541,7 +553,7 @@ impl ImapConnection {
|
|||
}
|
||||
self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await?;
|
||||
// 1) update cached flags for old messages;
|
||||
//1) update cached flags for old messages;
|
||||
let mut env_lck = self.uid_store.envelopes.lock().unwrap();
|
||||
let (_, v, _) = protocol_parser::fetch_responses(&response)?;
|
||||
for FetchResponse { uid, flags, .. } in v {
|
||||
|
@ -552,19 +564,19 @@ impl ImapConnection {
|
|||
}
|
||||
let (flags, tags) = flags.unwrap();
|
||||
if env_lck[&env_hash].inner.flags() != flags
|
||||
|| env_lck[&env_hash].inner.tags()
|
||||
|| env_lck[&env_hash].inner.labels()
|
||||
!= &tags
|
||||
.iter()
|
||||
.map(|t| TagHash::from_bytes(t.as_bytes()))
|
||||
.collect::<SmallVec<[TagHash; 8]>>()
|
||||
.map(|t| tag_hash!(t))
|
||||
.collect::<SmallVec<[u64; 8]>>()
|
||||
{
|
||||
env_lck.entry(env_hash).and_modify(|entry| {
|
||||
entry.inner.set_flags(flags);
|
||||
entry.inner.tags_mut().clear();
|
||||
entry.inner.labels_mut().clear();
|
||||
entry
|
||||
.inner
|
||||
.tags_mut()
|
||||
.extend(tags.iter().map(|t| TagHash::from_bytes(t.as_bytes())));
|
||||
.labels_mut()
|
||||
.extend(tags.iter().map(|t| tag_hash!(t)));
|
||||
});
|
||||
refresh_events.push((
|
||||
uid,
|
||||
|
@ -583,20 +595,19 @@ impl ImapConnection {
|
|||
.insert(mailbox_hash, Ok(new_highestmodseq));
|
||||
}
|
||||
let mut valid_envs = BTreeSet::default();
|
||||
// This should be UID SEARCH 1:<maxuid> but it's difficult to compare to cached
|
||||
// UIDs at the point of calling this function
|
||||
self.send_command(CommandBody::search(None, SearchKey::All, true))
|
||||
.await?;
|
||||
// This should be UID SEARCH 1:<maxuid> but it's difficult to compare to cached UIDs at the
|
||||
// point of calling this function
|
||||
self.send_command(b"UID SEARCH ALL").await?;
|
||||
self.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await?;
|
||||
// 1) update cached flags for old messages;
|
||||
//1) update cached flags for old messages;
|
||||
let (_, v) = protocol_parser::search_results(response.as_slice())?;
|
||||
for uid in v {
|
||||
valid_envs.insert(generate_envelope_hash(&mailbox_path, &uid));
|
||||
}
|
||||
{
|
||||
let mut env_lck = self.uid_store.envelopes.lock().unwrap();
|
||||
let olds = env_lck
|
||||
for env_hash in env_lck
|
||||
.iter()
|
||||
.filter_map(|(h, cenv)| {
|
||||
if cenv.mailbox_hash == mailbox_hash {
|
||||
|
@ -605,8 +616,9 @@ impl ImapConnection {
|
|||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<EnvelopeHash>>();
|
||||
for env_hash in olds.difference(&valid_envs) {
|
||||
.collect::<BTreeSet<EnvelopeHash>>()
|
||||
.difference(&valid_envs)
|
||||
{
|
||||
refresh_events.push((
|
||||
env_lck[env_hash].uid,
|
||||
RefreshEvent {
|
||||
|
@ -626,8 +638,7 @@ impl ImapConnection {
|
|||
Ok(Some(payload.into_iter().map(|(_, env)| env).collect()))
|
||||
}
|
||||
|
||||
//rfc7162_Quick Flag Changes Resynchronization (CONDSTORE)_and Quick Mailbox
|
||||
// Resynchronization (QRESYNC)
|
||||
//rfc7162_Quick Flag Changes Resynchronization (CONDSTORE)_and Quick Mailbox Resynchronization (QRESYNC)
|
||||
pub async fn resync_condstoreqresync(
|
||||
&mut self,
|
||||
_cache_handle: Box<dyn ImapCache>,
|
||||
|
@ -647,8 +658,8 @@ impl ImapConnection {
|
|||
)
|
||||
};
|
||||
|
||||
/* first SELECT the mailbox to get READ/WRITE permissions (because EXAMINE
|
||||
* only returns READ-ONLY for both cases) */
|
||||
/* first SELECT the mailbox to get READ/WRITE permissions (because EXAMINE only
|
||||
* returns READ-ONLY for both cases) */
|
||||
let mut select_response = self
|
||||
.select_mailbox(mailbox_hash, &mut response, true)
|
||||
.await?
|
||||
|
@ -693,23 +704,20 @@ impl ImapConnection {
|
|||
.await?;
|
||||
if select_response.uidnext == 0 {
|
||||
/* UIDNEXT shouldn't be 0, since exists != 0 at this point */
|
||||
self.send_command(CommandBody::status(
|
||||
mailbox_path,
|
||||
[StatusDataItemName::UidNext].as_slice(),
|
||||
)?)
|
||||
.await?;
|
||||
self.send_command(format!("STATUS \"{}\" (UIDNEXT)", mailbox_path).as_bytes())
|
||||
.await?;
|
||||
self.read_response(&mut response, RequiredResponses::STATUS)
|
||||
.await?;
|
||||
let (_, status) = protocol_parser::status_response(response.as_slice())?;
|
||||
if let Some(uidnext) = status.uidnext {
|
||||
if uidnext == 0 {
|
||||
return Err(Error::new(
|
||||
return Err(MeliError::new(
|
||||
"IMAP server error: zero UIDNEXT with nonzero exists.",
|
||||
));
|
||||
}
|
||||
select_response.uidnext = uidnext;
|
||||
} else {
|
||||
return Err(Error::new("IMAP server did not reply with UIDNEXT"));
|
||||
return Err(MeliError::new("IMAP server did not reply with UIDNEXT"));
|
||||
}
|
||||
}
|
||||
Ok(select_response)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,115 +0,0 @@
|
|||
/*
|
||||
* meli - imap module.
|
||||
*
|
||||
* Copyright 2023 Damian Poddebniak <poddebniak@mailbox.org>
|
||||
*
|
||||
* This file is part of meli.
|
||||
*
|
||||
* meli is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* meli is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
use imap_codec::{
|
||||
command::{AppendError, CopyError, ListError},
|
||||
core::LiteralError,
|
||||
extensions::r#move::MoveError,
|
||||
sequence::SequenceSetError,
|
||||
};
|
||||
|
||||
use crate::error::{Error, ErrorKind};
|
||||
|
||||
impl From<LiteralError> for Error {
|
||||
#[inline]
|
||||
fn from(error: LiteralError) -> Self {
|
||||
Self {
|
||||
summary: error.to_string().into(),
|
||||
details: None,
|
||||
source: Some(Arc::new(error)),
|
||||
kind: ErrorKind::Configuration,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SequenceSetError> for Error {
|
||||
#[inline]
|
||||
fn from(error: SequenceSetError) -> Self {
|
||||
Self {
|
||||
summary: error.to_string().into(),
|
||||
details: None,
|
||||
source: Some(Arc::new(error)),
|
||||
kind: ErrorKind::Bug,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, L> From<AppendError<S, L>> for Error
|
||||
where
|
||||
AppendError<S, L>: fmt::Debug + fmt::Display + Sync + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn from(error: AppendError<S, L>) -> Self {
|
||||
Self {
|
||||
summary: error.to_string().into(),
|
||||
details: None,
|
||||
source: Some(Arc::new(error)),
|
||||
kind: ErrorKind::Bug,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, L> From<CopyError<S, L>> for Error
|
||||
where
|
||||
CopyError<S, L>: fmt::Debug + fmt::Display + Sync + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn from(error: CopyError<S, L>) -> Self {
|
||||
Self {
|
||||
summary: error.to_string().into(),
|
||||
details: None,
|
||||
source: Some(Arc::new(error)),
|
||||
kind: ErrorKind::Bug,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, M> From<MoveError<S, M>> for Error
|
||||
where
|
||||
MoveError<S, M>: fmt::Debug + fmt::Display + Sync + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn from(error: MoveError<S, M>) -> Self {
|
||||
Self {
|
||||
summary: error.to_string().into(),
|
||||
details: None,
|
||||
source: Some(Arc::new(error)),
|
||||
kind: ErrorKind::Bug,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<L1, L2> From<ListError<L1, L2>> for Error
|
||||
where
|
||||
ListError<L1, L2>: fmt::Debug + fmt::Display + Sync + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn from(error: ListError<L1, L2>) -> Self {
|
||||
Self {
|
||||
summary: error.to_string().into(),
|
||||
details: None,
|
||||
source: Some(Arc::new(error)),
|
||||
kind: ErrorKind::Bug,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,15 +19,12 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
use super::protocol_parser::SelectResponse;
|
||||
use crate::{
|
||||
backends::{
|
||||
BackendMailbox, LazyCountSet, Mailbox, MailboxHash, MailboxPermissions, SpecialUsageMailbox,
|
||||
},
|
||||
error::*,
|
||||
use crate::backends::{
|
||||
BackendMailbox, LazyCountSet, Mailbox, MailboxHash, MailboxPermissions, SpecialUsageMailbox,
|
||||
};
|
||||
use crate::error::*;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ImapMailbox {
|
||||
|
@ -54,8 +51,7 @@ impl ImapMailbox {
|
|||
&self.imap_path
|
||||
}
|
||||
|
||||
/// Establish that mailbox contents have been fetched at least once during
|
||||
/// this execution
|
||||
/// Establish that mailbox contents have been fetched at least once during this execution
|
||||
#[inline(always)]
|
||||
pub fn set_warm(&self, new_value: bool) {
|
||||
*self.warm.lock().unwrap() = new_value;
|
||||
|
@ -87,6 +83,10 @@ impl BackendMailbox for ImapMailbox {
|
|||
&self.path
|
||||
}
|
||||
|
||||
fn change_name(&mut self, s: &str) {
|
||||
self.name = s.to_string();
|
||||
}
|
||||
|
||||
fn children(&self) -> &[MailboxHash] {
|
||||
&self.children
|
||||
}
|
||||
|
|
|
@ -19,32 +19,20 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
str::FromStr,
|
||||
sync::{Arc, Mutex},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use nom::{
|
||||
branch::alt, bytes::complete::tag, combinator::map, multi::separated_list1,
|
||||
sequence::separated_pair,
|
||||
};
|
||||
|
||||
use super::{ImapConnection, ImapProtocol, ImapServerConf, UIDStore};
|
||||
use crate::{
|
||||
conf::AccountSettings,
|
||||
email::parser::IResult,
|
||||
error::{Error, Result},
|
||||
get_conf_val,
|
||||
imap::RequiredResponses,
|
||||
use crate::conf::AccountSettings;
|
||||
use crate::error::{MeliError, Result};
|
||||
use crate::get_conf_val;
|
||||
use nom::{
|
||||
branch::alt, bytes::complete::tag, combinator::map, error::ErrorKind,
|
||||
multi::separated_nonempty_list, sequence::separated_pair, IResult,
|
||||
};
|
||||
|
||||
pub struct ManageSieveConnection {
|
||||
pub inner: ImapConnection,
|
||||
}
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::SystemTime;
|
||||
|
||||
pub fn managesieve_capabilities(input: &[u8]) -> Result<Vec<(&[u8], &[u8])>> {
|
||||
let (_, ret) = separated_list1(
|
||||
let (_, ret) = separated_nonempty_list(
|
||||
tag(b"\r\n"),
|
||||
alt((
|
||||
separated_pair(quoted_raw, tag(b" "), quoted_raw),
|
||||
|
@ -54,230 +42,23 @@ pub fn managesieve_capabilities(input: &[u8]) -> Result<Vec<(&[u8], &[u8])>> {
|
|||
Ok(ret)
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub enum ManageSieveResponse<'a> {
|
||||
Ok {
|
||||
code: Option<&'a [u8]>,
|
||||
message: Option<&'a [u8]>,
|
||||
},
|
||||
NoBye {
|
||||
code: Option<&'a [u8]>,
|
||||
message: Option<&'a [u8]>,
|
||||
},
|
||||
}
|
||||
#[test]
|
||||
fn test_managesieve_capabilities() {
|
||||
assert_eq!(managesieve_capabilities(b"\"IMPLEMENTATION\" \"Dovecot Pigeonhole\"\r\n\"SIEVE\" \"fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext\"\r\n\"NOTIFY\" \"mailto\"\r\n\"SASL\" \"PLAIN\"\r\n\"STARTTLS\"\r\n\"VERSION\" \"1.0\"\r\n").unwrap(), vec![
|
||||
(&b"IMPLEMENTATION"[..],&b"Dovecot Pigeonhole"[..]),
|
||||
(&b"SIEVE"[..],&b"fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext"[..]),
|
||||
(&b"NOTIFY"[..],&b"mailto"[..]),
|
||||
(&b"SASL"[..],&b"PLAIN"[..]),
|
||||
(&b"STARTTLS"[..], &b""[..]),
|
||||
(&b"VERSION"[..],&b"1.0"[..])]
|
||||
|
||||
mod parser {
|
||||
use nom::{
|
||||
bytes::complete::tag,
|
||||
character::complete::crlf,
|
||||
combinator::{iterator, map, opt},
|
||||
};
|
||||
pub use nom::{
|
||||
bytes::complete::{is_not, tag_no_case},
|
||||
sequence::{delimited, pair, preceded, terminated},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn sieve_name(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
crate::backends::imap::protocol_parser::string_token(input)
|
||||
}
|
||||
|
||||
// *(sieve-name [SP "ACTIVE"] CRLF)
|
||||
// response-oknobye
|
||||
pub fn listscripts(input: &[u8]) -> IResult<&[u8], Vec<(&[u8], bool)>> {
|
||||
let mut it = iterator(
|
||||
input,
|
||||
alt((
|
||||
terminated(
|
||||
map(terminated(sieve_name, tag_no_case(b" ACTIVE")), |r| {
|
||||
(r, true)
|
||||
}),
|
||||
crlf,
|
||||
),
|
||||
terminated(map(sieve_name, |r| (r, false)), crlf),
|
||||
)),
|
||||
);
|
||||
|
||||
let parsed = (&mut it).collect::<Vec<(&[u8], bool)>>();
|
||||
let res: IResult<_, _> = it.finish();
|
||||
let (rest, _) = res?;
|
||||
Ok((rest, parsed))
|
||||
}
|
||||
|
||||
// response-getscript = (sieve-script CRLF response-ok) /
|
||||
// response-nobye
|
||||
pub fn getscript(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
sieve_name(input)
|
||||
}
|
||||
|
||||
pub fn response_oknobye(input: &[u8]) -> IResult<&[u8], ManageSieveResponse> {
|
||||
alt((
|
||||
map(
|
||||
terminated(
|
||||
pair(
|
||||
preceded(
|
||||
tag_no_case(b"ok"),
|
||||
opt(preceded(
|
||||
tag(b" "),
|
||||
delimited(tag(b"("), is_not(")"), tag(b")")),
|
||||
)),
|
||||
),
|
||||
opt(preceded(tag(b" "), sieve_name)),
|
||||
),
|
||||
crlf,
|
||||
),
|
||||
|(code, message)| ManageSieveResponse::Ok { code, message },
|
||||
),
|
||||
map(
|
||||
terminated(
|
||||
pair(
|
||||
preceded(
|
||||
alt((tag_no_case(b"no"), tag_no_case(b"bye"))),
|
||||
opt(preceded(
|
||||
tag(b" "),
|
||||
delimited(tag(b"("), is_not(")"), tag(b")")),
|
||||
)),
|
||||
),
|
||||
opt(preceded(tag(b" "), sieve_name)),
|
||||
),
|
||||
crlf,
|
||||
),
|
||||
|(code, message)| ManageSieveResponse::NoBye { code, message },
|
||||
),
|
||||
))(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_managesieve_listscripts() {
|
||||
let input_1 = b"\"summer_script\"\r\n\"vacation_script\"\r\n{13}\r\nclever\"script\r\n\"main_script\" ACTIVE\r\nOK";
|
||||
assert_eq!(
|
||||
terminated(listscripts, tag_no_case(b"OK"))(input_1),
|
||||
Ok((
|
||||
&b""[..],
|
||||
vec![
|
||||
(&b"summer_script"[..], false),
|
||||
(&b"vacation_script"[..], false),
|
||||
(&b"clever\"script"[..], false),
|
||||
(&b"main_script"[..], true)
|
||||
]
|
||||
))
|
||||
);
|
||||
|
||||
let input_2 = b"\"summer_script\"\r\n\"main_script\" active\r\nok";
|
||||
assert_eq!(
|
||||
terminated(listscripts, tag_no_case(b"OK"))(input_2),
|
||||
Ok((
|
||||
&b""[..],
|
||||
vec![(&b"summer_script"[..], false), (&b"main_script"[..], true)]
|
||||
))
|
||||
);
|
||||
let input_3 = b"ok";
|
||||
assert_eq!(
|
||||
terminated(listscripts, tag_no_case(b"OK"))(input_3),
|
||||
Ok((&b""[..], vec![]))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_managesieve_general() {
|
||||
assert_eq!(managesieve_capabilities(b"\"IMPLEMENTATION\" \"Dovecot Pigeonhole\"\r\n\"SIEVE\" \"fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext\"\r\n\"NOTIFY\" \"mailto\"\r\n\"SASL\" \"PLAIN\"\r\n\"STARTTLS\"\r\n\"VERSION\" \"1.0\"\r\n").unwrap(), vec![
|
||||
(&b"IMPLEMENTATION"[..],&b"Dovecot Pigeonhole"[..]),
|
||||
(&b"SIEVE"[..],&b"fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext"[..]),
|
||||
(&b"NOTIFY"[..],&b"mailto"[..]),
|
||||
(&b"SASL"[..],&b"PLAIN"[..]),
|
||||
(&b"STARTTLS"[..], &b""[..]),
|
||||
(&b"VERSION"[..],&b"1.0"[..])]
|
||||
|
||||
);
|
||||
|
||||
let response_ok = b"OK (WARNINGS) \"line 8: server redirect action limit is 2, this redirect might be ignored\"\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_ok),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::Ok {
|
||||
code: Some(&b"WARNINGS"[..]),
|
||||
message: Some(&b"line 8: server redirect action limit is 2, this redirect might be ignored"[..]),
|
||||
}
|
||||
))
|
||||
);
|
||||
let response_ok = b"OK (WARNINGS)\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_ok),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::Ok {
|
||||
code: Some(&b"WARNINGS"[..]),
|
||||
message: None,
|
||||
}
|
||||
))
|
||||
);
|
||||
let response_ok =
|
||||
b"OK \"line 8: server redirect action limit is 2, this redirect might be ignored\"\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_ok),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::Ok {
|
||||
code: None,
|
||||
message: Some(&b"line 8: server redirect action limit is 2, this redirect might be ignored"[..]),
|
||||
}
|
||||
))
|
||||
);
|
||||
let response_ok = b"Ok\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_ok),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::Ok {
|
||||
code: None,
|
||||
message: None,
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
let response_nobye = b"No (NONEXISTENT) \"There is no script by that name\"\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_nobye),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::NoBye {
|
||||
code: Some(&b"NONEXISTENT"[..]),
|
||||
message: Some(&b"There is no script by that name"[..]),
|
||||
}
|
||||
))
|
||||
);
|
||||
let response_nobye = b"No (NONEXISTENT) {31}\r\nThere is no script by that name\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_nobye),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::NoBye {
|
||||
code: Some(&b"NONEXISTENT"[..]),
|
||||
message: Some(&b"There is no script by that name"[..]),
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
let response_nobye = b"No\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_nobye),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::NoBye {
|
||||
code: None,
|
||||
message: None,
|
||||
}
|
||||
))
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Return a byte sequence surrounded by "s and decoded if necessary
|
||||
pub fn quoted_raw(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
if input.is_empty() || input[0] != b'"' {
|
||||
return Err(nom::Err::Error((input, "empty").into()));
|
||||
return Err(nom::Err::Error((input, ErrorKind::Tag)));
|
||||
}
|
||||
|
||||
let mut i = 1;
|
||||
|
@ -288,194 +69,88 @@ pub fn quoted_raw(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
|||
i += 1;
|
||||
}
|
||||
|
||||
Err(nom::Err::Error((input, "no quotes").into()))
|
||||
Err(nom::Err::Error((input, ErrorKind::Tag)))
|
||||
}
|
||||
|
||||
impl ManageSieveConnection {
|
||||
pub fn new(
|
||||
account_hash: crate::backends::AccountHash,
|
||||
account_name: String,
|
||||
s: &AccountSettings,
|
||||
event_consumer: crate::backends::BackendEventConsumer,
|
||||
) -> Result<Self> {
|
||||
let server_hostname = get_conf_val!(s["server_hostname"])?;
|
||||
let server_username = get_conf_val!(s["server_username"])?;
|
||||
let server_password = get_conf_val!(s["server_password"])?;
|
||||
let server_port = get_conf_val!(s["server_port"], 4190)?;
|
||||
let danger_accept_invalid_certs: bool =
|
||||
get_conf_val!(s["danger_accept_invalid_certs"], false)?;
|
||||
let timeout = get_conf_val!(s["timeout"], 16_u64)?;
|
||||
let timeout = if timeout == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(std::time::Duration::from_secs(timeout))
|
||||
};
|
||||
let server_conf = ImapServerConf {
|
||||
server_hostname: server_hostname.to_string(),
|
||||
server_username: server_username.to_string(),
|
||||
server_password: server_password.to_string(),
|
||||
server_port,
|
||||
use_starttls: true,
|
||||
use_tls: true,
|
||||
danger_accept_invalid_certs,
|
||||
protocol: ImapProtocol::ManageSieve,
|
||||
timeout,
|
||||
};
|
||||
let uid_store = Arc::new(UIDStore {
|
||||
is_online: Arc::new(Mutex::new((
|
||||
SystemTime::now(),
|
||||
Err(Error::new("Account is uninitialised.")),
|
||||
))),
|
||||
..UIDStore::new(
|
||||
account_hash,
|
||||
account_name.into(),
|
||||
event_consumer,
|
||||
server_conf.timeout,
|
||||
)
|
||||
});
|
||||
Ok(Self {
|
||||
inner: ImapConnection::new_connection(
|
||||
&server_conf,
|
||||
#[cfg(debug_assertions)]
|
||||
"ManageSieveConnection::new()".into(),
|
||||
uid_store,
|
||||
),
|
||||
})
|
||||
}
|
||||
pub trait ManageSieve {
|
||||
fn havespace(&mut self) -> Result<()>;
|
||||
fn putscript(&mut self) -> Result<()>;
|
||||
|
||||
pub async fn havespace(&mut self) -> Result<()> {
|
||||
fn listscripts(&mut self) -> Result<()>;
|
||||
fn setactive(&mut self) -> Result<()>;
|
||||
|
||||
fn getscript(&mut self) -> Result<()>;
|
||||
|
||||
fn deletescript(&mut self) -> Result<()>;
|
||||
fn renamescript(&mut self) -> Result<()>;
|
||||
}
|
||||
|
||||
pub fn new_managesieve_connection(
|
||||
account_hash: crate::backends::AccountHash,
|
||||
account_name: String,
|
||||
s: &AccountSettings,
|
||||
event_consumer: crate::backends::BackendEventConsumer,
|
||||
) -> Result<ImapConnection> {
|
||||
let server_hostname = get_conf_val!(s["server_hostname"])?;
|
||||
let server_username = get_conf_val!(s["server_username"])?;
|
||||
let server_password = get_conf_val!(s["server_password"])?;
|
||||
let server_port = get_conf_val!(s["server_port"], 4190)?;
|
||||
let danger_accept_invalid_certs: bool = get_conf_val!(s["danger_accept_invalid_certs"], false)?;
|
||||
let timeout = get_conf_val!(s["timeout"], 16_u64)?;
|
||||
let timeout = if timeout == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(std::time::Duration::from_secs(timeout))
|
||||
};
|
||||
let server_conf = ImapServerConf {
|
||||
server_hostname: server_hostname.to_string(),
|
||||
server_username: server_username.to_string(),
|
||||
server_password: server_password.to_string(),
|
||||
server_port,
|
||||
use_starttls: true,
|
||||
use_tls: true,
|
||||
danger_accept_invalid_certs,
|
||||
protocol: ImapProtocol::ManageSieve,
|
||||
timeout,
|
||||
};
|
||||
let uid_store = Arc::new(UIDStore {
|
||||
is_online: Arc::new(Mutex::new((
|
||||
SystemTime::now(),
|
||||
Err(MeliError::new("Account is uninitialised.")),
|
||||
))),
|
||||
..UIDStore::new(
|
||||
account_hash,
|
||||
Arc::new(account_name),
|
||||
event_consumer,
|
||||
server_conf.timeout,
|
||||
)
|
||||
});
|
||||
Ok(ImapConnection::new_connection(&server_conf, uid_store))
|
||||
}
|
||||
|
||||
impl ManageSieve for ImapConnection {
|
||||
fn havespace(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn putscript(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn putscript(&mut self, script_name: &[u8], script: &[u8]) -> Result<()> {
|
||||
let mut ret = Vec::new();
|
||||
self.inner
|
||||
.send_literal(format!("Putscript {{{len}+}}\r\n", len = script_name.len()).as_bytes())
|
||||
.await?;
|
||||
self.inner.send_literal(script_name).await?;
|
||||
self.inner
|
||||
.send_literal(format!(" {{{len}+}}\r\n", len = script.len()).as_bytes())
|
||||
.await?;
|
||||
self.inner.send_literal(script).await?;
|
||||
self.inner
|
||||
.read_response(&mut ret, RequiredResponses::empty())
|
||||
.await?;
|
||||
let (_rest, response) = parser::response_oknobye(&ret)?;
|
||||
match response {
|
||||
ManageSieveResponse::Ok { .. } => Ok(()),
|
||||
ManageSieveResponse::NoBye { code, message } => Err(format!(
|
||||
"Could not upload script: {} {}",
|
||||
code.map(String::from_utf8_lossy).unwrap_or_default(),
|
||||
message.map(String::from_utf8_lossy).unwrap_or_default()
|
||||
)
|
||||
.into()),
|
||||
}
|
||||
fn listscripts(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn setactive(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn listscripts(&mut self) -> Result<Vec<(Vec<u8>, bool)>> {
|
||||
let mut ret = Vec::new();
|
||||
self.inner.send_command_raw(b"Listscripts").await?;
|
||||
self.inner
|
||||
.read_response(&mut ret, RequiredResponses::empty())
|
||||
.await?;
|
||||
let (_rest, scripts) =
|
||||
parser::terminated(parser::listscripts, parser::tag_no_case(b"OK"))(&ret)?;
|
||||
Ok(scripts
|
||||
.into_iter()
|
||||
.map(|(n, a)| (n.to_vec(), a))
|
||||
.collect::<Vec<(Vec<u8>, bool)>>())
|
||||
fn getscript(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn checkscript(&mut self, script: &[u8]) -> Result<()> {
|
||||
let mut ret = Vec::new();
|
||||
self.inner
|
||||
.send_literal(format!("Checkscript {{{len}+}}\r\n", len = script.len()).as_bytes())
|
||||
.await?;
|
||||
self.inner.send_literal(script).await?;
|
||||
self.inner
|
||||
.read_response(&mut ret, RequiredResponses::empty())
|
||||
.await?;
|
||||
let (_rest, response) = parser::response_oknobye(&ret)?;
|
||||
match response {
|
||||
ManageSieveResponse::Ok { .. } => Ok(()),
|
||||
ManageSieveResponse::NoBye { code, message } => Err(format!(
|
||||
"Checkscript reply: {} {}",
|
||||
code.map(String::from_utf8_lossy).unwrap_or_default(),
|
||||
message.map(String::from_utf8_lossy).unwrap_or_default()
|
||||
)
|
||||
.into()),
|
||||
}
|
||||
fn deletescript(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn setactive(&mut self, script_name: &[u8]) -> Result<()> {
|
||||
let mut ret = Vec::new();
|
||||
self.inner
|
||||
.send_literal(format!("Setactive {{{len}+}}\r\n", len = script_name.len()).as_bytes())
|
||||
.await?;
|
||||
self.inner.send_literal(script_name).await?;
|
||||
self.inner
|
||||
.read_response(&mut ret, RequiredResponses::empty())
|
||||
.await?;
|
||||
let (_rest, response) = parser::response_oknobye(&ret)?;
|
||||
match response {
|
||||
ManageSieveResponse::Ok { .. } => Ok(()),
|
||||
ManageSieveResponse::NoBye { code, message } => Err(format!(
|
||||
"Could not set active script: {} {}",
|
||||
code.map(String::from_utf8_lossy).unwrap_or_default(),
|
||||
message.map(String::from_utf8_lossy).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(String::from_utf8_lossy).unwrap_or_default(),
|
||||
message.map(String::from_utf8_lossy).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(String::from_utf8_lossy).unwrap_or_default(),
|
||||
message.map(String::from_utf8_lossy).unwrap_or_default()
|
||||
)
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn renamescript(&mut self) -> Result<()> {
|
||||
fn renamescript(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,12 +19,12 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use imap_codec::fetch::MessageDataItemName;
|
||||
|
||||
use super::*;
|
||||
use crate::{backends::*, email::*, error::Error};
|
||||
|
||||
use crate::backends::*;
|
||||
use crate::email::*;
|
||||
use crate::error::MeliError;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// `BackendOp` implementor for Imap
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -42,7 +42,7 @@ impl ImapOp {
|
|||
connection: Arc<FutureMutex<ImapConnection>>,
|
||||
uid_store: Arc<UIDStore>,
|
||||
) -> Self {
|
||||
Self {
|
||||
ImapOp {
|
||||
uid,
|
||||
connection,
|
||||
mailbox_hash,
|
||||
|
@ -70,12 +70,8 @@ impl BackendOp for ImapOp {
|
|||
conn.connect().await?;
|
||||
conn.examine_mailbox(mailbox_hash, &mut response, false)
|
||||
.await?;
|
||||
conn.send_command(CommandBody::fetch(
|
||||
uid,
|
||||
vec![MessageDataItemName::Flags, MessageDataItemName::Rfc822],
|
||||
true,
|
||||
)?)
|
||||
.await?;
|
||||
conn.send_command(format!("UID FETCH {} (FLAGS RFC822)", uid).as_bytes())
|
||||
.await?;
|
||||
conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await?;
|
||||
}
|
||||
|
@ -86,10 +82,11 @@ impl BackendOp for ImapOp {
|
|||
);
|
||||
let mut results = protocol_parser::fetch_responses(&response)?.1;
|
||||
if results.len() != 1 {
|
||||
return Err(
|
||||
Error::new(format!("Invalid/unexpected response: {:?}", response))
|
||||
.set_summary(format!("message with UID {} was not found?", uid)),
|
||||
);
|
||||
return Err(MeliError::new(format!(
|
||||
"Invalid/unexpected response: {:?}",
|
||||
response
|
||||
))
|
||||
.set_summary(format!("message with UID {} was not found?", uid)));
|
||||
}
|
||||
let FetchResponse {
|
||||
uid: _uid,
|
||||
|
@ -133,12 +130,8 @@ impl BackendOp for ImapOp {
|
|||
conn.connect().await?;
|
||||
conn.examine_mailbox(mailbox_hash, &mut response, false)
|
||||
.await?;
|
||||
conn.send_command(CommandBody::fetch(
|
||||
uid,
|
||||
vec![MessageDataItemName::Flags],
|
||||
true,
|
||||
)?)
|
||||
.await?;
|
||||
conn.send_command(format!("UID FETCH {} FLAGS", uid).as_bytes())
|
||||
.await?;
|
||||
conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await?;
|
||||
debug!(
|
||||
|
@ -148,19 +141,20 @@ impl BackendOp for ImapOp {
|
|||
);
|
||||
let v = protocol_parser::uid_fetch_flags_responses(&response)
|
||||
.map(|(_, v)| v)
|
||||
.map_err(Error::from)?;
|
||||
.map_err(MeliError::from)?;
|
||||
if v.len() != 1 {
|
||||
debug!("responses len is {}", v.len());
|
||||
debug!(String::from_utf8_lossy(&response));
|
||||
/* TODO: Trigger cache invalidation here. */
|
||||
debug!("message with UID {} was not found", uid);
|
||||
return Err(
|
||||
Error::new(format!("Invalid/unexpected response: {:?}", response))
|
||||
.set_summary(format!("message with UID {} was not found?", uid)),
|
||||
);
|
||||
return Err(MeliError::new(format!(
|
||||
"Invalid/unexpected response: {:?}",
|
||||
response
|
||||
))
|
||||
.set_summary(format!("message with UID {} was not found?", uid)));
|
||||
}
|
||||
let (_uid, (_flags, _)) = v[0];
|
||||
assert_eq!(_uid, uid);
|
||||
assert_eq!(uid, uid);
|
||||
let mut bytes_cache = uid_store.byte_cache.lock()?;
|
||||
let cache = bytes_cache.entry(uid).or_default();
|
||||
cache.flags = Some(_flags);
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,311 +0,0 @@
|
|||
/*
|
||||
* meli - imap module.
|
||||
*
|
||||
* Copyright 2017 - 2023 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/>.
|
||||
*/
|
||||
|
||||
//! Convert [`crate::search::Query`] into IMAP search criteria.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::{
|
||||
search::*,
|
||||
utils::datetime::{formats::IMAP_DATE, timestamp_to_string},
|
||||
};
|
||||
|
||||
mod private {
|
||||
pub trait Sealed {}
|
||||
}
|
||||
|
||||
pub trait ToImapSearch: private::Sealed {
|
||||
/// Convert [`crate::search::Query`] into IMAP search criteria.
|
||||
fn to_imap_search(&self) -> String;
|
||||
}
|
||||
|
||||
impl private::Sealed for Query {}
|
||||
|
||||
macro_rules! space_pad {
|
||||
($s:ident) => {{
|
||||
if !$s.is_empty() && !$s.ends_with('(') && !$s.ends_with(' ') {
|
||||
$s.push(' ');
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
impl ToImapSearch for Query {
|
||||
fn to_imap_search(&self) -> String {
|
||||
enum Step<'a> {
|
||||
Q(&'a Query),
|
||||
Lit(char),
|
||||
}
|
||||
use Step::*;
|
||||
|
||||
let mut stack = VecDeque::new();
|
||||
stack.push_front(Q(self));
|
||||
let mut s = String::new();
|
||||
while let Some(q) = stack.pop_front() {
|
||||
use Query::*;
|
||||
match q {
|
||||
Lit(lit) => {
|
||||
s.push(lit);
|
||||
}
|
||||
Q(Subject(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("SUBJECT \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Q(From(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("FROM \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Q(To(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("TO \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Q(Cc(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("CC \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Q(Bcc(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("BCC \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Q(AllText(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("TEXT \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Q(Flags(v)) => {
|
||||
space_pad!(s);
|
||||
for f in v {
|
||||
match f.as_str() {
|
||||
"draft" => {
|
||||
s.push_str("DRAFT ");
|
||||
}
|
||||
"deleted" => {
|
||||
s.push_str("DELETED ");
|
||||
}
|
||||
"flagged" => {
|
||||
s.push_str("FLAGGED ");
|
||||
}
|
||||
"recent" => {
|
||||
s.push_str("RECENT ");
|
||||
}
|
||||
"seen" | "read" => {
|
||||
s.push_str("SEEN ");
|
||||
}
|
||||
"unseen" | "unread" => {
|
||||
s.push_str("UNSEEN ");
|
||||
}
|
||||
"answered" => {
|
||||
s.push_str("ANSWERED ");
|
||||
}
|
||||
"unanswered" => {
|
||||
s.push_str("UNANSWERED ");
|
||||
}
|
||||
keyword => {
|
||||
s.push_str("KEYWORD ");
|
||||
s.push_str(keyword);
|
||||
s.push(' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Q(And(q1, q2)) => {
|
||||
let is_empty = space_pad!(s);
|
||||
if !is_empty {
|
||||
stack.push_front(Lit(')'));
|
||||
}
|
||||
stack.push_front(Q(q2));
|
||||
stack.push_front(Q(q1));
|
||||
if !is_empty {
|
||||
stack.push_front(Lit('('));
|
||||
}
|
||||
}
|
||||
Q(Or(q1, q2)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("OR");
|
||||
stack.push_front(Q(q2));
|
||||
stack.push_front(Q(q1));
|
||||
}
|
||||
Q(Not(q)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("NOT (");
|
||||
stack.push_front(Lit(')'));
|
||||
stack.push_front(Q(q));
|
||||
}
|
||||
Q(Before(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("BEFORE ");
|
||||
s.push_str(×tamp_to_string(*t, Some(IMAP_DATE), true));
|
||||
}
|
||||
Q(After(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("SINCE ");
|
||||
s.push_str(×tamp_to_string(*t, Some(IMAP_DATE), true));
|
||||
}
|
||||
Q(Between(t1, t2)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("(SINCE ");
|
||||
s.push_str(×tamp_to_string(*t1, Some(IMAP_DATE), true));
|
||||
s.push_str(" BEFORE ");
|
||||
s.push_str(×tamp_to_string(*t2, Some(IMAP_DATE), true));
|
||||
s.push(')');
|
||||
}
|
||||
Q(On(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str("ON ");
|
||||
s.push_str(×tamp_to_string(*t, Some(IMAP_DATE), true));
|
||||
}
|
||||
Q(InReplyTo(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str(r#"HEADER "In-Reply-To" ""#);
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Q(References(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str(r#"HEADER "References" ""#);
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Q(AllAddresses(t)) => {
|
||||
let is_empty = space_pad!(s);
|
||||
if !is_empty {
|
||||
s.push('(');
|
||||
}
|
||||
s.push_str("OR FROM \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push_str("\" (OR TO \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push_str("\" (OR CC \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push_str("\" BCC \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push_str(r#""))"#);
|
||||
if !is_empty {
|
||||
s.push(')');
|
||||
}
|
||||
}
|
||||
Q(Body(t)) => {
|
||||
space_pad!(s);
|
||||
s.push_str(r#"BODY ""#);
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Q(HasAttachment) => {
|
||||
log::warn!("HasAttachment in IMAP is unimplemented.");
|
||||
}
|
||||
Q(Answered) => {
|
||||
space_pad!(s);
|
||||
s.push_str(r#"ANSWERED ""#);
|
||||
}
|
||||
Q(AnsweredBy { by }) => {
|
||||
space_pad!(s);
|
||||
s.push_str(r#"HEADER "From" ""#);
|
||||
s.extend(escape_double_quote(by).chars());
|
||||
s.push('"');
|
||||
}
|
||||
Q(Larger { than }) => {
|
||||
space_pad!(s);
|
||||
s.push_str("LARGER ");
|
||||
s.push_str(&than.to_string());
|
||||
}
|
||||
Q(Smaller { than }) => {
|
||||
space_pad!(s);
|
||||
s.push_str("SMALLER ");
|
||||
s.push_str(&than.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
while s.ends_with(' ') {
|
||||
s.pop();
|
||||
}
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::utils::parsec::Parser;
|
||||
|
||||
#[test]
|
||||
fn test_imap_query_search() {
|
||||
let (_, q) = query().parse_complete("subject: test and i").unwrap();
|
||||
assert_eq!(&q.to_imap_search(), r#"SUBJECT "test" TEXT "i""#);
|
||||
|
||||
let (_, q) = query().parse_complete("is:unseen").unwrap();
|
||||
assert_eq!(&q.to_imap_search(), r#"UNSEEN"#);
|
||||
|
||||
let (_, q) = query().parse_complete("from:user@example.org").unwrap();
|
||||
assert_eq!(&q.to_imap_search(), r#"FROM "user@example.org""#);
|
||||
|
||||
let (_, q) = query()
|
||||
.parse_complete(
|
||||
"from:user@example.org and subject:
|
||||
\"foobar space\"",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
&q.to_imap_search(),
|
||||
r#"FROM "user@example.org" SUBJECT "foobar space""#
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
×tamp_to_string(1685739600, Some(IMAP_DATE), true),
|
||||
"03-Jun-2023"
|
||||
);
|
||||
|
||||
let (_, q) = query()
|
||||
.parse_complete("before:2023-06-04 from:user@example.org")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
&q.to_imap_search(),
|
||||
r#"BEFORE 04-Jun-2023 FROM "user@example.org""#
|
||||
);
|
||||
let (_, q) = query()
|
||||
.parse_complete(r#"subject:"wah ah ah" or (from:Manos and from:Sia)"#)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
&q.to_imap_search(),
|
||||
r#"OR SUBJECT "wah ah ah" (FROM "Manos" FROM "Sia")"#
|
||||
);
|
||||
|
||||
let (_, q) = query()
|
||||
.parse_complete(r#"subject:wo or (all-addresses:Manos)"#)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
&q.to_imap_search(),
|
||||
r#"OR SUBJECT "wo" (OR FROM "Manos" (OR TO "Manos" (OR CC "Manos" BCC "Manos")))"#
|
||||
);
|
||||
}
|
||||
}
|
|
@ -19,24 +19,17 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
use imap_codec::{command::CommandBody, search::SearchKey, sequence::SequenceSet};
|
||||
|
||||
use super::{ImapConnection, MailboxSelection, UID};
|
||||
use crate::{
|
||||
backends::{
|
||||
imap::protocol_parser::{
|
||||
generate_envelope_hash, FetchResponse, ImapLineSplit, RequiredResponses,
|
||||
UntaggedResponse,
|
||||
},
|
||||
BackendMailbox, RefreshEvent,
|
||||
RefreshEventKind::{self, *},
|
||||
TagHash,
|
||||
},
|
||||
email::common_attributes,
|
||||
error::*,
|
||||
use crate::backends::imap::protocol_parser::{
|
||||
generate_envelope_hash, FetchResponse, ImapLineSplit, RequiredResponses, UntaggedResponse,
|
||||
};
|
||||
use crate::backends::BackendMailbox;
|
||||
use crate::backends::{
|
||||
RefreshEvent,
|
||||
RefreshEventKind::{self, *},
|
||||
};
|
||||
use crate::error::*;
|
||||
use std::convert::TryInto;
|
||||
|
||||
impl ImapConnection {
|
||||
pub async fn process_untagged(&mut self, line: &[u8]) -> Result<bool> {
|
||||
|
@ -92,12 +85,7 @@ impl ImapConnection {
|
|||
n,
|
||||
self.uid_store.msn_index.lock().unwrap().get(&mailbox_hash)
|
||||
);
|
||||
self.send_command(CommandBody::search(
|
||||
None,
|
||||
SearchKey::SequenceSet(SequenceSet::from(..)),
|
||||
true,
|
||||
))
|
||||
.await?;
|
||||
self.send_command("UID SEARCH 1:*".as_bytes()).await?;
|
||||
self.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await?;
|
||||
let results = super::protocol_parser::search_results(&response)?
|
||||
|
@ -115,7 +103,7 @@ impl ImapConnection {
|
|||
);
|
||||
}
|
||||
let mut events = vec![];
|
||||
let deleteds = self
|
||||
for (deleted_uid, deleted_hash) in self
|
||||
.uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
|
@ -125,8 +113,8 @@ impl ImapConnection {
|
|||
*mailbox_hash_ == mailbox_hash && !results.contains(u)
|
||||
})
|
||||
.map(|((_, uid), hash)| (*uid, *hash))
|
||||
.collect::<Vec<(UID, crate::email::EnvelopeHash)>>();
|
||||
for (deleted_uid, deleted_hash) in deleteds {
|
||||
.collect::<Vec<(UID, crate::email::EnvelopeHash)>>()
|
||||
{
|
||||
mailbox.exists.lock().unwrap().remove(deleted_hash);
|
||||
mailbox.unseen.lock().unwrap().remove(deleted_hash);
|
||||
self.uid_store
|
||||
|
@ -207,7 +195,7 @@ impl ImapConnection {
|
|||
debug!("exists {}", n);
|
||||
try_fail!(
|
||||
mailbox_hash,
|
||||
self.send_command(CommandBody::fetch(n, common_attributes(), false)?).await
|
||||
self.send_command(format!("FETCH {} (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)", n).as_bytes()).await
|
||||
self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED).await
|
||||
);
|
||||
let mut v = match super::protocol_parser::fetch_responses(&response) {
|
||||
|
@ -234,20 +222,34 @@ 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 {
|
||||
let parse_result = crate::email::parser::address::msg_id_list(value);
|
||||
if let Ok((_, value)) = parse_result {
|
||||
let prev_val = env.references.take();
|
||||
for v in value {
|
||||
env.push_references(v);
|
||||
}
|
||||
if let Some(prev) = prev_val {
|
||||
for v in prev.refs {
|
||||
env.push_references(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
env.set_references(value);
|
||||
}
|
||||
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
|
||||
let mut tag_lck = self.uid_store.tag_index.write().unwrap();
|
||||
if let Some((flags, keywords)) = flags {
|
||||
env.set_flags(*flags);
|
||||
if !env.is_seen() {
|
||||
mailbox.unseen.lock().unwrap().insert_new(env.hash());
|
||||
}
|
||||
for f in keywords {
|
||||
let hash = TagHash::from_bytes(f.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| f.to_string());
|
||||
env.tags_mut().push(hash);
|
||||
let hash = tag_hash!(f);
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f.to_string());
|
||||
}
|
||||
env.labels_mut().push(hash);
|
||||
}
|
||||
}
|
||||
mailbox.exists.lock().unwrap().insert_new(env.hash());
|
||||
|
@ -293,7 +295,7 @@ impl ImapConnection {
|
|||
)
|
||||
})
|
||||
{
|
||||
log::info!("{err}");
|
||||
crate::log(err.to_string(), crate::INFO);
|
||||
}
|
||||
}
|
||||
for response in v {
|
||||
|
@ -313,12 +315,12 @@ impl ImapConnection {
|
|||
UntaggedResponse::Recent(_) => {
|
||||
try_fail!(
|
||||
mailbox_hash,
|
||||
self.send_command(CommandBody::search(None, SearchKey::Recent, true)).await
|
||||
self.send_command(b"UID SEARCH RECENT").await
|
||||
self.read_response(&mut response, RequiredResponses::SEARCH).await
|
||||
);
|
||||
match super::protocol_parser::search_results_raw(&response)
|
||||
.map(|(_, v)| v)
|
||||
.map_err(Error::from)
|
||||
.map_err(MeliError::from)
|
||||
{
|
||||
Ok(&[]) => {
|
||||
debug!("UID SEARCH RECENT returned no results");
|
||||
|
@ -327,20 +329,15 @@ impl ImapConnection {
|
|||
let command = {
|
||||
let mut iter = v.split(u8::is_ascii_whitespace);
|
||||
let first = iter.next().unwrap_or(v);
|
||||
let mut accum = to_str!(first).trim().to_string();
|
||||
let mut accum = format!("{}", to_str!(first).trim());
|
||||
for ms in iter {
|
||||
accum.push(',');
|
||||
accum.push_str(to_str!(ms).trim());
|
||||
accum = format!("{},{}", accum, to_str!(ms).trim());
|
||||
}
|
||||
format!(
|
||||
"UID FETCH {} (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS \
|
||||
(REFERENCES)] BODYSTRUCTURE)",
|
||||
accum
|
||||
)
|
||||
format!("UID FETCH {} (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)", accum)
|
||||
};
|
||||
try_fail!(
|
||||
mailbox_hash,
|
||||
self.send_command_raw(command.as_bytes()).await
|
||||
self.send_command(command.as_bytes()).await
|
||||
self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED).await
|
||||
);
|
||||
let mut v = match super::protocol_parser::fetch_responses(&response) {
|
||||
|
@ -367,20 +364,35 @@ 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 {
|
||||
let parse_result =
|
||||
crate::email::parser::address::msg_id_list(value);
|
||||
if let Ok((_, value)) = parse_result {
|
||||
let prev_val = env.references.take();
|
||||
for v in value {
|
||||
env.push_references(v);
|
||||
}
|
||||
if let Some(prev) = prev_val {
|
||||
for v in prev.refs {
|
||||
env.push_references(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
env.set_references(value);
|
||||
}
|
||||
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
|
||||
let mut tag_lck = self.uid_store.tag_index.write().unwrap();
|
||||
if let Some((flags, keywords)) = flags {
|
||||
env.set_flags(*flags);
|
||||
if !env.is_seen() {
|
||||
mailbox.unseen.lock().unwrap().insert_new(env.hash());
|
||||
}
|
||||
for f in keywords {
|
||||
let hash = TagHash::from_bytes(f.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| f.to_string());
|
||||
env.tags_mut().push(hash);
|
||||
let hash = tag_hash!(f);
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f.to_string());
|
||||
}
|
||||
env.labels_mut().push(hash);
|
||||
}
|
||||
}
|
||||
mailbox.exists.lock().unwrap().insert_new(env.hash());
|
||||
|
@ -395,7 +407,7 @@ impl ImapConnection {
|
|||
)
|
||||
})
|
||||
{
|
||||
log::info!("{err}");
|
||||
crate::log(err.to_string(), crate::INFO);
|
||||
}
|
||||
}
|
||||
for response in v {
|
||||
|
@ -469,12 +481,8 @@ impl ImapConnection {
|
|||
} else {
|
||||
try_fail!(
|
||||
mailbox_hash,
|
||||
self.send_command(CommandBody::search(
|
||||
None,
|
||||
SearchKey::SequenceSet(SequenceSet::try_from(msg_seq)?),
|
||||
true
|
||||
))
|
||||
.await,
|
||||
self.send_command(format!("UID SEARCH {}", msg_seq).as_bytes())
|
||||
.await,
|
||||
self.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await,
|
||||
);
|
||||
|
|
|
@ -18,12 +18,9 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
use std::sync::Arc;
|
||||
|
||||
use imap_codec::search::SearchKey;
|
||||
|
||||
use super::*;
|
||||
use crate::backends::SpecialUsageMailbox;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Arguments for IMAP watching functions
|
||||
pub struct ImapWatchKit {
|
||||
|
@ -55,8 +52,8 @@ pub async fn poll_with_examine(kit: ImapWatchKit) -> Result<()> {
|
|||
|
||||
pub async fn idle(kit: ImapWatchKit) -> Result<()> {
|
||||
debug!("IDLE");
|
||||
/* IDLE only watches the connection's selected mailbox. We will IDLE on INBOX
|
||||
* and every ~5 minutes wake up and poll the others */
|
||||
/* IDLE only watches the connection's selected mailbox. We will IDLE on INBOX and every ~5
|
||||
* minutes wake up and poll the others */
|
||||
let ImapWatchKit {
|
||||
mut conn,
|
||||
main_conn,
|
||||
|
@ -73,10 +70,7 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> {
|
|||
{
|
||||
Some(mailbox) => mailbox,
|
||||
None => {
|
||||
return Err(Error::new(
|
||||
"INBOX mailbox not found in local mailbox index. meli may have not parsed the \
|
||||
IMAP mailboxes correctly",
|
||||
));
|
||||
return Err(MeliError::new("INBOX mailbox not found in local mailbox index. meli may have not parsed the IMAP mailboxes correctly"));
|
||||
}
|
||||
};
|
||||
let mailbox_hash = mailbox.hash();
|
||||
|
@ -122,7 +116,7 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> {
|
|||
}
|
||||
examine_updates(mailbox, &mut conn, &uid_store).await?;
|
||||
}
|
||||
conn.send_command(CommandBody::Idle).await?;
|
||||
conn.send_command(b"IDLE").await?;
|
||||
let mut blockn = ImapBlockingConnection::from(conn);
|
||||
let mut watch = std::time::Instant::now();
|
||||
/* duration interval to send heartbeat */
|
||||
|
@ -146,7 +140,7 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> {
|
|||
.conn
|
||||
.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
blockn.conn.send_command(CommandBody::Idle).await?;
|
||||
blockn.conn.send_command(b"IDLE").await?;
|
||||
let mut main_conn_lck = timeout(uid_store.timeout, main_conn.lock()).await?;
|
||||
main_conn_lck.connect().await?;
|
||||
continue;
|
||||
|
@ -194,7 +188,7 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> {
|
|||
}
|
||||
blockn.conn.process_untagged(l).await?;
|
||||
}
|
||||
blockn.conn.send_command(CommandBody::Idle).await?;
|
||||
blockn.conn.send_command(b"IDLE").await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -261,8 +255,7 @@ pub async fn examine_updates(
|
|||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"LIST-STATUS"));
|
||||
if has_list_status {
|
||||
// TODO(#222): imap-codec does not support "LIST Command Extensions" currently.
|
||||
conn.send_command_raw(
|
||||
conn.send_command(
|
||||
format!(
|
||||
"LIST \"{}\" \"\" RETURN (STATUS (MESSAGES UNSEEN))",
|
||||
mailbox.imap_path()
|
||||
|
@ -283,7 +276,7 @@ pub async fn examine_updates(
|
|||
if !l.starts_with(b"*") {
|
||||
continue;
|
||||
}
|
||||
if let Ok(status) = protocol_parser::status_response(l).map(|(_, v)| v) {
|
||||
if let Ok(status) = protocol_parser::status_response(&l).map(|(_, v)| v) {
|
||||
if Some(mailbox_hash) == status.mailbox {
|
||||
if let Some(total) = status.messages {
|
||||
if let Ok(mut exists_lck) = mailbox.exists.lock() {
|
||||
|
@ -302,8 +295,7 @@ pub async fn examine_updates(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
conn.send_command(CommandBody::search(None, SearchKey::Unseen, false))
|
||||
.await?;
|
||||
conn.send_command(b"SEARCH UNSEEN").await?;
|
||||
conn.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await?;
|
||||
let unseen_count = protocol_parser::search_results(&response)?.1.len();
|
||||
|
@ -322,8 +314,7 @@ pub async fn examine_updates(
|
|||
|
||||
if select_response.recent > 0 {
|
||||
/* UID SEARCH RECENT */
|
||||
conn.send_command(CommandBody::search(None, SearchKey::Recent, true))
|
||||
.await?;
|
||||
conn.send_command(b"UID SEARCH RECENT").await?;
|
||||
conn.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await?;
|
||||
let v = protocol_parser::search_results(response.as_slice()).map(|(_, v)| v)?;
|
||||
|
@ -334,15 +325,31 @@ pub async fn examine_updates(
|
|||
);
|
||||
return Ok(());
|
||||
}
|
||||
conn.send_command(CommandBody::fetch(v.as_slice(), common_attributes(), true)?)
|
||||
.await?;
|
||||
let mut cmd = "UID FETCH ".to_string();
|
||||
if v.len() == 1 {
|
||||
cmd.push_str(&v[0].to_string());
|
||||
} else {
|
||||
cmd.push_str(&v[0].to_string());
|
||||
for n in v.into_iter().skip(1) {
|
||||
cmd.push(',');
|
||||
cmd.push_str(&n.to_string());
|
||||
}
|
||||
}
|
||||
cmd.push_str(
|
||||
" (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)",
|
||||
);
|
||||
conn.send_command(cmd.as_bytes()).await?;
|
||||
conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await?;
|
||||
} else if select_response.exists > mailbox.exists.lock().unwrap().len() {
|
||||
let min = std::cmp::max(mailbox.exists.lock().unwrap().len(), 1);
|
||||
|
||||
conn.send_command(CommandBody::fetch(min.., common_attributes(), false)?)
|
||||
.await?;
|
||||
conn.send_command(
|
||||
format!(
|
||||
"FETCH {}:* (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)",
|
||||
std::cmp::max(mailbox.exists.lock().unwrap().len(), 1)
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await?;
|
||||
} else {
|
||||
|
@ -365,11 +372,23 @@ pub async fn examine_updates(
|
|||
{
|
||||
let uid = uid.unwrap();
|
||||
let env = envelope.as_mut().unwrap();
|
||||
env.set_hash(generate_envelope_hash(mailbox.imap_path(), &uid));
|
||||
env.set_hash(generate_envelope_hash(&mailbox.imap_path(), &uid));
|
||||
if let Some(value) = references {
|
||||
let parse_result = crate::email::parser::address::msg_id_list(value);
|
||||
if let Ok((_, value)) = parse_result {
|
||||
let prev_val = env.references.take();
|
||||
for v in value {
|
||||
env.push_references(v);
|
||||
}
|
||||
if let Some(prev) = prev_val {
|
||||
for v in prev.refs {
|
||||
env.push_references(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
env.set_references(value);
|
||||
}
|
||||
let mut tag_lck = uid_store.collection.tag_index.write().unwrap();
|
||||
let mut tag_lck = uid_store.tag_index.write().unwrap();
|
||||
if let Some((flags, keywords)) = flags {
|
||||
env.set_flags(*flags);
|
||||
if !env.is_seen() {
|
||||
|
@ -377,21 +396,25 @@ pub async fn examine_updates(
|
|||
}
|
||||
mailbox.exists.lock().unwrap().insert_new(env.hash());
|
||||
for f in keywords {
|
||||
let hash = TagHash::from_bytes(f.as_bytes());
|
||||
tag_lck.entry(hash).or_insert_with(|| f.to_string());
|
||||
env.tags_mut().push(hash);
|
||||
let hash = tag_hash!(f);
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f.to_string());
|
||||
}
|
||||
env.labels_mut().push(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
if uid_store.keep_offline_cache && cache_handle.mailbox_state(mailbox_hash)?.is_some() {
|
||||
cache_handle
|
||||
.insert_envelopes(mailbox_hash, &v)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not save envelopes in cache for mailbox {}",
|
||||
mailbox.imap_path()
|
||||
)
|
||||
})?;
|
||||
if uid_store.keep_offline_cache {
|
||||
if !cache_handle.mailbox_state(mailbox_hash)?.is_none() {
|
||||
cache_handle
|
||||
.insert_envelopes(mailbox_hash, &v)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not save envelopes in cache for mailbox {}",
|
||||
mailbox.imap_path()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
for FetchResponse { uid, envelope, .. } in v {
|
||||
|
|
|
@ -19,26 +19,34 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
convert::TryFrom,
|
||||
str::FromStr,
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::backends::*;
|
||||
use crate::conf::AccountSettings;
|
||||
use crate::email::*;
|
||||
use crate::error::{MeliError, Result};
|
||||
use futures::lock::Mutex as FutureMutex;
|
||||
use isahc::{config::RedirectPolicy, AsyncReadResponseExt, HttpClient};
|
||||
use isahc::config::RedirectPolicy;
|
||||
use isahc::prelude::HttpClient;
|
||||
use isahc::ResponseExt;
|
||||
use serde_json::Value;
|
||||
use std::collections::{hash_map::DefaultHasher, BTreeMap, HashMap, HashSet};
|
||||
use std::convert::TryFrom;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::{
|
||||
backends::*,
|
||||
conf::AccountSettings,
|
||||
email::*,
|
||||
error::{Error, Result},
|
||||
utils::futures::{sleep, timeout},
|
||||
Collection,
|
||||
};
|
||||
macro_rules! tag_hash {
|
||||
($t:ident) => {{
|
||||
let mut hasher = DefaultHasher::default();
|
||||
$t.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}};
|
||||
($t:literal) => {{
|
||||
let mut hasher = DefaultHasher::default();
|
||||
$t.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! _impl {
|
||||
|
@ -84,25 +92,24 @@ use mailbox::*;
|
|||
#[derive(Debug, Default)]
|
||||
pub struct EnvelopeCache {
|
||||
bytes: Option<String>,
|
||||
// headers: Option<String>,
|
||||
// body: Option<String>,
|
||||
// flags: Option<Flag>,
|
||||
headers: Option<String>,
|
||||
body: Option<String>,
|
||||
flags: Option<Flag>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JmapServerConf {
|
||||
pub server_url: String,
|
||||
pub server_hostname: String,
|
||||
pub server_username: String,
|
||||
pub server_password: String,
|
||||
pub use_token: bool,
|
||||
pub server_port: u16,
|
||||
pub danger_accept_invalid_certs: bool,
|
||||
pub timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
macro_rules! get_conf_val {
|
||||
($s:ident[$var:literal]) => {
|
||||
$s.extra.get($var).ok_or_else(|| {
|
||||
Error::new(format!(
|
||||
MeliError::new(format!(
|
||||
"Configuration error ({}): JMAP connection requires the field `{}` set",
|
||||
$s.name.as_str(),
|
||||
$var
|
||||
|
@ -114,7 +121,7 @@ macro_rules! get_conf_val {
|
|||
.get($var)
|
||||
.map(|v| {
|
||||
<_>::from_str(v).map_err(|e| {
|
||||
Error::new(format!(
|
||||
MeliError::new(format!(
|
||||
"Configuration error ({}): Invalid value for field `{}`: {}\n{}",
|
||||
$s.name.as_str(),
|
||||
$var,
|
||||
|
@ -129,36 +136,44 @@ macro_rules! get_conf_val {
|
|||
|
||||
impl JmapServerConf {
|
||||
pub fn new(s: &AccountSettings) -> Result<Self> {
|
||||
let use_token: bool = get_conf_val!(s["use_token"], false)?;
|
||||
|
||||
if use_token
|
||||
&& !(s.extra.contains_key("server_password_command")
|
||||
^ s.extra.contains_key("server_password"))
|
||||
{
|
||||
return Err(Error::new(format!(
|
||||
"({}) `use_token` use requires either the `server_password_command` set with a \
|
||||
command that returns an Bearer token of your account, or `server_password` with \
|
||||
the API Bearer token as a string. Consult documentation for guidance.",
|
||||
s.name,
|
||||
)));
|
||||
}
|
||||
Ok(Self {
|
||||
server_url: get_conf_val!(s["server_url"])?.to_string(),
|
||||
Ok(JmapServerConf {
|
||||
server_hostname: get_conf_val!(s["server_hostname"])?.to_string(),
|
||||
server_username: get_conf_val!(s["server_username"])?.to_string(),
|
||||
server_password: s.server_password()?,
|
||||
use_token,
|
||||
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 {
|
||||
None
|
||||
} else {
|
||||
Some(Duration::from_secs(t))
|
||||
}
|
||||
})?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! get_conf_val {
|
||||
($s:ident[$var:literal]) => {
|
||||
$s.extra.get($var).ok_or_else(|| {
|
||||
MeliError::new(format!(
|
||||
"Configuration error ({}): JMAP connection requires the field `{}` set",
|
||||
$s.name.as_str(),
|
||||
$var
|
||||
))
|
||||
})
|
||||
};
|
||||
($s:ident[$var:literal], $default:expr) => {
|
||||
$s.extra
|
||||
.get($var)
|
||||
.map(|v| {
|
||||
<_>::from_str(v).map_err(|e| {
|
||||
MeliError::new(format!(
|
||||
"Configuration error ({}): Invalid value for field `{}`: {}\n{}",
|
||||
$s.name.as_str(),
|
||||
$var,
|
||||
v,
|
||||
e
|
||||
))
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| Ok($default))
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Store {
|
||||
pub account_name: Arc<String>,
|
||||
|
@ -168,7 +183,7 @@ pub struct Store {
|
|||
pub id_store: Arc<Mutex<HashMap<EnvelopeHash, Id<EmailObject>>>>,
|
||||
pub reverse_id_store: Arc<Mutex<HashMap<Id<EmailObject>, EnvelopeHash>>>,
|
||||
pub blob_id_store: Arc<Mutex<HashMap<EnvelopeHash, Id<BlobObject>>>>,
|
||||
pub collection: Collection,
|
||||
pub tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
|
||||
pub mailboxes: Arc<RwLock<HashMap<MailboxHash, JmapMailbox>>>,
|
||||
pub mailboxes_index: Arc<RwLock<HashMap<MailboxHash, HashSet<EnvelopeHash>>>>,
|
||||
pub mailbox_state: Arc<Mutex<State<MailboxObject>>>,
|
||||
|
@ -179,40 +194,34 @@ pub struct Store {
|
|||
|
||||
impl Store {
|
||||
pub fn add_envelope(&self, obj: EmailObject) -> Envelope {
|
||||
let mut flags = Flag::default();
|
||||
let mut labels: SmallVec<[TagHash; 8]> = SmallVec::new();
|
||||
let mut tag_lck = self.collection.tag_index.write().unwrap();
|
||||
for t in obj.keywords().keys() {
|
||||
match t.as_str() {
|
||||
"$draft" => {
|
||||
flags |= Flag::DRAFT;
|
||||
let mut tag_lck = self.tag_index.write().unwrap();
|
||||
let tags = obj
|
||||
.keywords()
|
||||
.keys()
|
||||
.map(|tag| {
|
||||
let tag_hash = {
|
||||
let mut hasher = DefaultHasher::default();
|
||||
tag.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
};
|
||||
if !tag_lck.contains_key(&tag_hash) {
|
||||
tag_lck.insert(tag_hash, tag.to_string());
|
||||
}
|
||||
"$seen" => {
|
||||
flags |= Flag::SEEN;
|
||||
}
|
||||
"$flagged" => {
|
||||
flags |= Flag::FLAGGED;
|
||||
}
|
||||
"$answered" => {
|
||||
flags |= Flag::REPLIED;
|
||||
}
|
||||
"$junk" | "$notjunk" => { /* ignore */ }
|
||||
_ => {
|
||||
let tag_hash = TagHash::from_bytes(t.as_bytes());
|
||||
tag_lck.entry(tag_hash).or_insert_with(|| t.to_string());
|
||||
labels.push(tag_hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tag_hash
|
||||
})
|
||||
.collect::<SmallVec<[u64; 1024]>>();
|
||||
let id = obj.id.clone();
|
||||
let mailbox_ids = obj.mailbox_ids.clone();
|
||||
let blob_id = obj.blob_id.clone();
|
||||
drop(tag_lck);
|
||||
let mut ret: Envelope = obj.into();
|
||||
ret.set_flags(flags);
|
||||
ret.tags_mut().append(&mut labels);
|
||||
|
||||
debug_assert_eq!(tag_hash!("$draft"), 6613915297903591176);
|
||||
debug_assert_eq!(tag_hash!("$seen"), 1683863812294339685);
|
||||
debug_assert_eq!(tag_hash!("$flagged"), 2714010747478170100);
|
||||
debug_assert_eq!(tag_hash!("$answered"), 8940855303929342213);
|
||||
debug_assert_eq!(tag_hash!("$junk"), 2656839745430720464);
|
||||
debug_assert_eq!(tag_hash!("$notjunk"), 4091323799684325059);
|
||||
let mut id_store_lck = self.id_store.lock().unwrap();
|
||||
let mut reverse_id_store_lck = self.reverse_id_store.lock().unwrap();
|
||||
let mut blob_id_store_lck = self.blob_id_store.lock().unwrap();
|
||||
|
@ -230,6 +239,24 @@ impl Store {
|
|||
reverse_id_store_lck.insert(id.clone(), ret.hash());
|
||||
id_store_lck.insert(ret.hash(), id);
|
||||
blob_id_store_lck.insert(ret.hash(), blob_id);
|
||||
for t in tags {
|
||||
match t {
|
||||
6613915297903591176 => {
|
||||
ret.set_flags(ret.flags() | Flag::DRAFT);
|
||||
}
|
||||
1683863812294339685 => {
|
||||
ret.set_flags(ret.flags() | Flag::SEEN);
|
||||
}
|
||||
2714010747478170100 => {
|
||||
ret.set_flags(ret.flags() | Flag::FLAGGED);
|
||||
}
|
||||
8940855303929342213 => {
|
||||
ret.set_flags(ret.flags() | Flag::REPLIED);
|
||||
}
|
||||
2656839745430720464 | 4091323799684325059 => { /* ignore */ }
|
||||
_ => ret.labels_mut().push(t),
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
|
@ -242,12 +269,9 @@ impl Store {
|
|||
self.blob_id_store.lock().unwrap().remove(&env_hash);
|
||||
self.byte_cache.lock().unwrap().remove(&env_hash);
|
||||
let mut mailbox_hashes = SmallVec::new();
|
||||
{
|
||||
let mut mailboxes_lck = self.mailboxes_index.write().unwrap();
|
||||
for (k, set) in mailboxes_lck.iter_mut() {
|
||||
if set.remove(&env_hash) {
|
||||
mailbox_hashes.push(*k);
|
||||
}
|
||||
for (k, set) in self.mailboxes_index.write().unwrap().iter_mut() {
|
||||
if set.remove(&env_hash) {
|
||||
mailbox_hashes.push(*k);
|
||||
}
|
||||
}
|
||||
Some((env_hash, mailbox_hashes))
|
||||
|
@ -276,17 +300,15 @@ impl MailBackend for JmapType {
|
|||
|
||||
fn is_online(&self) -> ResultFuture<()> {
|
||||
let online = self.store.online_status.clone();
|
||||
let connection = self.connection.clone();
|
||||
let timeout_dur = self.server_conf.timeout;
|
||||
Ok(Box::pin(async move {
|
||||
match timeout(timeout_dur, connection.lock()).await {
|
||||
Ok(_conn) => match timeout(timeout_dur, online.lock()).await {
|
||||
Err(err) => Err(err),
|
||||
Ok(lck) if lck.1.is_err() => lck.1.clone(),
|
||||
_ => Ok(()),
|
||||
},
|
||||
Err(err) => Err(err),
|
||||
//match timeout(std::time::Duration::from_secs(3), connection.lock()).await {
|
||||
let online_lck = online.lock().await;
|
||||
if online_lck.1.is_err()
|
||||
&& Instant::now().duration_since(online_lck.0) >= std::time::Duration::new(2, 0)
|
||||
{
|
||||
//let _ = self.mailboxes();
|
||||
}
|
||||
online_lck.1.clone()
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -304,9 +326,6 @@ impl MailBackend for JmapType {
|
|||
&store,
|
||||
mailbox_hash,
|
||||
).await?;
|
||||
if res.is_empty() {
|
||||
return;
|
||||
}
|
||||
yield res;
|
||||
}))
|
||||
}
|
||||
|
@ -322,32 +341,7 @@ impl MailBackend for JmapType {
|
|||
}
|
||||
|
||||
fn watch(&self) -> ResultFuture<()> {
|
||||
let connection = self.connection.clone();
|
||||
let store = self.store.clone();
|
||||
Ok(Box::pin(async move {
|
||||
{
|
||||
let mut conn = connection.lock().await;
|
||||
conn.connect().await?;
|
||||
}
|
||||
loop {
|
||||
{
|
||||
let mailbox_hashes = {
|
||||
store
|
||||
.mailboxes
|
||||
.read()
|
||||
.unwrap()
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect::<SmallVec<[MailboxHash; 16]>>()
|
||||
};
|
||||
let conn = connection.lock().await;
|
||||
for mailbox_hash in mailbox_hashes {
|
||||
conn.email_changes(mailbox_hash).await?;
|
||||
}
|
||||
}
|
||||
sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
}))
|
||||
Err(MeliError::from("JMAP watch for updates is unimplemented"))
|
||||
}
|
||||
|
||||
fn mailboxes(&self) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
|
||||
|
@ -414,28 +408,15 @@ impl MailBackend for JmapType {
|
|||
if let Some(mailbox) = mailboxes_lck.get(&mailbox_hash) {
|
||||
mailbox.id.clone()
|
||||
} else {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"Mailbox with hash {} not found",
|
||||
mailbox_hash
|
||||
)));
|
||||
}
|
||||
};
|
||||
let res_text = res.text().await?;
|
||||
let res_text = res.text_async().await?;
|
||||
|
||||
let upload_response: UploadResponse = match serde_json::from_str(&res_text) {
|
||||
Err(err) => {
|
||||
let err = Error::new(format!(
|
||||
"BUG: Could not deserialize {} server JSON response properly, please \
|
||||
report this!\nReply from server: {}",
|
||||
&conn.server_conf.server_url, &res_text
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(ErrorKind::Bug);
|
||||
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
Ok(s) => s,
|
||||
};
|
||||
let upload_response: UploadResponse = serde_json::from_str(&res_text)?;
|
||||
let mut req = Request::new(conn.request_no.clone());
|
||||
let creation_id: Id<EmailObject> = "1".to_string().into();
|
||||
let mut email_imports = HashMap::default();
|
||||
|
@ -449,7 +430,7 @@ impl MailBackend for JmapType {
|
|||
);
|
||||
|
||||
let import_call: ImportCall = ImportCall::new()
|
||||
.account_id(conn.mail_account_id())
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.emails(email_imports);
|
||||
|
||||
req.add_call(&import_call);
|
||||
|
@ -457,39 +438,30 @@ impl MailBackend for JmapType {
|
|||
.client
|
||||
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
|
||||
.await?;
|
||||
let res_text = res.text().await?;
|
||||
let res_text = res.text_async().await?;
|
||||
|
||||
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
|
||||
Err(err) => {
|
||||
let err = Error::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)).map_err(|err| {
|
||||
let mut v: MethodResponse = serde_json::from_str(&res_text)?;
|
||||
let m = ImportResponse::try_from(v.method_responses.remove(0)).or_else(|err| {
|
||||
let ierr: Result<ImportError> =
|
||||
serde_json::from_str(&res_text).map_err(|err| err.into());
|
||||
if let Ok(err) = ierr {
|
||||
Error::new(format!("Could not save message: {:?}", err))
|
||||
Err(MeliError::new(format!("Could not save message: {:?}", err)))
|
||||
} else {
|
||||
err
|
||||
Err(err.into())
|
||||
}
|
||||
})?;
|
||||
|
||||
if let Some(err) = m.not_created.get(&creation_id) {
|
||||
return Err(Error::new(format!("Could not save message: {:?}", err)));
|
||||
return Err(MeliError::new(format!("Could not save message: {:?}", err)));
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
|
||||
Some(self.store.tag_index.clone())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
@ -498,10 +470,6 @@ impl MailBackend for JmapType {
|
|||
self
|
||||
}
|
||||
|
||||
fn collection(&self) -> Collection {
|
||||
self.store.collection.clone()
|
||||
}
|
||||
|
||||
fn search(
|
||||
&self,
|
||||
q: crate::search::Query,
|
||||
|
@ -530,7 +498,7 @@ impl MailBackend for JmapType {
|
|||
conn.connect().await?;
|
||||
let email_call: EmailQuery = EmailQuery::new(
|
||||
Query::new()
|
||||
.account_id(conn.mail_account_id())
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.filter(Some(filter))
|
||||
.position(0),
|
||||
)
|
||||
|
@ -545,21 +513,8 @@ impl MailBackend for JmapType {
|
|||
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
|
||||
.await?;
|
||||
|
||||
let res_text = res.text().await?;
|
||||
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
|
||||
Err(err) => {
|
||||
let err = Error::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 res_text = res.text_async().await?;
|
||||
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
|
||||
*store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
|
||||
let m = QueryResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
|
||||
let QueryResponse::<EmailObject> { ids, .. } = m;
|
||||
|
@ -573,47 +528,14 @@ impl MailBackend for JmapType {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_new_path: String,
|
||||
) -> ResultFuture<Mailbox> {
|
||||
Err(Error::new(
|
||||
"Renaming mailbox is currently unimplemented for the JMAP backend.",
|
||||
))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn create_mailbox(
|
||||
&mut self,
|
||||
_path: String,
|
||||
) -> ResultFuture<(MailboxHash, HashMap<MailboxHash, Mailbox>)> {
|
||||
Err(Error::new(
|
||||
"Creating mailbox is currently unimplemented for the JMAP backend.",
|
||||
))
|
||||
}
|
||||
|
||||
fn delete_mailbox(
|
||||
&mut self,
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
|
||||
Err(Error::new(
|
||||
"Deleting a mailbox is currently unimplemented for the JMAP backend.",
|
||||
))
|
||||
}
|
||||
|
||||
fn set_mailbox_subscription(
|
||||
&mut self,
|
||||
_mailbox_hash: MailboxHash,
|
||||
_val: bool,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new(
|
||||
"Setting mailbox subscription is currently unimplemented for the JMAP backend.",
|
||||
))
|
||||
}
|
||||
|
||||
fn set_mailbox_permissions(
|
||||
&mut self,
|
||||
_mailbox_hash: MailboxHash,
|
||||
_val: MailboxPermissions,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new(
|
||||
"Setting mailbox permissions is currently unimplemented for the JMAP backend.",
|
||||
))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn copy_messages(
|
||||
|
@ -629,13 +551,13 @@ impl MailBackend for JmapType {
|
|||
let (source_mailbox_id, destination_mailbox_id) = {
|
||||
let mailboxes_lck = store.mailboxes.read().unwrap();
|
||||
if !mailboxes_lck.contains_key(&source_mailbox_hash) {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"Could not find source mailbox with hash {}",
|
||||
source_mailbox_hash
|
||||
)));
|
||||
}
|
||||
if !mailboxes_lck.contains_key(&destination_mailbox_hash) {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"Could not find destination mailbox with hash {}",
|
||||
destination_mailbox_hash
|
||||
)));
|
||||
|
@ -647,6 +569,8 @@ impl MailBackend for JmapType {
|
|||
)
|
||||
};
|
||||
let mut update_map: HashMap<Id<EmailObject>, Value> = HashMap::default();
|
||||
let mut ids: Vec<Id<EmailObject>> = Vec::with_capacity(env_hashes.rest.len() + 1);
|
||||
let mut id_map: HashMap<Id<EmailObject>, EnvelopeHash> = HashMap::default();
|
||||
let mut update_keywords: HashMap<String, Value> = HashMap::default();
|
||||
update_keywords.insert(
|
||||
format!("mailboxIds/{}", &destination_mailbox_id),
|
||||
|
@ -661,8 +585,8 @@ impl MailBackend for JmapType {
|
|||
{
|
||||
for env_hash in env_hashes.iter() {
|
||||
if let Some(id) = store.id_store.lock().unwrap().get(&env_hash) {
|
||||
// ids.push(id.clone());
|
||||
// id_map.insert(id.clone(), env_hash);
|
||||
ids.push(id.clone());
|
||||
id_map.insert(id.clone(), env_hash);
|
||||
update_map.insert(id.clone(), serde_json::json!(update_keywords.clone()));
|
||||
}
|
||||
}
|
||||
|
@ -672,7 +596,7 @@ impl MailBackend for JmapType {
|
|||
|
||||
let email_set_call: EmailSet = EmailSet::new(
|
||||
Set::<EmailObject>::new()
|
||||
.account_id(conn.mail_account_id())
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.update(Some(update_map)),
|
||||
);
|
||||
|
||||
|
@ -684,27 +608,14 @@ impl MailBackend for JmapType {
|
|||
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
|
||||
.await?;
|
||||
|
||||
let res_text = res.text().await?;
|
||||
let res_text = res.text_async().await?;
|
||||
|
||||
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
|
||||
Err(err) => {
|
||||
let err = Error::new(format!(
|
||||
"BUG: Could not deserialize {} server JSON response properly, please \
|
||||
report this!\nReply from server: {}",
|
||||
&conn.server_conf.server_url, &res_text
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(ErrorKind::Bug);
|
||||
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
Ok(s) => s,
|
||||
};
|
||||
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
|
||||
*store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
|
||||
let m = SetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
|
||||
if let Some(ids) = m.not_updated {
|
||||
if !ids.is_empty() {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"Could not update ids: {}",
|
||||
ids.into_iter()
|
||||
.map(|err| err.to_string())
|
||||
|
@ -778,7 +689,7 @@ impl MailBackend for JmapType {
|
|||
|
||||
let email_set_call: EmailSet = EmailSet::new(
|
||||
Set::<EmailObject>::new()
|
||||
.account_id(conn.mail_account_id())
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.update(Some(update_map)),
|
||||
);
|
||||
|
||||
|
@ -787,7 +698,7 @@ impl MailBackend for JmapType {
|
|||
let email_call: EmailGet = EmailGet::new(
|
||||
Get::new()
|
||||
.ids(Some(JmapArgument::Value(ids)))
|
||||
.account_id(conn.mail_account_id())
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.properties(Some(vec!["keywords".to_string()])),
|
||||
);
|
||||
|
||||
|
@ -799,33 +710,16 @@ impl MailBackend for JmapType {
|
|||
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
|
||||
.await?;
|
||||
|
||||
let res_text = res.text().await?;
|
||||
let res_text = res.text_async().await?;
|
||||
/*
|
||||
*{"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"}
|
||||
*{"methodResponses":[["Email/set",{"notUpdated":null,"notDestroyed":null,"oldState":"86","newState":"87","accountId":"u148940c7","updated":{"M045926eed54b11423918f392":{"id":"M045926eed54b11423918f392"}},"created":null,"destroyed":null,"notCreated":null},"m3"]],"sessionState":"cyrus-0;p-5;vfs-0"}
|
||||
*/
|
||||
//debug!("res_text = {}", &res_text);
|
||||
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
|
||||
Err(err) => {
|
||||
let err = Error::new(format!(
|
||||
"BUG: Could not deserialize {} server JSON response properly, please \
|
||||
report this!\nReply from server: {}",
|
||||
&conn.server_conf.server_url, &res_text
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(ErrorKind::Bug);
|
||||
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
Ok(s) => s,
|
||||
};
|
||||
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
|
||||
*store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
|
||||
let m = SetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
|
||||
if let Some(ids) = m.not_updated {
|
||||
return Err(Error::new(
|
||||
return Err(MeliError::new(
|
||||
ids.into_iter()
|
||||
.map(|err| err.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
|
@ -834,13 +728,13 @@ impl MailBackend for JmapType {
|
|||
}
|
||||
|
||||
{
|
||||
let mut tag_index_lck = store.collection.tag_index.write().unwrap();
|
||||
let mut tag_index_lck = store.tag_index.write().unwrap();
|
||||
for (flag, value) in flags.iter() {
|
||||
match flag {
|
||||
Ok(_) => {}
|
||||
Err(t) => {
|
||||
if *value {
|
||||
tag_index_lck.insert(TagHash::from_bytes(t.as_bytes()), t.clone());
|
||||
tag_index_lck.insert(tag_hash!(t), t.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -894,14 +788,11 @@ impl MailBackend for JmapType {
|
|||
_env_hashes: EnvelopeHashBatch,
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new(
|
||||
"Deleting messages is currently unimplemented for the JMAP backend.",
|
||||
))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapType {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(
|
||||
s: &AccountSettings,
|
||||
is_subscribed: Box<dyn Fn(&str) -> bool + Send + Sync>,
|
||||
|
@ -909,11 +800,15 @@ impl JmapType {
|
|||
) -> Result<Box<dyn MailBackend>> {
|
||||
let online_status = Arc::new(FutureMutex::new((
|
||||
std::time::Instant::now(),
|
||||
Err(Error::new("Account is uninitialised.")),
|
||||
Err(MeliError::new("Account is uninitialised.")),
|
||||
)));
|
||||
let server_conf = JmapServerConf::new(s)?;
|
||||
|
||||
let account_hash = AccountHash::from_bytes(s.name.as_bytes());
|
||||
let account_hash = {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
hasher.write(s.name.as_bytes());
|
||||
hasher.finish()
|
||||
};
|
||||
let store = Arc::new(Store {
|
||||
account_name: Arc::new(s.name.clone()),
|
||||
account_hash,
|
||||
|
@ -921,18 +816,18 @@ impl JmapType {
|
|||
online_status,
|
||||
event_consumer,
|
||||
is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)),
|
||||
collection: Collection::default(),
|
||||
|
||||
byte_cache: Default::default(),
|
||||
id_store: Default::default(),
|
||||
reverse_id_store: Default::default(),
|
||||
blob_id_store: Default::default(),
|
||||
tag_index: Default::default(),
|
||||
mailboxes: Default::default(),
|
||||
mailboxes_index: Default::default(),
|
||||
mailbox_state: Default::default(),
|
||||
});
|
||||
|
||||
Ok(Box::new(Self {
|
||||
Ok(Box::new(JmapType {
|
||||
connection: Arc::new(FutureMutex::new(JmapConnection::new(
|
||||
&server_conf,
|
||||
store.clone(),
|
||||
|
@ -942,42 +837,11 @@ impl JmapType {
|
|||
}))
|
||||
}
|
||||
|
||||
pub fn validate_config(s: &mut AccountSettings) -> Result<()> {
|
||||
macro_rules! get_conf_val {
|
||||
($s:ident[$var:literal]) => {
|
||||
$s.extra.remove($var).ok_or_else(|| {
|
||||
Error::new(format!(
|
||||
"Configuration error ({}): JMAP connection requires the field `{}` set",
|
||||
$s.name.as_str(),
|
||||
$var
|
||||
))
|
||||
})
|
||||
};
|
||||
($s:ident[$var:literal], $default:expr) => {
|
||||
$s.extra
|
||||
.remove($var)
|
||||
.map(|v| {
|
||||
<_>::from_str(&v).map_err(|e| {
|
||||
Error::new(format!(
|
||||
"Configuration error ({}): Invalid value for field `{}`: {}\n{}",
|
||||
$s.name.as_str(),
|
||||
$var,
|
||||
v,
|
||||
e
|
||||
))
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| Ok($default))
|
||||
};
|
||||
}
|
||||
get_conf_val!(s["server_url"])?;
|
||||
pub fn validate_config(s: &AccountSettings) -> Result<()> {
|
||||
get_conf_val!(s["server_hostname"])?;
|
||||
get_conf_val!(s["server_username"])?;
|
||||
|
||||
get_conf_val!(s["use_token"], false)?;
|
||||
// either of these two needed
|
||||
get_conf_val!(s["server_password"])
|
||||
.or_else(|_| get_conf_val!(s["server_password_command"]))?;
|
||||
|
||||
get_conf_val!(s["server_password"])?;
|
||||
get_conf_val!(s["server_port"], 443)?;
|
||||
get_conf_val!(s["danger_accept_invalid_certs"], false)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -19,11 +19,9 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::sync::MutexGuard;
|
||||
|
||||
use isahc::config::Configurable;
|
||||
|
||||
pub mod eventsource;
|
||||
use super::*;
|
||||
use isahc::config::Configurable;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JmapConnection {
|
||||
|
@ -38,28 +36,15 @@ impl JmapConnection {
|
|||
pub fn new(server_conf: &JmapServerConf, store: Arc<Store>) -> Result<Self> {
|
||||
let client = HttpClient::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.connection_cache_size(8)
|
||||
.connection_cache_ttl(std::time::Duration::from_secs(30 * 60))
|
||||
.default_header("Content-Type", "application/json")
|
||||
.redirect_policy(RedirectPolicy::Limit(10));
|
||||
let client = if server_conf.use_token {
|
||||
client
|
||||
.authentication(isahc::auth::Authentication::none())
|
||||
.default_header(
|
||||
"Authorization",
|
||||
format!("Bearer {}", &server_conf.server_password),
|
||||
)
|
||||
} else {
|
||||
client
|
||||
.authentication(isahc::auth::Authentication::basic())
|
||||
.credentials(isahc::auth::Credentials::new(
|
||||
&server_conf.server_username,
|
||||
&server_conf.server_password,
|
||||
))
|
||||
};
|
||||
let client = client.build()?;
|
||||
.redirect_policy(RedirectPolicy::Limit(10))
|
||||
.authentication(isahc::auth::Authentication::basic())
|
||||
.credentials(isahc::auth::Credentials::new(
|
||||
&server_conf.server_username,
|
||||
&server_conf.server_password,
|
||||
))
|
||||
.build()?;
|
||||
let server_conf = server_conf.clone();
|
||||
Ok(Self {
|
||||
Ok(JmapConnection {
|
||||
session: Arc::new(Mutex::new(Default::default())),
|
||||
request_no: Arc::new(Mutex::new(0)),
|
||||
client: Arc::new(client),
|
||||
|
@ -72,49 +57,24 @@ impl JmapConnection {
|
|||
if self.store.online_status.lock().await.1.is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut jmap_session_resource_url = self.server_conf.server_url.to_string();
|
||||
let mut jmap_session_resource_url =
|
||||
if self.server_conf.server_hostname.starts_with("https://") {
|
||||
self.server_conf.server_hostname.to_string()
|
||||
} else {
|
||||
format!("https://{}", &self.server_conf.server_hostname)
|
||||
};
|
||||
if self.server_conf.server_port != 443 {
|
||||
jmap_session_resource_url.push(':');
|
||||
jmap_session_resource_url.push_str(&self.server_conf.server_port.to_string());
|
||||
}
|
||||
jmap_session_resource_url.push_str("/.well-known/jmap");
|
||||
|
||||
let mut req = self
|
||||
.client
|
||||
.get_async(&jmap_session_resource_url)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
//*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
Error::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)))
|
||||
})?;
|
||||
|
||||
if !req.status().is_success() {
|
||||
let kind: crate::error::NetworkErrorKind = req.status().into();
|
||||
let res_text = req.text().await.unwrap_or_default();
|
||||
let err = Error::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 mut req = self.client.get_async(&jmap_session_resource_url).await?;
|
||||
let res_text = req.text_async().await?;
|
||||
|
||||
let session: JmapSession = match serde_json::from_str(&res_text) {
|
||||
Err(err) => {
|
||||
let err = Error::new(format!(
|
||||
"Could not connect to JMAP server endpoint for {}. Is your server url setting \
|
||||
correct? (i.e. \"jmap.mailserver.org\") (Note: only session resource \
|
||||
discovery via /.well-known/jmap is supported. DNS SRV records are not \
|
||||
suppported.)\nReply from server: {}",
|
||||
&self.server_conf.server_url, &res_text
|
||||
))
|
||||
.set_source(Some(Arc::new(err)));
|
||||
let err = MeliError::new(format!("Could not connect to JMAP server endpoint for {}. Is your server hostname setting correct? (i.e. \"jmap.mailserver.org\") (Note: only session resource discovery via /.well-known/jmap is supported. DNS SRV records are not suppported.)\nReply from server: {}", &self.server_conf.server_hostname, &res_text)).set_source(Some(Arc::new(err)));
|
||||
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
|
@ -124,17 +84,7 @@ impl JmapConnection {
|
|||
.capabilities
|
||||
.contains_key("urn:ietf:params:jmap:core")
|
||||
{
|
||||
let err = Error::new(format!(
|
||||
"Server {} did not return JMAP Core capability (urn:ietf:params:jmap:core). \
|
||||
Returned capabilities were: {}",
|
||||
&self.server_conf.server_url,
|
||||
session
|
||||
.capabilities
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<&str>>()
|
||||
.join(", ")
|
||||
));
|
||||
let err = MeliError::new(format!("Server {} did not return JMAP Core capability (urn:ietf:params:jmap:core). Returned capabilities were: {}", &self.server_conf.server_hostname, session.capabilities.keys().map(String::as_str).collect::<Vec<&str>>().join(", ")));
|
||||
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
|
@ -142,17 +92,7 @@ impl JmapConnection {
|
|||
.capabilities
|
||||
.contains_key("urn:ietf:params:jmap:mail")
|
||||
{
|
||||
let err = Error::new(format!(
|
||||
"Server {} does not support JMAP Mail capability (urn:ietf:params:jmap:mail). \
|
||||
Returned capabilities were: {}",
|
||||
&self.server_conf.server_url,
|
||||
session
|
||||
.capabilities
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<&str>>()
|
||||
.join(", ")
|
||||
));
|
||||
let err = MeliError::new(format!("Server {} does not support JMAP Mail capability (urn:ietf:params:jmap:mail). Returned capabilities were: {}", &self.server_conf.server_hostname, session.capabilities.keys().map(String::as_str).collect::<Vec<&str>>().join(", ")));
|
||||
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
|
@ -166,10 +106,6 @@ impl JmapConnection {
|
|||
self.session.lock().unwrap().primary_accounts["urn:ietf:params:jmap:mail"].clone()
|
||||
}
|
||||
|
||||
pub fn session_guard(&'_ self) -> MutexGuard<'_, JmapSession> {
|
||||
self.session.lock().unwrap()
|
||||
}
|
||||
|
||||
pub fn add_refresh_event(&self, event: RefreshEvent) {
|
||||
(self.store.event_consumer)(self.store.account_hash, BackendEvent::Refresh(event));
|
||||
}
|
||||
|
@ -200,7 +136,7 @@ impl JmapConnection {
|
|||
Get::new()
|
||||
.ids(Some(JmapArgument::reference(
|
||||
prev_seq,
|
||||
ResultField::<EmailChanges, EmailObject>::new("/created"),
|
||||
ResultField::<EmailChanges, EmailObject>::new("created"),
|
||||
)))
|
||||
.account_id(self.mail_account_id().clone()),
|
||||
);
|
||||
|
@ -221,7 +157,7 @@ impl JmapConnection {
|
|||
Get::new()
|
||||
.ids(Some(JmapArgument::reference(
|
||||
seq_no,
|
||||
ResultField::<EmailQueryChanges, EmailObject>::new("/removed"),
|
||||
ResultField::<EmailQueryChanges, EmailObject>::new("removed"),
|
||||
)))
|
||||
.account_id(self.mail_account_id().clone())
|
||||
.properties(Some(vec![
|
||||
|
@ -242,22 +178,9 @@ impl JmapConnection {
|
|||
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
|
||||
.await?;
|
||||
|
||||
let res_text = res.text().await?;
|
||||
let res_text = res.text_async().await?;
|
||||
debug!(&res_text);
|
||||
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
|
||||
Err(err) => {
|
||||
let err = Error::new(format!(
|
||||
"BUG: Could not deserialize {} server JSON response properly, please \
|
||||
report this!\nReply from server: {}",
|
||||
&self.server_conf.server_url, &res_text
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(ErrorKind::Bug);
|
||||
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
Ok(s) => s,
|
||||
};
|
||||
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
|
||||
let changes_response =
|
||||
ChangesResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
|
||||
if changes_response.new_state == current_state {
|
||||
|
|
|
@ -0,0 +1,266 @@
|
|||
/*
|
||||
* meli - jmap module.
|
||||
*
|
||||
* Copyright 2021 Manos Pitsidianakis
|
||||
*
|
||||
* This file is part of meli.
|
||||
*
|
||||
* meli is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* meli is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use super::{HttpClient, JmapConnection, JmapServerConf, Store};
|
||||
use crate::error::Result;
|
||||
use std::convert::TryFrom;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const DEFAULT_RETRY: u64 = 5000;
|
||||
|
||||
/// A single Server-Sent Event.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Event {
|
||||
/// Corresponds to the `id` field.
|
||||
pub id: Option<String>,
|
||||
/// Corresponds to the `event` field.
|
||||
pub event_type: Option<String>,
|
||||
/// All `data` fields concatenated by newlines.
|
||||
pub data: String,
|
||||
}
|
||||
|
||||
/// Possible results from parsing a single event-stream line.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ParseResult {
|
||||
/// Line parsed successfully, but the event is not complete yet.
|
||||
Next,
|
||||
/// The event is complete now. Pass a new (empty) event for the next call.
|
||||
Dispatch,
|
||||
/// Set retry time.
|
||||
SetRetry(Duration),
|
||||
}
|
||||
|
||||
pub fn parse_event_line(line: &str, event: &mut Event) -> ParseResult {
|
||||
let line = line.trim_end_matches(|c| c == '\r' || c == '\n');
|
||||
if line == "" {
|
||||
ParseResult::Dispatch
|
||||
} else {
|
||||
let (field, value) = if let Some(pos) = line.find(':') {
|
||||
let (f, v) = line.split_at(pos);
|
||||
// Strip : and an optional space.
|
||||
let v = &v[1..];
|
||||
let v = if v.starts_with(' ') { &v[1..] } else { v };
|
||||
(f, v)
|
||||
} else {
|
||||
(line, "")
|
||||
};
|
||||
match field {
|
||||
"event" => {
|
||||
event.event_type = Some(value.to_string());
|
||||
}
|
||||
"data" => {
|
||||
event.data.push_str(value);
|
||||
event.data.push('\n');
|
||||
}
|
||||
"id" => {
|
||||
event.id = Some(value.to_string());
|
||||
}
|
||||
"retry" => {
|
||||
if let Ok(retry) = value.parse::<u64>() {
|
||||
return ParseResult::SetRetry(Duration::from_millis(retry));
|
||||
}
|
||||
}
|
||||
_ => (), // ignored
|
||||
}
|
||||
|
||||
ParseResult::Next
|
||||
}
|
||||
}
|
||||
|
||||
impl Event {
|
||||
/// Creates an empty event.
|
||||
pub fn new() -> Event {
|
||||
Event::default()
|
||||
}
|
||||
|
||||
/// Returns `true` if the event is empty.
|
||||
///
|
||||
/// An event is empty if it has no id or event type and its data field is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.id.is_none() && self.event_type.is_none() && self.data.is_empty()
|
||||
}
|
||||
|
||||
/// Makes the event empty.
|
||||
pub fn clear(&mut self) {
|
||||
self.id = None;
|
||||
self.event_type = None;
|
||||
self.data.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// A client for a Server-Sent Events endpoint.
|
||||
///
|
||||
/// Read events by iterating over the client.
|
||||
pub struct JmapEventSourceConnection {
|
||||
pub client: Arc<HttpClient>,
|
||||
pub store: Arc<Store>,
|
||||
pub server_conf: JmapServerConf,
|
||||
response: Option<BufReader<isahc::Body>>,
|
||||
url: isahc::http::Uri,
|
||||
last_event_id: Option<String>,
|
||||
last_try: Option<Instant>,
|
||||
pub retry: Duration,
|
||||
}
|
||||
|
||||
impl JmapEventSourceConnection {
|
||||
pub fn new(conn: &JmapConnection) -> Result<Self> {
|
||||
let url =
|
||||
isahc::http::Uri::try_from(conn.session.lock().unwrap().event_source_url.as_str())
|
||||
.map_err(|err| err.to_string())?;
|
||||
debug!("event_source {}", &url);
|
||||
Ok(Self {
|
||||
client: conn.client.clone(),
|
||||
server_conf: conn.server_conf.clone(),
|
||||
store: conn.store.clone(),
|
||||
response: None,
|
||||
url: url,
|
||||
last_event_id: None,
|
||||
last_try: None,
|
||||
retry: Duration::from_millis(DEFAULT_RETRY),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn next_request(&mut self) -> Result<()> {
|
||||
use isahc::{http, http::request::Request, prelude::*};
|
||||
|
||||
let mut request = Request::get(&self.url)
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.redirect_policy(isahc::config::RedirectPolicy::Limit(10))
|
||||
.authentication(isahc::auth::Authentication::basic())
|
||||
.credentials(isahc::auth::Credentials::new(
|
||||
&self.server_conf.server_username,
|
||||
&self.server_conf.server_password,
|
||||
))
|
||||
.header(http::header::ACCEPT, "text/event-stream");
|
||||
if let Some(ref id) = self.last_event_id {
|
||||
request = request.header("Last-Event-ID", id.as_str());
|
||||
}
|
||||
let request = request.body(()).map_err(|err| err.to_string())?;
|
||||
|
||||
debug!(&request);
|
||||
let mut response = self.client.send_async(request).await?;
|
||||
debug!(&response);
|
||||
//debug_assert!(response.status().is_success());
|
||||
/*
|
||||
let mut headers = HeaderMap::with_capacity(2);
|
||||
headers.insert(ACCEPT, HeaderValue::from_str("text/event-stream").unwrap());
|
||||
if let Some(ref id) = self.last_event_id {
|
||||
headers.insert("Last-Event-ID", HeaderValue::from_str(id).unwrap());
|
||||
}
|
||||
|
||||
let res = self.client.get(self.url.clone()).headers(headers).send()?;
|
||||
*/
|
||||
|
||||
// Check status code and Content-Type.
|
||||
{
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
let res_text = response.text_async().await?;
|
||||
return Err(debug!(format!("{} {}", status.as_str(), res_text)).into());
|
||||
}
|
||||
|
||||
if let Some(content_type_hv) = response.headers().get(isahc::http::header::CONTENT_TYPE)
|
||||
{
|
||||
if content_type_hv.to_str().unwrap() != "text/event-stream" {
|
||||
panic!(content_type_hv.to_str().unwrap().to_string());
|
||||
}
|
||||
/*
|
||||
let content_type = content_type_hv
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.parse::<mime::Mime>()
|
||||
.unwrap();
|
||||
// Compare type and subtype only, MIME parameters are ignored.
|
||||
if (content_type.type_(), content_type.subtype())
|
||||
!= (mime::TEXT, mime::EVENT_STREAM)
|
||||
{
|
||||
return Err(ErrorKind::InvalidContentType(content_type.clone()).into());
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
self.response = Some(BufReader::new(response.into_body()));
|
||||
Ok(())
|
||||
}
|
||||
} /*
|
||||
|
||||
pub async fn next_event(&mut self) -> Result<Event> {
|
||||
let mut line = String::new();
|
||||
'main_loop: loop {
|
||||
match self.response.as_mut() {
|
||||
None => {
|
||||
// wait for the next request.
|
||||
if let Some(last_try) = self.last_try {
|
||||
let elapsed = last_try.elapsed();
|
||||
if elapsed < self.retry {
|
||||
crate::connection::sleep(self.retry - elapsed).await;
|
||||
}
|
||||
}
|
||||
// Set here in case the request fails.
|
||||
self.last_try = Some(Instant::now());
|
||||
|
||||
self.next_request().await?;
|
||||
}
|
||||
Some(reader) => {
|
||||
let mut event = Event::new();
|
||||
loop {
|
||||
match reader.read_line(&mut line) {
|
||||
// Got new bytes from stream
|
||||
Ok(_n) if _n > 0 => {
|
||||
match parse_event_line(&line, &mut event) {
|
||||
ParseResult::Next => {} // okay, just continue
|
||||
ParseResult::Dispatch => {
|
||||
if let Some(ref id) = event.id {
|
||||
self.last_event_id = Some(id.clone());
|
||||
}
|
||||
return Ok(event);
|
||||
}
|
||||
ParseResult::SetRetry(ref retry) => {
|
||||
self.retry = *retry;
|
||||
}
|
||||
}
|
||||
line.clear();
|
||||
}
|
||||
Ok(0) => {
|
||||
// EOF or a stream error, retry after timeout
|
||||
self.last_try = Some(Instant::now());
|
||||
self.response = None;
|
||||
continue 'main_loop;
|
||||
}
|
||||
Err(err) => {
|
||||
// EOF or a stream error, retry after timeout
|
||||
self.last_try = Some(Instant::now());
|
||||
self.response = None;
|
||||
debug!(&err);
|
||||
continue 'main_loop;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -19,10 +19,9 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
use super::*;
|
||||
use crate::backends::{LazyCountSet, MailboxPermissions, SpecialUsageMailbox};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JmapMailbox {
|
||||
|
@ -59,6 +58,8 @@ impl BackendMailbox for JmapMailbox {
|
|||
&self.path
|
||||
}
|
||||
|
||||
fn change_name(&mut self, _s: &str) {}
|
||||
|
||||
fn clone(&self) -> Mailbox {
|
||||
Box::new(std::clone::Clone::clone(self))
|
||||
}
|
||||
|
@ -76,7 +77,7 @@ impl BackendMailbox for JmapMailbox {
|
|||
}
|
||||
|
||||
fn special_usage(&self) -> SpecialUsageMailbox {
|
||||
match self.role.as_deref() {
|
||||
match self.role.as_ref().map(String::as_str) {
|
||||
Some("inbox") => SpecialUsageMailbox::Inbox,
|
||||
Some("archive") => SpecialUsageMailbox::Archive,
|
||||
Some("junk") => SpecialUsageMailbox::Junk,
|
||||
|
@ -94,11 +95,9 @@ impl BackendMailbox for JmapMailbox {
|
|||
None => SpecialUsageMailbox::Normal,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_subscribed(&self) -> bool {
|
||||
self.is_subscribed
|
||||
}
|
||||
|
||||
fn set_is_subscribed(&mut self, new_val: bool) -> Result<()> {
|
||||
self.is_subscribed = new_val;
|
||||
// FIXME: jmap subscribe
|
||||
|
|
|
@ -19,18 +19,16 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use core::marker::PhantomData;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::de::{Deserialize, Deserializer};
|
||||
use serde_json::{value::RawValue, Value};
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
backends::jmap::rfc8620::bool_false,
|
||||
email::address::{Address, MailboxAddress},
|
||||
utils::datetime,
|
||||
};
|
||||
use crate::backends::jmap::rfc8620::bool_false;
|
||||
use crate::email::address::{Address, MailboxAddress};
|
||||
use core::marker::PhantomData;
|
||||
use serde::de::{Deserialize, Deserializer};
|
||||
use serde_json::value::RawValue;
|
||||
use serde_json::Value;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hasher;
|
||||
|
||||
mod import;
|
||||
pub use import::*;
|
||||
|
@ -44,7 +42,9 @@ impl Object for ThreadObject {
|
|||
|
||||
impl Id<EmailObject> {
|
||||
pub fn into_hash(&self) -> EnvelopeHash {
|
||||
EnvelopeHash::from_bytes(self.inner.as_bytes())
|
||||
let mut h = DefaultHasher::new();
|
||||
h.write(self.inner.as_bytes());
|
||||
h.finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,13 +87,14 @@ impl Id<EmailObject> {
|
|||
// first character changed from "\" in IMAP to "$" in JMAP and have
|
||||
// particular semantic meaning:
|
||||
//
|
||||
// * "$draft": The Email is a draft the user is composing.
|
||||
// * "$draft": The Email is a draft the user is composing.
|
||||
//
|
||||
// * "$seen": The Email has been read.
|
||||
// * "$seen": The Email has been read.
|
||||
//
|
||||
// * "$flagged": The Email has been flagged for urgent/special attention.
|
||||
// * "$flagged": The Email has been flagged for urgent/special
|
||||
// attention.
|
||||
//
|
||||
// * "$answered": The Email has been replied to.
|
||||
// * "$answered": The Email has been replied to.
|
||||
//
|
||||
// The IMAP "\Recent" keyword is not exposed via JMAP. The IMAP
|
||||
// "\Deleted" keyword is also not present: IMAP uses a delete+expunge
|
||||
|
@ -118,19 +119,19 @@ impl Id<EmailObject> {
|
|||
// keywords in common use. New keywords may be established here in
|
||||
// the future. In particular, note:
|
||||
//
|
||||
// * "$forwarded": The Email has been forwarded.
|
||||
// * "$forwarded": The Email has been forwarded.
|
||||
//
|
||||
// * "$phishing": The Email is highly likely to be phishing. Clients SHOULD
|
||||
// warn users to take care when viewing this Email and disable links and
|
||||
// attachments.
|
||||
// * "$phishing": The Email is highly likely to be phishing.
|
||||
// Clients SHOULD warn users to take care when viewing this Email
|
||||
// and disable links and attachments.
|
||||
//
|
||||
// * "$junk": The Email is definitely spam. Clients SHOULD set this flag
|
||||
// when users report spam to help train automated spam- detection
|
||||
// systems.
|
||||
// * "$junk": The Email is definitely spam. Clients SHOULD set this
|
||||
// flag when users report spam to help train automated spam-
|
||||
// detection systems.
|
||||
//
|
||||
// * "$notjunk": The Email is definitely not spam. Clients SHOULD set this
|
||||
// flag when users indicate an Email is legitimate, to help train
|
||||
// automated spam-detection systems.
|
||||
// * "$notjunk": The Email is definitely not spam. Clients SHOULD
|
||||
// set this flag when users indicate an Email is legitimate, to
|
||||
// help train automated spam-detection systems.
|
||||
//
|
||||
// o size: "UnsignedInt" (immutable; server-set)
|
||||
//
|
||||
|
@ -157,7 +158,7 @@ pub struct EmailObject {
|
|||
pub size: u64,
|
||||
#[serde(default)]
|
||||
pub received_at: String,
|
||||
#[serde(default, deserialize_with = "deserialize_none_default")]
|
||||
#[serde(default)]
|
||||
pub message_id: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub to: Option<SmallVec<[EmailAddress; 1]>>,
|
||||
|
@ -202,18 +203,6 @@ pub struct EmailObject {
|
|||
pub extra: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
/// Deserializer that uses `Default::default()` in place of a present but `null`
|
||||
/// value. Note that `serde(default)` doesn't apply if the key is present but
|
||||
/// has a value of `null`.
|
||||
fn deserialize_none_default<'de, D, T>(deserializer: D) -> std::result::Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: Deserialize<'de> + Default,
|
||||
{
|
||||
let v = Option::<T>::deserialize(deserializer)?;
|
||||
Ok(v.unwrap_or_default())
|
||||
}
|
||||
|
||||
impl EmailObject {
|
||||
_impl!(get keywords, keywords: HashMap<String, bool>);
|
||||
}
|
||||
|
@ -242,9 +231,9 @@ pub struct EmailAddress {
|
|||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
impl From<EmailAddress> for crate::email::Address {
|
||||
fn from(val: EmailAddress) -> Self {
|
||||
let EmailAddress { email, mut name } = val;
|
||||
impl Into<crate::email::Address> for EmailAddress {
|
||||
fn into(self) -> crate::email::Address {
|
||||
let Self { email, mut name } = self;
|
||||
crate::make_address!((name.take().unwrap_or_default()), email)
|
||||
}
|
||||
}
|
||||
|
@ -260,15 +249,16 @@ impl std::fmt::Display for EmailAddress {
|
|||
}
|
||||
|
||||
impl std::convert::From<EmailObject> for crate::Envelope {
|
||||
fn from(mut t: EmailObject) -> Self {
|
||||
let mut env = Self::new(t.id.into_hash());
|
||||
fn from(mut t: EmailObject) -> crate::Envelope {
|
||||
let mut env = crate::Envelope::new(0);
|
||||
if let Ok(d) = crate::email::parser::dates::rfc5322_date(env.date_as_str().as_bytes()) {
|
||||
env.set_datetime(d);
|
||||
}
|
||||
if let Some(ref mut sent_at) = t.sent_at {
|
||||
let unix = datetime::rfc3339_to_timestamp(sent_at.as_bytes().to_vec()).unwrap_or(0);
|
||||
let unix =
|
||||
crate::datetime::rfc3339_to_timestamp(sent_at.as_bytes().to_vec()).unwrap_or(0);
|
||||
env.set_datetime(unix);
|
||||
env.set_date(std::mem::take(sent_at).as_bytes());
|
||||
env.set_date(std::mem::replace(sent_at, String::new()).as_bytes());
|
||||
}
|
||||
|
||||
if let Some(v) = t.message_id.get(0) {
|
||||
|
@ -276,11 +266,15 @@ impl std::convert::From<EmailObject> for crate::Envelope {
|
|||
}
|
||||
if let Some(ref in_reply_to) = t.in_reply_to {
|
||||
env.set_in_reply_to(in_reply_to[0].as_bytes());
|
||||
if let Some(in_reply_to) = env.in_reply_to().cloned() {
|
||||
env.push_references(in_reply_to);
|
||||
}
|
||||
env.push_references(env.in_reply_to().unwrap().clone());
|
||||
}
|
||||
if let Some(v) = t.headers.get("References") {
|
||||
let parse_result = crate::email::parser::address::msg_id_list(v.as_bytes());
|
||||
if let Ok((_, v)) = parse_result {
|
||||
for v in v {
|
||||
env.push_references(v);
|
||||
}
|
||||
}
|
||||
env.set_references(v.as_bytes());
|
||||
}
|
||||
if let Some(v) = t.headers.get("Date") {
|
||||
|
@ -288,17 +282,15 @@ impl std::convert::From<EmailObject> for crate::Envelope {
|
|||
if let Ok(d) = crate::email::parser::dates::rfc5322_date(v.as_bytes()) {
|
||||
env.set_datetime(d);
|
||||
}
|
||||
} else if let Ok(d) = crate::email::parser::dates::rfc5322_date(t.received_at.as_bytes()) {
|
||||
env.set_datetime(d);
|
||||
}
|
||||
env.set_has_attachments(t.has_attachment);
|
||||
if let Some(ref mut subject) = t.subject {
|
||||
env.set_subject(std::mem::take(subject).into_bytes());
|
||||
env.set_subject(std::mem::replace(subject, String::new()).into_bytes());
|
||||
}
|
||||
|
||||
if let Some(ref mut from) = t.from {
|
||||
env.set_from(
|
||||
std::mem::take(from)
|
||||
std::mem::replace(from, SmallVec::new())
|
||||
.into_iter()
|
||||
.map(|addr| addr.into())
|
||||
.collect::<SmallVec<[crate::email::Address; 1]>>(),
|
||||
|
@ -306,7 +298,7 @@ impl std::convert::From<EmailObject> for crate::Envelope {
|
|||
}
|
||||
if let Some(ref mut to) = t.to {
|
||||
env.set_to(
|
||||
std::mem::take(to)
|
||||
std::mem::replace(to, SmallVec::new())
|
||||
.into_iter()
|
||||
.map(|addr| addr.into())
|
||||
.collect::<SmallVec<[crate::email::Address; 1]>>(),
|
||||
|
@ -315,7 +307,7 @@ impl std::convert::From<EmailObject> for crate::Envelope {
|
|||
|
||||
if let Some(ref mut cc) = t.cc {
|
||||
env.set_cc(
|
||||
std::mem::take(cc)
|
||||
std::mem::replace(cc, SmallVec::new())
|
||||
.into_iter()
|
||||
.map(|addr| addr.into())
|
||||
.collect::<SmallVec<[crate::email::Address; 1]>>(),
|
||||
|
@ -324,17 +316,20 @@ impl std::convert::From<EmailObject> for crate::Envelope {
|
|||
|
||||
if let Some(ref mut bcc) = t.bcc {
|
||||
env.set_bcc(
|
||||
std::mem::take(bcc)
|
||||
std::mem::replace(bcc, Vec::new())
|
||||
.into_iter()
|
||||
.map(|addr| addr.into())
|
||||
.collect::<Vec<crate::email::Address>>(),
|
||||
);
|
||||
}
|
||||
|
||||
if let (Some(ref mut r), message_id) = (&mut env.references, &env.message_id) {
|
||||
r.refs.retain(|r| r != message_id);
|
||||
if let Some(ref r) = env.references {
|
||||
if let Some(pos) = r.refs.iter().position(|r| r == env.message_id()) {
|
||||
env.references.as_mut().unwrap().refs.remove(pos);
|
||||
}
|
||||
}
|
||||
|
||||
env.set_hash(t.id.into_hash());
|
||||
env
|
||||
}
|
||||
}
|
||||
|
@ -411,13 +406,14 @@ impl Method<EmailObject> for EmailQuery {
|
|||
}
|
||||
|
||||
impl EmailQuery {
|
||||
pub const RESULT_FIELD_IDS: ResultField<Self, EmailObject> = ResultField::<Self, EmailObject> {
|
||||
field: "/ids",
|
||||
_ph: PhantomData,
|
||||
};
|
||||
pub const RESULT_FIELD_IDS: ResultField<EmailQuery, EmailObject> =
|
||||
ResultField::<EmailQuery, EmailObject> {
|
||||
field: "/ids",
|
||||
_ph: PhantomData,
|
||||
};
|
||||
|
||||
pub fn new(query_call: Query<Filter<EmailFilterCondition, EmailObject>, EmailObject>) -> Self {
|
||||
Self {
|
||||
EmailQuery {
|
||||
query_call,
|
||||
collapse_threads: false,
|
||||
}
|
||||
|
@ -451,7 +447,7 @@ impl Method<EmailObject> for EmailGet {
|
|||
|
||||
impl EmailGet {
|
||||
pub fn new(get_call: Get<EmailObject>) -> Self {
|
||||
Self {
|
||||
EmailGet {
|
||||
get_call,
|
||||
body_properties: Vec::new(),
|
||||
fetch_text_body_values: false,
|
||||
|
@ -545,8 +541,8 @@ impl EmailFilterCondition {
|
|||
impl FilterTrait<EmailObject> for EmailFilterCondition {}
|
||||
|
||||
impl From<EmailFilterCondition> for FilterCondition<EmailFilterCondition, EmailObject> {
|
||||
fn from(val: EmailFilterCondition) -> Self {
|
||||
Self {
|
||||
fn from(val: EmailFilterCondition) -> FilterCondition<EmailFilterCondition, EmailObject> {
|
||||
FilterCondition {
|
||||
cond: val,
|
||||
_ph: PhantomData,
|
||||
}
|
||||
|
@ -583,12 +579,9 @@ pub enum MessageProperty {
|
|||
|
||||
impl From<crate::search::Query> for Filter<EmailFilterCondition, EmailObject> {
|
||||
fn from(val: crate::search::Query) -> Self {
|
||||
let mut ret = Self::Condition(EmailFilterCondition::new().into());
|
||||
let mut ret = Filter::Condition(EmailFilterCondition::new().into());
|
||||
fn rec(q: &crate::search::Query, f: &mut Filter<EmailFilterCondition, EmailObject>) {
|
||||
use datetime::{formats::RFC3339_DATE, timestamp_to_string};
|
||||
|
||||
use crate::search::Query::*;
|
||||
|
||||
match q {
|
||||
Subject(t) => {
|
||||
*f = Filter::Condition(EmailFilterCondition::new().subject(t.clone()).into());
|
||||
|
@ -611,48 +604,23 @@ impl From<crate::search::Query> for Filter<EmailFilterCondition, EmailObject> {
|
|||
Body(t) => {
|
||||
*f = Filter::Condition(EmailFilterCondition::new().body(t.clone()).into());
|
||||
}
|
||||
Before(t) => {
|
||||
*f = Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.before(timestamp_to_string(*t, Some(RFC3339_DATE), true))
|
||||
.into(),
|
||||
);
|
||||
Before(_) => {
|
||||
//TODO, convert UNIX timestamp into UtcDate
|
||||
}
|
||||
After(t) => {
|
||||
*f = Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.after(timestamp_to_string(*t, Some(RFC3339_DATE), true))
|
||||
.into(),
|
||||
);
|
||||
After(_) => {
|
||||
//TODO
|
||||
}
|
||||
Between(a, b) => {
|
||||
*f = Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.after(timestamp_to_string(*a, Some(RFC3339_DATE), true))
|
||||
.into(),
|
||||
);
|
||||
*f &= Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.before(timestamp_to_string(*b, Some(RFC3339_DATE), true))
|
||||
.into(),
|
||||
);
|
||||
Between(_, _) => {
|
||||
//TODO
|
||||
}
|
||||
On(t) => {
|
||||
rec(&Between(*t, *t), f);
|
||||
On(_) => {
|
||||
//TODO
|
||||
}
|
||||
InReplyTo(ref s) => {
|
||||
*f = Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.header(vec!["In-Reply-To".to_string().into(), s.to_string().into()])
|
||||
.into(),
|
||||
);
|
||||
InReplyTo(_) => {
|
||||
//TODO, look inside Headers
|
||||
}
|
||||
References(ref s) => {
|
||||
*f = Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.header(vec!["References".to_string().into(), s.to_string().into()])
|
||||
.into(),
|
||||
);
|
||||
References(_) => {
|
||||
//TODO
|
||||
}
|
||||
AllAddresses(_) => {
|
||||
//TODO
|
||||
|
@ -735,18 +703,6 @@ impl From<crate::search::Query> for Filter<EmailFilterCondition, EmailObject> {
|
|||
rec(q, &mut qhs);
|
||||
*f = !qhs;
|
||||
}
|
||||
Answered => {
|
||||
// TODO
|
||||
}
|
||||
AnsweredBy { .. } => {
|
||||
// TODO
|
||||
}
|
||||
Larger { .. } => {
|
||||
// TODO
|
||||
}
|
||||
Smaller { .. } => {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
rec(&val, &mut ret);
|
||||
|
@ -810,7 +766,7 @@ impl Method<EmailObject> for EmailSet {
|
|||
|
||||
impl EmailSet {
|
||||
pub fn new(set_call: Set<EmailObject>) -> Self {
|
||||
Self { set_call }
|
||||
EmailSet { set_call }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -827,7 +783,7 @@ impl Method<EmailObject> for EmailChanges {
|
|||
|
||||
impl EmailChanges {
|
||||
pub fn new(changes_call: Changes<EmailObject>) -> Self {
|
||||
Self { changes_call }
|
||||
EmailChanges { changes_call }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -846,7 +802,7 @@ impl EmailQueryChanges {
|
|||
pub fn new(
|
||||
query_changes_call: QueryChanges<Filter<EmailFilterCondition, EmailObject>, EmailObject>,
|
||||
) -> Self {
|
||||
Self { query_changes_call }
|
||||
EmailQueryChanges { query_changes_call }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -860,17 +816,9 @@ pub struct EmailQueryChangesResponse {
|
|||
}
|
||||
|
||||
impl std::convert::TryFrom<&RawValue> for EmailQueryChangesResponse {
|
||||
type Error = crate::error::Error;
|
||||
fn try_from(t: &RawValue) -> Result<Self> {
|
||||
let res: (String, Self, String) = serde_json::from_str(t.get()).map_err(|err| {
|
||||
crate::error::Error::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)
|
||||
})?;
|
||||
type Error = crate::error::MeliError;
|
||||
fn try_from(t: &RawValue) -> Result<EmailQueryChangesResponse> {
|
||||
let res: (String, EmailQueryChangesResponse, String) = serde_json::from_str(t.get())?;
|
||||
assert_eq!(&res.0, "Email/queryChanges");
|
||||
Ok(res.1)
|
||||
}
|
||||
|
|
|
@ -19,9 +19,8 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use serde_json::value::RawValue;
|
||||
|
||||
use super::*;
|
||||
use serde_json::value::RawValue;
|
||||
|
||||
/// #`import`
|
||||
///
|
||||
|
@ -32,6 +31,7 @@ use super::*;
|
|||
/// - `account_id`: "Id"
|
||||
///
|
||||
/// The id of the account to use.
|
||||
///
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ImportCall {
|
||||
|
@ -81,21 +81,16 @@ impl ImportCall {
|
|||
}
|
||||
|
||||
_impl!(
|
||||
/// - accountId: "Id"
|
||||
/// - accountId: "Id"
|
||||
///
|
||||
/// The id of the account to use.
|
||||
///
|
||||
account_id: Id<Account>
|
||||
);
|
||||
_impl!(if_in_state: Option<State<EmailObject>>);
|
||||
_impl!(emails: HashMap<Id<EmailObject>, EmailImport>);
|
||||
}
|
||||
|
||||
impl Default for ImportCall {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Method<EmailObject> for ImportCall {
|
||||
const NAME: &'static str = "Email/import";
|
||||
}
|
||||
|
@ -116,12 +111,6 @@ impl EmailImport {
|
|||
_impl!(received_at: Option<String>);
|
||||
}
|
||||
|
||||
impl Default for EmailImport {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(tag = "type")]
|
||||
|
@ -134,10 +123,9 @@ pub enum ImportError {
|
|||
AlreadyExists {
|
||||
description: Option<String>,
|
||||
/// An "existingId" property of type "Id" MUST be included on
|
||||
///the SetError object with the id of the existing Email. If
|
||||
/// duplicates are allowed, the newly created Email object MUST
|
||||
/// have a separate id and independent mutable properties to the
|
||||
/// existing object.
|
||||
///the SetError object with the id of the existing Email. If duplicates
|
||||
///are allowed, the newly created Email object MUST have a separate id
|
||||
///and independent mutable properties to the existing object.
|
||||
existing_id: Id<EmailObject>,
|
||||
},
|
||||
///If the "blobId", "mailboxIds", or "keywords" properties are invalid
|
||||
|
@ -158,8 +146,7 @@ pub enum ImportError {
|
|||
///different to the "blobId" on the EmailImport object. Alternatively,
|
||||
///the server MAY reject the import with an "invalidEmail" SetError.
|
||||
InvalidEmail { description: Option<String> },
|
||||
///An "ifInState" argument was supplied, and it does not match the current
|
||||
/// state.
|
||||
///An "ifInState" argument was supplied, and it does not match the current state.
|
||||
StateMismatch,
|
||||
}
|
||||
|
||||
|
@ -195,17 +182,9 @@ pub struct ImportResponse {
|
|||
}
|
||||
|
||||
impl std::convert::TryFrom<&RawValue> for ImportResponse {
|
||||
type Error = crate::error::Error;
|
||||
fn try_from(t: &RawValue) -> Result<Self> {
|
||||
let res: (String, Self, String) = serde_json::from_str(t.get()).map_err(|err| {
|
||||
crate::error::Error::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)
|
||||
})?;
|
||||
type Error = crate::error::MeliError;
|
||||
fn try_from(t: &RawValue) -> Result<ImportResponse> {
|
||||
let res: (String, ImportResponse, String) = serde_json::from_str(t.get())?;
|
||||
assert_eq!(&res.0, &ImportCall::NAME);
|
||||
Ok(res.1)
|
||||
}
|
||||
|
|
|
@ -23,7 +23,9 @@ use super::*;
|
|||
|
||||
impl Id<MailboxObject> {
|
||||
pub fn into_hash(&self) -> MailboxHash {
|
||||
MailboxHash::from_bytes(self.inner.as_bytes())
|
||||
let mut h = DefaultHasher::new();
|
||||
h.write(self.inner.as_bytes());
|
||||
h.finish()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,7 +70,7 @@ pub struct MailboxGet {
|
|||
}
|
||||
impl MailboxGet {
|
||||
pub fn new(get_call: Get<MailboxObject>) -> Self {
|
||||
Self { get_call }
|
||||
MailboxGet { get_call }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,9 +19,8 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// `BackendOp` implementor for Imap
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -37,7 +36,7 @@ impl JmapOp {
|
|||
connection: Arc<FutureMutex<JmapConnection>>,
|
||||
store: Arc<Store>,
|
||||
) -> Self {
|
||||
Self {
|
||||
JmapOp {
|
||||
hash,
|
||||
connection,
|
||||
store,
|
||||
|
@ -72,7 +71,7 @@ impl BackendOp for JmapOp {
|
|||
))
|
||||
.await?;
|
||||
|
||||
let res_text = res.text().await?;
|
||||
let res_text = res.text_async().await?;
|
||||
|
||||
store
|
||||
.byte_cache
|
||||
|
|
|
@ -19,12 +19,11 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
use super::mailbox::JmapMailbox;
|
||||
use super::*;
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use super::{mailbox::JmapMailbox, *};
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
pub type UtcDate = String;
|
||||
|
||||
|
@ -63,7 +62,7 @@ pub struct Request {
|
|||
|
||||
impl Request {
|
||||
pub fn new(request_no: Arc<Mutex<usize>>) -> Self {
|
||||
Self {
|
||||
Request {
|
||||
using: USING,
|
||||
method_calls: Vec::new(),
|
||||
request_no,
|
||||
|
@ -78,6 +77,13 @@ impl Request {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct JsonResponse<'a> {
|
||||
#[serde(borrow)]
|
||||
method_responses: Vec<MethodResponse<'a>>,
|
||||
}
|
||||
|
||||
pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash, JmapMailbox>> {
|
||||
let seq = get_request_no!(conn.request_no);
|
||||
let api_url = conn.session.lock().unwrap().api_url.clone();
|
||||
|
@ -95,39 +101,15 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
|
|||
)
|
||||
.await?;
|
||||
|
||||
let res_text = res.text().await?;
|
||||
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
|
||||
Err(err) => {
|
||||
let err = Error::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 res_text = res.text_async().await?;
|
||||
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
|
||||
*conn.store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
|
||||
let m = GetResponse::<MailboxObject>::try_from(v.method_responses.remove(0))?;
|
||||
let GetResponse::<MailboxObject> {
|
||||
list, account_id, ..
|
||||
} = m;
|
||||
// Is account set as `personal`? (`isPersonal` property). Then, even if
|
||||
// `isSubscribed` is false on a mailbox, it should be regarded as
|
||||
// subscribed.
|
||||
let is_personal: bool = {
|
||||
let session = conn.session_guard();
|
||||
session
|
||||
.accounts
|
||||
.get(&account_id)
|
||||
.map(|acc| acc.is_personal)
|
||||
.unwrap_or(false)
|
||||
};
|
||||
*conn.store.account_id.lock().unwrap() = account_id;
|
||||
let mut ret: HashMap<MailboxHash, JmapMailbox> = list
|
||||
Ok(list
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
let MailboxObject {
|
||||
|
@ -159,7 +141,7 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
|
|||
path: name,
|
||||
children: Vec::new(),
|
||||
id,
|
||||
is_subscribed: is_subscribed || is_personal,
|
||||
is_subscribed,
|
||||
my_rights,
|
||||
parent_id,
|
||||
parent_hash,
|
||||
|
@ -175,13 +157,7 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
|
|||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
for key in ret.keys().cloned().collect::<SmallVec<[MailboxHash; 24]>>() {
|
||||
if let Some(parent_hash) = ret[&key].parent_hash {
|
||||
ret.entry(parent_hash).and_modify(|e| e.children.push(key));
|
||||
}
|
||||
}
|
||||
Ok(ret)
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn get_message_list(
|
||||
|
@ -190,7 +166,7 @@ pub async fn get_message_list(
|
|||
) -> Result<Vec<Id<EmailObject>>> {
|
||||
let email_call: EmailQuery = EmailQuery::new(
|
||||
Query::new()
|
||||
.account_id(conn.mail_account_id())
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.filter(Some(Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.in_mailbox(Some(mailbox.id.clone()))
|
||||
|
@ -209,21 +185,8 @@ pub async fn get_message_list(
|
|||
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
|
||||
.await?;
|
||||
|
||||
let res_text = res.text().await?;
|
||||
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
|
||||
Err(err) => {
|
||||
let err = Error::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 res_text = res.text_async().await?;
|
||||
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
|
||||
*conn.store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
|
||||
let m = QueryResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
|
||||
let QueryResponse::<EmailObject> { ids, .. } = m;
|
||||
|
@ -245,7 +208,7 @@ pub async fn get_message(conn: &JmapConnection, ids: &[String]) -> Result<Vec<En
|
|||
.post_async(&conn.session.api_url, serde_json::to_string(&req)?)
|
||||
.await?;
|
||||
|
||||
let res_text = res.text().await?;
|
||||
let res_text = res.text_async().await?;
|
||||
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
|
||||
let e = GetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
|
||||
let GetResponse::<EmailObject> { list, .. } = e;
|
||||
|
@ -264,7 +227,7 @@ pub async fn fetch(
|
|||
let mailbox_id = store.mailboxes.read().unwrap()[&mailbox_hash].id.clone();
|
||||
let email_query_call: EmailQuery = EmailQuery::new(
|
||||
Query::new()
|
||||
.account_id(conn.mail_account_id())
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.filter(Some(Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.in_mailbox(Some(mailbox_id))
|
||||
|
@ -283,7 +246,7 @@ pub async fn fetch(
|
|||
prev_seq,
|
||||
EmailQuery::RESULT_FIELD_IDS,
|
||||
)))
|
||||
.account_id(conn.mail_account_id()),
|
||||
.account_id(conn.mail_account_id().clone()),
|
||||
);
|
||||
|
||||
req.add_call(&email_call);
|
||||
|
@ -294,22 +257,9 @@ pub async fn fetch(
|
|||
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
|
||||
.await?;
|
||||
|
||||
let res_text = res.text().await?;
|
||||
let res_text = res.text_async().await?;
|
||||
|
||||
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
|
||||
Err(err) => {
|
||||
let err = Error::new(format!(
|
||||
"BUG: Could not deserialize {} server JSON response properly, please report \
|
||||
this!\nReply from server: {}",
|
||||
&conn.server_conf.server_url, &res_text
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(ErrorKind::Bug);
|
||||
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
Ok(s) => s,
|
||||
};
|
||||
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
|
||||
let e = GetResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
|
||||
let query_response = QueryResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
|
||||
store
|
||||
|
|
|
@ -19,30 +19,23 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use core::marker::PhantomData;
|
||||
use std::{
|
||||
hash::{Hash, Hasher},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use serde::{
|
||||
de::DeserializeOwned,
|
||||
ser::{Serialize, SerializeStruct, Serializer},
|
||||
};
|
||||
use serde_json::{value::RawValue, Value};
|
||||
|
||||
use crate::email::parser::BytesExt;
|
||||
use core::marker::PhantomData;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::ser::{Serialize, SerializeStruct, Serializer};
|
||||
use serde_json::{value::RawValue, Value};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
|
||||
mod filters;
|
||||
pub use filters::*;
|
||||
mod comparator;
|
||||
pub use comparator::*;
|
||||
mod argument;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub use argument::*;
|
||||
|
||||
use super::protocol::Method;
|
||||
use std::collections::HashMap;
|
||||
pub trait Object {
|
||||
const NAME: &'static str;
|
||||
}
|
||||
|
@ -72,7 +65,7 @@ impl core::fmt::Debug for Id<String> {
|
|||
//, Hash, Eq, PartialEq, Default)]
|
||||
impl<OBJ> Clone for Id<OBJ> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
Id {
|
||||
inner: self.inner.clone(),
|
||||
_ph: PhantomData,
|
||||
}
|
||||
|
@ -101,7 +94,7 @@ impl<OBJ> Default for Id<OBJ> {
|
|||
|
||||
impl<OBJ> From<String> for Id<OBJ> {
|
||||
fn from(inner: String) -> Self {
|
||||
Self {
|
||||
Id {
|
||||
inner,
|
||||
_ph: PhantomData,
|
||||
}
|
||||
|
@ -146,7 +139,7 @@ pub struct State<OBJ> {
|
|||
//, Hash, Eq, PartialEq, Default)]
|
||||
impl<OBJ> Clone for State<OBJ> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
State {
|
||||
inner: self.inner.clone(),
|
||||
_ph: PhantomData,
|
||||
}
|
||||
|
@ -175,7 +168,7 @@ impl<OBJ> Default for State<OBJ> {
|
|||
|
||||
impl<OBJ> From<String> for State<OBJ> {
|
||||
fn from(inner: String) -> Self {
|
||||
Self {
|
||||
State {
|
||||
inner,
|
||||
_ph: PhantomData,
|
||||
}
|
||||
|
@ -234,32 +227,32 @@ impl Object for JmapSession {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CapabilitiesObject {
|
||||
#[serde(default)]
|
||||
pub max_size_upload: u64,
|
||||
max_size_upload: u64,
|
||||
#[serde(default)]
|
||||
pub max_concurrent_upload: u64,
|
||||
max_concurrent_upload: u64,
|
||||
#[serde(default)]
|
||||
pub max_size_request: u64,
|
||||
max_size_request: u64,
|
||||
#[serde(default)]
|
||||
pub max_concurrent_requests: u64,
|
||||
max_concurrent_requests: u64,
|
||||
#[serde(default)]
|
||||
pub max_calls_in_request: u64,
|
||||
max_calls_in_request: u64,
|
||||
#[serde(default)]
|
||||
pub max_objects_in_get: u64,
|
||||
max_objects_in_get: u64,
|
||||
#[serde(default)]
|
||||
pub max_objects_in_set: u64,
|
||||
max_objects_in_set: u64,
|
||||
#[serde(default)]
|
||||
pub collation_algorithms: Vec<String>,
|
||||
collation_algorithms: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Account {
|
||||
pub name: String,
|
||||
pub is_personal: bool,
|
||||
pub is_read_only: bool,
|
||||
pub account_capabilities: HashMap<String, Value>,
|
||||
name: String,
|
||||
is_personal: bool,
|
||||
is_read_only: bool,
|
||||
account_capabilities: HashMap<String, Value>,
|
||||
#[serde(flatten)]
|
||||
pub extra_properties: HashMap<String, Value>,
|
||||
extra_properties: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
impl Object for Account {
|
||||
|
@ -282,11 +275,12 @@ impl Object for BlobObject {
|
|||
/// - `account_id`: "Id"
|
||||
///
|
||||
/// The id of the account to use.
|
||||
///
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Get<OBJ>
|
||||
pub struct Get<OBJ: Object>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub account_id: Id<Account>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
@ -298,9 +292,9 @@ where
|
|||
_ph: PhantomData<fn() -> OBJ>,
|
||||
}
|
||||
|
||||
impl<OBJ> Get<OBJ>
|
||||
impl<OBJ: Object> Get<OBJ>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
@ -311,43 +305,35 @@ where
|
|||
}
|
||||
}
|
||||
_impl!(
|
||||
/// - accountId: "Id"
|
||||
/// - accountId: "Id"
|
||||
///
|
||||
/// The id of the account to use.
|
||||
///
|
||||
account_id: Id<Account>
|
||||
);
|
||||
_impl!(
|
||||
/// - ids: `Option<JmapArgument<Vec<String>>>`
|
||||
/// - ids: `Option<JmapArgument<Vec<String>>>`
|
||||
///
|
||||
/// The ids of the Foo objects to return. If `None`, then *all* records
|
||||
/// of the data type are returned, if this is supported for that data
|
||||
/// type and the number of records does not exceed the
|
||||
/// "max_objects_in_get" limit.
|
||||
///
|
||||
/// The ids of the Foo objects to return. If `None`, then *all*
|
||||
/// records of the data type are returned, if this is
|
||||
/// supported for that data type and the number of records
|
||||
/// does not exceed the "max_objects_in_get" limit.
|
||||
ids: Option<JmapArgument<Vec<Id<OBJ>>>>
|
||||
);
|
||||
_impl!(
|
||||
/// - properties: Option<Vec<String>>
|
||||
/// - properties: Option<Vec<String>>
|
||||
///
|
||||
/// If supplied, only the properties listed in the array are
|
||||
/// returned for each `Foo` object. If `None`, all
|
||||
/// properties of the object are returned. The `id`
|
||||
/// property of the object is *always* returned, even if
|
||||
/// not explicitly requested. If an invalid property is
|
||||
/// requested, the call WILL be rejected with an
|
||||
/// "invalid_arguments" error.
|
||||
/// If supplied, only the properties listed in the array are returned
|
||||
/// for each `Foo` object. If `None`, all properties of the object are
|
||||
/// returned. The `id` property of the object is *always* returned,
|
||||
/// even if not explicitly requested. If an invalid property is
|
||||
/// requested, the call WILL be rejected with an "invalid_arguments"
|
||||
/// error.
|
||||
properties: Option<Vec<String>>
|
||||
);
|
||||
}
|
||||
|
||||
impl<OBJ> Default for Get<OBJ>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<OBJ: Object + Serialize + std::fmt::Debug> Serialize for Get<OBJ> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
|
@ -425,17 +411,9 @@ pub struct GetResponse<OBJ: Object> {
|
|||
}
|
||||
|
||||
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for GetResponse<OBJ> {
|
||||
type Error = crate::error::Error;
|
||||
fn try_from(t: &RawValue) -> Result<Self, crate::error::Error> {
|
||||
let res: (String, Self, String) = serde_json::from_str(t.get()).map_err(|err| {
|
||||
crate::error::Error::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)
|
||||
})?;
|
||||
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())?;
|
||||
assert_eq!(&res.0, &format!("{}/get", OBJ::NAME));
|
||||
Ok(res.1)
|
||||
}
|
||||
|
@ -458,31 +436,31 @@ enum JmapError {
|
|||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Query<F: FilterTrait<OBJ>, OBJ>
|
||||
pub struct Query<F: FilterTrait<OBJ>, OBJ: Object>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub account_id: Id<Account>,
|
||||
pub filter: Option<F>,
|
||||
pub sort: Option<Comparator<OBJ>>,
|
||||
account_id: Id<Account>,
|
||||
filter: Option<F>,
|
||||
sort: Option<Comparator<OBJ>>,
|
||||
#[serde(default)]
|
||||
pub position: u64,
|
||||
position: u64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub anchor: Option<String>,
|
||||
anchor: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "u64_zero")]
|
||||
pub anchor_offset: u64,
|
||||
anchor_offset: u64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub limit: Option<u64>,
|
||||
limit: Option<u64>,
|
||||
#[serde(default = "bool_false")]
|
||||
pub calculate_total: bool,
|
||||
calculate_total: bool,
|
||||
#[serde(skip)]
|
||||
_ph: PhantomData<fn() -> OBJ>,
|
||||
}
|
||||
|
||||
impl<F: FilterTrait<OBJ>, OBJ> Query<F, OBJ>
|
||||
impl<F: FilterTrait<OBJ>, OBJ: Object> Query<F, OBJ>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
@ -508,15 +486,6 @@ where
|
|||
_impl!(calculate_total: bool);
|
||||
}
|
||||
|
||||
impl<F: FilterTrait<OBJ>, OBJ> Default for Query<F, OBJ>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn u64_zero(num: &u64) -> bool {
|
||||
*num == 0
|
||||
}
|
||||
|
@ -546,17 +515,9 @@ pub struct QueryResponse<OBJ: Object> {
|
|||
}
|
||||
|
||||
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for QueryResponse<OBJ> {
|
||||
type Error = crate::error::Error;
|
||||
fn try_from(t: &RawValue) -> Result<Self, crate::error::Error> {
|
||||
let res: (String, Self, String) = serde_json::from_str(t.get()).map_err(|err| {
|
||||
crate::error::Error::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)
|
||||
})?;
|
||||
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())?;
|
||||
assert_eq!(&res.0, &format!("{}/query", OBJ::NAME));
|
||||
Ok(res.1)
|
||||
}
|
||||
|
@ -573,15 +534,15 @@ pub struct ResultField<M: Method<OBJ>, OBJ: Object> {
|
|||
|
||||
impl<M: Method<OBJ>, OBJ: Object> ResultField<M, OBJ> {
|
||||
pub fn new(field: &'static str) -> Self {
|
||||
Self {
|
||||
ResultField {
|
||||
field,
|
||||
_ph: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// error[E0723]: trait bounds other than `Sized` on const fn parameters are
|
||||
// unstable --> melib/src/backends/jmap/rfc8620.rs:626:6
|
||||
// error[E0723]: trait bounds other than `Sized` on const fn parameters are unstable
|
||||
// --> melib/src/backends/jmap/rfc8620.rs:626:6
|
||||
// |
|
||||
// 626 | impl<M: Method<OBJ>, OBJ: Object> ResultField<M, OBJ> {
|
||||
// | ^
|
||||
|
@ -599,9 +560,8 @@ impl<M: Method<OBJ>, OBJ: Object> ResultField<M, OBJ> {
|
|||
|
||||
/// #`changes`
|
||||
///
|
||||
/// The "Foo/changes" method allows a client to efficiently update the state
|
||||
/// of its Foo cache to match the new state on the server. It takes the
|
||||
/// following arguments:
|
||||
/// The "Foo/changes" method allows a client to efficiently update the state of its Foo cache
|
||||
/// to match the new state on the server. It takes the following arguments:
|
||||
///
|
||||
/// - accountId: "Id" The id of the account to use.
|
||||
/// - sinceState: "String"
|
||||
|
@ -617,12 +577,13 @@ impl<M: Method<OBJ>, OBJ: Object> ResultField<M, OBJ> {
|
|||
/// to return. If supplied by the client, the value MUST be a
|
||||
/// positive integer greater than 0. If a value outside of this range
|
||||
/// is given, the server MUST re
|
||||
///
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
/* ch-ch-ch-ch-ch-Changes */
|
||||
pub struct Changes<OBJ>
|
||||
pub struct Changes<OBJ: Object>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub account_id: Id<Account>,
|
||||
pub since_state: State<OBJ>,
|
||||
|
@ -632,9 +593,9 @@ where
|
|||
_ph: PhantomData<fn() -> OBJ>,
|
||||
}
|
||||
|
||||
impl<OBJ> Changes<OBJ>
|
||||
impl<OBJ: Object> Changes<OBJ>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
@ -645,9 +606,10 @@ where
|
|||
}
|
||||
}
|
||||
_impl!(
|
||||
/// - accountId: "Id"
|
||||
/// - accountId: "Id"
|
||||
///
|
||||
/// The id of the account to use.
|
||||
///
|
||||
account_id: Id<Account>
|
||||
);
|
||||
_impl!(
|
||||
|
@ -656,6 +618,8 @@ where
|
|||
/// returned as the "state" argument in the "Foo/get" response. The
|
||||
/// server will return the changes that have occurred since this
|
||||
/// state.
|
||||
///
|
||||
///
|
||||
since_state: State<OBJ>
|
||||
);
|
||||
_impl!(
|
||||
|
@ -664,21 +628,12 @@ where
|
|||
/// MAY choose to return fewer than this value but MUST NOT return
|
||||
/// more. If not given by the client, the server may choose how many
|
||||
/// to return. If supplied by the client, the value MUST be a
|
||||
/// positive integer greater than 0. If a value outside of this
|
||||
/// range is given, the server MUST re
|
||||
/// positive integer greater than 0. If a value outside of this range
|
||||
/// is given, the server MUST re
|
||||
max_changes: Option<u64>
|
||||
);
|
||||
}
|
||||
|
||||
impl<OBJ> Default for Changes<OBJ>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChangesResponse<OBJ: Object> {
|
||||
|
@ -694,17 +649,9 @@ pub struct ChangesResponse<OBJ: Object> {
|
|||
}
|
||||
|
||||
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for ChangesResponse<OBJ> {
|
||||
type Error = crate::error::Error;
|
||||
fn try_from(t: &RawValue) -> Result<Self, crate::error::Error> {
|
||||
let res: (String, Self, String) = serde_json::from_str(t.get()).map_err(|err| {
|
||||
crate::error::Error::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)
|
||||
})?;
|
||||
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())?;
|
||||
assert_eq!(&res.0, &format!("{}/changes", OBJ::NAME));
|
||||
Ok(res.1)
|
||||
}
|
||||
|
@ -730,9 +677,9 @@ impl<OBJ: Object> ChangesResponse<OBJ> {
|
|||
///record type).
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Set<OBJ>
|
||||
pub struct Set<OBJ: Object>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
///o accountId: "Id"
|
||||
///
|
||||
|
@ -757,6 +704,7 @@ where
|
|||
///
|
||||
/// The client MUST omit any properties that may only be set by the
|
||||
/// server (for example, the "id" property on most object types).
|
||||
///
|
||||
pub create: Option<HashMap<Id<OBJ>, OBJ>>,
|
||||
///o update: "Id[PatchObject]|null"
|
||||
///
|
||||
|
@ -771,26 +719,26 @@ where
|
|||
/// All paths MUST also conform to the following restrictions; if
|
||||
/// there is any violation, the update MUST be rejected with an
|
||||
/// "invalidPatch" error:
|
||||
/// * The pointer MUST NOT reference inside an array (i.e., you MUST NOT
|
||||
/// insert/delete from an array; the array MUST be replaced in its
|
||||
/// entirety instead).
|
||||
/// * The pointer MUST NOT reference inside an array (i.e., you MUST
|
||||
/// NOT insert/delete from an array; the array MUST be replaced in
|
||||
/// its entirety instead).
|
||||
///
|
||||
/// * All parts prior to the last (i.e., the value after the final slash)
|
||||
/// MUST already exist on the object being patched.
|
||||
/// * All parts prior to the last (i.e., the value after the final
|
||||
/// slash) MUST already exist on the object being patched.
|
||||
///
|
||||
/// * There MUST NOT be two patches in the PatchObject where the pointer
|
||||
/// of one is the prefix of the pointer of the other, e.g.,
|
||||
/// "alerts/1/offset" and "alerts".
|
||||
/// * There MUST NOT be two patches in the PatchObject where the
|
||||
/// pointer of one is the prefix of the pointer of the other, e.g.,
|
||||
/// "alerts/1/offset" and "alerts".
|
||||
///
|
||||
/// The value associated with each pointer determines how to apply
|
||||
/// that patch:
|
||||
///
|
||||
/// * If null, set to the default value if specified for this property;
|
||||
/// otherwise, remove the property from the patched object. If the key
|
||||
/// is not present in the parent, this a no-op.
|
||||
/// * If null, set to the default value if specified for this
|
||||
/// property; otherwise, remove the property from the patched
|
||||
/// object. If the key is not present in the parent, this a no-op.
|
||||
///
|
||||
/// * Anything else: The value to set for this property (this may be a
|
||||
/// replacement or addition to the object being patched).
|
||||
/// * Anything else: The value to set for this property (this may be
|
||||
/// a replacement or addition to the object being patched).
|
||||
///
|
||||
/// Any server-set properties MAY be included in the patch if their
|
||||
/// value is identical to the current server value (before applying
|
||||
|
@ -809,9 +757,9 @@ where
|
|||
pub destroy: Option<Vec<Id<OBJ>>>,
|
||||
}
|
||||
|
||||
impl<OBJ> Set<OBJ>
|
||||
impl<OBJ: Object> Set<OBJ>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
@ -837,15 +785,6 @@ where
|
|||
_impl!(update: Option<HashMap<Id<OBJ>, Value>>);
|
||||
}
|
||||
|
||||
impl<OBJ> Default for Set<OBJ>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetResponse<OBJ: Object> {
|
||||
|
@ -908,17 +847,9 @@ pub struct SetResponse<OBJ: Object> {
|
|||
}
|
||||
|
||||
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for SetResponse<OBJ> {
|
||||
type Error = crate::error::Error;
|
||||
fn try_from(t: &RawValue) -> Result<Self, crate::error::Error> {
|
||||
let res: (String, Self, String) = serde_json::from_str(t.get()).map_err(|err| {
|
||||
crate::error::Error::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)
|
||||
})?;
|
||||
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())?;
|
||||
assert_eq!(&res.0, &format!("{}/set", OBJ::NAME));
|
||||
Ok(res.1)
|
||||
}
|
||||
|
@ -928,41 +859,31 @@ impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for SetRes
|
|||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(tag = "type", content = "description")]
|
||||
pub enum SetError {
|
||||
///(create; update; destroy). The create/update/destroy would violate an
|
||||
/// ACL or other permissions policy.
|
||||
///(create; update; destroy). The create/update/destroy would violate an ACL or other permissions policy.
|
||||
Forbidden(Option<String>),
|
||||
///(create; update). The create would exceed a server- defined limit on
|
||||
/// the number or total size of objects of this type.
|
||||
///(create; update). The create would exceed a server- defined limit on the number or total size of objects of this type.
|
||||
OverQuota(Option<String>),
|
||||
|
||||
///(create; update). The create/update would result in an object that
|
||||
/// exceeds a server-defined limit for the maximum size of a single object
|
||||
/// of this type.
|
||||
///(create; update). The create/update would result in an object that exceeds a server-defined limit for the maximum size of a single object of this type.
|
||||
TooLarge(Option<String>),
|
||||
|
||||
///(create). Too many objects of this type have been created recently, and
|
||||
/// a server-defined rate limit has been reached. It may work if tried
|
||||
/// again later.
|
||||
///(create). Too many objects of this type have been created recently, and a server-defined rate limit has been reached. It may work if tried again later.
|
||||
RateLimit(Option<String>),
|
||||
|
||||
///(update; destroy). The id given to update/destroy cannot be found.
|
||||
NotFound(Option<String>),
|
||||
|
||||
///(update). The PatchObject given to update the record was not a valid
|
||||
/// patch (see the patch description).
|
||||
///(update). The PatchObject given to update the record was not a valid patch (see the patch description).
|
||||
InvalidPatch(Option<String>),
|
||||
|
||||
///(update). The client requested that an object be both updated and
|
||||
/// destroyed in the same /set request, and the server has decided to
|
||||
/// therefore ignore the update.
|
||||
///(update). The client requested that an object be both updated and destroyed in the same /set request, and the server has decided to therefore ignore the update.
|
||||
WillDestroy(Option<String>),
|
||||
///(create; update). The record given is invalid in some way.
|
||||
InvalidProperties {
|
||||
description: Option<String>,
|
||||
properties: Vec<String>,
|
||||
},
|
||||
///(create; destroy). This is a singleton type, so you cannot create
|
||||
/// another one or destroy the existing one.
|
||||
///(create; destroy). This is a singleton type, so you cannot create another one or destroy the existing one.
|
||||
Singleton(Option<String>),
|
||||
RequestTooLarge(Option<String>),
|
||||
StateMismatch(Option<String>),
|
||||
|
@ -1034,7 +955,7 @@ pub fn download_request_format(
|
|||
ret.push_str(blob_id.as_str());
|
||||
prev_pos += "{blobId}".len();
|
||||
} else if download_url[prev_pos..].starts_with("{name}") {
|
||||
ret.push_str(name.as_deref().unwrap_or(""));
|
||||
ret.push_str(name.as_ref().map(String::as_str).unwrap_or(""));
|
||||
prev_pos += "{name}".len();
|
||||
}
|
||||
}
|
||||
|
@ -1076,9 +997,8 @@ pub struct UploadResponse {
|
|||
pub account_id: Id<Account>,
|
||||
///o blobId: "Id"
|
||||
///
|
||||
///The id representing the binary data uploaded. The data for this id is
|
||||
/// immutable. The id *only* refers to the binary data, not any
|
||||
/// metadata.
|
||||
///The id representing the binary data uploaded. The data for this id is immutable.
|
||||
///The id *only* refers to the binary data, not any metadata.
|
||||
pub blob_id: Id<BlobObject>,
|
||||
///o type: "String"
|
||||
///
|
||||
|
@ -1102,9 +1022,9 @@ pub struct UploadResponse {
|
|||
/// takes the following arguments:
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryChanges<F: FilterTrait<OBJ>, OBJ>
|
||||
pub struct QueryChanges<F: FilterTrait<OBJ>, OBJ: Object>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub account_id: Id<Account>,
|
||||
pub filter: Option<F>,
|
||||
|
@ -1146,9 +1066,9 @@ where
|
|||
_ph: PhantomData<fn() -> OBJ>,
|
||||
}
|
||||
|
||||
impl<F: FilterTrait<OBJ>, OBJ> QueryChanges<F, OBJ>
|
||||
impl<F: FilterTrait<OBJ>, OBJ: Object> QueryChanges<F, OBJ>
|
||||
where
|
||||
OBJ: Object + std::fmt::Debug + Serialize,
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub fn new(account_id: Id<Account>, since_query_state: String) -> Self {
|
||||
Self {
|
||||
|
@ -1174,15 +1094,11 @@ where
|
|||
pub struct QueryChangesResponse<OBJ: Object> {
|
||||
/// The id of the account used for the call.
|
||||
pub account_id: Id<Account>,
|
||||
/// This is the "sinceQueryState" argument echoed back; that is, the state
|
||||
/// from which the server is returning changes.
|
||||
/// This is the "sinceQueryState" argument echoed back; that is, the state from which the server is returning changes.
|
||||
pub old_query_state: String,
|
||||
///This is the state the query will be in after applying the set of changes
|
||||
/// to the old state.
|
||||
///This is the state the query will be in after applying the set of changes to the old state.
|
||||
pub new_query_state: String,
|
||||
/// The total number of Foos in the results (given the "filter"). This
|
||||
/// argument MUST be omitted if the "calculateTotal" request argument is not
|
||||
/// true.
|
||||
/// The total number of Foos in the results (given the "filter"). This argument MUST be omitted if the "calculateTotal" request argument is not true.
|
||||
#[serde(default)]
|
||||
pub total: Option<usize>,
|
||||
///The "id" for every Foo that was in the query results in the old
|
||||
|
@ -1219,9 +1135,9 @@ pub struct QueryChangesResponse<OBJ: Object> {
|
|||
|
||||
///An *AddedItem* object has the following properties:
|
||||
|
||||
/// * id: "Id"
|
||||
///* id: "Id"
|
||||
|
||||
/// * index: "UnsignedInt"
|
||||
///* index: "UnsignedInt"
|
||||
|
||||
///The result of this is that if the client has a cached sparse array of
|
||||
///Foo ids corresponding to the results in the old state, then:
|
||||
|
|
|
@ -19,10 +19,9 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::backends::jmap::{
|
||||
protocol::Method,
|
||||
rfc8620::{Object, ResultField},
|
||||
};
|
||||
use crate::backends::jmap::protocol::Method;
|
||||
use crate::backends::jmap::rfc8620::Object;
|
||||
use crate::backends::jmap::rfc8620::ResultField;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -37,7 +36,7 @@ pub enum JmapArgument<T> {
|
|||
|
||||
impl<T> JmapArgument<T> {
|
||||
pub fn value(v: T) -> Self {
|
||||
Self::Value(v)
|
||||
JmapArgument::Value(v)
|
||||
}
|
||||
|
||||
pub fn reference<M, OBJ>(result_of: usize, path: ResultField<M, OBJ>) -> Self
|
||||
|
@ -45,7 +44,7 @@ impl<T> JmapArgument<T> {
|
|||
M: Method<OBJ>,
|
||||
OBJ: Object,
|
||||
{
|
||||
Self::ResultReference {
|
||||
JmapArgument::ResultReference {
|
||||
result_of: format!("m{}", result_of),
|
||||
name: M::NAME.to_string(),
|
||||
path: path.field.to_string(),
|
||||
|
|
|
@ -51,9 +51,3 @@ impl<OBJ: Object> Comparator<OBJ> {
|
|||
_impl!(collation: Option<String>);
|
||||
_impl!(additional_properties: Vec<String>);
|
||||
}
|
||||
|
||||
impl<OBJ: Object> Default for Comparator<OBJ> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ pub struct FilterCondition<F: FilterTrait<OBJ>, OBJ: Object> {
|
|||
pub _ph: PhantomData<fn() -> OBJ>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Eq, PartialEq)]
|
||||
#[derive(Serialize, Debug, PartialEq)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum FilterOperator {
|
||||
And,
|
||||
|
@ -54,7 +54,7 @@ pub enum FilterOperator {
|
|||
|
||||
impl<F: FilterTrait<OBJ>, OBJ: Object> FilterCondition<F, OBJ> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
FilterCondition {
|
||||
cond: F::default(),
|
||||
_ph: PhantomData,
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ impl<F: FilterTrait<OBJ>, OBJ: Object> Default for FilterCondition<F, OBJ> {
|
|||
|
||||
impl<F: FilterTrait<OBJ>, OBJ: Object> Default for Filter<F, OBJ> {
|
||||
fn default() -> Self {
|
||||
Self::Condition(FilterCondition::default())
|
||||
Filter::Condition(FilterCondition::default())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,17 +78,17 @@ use std::ops::{BitAndAssign, BitOrAssign, Not};
|
|||
impl<F: FilterTrait<OBJ>, OBJ: Object> BitAndAssign for Filter<F, OBJ> {
|
||||
fn bitand_assign(&mut self, rhs: Self) {
|
||||
match self {
|
||||
Self::Operator {
|
||||
Filter::Operator {
|
||||
operator: FilterOperator::And,
|
||||
ref mut conditions,
|
||||
} => {
|
||||
conditions.push(rhs);
|
||||
}
|
||||
Self::Condition(_) | Self::Operator { .. } => {
|
||||
*self = Self::Operator {
|
||||
Filter::Condition(_) | Filter::Operator { .. } => {
|
||||
*self = Filter::Operator {
|
||||
operator: FilterOperator::And,
|
||||
conditions: vec![
|
||||
std::mem::replace(self, Self::Condition(FilterCondition::new())),
|
||||
std::mem::replace(self, Filter::Condition(FilterCondition::new())),
|
||||
rhs,
|
||||
],
|
||||
};
|
||||
|
@ -100,17 +100,17 @@ impl<F: FilterTrait<OBJ>, OBJ: Object> BitAndAssign for Filter<F, OBJ> {
|
|||
impl<F: FilterTrait<OBJ>, OBJ: Object> BitOrAssign for Filter<F, OBJ> {
|
||||
fn bitor_assign(&mut self, rhs: Self) {
|
||||
match self {
|
||||
Self::Operator {
|
||||
Filter::Operator {
|
||||
operator: FilterOperator::Or,
|
||||
ref mut conditions,
|
||||
} => {
|
||||
conditions.push(rhs);
|
||||
}
|
||||
Self::Condition(_) | Self::Operator { .. } => {
|
||||
*self = Self::Operator {
|
||||
Filter::Condition(_) | Filter::Operator { .. } => {
|
||||
*self = Filter::Operator {
|
||||
operator: FilterOperator::Or,
|
||||
conditions: vec![
|
||||
std::mem::replace(self, Self::Condition(FilterCondition::new())),
|
||||
std::mem::replace(self, Filter::Condition(FilterCondition::new())),
|
||||
rhs,
|
||||
],
|
||||
};
|
||||
|
@ -123,14 +123,14 @@ impl<F: FilterTrait<OBJ>, OBJ: Object> Not for Filter<F, OBJ> {
|
|||
type Output = Self;
|
||||
fn not(self) -> Self {
|
||||
match self {
|
||||
Self::Operator {
|
||||
Filter::Operator {
|
||||
operator,
|
||||
conditions,
|
||||
} if operator == FilterOperator::Not => Self::Operator {
|
||||
} if operator == FilterOperator::Not => Filter::Operator {
|
||||
operator: FilterOperator::Or,
|
||||
conditions,
|
||||
},
|
||||
Self::Condition(_) | Self::Operator { .. } => Self::Operator {
|
||||
Filter::Condition(_) | Filter::Operator { .. } => Filter::Operator {
|
||||
operator: FilterOperator::Not,
|
||||
conditions: vec![self],
|
||||
},
|
||||
|
|
|
@ -24,24 +24,19 @@ mod backend;
|
|||
pub use self::backend::*;
|
||||
|
||||
mod stream;
|
||||
use std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
fs,
|
||||
hash::{Hash, Hasher},
|
||||
io::{BufReader, Read},
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use futures::stream::Stream;
|
||||
pub use stream::*;
|
||||
|
||||
use crate::{
|
||||
backends::*,
|
||||
email::Flag,
|
||||
error::{Error, Result},
|
||||
utils::shellexpand::ShellExpandTrait,
|
||||
};
|
||||
use crate::backends::*;
|
||||
use crate::email::Flag;
|
||||
use crate::error::{MeliError, Result};
|
||||
use crate::shellexpand::ShellExpandTrait;
|
||||
pub use futures::stream::Stream;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::fs;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::{BufReader, Read};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// `BackendOp` implementor for Maildir
|
||||
#[derive(Debug)]
|
||||
|
@ -54,7 +49,7 @@ pub struct MaildirOp {
|
|||
|
||||
impl Clone for MaildirOp {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
MaildirOp {
|
||||
hash_index: self.hash_index.clone(),
|
||||
mailbox_hash: self.mailbox_hash,
|
||||
hash: self.hash,
|
||||
|
@ -65,7 +60,7 @@ impl Clone for MaildirOp {
|
|||
|
||||
impl MaildirOp {
|
||||
pub fn new(hash: EnvelopeHash, hash_index: HashIndexes, mailbox_hash: MailboxHash) -> Self {
|
||||
Self {
|
||||
MaildirOp {
|
||||
hash_index,
|
||||
mailbox_hash,
|
||||
hash,
|
||||
|
@ -81,13 +76,13 @@ impl MaildirOp {
|
|||
for e in map.iter() {
|
||||
debug!("{:#?}", e);
|
||||
}
|
||||
return Err(Error::new("File not found"));
|
||||
return Err(MeliError::new("File not found"));
|
||||
}
|
||||
|
||||
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()
|
||||
|
@ -95,13 +90,13 @@ impl MaildirOp {
|
|||
}
|
||||
}
|
||||
|
||||
impl BackendOp for MaildirOp {
|
||||
impl<'a> BackendOp for MaildirOp {
|
||||
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
|
||||
if self.slice.is_none() {
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(false)
|
||||
.open(self.path()?)?;
|
||||
.open(&self.path()?)?;
|
||||
let mut buf_reader = BufReader::new(file);
|
||||
let mut contents = Vec::new();
|
||||
buf_reader.read_to_end(&mut contents)?;
|
||||
|
@ -146,14 +141,14 @@ impl MaildirMailbox {
|
|||
let mut h = DefaultHasher::new();
|
||||
pathbuf.hash(&mut h);
|
||||
|
||||
/* Check if mailbox path (Eg `INBOX/Lists/luddites`) is included in the
|
||||
* subscribed mailboxes in user configuration */
|
||||
/* Check if mailbox path (Eg `INBOX/Lists/luddites`) is included in the subscribed
|
||||
* mailboxes in user configuration */
|
||||
let fname = pathbuf
|
||||
.strip_prefix(
|
||||
PathBuf::from(&settings.root_mailbox)
|
||||
.expand()
|
||||
.parent()
|
||||
.unwrap_or_else(|| Path::new("/")),
|
||||
.unwrap_or_else(|| &Path::new("/")),
|
||||
)
|
||||
.ok();
|
||||
|
||||
|
@ -163,8 +158,8 @@ impl MaildirMailbox {
|
|||
true
|
||||
};
|
||||
|
||||
let ret = Self {
|
||||
hash: MailboxHash(h.finish()),
|
||||
let ret = MaildirMailbox {
|
||||
hash: h.finish(),
|
||||
name: file_name,
|
||||
path: fname.unwrap().to_path_buf(),
|
||||
fs_path: pathbuf,
|
||||
|
@ -201,7 +196,7 @@ impl MaildirMailbox {
|
|||
for d in &["cur", "new", "tmp"] {
|
||||
p.push(d);
|
||||
if !p.is_dir() {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"{} is not a valid maildir mailbox",
|
||||
path.display()
|
||||
)));
|
||||
|
@ -222,7 +217,11 @@ impl BackendMailbox for MaildirMailbox {
|
|||
}
|
||||
|
||||
fn path(&self) -> &str {
|
||||
self.path.to_str().unwrap_or_else(|| self.name())
|
||||
self.path.to_str().unwrap_or(self.name())
|
||||
}
|
||||
|
||||
fn change_name(&mut self, s: &str) {
|
||||
self.name = s.to_string();
|
||||
}
|
||||
|
||||
fn children(&self) -> &[MailboxHash] {
|
||||
|
@ -284,11 +283,7 @@ impl MaildirPathTrait for Path {
|
|||
'S' => flag |= Flag::SEEN,
|
||||
'T' => flag |= Flag::TRASHED,
|
||||
_ => {
|
||||
debug!(
|
||||
"DEBUG: in MaildirPathTrait::flags(), encountered unknown flag marker \
|
||||
{:?}, path is {}",
|
||||
f, path
|
||||
);
|
||||
debug!("DEBUG: in MaildirPathTrait::flags(), encountered unknown flag marker {:?}, path is {}", f, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,38 +19,28 @@
|
|||
* 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;
|
||||
use crate::email::{Envelope, EnvelopeHash, Flag};
|
||||
use crate::error::{ErrorKind, MeliError, Result};
|
||||
use crate::shellexpand::ShellExpandTrait;
|
||||
use futures::prelude::Stream;
|
||||
|
||||
use super::{MaildirMailbox, MaildirOp, MaildirPathTrait};
|
||||
use crate::{
|
||||
backends::{RefreshEventKind::*, *},
|
||||
conf::AccountSettings,
|
||||
email::{Envelope, EnvelopeHash, Flag},
|
||||
error::{Error, ErrorKind, Result},
|
||||
utils::shellexpand::ShellExpandTrait,
|
||||
Collection,
|
||||
};
|
||||
|
||||
extern crate notify;
|
||||
use std::{
|
||||
collections::{hash_map::DefaultHasher, HashMap, HashSet},
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
hash::{Hash, Hasher},
|
||||
io::{self, Read, Write},
|
||||
ops::{Deref, DerefMut},
|
||||
os::unix::fs::PermissionsExt,
|
||||
path::{Component, Path, PathBuf},
|
||||
sync::{mpsc::channel, Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use self::notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
|
||||
use std::time::Duration;
|
||||
|
||||
use std::collections::{hash_map::DefaultHasher, HashMap, HashSet};
|
||||
use std::ffi::OsStr;
|
||||
use std::fs;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::{self, Read, Write};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(super) enum PathMod {
|
||||
|
@ -67,23 +57,22 @@ pub struct MaildirPath {
|
|||
|
||||
impl Deref for MaildirPath {
|
||||
type Target = PathBuf;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
fn deref(&self) -> &PathBuf {
|
||||
assert!(!(self.removed && self.modified.is_none()));
|
||||
&self.buf
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for MaildirPath {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
fn deref_mut(&mut self) -> &mut PathBuf {
|
||||
assert!(!(self.removed && self.modified.is_none()));
|
||||
&mut self.buf
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PathBuf> for MaildirPath {
|
||||
fn from(val: PathBuf) -> Self {
|
||||
Self {
|
||||
fn from(val: PathBuf) -> MaildirPath {
|
||||
MaildirPath {
|
||||
buf: val,
|
||||
modified: None,
|
||||
removed: false,
|
||||
|
@ -94,26 +83,25 @@ impl From<PathBuf> for MaildirPath {
|
|||
#[derive(Debug, Default)]
|
||||
pub struct HashIndex {
|
||||
index: HashMap<EnvelopeHash, MaildirPath>,
|
||||
_hash: MailboxHash,
|
||||
hash: MailboxHash,
|
||||
}
|
||||
|
||||
impl Deref for HashIndex {
|
||||
type Target = HashMap<EnvelopeHash, MaildirPath>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
fn deref(&self) -> &HashMap<EnvelopeHash, MaildirPath> {
|
||||
&self.index
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for HashIndex {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
fn deref_mut(&mut self) -> &mut HashMap<EnvelopeHash, MaildirPath> {
|
||||
&mut self.index
|
||||
}
|
||||
}
|
||||
|
||||
pub type HashIndexes = Arc<Mutex<HashMap<MailboxHash, HashIndex>>>;
|
||||
|
||||
/// The maildir backend instance type.
|
||||
/// Maildir backend https://cr.yp.to/proto/maildir.html
|
||||
#[derive(Debug)]
|
||||
pub struct MaildirType {
|
||||
name: String,
|
||||
|
@ -121,7 +109,6 @@ pub struct MaildirType {
|
|||
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
|
||||
hash_indexes: HashIndexes,
|
||||
event_consumer: BackendEventConsumer,
|
||||
collection: Collection,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
|
@ -153,10 +140,10 @@ macro_rules! get_path_hash {
|
|||
}};
|
||||
}
|
||||
|
||||
pub fn get_file_hash(file: &Path) -> EnvelopeHash {
|
||||
pub(super) fn get_file_hash(file: &Path) -> EnvelopeHash {
|
||||
let mut hasher = DefaultHasher::default();
|
||||
file.hash(&mut hasher);
|
||||
EnvelopeHash(hasher.finish())
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
pub fn move_to_cur(p: PathBuf) -> Result<PathBuf> {
|
||||
|
@ -211,17 +198,33 @@ 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 map = self.hash_indexes.clone();
|
||||
let mailbox_index = self.mailbox_index.clone();
|
||||
super::stream::MaildirStream::new(mailbox_hash, unseen, total, path, map, mailbox_index)
|
||||
super::stream::MaildirStream::new(
|
||||
&self.name,
|
||||
mailbox_hash,
|
||||
unseen,
|
||||
total,
|
||||
path,
|
||||
root_path,
|
||||
map,
|
||||
mailbox_index,
|
||||
)
|
||||
}
|
||||
|
||||
fn refresh(&mut self, mailbox_hash: MailboxHash) -> ResultFuture<()> {
|
||||
let account_hash = AccountHash::from_bytes(self.name.as_bytes());
|
||||
let cache_dir = xdg::BaseDirectories::with_profile("meli", &self.name).unwrap();
|
||||
let account_hash = {
|
||||
let mut hasher = DefaultHasher::default();
|
||||
hasher.write(self.name.as_bytes());
|
||||
hasher.finish()
|
||||
};
|
||||
let sender = self.event_consumer.clone();
|
||||
|
||||
let mailbox: &MaildirMailbox = &self.mailboxes[&mailbox_hash];
|
||||
let path: PathBuf = mailbox.fs_path().into();
|
||||
let root_path = self.path.to_path_buf();
|
||||
let map = self.hash_indexes.clone();
|
||||
let mailbox_index = self.mailbox_index.clone();
|
||||
|
||||
|
@ -229,7 +232,26 @@ impl MailBackend for MaildirType {
|
|||
let thunk = move |sender: &BackendEventConsumer| {
|
||||
debug!("refreshing");
|
||||
let mut buf = Vec::with_capacity(4096);
|
||||
let files = Self::list_mail_in_maildir_fs(path.clone(), false)?;
|
||||
let mut path = path.clone();
|
||||
path.push("new");
|
||||
for d in path.read_dir()? {
|
||||
if let Ok(p) = d {
|
||||
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 mut current_hashes = {
|
||||
let mut map = map.lock().unwrap();
|
||||
let map = map.entry(mailbox_hash).or_default();
|
||||
|
@ -256,6 +278,23 @@ impl MailBackend for MaildirType {
|
|||
.lock()
|
||||
.unwrap()
|
||||
.insert(env.hash(), mailbox_hash);
|
||||
let file_name = file.strip_prefix(&root_path).unwrap().to_path_buf();
|
||||
if let Ok(cached) = cache_dir.place_cache_file(file_name) {
|
||||
/* place result in cache directory */
|
||||
let f = fs::File::create(cached)?;
|
||||
let metadata = f.metadata()?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
permissions.set_mode(0o600); // Read/write for owner only.
|
||||
f.set_permissions(permissions)?;
|
||||
|
||||
let writer = io::BufWriter::new(f);
|
||||
bincode::Options::serialize_into(
|
||||
bincode::config::DefaultOptions::new(),
|
||||
writer,
|
||||
&env,
|
||||
)?;
|
||||
}
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
|
@ -302,12 +341,15 @@ impl MailBackend for MaildirType {
|
|||
let sender = self.event_consumer.clone();
|
||||
let (tx, rx) = channel();
|
||||
let mut watcher = watcher(tx, Duration::from_secs(2)).unwrap();
|
||||
let account_hash = AccountHash::from_bytes(self.name.as_bytes());
|
||||
let root_mailbox = self.path.to_path_buf();
|
||||
watcher
|
||||
.watch(&root_mailbox, RecursiveMode::Recursive)
|
||||
.unwrap();
|
||||
debug!("watching {:?}", root_mailbox);
|
||||
let account_hash = {
|
||||
let mut hasher = DefaultHasher::default();
|
||||
hasher.write(self.name.as_bytes());
|
||||
hasher.finish()
|
||||
};
|
||||
let root_path = self.path.to_path_buf();
|
||||
watcher.watch(&root_path, RecursiveMode::Recursive).unwrap();
|
||||
let cache_dir = xdg::BaseDirectories::with_profile("meli", &self.name).unwrap();
|
||||
debug!("watching {:?}", root_path);
|
||||
let hash_indexes = self.hash_indexes.clone();
|
||||
let mailbox_index = self.mailbox_index.clone();
|
||||
let root_mailbox_hash: MailboxHash = self
|
||||
|
@ -352,16 +394,17 @@ impl MailBackend for MaildirType {
|
|||
}
|
||||
};
|
||||
}
|
||||
let mailbox_hash = MailboxHash(get_path_hash!(pathbuf));
|
||||
let mailbox_hash = get_path_hash!(pathbuf);
|
||||
let file_name = pathbuf
|
||||
.as_path()
|
||||
.strip_prefix(&root_mailbox)
|
||||
.strip_prefix(&root_path)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
if let Ok(env) = add_path_to_index(
|
||||
&hash_indexes,
|
||||
mailbox_hash,
|
||||
pathbuf.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
|
@ -392,13 +435,13 @@ impl MailBackend for MaildirType {
|
|||
/* Update */
|
||||
DebouncedEvent::NoticeWrite(pathbuf) | DebouncedEvent::Write(pathbuf) => {
|
||||
debug!("DebouncedEvent::Write(path = {:?}", &pathbuf);
|
||||
let mailbox_hash = MailboxHash(get_path_hash!(pathbuf));
|
||||
let mailbox_hash = get_path_hash!(pathbuf);
|
||||
let mut hash_indexes_lock = hash_indexes.lock().unwrap();
|
||||
let index_lock =
|
||||
&mut hash_indexes_lock.entry(mailbox_hash).or_default();
|
||||
let file_name = pathbuf
|
||||
.as_path()
|
||||
.strip_prefix(&root_mailbox)
|
||||
.strip_prefix(&root_path)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
/* Linear search in hash_index to find old hash */
|
||||
|
@ -416,6 +459,7 @@ impl MailBackend for MaildirType {
|
|||
&hash_indexes,
|
||||
mailbox_hash,
|
||||
pathbuf.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
|
@ -468,7 +512,7 @@ impl MailBackend for MaildirType {
|
|||
/* Remove */
|
||||
DebouncedEvent::NoticeRemove(pathbuf) | DebouncedEvent::Remove(pathbuf) => {
|
||||
debug!("DebouncedEvent::Remove(path = {:?}", pathbuf);
|
||||
let mailbox_hash = MailboxHash(get_path_hash!(pathbuf));
|
||||
let mailbox_hash = get_path_hash!(pathbuf);
|
||||
let mut hash_indexes_lock = hash_indexes.lock().unwrap();
|
||||
let index_lock = hash_indexes_lock.entry(mailbox_hash).or_default();
|
||||
let hash: EnvelopeHash = if let Some((k, _)) =
|
||||
|
@ -489,7 +533,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| {
|
||||
|
@ -522,9 +566,9 @@ impl MailBackend for MaildirType {
|
|||
/* Envelope hasn't changed */
|
||||
DebouncedEvent::Rename(src, dest) => {
|
||||
debug!("DebouncedEvent::Rename(src = {:?}, dest = {:?})", src, dest);
|
||||
let mailbox_hash = MailboxHash(get_path_hash!(src));
|
||||
let mailbox_hash = get_path_hash!(src);
|
||||
let dest_mailbox = {
|
||||
let dest_mailbox = MailboxHash(get_path_hash!(dest));
|
||||
let dest_mailbox = get_path_hash!(dest);
|
||||
if dest_mailbox == mailbox_hash {
|
||||
None
|
||||
} else {
|
||||
|
@ -559,7 +603,7 @@ impl MailBackend for MaildirType {
|
|||
);
|
||||
let file_name = dest
|
||||
.as_path()
|
||||
.strip_prefix(&root_mailbox)
|
||||
.strip_prefix(&root_path)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
drop(hash_indexes_lock);
|
||||
|
@ -567,6 +611,7 @@ impl MailBackend for MaildirType {
|
|||
&hash_indexes,
|
||||
dest_mailbox,
|
||||
dest.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
|
@ -642,16 +687,13 @@ impl MailBackend for MaildirType {
|
|||
e.modified = Some(PathMod::Hash(new_hash));
|
||||
e.removed = false;
|
||||
});
|
||||
debug!(
|
||||
"contains_old_key, key was marked as removed (by external \
|
||||
source)"
|
||||
);
|
||||
debug!("contains_old_key, key was marked as removed (by external source)");
|
||||
} else {
|
||||
debug!("not contains_new_key");
|
||||
}
|
||||
let file_name = dest
|
||||
.as_path()
|
||||
.strip_prefix(&root_mailbox)
|
||||
.strip_prefix(&root_path)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
debug!("filename = {:?}", file_name);
|
||||
|
@ -660,6 +702,7 @@ impl MailBackend for MaildirType {
|
|||
&hash_indexes,
|
||||
dest_mailbox.unwrap_or(mailbox_hash),
|
||||
dest.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
|
@ -699,13 +742,14 @@ impl MailBackend for MaildirType {
|
|||
drop(hash_indexes_lock);
|
||||
let file_name = dest
|
||||
.as_path()
|
||||
.strip_prefix(&root_mailbox)
|
||||
.strip_prefix(&root_path)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
if let Ok(env) = add_path_to_index(
|
||||
&hash_indexes,
|
||||
dest_mailbox,
|
||||
dest.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
|
@ -800,7 +844,7 @@ impl MailBackend for MaildirType {
|
|||
) -> ResultFuture<()> {
|
||||
let path = self.mailboxes[&mailbox_hash].fs_path.clone();
|
||||
Ok(Box::pin(async move {
|
||||
Self::save_to_mailbox(path, bytes, flags)
|
||||
MaildirType::save_to_mailbox(path, bytes, flags)
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -812,7 +856,7 @@ impl MailBackend for MaildirType {
|
|||
) -> ResultFuture<()> {
|
||||
let hash_index = self.hash_indexes.clone();
|
||||
if flags.iter().any(|(f, _)| f.is_err()) {
|
||||
return Err(Error::new("Maildir doesn't support tags."));
|
||||
return Err(MeliError::new("Maildir doesn't support tags."));
|
||||
}
|
||||
|
||||
Ok(Box::pin(async move {
|
||||
|
@ -827,7 +871,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()
|
||||
|
@ -837,7 +881,7 @@ impl MailBackend for MaildirType {
|
|||
let path = _path.to_str().unwrap(); // Assume UTF-8 validity
|
||||
let idx: usize = path
|
||||
.rfind(":2,")
|
||||
.ok_or_else(|| Error::new(format!("Invalid email filename: {:?}", path)))?
|
||||
.ok_or_else(|| MeliError::new(format!("Invalid email filename: {:?}", path)))?
|
||||
+ 3;
|
||||
let mut new_name: String = path[..idx].to_string();
|
||||
for (f, value) in flags.iter() {
|
||||
|
@ -867,7 +911,7 @@ impl MailBackend for MaildirType {
|
|||
Some(PathMod::Path(new_name.clone()));
|
||||
|
||||
debug!("renaming {:?} to {:?}", path, new_name);
|
||||
fs::rename(path, &new_name)?;
|
||||
fs::rename(&path, &new_name)?;
|
||||
debug!("success in rename");
|
||||
}
|
||||
Ok(())
|
||||
|
@ -892,7 +936,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()
|
||||
|
@ -914,9 +958,9 @@ impl MailBackend for MaildirType {
|
|||
) -> ResultFuture<()> {
|
||||
let hash_index = self.hash_indexes.clone();
|
||||
if !self.mailboxes.contains_key(&source_mailbox_hash) {
|
||||
return Err(Error::new("Invalid source mailbox hash").set_kind(ErrorKind::Bug));
|
||||
return Err(MeliError::new("Invalid source mailbox hash").set_kind(ErrorKind::Bug));
|
||||
} else if !self.mailboxes.contains_key(&destination_mailbox_hash) {
|
||||
return Err(Error::new("Invalid destination mailbox hash").set_kind(ErrorKind::Bug));
|
||||
return Err(MeliError::new("Invalid destination mailbox hash").set_kind(ErrorKind::Bug));
|
||||
}
|
||||
let mut dest_path: PathBuf = self.mailboxes[&destination_mailbox_hash].fs_path().into();
|
||||
dest_path.push("cur");
|
||||
|
@ -932,15 +976,15 @@ impl MailBackend for MaildirType {
|
|||
if let Some(modif) = &hash_index[&env_hash].modified {
|
||||
match modif {
|
||||
PathMod::Path(ref path) => path.clone(),
|
||||
PathMod::Hash(hash) => hash_index[hash].to_path_buf(),
|
||||
PathMod::Hash(hash) => hash_index[&hash].to_path_buf(),
|
||||
}
|
||||
} else {
|
||||
hash_index[&env_hash].to_path_buf()
|
||||
}
|
||||
};
|
||||
let filename = path_src.file_name().ok_or_else(|| {
|
||||
format!("Could not get filename of `{}`", path_src.display(),)
|
||||
})?;
|
||||
let filename = path_src
|
||||
.file_name()
|
||||
.expect(&format!("Could not get filename of {}", path_src.display()));
|
||||
dest_path.push(filename);
|
||||
hash_index.entry(env_hash).or_default().modified =
|
||||
Some(PathMod::Path(dest_path.clone()));
|
||||
|
@ -959,10 +1003,6 @@ impl MailBackend for MaildirType {
|
|||
}))
|
||||
}
|
||||
|
||||
fn collection(&self) -> Collection {
|
||||
self.collection.clone()
|
||||
}
|
||||
|
||||
fn create_mailbox(
|
||||
&mut self,
|
||||
new_path: String,
|
||||
|
@ -970,16 +1010,12 @@ impl MailBackend for MaildirType {
|
|||
let mut path = self.path.clone();
|
||||
path.push(&new_path);
|
||||
if !path.starts_with(&self.path) {
|
||||
return Err(Error::new(format!(
|
||||
"Path given (`{}`) is absolute. Please provide a path relative to the account's \
|
||||
root mailbox.",
|
||||
&new_path
|
||||
)));
|
||||
return Err(MeliError::new(format!("Path given (`{}`) is absolute. Please provide a path relative to the account's root mailbox.", &new_path)));
|
||||
}
|
||||
|
||||
std::fs::create_dir(&path)?;
|
||||
/* create_dir does not create intermediate directories (like `mkdir -p`), so
|
||||
* the parent must be a valid mailbox at this point. */
|
||||
/* create_dir does not create intermediate directories (like `mkdir -p`), so the parent must be a valid
|
||||
* mailbox at this point. */
|
||||
|
||||
let parent = path.parent().and_then(|p| {
|
||||
self.mailboxes
|
||||
|
@ -988,7 +1024,7 @@ impl MailBackend for MaildirType {
|
|||
.map(|item| *item.0)
|
||||
});
|
||||
|
||||
let mailbox_hash = MailboxHash(get_path_hash!(&path));
|
||||
let mailbox_hash = get_path_hash!(&path);
|
||||
if let Some(parent) = parent {
|
||||
self.mailboxes
|
||||
.entry(parent)
|
||||
|
@ -1017,9 +1053,7 @@ impl MailBackend for MaildirType {
|
|||
&mut self,
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
|
||||
Err(Error::new(
|
||||
"Deleting mailboxes is currently unimplemented for maildir backend.",
|
||||
))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn set_mailbox_subscription(
|
||||
|
@ -1027,9 +1061,7 @@ impl MailBackend for MaildirType {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_val: bool,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new(
|
||||
"Mailbox subscriptions are not possible for the maildir backend.",
|
||||
))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn rename_mailbox(
|
||||
|
@ -1037,9 +1069,7 @@ impl MailBackend for MaildirType {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_new_path: String,
|
||||
) -> ResultFuture<Mailbox> {
|
||||
Err(Error::new(
|
||||
"Renaming mailboxes is currently unimplemented for maildir backend.",
|
||||
))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn set_mailbox_permissions(
|
||||
|
@ -1047,20 +1077,7 @@ impl MailBackend for MaildirType {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_val: crate::backends::MailboxPermissions,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new(
|
||||
"Setting mailbox permissions is not possible for the maildir backend.",
|
||||
))
|
||||
}
|
||||
|
||||
fn search(
|
||||
&self,
|
||||
_query: crate::search::Query,
|
||||
_mailbox_hash: Option<MailboxHash>,
|
||||
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>> {
|
||||
Err(
|
||||
Error::new("Search is unimplemented for the maildir backend.")
|
||||
.set_kind(ErrorKind::NotImplemented),
|
||||
)
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
|
@ -1073,7 +1090,6 @@ impl MailBackend for MaildirType {
|
|||
}
|
||||
|
||||
impl MaildirType {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(
|
||||
settings: &AccountSettings,
|
||||
is_subscribed: Box<dyn Fn(&str) -> bool>,
|
||||
|
@ -1086,7 +1102,7 @@ impl MaildirType {
|
|||
p: P,
|
||||
) -> Result<Vec<MailboxHash>> {
|
||||
if !p.as_ref().exists() || !p.as_ref().is_dir() {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"Configuration error: Path \"{}\" {}",
|
||||
p.as_ref().display(),
|
||||
if !p.as_ref().exists() {
|
||||
|
@ -1111,7 +1127,7 @@ impl MaildirType {
|
|||
None,
|
||||
Vec::new(),
|
||||
false,
|
||||
settings,
|
||||
&settings,
|
||||
) {
|
||||
f.children = recurse_mailboxes(mailboxes, settings, &path)?;
|
||||
for c in &f.children {
|
||||
|
@ -1122,9 +1138,8 @@ impl MaildirType {
|
|||
children.push(f.hash);
|
||||
mailboxes.insert(f.hash, f);
|
||||
} else {
|
||||
/* If directory is invalid (i.e. has no {cur,new,tmp}
|
||||
* subfolders), accept it ONLY if
|
||||
* it contains subdirs of any depth that are
|
||||
/* If directory is invalid (i.e. has no {cur,new,tmp} subfolders),
|
||||
* accept it ONLY if it contains subdirs of any depth that are
|
||||
* valid maildir paths
|
||||
*/
|
||||
let subdirs = recurse_mailboxes(mailboxes, settings, &path)?;
|
||||
|
@ -1135,7 +1150,7 @@ impl MaildirType {
|
|||
None,
|
||||
subdirs,
|
||||
true,
|
||||
settings,
|
||||
&settings,
|
||||
) {
|
||||
for c in &f.children {
|
||||
if let Some(f) = mailboxes.get_mut(c) {
|
||||
|
@ -1152,30 +1167,25 @@ impl MaildirType {
|
|||
}
|
||||
}
|
||||
Ok(children)
|
||||
}
|
||||
let root_mailbox = PathBuf::from(&settings.root_mailbox).expand();
|
||||
if !root_mailbox.exists() {
|
||||
return Err(Error::new(format!(
|
||||
"Configuration error ({}): root_mailbox `{}` is not a valid directory.",
|
||||
settings.name,
|
||||
};
|
||||
let root_path = PathBuf::from(settings.root_mailbox()).expand();
|
||||
if !root_path.exists() {
|
||||
return Err(MeliError::new(format!(
|
||||
"Configuration error ({}): root_path `{}` is not a valid directory.",
|
||||
settings.name(),
|
||||
settings.root_mailbox.as_str()
|
||||
)));
|
||||
} else if !root_mailbox.is_dir() {
|
||||
return Err(Error::new(format!(
|
||||
"Configuration error ({}): root_mailbox `{}` is not a directory.",
|
||||
settings.name,
|
||||
} else if !root_path.is_dir() {
|
||||
return Err(MeliError::new(format!(
|
||||
"Configuration error ({}): root_path `{}` is not a directory.",
|
||||
settings.name(),
|
||||
settings.root_mailbox.as_str()
|
||||
)));
|
||||
}
|
||||
|
||||
if let Ok(f) = MaildirMailbox::new(
|
||||
root_mailbox.to_str().unwrap().to_string(),
|
||||
root_mailbox
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
root_path.to_str().unwrap().to_string(),
|
||||
root_path.file_name().unwrap().to_str().unwrap().to_string(),
|
||||
None,
|
||||
Vec::with_capacity(0),
|
||||
false,
|
||||
|
@ -1185,7 +1195,7 @@ impl MaildirType {
|
|||
}
|
||||
|
||||
if mailboxes.is_empty() {
|
||||
let children = recurse_mailboxes(&mut mailboxes, settings, &root_mailbox)?;
|
||||
let children = recurse_mailboxes(&mut mailboxes, settings, &root_path)?;
|
||||
for c in &children {
|
||||
if let Some(f) = mailboxes.get_mut(c) {
|
||||
f.parent = None;
|
||||
|
@ -1193,7 +1203,7 @@ impl MaildirType {
|
|||
}
|
||||
} else {
|
||||
let root_hash = *mailboxes.keys().next().unwrap();
|
||||
let children = recurse_mailboxes(&mut mailboxes, settings, &root_mailbox)?;
|
||||
let children = recurse_mailboxes(&mut mailboxes, settings, &root_path)?;
|
||||
for c in &children {
|
||||
if let Some(f) = mailboxes.get_mut(c) {
|
||||
f.parent = Some(root_hash);
|
||||
|
@ -1216,18 +1226,17 @@ impl MaildirType {
|
|||
fh,
|
||||
HashIndex {
|
||||
index: HashMap::with_capacity_and_hasher(0, Default::default()),
|
||||
_hash: fh,
|
||||
hash: fh,
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(Box::new(Self {
|
||||
name: settings.name.to_string(),
|
||||
Ok(Box::new(MaildirType {
|
||||
name: settings.name().to_string(),
|
||||
mailboxes,
|
||||
hash_indexes: Arc::new(Mutex::new(hash_indexes)),
|
||||
mailbox_index: Default::default(),
|
||||
event_consumer,
|
||||
collection: Default::default(),
|
||||
path: root_mailbox,
|
||||
path: root_path,
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -1235,7 +1244,7 @@ impl MaildirType {
|
|||
for d in &["cur", "new", "tmp"] {
|
||||
path.push(d);
|
||||
if !path.is_dir() {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"{} is not a valid maildir mailbox",
|
||||
path.display()
|
||||
)));
|
||||
|
@ -1300,60 +1309,48 @@ impl MaildirType {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_config(s: &mut AccountSettings) -> Result<()> {
|
||||
let root_mailbox = PathBuf::from(&s.root_mailbox).expand();
|
||||
if !root_mailbox.exists() {
|
||||
return Err(Error::new(format!(
|
||||
"Configuration error ({}): root_mailbox `{}` is not a valid directory.",
|
||||
s.name,
|
||||
pub fn validate_config(s: &AccountSettings) -> Result<()> {
|
||||
let root_path = PathBuf::from(s.root_mailbox()).expand();
|
||||
if !root_path.exists() {
|
||||
return Err(MeliError::new(format!(
|
||||
"Configuration error ({}): root_path `{}` is not a valid directory.",
|
||||
s.name(),
|
||||
s.root_mailbox.as_str()
|
||||
)));
|
||||
} else if !root_mailbox.is_dir() {
|
||||
return Err(Error::new(format!(
|
||||
"Configuration error ({}): root_mailbox `{}` is not a directory.",
|
||||
s.name,
|
||||
} else if !root_path.is_dir() {
|
||||
return Err(MeliError::new(format!(
|
||||
"Configuration error ({}): root_path `{}` is not a directory.",
|
||||
s.name(),
|
||||
s.root_mailbox.as_str()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_mail_in_maildir_fs(mut path: PathBuf, read_only: bool) -> Result<Vec<PathBuf>> {
|
||||
let mut files: Vec<PathBuf> = vec![];
|
||||
path.push("new");
|
||||
for p in path.read_dir()?.flatten() {
|
||||
if !read_only {
|
||||
move_to_cur(p.path()).ok().take();
|
||||
} else {
|
||||
files.push(p.path());
|
||||
}
|
||||
}
|
||||
path.pop();
|
||||
path.push("cur");
|
||||
for e in path.read_dir()?.flatten() {
|
||||
files.push(e.path());
|
||||
}
|
||||
Ok(files)
|
||||
}
|
||||
}
|
||||
|
||||
fn add_path_to_index(
|
||||
hash_index: &HashIndexes,
|
||||
mailbox_hash: MailboxHash,
|
||||
path: &Path,
|
||||
cache_dir: &xdg::BaseDirectories,
|
||||
file_name: PathBuf,
|
||||
buf: &mut Vec<u8>,
|
||||
) -> Result<Envelope> {
|
||||
debug!("add_path_to_index path {:?} filename{:?}", path, file_name);
|
||||
let env_hash = get_file_hash(path);
|
||||
hash_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.or_default()
|
||||
.insert(env_hash, path.to_path_buf().into());
|
||||
let mut reader = io::BufReader::new(fs::File::open(path)?);
|
||||
{
|
||||
let mut map = hash_index.lock().unwrap();
|
||||
let map = map.entry(mailbox_hash).or_default();
|
||||
map.insert(env_hash, path.to_path_buf().into());
|
||||
debug!(
|
||||
"inserted {} in {} map, len={}",
|
||||
env_hash,
|
||||
mailbox_hash,
|
||||
map.len()
|
||||
);
|
||||
}
|
||||
let mut reader = io::BufReader::new(fs::File::open(&path)?);
|
||||
buf.clear();
|
||||
reader.read_to_end(buf)?;
|
||||
let mut env = Envelope::from_bytes(buf.as_slice(), Some(path.flags()))?;
|
||||
|
@ -1363,5 +1360,17 @@ fn add_path_to_index(
|
|||
env_hash,
|
||||
file_name.display()
|
||||
);
|
||||
if let Ok(cached) = cache_dir.place_cache_file(file_name) {
|
||||
debug!("putting in cache");
|
||||
/* place result in cache directory */
|
||||
let f = fs::File::create(cached)?;
|
||||
let metadata = f.metadata()?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
permissions.set_mode(0o600); // Read/write for owner only.
|
||||
f.set_permissions(permissions)?;
|
||||
let writer = io::BufWriter::new(f);
|
||||
bincode::Options::serialize_into(bincode::config::DefaultOptions::new(), writer, &env)?;
|
||||
}
|
||||
Ok(env)
|
||||
}
|
||||
|
|
|
@ -19,58 +19,68 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use core::{future::Future, pin::Pin};
|
||||
use std::{
|
||||
io::{self, Read},
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use futures::{
|
||||
stream::{FuturesUnordered, StreamExt},
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::backends::maildir::backend::move_to_cur;
|
||||
|
||||
type Payload = Pin<Box<dyn Future<Output = Result<Vec<Envelope>>> + Send + 'static>>;
|
||||
use core::future::Future;
|
||||
use core::pin::Pin;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use futures::task::{Context, Poll};
|
||||
use std::io::{self, Read};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
use std::result;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub struct MaildirStream {
|
||||
payloads: Pin<Box<FuturesUnordered<Payload>>>,
|
||||
payloads: Pin<
|
||||
Box<
|
||||
FuturesUnordered<Pin<Box<dyn Future<Output = Result<Vec<Envelope>>> + Send + 'static>>>,
|
||||
>,
|
||||
>,
|
||||
}
|
||||
|
||||
impl MaildirStream {
|
||||
#[allow(clippy::type_complexity, clippy::new_ret_no_self)]
|
||||
pub fn new(
|
||||
name: &str,
|
||||
mailbox_hash: MailboxHash,
|
||||
unseen: Arc<Mutex<usize>>,
|
||||
total: Arc<Mutex<usize>>,
|
||||
mut path: PathBuf,
|
||||
root_path: PathBuf,
|
||||
map: HashIndexes,
|
||||
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Result<Vec<Envelope>>> + Send + 'static>>> {
|
||||
let chunk_size = 2048;
|
||||
path.push("new");
|
||||
for p in path.read_dir()?.flatten() {
|
||||
move_to_cur(p.path()).ok().take();
|
||||
for d in path.read_dir()? {
|
||||
if let Ok(p) = d {
|
||||
move_to_cur(p.path()).ok().take();
|
||||
}
|
||||
}
|
||||
path.pop();
|
||||
path.push("cur");
|
||||
let files: Vec<PathBuf> = path
|
||||
.read_dir()?
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.collect::<Vec<_>>();
|
||||
let iter = path.read_dir()?;
|
||||
let count = path.read_dir()?.count();
|
||||
let mut files: Vec<PathBuf> = Vec::with_capacity(count);
|
||||
for e in iter {
|
||||
let e = e.and_then(|x| {
|
||||
let path = x.path();
|
||||
Ok(path)
|
||||
})?;
|
||||
files.push(e);
|
||||
}
|
||||
let payloads = Box::pin(if !files.is_empty() {
|
||||
files
|
||||
.chunks(chunk_size)
|
||||
.map(|chunk| {
|
||||
let cache_dir = xdg::BaseDirectories::with_profile("meli", &name).unwrap();
|
||||
Box::pin(Self::chunk(
|
||||
SmallVec::from(chunk),
|
||||
cache_dir,
|
||||
mailbox_hash,
|
||||
unseen.clone(),
|
||||
total.clone(),
|
||||
root_path.clone(),
|
||||
map.clone(),
|
||||
mailbox_index.clone(),
|
||||
)) as Pin<Box<dyn Future<Output = _> + Send + 'static>>
|
||||
|
@ -84,9 +94,11 @@ impl MaildirStream {
|
|||
|
||||
async fn chunk(
|
||||
chunk: SmallVec<[std::path::PathBuf; 2048]>,
|
||||
cache_dir: xdg::BaseDirectories,
|
||||
mailbox_hash: MailboxHash,
|
||||
unseen: Arc<Mutex<usize>>,
|
||||
total: Arc<Mutex<usize>>,
|
||||
root_path: PathBuf,
|
||||
map: HashIndexes,
|
||||
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
|
||||
) -> Result<Vec<Envelope>> {
|
||||
|
@ -94,13 +106,44 @@ impl MaildirStream {
|
|||
let mut unseen_total: usize = 0;
|
||||
let mut buf = Vec::with_capacity(4096);
|
||||
for file in chunk {
|
||||
/* Check if we have a cache file with this email's
|
||||
* filename */
|
||||
let file_name = PathBuf::from(&file)
|
||||
.strip_prefix(&root_path)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
if let Some(cached) = cache_dir.find_cache_file(&file_name) {
|
||||
/* Cached struct exists, try to load it */
|
||||
let cached_file = fs::File::open(&cached)?;
|
||||
let filesize = cached_file.metadata()?.len();
|
||||
let reader = io::BufReader::new(cached_file);
|
||||
let result: result::Result<Envelope, _> = bincode::Options::deserialize_from(
|
||||
bincode::Options::with_limit(
|
||||
bincode::config::DefaultOptions::new(),
|
||||
2 * filesize,
|
||||
),
|
||||
reader,
|
||||
);
|
||||
if let Ok(env) = result {
|
||||
let mut map = map.lock().unwrap();
|
||||
let map = map.entry(mailbox_hash).or_default();
|
||||
let hash = env.hash();
|
||||
map.insert(hash, file.clone().into());
|
||||
mailbox_index.lock().unwrap().insert(hash, mailbox_hash);
|
||||
if !env.is_seen() {
|
||||
unseen_total += 1;
|
||||
}
|
||||
local_r.push(env);
|
||||
continue;
|
||||
}
|
||||
/* Try delete invalid file */
|
||||
let _ = fs::remove_file(&cached);
|
||||
};
|
||||
let env_hash = get_file_hash(&file);
|
||||
{
|
||||
map.lock()
|
||||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.or_default()
|
||||
.insert(env_hash, PathBuf::from(&file).into());
|
||||
let mut map = map.lock().unwrap();
|
||||
let map = map.entry(mailbox_hash).or_default();
|
||||
map.insert(env_hash, PathBuf::from(&file).into());
|
||||
}
|
||||
let mut reader = io::BufReader::new(fs::File::open(&file)?);
|
||||
buf.clear();
|
||||
|
@ -109,6 +152,22 @@ impl MaildirStream {
|
|||
Ok(mut env) => {
|
||||
env.set_hash(env_hash);
|
||||
mailbox_index.lock().unwrap().insert(env_hash, mailbox_hash);
|
||||
if let Ok(cached) = cache_dir.place_cache_file(file_name) {
|
||||
/* place result in cache directory */
|
||||
let f = fs::File::create(cached)?;
|
||||
let metadata = f.metadata()?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
permissions.set_mode(0o600); // Read/write for owner only.
|
||||
f.set_permissions(permissions)?;
|
||||
|
||||
let writer = io::BufWriter::new(f);
|
||||
bincode::Options::serialize_into(
|
||||
bincode::config::DefaultOptions::new(),
|
||||
writer,
|
||||
&env,
|
||||
)?;
|
||||
}
|
||||
if !env.is_seen() {
|
||||
unseen_total += 1;
|
||||
}
|
||||
|
|
|
@ -19,153 +19,36 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! # Mbox formats
|
||||
//!
|
||||
//! ## 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>
|
||||
//!
|
||||
//! ## `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]
|
||||
//!
|
||||
//! "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
|
||||
//!
|
||||
//! "An mbox is a text file containing an arbitrary number of e-mail messages.
|
||||
//! Each message consists of a postmark, followed by an e-mail message formatted
|
||||
//! according to RFC822, RFC2822. The file format is line-oriented. Lines are
|
||||
//! separated by line feed characters (ASCII 10).
|
||||
//!
|
||||
//! "A postmark line consists of the four characters 'From', followed by a space
|
||||
//! character, followed by the message's envelope sender address, followed by
|
||||
//! whitespace, and followed by a time stamp. This line is often called From_
|
||||
//! line.
|
||||
//!
|
||||
//! "The sender address is expected to be addr-spec as defined in RFC2822 3.4.1.
|
||||
//! The date is expected to be date-time as output by asctime(3). For
|
||||
//! compatibility reasons with legacy software, two-digit years greater than or
|
||||
//! equal to 70 should be interpreted as the years 1970+, while two-digit years
|
||||
//! less than 70 should be interpreted as the years 2000-2069. Software reading
|
||||
//! files in this format should also be prepared to accept non-numeric timezone
|
||||
//! information such as 'CET DST' for Central European Time, daylight saving
|
||||
//! time.
|
||||
//!
|
||||
//! "Example:
|
||||
//!
|
||||
//!```text
|
||||
//! From example@example.com Fri Jun 23 02:56:55 2000
|
||||
//! ```
|
||||
//!
|
||||
//! "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]
|
||||
//!
|
||||
//! ## Metadata
|
||||
//!
|
||||
//! `melib` recognizes the CClient (a [Pine client API](https://web.archive.org/web/20050203003235/http://www.washington.edu/imap/)) convention for metadata in `mbox` format:
|
||||
//!
|
||||
//! - `Status`: R (Seen) and O (non-Recent) flags
|
||||
//! - `X-Status`: A (Answered), F (Flagged), T (Draft) and D (Deleted) flags
|
||||
//! - `X-Keywords`: Message’s keywords
|
||||
//!
|
||||
//! ## Parsing an mbox file
|
||||
//!
|
||||
//! ```
|
||||
//! # use melib::{Result, Envelope, EnvelopeHash, mbox::*};
|
||||
//! # use std::collections::HashMap;
|
||||
//! # use std::sync::{Arc, Mutex};
|
||||
//! let file_contents = vec![]; // Replace with actual mbox file contents
|
||||
//! let index: Arc<Mutex<HashMap<EnvelopeHash, (Offset, Length)>>> =
|
||||
//! Arc::new(Mutex::new(HashMap::default()));
|
||||
//! let mut message_iter = MessageIterator {
|
||||
//! index: index.clone(),
|
||||
//! input: &file_contents.as_slice(),
|
||||
//! offset: 0,
|
||||
//! file_offset: 0,
|
||||
//! format: Some(MboxFormat::MboxCl2),
|
||||
//! };
|
||||
//! let envelopes: Result<Vec<Envelope>> = message_iter.collect();
|
||||
//! ```
|
||||
//!
|
||||
//! ## Writing / Appending an mbox file
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use melib::mbox::*;
|
||||
//! # use std::io::Write;
|
||||
//! let mbox_1: &[u8] = br#"From: <a@b.c>\n\nHello World"#;
|
||||
//! let mbox_2: &[u8] = br#"From: <d@e.f>\n\nHello World #2"#;
|
||||
//! let mut file = std::io::BufWriter::new(std::fs::File::create(&"out.mbox")?);
|
||||
//! let format = MboxFormat::MboxCl2;
|
||||
//! format.append(
|
||||
//! &mut file,
|
||||
//! mbox_1,
|
||||
//! None, // Envelope From
|
||||
//! Some(melib::datetime::now()), // Delivered date
|
||||
//! Default::default(), // Flags and tags
|
||||
//! MboxMetadata::None,
|
||||
//! true,
|
||||
//! false,
|
||||
//! )?;
|
||||
//! format.append(
|
||||
//! &mut file,
|
||||
//! mbox_2,
|
||||
//! None,
|
||||
//! Some(melib::datetime::now()),
|
||||
//! Default::default(), // Flags and tags
|
||||
//! MboxMetadata::None,
|
||||
//! false,
|
||||
//! false,
|
||||
//! )?;
|
||||
//! file.flush()?;
|
||||
//! # Ok::<(), melib::Error>(())
|
||||
//! ```
|
||||
/*!
|
||||
* https://wiki2.dovecot.org/MailboxFormat/mbox
|
||||
*/
|
||||
|
||||
use nom::{
|
||||
self,
|
||||
bytes::complete::tag,
|
||||
character::complete::digit1,
|
||||
combinator::map_res,
|
||||
error::{Error as NomError, ErrorKind as NomErrorKind},
|
||||
IResult,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backends::*,
|
||||
collection::Collection,
|
||||
conf::AccountSettings,
|
||||
email::{parser::BytesExt, *},
|
||||
error::{Error, ErrorKind, Result},
|
||||
get_path_hash,
|
||||
utils::shellexpand::ShellExpandTrait,
|
||||
};
|
||||
use crate::backends::*;
|
||||
use crate::conf::AccountSettings;
|
||||
use crate::email::parser::BytesExt;
|
||||
use crate::email::*;
|
||||
use crate::error::{MeliError, Result};
|
||||
use crate::get_path_hash;
|
||||
use crate::shellexpand::ShellExpandTrait;
|
||||
use nom::bytes::complete::tag;
|
||||
use nom::character::complete::digit1;
|
||||
use nom::combinator::map_res;
|
||||
use nom::{self, error::ErrorKind, IResult};
|
||||
|
||||
extern crate notify;
|
||||
use std::{
|
||||
collections::hash_map::HashMap,
|
||||
fs::File,
|
||||
io::{BufReader, Read},
|
||||
os::unix::io::AsRawFd,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::{mpsc::channel, Arc, Mutex, RwLock},
|
||||
};
|
||||
|
||||
use self::notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
|
||||
use std::collections::hash_map::{DefaultHasher, HashMap};
|
||||
use std::fs::File;
|
||||
use std::hash::Hasher;
|
||||
use std::io::{BufReader, Read};
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
pub mod write;
|
||||
|
||||
pub type Offset = usize;
|
||||
pub type Length = usize;
|
||||
type Offset = usize;
|
||||
type Length = usize;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
const F_OFD_SETLKW: libc::c_int = 38;
|
||||
|
@ -180,8 +63,7 @@ fn get_rw_lock_blocking(f: &File, path: &Path) -> Result<()> {
|
|||
l_start: 0,
|
||||
l_len: 0, /* "Specifying 0 for l_len has the special meaning: lock all bytes starting at the location
|
||||
specified by l_whence and l_start through to the end of file, no matter how large the file grows." */
|
||||
l_pid: 0, /* "By contrast with traditional record locks, the l_pid field of that
|
||||
* structure must be set to zero when using the commands described below." */
|
||||
l_pid: 0, /* "By contrast with traditional record locks, the l_pid field of that structure must be set to zero when using the commands described below." */
|
||||
#[cfg(target_os = "freebsd")]
|
||||
l_sysid: 0,
|
||||
};
|
||||
|
@ -192,7 +74,7 @@ fn get_rw_lock_blocking(f: &File, path: &Path) -> Result<()> {
|
|||
let ret_val = unsafe { libc::fcntl(fd, F_OFD_SETLKW, ptr as *mut libc::c_void) };
|
||||
if ret_val == -1 {
|
||||
let err = nix::errno::Errno::from_i32(nix::errno::errno());
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"Could not lock {}: fcntl() returned {}",
|
||||
path.display(),
|
||||
err.desc()
|
||||
|
@ -232,8 +114,12 @@ impl BackendMailbox for MboxMailbox {
|
|||
self.path.to_str().unwrap()
|
||||
}
|
||||
|
||||
fn change_name(&mut self, s: &str) {
|
||||
self.name = s.to_string();
|
||||
}
|
||||
|
||||
fn clone(&self) -> Mailbox {
|
||||
Box::new(Self {
|
||||
Box::new(MboxMailbox {
|
||||
hash: self.hash,
|
||||
name: self.name.clone(),
|
||||
path: self.path.clone(),
|
||||
|
@ -285,7 +171,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,
|
||||
|
@ -293,9 +179,9 @@ pub struct MboxOp {
|
|||
}
|
||||
|
||||
impl MboxOp {
|
||||
pub fn new(_hash: EnvelopeHash, path: &Path, offset: Offset, length: Length) -> Self {
|
||||
Self {
|
||||
_hash,
|
||||
pub fn new(hash: EnvelopeHash, path: &Path, offset: Offset, length: Length) -> Self {
|
||||
MboxOp {
|
||||
hash,
|
||||
path: path.to_path_buf(),
|
||||
slice: std::cell::RefCell::new(None),
|
||||
offset,
|
||||
|
@ -317,10 +203,10 @@ impl BackendOp for MboxOp {
|
|||
buf_reader.read_to_end(&mut contents)?;
|
||||
*self.slice.get_mut() = Some(contents);
|
||||
}
|
||||
let ret = self.slice.get_mut().as_ref().unwrap().as_slice()
|
||||
let ret = Ok(self.slice.get_mut().as_ref().unwrap().as_slice()
|
||||
[self.offset..self.offset + self.length]
|
||||
.to_vec();
|
||||
Ok(Box::pin(async move { Ok(ret) }))
|
||||
.to_vec());
|
||||
Ok(Box::pin(async move { ret }))
|
||||
}
|
||||
|
||||
fn fetch_flags(&self) -> ResultFuture<Flag> {
|
||||
|
@ -385,33 +271,14 @@ impl BackendOp for MboxOp {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum MboxMetadata {
|
||||
/// Dovecot uses C-Client (ie. UW-IMAP, Pine) compatible headers in mbox
|
||||
/// messages to store me
|
||||
/// - X-IMAPbase: Contains UIDVALIDITY, last used UID and list of used
|
||||
/// keywords
|
||||
/// - X-IMAP: Same as X-IMAPbase but also specifies that the message is a
|
||||
/// “pseudo message”
|
||||
/// - X-UID: Message’s allocated UID
|
||||
/// - Status: R (Seen) and O (non-Recent) flags
|
||||
/// - X-Status: A (Answered), F (Flagged), T (Draft) and D (Deleted) flags
|
||||
/// - X-Keywords: Message’s keywords
|
||||
/// - Content-Length: Length of the message body in bytes
|
||||
CClient,
|
||||
None,
|
||||
}
|
||||
|
||||
/// Choose between "mboxo", "mboxrd", "mboxcl", "mboxcl2". For new mailboxes,
|
||||
/// prefer "mboxcl2" which does not alter the mail body.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum MboxFormat {
|
||||
pub enum MboxReader {
|
||||
MboxO,
|
||||
MboxRd,
|
||||
MboxCl,
|
||||
MboxCl2,
|
||||
}
|
||||
|
||||
impl Default for MboxFormat {
|
||||
impl Default for MboxReader {
|
||||
fn default() -> Self {
|
||||
Self::MboxCl2
|
||||
}
|
||||
|
@ -454,8 +321,8 @@ macro_rules! find_From__line {
|
|||
}};
|
||||
}
|
||||
|
||||
impl MboxFormat {
|
||||
pub fn parse<'i>(&self, input: &'i [u8]) -> IResult<&'i [u8], Envelope> {
|
||||
impl MboxReader {
|
||||
fn parse<'i>(&self, input: &'i [u8]) -> IResult<&'i [u8], Envelope> {
|
||||
let orig_input = input;
|
||||
let mut input = input;
|
||||
match self {
|
||||
|
@ -508,10 +375,7 @@ impl MboxFormat {
|
|||
}
|
||||
Err(err) => {
|
||||
debug!("Could not parse mail {:?}", err);
|
||||
Err(nom::Err::Error(NomError {
|
||||
input,
|
||||
code: NomErrorKind::Tag,
|
||||
}))
|
||||
Err(nom::Err::Error((input, ErrorKind::Tag)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -555,10 +419,7 @@ impl MboxFormat {
|
|||
}
|
||||
Err(err) => {
|
||||
debug!("Could not parse mail at {:?}", err);
|
||||
Err(nom::Err::Error(NomError {
|
||||
input,
|
||||
code: NomErrorKind::Tag,
|
||||
}))
|
||||
Err(nom::Err::Error((input, ErrorKind::Tag)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -612,10 +473,7 @@ impl MboxFormat {
|
|||
}
|
||||
Err(err) => {
|
||||
debug!("Could not parse mail {:?}", err);
|
||||
Err(nom::Err::Error(NomError {
|
||||
input,
|
||||
code: NomErrorKind::Tag,
|
||||
}))
|
||||
Err(nom::Err::Error((input, ErrorKind::Tag)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -659,10 +517,7 @@ impl MboxFormat {
|
|||
}
|
||||
Err(err) => {
|
||||
debug!("Could not parse mail {:?}", err);
|
||||
Err(nom::Err::Error(NomError {
|
||||
input,
|
||||
code: NomErrorKind::Tag,
|
||||
}))
|
||||
Err(nom::Err::Error((input, ErrorKind::Tag)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -750,21 +605,18 @@ pub fn mbox_parse(
|
|||
index: Arc<Mutex<HashMap<EnvelopeHash, (Offset, Length)>>>,
|
||||
input: &[u8],
|
||||
file_offset: usize,
|
||||
format: Option<MboxFormat>,
|
||||
reader: Option<MboxReader>,
|
||||
) -> IResult<&[u8], Vec<Envelope>> {
|
||||
if input.is_empty() {
|
||||
return Err(nom::Err::Error(NomError {
|
||||
input,
|
||||
code: NomErrorKind::Tag,
|
||||
}));
|
||||
return Err(nom::Err::Error((input, ErrorKind::Tag)));
|
||||
}
|
||||
let mut offset = 0;
|
||||
let mut index = index.lock().unwrap();
|
||||
let mut envelopes = Vec::with_capacity(32);
|
||||
|
||||
let format = format.unwrap_or(MboxFormat::MboxCl2);
|
||||
let reader = reader.unwrap_or(MboxReader::MboxCl2);
|
||||
while !input[offset + file_offset..].is_empty() {
|
||||
let (next_input, env) = match format.parse(&input[offset + file_offset..]) {
|
||||
let (next_input, env) = match reader.parse(&input[offset + file_offset..]) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
// Try to recover from this error by finding a new candidate From_ line
|
||||
|
@ -784,18 +636,8 @@ pub fn mbox_parse(
|
|||
}
|
||||
};
|
||||
let start: Offset = input[offset + file_offset..]
|
||||
.find(b"From ")
|
||||
.map(|from_offset| {
|
||||
input[offset + file_offset + from_offset..]
|
||||
.find(b"\n")
|
||||
.map(|v| v + 1)
|
||||
.unwrap_or_else(|| {
|
||||
input[offset + file_offset + from_offset..]
|
||||
.len()
|
||||
.saturating_sub(2)
|
||||
})
|
||||
})
|
||||
.map(|v| v + 2)
|
||||
.find(b"\n")
|
||||
.map(|v| v + 1)
|
||||
.unwrap_or(0);
|
||||
let len = input.len() - next_input.len() - offset - file_offset - start;
|
||||
index.insert(env.hash(), (offset + file_offset + start, len));
|
||||
|
@ -806,12 +648,12 @@ pub fn mbox_parse(
|
|||
Ok((&[], envelopes))
|
||||
}
|
||||
|
||||
pub struct MessageIterator<'a> {
|
||||
pub index: Arc<Mutex<HashMap<EnvelopeHash, (Offset, Length)>>>,
|
||||
pub input: &'a [u8],
|
||||
pub file_offset: usize,
|
||||
pub offset: usize,
|
||||
pub format: Option<MboxFormat>,
|
||||
struct MessageIterator<'a> {
|
||||
index: Arc<Mutex<HashMap<EnvelopeHash, (Offset, Length)>>>,
|
||||
input: &'a [u8],
|
||||
file_offset: usize,
|
||||
offset: usize,
|
||||
reader: Option<MboxReader>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for MessageIterator<'a> {
|
||||
|
@ -822,10 +664,10 @@ impl<'a> Iterator for MessageIterator<'a> {
|
|||
}
|
||||
let mut index = self.index.lock().unwrap();
|
||||
|
||||
let format = self.format.unwrap_or(MboxFormat::MboxCl2);
|
||||
let reader = self.reader.unwrap_or(MboxReader::MboxCl2);
|
||||
while !self.input[self.offset + self.file_offset..].is_empty() {
|
||||
let (next_input, env) =
|
||||
match format.parse(&self.input[self.offset + self.file_offset..]) {
|
||||
match reader.parse(&self.input[self.offset + self.file_offset..]) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
// Try to recover from this error by finding a new candidate From_ line
|
||||
|
@ -848,17 +690,7 @@ impl<'a> Iterator for MessageIterator<'a> {
|
|||
}
|
||||
};
|
||||
let start: Offset = self.input[self.offset + self.file_offset..]
|
||||
.find(b"From ")
|
||||
.map(|from_offset| {
|
||||
self.input[self.offset + self.file_offset + from_offset..]
|
||||
.find(b"\n")
|
||||
.map(|v| v + 1)
|
||||
.unwrap_or_else(|| {
|
||||
self.input[self.offset + self.file_offset + from_offset..]
|
||||
.len()
|
||||
.saturating_sub(2)
|
||||
})
|
||||
})
|
||||
.find(b"\n")
|
||||
.map(|v| v + 1)
|
||||
.unwrap_or(0);
|
||||
let len = self.input.len() - next_input.len() - self.offset - self.file_offset - start;
|
||||
|
@ -876,10 +708,9 @@ impl<'a> Iterator for MessageIterator<'a> {
|
|||
pub struct MboxType {
|
||||
account_name: String,
|
||||
path: PathBuf,
|
||||
collection: Collection,
|
||||
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
|
||||
mailboxes: Arc<Mutex<HashMap<MailboxHash, MboxMailbox>>>,
|
||||
prefer_mbox_type: Option<MboxFormat>,
|
||||
prefer_mbox_type: Option<MboxReader>,
|
||||
event_consumer: BackendEventConsumer,
|
||||
}
|
||||
|
||||
|
@ -908,7 +739,7 @@ impl MailBackend for MboxType {
|
|||
mailbox_hash: MailboxHash,
|
||||
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
|
||||
mailboxes: Arc<Mutex<HashMap<MailboxHash, MboxMailbox>>>,
|
||||
prefer_mbox_type: Option<MboxFormat>,
|
||||
prefer_mbox_type: Option<MboxReader>,
|
||||
offset: usize,
|
||||
file_offset: usize,
|
||||
contents: Vec<u8>,
|
||||
|
@ -920,10 +751,10 @@ 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,
|
||||
reader: self.prefer_mbox_type,
|
||||
};
|
||||
let mut payload = vec![];
|
||||
let mut done = false;
|
||||
|
@ -953,10 +784,9 @@ impl MailBackend for MboxType {
|
|||
if payload.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
let contents = std::mem::take(&mut self.contents);
|
||||
self.mailboxes
|
||||
.lock()
|
||||
.unwrap()
|
||||
let mut mailbox_lock = self.mailboxes.lock().unwrap();
|
||||
let contents = std::mem::replace(&mut self.contents, vec![]);
|
||||
mailbox_lock
|
||||
.entry(self.mailbox_hash)
|
||||
.and_modify(|f| f.content = contents);
|
||||
Ok(Some(payload))
|
||||
|
@ -1000,9 +830,7 @@ impl MailBackend for MboxType {
|
|||
}
|
||||
|
||||
fn refresh(&mut self, _mailbox_hash: MailboxHash) -> ResultFuture<()> {
|
||||
Err(Error::new(
|
||||
"Refreshing is currently unimplemented for mbox backend.",
|
||||
))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn watch(&self) -> ResultFuture<()> {
|
||||
|
@ -1010,18 +838,19 @@ impl MailBackend for MboxType {
|
|||
let (tx, rx) = channel();
|
||||
let mut watcher = watcher(tx, std::time::Duration::from_secs(10))
|
||||
.map_err(|e| e.to_string())
|
||||
.map_err(Error::new)?;
|
||||
{
|
||||
let mailboxes_lck = self.mailboxes.lock().unwrap();
|
||||
for f in mailboxes_lck.values() {
|
||||
watcher
|
||||
.watch(&f.fs_path, RecursiveMode::Recursive)
|
||||
.map_err(|e| e.to_string())
|
||||
.map_err(Error::new)?;
|
||||
log::debug!("watching {:?}", f.fs_path.as_path());
|
||||
}
|
||||
.map_err(MeliError::new)?;
|
||||
for f in self.mailboxes.lock().unwrap().values() {
|
||||
watcher
|
||||
.watch(&f.fs_path, RecursiveMode::Recursive)
|
||||
.map_err(|e| e.to_string())
|
||||
.map_err(MeliError::new)?;
|
||||
debug!("watching {:?}", f.fs_path.as_path());
|
||||
}
|
||||
let account_hash = AccountHash::from_bytes(self.account_name.as_bytes());
|
||||
let account_hash = {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
hasher.write(self.account_name.as_bytes());
|
||||
hasher.finish()
|
||||
};
|
||||
let mailboxes = self.mailboxes.clone();
|
||||
let mailbox_index = self.mailbox_index.clone();
|
||||
let prefer_mbox_type = self.prefer_mbox_type;
|
||||
|
@ -1041,7 +870,7 @@ impl MailBackend for MboxType {
|
|||
Ok(event) => match event {
|
||||
/* Update */
|
||||
DebouncedEvent::NoticeWrite(pathbuf) | DebouncedEvent::Write(pathbuf) => {
|
||||
let mailbox_hash = MailboxHash(get_path_hash!(&pathbuf));
|
||||
let mailbox_hash = get_path_hash!(&pathbuf);
|
||||
let file = match std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
|
@ -1103,13 +932,13 @@ impl MailBackend for MboxType {
|
|||
.values()
|
||||
.any(|f| f.fs_path == pathbuf)
|
||||
{
|
||||
let mailbox_hash = MailboxHash(get_path_hash!(&pathbuf));
|
||||
let mailbox_hash = get_path_hash!(&pathbuf);
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Failure(Error::new(format!(
|
||||
kind: RefreshEventKind::Failure(MeliError::new(format!(
|
||||
"mbox mailbox {} was removed.",
|
||||
pathbuf.display()
|
||||
))),
|
||||
|
@ -1120,13 +949,13 @@ impl MailBackend for MboxType {
|
|||
}
|
||||
DebouncedEvent::Rename(src, dest) => {
|
||||
if mailboxes.lock().unwrap().values().any(|f| f.fs_path == src) {
|
||||
let mailbox_hash = MailboxHash(get_path_hash!(&src));
|
||||
let mailbox_hash = get_path_hash!(&src);
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Failure(Error::new(format!(
|
||||
kind: RefreshEventKind::Failure(MeliError::new(format!(
|
||||
"mbox mailbox {} was renamed to {}.",
|
||||
src.display(),
|
||||
dest.display()
|
||||
|
@ -1138,8 +967,7 @@ impl MailBackend for MboxType {
|
|||
}
|
||||
/* Trigger rescan of mailboxes */
|
||||
DebouncedEvent::Rescan => {
|
||||
let mailboxes_lck = mailboxes.lock().unwrap();
|
||||
for &mailbox_hash in mailboxes_lck.keys() {
|
||||
for &mailbox_hash in mailboxes.lock().unwrap().keys() {
|
||||
(sender)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
|
@ -1153,12 +981,7 @@ impl MailBackend for MboxType {
|
|||
}
|
||||
_ => {}
|
||||
},
|
||||
Err(e) => {
|
||||
log::debug!("watch error: {:?}", e);
|
||||
return Err(Error::new(format!(
|
||||
"Mbox watching thread exited with error: {e}"
|
||||
)));
|
||||
}
|
||||
Err(e) => debug!("watch error: {:?}", e),
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
@ -1198,9 +1021,7 @@ impl MailBackend for MboxType {
|
|||
_destination_mailbox_hash: MailboxHash,
|
||||
_move_: bool,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new(
|
||||
"Copying messages is currently unimplemented for mbox backend",
|
||||
))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn set_flags(
|
||||
|
@ -1209,9 +1030,7 @@ impl MailBackend for MboxType {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_flags: SmallVec<[(std::result::Result<Flag, String>, bool); 8]>,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new(
|
||||
"Setting flags is currently unimplemented for mbox backend",
|
||||
))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn delete_messages(
|
||||
|
@ -1219,9 +1038,7 @@ impl MailBackend for MboxType {
|
|||
_env_hashes: EnvelopeHashBatch,
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new(
|
||||
"Deleting messages is currently unimplemented for mbox backend",
|
||||
))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn save(
|
||||
|
@ -1230,9 +1047,7 @@ impl MailBackend for MboxType {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_flags: Option<Flag>,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new(
|
||||
"Saving messages is currently unimplemented for mbox backend",
|
||||
))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
|
@ -1242,74 +1057,12 @@ impl MailBackend for MboxType {
|
|||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn collection(&self) -> Collection {
|
||||
self.collection.clone()
|
||||
}
|
||||
|
||||
fn delete_mailbox(
|
||||
&mut self,
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
|
||||
Err(Error::new(
|
||||
"Deleting mailboxes is currently unimplemented for mbox backend.",
|
||||
))
|
||||
}
|
||||
|
||||
fn set_mailbox_subscription(
|
||||
&mut self,
|
||||
_mailbox_hash: MailboxHash,
|
||||
_val: bool,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new(
|
||||
"Mailbox subscriptions are not possible for the mbox backend.",
|
||||
))
|
||||
}
|
||||
|
||||
fn rename_mailbox(
|
||||
&mut self,
|
||||
_mailbox_hash: MailboxHash,
|
||||
_new_path: String,
|
||||
) -> ResultFuture<Mailbox> {
|
||||
Err(Error::new(
|
||||
"Renaming mailboxes is currently unimplemented for mbox backend.",
|
||||
))
|
||||
}
|
||||
|
||||
fn set_mailbox_permissions(
|
||||
&mut self,
|
||||
_mailbox_hash: MailboxHash,
|
||||
_val: crate::backends::MailboxPermissions,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new(
|
||||
"Setting mailbox permissions is not possible for the mbox backend.",
|
||||
))
|
||||
}
|
||||
|
||||
fn search(
|
||||
&self,
|
||||
_query: crate::search::Query,
|
||||
_mailbox_hash: Option<MailboxHash>,
|
||||
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>> {
|
||||
Err(Error::new("Search is unimplemented for the mbox backend.")
|
||||
.set_kind(ErrorKind::NotImplemented))
|
||||
}
|
||||
|
||||
fn create_mailbox(
|
||||
&mut self,
|
||||
_new_path: String,
|
||||
) -> ResultFuture<(MailboxHash, HashMap<MailboxHash, Mailbox>)> {
|
||||
Err(
|
||||
Error::new("Creating mailboxes is unimplemented for the mbox backend.")
|
||||
.set_kind(ErrorKind::NotImplemented),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! get_conf_val {
|
||||
($s:ident[$var:literal]) => {
|
||||
$s.extra.get($var).ok_or_else(|| {
|
||||
Error::new(format!(
|
||||
MeliError::new(format!(
|
||||
"Configuration error ({}): mbox backend requires the field `{}` set",
|
||||
$s.name.as_str(),
|
||||
$var
|
||||
|
@ -1321,7 +1074,7 @@ macro_rules! get_conf_val {
|
|||
.get($var)
|
||||
.map(|v| {
|
||||
<_>::from_str(v).map_err(|e| {
|
||||
Error::new(format!(
|
||||
MeliError::new(format!(
|
||||
"Configuration error ({}): Invalid value for field `{}`: {}\n{}",
|
||||
$s.name.as_str(),
|
||||
$var,
|
||||
|
@ -1335,7 +1088,6 @@ macro_rules! get_conf_val {
|
|||
}
|
||||
|
||||
impl MboxType {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(
|
||||
s: &AccountSettings,
|
||||
_is_subscribed: Box<dyn Fn(&str) -> bool>,
|
||||
|
@ -1343,31 +1095,31 @@ impl MboxType {
|
|||
) -> Result<Box<dyn MailBackend>> {
|
||||
let path = Path::new(s.root_mailbox.as_str()).expand();
|
||||
if !path.exists() {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"\"root_mailbox\" {} for account {} is not a valid path.",
|
||||
s.root_mailbox.as_str(),
|
||||
s.name
|
||||
s.name()
|
||||
)));
|
||||
}
|
||||
let prefer_mbox_type: String = get_conf_val!(s["prefer_mbox_type"], "auto".to_string())?;
|
||||
let ret = Self {
|
||||
account_name: s.name.to_string(),
|
||||
let ret = MboxType {
|
||||
account_name: s.name().to_string(),
|
||||
event_consumer,
|
||||
path,
|
||||
prefer_mbox_type: match prefer_mbox_type.as_str() {
|
||||
"auto" => None,
|
||||
"mboxo" => Some(MboxFormat::MboxO),
|
||||
"mboxrd" => Some(MboxFormat::MboxRd),
|
||||
"mboxcl" => Some(MboxFormat::MboxCl),
|
||||
"mboxcl2" => Some(MboxFormat::MboxCl2),
|
||||
"mboxo" => Some(MboxReader::MboxO),
|
||||
"mboxrd" => Some(MboxReader::MboxRd),
|
||||
"mboxcl" => Some(MboxReader::MboxCl),
|
||||
"mboxcl2" => Some(MboxReader::MboxCl2),
|
||||
_ => {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"{} invalid `prefer_mbox_type` value: `{}`",
|
||||
s.name, prefer_mbox_type,
|
||||
s.name(),
|
||||
prefer_mbox_type,
|
||||
)))
|
||||
}
|
||||
},
|
||||
collection: Collection::default(),
|
||||
mailbox_index: Default::default(),
|
||||
mailboxes: Default::default(),
|
||||
};
|
||||
|
@ -1376,7 +1128,7 @@ impl MboxType {
|
|||
.file_name()
|
||||
.map(|f| f.to_string_lossy().into())
|
||||
.unwrap_or_default();
|
||||
let hash = MailboxHash(get_path_hash!(&ret.path));
|
||||
let hash = get_path_hash!(&ret.path);
|
||||
|
||||
let read_only = if let Ok(metadata) = std::fs::metadata(&ret.path) {
|
||||
metadata.permissions().readonly()
|
||||
|
@ -1414,10 +1166,10 @@ impl MboxType {
|
|||
/* Look for other mailboxes */
|
||||
for (k, f) in s.mailboxes.iter() {
|
||||
if let Some(path_str) = f.extra.get("path") {
|
||||
let hash = MailboxHash(get_path_hash!(path_str));
|
||||
let hash = get_path_hash!(path_str);
|
||||
let pathbuf: PathBuf = path_str.into();
|
||||
if !pathbuf.exists() || pathbuf.is_dir() {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"mbox mailbox configuration entry \"{}\" path value {} is not a file.",
|
||||
k, path_str
|
||||
)));
|
||||
|
@ -1456,9 +1208,8 @@ impl MboxType {
|
|||
},
|
||||
);
|
||||
} else {
|
||||
return Err(Error::new(format!(
|
||||
"mbox mailbox configuration entry \"{}\" should have a \"path\" value set \
|
||||
pointing to an mbox file.",
|
||||
return Err(MeliError::new(format!(
|
||||
"mbox mailbox configuration entry \"{}\" should have a \"path\" value set pointing to an mbox file.",
|
||||
k
|
||||
)));
|
||||
}
|
||||
|
@ -1466,40 +1217,13 @@ impl MboxType {
|
|||
Ok(Box::new(ret))
|
||||
}
|
||||
|
||||
pub fn validate_config(s: &mut AccountSettings) -> Result<()> {
|
||||
macro_rules! get_conf_val {
|
||||
($s:ident[$var:literal]) => {
|
||||
$s.extra.remove($var).ok_or_else(|| {
|
||||
Error::new(format!(
|
||||
"Configuration error ({}): mbox backend requires the field `{}` set",
|
||||
$s.name.as_str(),
|
||||
$var
|
||||
))
|
||||
})
|
||||
};
|
||||
($s:ident[$var:literal], $default:expr) => {
|
||||
$s.extra
|
||||
.remove($var)
|
||||
.map(|v| {
|
||||
<_>::from_str(&v).map_err(|e| {
|
||||
Error::new(format!(
|
||||
"Configuration error ({}): Invalid value for field `{}`: {}\n{}",
|
||||
$s.name.as_str(),
|
||||
$var,
|
||||
v,
|
||||
e
|
||||
))
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| Ok($default))
|
||||
};
|
||||
}
|
||||
pub fn validate_config(s: &AccountSettings) -> Result<()> {
|
||||
let path = Path::new(s.root_mailbox.as_str()).expand();
|
||||
if !path.exists() {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"\"root_mailbox\" {} for account {} is not a valid path.",
|
||||
s.root_mailbox.as_str(),
|
||||
s.name
|
||||
s.name()
|
||||
)));
|
||||
}
|
||||
let prefer_mbox_type: Result<String> =
|
||||
|
|
|
@ -1,262 +0,0 @@
|
|||
/*
|
||||
* meli - mailbox module.
|
||||
*
|
||||
* Copyright 2021 Manos Pitsidianakis
|
||||
*
|
||||
* This file is part of meli.
|
||||
*
|
||||
* meli is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* meli is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use super::*;
|
||||
use crate::utils::datetime;
|
||||
|
||||
impl MboxFormat {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn append(
|
||||
&self,
|
||||
writer: &mut dyn std::io::Write,
|
||||
input: &[u8],
|
||||
envelope_from: Option<&Address>,
|
||||
delivery_date: Option<crate::UnixTimestamp>,
|
||||
(flags, tags): (Flag, Vec<&str>),
|
||||
metadata_format: MboxMetadata,
|
||||
is_empty: bool,
|
||||
crlf: bool,
|
||||
) -> Result<()> {
|
||||
if tags.iter().any(|t| t.contains(' ')) {
|
||||
return Err(Error::new("mbox tags/keywords can't contain spaces"));
|
||||
}
|
||||
let line_ending: &'static [u8] = if crlf { &b"\r\n"[..] } else { &b"\n"[..] };
|
||||
if !is_empty {
|
||||
writer.write_all(line_ending)?;
|
||||
writer.write_all(line_ending)?;
|
||||
}
|
||||
writer.write_all(&b"From "[..])?;
|
||||
if let Some(from) = envelope_from {
|
||||
writer.write_all(from.address_spec_raw())?;
|
||||
} else {
|
||||
writer.write_all(&b"MAILER-DAEMON"[..])?;
|
||||
}
|
||||
writer.write_all(&b" "[..])?;
|
||||
writer.write_all(
|
||||
datetime::timestamp_to_string(
|
||||
delivery_date.unwrap_or_else(datetime::now),
|
||||
Some(datetime::formats::ASCTIME_FMT),
|
||||
true,
|
||||
)
|
||||
.trim()
|
||||
.as_bytes(),
|
||||
)?;
|
||||
writer.write_all(line_ending)?;
|
||||
let (mut headers, body) = parser::mail(input)?;
|
||||
headers.retain(|(header_name, _)| {
|
||||
!header_name.eq_ignore_ascii_case(b"Status")
|
||||
&& !header_name.eq_ignore_ascii_case(b"X-Status")
|
||||
&& !header_name.eq_ignore_ascii_case(b"X-Keywords")
|
||||
&& !header_name.eq_ignore_ascii_case(b"Content-Length")
|
||||
});
|
||||
let write_header_val_fn = |writer: &mut dyn std::io::Write, bytes: &[u8]| {
|
||||
let mut i = 0;
|
||||
if crlf {
|
||||
while i < bytes.len() {
|
||||
if bytes[i..].starts_with(b"\r\n") {
|
||||
writer.write_all(&[b'\r', b'\n'])?;
|
||||
i += 2;
|
||||
continue;
|
||||
} else if bytes[i] == b'\n' {
|
||||
writer.write_all(&[b'\r', b'\n'])?;
|
||||
} else {
|
||||
writer.write_all(&[bytes[i]])?;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
} else {
|
||||
while i < bytes.len() {
|
||||
if bytes[i..].starts_with(b"\r\n") {
|
||||
writer.write_all(&[b'\n'])?;
|
||||
i += 2;
|
||||
} else {
|
||||
writer.write_all(&[bytes[i]])?;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok::<(), Error>(())
|
||||
};
|
||||
let write_metadata_fn = |writer: &mut dyn std::io::Write| match metadata_format {
|
||||
MboxMetadata::CClient => {
|
||||
for (h, v) in {
|
||||
if flags.is_seen() {
|
||||
Some((&b"Status"[..], "R".into()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.into_iter()
|
||||
.chain(
|
||||
if !flags.is_flagged()
|
||||
&& !flags.is_replied()
|
||||
&& !flags.is_draft()
|
||||
&& !flags.is_trashed()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some((
|
||||
&b"X-Status"[..],
|
||||
format!(
|
||||
"{flagged}{replied}{draft}{trashed}",
|
||||
flagged = if flags.is_flagged() { "F" } else { "" },
|
||||
replied = if flags.is_replied() { "A" } else { "" },
|
||||
draft = if flags.is_draft() { "T" } else { "" },
|
||||
trashed = if flags.is_trashed() { "D" } else { "" }
|
||||
),
|
||||
))
|
||||
},
|
||||
)
|
||||
.chain(if tags.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((&b"X-Keywords"[..], tags.as_slice().join(" ")))
|
||||
})
|
||||
} {
|
||||
writer.write_all(h)?;
|
||||
writer.write_all(&b": "[..])?;
|
||||
writer.write_all(v.as_bytes())?;
|
||||
writer.write_all(line_ending)?;
|
||||
}
|
||||
Ok::<(), Error>(())
|
||||
}
|
||||
MboxMetadata::None => Ok(()),
|
||||
};
|
||||
|
||||
let body_len = {
|
||||
let mut len = body.len();
|
||||
if crlf {
|
||||
let stray_lfs = body.iter().filter(|b| **b == b'\n').count()
|
||||
- body.windows(b"\r\n".len()).filter(|w| w == b"\r\n").count();
|
||||
len += stray_lfs;
|
||||
} else {
|
||||
let crlfs = body.windows(b"\r\n".len()).filter(|w| w == b"\r\n").count();
|
||||
len -= crlfs;
|
||||
}
|
||||
len
|
||||
};
|
||||
|
||||
match self {
|
||||
Self::MboxO | Self::MboxRd => Err(Error::new("Unimplemented.")),
|
||||
Self::MboxCl => {
|
||||
let len = (body_len
|
||||
+ body
|
||||
.windows(b"\nFrom ".len())
|
||||
.filter(|w| w == b"\nFrom ")
|
||||
.count()
|
||||
+ if body.starts_with(b"From ") { 1 } else { 0 })
|
||||
.to_string();
|
||||
for (h, v) in headers
|
||||
.into_iter()
|
||||
.chain(Some((&b"Content-Length"[..], len.as_bytes())))
|
||||
{
|
||||
writer.write_all(h)?;
|
||||
writer.write_all(&b": "[..])?;
|
||||
write_header_val_fn(writer, v)?;
|
||||
writer.write_all(line_ending)?;
|
||||
}
|
||||
write_metadata_fn(writer)?;
|
||||
writer.write_all(line_ending)?;
|
||||
|
||||
if body.starts_with(b"From ") {
|
||||
writer.write_all(&[b'>'])?;
|
||||
}
|
||||
let mut i = 0;
|
||||
if crlf {
|
||||
while i < body.len() {
|
||||
if body[i..].starts_with(b"\r\n") {
|
||||
writer.write_all(&[b'\r', b'\n'])?;
|
||||
if body[i..].starts_with(b"\r\nFrom ") {
|
||||
writer.write_all(&[b'>'])?;
|
||||
}
|
||||
i += 2;
|
||||
} else if body[i] == b'\n' {
|
||||
writer.write_all(&[b'\r', b'\n'])?;
|
||||
if body[i..].starts_with(b"\nFrom ") {
|
||||
writer.write_all(&[b'>'])?;
|
||||
}
|
||||
i += 1;
|
||||
} else {
|
||||
writer.write_all(&[body[i]])?;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
while i < body.len() {
|
||||
if body[i..].starts_with(b"\r\n") {
|
||||
writer.write_all(&[b'\n'])?;
|
||||
if body[i..].starts_with(b"\r\nFrom ") {
|
||||
writer.write_all(&[b'>'])?;
|
||||
}
|
||||
i += 2;
|
||||
} else {
|
||||
writer.write_all(&[body[i]])?;
|
||||
if body[i..].starts_with(b"\nFrom ") {
|
||||
writer.write_all(&[b'>'])?;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Self::MboxCl2 => {
|
||||
let len = body_len.to_string();
|
||||
for (h, v) in headers
|
||||
.into_iter()
|
||||
.chain(Some((&b"Content-Length"[..], len.as_bytes())))
|
||||
{
|
||||
writer.write_all(h)?;
|
||||
writer.write_all(&b": "[..])?;
|
||||
write_header_val_fn(writer, v)?;
|
||||
writer.write_all(line_ending)?;
|
||||
}
|
||||
write_metadata_fn(writer)?;
|
||||
writer.write_all(line_ending)?;
|
||||
let mut i = 0;
|
||||
if crlf {
|
||||
while i < body.len() {
|
||||
if body[i..].starts_with(b"\r\n") {
|
||||
writer.write_all(&[b'\r', b'\n'])?;
|
||||
i += 2;
|
||||
continue;
|
||||
} else if body[i] == b'\n' {
|
||||
writer.write_all(&[b'\r', b'\n'])?;
|
||||
} else {
|
||||
writer.write_all(&[body[i]])?;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
} else {
|
||||
while i < body.len() {
|
||||
if body[i..].starts_with(b"\r\n") {
|
||||
writer.write_all(&[b'\n'])?;
|
||||
i += 2;
|
||||
} else {
|
||||
writer.write_all(&[body[i]])?;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,16 +19,9 @@
|
|||
* 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;
|
||||
|
||||
use crate::{get_conf_val, get_path_hash};
|
||||
#[macro_use]
|
||||
mod protocol_parser;
|
||||
pub use protocol_parser::*;
|
||||
|
@ -37,68 +30,27 @@ pub use mailbox::*;
|
|||
mod operations;
|
||||
pub use operations::*;
|
||||
mod connection;
|
||||
use std::{
|
||||
collections::{hash_map::DefaultHasher, BTreeSet, HashMap, HashSet},
|
||||
hash::Hasher,
|
||||
pin::Pin,
|
||||
str::FromStr,
|
||||
sync::{Arc, Mutex},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub use connection::*;
|
||||
use futures::{lock::Mutex as FutureMutex, stream::Stream};
|
||||
|
||||
use crate::{
|
||||
backends::*,
|
||||
conf::AccountSettings,
|
||||
email::*,
|
||||
error::{Error, Result, ResultIntoError},
|
||||
utils::futures::timeout,
|
||||
Collection,
|
||||
};
|
||||
use crate::backends::*;
|
||||
use crate::conf::AccountSettings;
|
||||
use crate::email::*;
|
||||
use crate::error::{MeliError, Result, ResultIntoMeliError};
|
||||
use futures::lock::Mutex as FutureMutex;
|
||||
use futures::stream::Stream;
|
||||
use std::collections::{hash_map::DefaultHasher, BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
use std::future::Future;
|
||||
use std::hash::Hasher;
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::time::Instant;
|
||||
pub type UID = usize;
|
||||
|
||||
macro_rules! get_conf_val {
|
||||
($s:ident[$var:literal]) => {
|
||||
$s.extra.get($var).ok_or_else(|| {
|
||||
Error::new(format!(
|
||||
"Configuration error ({}): NNTP connection requires the field `{}` set",
|
||||
$s.name.as_str(),
|
||||
$var
|
||||
))
|
||||
})
|
||||
};
|
||||
($s:ident[$var:literal], $default:expr) => {
|
||||
$s.extra
|
||||
.get($var)
|
||||
.map(|v| {
|
||||
<_>::from_str(v).map_err(|e| {
|
||||
Error::new(format!(
|
||||
"Configuration error ({}) NNTP: Invalid value for field `{}`: {}\n{}",
|
||||
$s.name.as_str(),
|
||||
$var,
|
||||
v,
|
||||
e
|
||||
))
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| Ok($default))
|
||||
};
|
||||
}
|
||||
|
||||
pub static SUPPORTED_CAPABILITIES: &[&str] = &[
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
"COMPRESS DEFLATE",
|
||||
"VERSION 2",
|
||||
"NEWNEWS",
|
||||
"POST",
|
||||
"OVER",
|
||||
"OVER MSGID",
|
||||
"READER",
|
||||
"STARTTLS",
|
||||
"HDR",
|
||||
"AUTHINFO USER",
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -119,13 +71,12 @@ type Capabilities = HashSet<String>;
|
|||
#[derive(Debug)]
|
||||
pub struct UIDStore {
|
||||
account_hash: AccountHash,
|
||||
account_name: Arc<str>,
|
||||
account_name: Arc<String>,
|
||||
offline_cache: bool,
|
||||
capabilities: Arc<Mutex<Capabilities>>,
|
||||
message_id_index: Arc<Mutex<HashMap<String, EnvelopeHash>>>,
|
||||
hash_index: Arc<Mutex<HashMap<EnvelopeHash, (UID, MailboxHash)>>>,
|
||||
uid_index: Arc<Mutex<HashMap<(MailboxHash, UID), EnvelopeHash>>>,
|
||||
|
||||
collection: Collection,
|
||||
mailboxes: Arc<FutureMutex<HashMap<MailboxHash, NntpMailbox>>>,
|
||||
is_online: Arc<Mutex<(Instant, Result<()>)>>,
|
||||
event_consumer: BackendEventConsumer,
|
||||
|
@ -134,22 +85,21 @@ pub struct UIDStore {
|
|||
impl UIDStore {
|
||||
fn new(
|
||||
account_hash: AccountHash,
|
||||
account_name: Arc<str>,
|
||||
account_name: Arc<String>,
|
||||
event_consumer: BackendEventConsumer,
|
||||
) -> Self {
|
||||
Self {
|
||||
UIDStore {
|
||||
account_hash,
|
||||
account_name,
|
||||
event_consumer,
|
||||
offline_cache: false,
|
||||
capabilities: Default::default(),
|
||||
message_id_index: Default::default(),
|
||||
hash_index: Default::default(),
|
||||
uid_index: Default::default(),
|
||||
mailboxes: Arc::new(FutureMutex::new(Default::default())),
|
||||
collection: Collection::new(),
|
||||
is_online: Arc::new(Mutex::new((
|
||||
Instant::now(),
|
||||
Err(Error::new("Account is uninitialised.")),
|
||||
Err(MeliError::new("Account is uninitialised.")),
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
@ -157,11 +107,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 {
|
||||
|
@ -179,7 +129,6 @@ impl MailBackend for NntpType {
|
|||
)
|
||||
})
|
||||
.collect::<Vec<(String, MailBackendExtensionStatus)>>();
|
||||
let mut supports_submission = false;
|
||||
let NntpExtensionUse {
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
deflate,
|
||||
|
@ -187,10 +136,6 @@ impl MailBackend for NntpType {
|
|||
{
|
||||
for (name, status) in extensions.iter_mut() {
|
||||
match name.as_str() {
|
||||
s if s.eq_ignore_ascii_case("POST") => {
|
||||
supports_submission = true;
|
||||
*status = MailBackendExtensionStatus::Enabled { comment: None };
|
||||
}
|
||||
"COMPRESS DEFLATE" => {
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
{
|
||||
|
@ -224,7 +169,7 @@ impl MailBackend for NntpType {
|
|||
supports_search: false,
|
||||
extensions: Some(extensions),
|
||||
supports_tags: false,
|
||||
supports_submission,
|
||||
supports_submission: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -254,109 +199,15 @@ impl MailBackend for NntpType {
|
|||
}))
|
||||
}
|
||||
|
||||
fn refresh(&mut self, mailbox_hash: MailboxHash) -> ResultFuture<()> {
|
||||
let uid_store = self.uid_store.clone();
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
/* To get updates, either issue NEWNEWS if it's supported by the server, and
|
||||
* fallback to OVER otherwise */
|
||||
let mbox: NntpMailbox = uid_store
|
||||
.mailboxes
|
||||
.lock()
|
||||
.await
|
||||
.get(&mailbox_hash)
|
||||
.map(std::clone::Clone::clone)
|
||||
.ok_or_else(|| {
|
||||
Error::new(format!(
|
||||
"Mailbox with hash {} not found in NNTP connection, this could possibly \
|
||||
be a bug or it was deleted.",
|
||||
mailbox_hash
|
||||
))
|
||||
})?;
|
||||
let latest_article: Option<crate::UnixTimestamp> = *mbox.latest_article.lock().unwrap();
|
||||
let (over_msgid_support, newnews_support): (bool, bool) = {
|
||||
let caps = uid_store.capabilities.lock().unwrap();
|
||||
|
||||
(
|
||||
caps.iter().any(|c| c.eq_ignore_ascii_case("OVER MSGID")),
|
||||
caps.iter().any(|c| c.eq_ignore_ascii_case("NEWNEWS")),
|
||||
)
|
||||
};
|
||||
let mut res = String::with_capacity(8 * 1024);
|
||||
let mut conn = timeout(Some(Duration::from_secs(60 * 16)), connection.lock()).await?;
|
||||
if let Some(mut latest_article) = latest_article {
|
||||
let timestamp = latest_article - 10 * 60;
|
||||
let datetime_str = crate::utils::datetime::timestamp_to_string(
|
||||
timestamp,
|
||||
Some("%Y%m%d %H%M%S"),
|
||||
true,
|
||||
);
|
||||
|
||||
if newnews_support {
|
||||
conn.send_command(
|
||||
format!("NEWNEWS {} {}", &mbox.nntp_path, datetime_str).as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
conn.read_response(&mut res, true, &["230 "]).await?;
|
||||
let message_ids = {
|
||||
let message_id_lck = uid_store.message_id_index.lock().unwrap();
|
||||
res.split_rn()
|
||||
.skip(1)
|
||||
.map(|s| s.trim())
|
||||
.filter(|msg_id| !message_id_lck.contains_key(*msg_id))
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<String>>()
|
||||
};
|
||||
if message_ids.is_empty() || !over_msgid_support {
|
||||
return Ok(());
|
||||
}
|
||||
let mut env_hash_set: BTreeSet<EnvelopeHash> = Default::default();
|
||||
for msg_id in message_ids {
|
||||
conn.send_command(format!("OVER {}", msg_id).as_bytes())
|
||||
.await?;
|
||||
conn.read_response(&mut res, true, &["224 "]).await?;
|
||||
let mut message_id_lck = uid_store.message_id_index.lock().unwrap();
|
||||
let mut hash_index_lck = uid_store.hash_index.lock().unwrap();
|
||||
let mut uid_index_lck = uid_store.uid_index.lock().unwrap();
|
||||
for l in res.split_rn().skip(1) {
|
||||
let (_, (num, env)) = protocol_parser::over_article(l)?;
|
||||
env_hash_set.insert(env.hash());
|
||||
message_id_lck.insert(env.message_id_display().to_string(), env.hash());
|
||||
hash_index_lck.insert(env.hash(), (num, mailbox_hash));
|
||||
uid_index_lck.insert((mailbox_hash, num), env.hash());
|
||||
latest_article = std::cmp::max(latest_article, env.timestamp);
|
||||
(uid_store.event_consumer)(
|
||||
uid_store.account_hash,
|
||||
crate::backends::BackendEvent::Refresh(RefreshEvent {
|
||||
mailbox_hash,
|
||||
account_hash: uid_store.account_hash,
|
||||
kind: RefreshEventKind::Create(Box::new(env)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
{
|
||||
let f = &uid_store.mailboxes.lock().await[&mailbox_hash];
|
||||
*f.latest_article.lock().unwrap() = Some(latest_article);
|
||||
f.exists
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert_existing_set(env_hash_set.clone());
|
||||
f.unseen.lock().unwrap().insert_existing_set(env_hash_set);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
//conn.select_group(mailbox_hash, false, &mut res).await?;
|
||||
Ok(())
|
||||
}))
|
||||
fn refresh(&mut self, _mailbox_hash: MailboxHash) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn mailboxes(&self) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
|
||||
let uid_store = self.uid_store.clone();
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
Self::nntp_mailboxes(&connection).await?;
|
||||
NntpType::nntp_mailboxes(&connection).await?;
|
||||
let mailboxes_lck = uid_store.mailboxes.lock().await;
|
||||
let ret = mailboxes_lck
|
||||
.iter()
|
||||
|
@ -369,11 +220,10 @@ impl MailBackend for NntpType {
|
|||
fn is_online(&self) -> ResultFuture<()> {
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
match timeout(Some(Duration::from_secs(60 * 16)), connection.lock()).await {
|
||||
match timeout(std::time::Duration::from_secs(3), connection.lock()).await {
|
||||
Ok(mut conn) => {
|
||||
debug!("is_online");
|
||||
match debug!(timeout(Some(Duration::from_secs(60 * 16)), conn.connect()).await)
|
||||
{
|
||||
match debug!(timeout(std::time::Duration::from_secs(3), conn.connect()).await) {
|
||||
Ok(Ok(())) => Ok(()),
|
||||
Err(err) | Ok(Err(err)) => {
|
||||
conn.stream = Err(err.clone());
|
||||
|
@ -387,22 +237,19 @@ impl MailBackend for NntpType {
|
|||
}
|
||||
|
||||
fn watch(&self) -> ResultFuture<()> {
|
||||
Err(
|
||||
Error::new("Watching is currently uniplemented for nntp backend")
|
||||
.set_kind(ErrorKind::NotImplemented),
|
||||
)
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn operation(&self, env_hash: EnvelopeHash) -> Result<Box<dyn BackendOp>> {
|
||||
let (uid, mailbox_hash) =
|
||||
if let Some(v) = self.uid_store.hash_index.lock().unwrap().get(&env_hash) {
|
||||
*v
|
||||
} else {
|
||||
return Err(Error::new(
|
||||
"Message not found in local cache, it might have been deleted before you \
|
||||
requested it.",
|
||||
let (uid, mailbox_hash) = if let Some(v) =
|
||||
self.uid_store.hash_index.lock().unwrap().get(&env_hash)
|
||||
{
|
||||
*v
|
||||
} else {
|
||||
return Err(MeliError::new(
|
||||
"Message not found in local cache, it might have been deleted before you requested it."
|
||||
));
|
||||
};
|
||||
};
|
||||
Ok(Box::new(NntpOp::new(
|
||||
uid,
|
||||
mailbox_hash,
|
||||
|
@ -417,7 +264,7 @@ impl MailBackend for NntpType {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_flags: Option<Flag>,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new("NNTP doesn't support saving."))
|
||||
Err(MeliError::new("NNTP doesn't support saving."))
|
||||
}
|
||||
|
||||
fn copy_messages(
|
||||
|
@ -427,7 +274,7 @@ impl MailBackend for NntpType {
|
|||
_destination_mailbox_hash: MailboxHash,
|
||||
_move_: bool,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new("NNTP doesn't support copying/moving."))
|
||||
Err(MeliError::new("NNTP doesn't support copying/moving."))
|
||||
}
|
||||
|
||||
fn set_flags(
|
||||
|
@ -436,7 +283,7 @@ impl MailBackend for NntpType {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_flags: SmallVec<[(std::result::Result<Flag, String>, bool); 8]>,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new("NNTP doesn't support flags."))
|
||||
Err(MeliError::new("NNTP doesn't support flags."))
|
||||
}
|
||||
|
||||
fn delete_messages(
|
||||
|
@ -444,7 +291,11 @@ impl MailBackend for NntpType {
|
|||
_env_hashes: EnvelopeHashBatch,
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new("NNTP doesn't support deletion."))
|
||||
Err(MeliError::new("NNTP doesn't support deletion."))
|
||||
}
|
||||
|
||||
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
|
@ -455,26 +306,18 @@ impl MailBackend for NntpType {
|
|||
self
|
||||
}
|
||||
|
||||
fn collection(&self) -> Collection {
|
||||
self.uid_store.collection.clone()
|
||||
}
|
||||
|
||||
fn create_mailbox(
|
||||
&mut self,
|
||||
_path: String,
|
||||
) -> ResultFuture<(MailboxHash, HashMap<MailboxHash, Mailbox>)> {
|
||||
Err(Error::new(
|
||||
"Creating mailbox is currently unimplemented for nntp backend.",
|
||||
))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn delete_mailbox(
|
||||
&mut self,
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
|
||||
Err(Error::new(
|
||||
"Deleting a mailbox is currently unimplemented for nntp backend.",
|
||||
))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn set_mailbox_subscription(
|
||||
|
@ -482,9 +325,7 @@ impl MailBackend for NntpType {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_new_val: bool,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new(
|
||||
"Setting mailbox description is currently unimplemented for nntp backend.",
|
||||
))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn rename_mailbox(
|
||||
|
@ -492,9 +333,7 @@ impl MailBackend for NntpType {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_new_path: String,
|
||||
) -> ResultFuture<Mailbox> {
|
||||
Err(Error::new(
|
||||
"Renaming mailbox is currently unimplemented for nntp backend.",
|
||||
))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn set_mailbox_permissions(
|
||||
|
@ -502,9 +341,7 @@ impl MailBackend for NntpType {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_val: crate::backends::MailboxPermissions,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new(
|
||||
"Setting mailbox permissions is currently unimplemented for nntp backend.",
|
||||
))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn search(
|
||||
|
@ -512,47 +349,11 @@ impl MailBackend for NntpType {
|
|||
_query: crate::search::Query,
|
||||
_mailbox_hash: Option<MailboxHash>,
|
||||
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>> {
|
||||
Err(Error::new(
|
||||
"Searching is currently unimplemented for nntp backend.",
|
||||
))
|
||||
}
|
||||
|
||||
fn submit(
|
||||
&self,
|
||||
bytes: Vec<u8>,
|
||||
mailbox_hash: Option<MailboxHash>,
|
||||
_flags: Option<Flag>,
|
||||
) -> ResultFuture<()> {
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
match timeout(Some(Duration::from_secs(60 * 16)), connection.lock()).await {
|
||||
Ok(mut conn) => {
|
||||
match &conn.stream {
|
||||
Ok(stream) => {
|
||||
if !stream.supports_submission {
|
||||
return Err(Error::new("Server prohibits posting."));
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(err.clone()),
|
||||
}
|
||||
let mut res = String::with_capacity(8 * 1024);
|
||||
if let Some(mailbox_hash) = mailbox_hash {
|
||||
conn.select_group(mailbox_hash, false, &mut res).await?;
|
||||
}
|
||||
conn.send_command(b"POST").await?;
|
||||
conn.read_response(&mut res, false, &["340 "]).await?;
|
||||
conn.send_multiline_data_block(&bytes).await?;
|
||||
conn.read_response(&mut res, false, &["240 "]).await?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
}
|
||||
|
||||
impl NntpType {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(
|
||||
s: &AccountSettings,
|
||||
is_subscribed: Box<dyn Fn(&str) -> bool + Send + Sync>,
|
||||
|
@ -571,7 +372,7 @@ impl NntpType {
|
|||
.stderr(std::process::Stdio::piped())
|
||||
.output()?;
|
||||
if !output.status.success() {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"({}) server_password_command `{}` returned {}: {}",
|
||||
s.name,
|
||||
get_conf_val!(s["server_password_command"])?,
|
||||
|
@ -584,10 +385,10 @@ impl NntpType {
|
|||
*/
|
||||
let server_port = get_conf_val!(s["server_port"], 119)?;
|
||||
let use_tls = get_conf_val!(s["use_tls"], server_port == 563)?;
|
||||
let use_starttls = use_tls && get_conf_val!(s["use_starttls"], false)?;
|
||||
let use_starttls = use_tls && get_conf_val!(s["use_starttls"], !(server_port == 563))?;
|
||||
let danger_accept_invalid_certs: bool =
|
||||
get_conf_val!(s["danger_accept_invalid_certs"], false)?;
|
||||
let require_auth = get_conf_val!(s["require_auth"], false)?;
|
||||
let require_auth = get_conf_val!(s["require_auth"], true)?;
|
||||
let server_conf = NntpServerConf {
|
||||
server_hostname: server_hostname.to_string(),
|
||||
server_username: if require_auth {
|
||||
|
@ -607,14 +408,18 @@ impl NntpType {
|
|||
danger_accept_invalid_certs,
|
||||
extension_use: NntpExtensionUse {
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
deflate: get_conf_val!(s["use_deflate"], false)?,
|
||||
deflate: get_conf_val!(s["use_deflate"], true)?,
|
||||
},
|
||||
};
|
||||
let account_hash = AccountHash::from_bytes(s.name.as_bytes());
|
||||
let account_name = s.name.to_string().into();
|
||||
let account_hash = {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
hasher.write(s.name.as_bytes());
|
||||
hasher.finish()
|
||||
};
|
||||
let account_name = Arc::new(s.name().to_string());
|
||||
let mut mailboxes = HashMap::default();
|
||||
for (k, _f) in s.mailboxes.iter() {
|
||||
let mailbox_hash = MailboxHash(get_path_hash!(&k));
|
||||
let mailbox_hash = get_path_hash!(&k);
|
||||
mailboxes.insert(
|
||||
mailbox_hash,
|
||||
NntpMailbox {
|
||||
|
@ -622,28 +427,28 @@ impl NntpType {
|
|||
nntp_path: k.to_string(),
|
||||
high_watermark: Arc::new(Mutex::new(0)),
|
||||
low_watermark: Arc::new(Mutex::new(0)),
|
||||
latest_article: Arc::new(Mutex::new(None)),
|
||||
exists: Default::default(),
|
||||
unseen: Default::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
if mailboxes.is_empty() {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"{} has no newsgroups configured.",
|
||||
account_name
|
||||
)));
|
||||
}
|
||||
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)
|
||||
});
|
||||
let connection = NntpConnection::new_connection(&server_conf, uid_store.clone());
|
||||
|
||||
Ok(Box::new(Self {
|
||||
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,
|
||||
}))
|
||||
|
@ -680,7 +485,7 @@ impl NntpType {
|
|||
if s.len() != 3 {
|
||||
continue;
|
||||
}
|
||||
let mailbox_hash = MailboxHash(get_path_hash!(&s[0]));
|
||||
let mailbox_hash = get_path_hash!(&s[0]);
|
||||
mailboxes_lck.entry(mailbox_hash).and_modify(|m| {
|
||||
*m.high_watermark.lock().unwrap() = usize::from_str(s[1]).unwrap_or(0);
|
||||
*m.low_watermark.lock().unwrap() = usize::from_str(s[2]).unwrap_or(0);
|
||||
|
@ -689,86 +494,36 @@ impl NntpType {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_config(s: &mut AccountSettings) -> Result<()> {
|
||||
let mut keys: HashSet<&'static str> = Default::default();
|
||||
macro_rules! get_conf_val {
|
||||
($s:ident[$var:literal]) => {{
|
||||
keys.insert($var);
|
||||
$s.extra.remove($var).ok_or_else(|| {
|
||||
Error::new(format!(
|
||||
"Configuration error ({}): NNTP connection requires the field `{}` set",
|
||||
$s.name.as_str(),
|
||||
$var
|
||||
))
|
||||
})
|
||||
}};
|
||||
($s:ident[$var:literal], $default:expr) => {{
|
||||
keys.insert($var);
|
||||
$s.extra
|
||||
.remove($var)
|
||||
.map(|v| {
|
||||
<_>::from_str(&v).map_err(|e| {
|
||||
Error::new(format!(
|
||||
"Configuration error ({}) NNTP: Invalid value for field `{}`: \
|
||||
{}\n{}",
|
||||
$s.name.as_str(),
|
||||
$var,
|
||||
v,
|
||||
e
|
||||
))
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| Ok($default))
|
||||
}};
|
||||
}
|
||||
get_conf_val!(s["require_auth"], false)?;
|
||||
pub fn validate_config(s: &AccountSettings) -> Result<()> {
|
||||
get_conf_val!(s["server_hostname"])?;
|
||||
get_conf_val!(s["server_username"], String::new())?;
|
||||
if !s.extra.contains_key("server_password_command") {
|
||||
get_conf_val!(s["server_password"], String::new())?;
|
||||
} else if s.extra.contains_key("server_password") {
|
||||
return Err(Error::new(format!(
|
||||
"Configuration error ({}): both server_password and server_password_command are \
|
||||
set, cannot choose",
|
||||
return Err(MeliError::new(format!(
|
||||
"Configuration error ({}): both server_password and server_password_command are set, cannot choose",
|
||||
s.name.as_str(),
|
||||
)));
|
||||
}
|
||||
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(Error::new(format!(
|
||||
"Configuration error ({}): incompatible use_tls and use_starttls values: use_tls \
|
||||
= false, use_starttls = true",
|
||||
return Err(MeliError::new(format!(
|
||||
"Configuration error ({}): incompatible use_tls and use_starttls values: use_tls = false, use_starttls = true",
|
||||
s.name.as_str(),
|
||||
)));
|
||||
}
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
get_conf_val!(s["use_deflate"], false)?;
|
||||
get_conf_val!(s["use_deflate"], true)?;
|
||||
#[cfg(not(feature = "deflate_compression"))]
|
||||
if s.extra.contains_key("use_deflate") {
|
||||
return Err(Error::new(format!(
|
||||
"Configuration error ({}): setting `use_deflate` is set but this version of meli \
|
||||
isn't compiled with DEFLATE support.",
|
||||
return Err(MeliError::new(format!(
|
||||
"Configuration error ({}): setting `use_deflate` is set but this version of meli isn't compiled with DEFLATE support.",
|
||||
s.name.as_str(),
|
||||
)));
|
||||
}
|
||||
get_conf_val!(s["danger_accept_invalid_certs"], false)?;
|
||||
let extra_keys = s
|
||||
.extra
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect::<HashSet<&str>>();
|
||||
let diff = extra_keys.difference(&keys).collect::<Vec<&&str>>();
|
||||
if !diff.is_empty() {
|
||||
return Err(Error::new(format!(
|
||||
"Configuration error ({}) NNTP: the following flags are set but are not \
|
||||
recognized: {:?}.",
|
||||
s.name.as_str(),
|
||||
diff
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -778,7 +533,7 @@ impl NntpType {
|
|||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|c| c.clone())
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
}
|
||||
|
@ -792,7 +547,7 @@ struct FetchState {
|
|||
|
||||
impl FetchState {
|
||||
async fn fetch_envs(&mut self) -> Result<Option<Vec<Envelope>>> {
|
||||
let Self {
|
||||
let FetchState {
|
||||
mailbox_hash,
|
||||
ref connection,
|
||||
ref uid_store,
|
||||
|
@ -815,14 +570,14 @@ impl FetchState {
|
|||
.name()
|
||||
.to_string();
|
||||
if s.len() != 5 {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"{} Could not select newsgroup {}: expected GROUP response but got: {}",
|
||||
&uid_store.account_name, path, res
|
||||
)));
|
||||
}
|
||||
let total = usize::from_str(s[1]).unwrap_or(0);
|
||||
let _low = usize::from_str(s[2]).unwrap_or(0);
|
||||
let high = usize::from_str(s[3]).unwrap_or(0);
|
||||
let total = usize::from_str(&s[1]).unwrap_or(0);
|
||||
let _low = usize::from_str(&s[2]).unwrap_or(0);
|
||||
let high = usize::from_str(&s[3]).unwrap_or(0);
|
||||
*high_low_total = Some((high, _low, total));
|
||||
{
|
||||
let f = &uid_store.mailboxes.lock().await[&mailbox_hash];
|
||||
|
@ -834,11 +589,10 @@ impl FetchState {
|
|||
if high <= low {
|
||||
return Ok(None);
|
||||
}
|
||||
const CHUNK_SIZE: usize = 50000;
|
||||
const CHUNK_SIZE: usize = 100;
|
||||
let new_low = std::cmp::max(low, high.saturating_sub(CHUNK_SIZE));
|
||||
high_low_total.as_mut().unwrap().0 = new_low;
|
||||
|
||||
// FIXME: server might not implement OVER capability
|
||||
conn.send_command(format!("OVER {}-{}", new_low, high).as_bytes())
|
||||
.await?;
|
||||
conn.read_response(&mut res, true, command_to_replycodes("OVER"))
|
||||
|
@ -852,28 +606,19 @@ impl FetchState {
|
|||
let mut ret = Vec::with_capacity(high - new_low);
|
||||
//hash_index: Arc<Mutex<HashMap<EnvelopeHash, (UID, MailboxHash)>>>,
|
||||
//uid_index: Arc<Mutex<HashMap<(MailboxHash, UID), EnvelopeHash>>>,
|
||||
let mut latest_article: Option<crate::UnixTimestamp> = None;
|
||||
{
|
||||
let mut message_id_lck = uid_store.message_id_index.lock().unwrap();
|
||||
let mut hash_index_lck = uid_store.hash_index.lock().unwrap();
|
||||
let mut uid_index_lck = uid_store.uid_index.lock().unwrap();
|
||||
for l in res.split_rn().skip(1) {
|
||||
let (_, (num, env)) = protocol_parser::over_article(l)?;
|
||||
message_id_lck.insert(env.message_id_display().to_string(), env.hash());
|
||||
let (_, (num, env)) = protocol_parser::over_article(&l)?;
|
||||
hash_index_lck.insert(env.hash(), (num, mailbox_hash));
|
||||
uid_index_lck.insert((mailbox_hash, num), env.hash());
|
||||
if let Some(ref mut v) = latest_article {
|
||||
*v = std::cmp::max(*v, env.timestamp);
|
||||
} else {
|
||||
latest_article = Some(env.timestamp);
|
||||
}
|
||||
ret.push(env);
|
||||
}
|
||||
}
|
||||
{
|
||||
let hash_set: BTreeSet<EnvelopeHash> = ret.iter().map(|env| env.hash()).collect();
|
||||
let f = &uid_store.mailboxes.lock().await[&mailbox_hash];
|
||||
*f.latest_article.lock().unwrap() = latest_article;
|
||||
f.exists
|
||||
.lock()
|
||||
.unwrap()
|
||||
|
@ -883,3 +628,15 @@ impl FetchState {
|
|||
Ok(Some(ret))
|
||||
}
|
||||
}
|
||||
|
||||
use futures::future::{self, Either};
|
||||
|
||||
async fn timeout<O>(dur: std::time::Duration, f: impl Future<Output = O>) -> Result<O> {
|
||||
futures::pin_mut!(f);
|
||||
match future::select(f, smol::Timer::after(dur)).await {
|
||||
Either::Left((out, _)) => Ok(out),
|
||||
Either::Right(_) => {
|
||||
Err(MeliError::new("Timedout").set_kind(crate::error::ErrorKind::Network))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,34 +19,42 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
backends::{BackendMailbox, MailboxHash},
|
||||
email::parser::BytesExt,
|
||||
error::*,
|
||||
log,
|
||||
utils::connections::{lookup_ipv4, Connection},
|
||||
};
|
||||
use crate::backends::{BackendMailbox, MailboxHash};
|
||||
use crate::connections::{lookup_ipv4, Connection};
|
||||
use crate::email::parser::BytesExt;
|
||||
use crate::error::*;
|
||||
extern crate native_tls;
|
||||
use std::{collections::HashSet, future::Future, pin::Pin, sync::Arc, time::Instant};
|
||||
|
||||
use futures::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use native_tls::TlsConnector;
|
||||
pub use smol::Async as AsyncWrapper;
|
||||
use std::collections::HashSet;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use super::{Capabilities, NntpServerConf, UIDStore};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct NntpExtensionUse {
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
pub deflate: bool,
|
||||
}
|
||||
|
||||
impl Default for NntpExtensionUse {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
deflate: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NntpStream {
|
||||
pub stream: AsyncWrapper<Connection>,
|
||||
pub extension_use: NntpExtensionUse,
|
||||
pub current_mailbox: MailboxSelection,
|
||||
pub supports_submission: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
|
@ -57,7 +65,7 @@ pub enum MailboxSelection {
|
|||
|
||||
impl MailboxSelection {
|
||||
pub fn take(&mut self) -> Self {
|
||||
std::mem::replace(self, Self::None)
|
||||
std::mem::replace(self, MailboxSelection::None)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,23 +81,25 @@ pub struct NntpConnection {
|
|||
}
|
||||
|
||||
impl NntpStream {
|
||||
pub async fn new_connection(server_conf: &NntpServerConf) -> Result<(Capabilities, Self)> {
|
||||
pub async fn new_connection(
|
||||
server_conf: &NntpServerConf,
|
||||
) -> Result<(Capabilities, NntpStream)> {
|
||||
use std::net::TcpStream;
|
||||
let path = &server_conf.server_hostname;
|
||||
|
||||
let stream = {
|
||||
let addr = lookup_ipv4(path, server_conf.server_port)?;
|
||||
AsyncWrapper::new(Connection::Tcp(TcpStream::connect_timeout(
|
||||
&addr,
|
||||
std::time::Duration::new(16, 0),
|
||||
)?))?
|
||||
AsyncWrapper::new(Connection::Tcp(
|
||||
TcpStream::connect_timeout(&addr, std::time::Duration::new(4, 0))
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?,
|
||||
))
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?
|
||||
};
|
||||
let mut res = String::with_capacity(8 * 1024);
|
||||
let mut ret = Self {
|
||||
let mut ret = NntpStream {
|
||||
stream,
|
||||
extension_use: server_conf.extension_use,
|
||||
current_mailbox: MailboxSelection::None,
|
||||
supports_submission: false,
|
||||
};
|
||||
|
||||
if server_conf.use_tls {
|
||||
|
@ -97,18 +107,18 @@ impl NntpStream {
|
|||
if server_conf.danger_accept_invalid_certs {
|
||||
connector.danger_accept_invalid_certs(true);
|
||||
}
|
||||
let connector = connector.build()?;
|
||||
let connector = connector
|
||||
.build()
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
|
||||
if server_conf.use_starttls {
|
||||
ret.read_response(&mut res, false, &["200 ", "201 "])
|
||||
.await?;
|
||||
ret.supports_submission = res.starts_with("200");
|
||||
|
||||
ret.send_command(b"CAPABILITIES").await?;
|
||||
ret.read_response(&mut res, true, command_to_replycodes("CAPABILITIES"))
|
||||
.await?;
|
||||
if !res.starts_with("101 ") {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"Could not connect to {}: expected CAPABILITIES response but got:{}",
|
||||
&server_conf.server_hostname, res
|
||||
)));
|
||||
|
@ -119,8 +129,8 @@ impl NntpStream {
|
|||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case("VERSION 2"))
|
||||
{
|
||||
return Err(Error::new(format!(
|
||||
"Could not connect to {}: server is not NNTP VERSION 2 compliant",
|
||||
return Err(MeliError::new(format!(
|
||||
"Could not connect to {}: server is not NNTP compliant",
|
||||
&server_conf.server_hostname
|
||||
)));
|
||||
}
|
||||
|
@ -128,17 +138,23 @@ impl NntpStream {
|
|||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case("STARTTLS"))
|
||||
{
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"Could not connect to {}: server does not support STARTTLS",
|
||||
&server_conf.server_hostname
|
||||
)));
|
||||
}
|
||||
ret.stream.write_all(b"STARTTLS\r\n").await?;
|
||||
ret.stream.flush().await?;
|
||||
ret.stream
|
||||
.write_all(b"STARTTLS\r\n")
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
ret.stream
|
||||
.flush()
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
ret.read_response(&mut res, false, command_to_replycodes("STARTTLS"))
|
||||
.await?;
|
||||
if !res.starts_with("382 ") {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"Could not connect to {}: could not begin TLS negotiation, got: {}",
|
||||
&server_conf.server_hostname, res
|
||||
)));
|
||||
|
@ -147,7 +163,10 @@ impl NntpStream {
|
|||
|
||||
{
|
||||
// FIXME: This is blocking
|
||||
let socket = ret.stream.into_inner()?;
|
||||
let socket = ret
|
||||
.stream
|
||||
.into_inner()
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
let mut conn_result = connector.connect(path, socket);
|
||||
if let Err(native_tls::HandshakeError::WouldBlock(midhandshake_stream)) =
|
||||
conn_result
|
||||
|
@ -163,17 +182,15 @@ impl NntpStream {
|
|||
midhandshake_stream = Some(stream);
|
||||
}
|
||||
p => {
|
||||
p.chain_err_kind(crate::error::ErrorKind::Network(
|
||||
crate::error::NetworkErrorKind::InvalidTLSConnection,
|
||||
))?;
|
||||
p.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ret.stream =
|
||||
AsyncWrapper::new(Connection::Tls(conn_result?)).chain_err_summary(|| {
|
||||
format!("Could not initiate TLS negotiation to {}.", path)
|
||||
})?;
|
||||
ret.stream = AsyncWrapper::new(Connection::Tls(
|
||||
conn_result.chain_err_kind(crate::error::ErrorKind::Network)?,
|
||||
))
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
}
|
||||
}
|
||||
//ret.send_command(
|
||||
|
@ -184,43 +201,28 @@ impl NntpStream {
|
|||
// .as_bytes(),
|
||||
//)
|
||||
//.await?;
|
||||
if let Err(err) = ret
|
||||
.stream
|
||||
.get_ref()
|
||||
.set_keepalive(Some(std::time::Duration::new(60 * 9, 0)))
|
||||
{
|
||||
log::warn!("Could not set TCP keepalive in NNTP connection: {}", err);
|
||||
}
|
||||
|
||||
ret.read_response(&mut res, false, &["200 ", "201 "])
|
||||
.await?;
|
||||
ret.supports_submission = res.starts_with("200");
|
||||
ret.send_command(b"CAPABILITIES").await?;
|
||||
ret.read_response(&mut res, true, command_to_replycodes("CAPABILITIES"))
|
||||
.await?;
|
||||
if !res.starts_with("101 ") {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"Could not connect to {}: expected CAPABILITIES response but got:{}",
|
||||
&server_conf.server_hostname, res
|
||||
)));
|
||||
}
|
||||
let capabilities: HashSet<String> =
|
||||
res.lines().skip(1).map(|l| l.trim().to_string()).collect();
|
||||
let capabilities: HashSet<String> = res.lines().skip(1).map(|l| l.to_string()).collect();
|
||||
if !capabilities
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case("VERSION 2"))
|
||||
{
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"Could not connect to {}: server is not NNTP compliant",
|
||||
&server_conf.server_hostname
|
||||
)));
|
||||
}
|
||||
if !capabilities
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case("POST"))
|
||||
{
|
||||
ret.supports_submission = false;
|
||||
}
|
||||
|
||||
if server_conf.require_auth {
|
||||
if capabilities.iter().any(|c| c.starts_with("AUTHINFO USER")) {
|
||||
|
@ -243,7 +245,7 @@ impl NntpStream {
|
|||
.chain_err_kind(ErrorKind::Authentication)?;
|
||||
}
|
||||
} else {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"Could not connect: no supported auth mechanisms in server capabilities: {:?}",
|
||||
capabilities
|
||||
))
|
||||
|
@ -262,20 +264,18 @@ impl NntpStream {
|
|||
server_conf.server_hostname, res
|
||||
)
|
||||
})?;
|
||||
let Self {
|
||||
let NntpStream {
|
||||
stream,
|
||||
extension_use,
|
||||
current_mailbox,
|
||||
supports_submission,
|
||||
} = ret;
|
||||
let stream = stream.into_inner()?;
|
||||
return Ok((
|
||||
capabilities,
|
||||
Self {
|
||||
NntpStream {
|
||||
stream: AsyncWrapper::new(stream.deflate())?,
|
||||
extension_use,
|
||||
current_mailbox,
|
||||
supports_submission,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
@ -310,20 +310,20 @@ impl NntpStream {
|
|||
ret.push_str(unsafe { std::str::from_utf8_unchecked(&buf[0..b]) });
|
||||
if ret.len() > 4 {
|
||||
if ret.starts_with("205 ") {
|
||||
return Err(Error::new(format!("Disconnected: {}", ret)));
|
||||
return Err(MeliError::new(format!("Disconnected: {}", ret)));
|
||||
} else if ret.starts_with("501 ") || ret.starts_with("500 ") {
|
||||
return Err(Error::new(format!("Syntax error: {}", ret)));
|
||||
return Err(MeliError::new(format!("Syntax error: {}", ret)));
|
||||
} else if ret.starts_with("403 ") {
|
||||
return Err(Error::new(format!("Internal error: {}", ret)));
|
||||
return Err(MeliError::new(format!("Internal error: {}", ret)));
|
||||
} else if ret.starts_with("502 ")
|
||||
|| ret.starts_with("480 ")
|
||||
|| ret.starts_with("483 ")
|
||||
|| ret.starts_with("401 ")
|
||||
{
|
||||
return Err(Error::new(format!("Connection state error: {}", ret))
|
||||
return Err(MeliError::new(format!("Connection state error: {}", ret))
|
||||
.set_err_kind(ErrorKind::Authentication));
|
||||
} else if !expected_reply_code.iter().any(|r| ret.starts_with(r)) {
|
||||
return Err(Error::new(format!("Unexpected reply code: {}", ret)));
|
||||
return Err(MeliError::new(format!("Unexpected reply code: {}", ret)));
|
||||
}
|
||||
}
|
||||
if let Some(mut pos) = ret[last_line_idx..].rfind("\r\n") {
|
||||
|
@ -342,8 +342,8 @@ impl NntpStream {
|
|||
last_line_idx += pos + "\r\n".len();
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(Error::from(err));
|
||||
Err(e) => {
|
||||
return Err(MeliError::from(e).set_err_kind(crate::error::ErrorKind::Network));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -352,9 +352,6 @@ impl NntpStream {
|
|||
}
|
||||
|
||||
pub async fn send_command(&mut self, command: &[u8]) -> Result<()> {
|
||||
debug!("sending: {}", unsafe {
|
||||
std::str::from_utf8_unchecked(command)
|
||||
});
|
||||
if let Err(err) = try_await(async move {
|
||||
let command = command.trim();
|
||||
self.stream.write_all(command).await?;
|
||||
|
@ -368,30 +365,19 @@ impl NntpStream {
|
|||
.await
|
||||
{
|
||||
debug!("stream send_command err {:?}", err);
|
||||
Err(err)
|
||||
Err(err.set_err_kind(crate::error::ErrorKind::Network))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_multiline_data_block(&mut self, data: &[u8]) -> Result<()> {
|
||||
pub async fn send_multiline_data_block(&mut self, data: &str) -> Result<()> {
|
||||
if let Err(err) = try_await(async move {
|
||||
let mut ptr = 0;
|
||||
while let Some(pos) = data[ptr..].find("\n") {
|
||||
let l = &data[ptr..ptr + pos].trim_end();
|
||||
if l.starts_with(b".") {
|
||||
for l in data.lines() {
|
||||
if l.starts_with('.') {
|
||||
self.stream.write_all(b".").await?;
|
||||
}
|
||||
self.stream.write_all(l).await?;
|
||||
self.stream.write_all(b"\r\n").await?;
|
||||
ptr += pos + 1;
|
||||
}
|
||||
let l = &data[ptr..].trim_end();
|
||||
if !l.is_empty() {
|
||||
if l.starts_with(b".") {
|
||||
self.stream.write_all(b".").await?;
|
||||
}
|
||||
self.stream.write_all(l).await?;
|
||||
self.stream.write_all(l.as_bytes()).await?;
|
||||
self.stream.write_all(b"\r\n").await?;
|
||||
}
|
||||
self.stream.write_all(b".\r\n").await?;
|
||||
|
@ -402,7 +388,7 @@ impl NntpStream {
|
|||
.await
|
||||
{
|
||||
debug!("stream send_multiline_data_block err {:?}", err);
|
||||
Err(err)
|
||||
Err(err.set_err_kind(crate::error::ErrorKind::Network))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
|
@ -410,9 +396,12 @@ impl NntpStream {
|
|||
}
|
||||
|
||||
impl NntpConnection {
|
||||
pub fn new_connection(server_conf: &NntpServerConf, uid_store: Arc<UIDStore>) -> Self {
|
||||
Self {
|
||||
stream: Err(Error::new("Offline".to_string())),
|
||||
pub fn new_connection(
|
||||
server_conf: &NntpServerConf,
|
||||
uid_store: Arc<UIDStore>,
|
||||
) -> NntpConnection {
|
||||
NntpConnection {
|
||||
stream: Err(MeliError::new("Offline".to_string())),
|
||||
server_conf: server_conf.clone(),
|
||||
uid_store,
|
||||
}
|
||||
|
@ -422,8 +411,8 @@ impl NntpConnection {
|
|||
Box::pin(async move {
|
||||
if let (instant, ref mut status @ Ok(())) = *self.uid_store.is_online.lock().unwrap() {
|
||||
if Instant::now().duration_since(instant) >= std::time::Duration::new(60 * 30, 0) {
|
||||
*status = Err(Error::new("Connection timed out"));
|
||||
self.stream = Err(Error::new("Connection timed out"));
|
||||
*status = Err(MeliError::new("Connection timed out"));
|
||||
self.stream = Err(MeliError::new("Connection timed out"));
|
||||
}
|
||||
}
|
||||
if self.stream.is_ok() {
|
||||
|
@ -522,7 +511,7 @@ impl NntpConnection {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_multiline_data_block(&mut self, message: &[u8]) -> Result<()> {
|
||||
pub async fn send_multiline_data_block(&mut self, message: &str) -> Result<()> {
|
||||
self.stream
|
||||
.as_mut()?
|
||||
.send_multiline_data_block(message)
|
||||
|
|
|
@ -18,16 +18,11 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::{
|
||||
backends::{
|
||||
BackendMailbox, LazyCountSet, Mailbox, MailboxHash, MailboxPermissions, SpecialUsageMailbox,
|
||||
},
|
||||
error::*,
|
||||
UnixTimestamp,
|
||||
use crate::backends::{
|
||||
BackendMailbox, LazyCountSet, Mailbox, MailboxHash, MailboxPermissions, SpecialUsageMailbox,
|
||||
};
|
||||
use crate::error::*;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct NntpMailbox {
|
||||
|
@ -39,8 +34,6 @@ pub struct NntpMailbox {
|
|||
|
||||
pub exists: Arc<Mutex<LazyCountSet>>,
|
||||
pub unseen: Arc<Mutex<LazyCountSet>>,
|
||||
|
||||
pub latest_article: Arc<Mutex<Option<UnixTimestamp>>>,
|
||||
}
|
||||
|
||||
impl NntpMailbox {
|
||||
|
@ -62,6 +55,10 @@ impl BackendMailbox for NntpMailbox {
|
|||
&self.nntp_path
|
||||
}
|
||||
|
||||
fn change_name(&mut self, s: &str) {
|
||||
self.nntp_path = s.to_string();
|
||||
}
|
||||
|
||||
fn children(&self) -> &[MailboxHash] {
|
||||
&[]
|
||||
}
|
||||
|
@ -87,11 +84,11 @@ impl BackendMailbox for NntpMailbox {
|
|||
}
|
||||
|
||||
fn set_is_subscribed(&mut self, _new_val: bool) -> Result<()> {
|
||||
Err(Error::new("Cannot set subscription in NNTP."))
|
||||
Err(MeliError::new("Cannot set subscription in NNTP."))
|
||||
}
|
||||
|
||||
fn set_special_usage(&mut self, _new_val: SpecialUsageMailbox) -> Result<()> {
|
||||
Err(Error::new("Cannot set special usage in NNTP."))
|
||||
Err(MeliError::new("Cannot set special usage in NNTP."))
|
||||
}
|
||||
|
||||
fn count(&self) -> Result<(usize, usize)> {
|
||||
|
|
|
@ -19,10 +19,12 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use crate::{backends::*, email::*, error::Error};
|
||||
|
||||
use crate::backends::*;
|
||||
use crate::email::*;
|
||||
use crate::error::MeliError;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// `BackendOp` implementor for Nntp
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -40,7 +42,7 @@ impl NntpOp {
|
|||
connection: Arc<FutureMutex<NntpConnection>>,
|
||||
uid_store: Arc<UIDStore>,
|
||||
) -> Self {
|
||||
Self {
|
||||
NntpOp {
|
||||
uid,
|
||||
connection,
|
||||
mailbox_hash,
|
||||
|
@ -65,7 +67,7 @@ impl BackendOp for NntpOp {
|
|||
.await?;
|
||||
conn.read_response(&mut res, false, &["211 "]).await?;
|
||||
if !res.starts_with("211 ") {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"{} Could not select newsgroup {}: expected GROUP response but got: {}",
|
||||
&uid_store.account_name, path, res
|
||||
)));
|
||||
|
@ -74,7 +76,7 @@ impl BackendOp for NntpOp {
|
|||
.await?;
|
||||
conn.read_response(&mut res, true, &["220 "]).await?;
|
||||
if !res.starts_with("220 ") {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"{} Could not select article {}: expected ARTICLE response but got: {}",
|
||||
&uid_store.account_name, path, res
|
||||
)));
|
||||
|
|
|
@ -19,15 +19,13 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
use crate::email::parser::IResult;
|
||||
use nom::{
|
||||
bytes::complete::{is_not, tag},
|
||||
combinator::opt,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::email::parser::IResult;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub struct NntpLineIterator<'a> {
|
||||
slice: &'a str,
|
||||
|
@ -121,7 +119,7 @@ pub fn over_article(input: &str) -> IResult<&str, (UID, Envelope)> {
|
|||
let mut hasher = DefaultHasher::new();
|
||||
hasher.write(num.as_bytes());
|
||||
hasher.write(message_id.unwrap_or_default().as_bytes());
|
||||
EnvelopeHash(hasher.finish())
|
||||
hasher.finish()
|
||||
};
|
||||
let mut env = Envelope::new(env_hash);
|
||||
if let Some(date) = date {
|
||||
|
@ -146,6 +144,15 @@ pub fn over_article(input: &str) -> IResult<&str, (UID, Envelope)> {
|
|||
}
|
||||
|
||||
if let Some(references) = references {
|
||||
{
|
||||
if let Ok((_, r)) =
|
||||
crate::email::parser::address::msg_id_list(references.as_bytes())
|
||||
{
|
||||
for v in r {
|
||||
env.push_references(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
env.set_references(references.as_bytes());
|
||||
}
|
||||
|
||||
|
|
|
@ -19,25 +19,23 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
collections::{hash_map::HashMap, BTreeMap},
|
||||
ffi::{CStr, CString, OsStr},
|
||||
io::Read,
|
||||
os::unix::ffi::OsStrExt,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
};
|
||||
|
||||
use crate::backends::*;
|
||||
use crate::conf::AccountSettings;
|
||||
use crate::email::{Envelope, EnvelopeHash, Flag};
|
||||
use crate::error::{MeliError, Result};
|
||||
use crate::shellexpand::ShellExpandTrait;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
backends::*,
|
||||
conf::AccountSettings,
|
||||
email::{Envelope, EnvelopeHash, Flag},
|
||||
error::{Error, Result},
|
||||
utils::shellexpand::ShellExpandTrait,
|
||||
Collection,
|
||||
use std::collections::{
|
||||
hash_map::{DefaultHasher, HashMap},
|
||||
BTreeMap,
|
||||
};
|
||||
use std::error::Error;
|
||||
use std::ffi::{CStr, CString, OsStr};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::Read;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
macro_rules! call {
|
||||
($lib:expr, $func:ty) => {{
|
||||
|
@ -71,7 +69,6 @@ pub use thread::*;
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct DbConnection {
|
||||
#[allow(dead_code)]
|
||||
pub lib: Arc<libloading::Library>,
|
||||
pub inner: Arc<RwLock<*mut notmuch_database_t>>,
|
||||
pub revision_uuid: Arc<RwLock<u64>>,
|
||||
|
@ -88,13 +85,12 @@ impl DbConnection {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)] // Don't judge me clippy.
|
||||
fn refresh(
|
||||
&mut self,
|
||||
mailboxes: Arc<RwLock<HashMap<MailboxHash, NotmuchMailbox>>>,
|
||||
index: Arc<RwLock<HashMap<EnvelopeHash, CString>>>,
|
||||
mailbox_index: Arc<RwLock<HashMap<EnvelopeHash, SmallVec<[MailboxHash; 16]>>>>,
|
||||
tag_index: Arc<RwLock<BTreeMap<TagHash, String>>>,
|
||||
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
|
||||
account_hash: AccountHash,
|
||||
event_consumer: BackendEventConsumer,
|
||||
new_revision_uuid: u64,
|
||||
|
@ -105,7 +101,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();
|
||||
|
@ -115,8 +111,12 @@ impl DbConnection {
|
|||
let tags: (Flag, Vec<String>) = message.tags().collect_flags_and_tags();
|
||||
let mut tag_lock = tag_index.write().unwrap();
|
||||
for tag in tags.1.iter() {
|
||||
let num = TagHash::from_bytes(tag.as_bytes());
|
||||
tag_lock.entry(num).or_insert_with(|| tag.clone());
|
||||
let mut hasher = DefaultHasher::new();
|
||||
hasher.write(tag.as_bytes());
|
||||
let num = hasher.finish();
|
||||
if !tag_lock.contains_key(&num) {
|
||||
tag_lock.insert(num, tag.clone());
|
||||
}
|
||||
}
|
||||
for &mailbox_hash in mailbox_hashes {
|
||||
(event_consumer)(
|
||||
|
@ -155,7 +155,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];
|
||||
|
@ -191,8 +191,8 @@ impl std::fmt::Display for NotmuchError {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for NotmuchError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
impl Error for NotmuchError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
@ -215,15 +215,14 @@ impl Drop for DbConnection {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct NotmuchDb {
|
||||
#[allow(dead_code)]
|
||||
lib: Arc<libloading::Library>,
|
||||
revision_uuid: Arc<RwLock<u64>>,
|
||||
mailboxes: Arc<RwLock<HashMap<MailboxHash, NotmuchMailbox>>>,
|
||||
index: Arc<RwLock<HashMap<EnvelopeHash, CString>>>,
|
||||
mailbox_index: Arc<RwLock<HashMap<EnvelopeHash, SmallVec<[MailboxHash; 16]>>>>,
|
||||
collection: Collection,
|
||||
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
|
||||
path: PathBuf,
|
||||
_account_name: Arc<str>,
|
||||
account_name: Arc<String>,
|
||||
account_hash: AccountHash,
|
||||
event_consumer: BackendEventConsumer,
|
||||
save_messages_to: Option<PathBuf>,
|
||||
|
@ -259,6 +258,8 @@ impl BackendMailbox for NotmuchMailbox {
|
|||
self.path.as_str()
|
||||
}
|
||||
|
||||
fn change_name(&mut self, _s: &str) {}
|
||||
|
||||
fn clone(&self) -> Mailbox {
|
||||
Box::new(std::clone::Clone::clone(self))
|
||||
}
|
||||
|
@ -301,80 +302,29 @@ unsafe impl Send for NotmuchMailbox {}
|
|||
unsafe impl Sync for NotmuchMailbox {}
|
||||
|
||||
impl NotmuchDb {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(
|
||||
s: &AccountSettings,
|
||||
_is_subscribed: Box<dyn Fn(&str) -> bool>,
|
||||
event_consumer: BackendEventConsumer,
|
||||
) -> Result<Box<dyn MailBackend>> {
|
||||
#[cfg(target_os = "linux")]
|
||||
let mut dlpath = "libnotmuch.so.5";
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut dlpath = "libnotmuch.5.dylib";
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
let mut dlpath = "libnotmuch.so";
|
||||
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(Error::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(Error::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();
|
||||
let lib = Arc::new(libloading::Library::new("libnotmuch.so.5")?);
|
||||
let path = Path::new(s.root_mailbox.as_str()).expand();
|
||||
if !path.exists() {
|
||||
return Err(Error::new(format!(
|
||||
"Notmuch `root_mailbox` {} for account {} does not exist.",
|
||||
return Err(MeliError::new(format!(
|
||||
"\"root_mailbox\" {} for account {} is not a valid path.",
|
||||
s.root_mailbox.as_str(),
|
||||
s.name
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
s.name()
|
||||
)));
|
||||
}
|
||||
if !path.is_dir() {
|
||||
return Err(Error::new(format!(
|
||||
"Notmuch `root_mailbox` {} for account {} is not a directory.",
|
||||
s.root_mailbox.as_str(),
|
||||
s.name
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
}
|
||||
path.push(".notmuch");
|
||||
if !path.exists() || !path.is_dir() {
|
||||
return Err(Error::new(format!(
|
||||
"Notmuch `root_mailbox` {} for account {} does not contain a `.notmuch` \
|
||||
subdirectory.",
|
||||
s.root_mailbox.as_str(),
|
||||
s.name
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
}
|
||||
path.pop();
|
||||
|
||||
let mut mailboxes = HashMap::with_capacity(s.mailboxes.len());
|
||||
let mut parents: Vec<(MailboxHash, &str)> = Vec::with_capacity(s.mailboxes.len());
|
||||
let mut mailboxes = HashMap::default();
|
||||
for (k, f) in s.mailboxes.iter() {
|
||||
if let Some(query_str) = f.extra.get("query") {
|
||||
let hash = MailboxHash::from_bytes(k.as_bytes());
|
||||
if let Some(parent) = f.extra.get("parent") {
|
||||
parents.push((hash, parent));
|
||||
}
|
||||
let hash = {
|
||||
let mut h = DefaultHasher::new();
|
||||
k.hash(&mut h);
|
||||
h.finish()
|
||||
};
|
||||
mailboxes.insert(
|
||||
hash,
|
||||
NotmuchMailbox {
|
||||
|
@ -390,132 +340,49 @@ impl NotmuchDb {
|
|||
},
|
||||
);
|
||||
} else {
|
||||
return Err(Error::new(format!(
|
||||
"notmuch mailbox configuration entry `{}` for account {} should have a \
|
||||
`query` value set.",
|
||||
k, s.name,
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
}
|
||||
}
|
||||
for (hash, parent) in parents {
|
||||
if let Some(&parent_hash) = mailboxes
|
||||
.iter()
|
||||
.find(|(_, v)| v.name == parent)
|
||||
.map(|(k, _)| k)
|
||||
{
|
||||
mailboxes
|
||||
.entry(parent_hash)
|
||||
.or_default()
|
||||
.children
|
||||
.push(hash);
|
||||
mailboxes.entry(hash).or_default().parent = Some(parent_hash);
|
||||
} else {
|
||||
return Err(Error::new(format!(
|
||||
"Mailbox configuration for `{}` defines its parent mailbox as `{}` but no \
|
||||
mailbox exists with this exact name.",
|
||||
mailboxes[&hash].name(),
|
||||
parent
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
return Err(MeliError::new(format!(
|
||||
"notmuch mailbox configuration entry \"{}\" should have a \"query\" value set.",
|
||||
k
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let account_hash = AccountHash::from_bytes(s.name.as_bytes());
|
||||
Ok(Box::new(Self {
|
||||
let account_hash = {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
hasher.write(s.name().as_bytes());
|
||||
hasher.finish()
|
||||
};
|
||||
Ok(Box::new(NotmuchDb {
|
||||
lib,
|
||||
revision_uuid: Arc::new(RwLock::new(0)),
|
||||
path,
|
||||
index: Arc::new(RwLock::new(Default::default())),
|
||||
mailbox_index: Arc::new(RwLock::new(Default::default())),
|
||||
collection: Collection::default(),
|
||||
tag_index: Arc::new(RwLock::new(Default::default())),
|
||||
|
||||
mailboxes: Arc::new(RwLock::new(mailboxes)),
|
||||
save_messages_to: None,
|
||||
_account_name: s.name.to_string().into(),
|
||||
account_name: Arc::new(s.name().to_string()),
|
||||
account_hash,
|
||||
event_consumer,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn validate_config(s: &mut AccountSettings) -> Result<()> {
|
||||
let mut path = Path::new(s.root_mailbox.as_str()).expand();
|
||||
pub fn validate_config(s: &AccountSettings) -> Result<()> {
|
||||
let path = Path::new(s.root_mailbox.as_str()).expand();
|
||||
if !path.exists() {
|
||||
return Err(Error::new(format!(
|
||||
"Notmuch `root_mailbox` {} for account {} does not exist.",
|
||||
return Err(MeliError::new(format!(
|
||||
"\"root_mailbox\" {} for account {} is not a valid path.",
|
||||
s.root_mailbox.as_str(),
|
||||
s.name
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
s.name()
|
||||
)));
|
||||
}
|
||||
if !path.is_dir() {
|
||||
return Err(Error::new(format!(
|
||||
"Notmuch `root_mailbox` {} for account {} is not a directory.",
|
||||
s.root_mailbox.as_str(),
|
||||
s.name
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
}
|
||||
path.push(".notmuch");
|
||||
if !path.exists() || !path.is_dir() {
|
||||
return Err(Error::new(format!(
|
||||
"Notmuch `root_mailbox` {} for account {} does not contain a `.notmuch` \
|
||||
subdirectory.",
|
||||
s.root_mailbox.as_str(),
|
||||
s.name
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
}
|
||||
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(Error::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));
|
||||
}
|
||||
}
|
||||
let mut parents: Vec<(String, String)> = Vec::with_capacity(s.mailboxes.len());
|
||||
for (k, f) in s.mailboxes.iter_mut() {
|
||||
if f.extra.remove("query").is_none() {
|
||||
return Err(Error::new(format!(
|
||||
"notmuch mailbox configuration entry `{}` for account {} should have a \
|
||||
`query` value set.",
|
||||
k, account_name,
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
}
|
||||
if let Some(parent) = f.extra.remove("parent") {
|
||||
parents.push((k.clone(), parent));
|
||||
}
|
||||
}
|
||||
let mut path = Vec::with_capacity(8);
|
||||
for (mbox, parent) in parents.iter() {
|
||||
if !s.mailboxes.contains_key(parent) {
|
||||
return Err(Error::new(format!(
|
||||
"Mailbox configuration for `{}` defines its parent mailbox as `{}` but no \
|
||||
mailbox exists with this exact name.",
|
||||
mbox, parent
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
}
|
||||
path.clear();
|
||||
path.push(mbox.as_str());
|
||||
let mut iter = parent.as_str();
|
||||
while let Some((k, v)) = parents.iter().find(|(k, _v)| k == iter) {
|
||||
if k == mbox {
|
||||
return Err(Error::new(format!(
|
||||
"Found cycle in mailbox hierarchy: {}",
|
||||
path.join("->")
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
}
|
||||
path.push(k.as_str());
|
||||
iter = v.as_str();
|
||||
for (k, f) in s.mailboxes.iter() {
|
||||
if f.extra.get("query").is_none() {
|
||||
return Err(MeliError::new(format!(
|
||||
"notmuch mailbox configuration entry \"{}\" should have a \"query\" value set.",
|
||||
k
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
@ -538,11 +405,11 @@ impl NotmuchDb {
|
|||
} else {
|
||||
notmuch_database_mode_t_NOTMUCH_DATABASE_MODE_READ_ONLY
|
||||
},
|
||||
std::ptr::addr_of_mut!(database),
|
||||
&mut database as *mut _,
|
||||
)
|
||||
};
|
||||
if status != 0 {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"Could not open notmuch database at path {}. notmuch_database_open returned {}.",
|
||||
path.display(),
|
||||
status
|
||||
|
@ -589,8 +456,8 @@ impl MailBackend for NotmuchDb {
|
|||
database: Arc<DbConnection>,
|
||||
index: Arc<RwLock<HashMap<EnvelopeHash, CString>>>,
|
||||
mailbox_index: Arc<RwLock<HashMap<EnvelopeHash, SmallVec<[MailboxHash; 16]>>>>,
|
||||
mailboxes: Arc<RwLock<HashMap<MailboxHash, NotmuchMailbox>>>,
|
||||
tag_index: Arc<RwLock<BTreeMap<TagHash, String>>>,
|
||||
mailboxes: Arc<RwLock<HashMap<u64, NotmuchMailbox>>>,
|
||||
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
|
||||
iter: std::vec::IntoIter<CString>,
|
||||
}
|
||||
impl FetchState {
|
||||
|
@ -635,7 +502,7 @@ impl MailBackend for NotmuchDb {
|
|||
}
|
||||
}
|
||||
}
|
||||
let database = Arc::new(Self::new_connection(
|
||||
let database = Arc::new(NotmuchDb::new_connection(
|
||||
self.path.as_path(),
|
||||
self.revision_uuid.clone(),
|
||||
self.lib.clone(),
|
||||
|
@ -643,7 +510,7 @@ impl MailBackend for NotmuchDb {
|
|||
)?);
|
||||
let index = self.index.clone();
|
||||
let mailbox_index = self.mailbox_index.clone();
|
||||
let tag_index = self.collection.tag_index.clone();
|
||||
let tag_index = self.tag_index.clone();
|
||||
let mailboxes = self.mailboxes.clone();
|
||||
let v: Vec<CString>;
|
||||
{
|
||||
|
@ -659,6 +526,7 @@ impl MailBackend for NotmuchDb {
|
|||
let mut index_lck = index.write().unwrap();
|
||||
v = query
|
||||
.search()?
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
index_lck.insert(m.env_hash(), m.msg_id_cstr().into());
|
||||
m.msg_id_cstr().into()
|
||||
|
@ -684,7 +552,7 @@ impl MailBackend for NotmuchDb {
|
|||
|
||||
fn refresh(&mut self, _mailbox_hash: MailboxHash) -> ResultFuture<()> {
|
||||
let account_hash = self.account_hash;
|
||||
let mut database = Self::new_connection(
|
||||
let mut database = NotmuchDb::new_connection(
|
||||
self.path.as_path(),
|
||||
self.revision_uuid.clone(),
|
||||
self.lib.clone(),
|
||||
|
@ -693,7 +561,7 @@ impl MailBackend for NotmuchDb {
|
|||
let mailboxes = self.mailboxes.clone();
|
||||
let index = self.index.clone();
|
||||
let mailbox_index = self.mailbox_index.clone();
|
||||
let tag_index = self.collection.tag_index.clone();
|
||||
let tag_index = self.tag_index.clone();
|
||||
let event_consumer = self.event_consumer.clone();
|
||||
Ok(Box::pin(async move {
|
||||
let new_revision_uuid = database.get_revision_uuid();
|
||||
|
@ -718,13 +586,13 @@ impl MailBackend for NotmuchDb {
|
|||
use notify::{watcher, RecursiveMode, Watcher};
|
||||
|
||||
let account_hash = self.account_hash;
|
||||
let collection = self.collection.clone();
|
||||
let lib = self.lib.clone();
|
||||
let path = self.path.clone();
|
||||
let revision_uuid = self.revision_uuid.clone();
|
||||
let mailboxes = self.mailboxes.clone();
|
||||
let index = self.index.clone();
|
||||
let mailbox_index = self.mailbox_index.clone();
|
||||
let tag_index = self.tag_index.clone();
|
||||
let event_consumer = self.event_consumer.clone();
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
@ -736,7 +604,7 @@ impl MailBackend for NotmuchDb {
|
|||
loop {
|
||||
let _ = rx.recv().map_err(|err| err.to_string())?;
|
||||
{
|
||||
let mut database = Self::new_connection(
|
||||
let mut database = NotmuchDb::new_connection(
|
||||
path.as_path(),
|
||||
revision_uuid.clone(),
|
||||
lib.clone(),
|
||||
|
@ -748,8 +616,8 @@ impl MailBackend for NotmuchDb {
|
|||
mailboxes.clone(),
|
||||
index.clone(),
|
||||
mailbox_index.clone(),
|
||||
collection.tag_index.clone(),
|
||||
account_hash,
|
||||
tag_index.clone(),
|
||||
account_hash.clone(),
|
||||
event_consumer.clone(),
|
||||
new_revision_uuid,
|
||||
)?;
|
||||
|
@ -783,6 +651,7 @@ impl MailBackend for NotmuchDb {
|
|||
hash,
|
||||
index: self.index.clone(),
|
||||
bytes: None,
|
||||
tag_index: self.tag_index.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -809,9 +678,7 @@ impl MailBackend for NotmuchDb {
|
|||
_destination_mailbox_hash: MailboxHash,
|
||||
_move_: bool,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new(
|
||||
"Copying messages is currently unimplemented for notmuch backend",
|
||||
))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn set_flags(
|
||||
|
@ -826,7 +693,7 @@ impl MailBackend for NotmuchDb {
|
|||
self.lib.clone(),
|
||||
true,
|
||||
)?;
|
||||
let collection = self.collection.clone();
|
||||
let tag_index = self.tag_index.clone();
|
||||
let index = self.index.clone();
|
||||
|
||||
Ok(Box::pin(async move {
|
||||
|
@ -898,7 +765,7 @@ impl MailBackend for NotmuchDb {
|
|||
}
|
||||
Err(tag) => {
|
||||
let c_tag = CString::new(tag.as_str()).unwrap();
|
||||
remove_tag!(&c_tag.as_ref());
|
||||
add_tag!(&c_tag.as_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -913,12 +780,8 @@ impl MailBackend for NotmuchDb {
|
|||
}
|
||||
for (f, v) in flags.iter() {
|
||||
if let (Err(tag), true) = (f, v) {
|
||||
let hash = TagHash::from_bytes(tag.as_bytes());
|
||||
collection
|
||||
.tag_index
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(hash, tag.to_string());
|
||||
let hash = tag_hash!(tag);
|
||||
tag_index.write().unwrap().insert(hash, tag.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -931,9 +794,7 @@ impl MailBackend for NotmuchDb {
|
|||
_env_hashes: EnvelopeHashBatch,
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new(
|
||||
"Deleting messages is currently unimplemented for notmuch backend",
|
||||
))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn search(
|
||||
|
@ -941,7 +802,7 @@ impl MailBackend for NotmuchDb {
|
|||
melib_query: crate::search::Query,
|
||||
mailbox_hash: Option<MailboxHash>,
|
||||
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>> {
|
||||
let database = Self::new_connection(
|
||||
let database = NotmuchDb::new_connection(
|
||||
self.path.as_path(),
|
||||
self.revision_uuid.clone(),
|
||||
self.lib.clone(),
|
||||
|
@ -956,11 +817,8 @@ impl MailBackend for NotmuchDb {
|
|||
s.push(' ');
|
||||
s
|
||||
} else {
|
||||
return Err(Error::new(format!(
|
||||
"Mailbox with hash {} not found!",
|
||||
mailbox_hash
|
||||
))
|
||||
.set_kind(crate::error::ErrorKind::Bug));
|
||||
return Err(MeliError::new("Mailbox with hash {} not found!")
|
||||
.set_kind(crate::error::ErrorKind::Bug));
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
|
@ -976,8 +834,8 @@ impl MailBackend for NotmuchDb {
|
|||
}))
|
||||
}
|
||||
|
||||
fn collection(&self) -> Collection {
|
||||
self.collection.clone()
|
||||
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
|
||||
Some(self.tag_index.clone())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
|
@ -987,64 +845,15 @@ impl MailBackend for NotmuchDb {
|
|||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn delete_mailbox(
|
||||
&mut self,
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
|
||||
Err(Error::new(
|
||||
"Deleting mailboxes is currently unimplemented for notmuch backend.",
|
||||
))
|
||||
}
|
||||
|
||||
fn set_mailbox_subscription(
|
||||
&mut self,
|
||||
_mailbox_hash: MailboxHash,
|
||||
_val: bool,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new(
|
||||
"Mailbox subscriptions are not possible for the notmuch backend.",
|
||||
))
|
||||
}
|
||||
|
||||
fn rename_mailbox(
|
||||
&mut self,
|
||||
_mailbox_hash: MailboxHash,
|
||||
_new_path: String,
|
||||
) -> ResultFuture<Mailbox> {
|
||||
Err(Error::new(
|
||||
"Renaming mailboxes is currently unimplemented for notmuch backend.",
|
||||
))
|
||||
}
|
||||
|
||||
fn set_mailbox_permissions(
|
||||
&mut self,
|
||||
_mailbox_hash: MailboxHash,
|
||||
_val: crate::backends::MailboxPermissions,
|
||||
) -> ResultFuture<()> {
|
||||
Err(Error::new(
|
||||
"Setting mailbox permissions is not possible for the notmuch backend.",
|
||||
))
|
||||
}
|
||||
|
||||
fn create_mailbox(
|
||||
&mut self,
|
||||
_new_path: String,
|
||||
) -> ResultFuture<(MailboxHash, HashMap<MailboxHash, Mailbox>)> {
|
||||
Err(
|
||||
Error::new("Creating mailboxes is unimplemented for the notmuch backend.")
|
||||
.set_kind(ErrorKind::NotImplemented),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct NotmuchOp {
|
||||
hash: EnvelopeHash,
|
||||
index: Arc<RwLock<HashMap<EnvelopeHash, CString>>>,
|
||||
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
|
||||
database: Arc<DbConnection>,
|
||||
bytes: Option<Vec<u8>>,
|
||||
#[allow(dead_code)]
|
||||
lib: Arc<libloading::Library>,
|
||||
}
|
||||
|
||||
|
@ -1069,7 +878,6 @@ impl BackendOp for NotmuchOp {
|
|||
}
|
||||
|
||||
pub struct Query<'s> {
|
||||
#[allow(dead_code)]
|
||||
lib: Arc<libloading::Library>,
|
||||
ptr: *mut notmuch_query_t,
|
||||
query_str: &'s str,
|
||||
|
@ -1083,7 +891,7 @@ impl<'s> Query<'s> {
|
|||
call!(lib, notmuch_query_create)(*database.inner.read().unwrap(), query_cstr.as_ptr())
|
||||
};
|
||||
if query.is_null() {
|
||||
return Err(Error::new("Could not create query. Out of memory?"));
|
||||
return Err(MeliError::new("Could not create query. Out of memory?"));
|
||||
}
|
||||
Ok(Query {
|
||||
lib,
|
||||
|
@ -1097,10 +905,7 @@ impl<'s> Query<'s> {
|
|||
unsafe {
|
||||
try_call!(
|
||||
self.lib,
|
||||
call!(self.lib, notmuch_query_count_messages)(
|
||||
self.ptr,
|
||||
std::ptr::addr_of_mut!(count)
|
||||
)
|
||||
call!(self.lib, notmuch_query_count_messages)(self.ptr, &mut count as *mut _)
|
||||
)
|
||||
.map_err(|err| err.0)?;
|
||||
}
|
||||
|
@ -1110,13 +915,10 @@ impl<'s> Query<'s> {
|
|||
fn search(&'s self) -> Result<MessageIterator<'s>> {
|
||||
let mut messages: *mut notmuch_messages_t = std::ptr::null_mut();
|
||||
let status = unsafe {
|
||||
call!(self.lib, notmuch_query_search_messages)(
|
||||
self.ptr,
|
||||
std::ptr::addr_of_mut!(messages),
|
||||
)
|
||||
call!(self.lib, notmuch_query_search_messages)(self.ptr, &mut messages as *mut _)
|
||||
};
|
||||
if status != 0 {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"Search for {} returned {}",
|
||||
self.query_str, status,
|
||||
)));
|
||||
|
@ -1176,7 +978,7 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
|
|||
ret.push(c);
|
||||
}
|
||||
}
|
||||
ret.push('"');
|
||||
ret.push_str("\"");
|
||||
}
|
||||
To(s) | Cc(s) | Bcc(s) => {
|
||||
ret.push_str("to:\"");
|
||||
|
@ -1187,7 +989,7 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
|
|||
ret.push(c);
|
||||
}
|
||||
}
|
||||
ret.push('"');
|
||||
ret.push_str("\"");
|
||||
}
|
||||
InReplyTo(_s) | References(_s) | AllAddresses(_s) => {}
|
||||
/* * * * */
|
||||
|
@ -1200,7 +1002,7 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
|
|||
ret.push(c);
|
||||
}
|
||||
}
|
||||
ret.push('"');
|
||||
ret.push_str("\"");
|
||||
}
|
||||
Subject(s) => {
|
||||
ret.push_str("subject:\"");
|
||||
|
@ -1211,10 +1013,10 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
|
|||
ret.push(c);
|
||||
}
|
||||
}
|
||||
ret.push('"');
|
||||
ret.push_str("\"");
|
||||
}
|
||||
AllText(s) => {
|
||||
ret.push('"');
|
||||
ret.push_str("\"");
|
||||
for c in s.chars() {
|
||||
if c == '"' {
|
||||
ret.push_str("\\\"");
|
||||
|
@ -1222,7 +1024,7 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
|
|||
ret.push(c);
|
||||
}
|
||||
}
|
||||
ret.push('"');
|
||||
ret.push_str("\"");
|
||||
}
|
||||
/* * * * */
|
||||
Flags(v) => {
|
||||
|
@ -1245,28 +1047,24 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
|
|||
ret.push_str("tag:attachment");
|
||||
}
|
||||
And(q1, q2) => {
|
||||
ret.push('(');
|
||||
ret.push_str("(");
|
||||
q1.query_to_string(ret);
|
||||
ret.push_str(") AND (");
|
||||
q2.query_to_string(ret);
|
||||
ret.push(')');
|
||||
ret.push_str(")");
|
||||
}
|
||||
Or(q1, q2) => {
|
||||
ret.push('(');
|
||||
ret.push_str("(");
|
||||
q1.query_to_string(ret);
|
||||
ret.push_str(") OR (");
|
||||
q2.query_to_string(ret);
|
||||
ret.push(')');
|
||||
ret.push_str(")");
|
||||
}
|
||||
Not(q) => {
|
||||
ret.push_str("(NOT (");
|
||||
q.query_to_string(ret);
|
||||
ret.push_str("))");
|
||||
}
|
||||
Answered => todo!(),
|
||||
AnsweredBy { .. } => todo!(),
|
||||
Larger { .. } => todo!(),
|
||||
Smaller { .. } => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,11 +16,9 @@ pub const _notmuch_status_NOTMUCH_STATUS_OUT_OF_MEMORY: _notmuch_status = 1;
|
|||
pub const _notmuch_status_NOTMUCH_STATUS_READ_ONLY_DATABASE: _notmuch_status = 2;
|
||||
/// A Xapian exception occurred.
|
||||
///
|
||||
/// ```text
|
||||
/// @todo We don't really want to expose this lame XAPIAN_EXCEPTION
|
||||
/// value. Instead we should map to things like DATABASE_LOCKED or
|
||||
/// whatever.
|
||||
/// ```
|
||||
pub const _notmuch_status_NOTMUCH_STATUS_XAPIAN_EXCEPTION: _notmuch_status = 3;
|
||||
/// An error occurred trying to read or write to a file (this could
|
||||
/// be file not found, permission denied, etc.)
|
||||
|
@ -244,6 +242,7 @@ pub type notmuch_database_open_verbose = unsafe extern "C" fn(
|
|||
) -> notmuch_status_t;
|
||||
|
||||
/// Retrieve last status string for given database.
|
||||
///
|
||||
pub type notmuch_database_status_string =
|
||||
unsafe extern "C" fn(notmuch: *const notmuch_database_t) -> *const ::std::os::raw::c_char;
|
||||
|
||||
|
@ -467,7 +466,6 @@ pub type notmuch_database_get_directory = unsafe extern "C" fn(
|
|||
///
|
||||
/// Return value:
|
||||
///
|
||||
/// ```text
|
||||
/// NOTMUCH_STATUS_SUCCESS: Message successfully added to database.
|
||||
///
|
||||
/// NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred,
|
||||
|
@ -492,7 +490,6 @@ pub type notmuch_database_get_directory = unsafe extern "C" fn(
|
|||
/// database to use this function.
|
||||
///
|
||||
/// @since libnotmuch 5.1 (notmuch 0.26)
|
||||
/// ```
|
||||
pub type notmuch_database_index_file = unsafe extern "C" fn(
|
||||
database: *mut notmuch_database_t,
|
||||
filename: *const ::std::os::raw::c_char,
|
||||
|
@ -504,10 +501,9 @@ 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,
|
||||
filename: *const ::std::os::raw::c_char,
|
||||
|
@ -620,7 +616,7 @@ pub type notmuch_database_get_all_tags =
|
|||
/// completely in the future, but it's likely to be a specialized
|
||||
/// version of the general Xapian query syntax:
|
||||
///
|
||||
/// <https://xapian.org/docs/queryparser.html>
|
||||
/// https://xapian.org/docs/queryparser.html
|
||||
///
|
||||
/// As a special case, passing either a length-zero string, (that is ""),
|
||||
/// or a string consisting of a single asterisk (that is "*"), will
|
||||
|
@ -707,9 +703,7 @@ pub type notmuch_query_get_sort =
|
|||
/// This exclusion will be ignored if this tag appears explicitly in
|
||||
/// the query.
|
||||
///
|
||||
/// ```text
|
||||
/// @returns
|
||||
/// ```
|
||||
///
|
||||
/// NOTMUCH_STATUS_SUCCESS: excluded was added successfully.
|
||||
///
|
||||
|
@ -749,7 +743,7 @@ pub type notmuch_query_add_tag_exclude = unsafe extern "C" fn(
|
|||
/// }
|
||||
///
|
||||
/// notmuch_query_destroy (query);
|
||||
/// ```
|
||||
///```
|
||||
///
|
||||
/// Note: If you are finished with a thread before its containing
|
||||
/// query, you can call notmuch_thread_destroy to clean up some memory
|
||||
|
@ -763,9 +757,7 @@ pub type notmuch_query_add_tag_exclude = unsafe extern "C" fn(
|
|||
/// notmuch_threads_destroy function, but there's no good reason
|
||||
/// to call it if the query is about to be destroyed).
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 5.0 (notmuch 0.25)
|
||||
/// ```
|
||||
pub type notmuch_query_search_threads = unsafe extern "C" fn(
|
||||
query: *mut notmuch_query_t,
|
||||
out: *mut *mut notmuch_threads_t,
|
||||
|
@ -773,10 +765,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(
|
||||
query: *mut notmuch_query_t,
|
||||
out: *mut *mut notmuch_threads_t,
|
||||
|
@ -806,7 +797,7 @@ pub type notmuch_query_search_threads_st = unsafe extern "C" fn(
|
|||
/// }
|
||||
///
|
||||
/// notmuch_query_destroy (query);
|
||||
/// ```
|
||||
///```
|
||||
///
|
||||
/// Note: If you are finished with a message before its containing
|
||||
/// query, you can call notmuch_message_destroy to clean up some memory
|
||||
|
@ -822,9 +813,7 @@ pub type notmuch_query_search_threads_st = unsafe extern "C" fn(
|
|||
///
|
||||
/// If a Xapian exception occurs this function will return NULL.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 5 (notmuch 0.25)
|
||||
/// ```
|
||||
pub type notmuch_query_search_messages = unsafe extern "C" fn(
|
||||
query: *mut notmuch_query_t,
|
||||
out: *mut *mut notmuch_messages_t,
|
||||
|
@ -832,10 +821,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(
|
||||
query: *mut notmuch_query_t,
|
||||
out: *mut *mut notmuch_messages_t,
|
||||
|
@ -899,7 +887,6 @@ pub type notmuch_threads_destroy = unsafe extern "C" fn(threads: *mut notmuch_th
|
|||
/// This function performs a search and returns the number of matching
|
||||
/// messages.
|
||||
///
|
||||
/// ```text
|
||||
/// @returns
|
||||
///
|
||||
/// NOTMUCH_STATUS_SUCCESS: query completed successfully.
|
||||
|
@ -908,7 +895,6 @@ pub type notmuch_threads_destroy = unsafe extern "C" fn(threads: *mut notmuch_th
|
|||
/// value of *count is not defined.
|
||||
///
|
||||
/// @since libnotmuch 5 (notmuch 0.25)
|
||||
/// ```
|
||||
pub type notmuch_query_count_messages = unsafe extern "C" fn(
|
||||
query: *mut notmuch_query_t,
|
||||
count: *mut ::std::os::raw::c_uint,
|
||||
|
@ -916,10 +902,9 @@ pub type notmuch_query_count_messages = unsafe extern "C" fn(
|
|||
|
||||
/// Deprecated alias for notmuch_query_count_messages
|
||||
///
|
||||
/// ```text
|
||||
///
|
||||
/// @deprecated Deprecated since libnotmuch 5.0 (notmuch 0.25). Please
|
||||
/// use notmuch_query_count_messages instead.
|
||||
/// ```
|
||||
pub type notmuch_query_count_messages_st = unsafe extern "C" fn(
|
||||
query: *mut notmuch_query_t,
|
||||
count: *mut ::std::os::raw::c_uint,
|
||||
|
@ -934,7 +919,6 @@ pub type notmuch_query_count_messages_st = unsafe extern "C" fn(
|
|||
/// Note that this is a significantly heavier operation than
|
||||
/// notmuch_query_count_messages{_st}().
|
||||
///
|
||||
/// ```text
|
||||
/// @returns
|
||||
///
|
||||
/// NOTMUCH_STATUS_OUT_OF_MEMORY: Memory allocation failed. The value
|
||||
|
@ -946,7 +930,6 @@ pub type notmuch_query_count_messages_st = unsafe extern "C" fn(
|
|||
/// value of *count is not defined.
|
||||
///
|
||||
/// @since libnotmuch 5 (notmuch 0.25)
|
||||
/// ```
|
||||
pub type notmuch_query_count_threads = unsafe extern "C" fn(
|
||||
query: *mut notmuch_query_t,
|
||||
count: *mut ::std::os::raw::c_uint,
|
||||
|
@ -954,10 +937,8 @@ pub type notmuch_query_count_threads = unsafe extern "C" fn(
|
|||
|
||||
/// Deprecated alias for notmuch_query_count_threads
|
||||
///
|
||||
/// ```text
|
||||
/// @deprecated Deprecated as of libnotmuch 5.0 (notmuch 0.25). Please
|
||||
/// use notmuch_query_count_threads_st instead.
|
||||
/// ```
|
||||
pub type notmuch_query_count_threads_st = unsafe extern "C" fn(
|
||||
query: *mut notmuch_query_t,
|
||||
count: *mut ::std::os::raw::c_uint,
|
||||
|
@ -983,10 +964,8 @@ pub type notmuch_thread_get_total_messages =
|
|||
///
|
||||
/// This sums notmuch_message_count_files over all messages in the
|
||||
/// thread
|
||||
/// ```text
|
||||
/// @returns Non-negative integer
|
||||
/// @since libnotmuch 5.0 (notmuch 0.25)
|
||||
/// ```
|
||||
pub type notmuch_thread_get_total_files =
|
||||
unsafe extern "C" fn(thread: *mut notmuch_thread_t) -> ::std::os::raw::c_int;
|
||||
|
||||
|
@ -1087,7 +1066,7 @@ pub type notmuch_thread_get_newest_date =
|
|||
/// }
|
||||
///
|
||||
/// notmuch_thread_destroy (thread);
|
||||
/// ```
|
||||
///```
|
||||
///
|
||||
/// Note that there's no explicit destructor needed for the
|
||||
/// notmuch_tags_t object. (For consistency, we do provide a
|
||||
|
@ -1157,9 +1136,7 @@ pub type notmuch_messages_collect_tags =
|
|||
|
||||
/// Get the database associated with this message.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 5.2 (notmuch 0.27)
|
||||
/// ```
|
||||
pub type notmuch_message_get_database =
|
||||
unsafe extern "C" fn(message: *const notmuch_message_t) -> *mut notmuch_database_t;
|
||||
|
||||
|
@ -1211,10 +1188,8 @@ pub type notmuch_message_get_replies =
|
|||
unsafe extern "C" fn(message: *mut notmuch_message_t) -> *mut notmuch_messages_t;
|
||||
|
||||
/// Get the total number of files associated with a message.
|
||||
/// ```text
|
||||
/// @returns Non-negative integer
|
||||
/// @since libnotmuch 5.0 (notmuch 0.25)
|
||||
/// ```
|
||||
pub type notmuch_message_count_files =
|
||||
unsafe extern "C" fn(message: *mut notmuch_message_t) -> ::std::os::raw::c_int;
|
||||
|
||||
|
@ -1246,8 +1221,7 @@ pub type notmuch_message_get_filename =
|
|||
pub type notmuch_message_get_filenames =
|
||||
unsafe extern "C" fn(message: *mut notmuch_message_t) -> *mut notmuch_filenames_t;
|
||||
|
||||
/// Re-index the e-mail corresponding to 'message' using the supplied index
|
||||
/// options
|
||||
/// Re-index the e-mail corresponding to 'message' using the supplied index options
|
||||
///
|
||||
/// Returns the status of the re-index operation. (see the return
|
||||
/// codes documented in notmuch_database_index_file)
|
||||
|
@ -1330,7 +1304,7 @@ pub type notmuch_message_get_header = unsafe extern "C" fn(
|
|||
/// }
|
||||
///
|
||||
/// notmuch_message_destroy (message);
|
||||
/// ```
|
||||
///```
|
||||
///
|
||||
/// Note that there's no explicit destructor needed for the
|
||||
/// notmuch_tags_t object. (For consistency, we do provide a
|
||||
|
@ -1420,6 +1394,7 @@ pub type notmuch_message_maildir_flags_to_tags =
|
|||
|
||||
/// return TRUE if any filename of 'message' has maildir flag 'flag',
|
||||
/// FALSE otherwise.
|
||||
///
|
||||
pub type notmuch_message_has_maildir_flag = unsafe extern "C" fn(
|
||||
message: *mut notmuch_message_t,
|
||||
flag: ::std::os::raw::c_char,
|
||||
|
@ -1475,7 +1450,6 @@ pub type notmuch_message_tags_to_maildir_flags =
|
|||
/// change tag values. For example, explicitly setting a message to
|
||||
/// have a given set of tags might look like this:
|
||||
///
|
||||
/// ```c
|
||||
/// notmuch_message_freeze (message);
|
||||
///
|
||||
/// notmuch_message_remove_all_tags (message);
|
||||
|
@ -1484,7 +1458,6 @@ pub type notmuch_message_tags_to_maildir_flags =
|
|||
/// notmuch_message_add_tag (message, tags[i]);
|
||||
///
|
||||
/// notmuch_message_thaw (message);
|
||||
/// ```
|
||||
///
|
||||
/// With freeze/thaw used like this, the message in the database is
|
||||
/// guaranteed to have either the full set of original tag values, or
|
||||
|
@ -1535,9 +1508,7 @@ pub type notmuch_message_thaw =
|
|||
/// the messages get reclaimed when the containing query is destroyed.)
|
||||
pub type notmuch_message_destroy = unsafe extern "C" fn(message: *mut notmuch_message_t);
|
||||
|
||||
/// ```text
|
||||
/// @name Message Properties
|
||||
/// ```
|
||||
///
|
||||
/// This interface provides the ability to attach arbitrary (key,value)
|
||||
/// string pairs to a message, to remove such pairs, and to iterate
|
||||
|
@ -1554,12 +1525,10 @@ pub type notmuch_message_destroy = unsafe extern "C" fn(message: *mut notmuch_me
|
|||
/// no such key. In the case of multiple values for the given key, the
|
||||
/// first one is retrieved.
|
||||
///
|
||||
/// ```text
|
||||
/// @returns
|
||||
/// - NOTMUCH_STATUS_NULL_POINTER: *value* may not be NULL.
|
||||
/// - NOTMUCH_STATUS_SUCCESS: No error occurred.
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_message_get_property = unsafe extern "C" fn(
|
||||
message: *mut notmuch_message_t,
|
||||
key: *const ::std::os::raw::c_char,
|
||||
|
@ -1568,13 +1537,11 @@ pub type notmuch_message_get_property = unsafe extern "C" fn(
|
|||
|
||||
/// Add a (key,value) pair to a message
|
||||
///
|
||||
/// ```text
|
||||
/// @returns
|
||||
/// - NOTMUCH_STATUS_ILLEGAL_ARGUMENT: *key* may not contain an '=' character.
|
||||
/// - NOTMUCH_STATUS_NULL_POINTER: Neither *key* nor *value* may be NULL.
|
||||
/// - NOTMUCH_STATUS_SUCCESS: No error occurred.
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_message_add_property = unsafe extern "C" fn(
|
||||
message: *mut notmuch_message_t,
|
||||
key: *const ::std::os::raw::c_char,
|
||||
|
@ -1585,13 +1552,11 @@ pub type notmuch_message_add_property = unsafe extern "C" fn(
|
|||
///
|
||||
/// It is not an error to remove a non-existant (key,value) pair
|
||||
///
|
||||
/// ```text
|
||||
/// @returns
|
||||
/// - NOTMUCH_STATUS_ILLEGAL_ARGUMENT: *key* may not contain an '=' character.
|
||||
/// - NOTMUCH_STATUS_NULL_POINTER: Neither *key* nor *value* may be NULL.
|
||||
/// - NOTMUCH_STATUS_SUCCESS: No error occurred.
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_message_remove_property = unsafe extern "C" fn(
|
||||
message: *mut notmuch_message_t,
|
||||
key: *const ::std::os::raw::c_char,
|
||||
|
@ -1600,7 +1565,6 @@ pub type notmuch_message_remove_property = unsafe extern "C" fn(
|
|||
|
||||
/// Remove all (key,value) pairs from the given message.
|
||||
///
|
||||
/// ```text
|
||||
/// @param[in,out] message message to operate on.
|
||||
/// @param[in] key key to delete properties for. If NULL, delete
|
||||
/// properties for all keys
|
||||
|
@ -1610,7 +1574,6 @@ pub type notmuch_message_remove_property = unsafe extern "C" fn(
|
|||
/// - NOTMUCH_STATUS_SUCCESS: No error occurred.
|
||||
///
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_message_remove_all_properties = unsafe extern "C" fn(
|
||||
message: *mut notmuch_message_t,
|
||||
key: *const ::std::os::raw::c_char,
|
||||
|
@ -1618,7 +1581,6 @@ pub type notmuch_message_remove_all_properties = unsafe extern "C" fn(
|
|||
|
||||
/// Remove all (prefix*,value) pairs from the given message
|
||||
///
|
||||
/// ```text
|
||||
/// @param[in,out] message message to operate on.
|
||||
/// @param[in] prefix delete properties with keys that start with prefix.
|
||||
/// If NULL, delete all properties
|
||||
|
@ -1628,7 +1590,6 @@ pub type notmuch_message_remove_all_properties = unsafe extern "C" fn(
|
|||
/// - NOTMUCH_STATUS_SUCCESS: No error occurred.
|
||||
///
|
||||
/// @since libnotmuch 5.1 (notmuch 0.26)
|
||||
/// ```
|
||||
pub type notmuch_message_remove_all_properties_with_prefix =
|
||||
unsafe extern "C" fn(
|
||||
message: *mut notmuch_message_t,
|
||||
|
@ -1651,12 +1612,10 @@ extern "C" {
|
|||
/// as such, will only be valid for as long as the message is valid,
|
||||
/// (which is until the query from which it derived is destroyed).
|
||||
///
|
||||
/// ```text
|
||||
/// @param[in] message The message to examine
|
||||
/// @param[in] key key or key prefix
|
||||
/// @param[in] exact if TRUE, require exact match with key. Otherwise
|
||||
/// treat as prefix.
|
||||
/// ```
|
||||
///
|
||||
/// Typical usage might be:
|
||||
///
|
||||
|
@ -1669,26 +1628,22 @@ extern "C" {
|
|||
/// }
|
||||
///
|
||||
/// notmuch_message_properties_destroy (list);
|
||||
/// ```
|
||||
///```
|
||||
///
|
||||
/// Note that there's no explicit destructor needed for the
|
||||
/// notmuch_message_properties_t object. (For consistency, we do
|
||||
/// 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,
|
||||
exact: notmuch_bool_t,
|
||||
) -> *mut notmuch_message_properties_t;
|
||||
}
|
||||
/// Return the number of properties named "key" belonging to the specific
|
||||
/// message.
|
||||
/// 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.
|
||||
|
@ -1698,7 +1653,6 @@ extern "C" {
|
|||
/// NOTMUCH_STATUS_SUCCESS: successful count, possibly some other error.
|
||||
///
|
||||
/// @since libnotmuch 5.2 (notmuch 0.27)
|
||||
/// ```
|
||||
pub type notmuch_message_count_properties = unsafe extern "C" fn(
|
||||
message: *mut notmuch_message_t,
|
||||
key: *const ::std::os::raw::c_char,
|
||||
|
@ -1718,9 +1672,7 @@ pub type notmuch_message_count_properties = unsafe extern "C" fn(
|
|||
/// code showing how to iterate over a notmuch_message_properties_t
|
||||
/// object.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_message_properties_valid =
|
||||
unsafe extern "C" fn(properties: *mut notmuch_message_properties_t) -> notmuch_bool_t;
|
||||
|
||||
|
@ -1733,9 +1685,7 @@ pub type notmuch_message_properties_valid =
|
|||
/// See the documentation of notmuch_message_get_properties for example
|
||||
/// code showing how to iterate over a notmuch_message_properties_t object.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_message_properties_move_to_next =
|
||||
unsafe extern "C" fn(properties: *mut notmuch_message_properties_t);
|
||||
|
||||
|
@ -1743,9 +1693,7 @@ pub type notmuch_message_properties_move_to_next =
|
|||
///
|
||||
/// this could be useful if iterating for a prefix
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_message_properties_key = unsafe extern "C" fn(
|
||||
properties: *mut notmuch_message_properties_t,
|
||||
) -> *const ::std::os::raw::c_char;
|
||||
|
@ -1754,9 +1702,7 @@ pub type notmuch_message_properties_key = unsafe extern "C" fn(
|
|||
///
|
||||
/// This could be useful if iterating for a prefix.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_message_properties_value = unsafe extern "C" fn(
|
||||
properties: *mut notmuch_message_properties_t,
|
||||
) -> *const ::std::os::raw::c_char;
|
||||
|
@ -1767,9 +1713,7 @@ pub type notmuch_message_properties_value = unsafe extern "C" fn(
|
|||
/// the notmuch_message_properties_t object will be reclaimed when the
|
||||
/// containing message object is destroyed.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_message_properties_destroy =
|
||||
unsafe extern "C" fn(properties: *mut notmuch_message_properties_t);
|
||||
|
||||
|
@ -1877,9 +1821,7 @@ pub type notmuch_directory_get_child_directories =
|
|||
/// notmuch_directory_t object. Assumes any child directories and files
|
||||
/// have been deleted by the caller.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.3 (notmuch 0.21)
|
||||
/// ```
|
||||
pub type notmuch_directory_delete =
|
||||
unsafe extern "C" fn(directory: *mut notmuch_directory_t) -> notmuch_status_t;
|
||||
|
||||
|
@ -1930,9 +1872,7 @@ pub type notmuch_filenames_destroy = unsafe extern "C" fn(filenames: *mut notmuc
|
|||
|
||||
/// set config 'key' to 'value'
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_database_set_config = unsafe extern "C" fn(
|
||||
db: *mut notmuch_database_t,
|
||||
key: *const ::std::os::raw::c_char,
|
||||
|
@ -1947,9 +1887,7 @@ pub type notmuch_database_set_config = unsafe extern "C" fn(
|
|||
/// return value is allocated by malloc and should be freed by the
|
||||
/// caller.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_database_get_config = unsafe extern "C" fn(
|
||||
db: *mut notmuch_database_t,
|
||||
key: *const ::std::os::raw::c_char,
|
||||
|
@ -1958,21 +1896,16 @@ 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,
|
||||
out: *mut *mut notmuch_config_list_t,
|
||||
) -> notmuch_status_t;
|
||||
|
||||
/// Is 'config_list' iterator valid (i.e. _key, _value, _move_to_next can be
|
||||
/// called).
|
||||
/// 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;
|
||||
|
||||
|
@ -1981,9 +1914,7 @@ pub type notmuch_config_list_valid =
|
|||
/// return value is owned by the iterator, and will be destroyed by the
|
||||
/// next call to notmuch_config_list_key or notmuch_config_list_destroy.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_config_list_key =
|
||||
unsafe extern "C" fn(config_list: *mut notmuch_config_list_t) -> *const ::std::os::raw::c_char;
|
||||
|
||||
|
@ -1992,25 +1923,19 @@ pub type notmuch_config_list_key =
|
|||
/// return value is owned by the iterator, and will be destroyed by the
|
||||
/// next call to notmuch_config_list_value or notmuch config_list_destroy
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_config_list_value =
|
||||
unsafe extern "C" fn(config_list: *mut notmuch_config_list_t) -> *const ::std::os::raw::c_char;
|
||||
|
||||
/// move 'config_list' iterator to the next pair
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_config_list_move_to_next =
|
||||
unsafe extern "C" fn(config_list: *mut notmuch_config_list_t);
|
||||
|
||||
/// free any resources held by 'config_list'
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_config_list_destroy =
|
||||
unsafe extern "C" fn(config_list: *mut notmuch_config_list_t);
|
||||
|
||||
|
@ -2023,9 +1948,7 @@ pub type notmuch_config_list_destroy =
|
|||
/// This object represents a set of options on how a message can be
|
||||
/// added to the index. At the moment it is a featureless stub.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 5.1 (notmuch 0.26)
|
||||
/// ```
|
||||
pub type notmuch_database_get_default_indexopts =
|
||||
unsafe extern "C" fn(db: *mut notmuch_database_t) -> *mut notmuch_indexopts_t;
|
||||
|
||||
|
@ -2044,9 +1967,7 @@ pub type notmuch_decryption_policy_t = u32;
|
|||
/// message index is adequately protected. DO NOT SET THIS FLAG TO TRUE
|
||||
/// without considering the security of your index.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 5.1 (notmuch 0.26)
|
||||
/// ```
|
||||
pub type notmuch_indexopts_set_decrypt_policy = unsafe extern "C" fn(
|
||||
indexopts: *mut notmuch_indexopts_t,
|
||||
decrypt_policy: notmuch_decryption_policy_t,
|
||||
|
@ -2055,23 +1976,17 @@ pub type notmuch_indexopts_set_decrypt_policy = unsafe extern "C" fn(
|
|||
/// Return whether to decrypt encrypted parts while indexing.
|
||||
/// see notmuch_indexopts_set_decrypt_policy.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 5.1 (notmuch 0.26)
|
||||
/// ```
|
||||
pub type notmuch_indexopts_get_decrypt_policy =
|
||||
unsafe extern "C" fn(indexopts: *const notmuch_indexopts_t) -> notmuch_decryption_policy_t;
|
||||
|
||||
/// Destroy a notmuch_indexopts_t object.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 5.1 (notmuch 0.26)
|
||||
/// ```
|
||||
pub type notmuch_indexopts_destroy = unsafe extern "C" fn(options: *mut notmuch_indexopts_t);
|
||||
|
||||
/// interrogate the library for compile time features
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_built_with =
|
||||
unsafe extern "C" fn(name: *const ::std::os::raw::c_char) -> notmuch_bool_t;
|
||||
|
|
|
@ -38,11 +38,11 @@ impl<'m> Message<'m> {
|
|||
call!(lib, notmuch_database_find_message)(
|
||||
*db.inner.read().unwrap(),
|
||||
msg_id.as_ptr(),
|
||||
std::ptr::addr_of_mut!(message),
|
||||
&mut message as *mut _,
|
||||
)
|
||||
};
|
||||
if message.is_null() {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"Message with message id {:?} not found in notmuch database.",
|
||||
msg_id
|
||||
)));
|
||||
|
@ -58,7 +58,11 @@ impl<'m> Message<'m> {
|
|||
pub fn env_hash(&self) -> EnvelopeHash {
|
||||
let msg_id = unsafe { call!(self.lib, notmuch_message_get_message_id)(self.message) };
|
||||
let c_str = unsafe { CStr::from_ptr(msg_id) };
|
||||
EnvelopeHash::from_bytes(c_str.to_bytes_with_nul())
|
||||
{
|
||||
let mut hasher = DefaultHasher::default();
|
||||
c_str.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn header(&self, header: &CStr) -> Option<&[u8]> {
|
||||
|
@ -81,14 +85,14 @@ impl<'m> Message<'m> {
|
|||
unsafe { CStr::from_ptr(msg_id) }
|
||||
}
|
||||
|
||||
pub fn date(&self) -> crate::UnixTimestamp {
|
||||
pub fn date(&self) -> crate::datetime::UnixTimestamp {
|
||||
(unsafe { call!(self.lib, notmuch_message_get_date)(self.message) }) as u64
|
||||
}
|
||||
|
||||
pub fn into_envelope(
|
||||
self,
|
||||
index: &RwLock<HashMap<EnvelopeHash, CString>>,
|
||||
tag_index: &RwLock<BTreeMap<TagHash, String>>,
|
||||
tag_index: &RwLock<BTreeMap<u64, String>>,
|
||||
) -> Envelope {
|
||||
let env_hash = self.env_hash();
|
||||
let mut env = Envelope::new(env_hash);
|
||||
|
@ -99,9 +103,13 @@ impl<'m> Message<'m> {
|
|||
let mut tag_lock = tag_index.write().unwrap();
|
||||
let (flags, tags) = TagIterator::new(&self).collect_flags_and_tags();
|
||||
for tag in tags {
|
||||
let num = TagHash::from_bytes(tag.as_bytes());
|
||||
tag_lock.entry(num).or_insert(tag);
|
||||
env.tags_mut().push(num);
|
||||
let mut hasher = DefaultHasher::new();
|
||||
hasher.write(tag.as_bytes());
|
||||
let num = hasher.finish();
|
||||
if !tag_lock.contains_key(&num) {
|
||||
tag_lock.insert(num, tag);
|
||||
}
|
||||
env.labels_mut().push(num);
|
||||
}
|
||||
unsafe {
|
||||
use crate::email::parser::address::rfc2822address_list;
|
||||
|
@ -203,7 +211,7 @@ impl<'m> Message<'m> {
|
|||
call!(self.lib, notmuch_message_add_tag)(self.message, tag.as_ptr())
|
||||
)
|
||||
} {
|
||||
return Err(Error::new("Could not set tag.").set_source(Some(Arc::new(err))));
|
||||
return Err(MeliError::new("Could not set tag.").set_source(Some(Arc::new(err))));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -215,7 +223,7 @@ impl<'m> Message<'m> {
|
|||
call!(self.lib, notmuch_message_remove_tag)(self.message, tag.as_ptr())
|
||||
)
|
||||
} {
|
||||
return Err(Error::new("Could not set tag.").set_source(Some(Arc::new(err))));
|
||||
return Err(MeliError::new("Could not set tag.").set_source(Some(Arc::new(err))));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -231,7 +239,7 @@ impl<'m> Message<'m> {
|
|||
call!(self.lib, notmuch_message_tags_to_maildir_flags)(self.message)
|
||||
)
|
||||
} {
|
||||
return Err(Error::new("Could not set flags.").set_source(Some(Arc::new(err))));
|
||||
return Err(MeliError::new("Could not set flags.").set_source(Some(Arc::new(err))));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -239,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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ impl<'q> Thread<'q> {
|
|||
ThreadHash::from(c_str.to_bytes())
|
||||
}
|
||||
|
||||
pub fn date(&self) -> crate::UnixTimestamp {
|
||||
pub fn date(&self) -> crate::datetime::UnixTimestamp {
|
||||
(unsafe { call!(self.lib, notmuch_thread_get_newest_date)(self.ptr) }) as u64
|
||||
}
|
||||
|
||||
|
@ -42,10 +42,6 @@ impl<'q> Thread<'q> {
|
|||
(unsafe { call!(self.lib, notmuch_thread_get_total_messages)(self.ptr) }) as usize
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
pub fn iter(&'q self) -> MessageIterator<'q> {
|
||||
let ptr = unsafe { call!(self.lib, notmuch_thread_get_messages)(self.ptr) };
|
||||
MessageIterator {
|
||||
|
|
|
@ -1,187 +0,0 @@
|
|||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2021 Ilya Medvedev
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation
|
||||
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
* DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/* Code from <https://github.com/iam-medvedev/rust-utf7-imap> */
|
||||
|
||||
//! A Rust library for encoding and decoding [UTF-7](https://datatracker.ietf.org/doc/html/rfc2152) string as defined by the [IMAP](https://datatracker.ietf.org/doc/html/rfc3501) standard in [RFC 3501 (#5.1.3)](https://datatracker.ietf.org/doc/html/rfc3501#section-5.1.3).
|
||||
//!
|
||||
//! Idea is based on Python [mutf7](https://github.com/cheshire-mouse/mutf7) library.
|
||||
|
||||
use encoding_rs::UTF_16BE;
|
||||
use regex::{Captures, Regex};
|
||||
|
||||
/// Encode UTF-7 IMAP mailbox name
|
||||
///
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc3501#section-5.1.3>
|
||||
pub fn encode_utf7_imap(text: &str) -> String {
|
||||
let mut result = "".to_string();
|
||||
let text = text.replace('&', "&-");
|
||||
let mut text = text.as_str();
|
||||
while !text.is_empty() {
|
||||
result = format!("{}{}", result, get_ascii(text));
|
||||
text = remove_ascii(text);
|
||||
if !text.is_empty() {
|
||||
let tmp = get_nonascii(text);
|
||||
result = format!("{}{}", result, encode_modified_utf7(tmp));
|
||||
text = remove_nonascii(text);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
fn is_ascii_custom(c: u8) -> bool {
|
||||
(0x20..=0x7f).contains(&c)
|
||||
}
|
||||
|
||||
fn get_ascii(s: &str) -> &str {
|
||||
let bytes = s.as_bytes();
|
||||
for (i, &item) in bytes.iter().enumerate() {
|
||||
if !is_ascii_custom(item) {
|
||||
return &s[0..i];
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn get_nonascii(s: &str) -> &str {
|
||||
let bytes = s.as_bytes();
|
||||
for (i, &item) in bytes.iter().enumerate() {
|
||||
if is_ascii_custom(item) {
|
||||
return &s[0..i];
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn remove_ascii(s: &str) -> &str {
|
||||
let bytes = s.as_bytes();
|
||||
for (i, &item) in bytes.iter().enumerate() {
|
||||
if !is_ascii_custom(item) {
|
||||
return &s[i..];
|
||||
}
|
||||
}
|
||||
""
|
||||
}
|
||||
|
||||
fn remove_nonascii(s: &str) -> &str {
|
||||
let bytes = s.as_bytes();
|
||||
for (i, &item) in bytes.iter().enumerate() {
|
||||
if is_ascii_custom(item) {
|
||||
return &s[i..];
|
||||
}
|
||||
}
|
||||
""
|
||||
}
|
||||
|
||||
fn encode_modified_utf7(text: &str) -> String {
|
||||
let capacity = 2 * text.len();
|
||||
let mut input = Vec::with_capacity(capacity);
|
||||
let text_u16 = text.encode_utf16();
|
||||
for value in text_u16 {
|
||||
input.extend_from_slice(&value.to_be_bytes());
|
||||
}
|
||||
let text_u16 = base64::encode(input);
|
||||
let text_u16 = text_u16.trim_end_matches('=');
|
||||
let result = text_u16.replace('/', ",");
|
||||
format!("&{}-", result)
|
||||
}
|
||||
|
||||
/// Decode UTF-7 IMAP mailbox name
|
||||
///
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc3501#section-5.1.3>
|
||||
pub fn decode_utf7_imap(text: &str) -> String {
|
||||
let pattern = Regex::new(r"&([^-]*)-").unwrap();
|
||||
pattern.replace_all(text, expand).to_string()
|
||||
}
|
||||
|
||||
fn expand(cap: &Captures) -> String {
|
||||
if cap.get(1).unwrap().as_str() == "" {
|
||||
"&".to_string()
|
||||
} else {
|
||||
decode_utf7_part(cap.get(0).unwrap().as_str())
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_utf7_part(text: &str) -> String {
|
||||
if text == "&-" {
|
||||
return String::from("&");
|
||||
}
|
||||
|
||||
let text_mb64 = &text[1..text.len() - 1];
|
||||
let mut text_b64 = text_mb64.replace(',', "/");
|
||||
|
||||
while (text_b64.len() % 4) != 0 {
|
||||
text_b64 += "=";
|
||||
}
|
||||
|
||||
let text_u16 = base64::decode(text_b64).unwrap();
|
||||
let (cow, _encoding_used, _had_errors) = UTF_16BE.decode(&text_u16);
|
||||
let result = cow.as_ref();
|
||||
|
||||
String::from(result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn encode_test() {
|
||||
assert_eq!(
|
||||
encode_utf7_imap("Отправленные"),
|
||||
"&BB4EQgQ,BEAEMAQyBDsENQQ9BD0ESwQ1-"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn encode_test_split() {
|
||||
assert_eq!(
|
||||
encode_utf7_imap("Šiukšliadėžė"),
|
||||
"&AWA-iuk&AWE-liad&ARcBfgEX-"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_consecutive_accents() {
|
||||
assert_eq!(encode_utf7_imap("théâtre"), "th&AOkA4g-tre")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_test() {
|
||||
assert_eq!(
|
||||
decode_utf7_imap("&BB4EQgQ,BEAEMAQyBDsENQQ9BD0ESwQ1-"),
|
||||
"Отправленные"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn decode_test_split() {
|
||||
// input string with utf7 encoded bits being separated by ascii
|
||||
assert_eq!(
|
||||
decode_utf7_imap("&AWA-iuk&AWE-liad&ARcBfgEX-"),
|
||||
"Šiukšliadėžė"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_consecutive_accents() {
|
||||
assert_eq!(decode_utf7_imap("th&AOkA4g-tre"), "théâtre")
|
||||
}
|
||||
}
|
|
@ -19,28 +19,53 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
ops::{Deref, DerefMut},
|
||||
sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard},
|
||||
};
|
||||
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::*;
|
||||
use crate::backends::{MailboxHash, TagHash};
|
||||
use crate::backends::MailboxHash;
|
||||
use smallvec::SmallVec;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
|
||||
pub type EnvelopeRef<'g> = RwRef<'g, EnvelopeHash, Envelope>;
|
||||
pub type EnvelopeRefMut<'g> = RwRefMut<'g, EnvelopeHash, Envelope>;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
pub struct EnvelopeRef<'g> {
|
||||
guard: RwLockReadGuard<'g, HashMap<EnvelopeHash, Envelope>>,
|
||||
env_hash: EnvelopeHash,
|
||||
}
|
||||
|
||||
impl Deref for EnvelopeRef<'_> {
|
||||
type Target = Envelope;
|
||||
|
||||
fn deref(&self) -> &Envelope {
|
||||
self.guard.get(&self.env_hash).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EnvelopeRefMut<'g> {
|
||||
guard: RwLockWriteGuard<'g, HashMap<EnvelopeHash, Envelope>>,
|
||||
env_hash: EnvelopeHash,
|
||||
}
|
||||
|
||||
impl Deref for EnvelopeRefMut<'_> {
|
||||
type Target = Envelope;
|
||||
|
||||
fn deref(&self) -> &Envelope {
|
||||
self.guard.get(&self.env_hash).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for EnvelopeRefMut<'_> {
|
||||
fn deref_mut(&mut self) -> &mut Envelope {
|
||||
self.guard.get_mut(&self.env_hash).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Collection {
|
||||
pub envelopes: Arc<RwLock<HashMap<EnvelopeHash, Envelope>>>,
|
||||
pub message_id_index: Arc<RwLock<HashMap<Vec<u8>, EnvelopeHash>>>,
|
||||
pub threads: Arc<RwLock<HashMap<MailboxHash, Threads>>>,
|
||||
pub sent_mailbox: Arc<RwLock<Option<MailboxHash>>>,
|
||||
sent_mailbox: Arc<RwLock<Option<MailboxHash>>>,
|
||||
pub mailboxes: Arc<RwLock<HashMap<MailboxHash, HashSet<EnvelopeHash>>>>,
|
||||
pub tag_index: Arc<RwLock<BTreeMap<TagHash, String>>>,
|
||||
}
|
||||
|
||||
impl Default for Collection {
|
||||
|
@ -49,8 +74,32 @@ impl Default for Collection {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
impl Drop for Collection {
|
||||
fn drop(&mut self) {
|
||||
let cache_dir: xdg::BaseDirectories =
|
||||
xdg::BaseDirectories::with_profile("meli", "threads".to_string()).unwrap();
|
||||
if let Ok(cached) = cache_dir.place_cache_file("threads") {
|
||||
/* place result in cache directory */
|
||||
let f = match fs::File::create(cached) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
panic!("{}", e);
|
||||
}
|
||||
};
|
||||
let writer = io::BufWriter::new(f);
|
||||
let _ = bincode::Options::serialize_into(
|
||||
bincode::config::DefaultOptions::new(),
|
||||
writer,
|
||||
&self.thread,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
impl Collection {
|
||||
pub fn new() -> Self {
|
||||
pub fn new() -> Collection {
|
||||
let message_id_index = Arc::new(RwLock::new(HashMap::with_capacity_and_hasher(
|
||||
16,
|
||||
Default::default(),
|
||||
|
@ -64,9 +113,8 @@ impl Collection {
|
|||
Default::default(),
|
||||
)));
|
||||
|
||||
Self {
|
||||
Collection {
|
||||
envelopes: Arc::new(RwLock::new(Default::default())),
|
||||
tag_index: Arc::new(RwLock::new(BTreeMap::default())),
|
||||
message_id_index,
|
||||
threads,
|
||||
mailboxes,
|
||||
|
@ -145,7 +193,9 @@ impl Collection {
|
|||
if *h == mailbox_hash {
|
||||
continue;
|
||||
}
|
||||
_ = t.update_envelope(&self.envelopes, old_hash, new_hash);
|
||||
t.update_envelope(&self.envelopes, old_hash, new_hash)
|
||||
.ok()
|
||||
.take();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
@ -160,7 +210,7 @@ impl Collection {
|
|||
) -> Option<SmallVec<[MailboxHash; 8]>> {
|
||||
*self.sent_mailbox.write().unwrap() = sent_mailbox;
|
||||
|
||||
let Self {
|
||||
let Collection {
|
||||
ref threads,
|
||||
ref envelopes,
|
||||
ref mailboxes,
|
||||
|
@ -170,8 +220,8 @@ impl Collection {
|
|||
|
||||
let mut threads_lck = threads.write().unwrap();
|
||||
let mut mailboxes_lck = mailboxes.write().unwrap();
|
||||
if let std::collections::hash_map::Entry::Vacant(e) = threads_lck.entry(mailbox_hash) {
|
||||
e.insert(Threads::new(new_envelopes.len()));
|
||||
if !threads_lck.contains_key(&mailbox_hash) {
|
||||
threads_lck.insert(mailbox_hash, Threads::new(new_envelopes.len()));
|
||||
mailboxes_lck.insert(mailbox_hash, new_envelopes.keys().cloned().collect());
|
||||
for (h, e) in new_envelopes {
|
||||
envelopes.write().unwrap().insert(h, e);
|
||||
|
@ -301,7 +351,8 @@ impl Collection {
|
|||
.unwrap_or(false)
|
||||
{
|
||||
for (_, t) in threads_lck.iter_mut() {
|
||||
_ = t.update_envelope(&self.envelopes, old_hash, new_hash);
|
||||
t.update_envelope(&self.envelopes, old_hash, new_hash)
|
||||
.unwrap_or(());
|
||||
}
|
||||
}
|
||||
{
|
||||
|
@ -323,7 +374,9 @@ impl Collection {
|
|||
if *h == mailbox_hash {
|
||||
continue;
|
||||
}
|
||||
_ = t.update_envelope(&self.envelopes, old_hash, new_hash);
|
||||
t.update_envelope(&self.envelopes, old_hash, new_hash)
|
||||
.ok()
|
||||
.take();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -337,7 +390,8 @@ impl Collection {
|
|||
.unwrap_or(false)
|
||||
{
|
||||
for (_, t) in threads_lck.iter_mut() {
|
||||
_ = t.update_envelope(&self.envelopes, env_hash, env_hash);
|
||||
t.update_envelope(&self.envelopes, env_hash, env_hash)
|
||||
.unwrap_or(());
|
||||
}
|
||||
}
|
||||
{
|
||||
|
@ -359,7 +413,9 @@ impl Collection {
|
|||
if *h == mailbox_hash {
|
||||
continue;
|
||||
}
|
||||
_ = t.update_envelope(&self.envelopes, env_hash, env_hash);
|
||||
t.update_envelope(&self.envelopes, env_hash, env_hash)
|
||||
.ok()
|
||||
.take();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -393,20 +449,19 @@ impl Collection {
|
|||
|
||||
pub fn insert_reply(&self, env_hash: EnvelopeHash) {
|
||||
debug_assert!(self.envelopes.read().unwrap().contains_key(&env_hash));
|
||||
let mut iter = self.threads.write().unwrap();
|
||||
for (_, t) in iter.iter_mut() {
|
||||
for (_, t) in self.threads.write().unwrap().iter_mut() {
|
||||
t.insert_reply(&self.envelopes, env_hash);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_env(&'_ self, hash: EnvelopeHash) -> EnvelopeRef<'_> {
|
||||
pub fn get_env(&'_ self, env_hash: EnvelopeHash) -> EnvelopeRef<'_> {
|
||||
let guard: RwLockReadGuard<'_, _> = self.envelopes.read().unwrap();
|
||||
EnvelopeRef { guard, hash }
|
||||
EnvelopeRef { guard, env_hash }
|
||||
}
|
||||
|
||||
pub fn get_env_mut(&'_ self, hash: EnvelopeHash) -> EnvelopeRefMut<'_> {
|
||||
pub fn get_env_mut(&'_ self, env_hash: EnvelopeHash) -> EnvelopeRefMut<'_> {
|
||||
let guard = self.envelopes.write().unwrap();
|
||||
EnvelopeRefMut { guard, hash }
|
||||
EnvelopeRefMut { guard, env_hash }
|
||||
}
|
||||
|
||||
pub fn get_threads(&'_ self, hash: MailboxHash) -> RwRef<'_, MailboxHash, Threads> {
|
||||
|
@ -428,8 +483,8 @@ impl Collection {
|
|||
|
||||
pub fn new_mailbox(&self, mailbox_hash: MailboxHash) {
|
||||
let mut mailboxes_lck = self.mailboxes.write().unwrap();
|
||||
if let std::collections::hash_map::Entry::Vacant(e) = mailboxes_lck.entry(mailbox_hash) {
|
||||
e.insert(Default::default());
|
||||
if !mailboxes_lck.contains_key(&mailbox_hash) {
|
||||
mailboxes_lck.insert(mailbox_hash, Default::default());
|
||||
self.threads
|
||||
.write()
|
||||
.unwrap()
|
||||
|
@ -447,43 +502,6 @@ impl<K: std::cmp::Eq + std::hash::Hash, V> Deref for RwRef<'_, K, V> {
|
|||
type Target = V;
|
||||
|
||||
fn deref(&self) -> &V {
|
||||
self.guard.get(&self.hash).expect("Hash was not found")
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: std::cmp::Eq + std::hash::Hash, V> AsRef<V> for RwRef<'_, K, V> {
|
||||
fn as_ref(&self) -> &V {
|
||||
self.guard.get(&self.hash).expect("Hash was not found")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RwRefMut<'g, K: std::cmp::Eq + std::hash::Hash, V> {
|
||||
guard: RwLockWriteGuard<'g, HashMap<K, V>>,
|
||||
hash: K,
|
||||
}
|
||||
|
||||
impl<K: std::cmp::Eq + std::hash::Hash, V> DerefMut for RwRefMut<'_, K, V> {
|
||||
fn deref_mut(&mut self) -> &mut V {
|
||||
self.guard.get_mut(&self.hash).expect("Hash was not found")
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: std::cmp::Eq + std::hash::Hash, V> Deref for RwRefMut<'_, K, V> {
|
||||
type Target = V;
|
||||
|
||||
fn deref(&self) -> &V {
|
||||
self.guard.get(&self.hash).expect("Hash was not found")
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: std::cmp::Eq + std::hash::Hash, V> AsRef<V> for RwRefMut<'_, K, V> {
|
||||
fn as_ref(&self) -> &V {
|
||||
self.guard.get(&self.hash).expect("Hash was not found")
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: std::cmp::Eq + std::hash::Hash, V> AsMut<V> for RwRefMut<'_, K, V> {
|
||||
fn as_mut(&mut self) -> &mut V {
|
||||
self.guard.get_mut(&self.hash).expect("Hash was not found")
|
||||
self.guard.get(&self.hash).unwrap()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,17 +19,10 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Basic mail account configuration to use with
|
||||
//! [`backends`](./backends/index.html)
|
||||
use std::collections::HashMap;
|
||||
|
||||
//! Basic mail account configuration to use with [`backends`](./backends/index.html)
|
||||
use crate::backends::SpecialUsageMailbox;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use crate::{
|
||||
backends::SpecialUsageMailbox,
|
||||
error::{Error, Result},
|
||||
};
|
||||
pub use crate::{SortField, SortOrder};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Serialize, Default, Clone)]
|
||||
pub struct AccountSettings {
|
||||
|
@ -37,11 +30,8 @@ pub struct AccountSettings {
|
|||
pub root_mailbox: String,
|
||||
pub format: String,
|
||||
pub identity: String,
|
||||
pub extra_identities: Vec<String>,
|
||||
pub read_only: bool,
|
||||
pub display_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub order: (SortField, SortOrder),
|
||||
pub subscribed_mailboxes: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub mailboxes: HashMap<String, MailboxConf>,
|
||||
|
@ -52,61 +42,40 @@ pub struct AccountSettings {
|
|||
}
|
||||
|
||||
impl AccountSettings {
|
||||
/// Create the account's display name from fields
|
||||
/// [`AccountSettings::identity`] and [`AccountSettings::display_name`].
|
||||
pub fn make_display_name(&self) -> String {
|
||||
if let Some(d) = self.display_name.as_ref() {
|
||||
format!("{} <{}>", d, self.identity)
|
||||
} else {
|
||||
self.identity.to_string()
|
||||
}
|
||||
pub fn format(&self) -> &str {
|
||||
&self.format
|
||||
}
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
pub fn set_name(&mut self, s: String) {
|
||||
self.name = s;
|
||||
}
|
||||
pub fn root_mailbox(&self) -> &str {
|
||||
&self.root_mailbox
|
||||
}
|
||||
pub fn identity(&self) -> &str {
|
||||
&self.identity
|
||||
}
|
||||
pub fn read_only(&self) -> bool {
|
||||
self.read_only
|
||||
}
|
||||
pub fn display_name(&self) -> Option<&String> {
|
||||
self.display_name.as_ref()
|
||||
}
|
||||
|
||||
pub fn order(&self) -> Option<(SortField, SortOrder)> {
|
||||
Some(self.order)
|
||||
pub fn subscribed_mailboxes(&self) -> &Vec<String> {
|
||||
&self.subscribed_mailboxes
|
||||
}
|
||||
|
||||
#[cfg(feature = "vcard")]
|
||||
pub fn vcard_folder(&self) -> Option<&str> {
|
||||
self.extra.get("vcard_folder").map(String::as_str)
|
||||
}
|
||||
|
||||
/// Get the server password, either directly from the `server_password`
|
||||
/// settings value, or by running the `server_password_command` and reading
|
||||
/// the output.
|
||||
pub fn server_password(&self) -> Result<String> {
|
||||
if let Some(cmd) = self.extra.get("server_password_command") {
|
||||
let output = std::process::Command::new("sh")
|
||||
.args(["-c", cmd])
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(std::str::from_utf8(&output.stdout)?.trim_end().to_string())
|
||||
} else {
|
||||
Err(Error::new(format!(
|
||||
"({}) server_password_command `{}` returned {}: {}",
|
||||
self.name,
|
||||
cmd,
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)))
|
||||
}
|
||||
} else if let Some(pass) = self.extra.get("server_password") {
|
||||
Ok(pass.to_owned())
|
||||
} else {
|
||||
Err(Error::new(
|
||||
"Configuration error: connection requires either server_password or \
|
||||
server_password_command",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MailboxConf {
|
||||
#[serde(alias = "rename")]
|
||||
pub alias: Option<String>,
|
||||
|
@ -118,24 +87,18 @@ pub struct MailboxConf {
|
|||
pub ignore: ToggleFlag,
|
||||
#[serde(default = "none")]
|
||||
pub usage: Option<SpecialUsageMailbox>,
|
||||
#[serde(default = "none")]
|
||||
pub sort_order: Option<usize>,
|
||||
#[serde(default = "none")]
|
||||
pub encoding: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub extra: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Default for MailboxConf {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
MailboxConf {
|
||||
alias: None,
|
||||
autoload: false,
|
||||
subscribe: ToggleFlag::Unset,
|
||||
ignore: ToggleFlag::Unset,
|
||||
usage: None,
|
||||
sort_order: None,
|
||||
encoding: None,
|
||||
extra: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
@ -147,52 +110,20 @@ impl MailboxConf {
|
|||
}
|
||||
}
|
||||
|
||||
pub const fn true_val() -> bool {
|
||||
pub fn true_val() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub const fn false_val() -> bool {
|
||||
pub fn false_val() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub const fn none<T>() -> Option<T> {
|
||||
pub fn none<T>() -> Option<T> {
|
||||
None
|
||||
}
|
||||
|
||||
macro_rules! named_unit_variant {
|
||||
($variant:ident) => {
|
||||
pub mod $variant {
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<(), D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct V;
|
||||
impl<'de> serde::de::Visitor<'de> for V {
|
||||
type Value = ();
|
||||
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.write_str(concat!("\"", stringify!($variant), "\""))
|
||||
}
|
||||
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
|
||||
if value == stringify!($variant) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(E::invalid_value(serde::de::Unexpected::Str(value), &self))
|
||||
}
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_str(V)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mod strings {
|
||||
named_unit_variant!(ask);
|
||||
}
|
||||
|
||||
#[derive(Copy, Default, Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Copy, Debug, Clone, PartialEq)]
|
||||
pub enum ToggleFlag {
|
||||
#[default]
|
||||
Unset,
|
||||
InternalVal(bool),
|
||||
False,
|
||||
|
@ -203,32 +134,41 @@ pub enum ToggleFlag {
|
|||
impl From<bool> for ToggleFlag {
|
||||
fn from(val: bool) -> Self {
|
||||
if val {
|
||||
Self::True
|
||||
ToggleFlag::True
|
||||
} else {
|
||||
Self::False
|
||||
ToggleFlag::False
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ToggleFlag {
|
||||
fn default() -> Self {
|
||||
ToggleFlag::Unset
|
||||
}
|
||||
}
|
||||
|
||||
impl ToggleFlag {
|
||||
pub fn is_unset(&self) -> bool {
|
||||
Self::Unset == *self
|
||||
ToggleFlag::Unset == *self
|
||||
}
|
||||
|
||||
pub fn is_internal(&self) -> bool {
|
||||
matches!(self, Self::InternalVal(_))
|
||||
if let ToggleFlag::InternalVal(_) = *self {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_ask(&self) -> bool {
|
||||
matches!(self, Self::Ask)
|
||||
*self == ToggleFlag::Ask
|
||||
}
|
||||
|
||||
pub fn is_false(&self) -> bool {
|
||||
matches!(self, Self::False | Self::InternalVal(false))
|
||||
ToggleFlag::False == *self || ToggleFlag::InternalVal(false) == *self
|
||||
}
|
||||
|
||||
pub fn is_true(&self) -> bool {
|
||||
matches!(self, Self::True | Self::InternalVal(true))
|
||||
ToggleFlag::True == *self || ToggleFlag::InternalVal(true) == *self
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -238,10 +178,10 @@ impl Serialize for ToggleFlag {
|
|||
S: Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::Unset | Self::InternalVal(_) => serializer.serialize_none(),
|
||||
Self::False => serializer.serialize_bool(false),
|
||||
Self::True => serializer.serialize_bool(true),
|
||||
Self::Ask => serializer.serialize_str("ask"),
|
||||
ToggleFlag::Unset | ToggleFlag::InternalVal(_) => serializer.serialize_none(),
|
||||
ToggleFlag::False => serializer.serialize_bool(false),
|
||||
ToggleFlag::True => serializer.serialize_bool(true),
|
||||
ToggleFlag::Ask => serializer.serialize_str("ask"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -251,25 +191,17 @@ impl<'de> Deserialize<'de> for ToggleFlag {
|
|||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum InnerToggleFlag {
|
||||
Bool(bool),
|
||||
#[serde(with = "strings::ask")]
|
||||
Ask,
|
||||
}
|
||||
let s = <InnerToggleFlag>::deserialize(deserializer);
|
||||
Ok(
|
||||
match s.map_err(|err| {
|
||||
serde::de::Error::custom(format!(
|
||||
let s = <String>::deserialize(deserializer);
|
||||
Ok(match s? {
|
||||
s if s.eq_ignore_ascii_case("true") => ToggleFlag::True,
|
||||
s if s.eq_ignore_ascii_case("false") => ToggleFlag::False,
|
||||
s if s.eq_ignore_ascii_case("ask") => ToggleFlag::Ask,
|
||||
s => {
|
||||
return Err(serde::de::Error::custom(format!(
|
||||
r#"expected one of "true", "false", "ask", found `{}`"#,
|
||||
err
|
||||
))
|
||||
})? {
|
||||
InnerToggleFlag::Bool(true) => Self::True,
|
||||
InnerToggleFlag::Bool(false) => Self::False,
|
||||
InnerToggleFlag::Ask => Self::Ask,
|
||||
},
|
||||
)
|
||||
s
|
||||
)))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,6 @@
|
|||
*/
|
||||
|
||||
//! Connections layers (TCP/fd/TLS/Deflate) to use with remote backends.
|
||||
use std::{os::unix::io::AsRawFd, time::Duration};
|
||||
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
use flate2::{read::DeflateDecoder, write::DeflateEncoder, Compression};
|
||||
#[cfg(any(target_os = "openbsd", target_os = "netbsd", target_os = "haiku"))]
|
||||
|
@ -37,6 +35,8 @@ use libc::TCP_KEEPALIVE as KEEPALIVE_OPTION;
|
|||
)))]
|
||||
use libc::TCP_KEEPIDLE as KEEPALIVE_OPTION;
|
||||
use libc::{self, c_int, c_void};
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Connection {
|
||||
|
@ -68,7 +68,7 @@ impl Connection {
|
|||
pub const IO_BUF_SIZE: usize = 64 * 1024;
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
pub fn deflate(self) -> Self {
|
||||
Self::Deflate {
|
||||
Connection::Deflate {
|
||||
inner: DeflateEncoder::new(
|
||||
DeflateDecoder::new_with_buf(Box::new(self), vec![0; Self::IO_BUF_SIZE]),
|
||||
Compression::default(),
|
||||
|
@ -91,7 +91,9 @@ impl Connection {
|
|||
!nix::fcntl::OFlag::O_NONBLOCK
|
||||
}),
|
||||
)
|
||||
.map_err(|err| std::io::Error::from_raw_os_error(err as i32))?;
|
||||
.map_err(|err| {
|
||||
std::io::Error::from_raw_os_error(err.as_errno().map(|n| n as i32).unwrap_or(0))
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
|
@ -157,7 +159,7 @@ impl Connection {
|
|||
where
|
||||
T: Copy,
|
||||
{
|
||||
let payload = std::ptr::addr_of!(payload) as *const c_void;
|
||||
let payload = &payload as *const T as *const c_void;
|
||||
syscall!(setsockopt(
|
||||
self.as_raw_fd(),
|
||||
opt,
|
||||
|
@ -175,7 +177,7 @@ impl Connection {
|
|||
self.as_raw_fd(),
|
||||
opt,
|
||||
val,
|
||||
std::ptr::addr_of_mut!(slot) as *mut _,
|
||||
&mut slot as *mut _ as *mut _,
|
||||
&mut len,
|
||||
))?;
|
||||
assert_eq!(len as usize, std::mem::size_of::<T>());
|
||||
|
@ -270,8 +272,22 @@ pub fn lookup_ipv4(host: &str, port: u16) -> crate::Result<std::net::SocketAddr>
|
|||
}
|
||||
|
||||
Err(
|
||||
crate::error::Error::new(format!("Could not lookup address {}:{}", host, port)).set_kind(
|
||||
crate::error::ErrorKind::Network(crate::error::NetworkErrorKind::HostLookupFailed),
|
||||
),
|
||||
crate::error::MeliError::new(format!("Could not lookup address {}:{}", host, port))
|
||||
.set_kind(crate::error::ErrorKind::Network),
|
||||
)
|
||||
}
|
||||
|
||||
use futures::future::{self, Either, Future};
|
||||
|
||||
pub async fn timeout<O>(dur: Option<Duration>, f: impl Future<Output = O>) -> crate::Result<O> {
|
||||
futures::pin_mut!(f);
|
||||
if let Some(dur) = dur {
|
||||
match future::select(f, smol::Timer::after(dur)).await {
|
||||
Either::Left((out, _)) => Ok(out),
|
||||
Either::Right(_) => Err(crate::error::MeliError::new("Timed out.")
|
||||
.set_kind(crate::error::ErrorKind::Timeout)),
|
||||
}
|
||||
} else {
|
||||
Ok(f.await)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,712 @@
|
|||
/*
|
||||
* meli - melib POSIX libc time interface
|
||||
*
|
||||
* Copyright 2020 Manos Pitsidianakis
|
||||
*
|
||||
* This file is part of meli.
|
||||
*
|
||||
* meli is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* meli is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Functions for dealing with date strings and UNIX Epoch timestamps.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use melib::datetime::*;
|
||||
//! // Get current UNIX Epoch timestamp.
|
||||
//! let now: UnixTimestamp = now();
|
||||
//!
|
||||
//! // Parse date from string
|
||||
//! let date_val = "Wed, 8 Jan 2020 10:44:03 -0800";
|
||||
//! let timestamp = rfc822_to_timestamp(date_val).unwrap();
|
||||
//! assert_eq!(timestamp, 1578509043);
|
||||
//!
|
||||
//! // Convert timestamp back to string
|
||||
//! let s = timestamp_to_string(timestamp, Some("%Y-%m-%d"));
|
||||
//! assert_eq!(s, "2020-01-08");
|
||||
//! ```
|
||||
use crate::error::{Result, ResultIntoMeliError};
|
||||
use std::convert::TryInto;
|
||||
use std::ffi::{CStr, CString};
|
||||
|
||||
pub type UnixTimestamp = u64;
|
||||
|
||||
use libc::{locale_t, timeval, timezone};
|
||||
|
||||
extern "C" {
|
||||
fn strptime(
|
||||
s: *const ::std::os::raw::c_char,
|
||||
format: *const ::std::os::raw::c_char,
|
||||
tm: *mut ::libc::tm,
|
||||
) -> *const ::std::os::raw::c_char;
|
||||
|
||||
fn strftime(
|
||||
s: *mut ::std::os::raw::c_char,
|
||||
max: ::libc::size_t,
|
||||
format: *const ::std::os::raw::c_char,
|
||||
tm: *const ::libc::tm,
|
||||
) -> ::libc::size_t;
|
||||
|
||||
fn mktime(tm: *const ::libc::tm) -> ::libc::time_t;
|
||||
|
||||
fn localtime_r(timep: *const ::libc::time_t, tm: *mut ::libc::tm) -> *mut ::libc::tm;
|
||||
|
||||
fn gettimeofday(tv: *mut timeval, tz: *mut timezone) -> i32;
|
||||
}
|
||||
|
||||
struct Locale {
|
||||
new_locale: locale_t,
|
||||
old_locale: locale_t,
|
||||
}
|
||||
|
||||
impl Drop for Locale {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let _ = libc::uselocale(self.old_locale);
|
||||
libc::freelocale(self.new_locale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// How to unit test this? Test machine is not guaranteed to have non-english locales.
|
||||
impl Locale {
|
||||
fn new(
|
||||
mask: ::std::os::raw::c_int,
|
||||
locale: *const ::std::os::raw::c_char,
|
||||
base: locale_t,
|
||||
) -> Result<Self> {
|
||||
let new_locale = unsafe { libc::newlocale(mask, locale, base) };
|
||||
if new_locale.is_null() {
|
||||
return Err(nix::Error::last().into());
|
||||
}
|
||||
let old_locale = unsafe { libc::uselocale(new_locale) };
|
||||
if old_locale.is_null() {
|
||||
unsafe { libc::freelocale(new_locale) };
|
||||
return Err(nix::Error::last().into());
|
||||
}
|
||||
Ok(Locale {
|
||||
new_locale,
|
||||
old_locale,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn timestamp_to_string(timestamp: UnixTimestamp, fmt: Option<&str>) -> String {
|
||||
let mut new_tm: ::libc::tm = unsafe { std::mem::zeroed() };
|
||||
unsafe {
|
||||
let i: i64 = timestamp.try_into().unwrap_or(0);
|
||||
localtime_r(&i as *const i64, &mut new_tm as *mut ::libc::tm);
|
||||
}
|
||||
let fmt = fmt
|
||||
.map(CString::new)
|
||||
.map(|res| res.ok())
|
||||
.and_then(|opt| opt);
|
||||
let format: &CStr = if let Some(ref s) = fmt {
|
||||
&s
|
||||
} else {
|
||||
unsafe { CStr::from_bytes_with_nul_unchecked(b"%a, %d %b %Y %T %z\0") }
|
||||
};
|
||||
let mut vec: [u8; 256] = [0; 256];
|
||||
let ret = unsafe {
|
||||
strftime(
|
||||
vec.as_mut_ptr() as *mut _,
|
||||
256,
|
||||
format.as_ptr(),
|
||||
&new_tm as *const _,
|
||||
)
|
||||
};
|
||||
|
||||
String::from_utf8_lossy(&vec[0..ret]).into_owned()
|
||||
}
|
||||
|
||||
fn tm_to_secs(tm: ::libc::tm) -> std::result::Result<i64, ()> {
|
||||
let mut is_leap = false;
|
||||
let mut year = tm.tm_year;
|
||||
let mut month = tm.tm_mon;
|
||||
if month >= 12 || month < 0 {
|
||||
let mut adj = month / 12;
|
||||
month %= 12;
|
||||
if month < 0 {
|
||||
adj -= 1;
|
||||
month += 12;
|
||||
}
|
||||
year += adj;
|
||||
}
|
||||
let mut t = year_to_secs(year.into(), &mut is_leap)?;
|
||||
t += month_to_secs(month.try_into().unwrap_or(0), is_leap);
|
||||
t += 86400 * (tm.tm_mday - 1) as i64;
|
||||
t += 3600 * (tm.tm_hour) as i64;
|
||||
t += 60 * (tm.tm_min) as i64;
|
||||
t += tm.tm_sec as i64;
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
fn year_to_secs(year: i64, is_leap: &mut bool) -> std::result::Result<i64, ()> {
|
||||
if year < -100 {
|
||||
/* Sorry time travelers. */
|
||||
return Err(());
|
||||
}
|
||||
|
||||
if year - 2 <= 136 {
|
||||
let y = year;
|
||||
let mut leaps = (y - 68) >> 2;
|
||||
if (y - 68) & 3 == 0 {
|
||||
leaps -= 1;
|
||||
*is_leap = true;
|
||||
} else {
|
||||
*is_leap = false;
|
||||
}
|
||||
return Ok((31536000 * (y - 70) + 86400 * leaps)
|
||||
.try_into()
|
||||
.unwrap_or(0));
|
||||
}
|
||||
|
||||
let cycles = (year - 100) / 400;
|
||||
let centuries;
|
||||
let mut leaps;
|
||||
let mut rem;
|
||||
|
||||
rem = (year - 100) % 400;
|
||||
|
||||
if rem == 0 {
|
||||
*is_leap = true;
|
||||
centuries = 0;
|
||||
leaps = 0;
|
||||
} else {
|
||||
if rem >= 200 {
|
||||
if rem >= 300 {
|
||||
centuries = 3;
|
||||
rem -= 300;
|
||||
} else {
|
||||
centuries = 2;
|
||||
rem -= 200;
|
||||
}
|
||||
} else if rem >= 100 {
|
||||
centuries = 1;
|
||||
rem -= 100;
|
||||
} else {
|
||||
centuries = 0;
|
||||
}
|
||||
if rem == 0 {
|
||||
*is_leap = false;
|
||||
leaps = 0;
|
||||
} else {
|
||||
leaps = rem / 4;
|
||||
rem %= 4;
|
||||
*is_leap = rem == 0;
|
||||
}
|
||||
}
|
||||
|
||||
leaps += 97 * cycles + 24 * centuries - if *is_leap { 1 } else { 0 };
|
||||
|
||||
match (year - 100).overflowing_mul(31536000) {
|
||||
(_, true) => Err(()),
|
||||
(res, false) => Ok(res + leaps * 86400 + 946684800 + 86400),
|
||||
}
|
||||
}
|
||||
|
||||
fn month_to_secs(month: usize, is_leap: bool) -> i64 {
|
||||
const SECS_THROUGH_MONTH: [i64; 12] = [
|
||||
0,
|
||||
31 * 86400,
|
||||
59 * 86400,
|
||||
90 * 86400,
|
||||
120 * 86400,
|
||||
151 * 86400,
|
||||
181 * 86400,
|
||||
212 * 86400,
|
||||
243 * 86400,
|
||||
273 * 86400,
|
||||
304 * 86400,
|
||||
334 * 86400,
|
||||
];
|
||||
let mut t = SECS_THROUGH_MONTH[month];
|
||||
if is_leap && month >= 2 {
|
||||
t += 86400;
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
pub fn rfc822_to_timestamp<T>(s: T) -> Result<UnixTimestamp>
|
||||
where
|
||||
T: Into<Vec<u8>>,
|
||||
{
|
||||
let s = CString::new(s)?;
|
||||
let mut new_tm: ::libc::tm = unsafe { std::mem::zeroed() };
|
||||
for fmt in &[
|
||||
&b"%a, %e %h %Y %H:%M:%S \0"[..],
|
||||
&b"%e %h %Y %H:%M:%S \0"[..],
|
||||
] {
|
||||
unsafe {
|
||||
let fmt = CStr::from_bytes_with_nul_unchecked(fmt);
|
||||
|
||||
let ret = {
|
||||
let _with_locale = Locale::new(
|
||||
::libc::LC_TIME,
|
||||
b"C\0".as_ptr() as *const i8,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
.chain_err_summary(|| "Could not set locale for datetime conversion")
|
||||
.chain_err_kind(crate::error::ErrorKind::External)?;
|
||||
strptime(s.as_ptr(), fmt.as_ptr(), &mut new_tm as *mut _)
|
||||
};
|
||||
|
||||
if ret.is_null() {
|
||||
continue;
|
||||
}
|
||||
let rest = CStr::from_ptr(ret);
|
||||
let tm_gmtoff = if rest.to_bytes().len() > 4
|
||||
&& rest.to_bytes().is_ascii()
|
||||
&& rest.to_bytes()[1..5].iter().all(u8::is_ascii_digit)
|
||||
{
|
||||
let offset = std::str::from_utf8_unchecked(&rest.to_bytes()[0..5]);
|
||||
if let (Ok(mut hr_offset), Ok(mut min_offset)) =
|
||||
(offset[1..3].parse::<i64>(), offset[3..5].parse::<i64>())
|
||||
{
|
||||
if rest.to_bytes()[0] == b'-' {
|
||||
hr_offset = -hr_offset;
|
||||
min_offset = -min_offset;
|
||||
}
|
||||
hr_offset * 60 * 60 + min_offset * 60
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
let rest = if rest.to_bytes().starts_with(b"(") && rest.to_bytes().ends_with(b")") {
|
||||
&rest.to_bytes()[1..rest.to_bytes().len() - 1]
|
||||
} else {
|
||||
rest.to_bytes()
|
||||
};
|
||||
|
||||
if let Ok(idx) = TIMEZONE_ABBR.binary_search_by(|probe| probe.0.cmp(rest)) {
|
||||
let (hr_offset, min_offset) = TIMEZONE_ABBR[idx].1;
|
||||
(hr_offset as i64) * 60 * 60 + (min_offset as i64) * 60
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
return Ok(tm_to_secs(new_tm)
|
||||
.map(|res| (res - tm_gmtoff) as u64)
|
||||
.unwrap_or(0));
|
||||
}
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
pub fn rfc3339_to_timestamp<T>(s: T) -> Result<UnixTimestamp>
|
||||
where
|
||||
T: Into<Vec<u8>>,
|
||||
{
|
||||
let s = CString::new(s)?;
|
||||
let mut new_tm: ::libc::tm = unsafe { std::mem::zeroed() };
|
||||
for fmt in &[&b"%Y-%m-%dT%H:%M:%S\0"[..], &b"%Y-%m-%d\0"[..]] {
|
||||
unsafe {
|
||||
let fmt = CStr::from_bytes_with_nul_unchecked(fmt);
|
||||
let ret = {
|
||||
let _with_locale = Locale::new(
|
||||
::libc::LC_TIME,
|
||||
b"C\0".as_ptr() as *const i8,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
.chain_err_summary(|| "Could not set locale for datetime conversion")
|
||||
.chain_err_kind(crate::error::ErrorKind::External)?;
|
||||
strptime(s.as_ptr(), fmt.as_ptr(), &mut new_tm as *mut _)
|
||||
};
|
||||
if ret.is_null() {
|
||||
continue;
|
||||
}
|
||||
let rest = CStr::from_ptr(ret);
|
||||
let tm_gmtoff = if rest.to_bytes().len() > 4
|
||||
&& rest.to_bytes().is_ascii()
|
||||
&& rest.to_bytes()[1..3].iter().all(u8::is_ascii_digit)
|
||||
&& rest.to_bytes()[4..6].iter().all(u8::is_ascii_digit)
|
||||
{
|
||||
let offset = std::str::from_utf8_unchecked(&rest.to_bytes()[0..6]);
|
||||
if let (Ok(mut hr_offset), Ok(mut min_offset)) =
|
||||
(offset[1..3].parse::<i64>(), offset[4..6].parse::<i64>())
|
||||
{
|
||||
if rest.to_bytes()[0] == b'-' {
|
||||
hr_offset = -hr_offset;
|
||||
min_offset = -min_offset;
|
||||
}
|
||||
hr_offset * 60 * 60 + min_offset * 60
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
let rest = if rest.to_bytes().starts_with(b"(") && rest.to_bytes().ends_with(b")") {
|
||||
&rest.to_bytes()[1..rest.to_bytes().len() - 1]
|
||||
} else {
|
||||
rest.to_bytes()
|
||||
};
|
||||
|
||||
if let Ok(idx) = TIMEZONE_ABBR.binary_search_by(|probe| probe.0.cmp(rest)) {
|
||||
let (hr_offset, min_offset) = debug!(TIMEZONE_ABBR[idx]).1;
|
||||
(hr_offset as i64) * 60 * 60 + (min_offset as i64) * 60
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
return Ok(tm_to_secs(new_tm)
|
||||
.map(|res| (res - tm_gmtoff) as u64)
|
||||
.unwrap_or(0));
|
||||
}
|
||||
}
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
// FIXME: Handle non-local timezone?
|
||||
pub fn timestamp_from_string<T>(s: T, fmt: &str) -> Result<Option<UnixTimestamp>>
|
||||
where
|
||||
T: Into<Vec<u8>>,
|
||||
{
|
||||
let mut new_tm: ::libc::tm = unsafe { std::mem::zeroed() };
|
||||
let fmt = CString::new(fmt)?;
|
||||
unsafe {
|
||||
let ret = strptime(
|
||||
CString::new(s)?.as_ptr(),
|
||||
fmt.as_ptr(),
|
||||
&mut new_tm as *mut _,
|
||||
);
|
||||
if ret.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(mktime(&new_tm as *const _) as u64))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn now() -> UnixTimestamp {
|
||||
use std::mem::MaybeUninit;
|
||||
let mut tv = MaybeUninit::<::libc::timeval>::uninit();
|
||||
let mut tz = MaybeUninit::<::libc::timezone>::uninit();
|
||||
unsafe {
|
||||
let ret = gettimeofday(tv.as_mut_ptr(), tz.as_mut_ptr());
|
||||
if ret == -1 {
|
||||
unreachable!("gettimeofday returned -1");
|
||||
}
|
||||
(tv.assume_init()).tv_sec as UnixTimestamp
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timestamp() {
|
||||
timestamp_to_string(0, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rfcs() {
|
||||
if unsafe { libc::setlocale(libc::LC_ALL, b"\0".as_ptr() as _) }.is_null() {
|
||||
println!("Unable to set locale.");
|
||||
}
|
||||
/* Some tests were lazily stolen from https://rachelbythebay.com/w/2013/06/11/time/ */
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Wed, 8 Jan 2020 10:44:03 -0800").unwrap(),
|
||||
1578509043
|
||||
);
|
||||
|
||||
/*
|
||||
macro_rules! mkt {
|
||||
($year:literal, $month:literal, $day:literal, $hour:literal, $minute:literal, $second:literal) => {
|
||||
::libc::tm {
|
||||
tm_sec: $second,
|
||||
tm_min: $minute,
|
||||
tm_hour: $hour,
|
||||
tm_mday: $day,
|
||||
tm_mon: $month - 1,
|
||||
tm_year: $year - 1900,
|
||||
tm_wday: 0,
|
||||
tm_yday: 0,
|
||||
tm_isdst: 0,
|
||||
tm_gmtoff: 0,
|
||||
tm_zone: std::ptr::null(),
|
||||
}
|
||||
};
|
||||
}
|
||||
*/
|
||||
//unsafe { __tm_to_secs(&mkt!(2009, 02, 13, 23, 31, 30) as *const _) },
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Fri, 13 Feb 2009 15:31:30 -0800").unwrap(),
|
||||
1234567890
|
||||
);
|
||||
|
||||
//unsafe { __tm_to_secs(&mkt!(2931, 05, 05, 00, 33, 09) as *const _) },
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Sat, 05 May 2931 00:33:09 +0000").unwrap(),
|
||||
30336942789
|
||||
);
|
||||
//2214-11-06 20:05:12 = 7726651512 [OK]
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Sun, 06 Nov 2214 17:05:12 -0300").unwrap(), //2214-11-06 20:05:12
|
||||
7726651512
|
||||
);
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Sun, 06 Nov 2214 17:05:12 -0300").unwrap(), //2214-11-06 20:05:12
|
||||
rfc822_to_timestamp("Sun, 06 Nov 2214 17:05:12 (ADT)").unwrap(), //2214-11-06 20:05:12
|
||||
);
|
||||
//2661-11-06 06:38:02 = 21832612682 [OK]
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Wed, 06 Nov 2661 06:38:02 +0000").unwrap(), //2661-11-06 06:38:02
|
||||
21832612682
|
||||
);
|
||||
//2508-12-09 04:27:08 = 17007251228 [OK]
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Sun, 09 Dec 2508 04:27:08 +0000").unwrap(), //2508-12-09 04:27:08
|
||||
17007251228
|
||||
);
|
||||
//2375-11-07 05:08:24 = 12807349704 [OK]
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Fri, 07 Nov 2375 05:08:24 +0000").unwrap(), //2375-11-07 05:08:24
|
||||
12807349704
|
||||
);
|
||||
//2832-09-03 02:46:10 = 27223353970 [OK]
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Fri, 03 Sep 2832 02:46:10 +0000").unwrap(), //2832-09-03 02:46:10
|
||||
27223353970
|
||||
);
|
||||
//2983-02-25 12:47:17 = 31972020437 [OK]
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Tue, 25 Feb 2983 15:47:17 +0300").unwrap(), //2983-02-25 12:47:17
|
||||
31972020437
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Thu, 30 Mar 2017 17:32:06 +0300 (EEST)").unwrap(),
|
||||
1490884326
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 17:36:34 +0530").unwrap(),
|
||||
1493035594
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 17:36:34 +0530").unwrap(),
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 12:06:34 +0000").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 17:36:34 +0530").unwrap(),
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 17:36:34 (SLST)").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 17:36:34 +0530").unwrap(),
|
||||
rfc822_to_timestamp("Mon, 24 Apr 2017 17:36:34 SLST").unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("27 Dec 2019 14:42:46 +0100").unwrap(),
|
||||
1577454166
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
rfc822_to_timestamp("Mon, 16 Mar 2020 10:23:01 +0200").unwrap(),
|
||||
1584346981
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::zero_prefixed_literal)]
|
||||
const TIMEZONE_ABBR: &[(&[u8], (i8, i8))] = &[
|
||||
(b"ACDT", (10, 30)),
|
||||
(b"ACST", (09, 30)),
|
||||
(b"ACT", (-05, 0)),
|
||||
(b"ACWST", (08, 45)),
|
||||
(b"ADT", (-03, 0)),
|
||||
(b"AEDT", (11, 0)),
|
||||
(b"AEST", (10, 0)),
|
||||
(b"AFT", (04, 30)),
|
||||
(b"AKDT", (-08, 0)),
|
||||
(b"AKST", (-09, 0)),
|
||||
(b"ALMT", (06, 0)),
|
||||
(b"AMST", (-03, 0)),
|
||||
(b"AMT", (-04, 0)), /* Amazon Time */
|
||||
(b"ANAT", (12, 0)),
|
||||
(b"AQTT", (05, 0)),
|
||||
(b"ART", (-03, 0)),
|
||||
(b"AST", (-04, 0)),
|
||||
(b"AST", (03, 0)),
|
||||
(b"AWST", (08, 0)),
|
||||
(b"AZOST", (0, 0)),
|
||||
(b"AZOT", (-01, 0)),
|
||||
(b"AZT", (04, 0)),
|
||||
(b"BDT", (08, 0)),
|
||||
(b"BIOT", (06, 0)),
|
||||
(b"BIT", (-12, 0)),
|
||||
(b"BOT", (-04, 0)),
|
||||
(b"BRST", (-02, 0)),
|
||||
(b"BRT", (-03, 0)),
|
||||
(b"BST", (06, 0)),
|
||||
(b"BTT", (06, 0)),
|
||||
(b"CAT", (02, 0)),
|
||||
(b"CCT", (06, 30)),
|
||||
(b"CDT", (-05, 0)),
|
||||
(b"CEST", (02, 0)),
|
||||
(b"CET", (01, 0)),
|
||||
(b"CHADT", (13, 45)),
|
||||
(b"CHAST", (12, 45)),
|
||||
(b"CHOST", (09, 0)),
|
||||
(b"CHOT", (08, 0)),
|
||||
(b"CHST", (10, 0)),
|
||||
(b"CHUT", (10, 0)),
|
||||
(b"CIST", (-08, 0)),
|
||||
(b"CIT", (08, 0)),
|
||||
(b"CKT", (-10, 0)),
|
||||
(b"CLST", (-03, 0)),
|
||||
(b"CLT", (-04, 0)),
|
||||
(b"COST", (-04, 0)),
|
||||
(b"COT", (-05, 0)),
|
||||
(b"CST", (-06, 0)),
|
||||
(b"CT", (08, 0)),
|
||||
(b"CVT", (-01, 0)),
|
||||
(b"CWST", (08, 45)),
|
||||
(b"CXT", (07, 0)),
|
||||
(b"DAVT", (07, 0)),
|
||||
(b"DDUT", (10, 0)),
|
||||
(b"DFT", (01, 0)),
|
||||
(b"EASST", (-05, 0)),
|
||||
(b"EAST", (-06, 0)),
|
||||
(b"EAT", (03, 0)),
|
||||
(b"ECT", (-05, 0)),
|
||||
(b"EDT", (-04, 0)),
|
||||
(b"EEST", (03, 0)),
|
||||
(b"EET", (02, 0)),
|
||||
(b"EGST", (0, 0)),
|
||||
(b"EGT", (-01, 0)),
|
||||
(b"EIT", (09, 0)),
|
||||
(b"EST", (-05, 0)),
|
||||
(b"FET", (03, 0)),
|
||||
(b"FJT", (12, 0)),
|
||||
(b"FKST", (-03, 0)),
|
||||
(b"FKT", (-04, 0)),
|
||||
(b"FNT", (-02, 0)),
|
||||
(b"GALT", (-06, 0)),
|
||||
(b"GAMT", (-09, 0)),
|
||||
(b"GET", (04, 0)),
|
||||
(b"GFT", (-03, 0)),
|
||||
(b"GILT", (12, 0)),
|
||||
(b"GIT", (-09, 0)),
|
||||
(b"GMT", (0, 0)),
|
||||
(b"GST", (04, 0)),
|
||||
(b"GYT", (-04, 0)),
|
||||
(b"HAEC", (02, 0)),
|
||||
(b"HDT", (-09, 0)),
|
||||
(b"HKT", (08, 0)),
|
||||
(b"HMT", (05, 0)),
|
||||
(b"HOVST", (08, 0)),
|
||||
(b"HOVT", (07, 0)),
|
||||
(b"HST", (-10, 0)),
|
||||
(b"ICT", (07, 0)),
|
||||
(b"IDLW", (-12, 0)),
|
||||
(b"IDT", (03, 0)),
|
||||
(b"IOT", (03, 0)),
|
||||
(b"IRDT", (04, 30)),
|
||||
(b"IRKT", (08, 0)),
|
||||
(b"IRST", (03, 30)),
|
||||
(b"IST", (05, 30)),
|
||||
(b"JST", (09, 0)),
|
||||
(b"KALT", (02, 0)),
|
||||
(b"KGT", (06, 0)),
|
||||
(b"KOST", (11, 0)),
|
||||
(b"KRAT", (07, 0)),
|
||||
(b"KST", (09, 0)),
|
||||
(b"LHST", (10, 30)),
|
||||
(b"LINT", (14, 0)),
|
||||
(b"MAGT", (12, 0)),
|
||||
(b"MART", (-09, -30)),
|
||||
(b"MAWT", (05, 0)),
|
||||
(b"MDT", (-06, 0)),
|
||||
(b"MEST", (02, 0)),
|
||||
(b"MET", (01, 0)),
|
||||
(b"MHT", (12, 0)),
|
||||
(b"MIST", (11, 0)),
|
||||
(b"MIT", (-09, -30)),
|
||||
(b"MMT", (06, 30)),
|
||||
(b"MSK", (03, 0)),
|
||||
(b"MST", (08, 0)),
|
||||
(b"MUT", (04, 0)),
|
||||
(b"MVT", (05, 0)),
|
||||
(b"MYT", (08, 0)),
|
||||
(b"NCT", (11, 0)),
|
||||
(b"NDT", (-02, -30)),
|
||||
(b"NFT", (11, 0)),
|
||||
(b"NOVT", (07, 0)),
|
||||
(b"NPT", (05, 45)),
|
||||
(b"NST", (-03, -30)),
|
||||
(b"NT", (-03, -30)),
|
||||
(b"NUT", (-11, 0)),
|
||||
(b"NZDT", (13, 0)),
|
||||
(b"NZST", (12, 0)),
|
||||
(b"OMST", (06, 0)),
|
||||
(b"ORAT", (05, 0)),
|
||||
(b"PDT", (-07, 0)),
|
||||
(b"PET", (-05, 0)),
|
||||
(b"PETT", (12, 0)),
|
||||
(b"PGT", (10, 0)),
|
||||
(b"PHOT", (13, 0)),
|
||||
(b"PHT", (08, 0)),
|
||||
(b"PKT", (05, 0)),
|
||||
(b"PMDT", (-02, 0)),
|
||||
(b"PMST", (-03, 0)),
|
||||
(b"PONT", (11, 0)),
|
||||
(b"PST", (-08, 0)),
|
||||
(b"PST", (08, 0)),
|
||||
(b"PYST", (-03, 0)),
|
||||
(b"PYT", (-04, 0)),
|
||||
(b"RET", (04, 0)),
|
||||
(b"ROTT", (-03, 0)),
|
||||
(b"SAKT", (11, 0)),
|
||||
(b"SAMT", (04, 0)),
|
||||
(b"SAST", (02, 0)),
|
||||
(b"SBT", (11, 0)),
|
||||
(b"SCT", (04, 0)),
|
||||
(b"SDT", (-10, 0)),
|
||||
(b"SGT", (08, 0)),
|
||||
(b"SLST", (05, 30)),
|
||||
(b"SRET", (11, 0)),
|
||||
(b"SRT", (-03, 0)),
|
||||
(b"SST", (08, 0)),
|
||||
(b"SYOT", (03, 0)),
|
||||
(b"TAHT", (-10, 0)),
|
||||
(b"TFT", (05, 0)),
|
||||
(b"THA", (07, 0)),
|
||||
(b"TJT", (05, 0)),
|
||||
(b"TKT", (13, 0)),
|
||||
(b"TLT", (09, 0)),
|
||||
(b"TMT", (05, 0)),
|
||||
(b"TOT", (13, 0)),
|
||||
(b"TRT", (03, 0)),
|
||||
(b"TVT", (12, 0)),
|
||||
(b"ULAST", (09, 0)),
|
||||
(b"ULAT", (08, 0)),
|
||||
(b"UTC", (0, 0)),
|
||||
(b"UYST", (-02, 0)),
|
||||
(b"UYT", (-03, 0)),
|
||||
(b"UZT", (05, 0)),
|
||||
(b"VET", (-04, 0)),
|
||||
(b"VLAT", (10, 0)),
|
||||
(b"VOLT", (04, 0)),
|
||||
(b"VOST", (06, 0)),
|
||||
(b"VUT", (11, 0)),
|
||||
(b"WAKT", (12, 0)),
|
||||
(b"WAST", (02, 0)),
|
||||
(b"WAT", (01, 0)),
|
||||
(b"WEST", (01, 0)),
|
||||
(b"WET", (0, 0)),
|
||||
(b"WIT", (07, 0)),
|
||||
(b"WST", (08, 0)),
|
||||
(b"YAKT", (09, 0)),
|
||||
(b"YEKT", (05, 0)),
|
||||
];
|
|
@ -19,80 +19,75 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Email parsing and composing.
|
||||
//!
|
||||
//! # Parsing bytes into an `Envelope`
|
||||
//!
|
||||
//! An [`Envelope`](Envelope) represents the information you can get from an
|
||||
//! email's headers and body structure. Addresses in `To`, `From` fields etc
|
||||
//! are parsed into [`Address`](crate::email::Address) types.
|
||||
//!
|
||||
//! ```
|
||||
//! use melib::{Attachment, Envelope};
|
||||
//!
|
||||
//! let raw_mail = r#"From: "some name" <some@example.com>
|
||||
//! To: "me" <myself@example.com>
|
||||
//! Cc:
|
||||
//! Subject: =?utf-8?Q?gratuitously_encoded_subject?=
|
||||
//! Message-ID: <h2g7f.z0gy2pgaen5m@example.com>
|
||||
//! MIME-Version: 1.0
|
||||
//! Content-Type: multipart/mixed; charset="utf-8";
|
||||
//! boundary="bzz_bzz__bzz__"
|
||||
//!
|
||||
//! This is a MIME formatted message with attachments. Use a MIME-compliant client to view it properly.
|
||||
//! --bzz_bzz__bzz__
|
||||
//!
|
||||
//! hello world.
|
||||
//! --bzz_bzz__bzz__
|
||||
//! Content-Type: image/gif; name="test_image.gif"; charset="utf-8"
|
||||
//! Content-Disposition: attachment
|
||||
//! Content-Transfer-Encoding: base64
|
||||
//!
|
||||
//! R0lGODdhKAAXAOfZAAABzAADzQAEzgQFtBEAxAAGxBcAxwALvRcFwAAPwBcLugATuQEUuxoNuxYQ
|
||||
//! sxwOvAYVvBsStSAVtx8YsRUcuhwhth4iuCQsyDAwuDc1vTc3uDg4uT85rkc9ukJBvENCvURGukdF
|
||||
//! wUVKt0hLuUxPvVZSvFlYu1hbt2BZuFxdul5joGhqlnNuf3FvlnBvwXJyt3Jxw3N0oXx1gH12gV99
|
||||
//! z317f3N7spFxwHp5wH99gYB+goF/g25+26tziIOBhWqD3oiBjICAuudkjIN+zHeC2n6Bzc1vh4eF
|
||||
//! iYaBw8F0kImHi4KFxYyHmIWIvI2Lj4uIvYaJyY+IuJGMi5iJl4qKxZSMmIuLxpONnpGPk42NvI2M
|
||||
//! 1LKGl46OvZePm5ORlZiQnJqSnpaUmLyJnJuTn5iVmZyUoJGVyZ2VoZSVw5iXoZmWrO18rJiUyp6W
|
||||
//! opuYnKaVnZ+Xo5yZncaMoaCYpJiaqo+Z2Z2annuf5qGZpa2WoJybpZmayZ2Z0KCZypydrZ6dp6Cd
|
||||
//! oZ6a0aGay5ucy5+eqKGeouWMgp+b0qKbzKCfqdqPnp2ezaGgqqOgpKafqrScpp+gz6ajqKujr62j
|
||||
//! qayksKmmq62lsaiosqqorOyWnaqqtKeqzLGptaurta2rr7Kqtq+ssLOrt6+uuLGusuqhfbWtubCv
|
||||
//! ubKvs7GwurOwtPSazbevu+ali7SxtbiwvOykjLOyvLWytuCmqOankrSzvbazuLmyvrW0vre0uba1
|
||||
//! wLi1ury0wLm2u721wbe3wbq3vMC2vLi4wr+3w7m5w8C4xLi6yry6vsG5xbu7xcC6zMK6xry8xry+
|
||||
//! u8O7x729x8C9wb++yMG+wsO+vMK/w8a+y8e/zMnBzcXH18nL2///////////////////////////
|
||||
//! ////////////////////////////////////////////////////////////////////////////
|
||||
//! /////////////////////////////////////////////////////ywAAAAAKAAXAAAI/gBP4Cjh
|
||||
//! IYMLEh0w4EgBgsMLEyFGFBEB5cOFABgzatS4AVssZAOsLOHCxooVMzCyoNmzaBOkJlS0VEDyZMjG
|
||||
//! mxk3XOMF60CDBgsoPABK9KcDCRImPCiQYAECAgQCRMU4VSrGCjFarBgUSJCgQ10FBTrkNRCfPnz4
|
||||
//! dA3UNa1btnDZqgU7Ntqzu3ej2X2mFy9eaHuhNRtMGJrhwYYN930G2K7eaNIY34U2mfJkwpgzI9Yr
|
||||
//! GBqwR2KSvAlMOXHnw5pTNzPdLNoWIWtU9XjGjDEYS8LAlFm1SrVvzIKj5TH0KpORSZOryPgCZgqL
|
||||
//! Ob+jG0YVRBErUrOiiGJ8KxgtYsh27xWL/tswnTtEbsiRVYdJNMHk4yOGhswGjR88UKjQ9Ey+/8TL
|
||||
//! XKKGGn7Akph/8XX2WDTTcAYfguVt9hhrEPqmzIOJ3VUheb48WJiHG6amC4i+WVJKKCimqGIoYxyj
|
||||
//! WWK8kKjaJ9bA18sxvXjYhourmbbMMrjI+OIn1QymDCVXANGFK4S1gQw0PxozzC+33FLLKUJq9gk1
|
||||
//! gyWDhyNwrMLkYGUEM4wvuLRiCiieXIJJJVlmJskcZ9TZRht1lnFGGmTMkMoonVQSSSOFAGJHHI0w
|
||||
//! ouiijDaaCCGQRgrpH3q4QYYXWDihxBE+7KCDDjnUIEVAADs=
|
||||
//! --bzz_bzz__bzz__--"#;
|
||||
//!
|
||||
//! let envelope = Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
|
||||
//! assert_eq!(envelope.subject().as_ref(), "gratuitously encoded subject");
|
||||
//! assert_eq!(
|
||||
//! envelope.message_id_display().as_ref(),
|
||||
//! "<h2g7f.z0gy2pgaen5m@example.com>"
|
||||
//! );
|
||||
//!
|
||||
//! let body = envelope.body_bytes(raw_mail.as_bytes());
|
||||
//! assert_eq!(body.content_type().to_string().as_str(), "multipart/mixed");
|
||||
//!
|
||||
//! let body_text = body.text();
|
||||
//! assert_eq!(body_text.as_str(), "hello world.");
|
||||
//!
|
||||
//! let subattachments: Vec<Attachment> = body.attachments();
|
||||
//! assert_eq!(subattachments.len(), 3);
|
||||
//! assert_eq!(
|
||||
//! subattachments[2].content_type().name().unwrap(),
|
||||
//! "test_image.gif"
|
||||
//! );
|
||||
//! ```
|
||||
/*!
|
||||
* Email parsing and composing.
|
||||
*
|
||||
* # 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.
|
||||
*
|
||||
* ```
|
||||
* use melib::{Attachment, Envelope};
|
||||
*
|
||||
* let raw_mail = r#"From: "some name" <some@address.com>
|
||||
* To: "me" <myself@i.tld>
|
||||
* Cc:
|
||||
* Subject: =?utf-8?Q?gratuitously_encoded_subject?=
|
||||
* Message-ID: <h2g7f.z0gy2pgaen5m@address.com>
|
||||
* MIME-Version: 1.0
|
||||
* Content-Type: multipart/mixed; charset="utf-8";
|
||||
* boundary="bzz_bzz__bzz__"
|
||||
*
|
||||
* This is a MIME formatted message with attachments. Use a MIME-compliant client to view it properly.
|
||||
* --bzz_bzz__bzz__
|
||||
*
|
||||
* hello world.
|
||||
* --bzz_bzz__bzz__
|
||||
* Content-Type: image/gif; name="test_image.gif"; charset="utf-8"
|
||||
* Content-Disposition: attachment
|
||||
* Content-Transfer-Encoding: base64
|
||||
*
|
||||
* R0lGODdhKAAXAOfZAAABzAADzQAEzgQFtBEAxAAGxBcAxwALvRcFwAAPwBcLugATuQEUuxoNuxYQ
|
||||
* sxwOvAYVvBsStSAVtx8YsRUcuhwhth4iuCQsyDAwuDc1vTc3uDg4uT85rkc9ukJBvENCvURGukdF
|
||||
* wUVKt0hLuUxPvVZSvFlYu1hbt2BZuFxdul5joGhqlnNuf3FvlnBvwXJyt3Jxw3N0oXx1gH12gV99
|
||||
* z317f3N7spFxwHp5wH99gYB+goF/g25+26tziIOBhWqD3oiBjICAuudkjIN+zHeC2n6Bzc1vh4eF
|
||||
* iYaBw8F0kImHi4KFxYyHmIWIvI2Lj4uIvYaJyY+IuJGMi5iJl4qKxZSMmIuLxpONnpGPk42NvI2M
|
||||
* 1LKGl46OvZePm5ORlZiQnJqSnpaUmLyJnJuTn5iVmZyUoJGVyZ2VoZSVw5iXoZmWrO18rJiUyp6W
|
||||
* opuYnKaVnZ+Xo5yZncaMoaCYpJiaqo+Z2Z2annuf5qGZpa2WoJybpZmayZ2Z0KCZypydrZ6dp6Cd
|
||||
* oZ6a0aGay5ucy5+eqKGeouWMgp+b0qKbzKCfqdqPnp2ezaGgqqOgpKafqrScpp+gz6ajqKujr62j
|
||||
* qayksKmmq62lsaiosqqorOyWnaqqtKeqzLGptaurta2rr7Kqtq+ssLOrt6+uuLGusuqhfbWtubCv
|
||||
* ubKvs7GwurOwtPSazbevu+ali7SxtbiwvOykjLOyvLWytuCmqOankrSzvbazuLmyvrW0vre0uba1
|
||||
* wLi1ury0wLm2u721wbe3wbq3vMC2vLi4wr+3w7m5w8C4xLi6yry6vsG5xbu7xcC6zMK6xry8xry+
|
||||
* u8O7x729x8C9wb++yMG+wsO+vMK/w8a+y8e/zMnBzcXH18nL2///////////////////////////
|
||||
* ////////////////////////////////////////////////////////////////////////////
|
||||
* /////////////////////////////////////////////////////ywAAAAAKAAXAAAI/gBP4Cjh
|
||||
* IYMLEh0w4EgBgsMLEyFGFBEB5cOFABgzatS4AVssZAOsLOHCxooVMzCyoNmzaBOkJlS0VEDyZMjG
|
||||
* mxk3XOMF60CDBgsoPABK9KcDCRImPCiQYAECAgQCRMU4VSrGCjFarBgUSJCgQ10FBTrkNRCfPnz4
|
||||
* dA3UNa1btnDZqgU7Ntqzu3ej2X2mFy9eaHuhNRtMGJrhwYYN930G2K7eaNIY34U2mfJkwpgzI9Yr
|
||||
* GBqwR2KSvAlMOXHnw5pTNzPdLNoWIWtU9XjGjDEYS8LAlFm1SrVvzIKj5TH0KpORSZOryPgCZgqL
|
||||
* Ob+jG0YVRBErUrOiiGJ8KxgtYsh27xWL/tswnTtEbsiRVYdJNMHk4yOGhswGjR88UKjQ9Ey+/8TL
|
||||
* XKKGGn7Akph/8XX2WDTTcAYfguVt9hhrEPqmzIOJ3VUheb48WJiHG6amC4i+WVJKKCimqGIoYxyj
|
||||
* WWK8kKjaJ9bA18sxvXjYhourmbbMMrjI+OIn1QymDCVXANGFK4S1gQw0PxozzC+33FLLKUJq9gk1
|
||||
* gyWDhyNwrMLkYGUEM4wvuLRiCiieXIJJJVlmJskcZ9TZRht1lnFGGmTMkMoonVQSSSOFAGJHHI0w
|
||||
* ouiijDaaCCGQRgrpH3q4QYYXWDihxBE+7KCDDjnUIEVAADs=
|
||||
* --bzz_bzz__bzz__--"#;
|
||||
*
|
||||
* let envelope = Envelope::from_bytes(raw_mail.as_bytes(), None).expect("Could not parse mail");
|
||||
* assert_eq!(envelope.subject().as_ref(), "gratuitously encoded subject");
|
||||
* assert_eq!(envelope.message_id_display().as_ref(), "<h2g7f.z0gy2pgaen5m@address.com>");
|
||||
*
|
||||
* let body = envelope.body_bytes(raw_mail.as_bytes());
|
||||
* assert_eq!(body.content_type().to_string().as_str(), "multipart/mixed");
|
||||
*
|
||||
* let body_text = body.text();
|
||||
* assert_eq!(body_text.as_str(), "hello world.");
|
||||
*
|
||||
* let subattachments: Vec<Attachment> = body.attachments();
|
||||
* assert_eq!(subattachments.len(), 3);
|
||||
* assert_eq!(subattachments[2].content_type().name().unwrap(), "test_image.gif");
|
||||
* ```
|
||||
*/
|
||||
|
||||
pub mod address;
|
||||
pub mod attachment_types;
|
||||
|
@ -104,46 +99,23 @@ pub mod mailto;
|
|||
pub mod parser;
|
||||
pub mod pgp;
|
||||
|
||||
use std::{borrow::Cow, convert::TryInto, ops::Deref};
|
||||
|
||||
pub use address::{Address, MessageID, References, StrBuild, StrBuilder};
|
||||
pub use attachments::{Attachment, AttachmentBuilder};
|
||||
pub use compose::{attachment_from_file, Draft};
|
||||
pub use headers::*;
|
||||
#[cfg(feature = "imap_backend")]
|
||||
use imap_codec::{
|
||||
core::{AString, Atom, NonEmptyVec},
|
||||
fetch::{MacroOrMessageDataItemNames, MessageDataItemName},
|
||||
flag::Flag as ImapCodecFlag,
|
||||
section::Section,
|
||||
};
|
||||
pub use mailto::*;
|
||||
|
||||
use crate::datetime::UnixTimestamp;
|
||||
use crate::error::{MeliError, Result};
|
||||
use crate::parser::BytesExt;
|
||||
use crate::thread::ThreadNodeHash;
|
||||
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
parser::BytesExt,
|
||||
thread::ThreadNodeHash,
|
||||
TagHash, UnixTimestamp,
|
||||
};
|
||||
|
||||
#[cfg(feature = "imap_backend")]
|
||||
// TODO(#222): Make this `const` as soon as it is possible.
|
||||
pub(crate) fn common_attributes() -> MacroOrMessageDataItemNames<'static> {
|
||||
MacroOrMessageDataItemNames::MessageDataItemNames(vec![
|
||||
MessageDataItemName::Uid,
|
||||
MessageDataItemName::Flags,
|
||||
MessageDataItemName::Envelope,
|
||||
MessageDataItemName::BodyExt {
|
||||
section: Some(Section::HeaderFields(
|
||||
None,
|
||||
NonEmptyVec::from(AString::from(Atom::unvalidated("REFERENCES"))),
|
||||
)),
|
||||
partial: None,
|
||||
peek: true,
|
||||
},
|
||||
MessageDataItemName::BodyStructure,
|
||||
])
|
||||
}
|
||||
use std::borrow::Cow;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::convert::TryInto;
|
||||
use std::hash::Hasher;
|
||||
use std::ops::Deref;
|
||||
|
||||
bitflags! {
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
|
@ -159,70 +131,21 @@ bitflags! {
|
|||
|
||||
impl PartialEq<&str> for Flag {
|
||||
fn eq(&self, other: &&str) -> bool {
|
||||
(other.eq_ignore_ascii_case("passed") && self.contains(Self::PASSED))
|
||||
|| (other.eq_ignore_ascii_case("replied") && self.contains(Self::REPLIED))
|
||||
|| (other.eq_ignore_ascii_case("seen") && self.contains(Self::SEEN))
|
||||
|| (other.eq_ignore_ascii_case("read") && self.contains(Self::SEEN))
|
||||
|| (other.eq_ignore_ascii_case("junk") && self.contains(Self::TRASHED))
|
||||
|| (other.eq_ignore_ascii_case("trash") && self.contains(Self::TRASHED))
|
||||
|| (other.eq_ignore_ascii_case("trashed") && self.contains(Self::TRASHED))
|
||||
|| (other.eq_ignore_ascii_case("draft") && self.contains(Self::DRAFT))
|
||||
|| (other.eq_ignore_ascii_case("flagged") && self.contains(Self::FLAGGED))
|
||||
(other.eq_ignore_ascii_case("passed") && self.contains(Flag::PASSED))
|
||||
|| (other.eq_ignore_ascii_case("replied") && self.contains(Flag::REPLIED))
|
||||
|| (other.eq_ignore_ascii_case("seen") && self.contains(Flag::SEEN))
|
||||
|| (other.eq_ignore_ascii_case("read") && self.contains(Flag::SEEN))
|
||||
|| (other.eq_ignore_ascii_case("junk") && self.contains(Flag::TRASHED))
|
||||
|| (other.eq_ignore_ascii_case("trash") && self.contains(Flag::TRASHED))
|
||||
|| (other.eq_ignore_ascii_case("trashed") && self.contains(Flag::TRASHED))
|
||||
|| (other.eq_ignore_ascii_case("draft") && self.contains(Flag::DRAFT))
|
||||
|| (other.eq_ignore_ascii_case("flagged") && self.contains(Flag::FLAGGED))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! flag_impl {
|
||||
(fn $name:ident, $val:expr) => {
|
||||
pub const fn $name(&self) -> bool {
|
||||
self.contains($val)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Flag {
|
||||
flag_impl!(fn is_passed, Self::PASSED);
|
||||
flag_impl!(fn is_replied, Self::REPLIED);
|
||||
flag_impl!(fn is_seen, Self::SEEN);
|
||||
flag_impl!(fn is_trashed, Self::TRASHED);
|
||||
flag_impl!(fn is_draft, Self::DRAFT);
|
||||
flag_impl!(fn is_flagged, Self::FLAGGED);
|
||||
|
||||
#[cfg(feature = "imap_backend")]
|
||||
pub(crate) fn derive_imap_codec_flags(&self) -> Vec<ImapCodecFlag> {
|
||||
let mut flags = Vec::new();
|
||||
|
||||
if self.is_passed() {
|
||||
// This is from http://cr.yp.to/proto/maildir.html and not meaningful in IMAP.
|
||||
}
|
||||
|
||||
if self.is_replied() {
|
||||
flags.push(ImapCodecFlag::Answered);
|
||||
}
|
||||
|
||||
if self.is_seen() {
|
||||
flags.push(ImapCodecFlag::Seen);
|
||||
}
|
||||
|
||||
if self.is_trashed() {
|
||||
flags.push(ImapCodecFlag::Deleted);
|
||||
}
|
||||
|
||||
if self.is_draft() {
|
||||
flags.push(ImapCodecFlag::Draft);
|
||||
}
|
||||
|
||||
if self.is_flagged() {
|
||||
flags.push(ImapCodecFlag::Flagged);
|
||||
}
|
||||
|
||||
flags
|
||||
}
|
||||
}
|
||||
|
||||
///`Mail` holds both the envelope info of an email in its `envelope` field and
|
||||
/// the raw bytes that describe the email in `bytes`. Its body as an
|
||||
/// `melib::email::Attachment` can be parsed on demand
|
||||
/// with the `melib::email::Mail::body` method.
|
||||
///`Mail` holds both the envelope info of an email in its `envelope` field and the raw bytes that
|
||||
///describe the email in `bytes`. Its body as an `melib::email::Attachment` can be parsed on demand
|
||||
///with the `melib::email::Mail::body` method.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Mail {
|
||||
pub envelope: Envelope,
|
||||
|
@ -239,7 +162,7 @@ impl Deref for Mail {
|
|||
|
||||
impl Mail {
|
||||
pub fn new(bytes: Vec<u8>, flags: Option<Flag>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
Ok(Mail {
|
||||
envelope: Envelope::from_bytes(&bytes, flags)?,
|
||||
bytes,
|
||||
})
|
||||
|
@ -258,37 +181,32 @@ impl Mail {
|
|||
}
|
||||
}
|
||||
|
||||
crate::declare_u64_hash!(EnvelopeHash);
|
||||
pub type EnvelopeHash = u64;
|
||||
|
||||
/// `Envelope` represents all the header and structure data of an email we need
|
||||
/// to know.
|
||||
/// `Envelope` represents all the header and structure data of an email we need to know.
|
||||
///
|
||||
/// Attachments (the email's body) is parsed on demand with `body` method.
|
||||
///
|
||||
///To access the email attachments, you need to parse them from the raw email
|
||||
/// bytes into an `Attachment` object.
|
||||
///To access the email attachments, you need to parse them from the raw email bytes into an
|
||||
///`Attachment` object.
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Envelope {
|
||||
// ----- IMAP4rev1 -----
|
||||
pub hash: EnvelopeHash,
|
||||
pub date: String,
|
||||
pub subject: Option<String>,
|
||||
pub timestamp: UnixTimestamp,
|
||||
pub from: SmallVec<[Address; 1]>,
|
||||
// pub sender
|
||||
// pub reply_to
|
||||
pub to: SmallVec<[Address; 1]>,
|
||||
pub cc: SmallVec<[Address; 1]>,
|
||||
pub bcc: Vec<Address>,
|
||||
pub in_reply_to: Option<MessageID>,
|
||||
pub subject: Option<String>,
|
||||
pub message_id: MessageID,
|
||||
// ----- Other -----
|
||||
pub hash: EnvelopeHash,
|
||||
pub timestamp: UnixTimestamp,
|
||||
pub in_reply_to: Option<MessageID>,
|
||||
pub references: Option<References>,
|
||||
pub other_headers: HeaderMap,
|
||||
pub thread: ThreadNodeHash,
|
||||
pub flags: Flag,
|
||||
pub has_attachments: bool,
|
||||
pub tags: SmallVec<[TagHash; 8]>,
|
||||
pub labels: SmallVec<[u64; 8]>,
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for Envelope {
|
||||
|
@ -309,13 +227,13 @@ impl core::fmt::Debug for Envelope {
|
|||
|
||||
impl Default for Envelope {
|
||||
fn default() -> Self {
|
||||
Self::new(EnvelopeHash::default())
|
||||
Envelope::new(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Envelope {
|
||||
pub fn new(hash: EnvelopeHash) -> Self {
|
||||
Self {
|
||||
Envelope {
|
||||
hash,
|
||||
date: String::new(),
|
||||
timestamp: 0,
|
||||
|
@ -331,7 +249,7 @@ impl Envelope {
|
|||
thread: ThreadNodeHash::null(),
|
||||
has_attachments: false,
|
||||
flags: Flag::default(),
|
||||
tags: SmallVec::new(),
|
||||
labels: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -340,8 +258,10 @@ impl Envelope {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8], flags: Option<Flag>) -> Result<Self> {
|
||||
let mut e = Self::new(EnvelopeHash::from_bytes(bytes.trim()));
|
||||
pub fn from_bytes(bytes: &[u8], flags: Option<Flag>) -> Result<Envelope> {
|
||||
let mut h = DefaultHasher::new();
|
||||
h.write(bytes);
|
||||
let mut e = Envelope::new(h.finish());
|
||||
let res = e.populate_headers(bytes).ok();
|
||||
if res.is_some() {
|
||||
if let Some(f) = flags {
|
||||
|
@ -349,7 +269,7 @@ impl Envelope {
|
|||
}
|
||||
return Ok(e);
|
||||
}
|
||||
Err(Error::new("Couldn't parse mail."))
|
||||
Err(MeliError::new("Couldn't parse mail."))
|
||||
}
|
||||
|
||||
pub fn hash(&self) -> EnvelopeHash {
|
||||
|
@ -368,7 +288,7 @@ impl Envelope {
|
|||
Err(e) => {
|
||||
debug!("error in parsing mail\n{:?}\n", e);
|
||||
let error_msg = String::from("Mail cannot be shown because of errors.");
|
||||
return Err(Error::new(error_msg));
|
||||
return Err(MeliError::new(error_msg));
|
||||
}
|
||||
};
|
||||
for (name, value) in headers {
|
||||
|
@ -430,11 +350,7 @@ impl Envelope {
|
|||
self.has_attachments =
|
||||
Attachment::check_if_has_attachments_quick(body, boundary);
|
||||
} else {
|
||||
debug!(
|
||||
"{:?} has no boundary field set in multipart/mixed content-type \
|
||||
field.",
|
||||
&self
|
||||
);
|
||||
debug!("{:?} has no boundary field set in multipart/mixed content-type field.", &self);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
@ -458,12 +374,12 @@ 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() {
|
||||
let hash = self.hash;
|
||||
self.set_message_id(format!("<{:x}>", hash.0).as_bytes());
|
||||
self.set_message_id(format!("<{:x}>", hash).as_bytes());
|
||||
}
|
||||
if self.references.is_some() {
|
||||
if let Some(pos) = self
|
||||
|
@ -501,6 +417,7 @@ impl Envelope {
|
|||
if self.bcc.is_empty() {
|
||||
self.other_headers
|
||||
.get("Bcc")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
} else {
|
||||
|
@ -516,7 +433,11 @@ impl Envelope {
|
|||
|
||||
pub fn field_cc_to_string(&self) -> String {
|
||||
if self.cc.is_empty() {
|
||||
self.other_headers.get("Cc").unwrap_or_default().to_string()
|
||||
self.other_headers
|
||||
.get("Cc")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
} else {
|
||||
self.cc.iter().fold(String::new(), |mut acc, x| {
|
||||
if !acc.is_empty() {
|
||||
|
@ -532,6 +453,7 @@ impl Envelope {
|
|||
if self.from.is_empty() {
|
||||
self.other_headers
|
||||
.get("From")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
} else {
|
||||
|
@ -549,17 +471,13 @@ impl Envelope {
|
|||
self.to.as_slice()
|
||||
}
|
||||
|
||||
pub fn cc(&self) -> &[Address] {
|
||||
self.cc.as_slice()
|
||||
}
|
||||
|
||||
pub fn bcc(&self) -> &[Address] {
|
||||
self.bcc.as_slice()
|
||||
}
|
||||
|
||||
pub fn field_to_to_string(&self) -> String {
|
||||
if self.to.is_empty() {
|
||||
self.other_headers.get("To").unwrap_or_default().to_string()
|
||||
self.other_headers
|
||||
.get("To")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
} else {
|
||||
self.to
|
||||
.iter()
|
||||
|
@ -579,6 +497,7 @@ impl Envelope {
|
|||
if refs.is_empty() {
|
||||
self.other_headers
|
||||
.get("References")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
} else {
|
||||
|
@ -843,31 +762,31 @@ impl Envelope {
|
|||
self.has_attachments
|
||||
}
|
||||
|
||||
pub fn tags(&self) -> &SmallVec<[TagHash; 8]> {
|
||||
&self.tags
|
||||
pub fn labels(&self) -> &SmallVec<[u64; 8]> {
|
||||
&self.labels
|
||||
}
|
||||
|
||||
pub fn tags_mut(&mut self) -> &mut SmallVec<[TagHash; 8]> {
|
||||
&mut self.tags
|
||||
pub fn labels_mut(&mut self) -> &mut SmallVec<[u64; 8]> {
|
||||
&mut self.labels
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Envelope {}
|
||||
|
||||
impl Ord for Envelope {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
fn cmp(&self, other: &Envelope) -> std::cmp::Ordering {
|
||||
self.datetime().cmp(&other.datetime())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Envelope {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
fn partial_cmp(&self, other: &Envelope) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Envelope {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
fn eq(&self, other: &Envelope) -> bool {
|
||||
self.hash == other.hash
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,15 +19,11 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Email addresses. Parsing functions are in
|
||||
//! [melib::email::parser::address](../parser/address/index.html).
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
convert::TryFrom,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
//! Email addresses. Parsing functions are in [melib::email::parser::address](../parser/address/index.html).
|
||||
use super::*;
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryFrom;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct GroupAddress {
|
||||
|
@ -57,7 +53,7 @@ pub struct GroupAddress {
|
|||
* > display_name │
|
||||
* > │
|
||||
* > address_spec
|
||||
* ```
|
||||
*```
|
||||
*/
|
||||
pub struct MailboxAddress {
|
||||
pub raw: Vec<u8>,
|
||||
|
@ -68,7 +64,7 @@ pub struct MailboxAddress {
|
|||
impl Eq for MailboxAddress {}
|
||||
|
||||
impl PartialEq for MailboxAddress {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
fn eq(&self, other: &MailboxAddress) -> bool {
|
||||
self.address_spec.display_bytes(&self.raw) == other.address_spec.display_bytes(&other.raw)
|
||||
}
|
||||
}
|
||||
|
@ -82,20 +78,14 @@ impl PartialEq for MailboxAddress {
|
|||
///
|
||||
/// ```rust
|
||||
/// # use melib::email::Address;
|
||||
/// let addr = Address::new(
|
||||
/// Some("Jörg Doe".to_string()),
|
||||
/// "joerg@example.com".to_string(),
|
||||
/// );
|
||||
/// let addr = Address::new(Some("Jörg Doe".to_string()), "joerg@example.com".to_string());
|
||||
/// assert_eq!(addr.to_string().as_str(), "Jörg Doe <joerg@example.com>");
|
||||
/// ```
|
||||
///
|
||||
/// or parse it from a raw value:
|
||||
///
|
||||
/// ```rust
|
||||
/// let (rest_bytes, addr) = melib::email::parser::address::address(
|
||||
/// "=?utf-8?q?J=C3=B6rg_Doe?= <joerg@example.com>".as_bytes(),
|
||||
/// )
|
||||
/// .unwrap();
|
||||
/// let (rest_bytes, addr) = melib::email::parser::address::address("=?utf-8?q?J=C3=B6rg_Doe?= <joerg@example.com>".as_bytes()).unwrap();
|
||||
/// assert!(rest_bytes.is_empty());
|
||||
/// assert_eq!(addr.get_display_name(), Some("Jörg Doe".to_string()));
|
||||
/// assert_eq!(addr.get_email(), "joerg@example.com".to_string());
|
||||
|
@ -108,7 +98,7 @@ pub enum Address {
|
|||
|
||||
impl Address {
|
||||
pub fn new(display_name: Option<String>, address: String) -> Self {
|
||||
Self::Mailbox(if let Some(d) = display_name {
|
||||
Address::Mailbox(if let Some(d) = display_name {
|
||||
MailboxAddress {
|
||||
raw: format!("{} <{}>", d, address).into_bytes(),
|
||||
display_name: StrBuilder {
|
||||
|
@ -122,7 +112,7 @@ impl Address {
|
|||
}
|
||||
} else {
|
||||
MailboxAddress {
|
||||
raw: address.to_string().into_bytes(),
|
||||
raw: format!("{}", address).into_bytes(),
|
||||
display_name: StrBuilder {
|
||||
offset: 0,
|
||||
length: 0,
|
||||
|
@ -135,8 +125,8 @@ impl Address {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn new_group(display_name: String, mailbox_list: Vec<Self>) -> Self {
|
||||
Self::Group(GroupAddress {
|
||||
pub fn new_group(display_name: String, mailbox_list: Vec<Address>) -> Self {
|
||||
Address::Group(GroupAddress {
|
||||
raw: format!(
|
||||
"{}:{};",
|
||||
display_name,
|
||||
|
@ -157,15 +147,15 @@ impl Address {
|
|||
|
||||
pub fn raw(&self) -> &[u8] {
|
||||
match self {
|
||||
Self::Mailbox(m) => m.raw.as_slice(),
|
||||
Self::Group(g) => g.raw.as_slice(),
|
||||
Address::Mailbox(m) => m.raw.as_slice(),
|
||||
Address::Group(g) => g.raw.as_slice(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the display name of this address.
|
||||
///
|
||||
/// If it's a group, it's the name of the group. Otherwise it's the
|
||||
/// `display_name` part of the mailbox:
|
||||
/// If it's a group, it's the name of the group. Otherwise it's the `display_name` part of
|
||||
/// the mailbox:
|
||||
///
|
||||
///
|
||||
/// ```text
|
||||
|
@ -176,11 +166,11 @@ impl Address {
|
|||
/// display_name │ display_name │
|
||||
/// │ │
|
||||
/// address_spec address_spec
|
||||
/// ```
|
||||
///```
|
||||
pub fn get_display_name(&self) -> Option<String> {
|
||||
let ret = match self {
|
||||
Self::Mailbox(m) => m.display_name.display(&m.raw),
|
||||
Self::Group(g) => g.display_name.display(&g.raw),
|
||||
Address::Mailbox(m) => m.display_name.display(&m.raw),
|
||||
Address::Group(g) => g.display_name.display(&g.raw),
|
||||
};
|
||||
if ret.is_empty() {
|
||||
None
|
||||
|
@ -189,30 +179,29 @@ impl Address {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get the address spec part of this address. A group returns an empty
|
||||
/// `String`.
|
||||
/// Get the address spec part of this address. A group returns an empty `String`.
|
||||
pub fn get_email(&self) -> String {
|
||||
match self {
|
||||
Self::Mailbox(m) => m.address_spec.display(&m.raw),
|
||||
Self::Group(_) => String::new(),
|
||||
Address::Mailbox(m) => m.address_spec.display(&m.raw),
|
||||
Address::Group(_) => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn address_spec_raw(&self) -> &[u8] {
|
||||
match self {
|
||||
Self::Mailbox(m) => m.address_spec.display_bytes(&m.raw),
|
||||
Self::Group(g) => &g.raw,
|
||||
Address::Mailbox(m) => m.address_spec.display_bytes(&m.raw),
|
||||
Address::Group(g) => &g.raw,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_fqdn(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::Mailbox(m) => {
|
||||
Address::Mailbox(m) => {
|
||||
let raw_address = m.address_spec.display_bytes(&m.raw);
|
||||
let fqdn_pos = raw_address.iter().position(|&b| b == b'@')? + 1;
|
||||
Some(String::from_utf8_lossy(&raw_address[fqdn_pos..]).into())
|
||||
}
|
||||
Self::Group(_) => None,
|
||||
Address::Group(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -231,16 +220,16 @@ impl Address {
|
|||
.collect::<_>()
|
||||
}
|
||||
|
||||
pub fn list_try_from<T: AsRef<[u8]>>(val: T) -> Result<Vec<Self>> {
|
||||
Ok(parser::address::rfc2822address_list(val.as_ref())?
|
||||
pub fn list_try_from(val: &str) -> Result<Vec<Address>> {
|
||||
Ok(parser::address::rfc2822address_list(val.as_bytes())?
|
||||
.1
|
||||
.to_vec())
|
||||
}
|
||||
|
||||
pub fn contains_address(&self, other: &Self) -> bool {
|
||||
pub fn contains_address(&self, other: &Address) -> bool {
|
||||
match self {
|
||||
Self::Mailbox(_) => self == other,
|
||||
Self::Group(g) => g
|
||||
Address::Mailbox(_) => self == other,
|
||||
Address::Group(g) => g
|
||||
.mailbox_list
|
||||
.iter()
|
||||
.any(|addr| addr.contains_address(other)),
|
||||
|
@ -249,8 +238,8 @@ impl Address {
|
|||
|
||||
/// Get subaddress out of an address (e.g. `ken+subaddress@example.org`).
|
||||
///
|
||||
/// Subaddresses are commonly text following a "+" character in an email
|
||||
/// address's local part . They are defined in [RFC5233 `Sieve Email Filtering: Subaddress Extension`](https://tools.ietf.org/html/rfc5233.html)
|
||||
/// Subaddresses are commonly text following a "+" character in an email address's local part
|
||||
/// . They are defined in [RFC5233 `Sieve Email Filtering: Subaddress Extension`](https://tools.ietf.org/html/rfc5233.html)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
|
@ -269,11 +258,11 @@ impl Address {
|
|||
/// ```
|
||||
pub fn subaddress(&self, separator: &str) -> Option<(Self, String)> {
|
||||
match self {
|
||||
Self::Mailbox(_) => {
|
||||
Address::Mailbox(_) => {
|
||||
let email = self.get_email();
|
||||
let (local_part, domain) =
|
||||
match super::parser::address::addr_spec_raw(email.as_bytes())
|
||||
.map_err(Into::<Error>::into)
|
||||
.map_err(|err| Into::<MeliError>::into(err))
|
||||
.and_then(|(_, (l, d))| {
|
||||
Ok((String::from_utf8(l.into())?, String::from_utf8(d.into())?))
|
||||
}) {
|
||||
|
@ -292,18 +281,7 @@ impl Address {
|
|||
subaddress.to_string(),
|
||||
))
|
||||
}
|
||||
Self::Group(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the display name of this address in bytes.
|
||||
///
|
||||
/// For a string, see the [`display_name`](fn@Self::get_display_name)
|
||||
/// method.
|
||||
pub fn display_name_bytes(&self) -> &[u8] {
|
||||
match self {
|
||||
Self::Mailbox(m) => m.display_name.display_bytes(&m.raw),
|
||||
Self::Group(g) => g.display_name.display_bytes(&g.raw),
|
||||
Address::Group(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -311,12 +289,14 @@ impl Address {
|
|||
impl Eq for Address {}
|
||||
|
||||
impl PartialEq for Address {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
fn eq(&self, other: &Address) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Mailbox(_), Self::Group(_)) | (Self::Group(_), Self::Mailbox(_)) => false,
|
||||
(Self::Mailbox(s), Self::Mailbox(o)) => s == o,
|
||||
(Self::Group(s), Self::Group(o)) => {
|
||||
self.display_name_bytes() == other.display_name_bytes()
|
||||
(Address::Mailbox(_), Address::Group(_)) | (Address::Group(_), Address::Mailbox(_)) => {
|
||||
false
|
||||
}
|
||||
(Address::Mailbox(s), Address::Mailbox(o)) => s == o,
|
||||
(Address::Group(s), Address::Group(o)) => {
|
||||
s.display_name.display_bytes(&s.raw) == o.display_name.display_bytes(&o.raw)
|
||||
&& s.mailbox_list.iter().collect::<HashSet<_>>()
|
||||
== o.mailbox_list.iter().collect::<HashSet<_>>()
|
||||
}
|
||||
|
@ -327,10 +307,10 @@ impl PartialEq for Address {
|
|||
impl Hash for Address {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
match self {
|
||||
Self::Mailbox(s) => {
|
||||
Address::Mailbox(s) => {
|
||||
s.address_spec.display_bytes(&s.raw).hash(state);
|
||||
}
|
||||
Self::Group(s) => {
|
||||
Address::Group(s) => {
|
||||
s.display_name.display_bytes(&s.raw).hash(state);
|
||||
for sub in &s.mailbox_list {
|
||||
sub.hash(state);
|
||||
|
@ -343,13 +323,13 @@ impl Hash for Address {
|
|||
impl core::fmt::Display for Address {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::Mailbox(m) if m.display_name.length > 0 => match m.display_name.display(&m.raw) {
|
||||
d if d.contains('.') || d.contains(',') => {
|
||||
write!(f, "\"{}\" <{}>", d, m.address_spec.display(&m.raw))
|
||||
}
|
||||
d => write!(f, "{} <{}>", d, m.address_spec.display(&m.raw)),
|
||||
},
|
||||
Self::Group(g) => {
|
||||
Address::Mailbox(m) if m.display_name.length > 0 => write!(
|
||||
f,
|
||||
"{} <{}>",
|
||||
m.display_name.display(&m.raw),
|
||||
m.address_spec.display(&m.raw)
|
||||
),
|
||||
Address::Group(g) => {
|
||||
let attachment_strings: Vec<String> =
|
||||
g.mailbox_list.iter().map(|a| format!("{}", a)).collect();
|
||||
write!(
|
||||
|
@ -359,7 +339,7 @@ impl core::fmt::Display for Address {
|
|||
attachment_strings.join(", ")
|
||||
)
|
||||
}
|
||||
Self::Mailbox(m) => write!(f, "{}", m.address_spec.display(&m.raw)),
|
||||
Address::Mailbox(m) => write!(f, "{}", m.address_spec.display(&m.raw)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -367,12 +347,12 @@ impl core::fmt::Display for Address {
|
|||
impl core::fmt::Debug for Address {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::Mailbox(m) => f
|
||||
Address::Mailbox(m) => f
|
||||
.debug_struct("Address::Mailbox")
|
||||
.field("display_name", &m.display_name.display(&m.raw))
|
||||
.field("address_spec", &m.address_spec.display(&m.raw))
|
||||
.finish(),
|
||||
Self::Group(g) => {
|
||||
Address::Group(g) => {
|
||||
let attachment_strings: Vec<String> =
|
||||
g.mailbox_list.iter().map(|a| format!("{}", a)).collect();
|
||||
|
||||
|
@ -386,9 +366,8 @@ impl core::fmt::Debug for Address {
|
|||
}
|
||||
|
||||
impl TryFrom<&str> for Address {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(val: &str) -> Result<Self> {
|
||||
type Error = MeliError;
|
||||
fn try_from(val: &str) -> Result<Address> {
|
||||
Ok(parser::address::address(val.as_bytes())?.1)
|
||||
}
|
||||
}
|
||||
|
@ -411,7 +390,7 @@ pub trait StrBuild {
|
|||
}
|
||||
|
||||
impl StrBuilder {
|
||||
pub fn display(&self, s: &[u8]) -> String {
|
||||
pub fn display<'a>(&self, s: &'a [u8]) -> String {
|
||||
let offset = self.offset;
|
||||
let length = self.length;
|
||||
String::from_utf8_lossy(&s[offset..offset + length]).to_string()
|
||||
|
@ -429,7 +408,7 @@ pub struct MessageID(pub Vec<u8>, pub StrBuilder);
|
|||
impl StrBuild for MessageID {
|
||||
fn new(string: &[u8], slice: &[u8]) -> Self {
|
||||
let offset = string.find(slice).unwrap_or(0);
|
||||
Self(
|
||||
MessageID(
|
||||
string.to_owned(),
|
||||
StrBuilder {
|
||||
offset,
|
||||
|
@ -437,13 +416,11 @@ impl StrBuild for MessageID {
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn raw(&self) -> &[u8] {
|
||||
let offset = self.1.offset;
|
||||
let length = self.1.length;
|
||||
&self.0[offset..offset + length.saturating_sub(1)]
|
||||
}
|
||||
|
||||
fn val(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
|
@ -478,11 +455,10 @@ impl core::fmt::Display for MessageID {
|
|||
}
|
||||
|
||||
impl PartialEq for MessageID {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
fn eq(&self, other: &MessageID) -> bool {
|
||||
self.raw() == other.raw()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for MessageID {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "{}", String::from_utf8(self.raw().to_vec()).unwrap())
|
||||
|
|
|
@ -18,20 +18,15 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
use std::{
|
||||
fmt::{Display, Formatter, Result as FmtResult},
|
||||
str,
|
||||
};
|
||||
use crate::email::attachments::{Attachment, AttachmentBuilder};
|
||||
use crate::email::parser::BytesExt;
|
||||
|
||||
use crate::email::{
|
||||
attachments::{Attachment, AttachmentBuilder},
|
||||
parser::BytesExt,
|
||||
};
|
||||
use std::fmt::{Display, Formatter, Result as FmtResult};
|
||||
use std::str;
|
||||
|
||||
#[derive(Clone, Default, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Charset {
|
||||
Ascii,
|
||||
#[default]
|
||||
UTF8,
|
||||
UTF16,
|
||||
ISO8859_1,
|
||||
|
@ -53,7 +48,6 @@ pub enum Charset {
|
|||
Windows1253,
|
||||
GBK,
|
||||
GB2312,
|
||||
GB18030,
|
||||
BIG5,
|
||||
ISO2022JP,
|
||||
EUCJP,
|
||||
|
@ -61,102 +55,105 @@ pub enum Charset {
|
|||
KOI8U,
|
||||
}
|
||||
|
||||
impl Default for Charset {
|
||||
fn default() -> Self {
|
||||
Charset::UTF8
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [u8]> for Charset {
|
||||
fn from(b: &'a [u8]) -> Self {
|
||||
match b.trim() {
|
||||
b if b.eq_ignore_ascii_case(b"us-ascii") || b.eq_ignore_ascii_case(b"ascii") => {
|
||||
Self::Ascii
|
||||
Charset::Ascii
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"utf-8") || b.eq_ignore_ascii_case(b"utf8") => {
|
||||
Charset::UTF8
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"utf-8") || b.eq_ignore_ascii_case(b"utf8") => Self::UTF8,
|
||||
b if b.eq_ignore_ascii_case(b"utf-16") || b.eq_ignore_ascii_case(b"utf16") => {
|
||||
Self::UTF16
|
||||
Charset::UTF16
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-1") || b.eq_ignore_ascii_case(b"iso8859-1") => {
|
||||
Self::ISO8859_1
|
||||
Charset::ISO8859_1
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-2") || b.eq_ignore_ascii_case(b"iso8859-2") => {
|
||||
Self::ISO8859_2
|
||||
Charset::ISO8859_2
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-3") || b.eq_ignore_ascii_case(b"iso8859-3") => {
|
||||
Self::ISO8859_3
|
||||
Charset::ISO8859_3
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-4") || b.eq_ignore_ascii_case(b"iso8859-4") => {
|
||||
Self::ISO8859_4
|
||||
Charset::ISO8859_4
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-5") || b.eq_ignore_ascii_case(b"iso8859-5") => {
|
||||
Self::ISO8859_5
|
||||
Charset::ISO8859_5
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-6") || b.eq_ignore_ascii_case(b"iso8859-6") => {
|
||||
Self::ISO8859_6
|
||||
Charset::ISO8859_6
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-7") || b.eq_ignore_ascii_case(b"iso8859-7") => {
|
||||
Self::ISO8859_7
|
||||
Charset::ISO8859_7
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-8") || b.eq_ignore_ascii_case(b"iso8859-8") => {
|
||||
Self::ISO8859_8
|
||||
Charset::ISO8859_8
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-10")
|
||||
|| b.eq_ignore_ascii_case(b"iso8859-10") =>
|
||||
{
|
||||
Self::ISO8859_10
|
||||
Charset::ISO8859_10
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-13")
|
||||
|| b.eq_ignore_ascii_case(b"iso8859-13") =>
|
||||
{
|
||||
Self::ISO8859_13
|
||||
Charset::ISO8859_13
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-14")
|
||||
|| b.eq_ignore_ascii_case(b"iso8859-14") =>
|
||||
{
|
||||
Self::ISO8859_14
|
||||
Charset::ISO8859_14
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-15")
|
||||
|| b.eq_ignore_ascii_case(b"iso8859-15") =>
|
||||
{
|
||||
Self::ISO8859_15
|
||||
Charset::ISO8859_15
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-16")
|
||||
|| b.eq_ignore_ascii_case(b"iso8859-16") =>
|
||||
{
|
||||
Self::ISO8859_16
|
||||
Charset::ISO8859_16
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"windows-1250")
|
||||
|| b.eq_ignore_ascii_case(b"windows1250") =>
|
||||
{
|
||||
Self::Windows1250
|
||||
Charset::Windows1250
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"windows-1251")
|
||||
|| b.eq_ignore_ascii_case(b"windows1251") =>
|
||||
{
|
||||
Self::Windows1251
|
||||
Charset::Windows1251
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"windows-1252")
|
||||
|| b.eq_ignore_ascii_case(b"windows1252") =>
|
||||
{
|
||||
Self::Windows1252
|
||||
Charset::Windows1252
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"windows-1253")
|
||||
|| b.eq_ignore_ascii_case(b"windows1253")
|
||||
|| b.eq_ignore_ascii_case(b"cp1253")
|
||||
|| b.eq_ignore_ascii_case(b"cp-1253") =>
|
||||
|| b.eq_ignore_ascii_case(b"windows1253") =>
|
||||
{
|
||||
Self::Windows1253
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"gbk") => Self::GBK,
|
||||
b if b.eq_ignore_ascii_case(b"gb18030") || b.eq_ignore_ascii_case(b"gb-18030") => {
|
||||
Self::GB18030
|
||||
Charset::Windows1253
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"gbk") => Charset::GBK,
|
||||
b if b.eq_ignore_ascii_case(b"gb2312") || b.eq_ignore_ascii_case(b"gb-2312") => {
|
||||
Self::GB2312
|
||||
Charset::GB2312
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"big5") => Self::BIG5,
|
||||
b if b.eq_ignore_ascii_case(b"iso-2022-jp") => Self::ISO2022JP,
|
||||
b if b.eq_ignore_ascii_case(b"euc-jp") => Self::EUCJP,
|
||||
b if b.eq_ignore_ascii_case(b"koi8-r") => Self::KOI8R,
|
||||
b if b.eq_ignore_ascii_case(b"koi8-u") => Self::KOI8U,
|
||||
b if b.eq_ignore_ascii_case(b"big5") => Charset::BIG5,
|
||||
b if b.eq_ignore_ascii_case(b"iso-2022-jp") => Charset::ISO2022JP,
|
||||
b if b.eq_ignore_ascii_case(b"euc-jp") => Charset::EUCJP,
|
||||
b if b.eq_ignore_ascii_case(b"koi8-r") => Charset::KOI8R,
|
||||
b if b.eq_ignore_ascii_case(b"koi8-u") => Charset::KOI8U,
|
||||
_ => {
|
||||
debug!("unknown tag is {:?}", str::from_utf8(b));
|
||||
Self::Ascii
|
||||
Charset::Ascii
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -165,80 +162,84 @@ impl<'a> From<&'a [u8]> for Charset {
|
|||
impl Display for Charset {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
match self {
|
||||
Self::Ascii => write!(f, "us-ascii"),
|
||||
Self::UTF8 => write!(f, "utf-8"),
|
||||
Self::UTF16 => write!(f, "utf-16"),
|
||||
Self::ISO8859_1 => write!(f, "iso-8859-1"),
|
||||
Self::ISO8859_2 => write!(f, "iso-8859-2"),
|
||||
Self::ISO8859_3 => write!(f, "iso-8859-3"),
|
||||
Self::ISO8859_4 => write!(f, "iso-8859-4"),
|
||||
Self::ISO8859_5 => write!(f, "iso-8859-5"),
|
||||
Self::ISO8859_6 => write!(f, "iso-8859-6"),
|
||||
Self::ISO8859_7 => write!(f, "iso-8859-7"),
|
||||
Self::ISO8859_8 => write!(f, "iso-8859-8"),
|
||||
Self::ISO8859_10 => write!(f, "iso-8859-10"),
|
||||
Self::ISO8859_13 => write!(f, "iso-8859-13"),
|
||||
Self::ISO8859_14 => write!(f, "iso-8859-14"),
|
||||
Self::ISO8859_15 => write!(f, "iso-8859-15"),
|
||||
Self::ISO8859_16 => write!(f, "iso-8859-16"),
|
||||
Self::Windows1250 => write!(f, "windows-1250"),
|
||||
Self::Windows1251 => write!(f, "windows-1251"),
|
||||
Self::Windows1252 => write!(f, "windows-1252"),
|
||||
Self::Windows1253 => write!(f, "windows-1253"),
|
||||
Self::GBK => write!(f, "gbk"),
|
||||
Self::GB2312 => write!(f, "gb2312"),
|
||||
Self::GB18030 => write!(f, "gb18030"),
|
||||
Self::BIG5 => write!(f, "big5"),
|
||||
Self::ISO2022JP => write!(f, "iso-2022-jp"),
|
||||
Self::EUCJP => write!(f, "euc-jp"),
|
||||
Self::KOI8R => write!(f, "koi8-r"),
|
||||
Self::KOI8U => write!(f, "koi8-u"),
|
||||
Charset::Ascii => write!(f, "us-ascii"),
|
||||
Charset::UTF8 => write!(f, "utf-8"),
|
||||
Charset::UTF16 => write!(f, "utf-16"),
|
||||
Charset::ISO8859_1 => write!(f, "iso-8859-1"),
|
||||
Charset::ISO8859_2 => write!(f, "iso-8859-2"),
|
||||
Charset::ISO8859_3 => write!(f, "iso-8859-3"),
|
||||
Charset::ISO8859_4 => write!(f, "iso-8859-4"),
|
||||
Charset::ISO8859_5 => write!(f, "iso-8859-5"),
|
||||
Charset::ISO8859_6 => write!(f, "iso-8859-6"),
|
||||
Charset::ISO8859_7 => write!(f, "iso-8859-7"),
|
||||
Charset::ISO8859_8 => write!(f, "iso-8859-8"),
|
||||
Charset::ISO8859_10 => write!(f, "iso-8859-10"),
|
||||
Charset::ISO8859_13 => write!(f, "iso-8859-13"),
|
||||
Charset::ISO8859_14 => write!(f, "iso-8859-14"),
|
||||
Charset::ISO8859_15 => write!(f, "iso-8859-15"),
|
||||
Charset::ISO8859_16 => write!(f, "iso-8859-16"),
|
||||
Charset::Windows1250 => write!(f, "windows-1250"),
|
||||
Charset::Windows1251 => write!(f, "windows-1251"),
|
||||
Charset::Windows1252 => write!(f, "windows-1252"),
|
||||
Charset::Windows1253 => write!(f, "windows-1253"),
|
||||
Charset::GBK => write!(f, "gbk"),
|
||||
Charset::GB2312 => write!(f, "gb2312"),
|
||||
Charset::BIG5 => write!(f, "big5"),
|
||||
Charset::ISO2022JP => write!(f, "iso-2022-jp"),
|
||||
Charset::EUCJP => write!(f, "euc-jp"),
|
||||
Charset::KOI8R => write!(f, "koi8-r"),
|
||||
Charset::KOI8U => write!(f, "koi8-u"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum MultipartType {
|
||||
Alternative,
|
||||
Digest,
|
||||
Encrypted,
|
||||
#[default]
|
||||
Mixed,
|
||||
Related,
|
||||
Signed,
|
||||
}
|
||||
|
||||
impl Default for MultipartType {
|
||||
fn default() -> Self {
|
||||
MultipartType::Mixed
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MultipartType {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Alternative => "multipart/alternative",
|
||||
Self::Digest => "multipart/digest",
|
||||
Self::Encrypted => "multipart/encrypted",
|
||||
Self::Mixed => "multipart/mixed",
|
||||
Self::Related => "multipart/related",
|
||||
Self::Signed => "multipart/signed",
|
||||
MultipartType::Alternative => "multipart/alternative",
|
||||
MultipartType::Digest => "multipart/digest",
|
||||
MultipartType::Encrypted => "multipart/encrypted",
|
||||
MultipartType::Mixed => "multipart/mixed",
|
||||
MultipartType::Related => "multipart/related",
|
||||
MultipartType::Signed => "multipart/signed",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for MultipartType {
|
||||
fn from(val: &[u8]) -> Self {
|
||||
fn from(val: &[u8]) -> MultipartType {
|
||||
if val.eq_ignore_ascii_case(b"mixed") {
|
||||
Self::Mixed
|
||||
MultipartType::Mixed
|
||||
} else if val.eq_ignore_ascii_case(b"alternative") {
|
||||
Self::Alternative
|
||||
MultipartType::Alternative
|
||||
} else if val.eq_ignore_ascii_case(b"digest") {
|
||||
Self::Digest
|
||||
MultipartType::Digest
|
||||
} else if val.eq_ignore_ascii_case(b"encrypted") {
|
||||
Self::Encrypted
|
||||
MultipartType::Encrypted
|
||||
} else if val.eq_ignore_ascii_case(b"signed") {
|
||||
Self::Signed
|
||||
MultipartType::Signed
|
||||
} else if val.eq_ignore_ascii_case(b"related") {
|
||||
Self::Related
|
||||
MultipartType::Related
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
|
@ -254,7 +255,6 @@ pub enum ContentType {
|
|||
},
|
||||
Multipart {
|
||||
boundary: Vec<u8>,
|
||||
parameters: Vec<(Vec<u8>, Vec<u8>)>,
|
||||
kind: MultipartType,
|
||||
parts: Vec<Attachment>,
|
||||
},
|
||||
|
@ -264,17 +264,15 @@ pub enum ContentType {
|
|||
Other {
|
||||
tag: Vec<u8>,
|
||||
name: Option<String>,
|
||||
parameters: Vec<(Vec<u8>, Vec<u8>)>,
|
||||
},
|
||||
OctetStream {
|
||||
name: Option<String>,
|
||||
parameters: Vec<(Vec<u8>, Vec<u8>)>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for ContentType {
|
||||
fn default() -> Self {
|
||||
Self::Text {
|
||||
ContentType::Text {
|
||||
kind: Text::Plain,
|
||||
parameters: Vec::new(),
|
||||
charset: Charset::UTF8,
|
||||
|
@ -282,106 +280,107 @@ impl Default for ContentType {
|
|||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&[u8]> for ContentType {
|
||||
fn eq(&self, other: &&[u8]) -> bool {
|
||||
impl PartialEq<&str> for ContentType {
|
||||
fn eq(&self, other: &&str) -> bool {
|
||||
match (self, *other) {
|
||||
(
|
||||
Self::Text {
|
||||
ContentType::Text {
|
||||
kind: Text::Plain, ..
|
||||
},
|
||||
b"text/plain",
|
||||
"text/plain",
|
||||
) => true,
|
||||
(
|
||||
Self::Text {
|
||||
ContentType::Text {
|
||||
kind: Text::Html, ..
|
||||
},
|
||||
b"text/html",
|
||||
"text/html",
|
||||
) => true,
|
||||
(
|
||||
Self::Multipart {
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Alternative,
|
||||
..
|
||||
},
|
||||
b"multipart/alternative",
|
||||
"multipart/alternative",
|
||||
) => true,
|
||||
(
|
||||
Self::Multipart {
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Digest,
|
||||
..
|
||||
},
|
||||
b"multipart/digest",
|
||||
"multipart/digest",
|
||||
) => true,
|
||||
(
|
||||
Self::Multipart {
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Encrypted,
|
||||
..
|
||||
},
|
||||
b"multipart/encrypted",
|
||||
"multipart/encrypted",
|
||||
) => true,
|
||||
(
|
||||
Self::Multipart {
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Mixed,
|
||||
..
|
||||
},
|
||||
b"multipart/mixed",
|
||||
"multipart/mixed",
|
||||
) => true,
|
||||
(
|
||||
Self::Multipart {
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Related,
|
||||
..
|
||||
},
|
||||
b"multipart/related",
|
||||
"multipart/related",
|
||||
) => true,
|
||||
(
|
||||
Self::Multipart {
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Signed,
|
||||
..
|
||||
},
|
||||
b"multipart/signed",
|
||||
"multipart/signed",
|
||||
) => true,
|
||||
(Self::PGPSignature, b"application/pgp-signature") => true,
|
||||
(Self::CMSSignature, b"application/pkcs7-signature") => true,
|
||||
(Self::MessageRfc822, b"message/rfc822") => true,
|
||||
(Self::Other { tag, .. }, _) => other.eq_ignore_ascii_case(tag),
|
||||
(Self::OctetStream { .. }, b"application/octet-stream") => true,
|
||||
(ContentType::PGPSignature, "application/pgp-signature") => true,
|
||||
(ContentType::CMSSignature, "application/pkcs7-signature") => true,
|
||||
(ContentType::MessageRfc822, "message/rfc822") => true,
|
||||
(ContentType::Other { tag, .. }, _) => {
|
||||
other.eq_ignore_ascii_case(&String::from_utf8_lossy(&tag))
|
||||
}
|
||||
(ContentType::OctetStream { .. }, "application/octet-stream") => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&str> for ContentType {
|
||||
fn eq(&self, other: &&str) -> bool {
|
||||
self.eq(&other.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ContentType {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
match self {
|
||||
Self::Text { kind: t, .. } => t.fmt(f),
|
||||
Self::Multipart { kind: k, .. } => k.fmt(f),
|
||||
Self::Other { ref tag, .. } => write!(f, "{}", String::from_utf8_lossy(tag)),
|
||||
Self::PGPSignature => write!(f, "application/pgp-signature"),
|
||||
Self::CMSSignature => write!(f, "application/pkcs7-signature"),
|
||||
Self::MessageRfc822 => write!(f, "message/rfc822"),
|
||||
Self::OctetStream { .. } => write!(f, "application/octet-stream"),
|
||||
ContentType::Text { kind: t, .. } => t.fmt(f),
|
||||
ContentType::Multipart { kind: k, .. } => k.fmt(f),
|
||||
ContentType::Other { ref tag, .. } => write!(f, "{}", String::from_utf8_lossy(tag)),
|
||||
ContentType::PGPSignature => write!(f, "application/pgp-signature"),
|
||||
ContentType::CMSSignature => write!(f, "application/pkcs7-signature"),
|
||||
ContentType::MessageRfc822 => write!(f, "message/rfc822"),
|
||||
ContentType::OctetStream { .. } => write!(f, "application/octet-stream"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContentType {
|
||||
pub fn is_text(&self) -> bool {
|
||||
matches!(self, Self::Text { .. })
|
||||
if let ContentType::Text { .. } = self {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_text_html(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Text {
|
||||
kind: Text::Html,
|
||||
..
|
||||
}
|
||||
)
|
||||
if let ContentType::Text {
|
||||
kind: Text::Html, ..
|
||||
} = self
|
||||
{
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_boundary(parts: &[AttachmentBuilder]) -> String {
|
||||
|
@ -413,27 +412,23 @@ impl ContentType {
|
|||
|
||||
boundary.push_str(&random_boundary);
|
||||
/* rfc134
|
||||
* "The only mandatory parameter for the multipart Content-Type is the
|
||||
* boundary parameter, which consists of 1 to 70 characters from a
|
||||
* set of characters known to be very robust through email gateways,
|
||||
* and NOT ending with white space" */
|
||||
* "The only mandatory parameter for the multipart Content-Type is the boundary parameter,
|
||||
* which consists of 1 to 70 characters from a set of characters known to be very robust
|
||||
* through email gateways, and NOT ending with white space"*/
|
||||
boundary.truncate(70);
|
||||
boundary
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Other { ref name, .. } => name.as_ref().map(|n| n.as_ref()),
|
||||
Self::OctetStream {
|
||||
ref name,
|
||||
parameters: _,
|
||||
} => name.as_ref().map(|n| n.as_ref()),
|
||||
ContentType::Other { ref name, .. } => name.as_ref().map(|n| n.as_ref()),
|
||||
ContentType::OctetStream { ref name } => name.as_ref().map(|n| n.as_ref()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parts(&self) -> Option<&[Attachment]> {
|
||||
if let Self::Multipart { ref parts, .. } = self {
|
||||
if let ContentType::Multipart { ref parts, .. } = self {
|
||||
Some(parts)
|
||||
} else {
|
||||
None
|
||||
|
@ -451,58 +446,65 @@ pub enum Text {
|
|||
|
||||
impl Text {
|
||||
pub fn is_html(&self) -> bool {
|
||||
matches!(self, Self::Html)
|
||||
if let Text::Html = self {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Text {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
match *self {
|
||||
Self::Plain => write!(f, "text/plain"),
|
||||
Self::Html => write!(f, "text/html"),
|
||||
Self::Rfc822 => write!(f, "text/rfc822"),
|
||||
Self::Other { tag: ref t } => write!(f, "text/{}", String::from_utf8_lossy(t)),
|
||||
Text::Plain => write!(f, "text/plain"),
|
||||
Text::Html => write!(f, "text/html"),
|
||||
Text::Rfc822 => write!(f, "text/rfc822"),
|
||||
Text::Other { tag: ref t } => write!(f, "text/{}", String::from_utf8_lossy(t)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum ContentTransferEncoding {
|
||||
#[default]
|
||||
_8Bit,
|
||||
_7Bit,
|
||||
Base64,
|
||||
QuotedPrintable,
|
||||
Other {
|
||||
tag: Vec<u8>,
|
||||
},
|
||||
Other { tag: Vec<u8> },
|
||||
}
|
||||
|
||||
impl Default for ContentTransferEncoding {
|
||||
fn default() -> Self {
|
||||
ContentTransferEncoding::_8Bit
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ContentTransferEncoding {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
match *self {
|
||||
Self::_7Bit => write!(f, "7bit"),
|
||||
Self::_8Bit => write!(f, "8bit"),
|
||||
Self::Base64 => write!(f, "base64"),
|
||||
Self::QuotedPrintable => write!(f, "quoted-printable"),
|
||||
Self::Other { tag: ref t } => {
|
||||
ContentTransferEncoding::_7Bit => write!(f, "7bit"),
|
||||
ContentTransferEncoding::_8Bit => write!(f, "8bit"),
|
||||
ContentTransferEncoding::Base64 => write!(f, "base64"),
|
||||
ContentTransferEncoding::QuotedPrintable => write!(f, "quoted-printable"),
|
||||
ContentTransferEncoding::Other { tag: ref t } => {
|
||||
panic!("unknown encoding {:?}", str::from_utf8(t))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<&[u8]> for ContentTransferEncoding {
|
||||
fn from(val: &[u8]) -> Self {
|
||||
fn from(val: &[u8]) -> ContentTransferEncoding {
|
||||
if val.eq_ignore_ascii_case(b"base64") {
|
||||
Self::Base64
|
||||
ContentTransferEncoding::Base64
|
||||
} else if val.eq_ignore_ascii_case(b"7bit") {
|
||||
Self::_7Bit
|
||||
ContentTransferEncoding::_7Bit
|
||||
} else if val.eq_ignore_ascii_case(b"8bit") {
|
||||
Self::_8Bit
|
||||
ContentTransferEncoding::_8Bit
|
||||
} else if val.eq_ignore_ascii_case(b"quoted-printable") {
|
||||
Self::QuotedPrintable
|
||||
ContentTransferEncoding::QuotedPrintable
|
||||
} else {
|
||||
Self::Other {
|
||||
ContentTransferEncoding::Other {
|
||||
tag: val.to_ascii_lowercase(),
|
||||
}
|
||||
}
|
||||
|
@ -520,44 +522,40 @@ pub struct ContentDisposition {
|
|||
pub parameter: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum ContentDispositionKind {
|
||||
#[default]
|
||||
Inline,
|
||||
Attachment,
|
||||
}
|
||||
|
||||
impl ContentDispositionKind {
|
||||
pub fn is_inline(&self) -> bool {
|
||||
matches!(self, Self::Inline)
|
||||
*self == ContentDispositionKind::Inline
|
||||
}
|
||||
|
||||
pub fn is_attachment(&self) -> bool {
|
||||
matches!(self, Self::Attachment)
|
||||
*self == ContentDispositionKind::Attachment
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ContentDispositionKind {
|
||||
fn default() -> Self {
|
||||
ContentDispositionKind::Inline
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ContentDispositionKind {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
match *self {
|
||||
Self::Inline => write!(f, "inline"),
|
||||
Self::Attachment => write!(f, "attachment"),
|
||||
ContentDispositionKind::Inline => write!(f, "inline"),
|
||||
ContentDispositionKind::Attachment => write!(f, "attachment"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<&[u8]> for ContentDisposition {
|
||||
fn from(val: &[u8]) -> Self {
|
||||
fn from(val: &[u8]) -> ContentDisposition {
|
||||
crate::email::parser::attachments::content_disposition(val)
|
||||
.map(|(_, v)| v)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ContentDispositionKind> for ContentDisposition {
|
||||
fn from(kind: ContentDispositionKind) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,50 +19,20 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Attachment encoding and decoding.
|
||||
|
||||
use core::{fmt, str};
|
||||
|
||||
/*! Encoding/decoding of attachments */
|
||||
use crate::email::{
|
||||
address::StrBuilder,
|
||||
parser::{self, BytesExt},
|
||||
Mail,
|
||||
};
|
||||
use core::fmt;
|
||||
use core::str;
|
||||
use data_encoding::BASE64_MIME;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
email::{
|
||||
address::StrBuilder,
|
||||
attachment_types::*,
|
||||
parser::{self, BytesExt},
|
||||
Mail,
|
||||
},
|
||||
BytesDisplay,
|
||||
};
|
||||
|
||||
/// Type alias for function that takes an [`Attachment`] and appends a bytes
|
||||
/// representation in its second argument.
|
||||
pub type Filter<'a> = Box<dyn FnMut(&Attachment, &mut Vec<u8>) + 'a>;
|
||||
|
||||
#[derive(Default)]
|
||||
/// Options for decoding an [`Attachment`].
|
||||
pub struct DecodeOptions<'att> {
|
||||
/// [`Filter`] to use.
|
||||
pub filter: Option<Filter<'att>>,
|
||||
/// Override the attachment's [`Charset`], if any, with a user-provided
|
||||
/// value.
|
||||
pub force_charset: Option<Charset>,
|
||||
}
|
||||
|
||||
impl<'att> From<Option<Charset>> for DecodeOptions<'att> {
|
||||
fn from(force_charset: Option<Charset>) -> DecodeOptions<'att> {
|
||||
Self {
|
||||
filter: None,
|
||||
force_charset,
|
||||
}
|
||||
}
|
||||
}
|
||||
use crate::email::attachment_types::*;
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
/// A struct analogous to [`Attachment`] which can have incomplete and partial
|
||||
/// content before being turned into an [`Attachment`] with
|
||||
/// [`AttachmentBuilder::build`].
|
||||
pub struct AttachmentBuilder {
|
||||
pub content_type: ContentType,
|
||||
pub content_transfer_encoding: ContentTransferEncoding,
|
||||
|
@ -76,13 +46,13 @@ impl AttachmentBuilder {
|
|||
pub fn new(content: &[u8]) -> Self {
|
||||
let (headers, body) = match parser::attachments::attachment(content) {
|
||||
Ok((_, v)) => v,
|
||||
Err(err) => {
|
||||
log::debug!("error in parsing attachment: {}", err);
|
||||
log::debug!("\n-------------------------------");
|
||||
log::debug!("{}\n", String::from_utf8_lossy(content));
|
||||
log::debug!("-------------------------------\n");
|
||||
Err(_) => {
|
||||
debug!("error in parsing attachment");
|
||||
debug!("\n-------------------------------");
|
||||
debug!("{}\n", ::std::string::String::from_utf8_lossy(content));
|
||||
debug!("-------------------------------\n");
|
||||
|
||||
return Self {
|
||||
return AttachmentBuilder {
|
||||
content_type: Default::default(),
|
||||
content_transfer_encoding: ContentTransferEncoding::_7Bit,
|
||||
content_disposition: ContentDisposition::default(),
|
||||
|
@ -100,7 +70,7 @@ impl AttachmentBuilder {
|
|||
offset: content.len() - body.len(),
|
||||
length: body.len(),
|
||||
};
|
||||
let mut builder = Self {
|
||||
let mut builder = AttachmentBuilder {
|
||||
raw,
|
||||
body,
|
||||
..Default::default()
|
||||
|
@ -130,9 +100,8 @@ impl AttachmentBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set body to the entire raw contents, use this if raw contains only data
|
||||
/// and no headers If raw contains data and headers pass it through
|
||||
/// AttachmentBuilder::new().
|
||||
/// Set body to the entire raw contents, use this if raw contains only data and no headers
|
||||
/// If raw contains data and headers pass it through AttachmentBuilder::new().
|
||||
pub fn set_body_to_raw(&mut self) -> &mut Self {
|
||||
self.body = StrBuilder {
|
||||
offset: 0,
|
||||
|
@ -173,23 +142,19 @@ impl AttachmentBuilder {
|
|||
Ok((_, (ct, cst, params))) => {
|
||||
if ct.eq_ignore_ascii_case(b"multipart") {
|
||||
let mut boundary = None;
|
||||
for (n, v) in ¶ms {
|
||||
for (n, v) in params {
|
||||
if n.eq_ignore_ascii_case(b"boundary") {
|
||||
boundary = Some(v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
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 {
|
||||
boundary,
|
||||
kind: MultipartType::from(cst),
|
||||
parameters: params
|
||||
.into_iter()
|
||||
.map(|(kb, vb)| (kb.to_vec(), vb.to_vec()))
|
||||
.collect::<Vec<(Vec<u8>, Vec<u8>)>>(),
|
||||
parts,
|
||||
};
|
||||
} else {
|
||||
|
@ -243,7 +208,7 @@ impl AttachmentBuilder {
|
|||
self.content_type = ContentType::CMSSignature;
|
||||
} else {
|
||||
let mut name: Option<String> = None;
|
||||
for (n, v) in ¶ms {
|
||||
for (n, v) in params {
|
||||
if n.eq_ignore_ascii_case(b"name") {
|
||||
if let Ok(v) = crate::email::parser::encodings::phrase(v.trim(), false)
|
||||
.as_ref()
|
||||
|
@ -260,21 +225,14 @@ impl AttachmentBuilder {
|
|||
tag.extend(ct);
|
||||
tag.push(b'/');
|
||||
tag.extend(cst);
|
||||
self.content_type = ContentType::Other {
|
||||
tag,
|
||||
name,
|
||||
parameters: params
|
||||
.into_iter()
|
||||
.map(|(kb, vb)| (kb.to_vec(), vb.to_vec()))
|
||||
.collect::<Vec<(Vec<u8>, Vec<u8>)>>(),
|
||||
};
|
||||
self.content_type = ContentType::Other { tag, name };
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::debug!(
|
||||
"parsing error in content_type: {:?} {}",
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"parsing error in content_type: {:?} {:?}",
|
||||
String::from_utf8_lossy(value),
|
||||
err
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -300,14 +258,14 @@ impl AttachmentBuilder {
|
|||
Ok((_, attachments)) => {
|
||||
let mut vec = Vec::with_capacity(attachments.len());
|
||||
for a in attachments {
|
||||
let mut builder = Self::default();
|
||||
let (headers, body) = match parser::attachments::attachment(a) {
|
||||
let mut builder = AttachmentBuilder::default();
|
||||
let (headers, body) = match parser::attachments::attachment(&a) {
|
||||
Ok((_, v)) => v,
|
||||
Err(err) => {
|
||||
log::debug!("error in parsing attachment: {}", err);
|
||||
log::debug!("\n-------------------------------");
|
||||
log::debug!("{}\n", String::from_utf8_lossy(a));
|
||||
log::debug!("-------------------------------\n");
|
||||
Err(_) => {
|
||||
debug!("error in parsing attachment");
|
||||
debug!("\n-------------------------------");
|
||||
debug!("{}\n", ::std::string::String::from_utf8_lossy(a));
|
||||
debug!("-------------------------------\n");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
@ -334,7 +292,7 @@ impl AttachmentBuilder {
|
|||
vec
|
||||
}
|
||||
a => {
|
||||
log::debug!(
|
||||
debug!(
|
||||
"error {:?}\n\traw: {:?}\n\tboundary: {:?}",
|
||||
a,
|
||||
str::from_utf8(raw).unwrap(),
|
||||
|
@ -355,7 +313,7 @@ impl From<Attachment> for AttachmentBuilder {
|
|||
raw,
|
||||
body,
|
||||
} = val;
|
||||
Self {
|
||||
AttachmentBuilder {
|
||||
content_type,
|
||||
content_disposition,
|
||||
content_transfer_encoding,
|
||||
|
@ -374,7 +332,7 @@ impl From<AttachmentBuilder> for Attachment {
|
|||
raw,
|
||||
body,
|
||||
} = val;
|
||||
Self {
|
||||
Attachment {
|
||||
content_type,
|
||||
content_transfer_encoding,
|
||||
content_disposition,
|
||||
|
@ -400,9 +358,9 @@ impl fmt::Debug for Attachment {
|
|||
let mut text = Vec::with_capacity(4096);
|
||||
self.get_text_recursive(&mut text);
|
||||
f.debug_struct("Attachment")
|
||||
.field("Content-Type", &self.content_type)
|
||||
.field("Content-Transfer-Encoding", &self.content_transfer_encoding)
|
||||
.field("bytes", &self.raw.len())
|
||||
.field("content_type", &self.content_type)
|
||||
.field("content_transfer_encoding", &self.content_transfer_encoding)
|
||||
.field("raw bytes length", &self.raw.len())
|
||||
.field("body", &String::from_utf8_lossy(&text))
|
||||
.finish()
|
||||
}
|
||||
|
@ -415,17 +373,17 @@ impl fmt::Display for Attachment {
|
|||
match Mail::new(self.body.display_bytes(&self.raw).to_vec(), None) {
|
||||
Ok(wrapper) => write!(
|
||||
f,
|
||||
"{} {} {} [message/rfc822] {}",
|
||||
wrapper.subject(),
|
||||
"{} - {} - {} [message/rfc822] {}",
|
||||
wrapper.date(),
|
||||
wrapper.field_from_to_string(),
|
||||
wrapper.date_as_str(),
|
||||
BytesDisplay(self.raw.len()),
|
||||
wrapper.subject(),
|
||||
crate::Bytes(self.raw.len()),
|
||||
),
|
||||
Err(err) => write!(
|
||||
f,
|
||||
"could not parse: {} [message/rfc822] {}",
|
||||
err,
|
||||
BytesDisplay(self.raw.len()),
|
||||
crate::Bytes(self.raw.len()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@ -438,14 +396,14 @@ impl fmt::Display for Attachment {
|
|||
"\"{}\", [{}] {}",
|
||||
name,
|
||||
self.mime_type(),
|
||||
BytesDisplay(self.raw.len())
|
||||
crate::Bytes(self.raw.len())
|
||||
)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"Data attachment [{}] {}",
|
||||
self.mime_type(),
|
||||
BytesDisplay(self.raw.len())
|
||||
crate::Bytes(self.raw.len())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -456,14 +414,14 @@ impl fmt::Display for Attachment {
|
|||
"\"{}\", [{}] {}",
|
||||
name,
|
||||
self.mime_type(),
|
||||
BytesDisplay(self.raw.len())
|
||||
crate::Bytes(self.raw.len())
|
||||
)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"Text attachment [{}] {}",
|
||||
self.mime_type(),
|
||||
BytesDisplay(self.raw.len())
|
||||
crate::Bytes(self.raw.len())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -486,7 +444,7 @@ impl Attachment {
|
|||
content_transfer_encoding: ContentTransferEncoding,
|
||||
raw: Vec<u8>,
|
||||
) -> Self {
|
||||
Self {
|
||||
Attachment {
|
||||
content_type,
|
||||
content_disposition: ContentDisposition::default(),
|
||||
content_transfer_encoding,
|
||||
|
@ -516,11 +474,11 @@ impl Attachment {
|
|||
match parser::attachments::multipart_parts(self.body(), boundary) {
|
||||
Ok((_, v)) => v,
|
||||
Err(e) => {
|
||||
log::debug!("error in parsing attachment");
|
||||
log::debug!("\n-------------------------------");
|
||||
log::debug!("{}\n", String::from_utf8_lossy(&self.raw));
|
||||
log::debug!("-------------------------------\n");
|
||||
log::debug!("{:?}\n", e);
|
||||
debug!("error in parsing attachment");
|
||||
debug!("\n-------------------------------");
|
||||
debug!("{}\n", ::std::string::String::from_utf8_lossy(&self.raw));
|
||||
debug!("-------------------------------\n");
|
||||
debug!("{:?}\n", e);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
@ -529,8 +487,8 @@ impl Attachment {
|
|||
}
|
||||
}
|
||||
|
||||
/* Call on the body of a multipart/mixed Envelope to check if there are
|
||||
* attachments without completely parsing them */
|
||||
/* Call on the body of a multipart/mixed Envelope to check if there are attachments without
|
||||
* completely parsing them */
|
||||
pub fn check_if_has_attachments_quick(bytes: &[u8], boundary: &[u8]) -> bool {
|
||||
if bytes.is_empty() {
|
||||
return false;
|
||||
|
@ -558,35 +516,36 @@ impl Attachment {
|
|||
.iter()
|
||||
.find(|(n, _)| n.eq_ignore_ascii_case(b"content-type"))
|
||||
.and_then(|(_, v)| {
|
||||
if let Ok((_, (ct, _cst, params))) =
|
||||
parser::attachments::content_type(v)
|
||||
{
|
||||
if ct.eq_ignore_ascii_case(b"multipart") {
|
||||
let mut boundary = None;
|
||||
for (n, v) in params {
|
||||
if n.eq_ignore_ascii_case(b"boundary") {
|
||||
boundary = Some(v);
|
||||
break;
|
||||
match parser::attachments::content_type(v) {
|
||||
Ok((_, (ct, _cst, params))) => {
|
||||
if ct.eq_ignore_ascii_case(b"multipart") {
|
||||
let mut boundary = None;
|
||||
for (n, v) in params {
|
||||
if n.eq_ignore_ascii_case(b"boundary") {
|
||||
boundary = Some(v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return boundary;
|
||||
}
|
||||
return boundary;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
None
|
||||
})
|
||||
{
|
||||
if Self::check_if_has_attachments_quick(body, boundary) {
|
||||
if Attachment::check_if_has_attachments_quick(body, boundary) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::debug!("error in parsing multipart_parts");
|
||||
log::debug!("\n-------------------------------");
|
||||
log::debug!("{}\n", String::from_utf8_lossy(bytes));
|
||||
log::debug!("-------------------------------\n");
|
||||
log::debug!("{:?}\n", e);
|
||||
debug!("error in parsing multipart_parts");
|
||||
debug!("\n-------------------------------");
|
||||
debug!("{}\n", ::std::string::String::from_utf8_lossy(bytes));
|
||||
debug!("-------------------------------\n");
|
||||
debug!("{:?}\n", e);
|
||||
}
|
||||
}
|
||||
false
|
||||
|
@ -595,21 +554,14 @@ impl Attachment {
|
|||
fn get_text_recursive(&self, text: &mut Vec<u8>) {
|
||||
match self.content_type {
|
||||
ContentType::Text { .. } | ContentType::PGPSignature | ContentType::CMSSignature => {
|
||||
text.extend(self.decode(Default::default()));
|
||||
text.extend(decode(self, None));
|
||||
}
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Related,
|
||||
ref kind,
|
||||
ref parts,
|
||||
ref parameters,
|
||||
..
|
||||
} => {
|
||||
if let Some(main_attachment) = parameters
|
||||
.iter()
|
||||
.find_map(|(k, v)| if k == b"type" { Some(v) } else { None })
|
||||
.and_then(|t| parts.iter().find(|a| a.content_type == t.as_slice()))
|
||||
{
|
||||
main_attachment.get_text_recursive(text);
|
||||
} else {
|
||||
} => match kind {
|
||||
MultipartType::Alternative => {
|
||||
for a in parts {
|
||||
if a.content_disposition.kind.is_inline() {
|
||||
if let ContentType::Text {
|
||||
|
@ -622,37 +574,17 @@ impl Attachment {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Alternative,
|
||||
ref parts,
|
||||
..
|
||||
} => {
|
||||
for a in parts {
|
||||
if a.content_disposition.kind.is_inline() {
|
||||
if let ContentType::Text {
|
||||
kind: Text::Plain, ..
|
||||
} = a.content_type
|
||||
{
|
||||
_ => {
|
||||
for a in parts {
|
||||
if a.content_disposition.kind.is_inline() {
|
||||
a.get_text_recursive(text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ContentType::Multipart {
|
||||
kind: _, ref parts, ..
|
||||
} => {
|
||||
for a in parts {
|
||||
if a.content_disposition.kind.is_inline() {
|
||||
a.get_text_recursive(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text(&self) -> String {
|
||||
let mut text = Vec::with_capacity(self.body.length);
|
||||
self.get_text_recursive(&mut text);
|
||||
|
@ -662,8 +594,7 @@ impl Attachment {
|
|||
pub fn mime_type(&self) -> String {
|
||||
self.content_type.to_string()
|
||||
}
|
||||
|
||||
pub fn attachments(&self) -> Vec<Self> {
|
||||
pub fn attachments(&self) -> Vec<Attachment> {
|
||||
let mut ret = Vec::new();
|
||||
fn count_recursive(att: &Attachment, ret: &mut Vec<Attachment>) {
|
||||
match att.content_type {
|
||||
|
@ -681,26 +612,24 @@ impl Attachment {
|
|||
}
|
||||
}
|
||||
|
||||
count_recursive(self, &mut ret);
|
||||
count_recursive(&self, &mut ret);
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn count_attachments(&self) -> usize {
|
||||
self.attachments().len()
|
||||
}
|
||||
|
||||
pub fn content_type(&self) -> &ContentType {
|
||||
&self.content_type
|
||||
}
|
||||
|
||||
pub fn content_transfer_encoding(&self) -> &ContentTransferEncoding {
|
||||
&self.content_transfer_encoding
|
||||
}
|
||||
|
||||
pub fn is_text(&self) -> bool {
|
||||
matches!(self.content_type, ContentType::Text { .. })
|
||||
match self.content_type {
|
||||
ContentType::Text { .. } => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_html(&self) -> bool {
|
||||
match self.content_type {
|
||||
ContentType::Text {
|
||||
|
@ -713,34 +642,31 @@ impl Attachment {
|
|||
kind: MultipartType::Alternative,
|
||||
ref parts,
|
||||
..
|
||||
} => parts.iter().all(Self::is_html),
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Related,
|
||||
..
|
||||
} => false,
|
||||
ContentType::Multipart { ref parts, .. } => parts.iter().any(Self::is_html),
|
||||
} => parts.iter().all(Attachment::is_html),
|
||||
|
||||
ContentType::Multipart { ref parts, .. } => parts.iter().any(Attachment::is_html),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
matches!(
|
||||
self.content_type,
|
||||
match self.content_type {
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Encrypted,
|
||||
..
|
||||
}
|
||||
)
|
||||
} => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_signed(&self) -> bool {
|
||||
matches!(
|
||||
self.content_type,
|
||||
match self.content_type {
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Signed,
|
||||
..
|
||||
}
|
||||
)
|
||||
} => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_raw(&self) -> String {
|
||||
|
@ -763,13 +689,13 @@ impl Attachment {
|
|||
for (n, v) in parameters {
|
||||
ret.push_str("; ");
|
||||
ret.push_str(&String::from_utf8_lossy(n));
|
||||
ret.push('=');
|
||||
ret.push_str("=");
|
||||
if v.contains(&b' ') {
|
||||
ret.push('"');
|
||||
ret.push_str("\"");
|
||||
}
|
||||
ret.push_str(&String::from_utf8_lossy(v));
|
||||
if v.contains(&b' ') {
|
||||
ret.push('"');
|
||||
ret.push_str("\"");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -780,25 +706,12 @@ impl Attachment {
|
|||
boundary,
|
||||
kind,
|
||||
parts,
|
||||
parameters,
|
||||
} => {
|
||||
let boundary = String::from_utf8_lossy(boundary);
|
||||
ret.push_str(&format!("Content-Type: {}; boundary={}", kind, boundary));
|
||||
if *kind == MultipartType::Signed {
|
||||
ret.push_str("; micalg=pgp-sha512; protocol=\"application/pgp-signature\"");
|
||||
}
|
||||
for (n, v) in parameters {
|
||||
ret.push_str("; ");
|
||||
ret.push_str(&String::from_utf8_lossy(n));
|
||||
ret.push('=');
|
||||
if v.contains(&b' ') {
|
||||
ret.push('"');
|
||||
}
|
||||
ret.push_str(&String::from_utf8_lossy(v));
|
||||
if v.contains(&b' ') {
|
||||
ret.push('"');
|
||||
}
|
||||
}
|
||||
ret.push_str("\r\n");
|
||||
|
||||
let boundary_start = format!("\r\n--{}\r\n", boundary);
|
||||
|
@ -816,26 +729,16 @@ impl Attachment {
|
|||
ret.push_str(&format!("Content-Type: {}\r\n\r\n", a.content_type));
|
||||
ret.push_str(&String::from_utf8_lossy(a.body()));
|
||||
}
|
||||
ContentType::OctetStream { name, parameters } => {
|
||||
ContentType::OctetStream { ref name } => {
|
||||
if let Some(name) = name {
|
||||
ret.push_str(&format!("Content-Type: {}; name={}", a.content_type, name));
|
||||
ret.push_str(&format!(
|
||||
"Content-Type: {}; name={}\r\n\r\n",
|
||||
a.content_type, name
|
||||
));
|
||||
} else {
|
||||
ret.push_str(&format!("Content-Type: {}", a.content_type));
|
||||
ret.push_str(&format!("Content-Type: {}\r\n\r\n", a.content_type));
|
||||
}
|
||||
for (n, v) in parameters {
|
||||
ret.push_str("; ");
|
||||
ret.push_str(&String::from_utf8_lossy(n));
|
||||
ret.push('=');
|
||||
if v.contains(&b' ') {
|
||||
ret.push('"');
|
||||
}
|
||||
ret.push_str(&String::from_utf8_lossy(v));
|
||||
if v.contains(&b' ') {
|
||||
ret.push('"');
|
||||
}
|
||||
}
|
||||
ret.push_str("\r\n\r\n");
|
||||
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));
|
||||
|
@ -855,8 +758,11 @@ impl Attachment {
|
|||
};
|
||||
for (name, value) in headers {
|
||||
if name.eq_ignore_ascii_case(b"content-type") {
|
||||
if let Ok((_, (_, _, params))) = parser::attachments::content_type(value) {
|
||||
ret = params;
|
||||
match parser::attachments::content_type(value) {
|
||||
Ok((_, (_, _, params))) => {
|
||||
ret = params;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -888,129 +794,123 @@ impl Attachment {
|
|||
.map(|(_, v)| v)
|
||||
.ok()
|
||||
.and_then(|n| String::from_utf8(n).ok())
|
||||
.unwrap_or(s)
|
||||
.unwrap_or_else(|| s)
|
||||
})
|
||||
.map(|n| n.replace(|c| std::path::is_separator(c) || c.is_ascii_control(), "_"))
|
||||
}
|
||||
|
||||
fn decode_rec_helper(&self, options: &mut DecodeOptions<'_>) -> Vec<u8> {
|
||||
match self.content_type {
|
||||
ContentType::Other { .. } => Vec::new(),
|
||||
ContentType::Text { .. } => self.decode_helper(options),
|
||||
ContentType::OctetStream {
|
||||
ref name,
|
||||
parameters: _,
|
||||
} => 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,
|
||||
parameters: _,
|
||||
..
|
||||
} => 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(&self, mut options: DecodeOptions<'_>) -> Vec<u8> {
|
||||
self.decode_rec_helper(&mut options)
|
||||
}
|
||||
|
||||
fn decode_helper(&self, options: &mut DecodeOptions<'_>) -> 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(&self, mut options: DecodeOptions<'_>) -> 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>;
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -19,37 +19,31 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Compose a [``Draft`], with `MIME` and attachment support.
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
ffi::OsStr,
|
||||
io::Read,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use data_encoding::BASE64_MIME;
|
||||
use xdg_utils::query_mime_info;
|
||||
|
||||
/*! Compose a `Draft`, with MIME and attachment support */
|
||||
use super::*;
|
||||
use crate::{
|
||||
email::{
|
||||
attachment_types::{Charset, ContentTransferEncoding, ContentType, MultipartType},
|
||||
attachments::AttachmentBuilder,
|
||||
},
|
||||
utils::{datetime, shellexpand::ShellExpandTrait},
|
||||
use crate::email::attachment_types::{
|
||||
Charset, ContentTransferEncoding, ContentType, MultipartType,
|
||||
};
|
||||
use crate::email::attachments::{decode, decode_rec, 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 xdg_utils::query_mime_info;
|
||||
|
||||
pub mod mime;
|
||||
pub mod random;
|
||||
|
||||
//use self::mime::*;
|
||||
|
||||
use super::parser;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Draft {
|
||||
pub headers: HeaderMap,
|
||||
pub body: String,
|
||||
pub wrap_header_preamble: Option<(String, String)>,
|
||||
|
||||
pub attachments: Vec<AttachmentBuilder>,
|
||||
}
|
||||
|
@ -58,46 +52,41 @@ impl Default for Draft {
|
|||
fn default() -> Self {
|
||||
let mut headers = HeaderMap::default();
|
||||
headers.insert(
|
||||
HeaderName::DATE,
|
||||
datetime::timestamp_to_string(
|
||||
datetime::now(),
|
||||
Some(datetime::formats::RFC822_DATE),
|
||||
true,
|
||||
),
|
||||
HeaderName::new_unchecked("Date"),
|
||||
crate::datetime::timestamp_to_string(crate::datetime::now(), None),
|
||||
);
|
||||
headers.insert(HeaderName::FROM, "".into());
|
||||
headers.insert(HeaderName::TO, "".into());
|
||||
headers.insert(HeaderName::CC, "".into());
|
||||
headers.insert(HeaderName::BCC, "".into());
|
||||
headers.insert(HeaderName::SUBJECT, "".into());
|
||||
headers.insert(HeaderName::new_unchecked("From"), "".into());
|
||||
headers.insert(HeaderName::new_unchecked("To"), "".into());
|
||||
headers.insert(HeaderName::new_unchecked("Cc"), "".into());
|
||||
headers.insert(HeaderName::new_unchecked("Bcc"), "".into());
|
||||
headers.insert(HeaderName::new_unchecked("Subject"), "".into());
|
||||
|
||||
Self {
|
||||
Draft {
|
||||
headers,
|
||||
body: String::new(),
|
||||
wrap_header_preamble: None,
|
||||
|
||||
attachments: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Draft {
|
||||
type Err = Error;
|
||||
impl str::FromStr for Draft {
|
||||
type Err = MeliError;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
if s.is_empty() {
|
||||
return Err(Error::new("Empty input in Draft::from_str"));
|
||||
return Err(MeliError::new("Empty input in Draft::from_str"));
|
||||
}
|
||||
|
||||
let (headers, _) = parser::mail(s.as_bytes())?;
|
||||
let mut ret = Self::default();
|
||||
let mut ret = Draft::default();
|
||||
|
||||
for (k, v) in headers {
|
||||
ret.headers
|
||||
.insert(k.try_into()?, String::from_utf8(v.to_vec())?);
|
||||
}
|
||||
let body = Envelope::new(EnvelopeHash::default()).body_bytes(s.as_bytes());
|
||||
let body = Envelope::new(0).body_bytes(s.as_bytes());
|
||||
|
||||
ret.body = String::from_utf8(body.decode(Default::default()))?;
|
||||
ret.body = String::from_utf8(decode(&body, None))?;
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
@ -105,69 +94,26 @@ impl FromStr for Draft {
|
|||
|
||||
impl Draft {
|
||||
pub fn edit(envelope: &Envelope, bytes: &[u8]) -> Result<Self> {
|
||||
let mut ret = Self::default();
|
||||
for (k, v) in envelope.headers(bytes).unwrap_or_else(|_| Vec::new()) {
|
||||
let mut ret = Draft::default();
|
||||
for (k, v) in envelope.headers(&bytes).unwrap_or_else(|_| Vec::new()) {
|
||||
ret.headers.insert(k.try_into()?, v.into());
|
||||
}
|
||||
|
||||
let body = envelope.body_bytes(bytes);
|
||||
ret.body = body.text();
|
||||
ret.attachments = body.attachments().into_iter().map(Into::into).collect();
|
||||
ret.body = envelope.body_bytes(bytes).text();
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
pub fn set_header(&mut self, header: HeaderName, value: String) -> &mut Self {
|
||||
self.headers.insert(header, value);
|
||||
pub fn set_header(&mut self, header: &str, value: String) -> &mut Self {
|
||||
self.headers
|
||||
.insert(HeaderName::new_unchecked(header), value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn try_set_header(
|
||||
&mut self,
|
||||
header: &str,
|
||||
value: String,
|
||||
) -> std::result::Result<&mut Self, InvalidHeaderName> {
|
||||
self.headers.insert(HeaderName::try_from(header)?, value);
|
||||
Ok(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 = Self::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 = Self::default();
|
||||
let mut ret = Draft::default();
|
||||
ret.headers_mut().insert(
|
||||
HeaderName::REFERENCES,
|
||||
HeaderName::new_unchecked("References"),
|
||||
format!(
|
||||
"{} {}",
|
||||
envelope
|
||||
|
@ -184,7 +130,7 @@ impl Draft {
|
|||
),
|
||||
);
|
||||
ret.headers_mut().insert(
|
||||
HeaderName::IN_REPLY_TO,
|
||||
HeaderName::new_unchecked("In-Reply-To"),
|
||||
envelope.message_id_display().into(),
|
||||
);
|
||||
// "Mail-Followup-To/(To+Cc+(Mail-Reply-To/Reply-To/From)) for follow-up,
|
||||
|
@ -193,36 +139,46 @@ impl Draft {
|
|||
if reply_to_all {
|
||||
if let Some(reply_to) = envelope.other_headers().get("Mail-Followup-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::TO, reply_to.to_string());
|
||||
.insert(HeaderName::new_unchecked("To"), reply_to.to_string());
|
||||
} else {
|
||||
if let Some(reply_to) = envelope.other_headers().get("Reply-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::new_unchecked("To"), reply_to.to_string());
|
||||
} else {
|
||||
ret.headers_mut().insert(
|
||||
HeaderName::new_unchecked("To"),
|
||||
envelope.field_from_to_string(),
|
||||
);
|
||||
}
|
||||
// FIXME: add To/Cc
|
||||
}
|
||||
} else {
|
||||
if let Some(reply_to) = envelope.other_headers().get("Mail-Reply-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::new_unchecked("To"), reply_to.to_string());
|
||||
} else if let Some(reply_to) = envelope.other_headers().get("Reply-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::TO, reply_to.to_string());
|
||||
.insert(HeaderName::new_unchecked("To"), reply_to.to_string());
|
||||
} else {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::TO, envelope.field_from_to_string());
|
||||
ret.headers_mut().insert(
|
||||
HeaderName::new_unchecked("To"),
|
||||
envelope.field_from_to_string(),
|
||||
);
|
||||
}
|
||||
// FIXME: add To/Cc
|
||||
} else if let Some(reply_to) = envelope.other_headers().get("Mail-Reply-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::TO, reply_to.to_string());
|
||||
} else if let Some(reply_to) = envelope.other_headers().get("Reply-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::TO, reply_to.to_string());
|
||||
} else {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::TO, envelope.field_from_to_string());
|
||||
}
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::CC, envelope.field_cc_to_string());
|
||||
ret.headers_mut().insert(
|
||||
HeaderName::new_unchecked("Cc"),
|
||||
envelope.field_cc_to_string(),
|
||||
);
|
||||
let body = envelope.body_bytes(bytes);
|
||||
ret.body = {
|
||||
let reply_body_bytes = body.decode_rec(Default::default());
|
||||
let reply_body_bytes = decode_rec(&body, None);
|
||||
let reply_body = String::from_utf8_lossy(&reply_body_bytes);
|
||||
let lines: Vec<&str> = reply_body.lines().collect();
|
||||
let mut ret = format!(
|
||||
"On {} {} wrote:\n",
|
||||
envelope.date_as_str(),
|
||||
&ret.headers()[HeaderName::TO]
|
||||
&ret.headers()["To"]
|
||||
);
|
||||
for l in lines {
|
||||
ret.push('>');
|
||||
|
@ -261,52 +217,30 @@ impl Draft {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn to_edit_string(&self) -> String {
|
||||
pub fn to_string(&self) -> Result<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);
|
||||
|
||||
ret
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
pub fn finalise(mut self) -> Result<String> {
|
||||
let mut ret = String::new();
|
||||
let has_from: bool = self.headers.contains_key("From");
|
||||
let has_msg_id: bool = self.headers.contains_key("Message-ID");
|
||||
let has_mime: bool = self.headers.contains_key("MIME-Version");
|
||||
let has_ctype: bool = self.headers.contains_key("Content-Type");
|
||||
let has_cte: bool = self.headers.contains_key("Content-Transfer-Encoding");
|
||||
|
||||
if has_from && !has_msg_id {
|
||||
if self.headers.contains_key("From") && !self.headers.contains_key("Message-ID") {
|
||||
if let Ok((_, addr)) = super::parser::address::mailbox(self.headers["From"].as_bytes())
|
||||
{
|
||||
if let Some(fqdn) = addr.get_fqdn() {
|
||||
self.headers
|
||||
.insert(HeaderName::MESSAGE_ID, random::gen_message_id(&fqdn));
|
||||
self.headers.insert(
|
||||
HeaderName::new_unchecked("Message-ID"),
|
||||
random::gen_message_id(&fqdn),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -317,56 +251,44 @@ impl Draft {
|
|||
ret.push_str(&format!("{}: {}\r\n", k, mime::encode_header(v)));
|
||||
}
|
||||
}
|
||||
if !has_mime {
|
||||
ret.push_str("MIME-Version: 1.0\r\n");
|
||||
}
|
||||
ret.push_str("MIME-Version: 1.0\r\n");
|
||||
|
||||
if self.attachments.is_empty() {
|
||||
if !has_ctype {
|
||||
let content_type: ContentType = Default::default();
|
||||
let content_transfer_encoding: ContentTransferEncoding =
|
||||
ContentTransferEncoding::_8Bit;
|
||||
ret.push_str(&format!(
|
||||
"Content-Type: {}; charset=\"utf-8\"\r\n",
|
||||
content_type
|
||||
));
|
||||
if !has_cte {
|
||||
ret.push_str(&format!(
|
||||
"Content-Transfer-Encoding: {}\r\n",
|
||||
content_transfer_encoding
|
||||
));
|
||||
}
|
||||
}
|
||||
let content_type: ContentType = Default::default();
|
||||
let content_transfer_encoding: ContentTransferEncoding = ContentTransferEncoding::_8Bit;
|
||||
ret.push_str(&format!(
|
||||
"Content-Type: {}; charset=\"utf-8\"\r\n",
|
||||
content_type
|
||||
));
|
||||
ret.push_str(&format!(
|
||||
"Content-Transfer-Encoding: {}\r\n",
|
||||
content_transfer_encoding
|
||||
));
|
||||
ret.push_str("\r\n");
|
||||
for line in self.body.lines() {
|
||||
ret.push_str(line);
|
||||
ret.push_str("\r\n");
|
||||
}
|
||||
} else if self.body.is_empty() && self.attachments.len() == 1 {
|
||||
let attachment = std::mem::take(&mut self.attachments).remove(0);
|
||||
let attachment = std::mem::replace(&mut self.attachments, Vec::new()).remove(0);
|
||||
print_attachment(&mut ret, attachment);
|
||||
} else {
|
||||
let mut parts = Vec::with_capacity(self.attachments.len() + 1);
|
||||
let attachments = std::mem::take(&mut self.attachments);
|
||||
let attachments = std::mem::replace(&mut self.attachments, Vec::new());
|
||||
if !self.body.is_empty() {
|
||||
let mut body_attachment = AttachmentBuilder::default();
|
||||
body_attachment.set_raw(self.body.as_bytes().to_vec());
|
||||
parts.push(body_attachment);
|
||||
}
|
||||
parts.extend(attachments.into_iter());
|
||||
build_multipart(&mut ret, MultipartType::Mixed, &[], parts);
|
||||
build_multipart(&mut ret, MultipartType::Mixed, parts);
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
||||
|
||||
fn build_multipart(
|
||||
ret: &mut String,
|
||||
kind: MultipartType,
|
||||
parameters: &[(Vec<u8>, Vec<u8>)],
|
||||
parts: Vec<AttachmentBuilder>,
|
||||
) {
|
||||
fn build_multipart(ret: &mut String, kind: MultipartType, parts: Vec<AttachmentBuilder>) {
|
||||
let boundary = ContentType::make_boundary(&parts);
|
||||
ret.push_str(&format!(
|
||||
r#"Content-Type: {}; charset="utf-8"; boundary="{}""#,
|
||||
|
@ -375,24 +297,9 @@ fn build_multipart(
|
|||
if kind == MultipartType::Encrypted {
|
||||
ret.push_str(r#"; protocol="application/pgp-encrypted""#);
|
||||
}
|
||||
for (n, v) in parameters {
|
||||
ret.push_str("; ");
|
||||
ret.push_str(&String::from_utf8_lossy(n));
|
||||
ret.push('=');
|
||||
if v.contains(&b' ') {
|
||||
ret.push('"');
|
||||
}
|
||||
ret.push_str(&String::from_utf8_lossy(v));
|
||||
if v.contains(&b' ') {
|
||||
ret.push('"');
|
||||
}
|
||||
}
|
||||
ret.push_str("\r\n\r\n");
|
||||
/* rfc1341 */
|
||||
ret.push_str(
|
||||
"This is a MIME formatted message with attachments. Use a MIME-compliant client to view \
|
||||
it properly.\r\n",
|
||||
);
|
||||
ret.push_str("This is a MIME formatted message with attachments. Use a MIME-compliant client to view it properly.\r\n");
|
||||
for sub in parts {
|
||||
ret.push_str("--");
|
||||
ret.push_str(&boundary);
|
||||
|
@ -428,12 +335,10 @@ fn print_attachment(ret: &mut String, a: AttachmentBuilder) {
|
|||
boundary: _,
|
||||
kind,
|
||||
parts,
|
||||
parameters,
|
||||
} => {
|
||||
build_multipart(
|
||||
ret,
|
||||
kind,
|
||||
¶meters,
|
||||
parts
|
||||
.into_iter()
|
||||
.map(|s| s.into())
|
||||
|
@ -504,125 +409,33 @@ fn print_attachment(ret: &mut String, a: AttachmentBuilder) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Reads file from given path, and returns an 'application/octet-stream'
|
||||
/// AttachmentBuilder object
|
||||
pub fn attachment_from_file<I>(path: &I) -> Result<AttachmentBuilder>
|
||||
where
|
||||
I: AsRef<OsStr>,
|
||||
{
|
||||
let path: PathBuf = Path::new(path).expand();
|
||||
if !path.is_file() {
|
||||
return Err(Error::new(format!("{} is not a file", path.display())));
|
||||
}
|
||||
|
||||
let mut file = std::fs::File::open(&path)?;
|
||||
let mut contents = Vec::new();
|
||||
file.read_to_end(&mut contents)?;
|
||||
let mut attachment = AttachmentBuilder::default();
|
||||
|
||||
attachment
|
||||
.set_raw(contents)
|
||||
.set_body_to_raw()
|
||||
.set_content_type(ContentType::Other {
|
||||
name: path.file_name().map(|s| s.to_string_lossy().into()),
|
||||
tag: query_mime_info(&path).unwrap_or_else(|_| b"application/octet-stream".to_vec()),
|
||||
parameters: vec![],
|
||||
});
|
||||
|
||||
Ok(attachment)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_new_draft() {
|
||||
fn test_new() {
|
||||
let mut default = Draft::default();
|
||||
assert_eq!(Draft::from_str(&default.to_edit_string()).unwrap(), default);
|
||||
assert_eq!(
|
||||
Draft::from_str(&default.to_string().unwrap()).unwrap(),
|
||||
default
|
||||
);
|
||||
default.set_body("αδφαφσαφασ".to_string());
|
||||
assert_eq!(Draft::from_str(&default.to_edit_string()).unwrap(), default);
|
||||
assert_eq!(
|
||||
Draft::from_str(&default.to_string().unwrap()).unwrap(),
|
||||
default
|
||||
);
|
||||
default.set_body("ascii only".to_string());
|
||||
assert_eq!(Draft::from_str(&default.to_edit_string()).unwrap(), default);
|
||||
assert_eq!(
|
||||
Draft::from_str(&default.to_string().unwrap()).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(HeaderName::SUBJECT, "test_update()".into())
|
||||
.set_header(HeaderName::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();
|
||||
#[rustfmt::skip]
|
||||
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());
|
||||
|
||||
|
@ -640,6 +453,36 @@ 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
|
||||
pub fn attachment_from_file<I>(path: &I) -> Result<AttachmentBuilder>
|
||||
where
|
||||
I: AsRef<OsStr>,
|
||||
{
|
||||
let path: PathBuf = Path::new(path).expand();
|
||||
if !path.is_file() {
|
||||
return Err(MeliError::new(format!("{} is not a file", path.display())));
|
||||
}
|
||||
|
||||
let mut file = std::fs::File::open(&path)?;
|
||||
let mut contents = Vec::new();
|
||||
file.read_to_end(&mut contents)?;
|
||||
let mut attachment = AttachmentBuilder::default();
|
||||
|
||||
attachment
|
||||
.set_raw(contents)
|
||||
.set_body_to_raw()
|
||||
.set_content_type(ContentType::Other {
|
||||
name: path.file_name().map(|s| s.to_string_lossy().into()),
|
||||
tag: if let Ok(mime_type) = query_mime_info(&path) {
|
||||
mime_type
|
||||
} else {
|
||||
b"application/octet-stream".to_vec()
|
||||
},
|
||||
});
|
||||
|
||||
Ok(attachment)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
*/
|
||||
|
||||
use super::*;
|
||||
|
||||
#[cfg(feature = "unicode_algorithms")]
|
||||
use crate::text_processing::grapheme_clusters::TextProcessing;
|
||||
|
||||
|
@ -60,9 +61,9 @@ pub fn encode_header(value: &str) -> String {
|
|||
is_current_window_ascii = false;
|
||||
}
|
||||
/* RFC2047 recommends:
|
||||
* 'While there is no limit to the length of a multiple-line header field, each
|
||||
* line of a header field that contains one or more
|
||||
* 'encoded-word's is limited to 76 characters.'
|
||||
* 'While there is no limit to the length of a multiple-line header field, each line of
|
||||
* a header field that contains one or more 'encoded-word's is limited to 76
|
||||
* characters.'
|
||||
* This is a rough compliance.
|
||||
*/
|
||||
(false, false) if (((4 * (idx - current_window_start) / 3) + 3) & !3) > 33 => {
|
||||
|
@ -83,8 +84,8 @@ pub fn encode_header(value: &str) -> String {
|
|||
}
|
||||
#[cfg(not(feature = "unicode_algorithms"))]
|
||||
{
|
||||
/* TODO: test this. If it works as fine as the one above, there's no need to
|
||||
* keep the above implementation. */
|
||||
/* TODO: test this. If it works as fine as the one above, there's no need to keep the above
|
||||
* implementation.*/
|
||||
for (i, g) in value.char_indices() {
|
||||
match (g.is_ascii(), is_current_window_ascii) {
|
||||
(true, true) => {
|
||||
|
@ -115,9 +116,9 @@ pub fn encode_header(value: &str) -> String {
|
|||
is_current_window_ascii = false;
|
||||
}
|
||||
/* RFC2047 recommends:
|
||||
* 'While there is no limit to the length of a multiple-line header field, each
|
||||
* line of a header field that contains one or more
|
||||
* 'encoded-word's is limited to 76 characters.'
|
||||
* 'While there is no limit to the length of a multiple-line header field, each line of
|
||||
* a header field that contains one or more 'encoded-word's is limited to 76
|
||||
* characters.'
|
||||
* This is a rough compliance.
|
||||
*/
|
||||
(false, false)
|
||||
|
@ -138,8 +139,8 @@ pub fn encode_header(value: &str) -> String {
|
|||
}
|
||||
}
|
||||
}
|
||||
/* If the last part of the header value is encoded, it won't be pushed inside
|
||||
* the previous for block */
|
||||
/* If the last part of the header value is encoded, it won't be pushed inside the previous for
|
||||
* block */
|
||||
if !is_current_window_ascii {
|
||||
ret.push_str(&format!(
|
||||
"=?UTF-8?B?{}?=",
|
||||
|
@ -155,39 +156,35 @@ fn test_encode_header() {
|
|||
let words = "compilers/2020a σε Rust";
|
||||
assert_eq!(
|
||||
"compilers/2020a =?UTF-8?B?z4POtSA=?=Rust",
|
||||
&encode_header(words),
|
||||
&encode_header(&words),
|
||||
);
|
||||
assert_eq!(
|
||||
&std::str::from_utf8(
|
||||
&crate::email::parser::encodings::phrase(encode_header(words).as_bytes(), false)
|
||||
&crate::email::parser::encodings::phrase(encode_header(&words).as_bytes(), false)
|
||||
.unwrap()
|
||||
.1
|
||||
)
|
||||
.unwrap(),
|
||||
&words,
|
||||
);
|
||||
let words = "[internal] =?UTF-8?B?zp3Orc6/z4Igzp/OtM63zrPPjM+CIM6jz4U=?= \
|
||||
=?UTF-8?B?zrPOs8+BzrHPhs6uz4I=?=";
|
||||
let words = "[internal] =?UTF-8?B?zp3Orc6/z4Igzp/OtM63zrPPjM+CIM6jz4U=?= =?UTF-8?B?zrPOs8+BzrHPhs6uz4I=?=";
|
||||
let words_enc = r#"[internal] Νέος Οδηγός Συγγραφής"#;
|
||||
assert_eq!(words, &encode_header(words_enc),);
|
||||
assert_eq!(words, &encode_header(&words_enc),);
|
||||
assert_eq!(
|
||||
r#"[internal] Νέος Οδηγός Συγγραφής"#,
|
||||
std::str::from_utf8(
|
||||
&crate::email::parser::encodings::phrase(encode_header(words_enc).as_bytes(), false)
|
||||
&crate::email::parser::encodings::phrase(encode_header(&words_enc).as_bytes(), false)
|
||||
.unwrap()
|
||||
.1
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
//let words = "[Advcomparch]
|
||||
// =?utf-8?b?zqPPhc68z4DOtc+BzrnPhs6/z4HOrCDPg861IGZs?=\n\t=?utf-8?b?
|
||||
// dXNoIM67z4zOs8+JIG1pc3ByZWRpY3Rpb24gzrrOsc+Ezqwgz4TOt869?=\n\t=?utf-8?b?
|
||||
// IM61zrrPhM6tzrvOtc+Dzrcgc3RvcmU=?=";
|
||||
//let words = "[Advcomparch] =?utf-8?b?zqPPhc68z4DOtc+BzrnPhs6/z4HOrCDPg861IGZs?=\n\t=?utf-8?b?dXNoIM67z4zOs8+JIG1pc3ByZWRpY3Rpb24gzrrOsc+Ezqwgz4TOt869?=\n\t=?utf-8?b?IM61zrrPhM6tzrvOtc+Dzrcgc3RvcmU=?=";
|
||||
let words_enc = "[Advcomparch] Συμπεριφορά σε flush λόγω misprediction κατά την εκτέλεση store";
|
||||
assert_eq!(
|
||||
"[Advcomparch] Συμπεριφορά σε flush λόγω misprediction κατά την εκτέλεση store",
|
||||
std::str::from_utf8(
|
||||
&crate::email::parser::encodings::phrase(encode_header(words_enc).as_bytes(), false)
|
||||
&crate::email::parser::encodings::phrase(encode_header(&words_enc).as_bytes(), false)
|
||||
.unwrap()
|
||||
.1
|
||||
)
|
||||
|
|
|
@ -19,7 +19,34 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::utils::random::{clock, random_u64};
|
||||
use std::char;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::time::SystemTime;
|
||||
|
||||
fn random_u64() -> u64 {
|
||||
let mut f = File::open("/dev/urandom").unwrap();
|
||||
let mut buffer = [0; 8];
|
||||
|
||||
// read exactly 10 bytes
|
||||
f.read_exact(&mut buffer).unwrap();
|
||||
|
||||
u64::from(buffer[0])
|
||||
| (u64::from(buffer[1]) << 8)
|
||||
| (u64::from(buffer[2]) << 16)
|
||||
| (u64::from(buffer[3]) << 24)
|
||||
| (u64::from(buffer[4]) << 32)
|
||||
| (u64::from(buffer[5]) << 40)
|
||||
| (u64::from(buffer[6]) << 48)
|
||||
| (u64::from(buffer[7]) << 56)
|
||||
}
|
||||
|
||||
fn clock() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
fn base36(mut m: u64) -> String {
|
||||
let mut stack = Vec::with_capacity(32);
|
||||
|
|
|
@ -19,19 +19,98 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Wrapper type [`HeaderName`] for case-insensitive comparisons.
|
||||
|
||||
pub mod names;
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
cmp::{Eq, PartialEq},
|
||||
convert::{TryFrom, TryInto},
|
||||
hash::{Hash, Hasher},
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
|
||||
/*! Wrapper type `HeaderName` for case-insensitive comparisons */
|
||||
use crate::error::MeliError;
|
||||
use indexmap::IndexMap;
|
||||
pub use names::{HeaderName, InvalidHeaderName, Protocol, Standard, StandardHeader, Status};
|
||||
use smallvec::SmallVec;
|
||||
use std::borrow::Borrow;
|
||||
use std::cmp::{Eq, PartialEq};
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct HeaderNameType<S>(S);
|
||||
|
||||
///Case insensitive wrapper for a header name. As of `RFC5322` it's guaranteened to be ASCII.
|
||||
pub type HeaderName = HeaderNameType<SmallVec<[u8; 32]>>;
|
||||
|
||||
impl HeaderName {
|
||||
pub fn new_unchecked(from: &str) -> Self {
|
||||
HeaderNameType(from.as_bytes().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<[u8]>> fmt::Display for HeaderNameType<S> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.normalize())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<[u8]>> fmt::Debug for HeaderNameType<S> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<[u8]>> PartialEq<[u8]> for HeaderNameType<S> {
|
||||
fn eq(&self, other: &[u8]) -> bool {
|
||||
self.0.as_ref().eq_ignore_ascii_case(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<[u8]>> PartialEq<&str> for HeaderNameType<S> {
|
||||
fn eq(&self, other: &&str) -> bool {
|
||||
self.0.as_ref().eq_ignore_ascii_case(other.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S1: AsRef<[u8]>, S2: AsRef<[u8]>> PartialEq<HeaderNameType<S2>> for HeaderNameType<S1> {
|
||||
fn eq(&self, other: &HeaderNameType<S2>) -> bool {
|
||||
self.0.as_ref().eq_ignore_ascii_case(other.0.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<[u8]>> Eq for HeaderNameType<S> {}
|
||||
|
||||
impl<S: AsRef<[u8]>> Hash for HeaderNameType<S> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
for b in self.0.as_ref().iter() {
|
||||
b.to_ascii_lowercase().hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for HeaderName {
|
||||
type Error = MeliError;
|
||||
|
||||
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||
if value.is_ascii() {
|
||||
Ok(HeaderNameType(value.into()))
|
||||
} else {
|
||||
Err(MeliError::new(format!(
|
||||
"Header value is not ascii: {:?}",
|
||||
value
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for HeaderName {
|
||||
type Error = MeliError;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
if value.is_ascii() {
|
||||
Ok(HeaderNameType(value.as_bytes().into()))
|
||||
} else {
|
||||
Err(MeliError::new(format!(
|
||||
"Header value is not ascii: {:?}",
|
||||
value
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait HeaderKey {
|
||||
fn to_key(&self) -> &[u8];
|
||||
|
@ -53,9 +132,9 @@ impl PartialEq for dyn HeaderKey + '_ {
|
|||
|
||||
impl Eq for dyn HeaderKey + '_ {}
|
||||
|
||||
impl HeaderKey for HeaderName {
|
||||
impl<S: AsRef<[u8]>> HeaderKey for HeaderNameType<S> {
|
||||
fn to_key(&self) -> &[u8] {
|
||||
self.as_bytes()
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,100 +146,117 @@ impl<'a> Borrow<dyn HeaderKey + 'a> for HeaderName {
|
|||
}
|
||||
}
|
||||
|
||||
/// Map of mail headers and values.
|
||||
///
|
||||
/// Can be indexed by:
|
||||
///
|
||||
/// - `usize`
|
||||
/// - `&[u8]`, which panics if it's not a valid header value.
|
||||
/// - `&str`, which also panics if it's not a valid header value.
|
||||
/// - [HeaderName], which is guaranteed to be valid.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Except for the above, indexing will also panic if index is out of range or
|
||||
/// header key is not present in the map.
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
pub struct HeaderMap(indexmap::IndexMap<HeaderName, String>);
|
||||
impl<S: AsRef<[u8]>> HeaderNameType<S> {
|
||||
pub fn as_str(&self) -> &str {
|
||||
//HeadersType are ascii so valid utf8
|
||||
unsafe { std::str::from_utf8_unchecked(self.0.as_ref()) }
|
||||
}
|
||||
|
||||
impl std::ops::Index<usize> for HeaderMap {
|
||||
type Output = str;
|
||||
fn index(&self, k: usize) -> &Self::Output {
|
||||
(self.0)[k].as_str()
|
||||
pub fn normalize(&self) -> &str {
|
||||
if self == &b"subject"[..] {
|
||||
"Subject"
|
||||
} else if self == &b"from"[..] {
|
||||
"From"
|
||||
} else if self == &b"to"[..] {
|
||||
"To"
|
||||
} else if self == &b"cc"[..] {
|
||||
"Cc"
|
||||
} else if self == &b"bcc"[..] {
|
||||
"Bcc"
|
||||
} else if self == &b"reply-to"[..] {
|
||||
"Reply-To"
|
||||
} else if self == &b"in-reply-to"[..] {
|
||||
"In-Reply-To"
|
||||
} else if self == &b"references"[..] {
|
||||
"References"
|
||||
} else if self == &b"sender"[..] {
|
||||
"Sender"
|
||||
} else if self == &b"mail-reply-to"[..] {
|
||||
"Mail-Reply-To"
|
||||
} else if self == &b"mail-followup-to"[..] {
|
||||
"Mail-Followup-To"
|
||||
} else if self == &b"mime-version"[..] {
|
||||
"MIME-Version"
|
||||
} else if self == &b"content-disposition"[..] {
|
||||
"Content-Disposition"
|
||||
} else if self == &b"content-transfer-encoding"[..] {
|
||||
"Content-Transfer-Encoding"
|
||||
} else if self == &b"content-type"[..] {
|
||||
"Content-Type"
|
||||
} else if self == &b"content-id"[..] {
|
||||
"Content-ID"
|
||||
} else if self == &b"content-description"[..] {
|
||||
"Content-Description"
|
||||
} else if self == &b"authentication-results"[..] {
|
||||
"Authentication-Results"
|
||||
} else if self == &b"dkim-signature"[..] {
|
||||
"DKIM-Signature"
|
||||
} else if self == &b"delivered-to"[..] {
|
||||
"Delivered-To"
|
||||
} else if self == &b"message-id"[..] {
|
||||
"Message-ID"
|
||||
} else if self == &b"comments"[..] {
|
||||
"Comments"
|
||||
} else if self == &b"keywords"[..] {
|
||||
"Keywords"
|
||||
} else if self == &b"resent-from"[..] {
|
||||
"Resent-From"
|
||||
} else if self == &b"resent-sender"[..] {
|
||||
"Resent-Sender"
|
||||
} else if self == &b"resent-to"[..] {
|
||||
"Resent-To"
|
||||
} else if self == &b"resent-cc"[..] {
|
||||
"Resent-Cc"
|
||||
} else if self == &b"resent-bcc"[..] {
|
||||
"Resent-Bcc"
|
||||
} else if self == &b"resent-date"[..] {
|
||||
"Resent-Date"
|
||||
} else if self == &b"resent-message-id"[..] {
|
||||
"Resent-Message-ID"
|
||||
} else if self == &b"resent-reply-to"[..] {
|
||||
"Resent-Reply-To"
|
||||
} else if self == &b"return-path"[..] {
|
||||
"Return-Path"
|
||||
} else if self == &b"received"[..] {
|
||||
"Received"
|
||||
} else {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
pub struct HeaderMap(indexmap::IndexMap<HeaderName, String>);
|
||||
|
||||
impl std::ops::Index<&[u8]> for HeaderMap {
|
||||
type Output = str;
|
||||
fn index(&self, k: &[u8]) -> &Self::Output {
|
||||
(self.0)[&HeaderName::try_from(k).expect("Invalid bytes in header name.")].as_str()
|
||||
(self.0)[HeaderNameType(k).borrow() as &dyn HeaderKey].as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<&str> for HeaderMap {
|
||||
type Output = str;
|
||||
fn index(&self, k: &str) -> &Self::Output {
|
||||
(self.0)[&HeaderName::try_from(k).expect("Invalid bytes in header name.")].as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<&HeaderName> for HeaderMap {
|
||||
type Output = str;
|
||||
fn index(&self, k: &HeaderName) -> &Self::Output {
|
||||
(self.0)[k].as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<HeaderName> for HeaderMap {
|
||||
type Output = str;
|
||||
fn index(&self, k: HeaderName) -> &Self::Output {
|
||||
(self.0)[&k].as_str()
|
||||
(self.0)[HeaderNameType(k).borrow() as &dyn HeaderKey].as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl HeaderMap {
|
||||
pub fn empty() -> Self {
|
||||
Self::default()
|
||||
pub fn get_mut(&mut self, key: &str) -> Option<&mut String> {
|
||||
(self.0).get_mut(HeaderNameType(key).borrow() as &dyn HeaderKey)
|
||||
}
|
||||
|
||||
pub fn get_mut<T: TryInto<HeaderName> + std::fmt::Debug>(
|
||||
&mut self,
|
||||
key: T,
|
||||
) -> Option<&mut String>
|
||||
where
|
||||
<T as TryInto<HeaderName>>::Error: std::fmt::Debug,
|
||||
{
|
||||
let k = key.try_into().ok()?;
|
||||
(self.0).get_mut(&k)
|
||||
pub fn get(&self, key: &str) -> Option<&String> {
|
||||
(self.0).get(HeaderNameType(key).borrow() as &dyn HeaderKey)
|
||||
}
|
||||
|
||||
pub fn get<T: TryInto<HeaderName> + std::fmt::Debug>(&self, key: T) -> Option<&str>
|
||||
where
|
||||
<T as TryInto<HeaderName>>::Error: std::fmt::Debug,
|
||||
{
|
||||
let k = key.try_into().ok()?;
|
||||
(self.0).get(&k).map(|x| x.as_str())
|
||||
pub fn contains_key(&self, key: &str) -> bool {
|
||||
(self.0).contains_key(HeaderNameType(key).borrow() as &dyn HeaderKey)
|
||||
}
|
||||
|
||||
pub fn contains_key<T: TryInto<HeaderName> + std::fmt::Debug>(&self, key: T) -> bool
|
||||
where
|
||||
<T as TryInto<HeaderName>>::Error: std::fmt::Debug,
|
||||
{
|
||||
key.try_into()
|
||||
.ok()
|
||||
.map(|k| (self.0).contains_key(&k))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn remove<T: TryInto<HeaderName> + std::fmt::Debug>(&mut self, key: T) -> Option<String>
|
||||
where
|
||||
<T as TryInto<HeaderName>>::Error: std::fmt::Debug,
|
||||
{
|
||||
key.try_into().ok().and_then(|k| (self.0).remove(&k))
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> indexmap::IndexMap<HeaderName, String> {
|
||||
self.0
|
||||
pub fn remove(&mut self, key: &str) -> Option<String> {
|
||||
(self.0).remove(HeaderNameType(key).borrow() as &dyn HeaderKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -173,39 +269,21 @@ impl Deref for HeaderMap {
|
|||
}
|
||||
|
||||
impl DerefMut for HeaderMap {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
fn deref_mut(&mut self) -> &mut IndexMap<HeaderName, String> {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_headers_case_sensitivity() {
|
||||
let mut headers = HeaderMap::default();
|
||||
headers.insert("from".try_into().unwrap(), "Myself <a@b.c>".into());
|
||||
assert_eq!(&headers["From"], "Myself <a@b.c>");
|
||||
assert_eq!(&headers["From"], &headers["from"]);
|
||||
assert_eq!(&headers["fROm"], &headers["from"]);
|
||||
headers.get_mut("from").unwrap().pop();
|
||||
assert_eq!(&headers["From"], "Myself <a@b.c");
|
||||
headers.insert("frOM".try_into().unwrap(), "nada".into());
|
||||
assert_eq!(&headers["fROm"], "nada");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_headers_map_index() {
|
||||
let mut headers = HeaderMap::default();
|
||||
headers.insert(HeaderName::SUBJECT, "foobar".into());
|
||||
headers.insert(HeaderName::MESSAGE_ID, "foobar@examplecom".into());
|
||||
assert_eq!(&headers[0], "foobar");
|
||||
assert_eq!(&headers[HeaderName::SUBJECT], "foobar");
|
||||
assert_eq!(&headers[&HeaderName::SUBJECT], "foobar");
|
||||
assert_eq!(&headers["subject"], "foobar");
|
||||
assert_eq!(&headers["Subject"], "foobar");
|
||||
assert_eq!(&headers[b"Subject".as_slice()], "foobar");
|
||||
assert!(&headers[HeaderName::MESSAGE_ID] != "foobar");
|
||||
}
|
||||
#[test]
|
||||
fn test_headers_case_sensitivity() {
|
||||
use std::convert::TryInto;
|
||||
let mut headers = HeaderMap::default();
|
||||
headers.insert("from".try_into().unwrap(), "Myself <a@b.c>".into());
|
||||
assert_eq!(&headers["From"], "Myself <a@b.c>");
|
||||
assert_eq!(&headers["From"], &headers["from"]);
|
||||
assert_eq!(&headers["fROm"], &headers["from"]);
|
||||
headers.get_mut("from").unwrap().pop();
|
||||
assert_eq!(&headers["From"], "Myself <a@b.c");
|
||||
headers.insert("frOM".try_into().unwrap(), "nada".into());
|
||||
assert_eq!(&headers["fROm"], "nada");
|
||||
}
|
||||
|
|
|
@ -1,954 +0,0 @@
|
|||
/*
|
||||
* meli - melib crate.
|
||||
*
|
||||
* Copyright 2023 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/>.
|
||||
*/
|
||||
|
||||
//! E-mail header names. Also referred to as `Fields` in `RFC5322`.
|
||||
#![allow(non_upper_case_globals)]
|
||||
|
||||
use std::{
|
||||
borrow::{Borrow, Cow},
|
||||
cmp::Ordering,
|
||||
convert::TryFrom,
|
||||
error::Error,
|
||||
hash::{Hash, Hasher},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::email::parser::BytesExt;
|
||||
|
||||
bitflags! {
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct Protocol: u32 {
|
||||
const None = 0b00000001;
|
||||
const Mail = Self::None.bits() << 1;
|
||||
const NNTP = Self::Mail.bits() << 1;
|
||||
const MIME = Self::NNTP.bits() << 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Case insensitive wrapper for a header name. As of `RFC5322` it's
|
||||
/// guaranteed to be ASCII.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct HeaderName {
|
||||
inner: Repr<Custom>,
|
||||
}
|
||||
|
||||
impl Custom {
|
||||
fn as_str(&self) -> &str {
|
||||
unsafe { std::str::from_utf8_unchecked(&self.0) }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
enum Repr<T> {
|
||||
Standard(StandardHeader),
|
||||
Custom(T),
|
||||
}
|
||||
|
||||
impl<T: std::fmt::Display> std::fmt::Display for Repr<T> {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Standard(inner) => write!(fmt, "{}", inner.as_str()),
|
||||
Self::Custom(inner) => inner.fmt(fmt),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Used to hijack the Hash impl
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
struct Custom(SmallVec<[u8; 32]>);
|
||||
|
||||
/// A possible error when converting a `HeaderName` from another type.
|
||||
pub struct InvalidHeaderName;
|
||||
|
||||
impl Error for InvalidHeaderName {}
|
||||
|
||||
impl std::fmt::Debug for InvalidHeaderName {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(fmt, "Invalid header name.")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for InvalidHeaderName {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(fmt, "{}", stringify!(InvalidHeaderName))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! standard_headers {
|
||||
(
|
||||
$(
|
||||
$(#[$docs:meta])*
|
||||
($konst:ident, $upcase:ident, $name:literal, $template:expr, $(Protocol::$var:tt)|+,$status:expr,$standards:expr);
|
||||
)+
|
||||
) => {
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum StandardHeader {
|
||||
$(
|
||||
$konst,
|
||||
)+
|
||||
}
|
||||
|
||||
$(
|
||||
$(#[$docs])*
|
||||
pub const $upcase: HeaderName = HeaderName {
|
||||
inner: Repr::Standard(StandardHeader::$konst),
|
||||
};
|
||||
)+
|
||||
|
||||
impl HeaderName {
|
||||
$(
|
||||
pub const $upcase: Self = $upcase;
|
||||
)+
|
||||
}
|
||||
|
||||
impl StandardHeader {
|
||||
#[inline]
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match *self {
|
||||
$(
|
||||
Self::$konst => $name,
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn protocol(&self) -> Protocol {
|
||||
match *self {
|
||||
$(
|
||||
Self::$konst => Protocol::from_bits_truncate($(Protocol::$var.bits()|)* u32::MAX),
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn status(&self) -> Status {
|
||||
match *self {
|
||||
$(
|
||||
Self::$konst => $status,
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn standards(&self) -> &[Standard] {
|
||||
match *self {
|
||||
$(
|
||||
Self::$konst => $standards,
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
// invalid clippy lint match here
|
||||
#[allow(clippy::string_lit_as_bytes)]
|
||||
pub fn from_bytes(name_bytes: &[u8]) -> Option<Self> {
|
||||
match name_bytes {
|
||||
$(
|
||||
_ if name_bytes.eq_ignore_ascii_case($name.as_bytes()) => Some(Self::$konst),
|
||||
)+
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
const TEST_HEADERS: &[(StandardHeader, &str)] = &[
|
||||
$(
|
||||
(StandardHeader::$konst, $name),
|
||||
)+
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! standards {
|
||||
(
|
||||
$(
|
||||
$(#[$docs:meta])*
|
||||
($konst:ident, $upcase:ident, $name:literal, $lowername:literal );
|
||||
)+
|
||||
) => {
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Standard {
|
||||
$(
|
||||
$konst,
|
||||
)+
|
||||
}
|
||||
|
||||
$(
|
||||
$(#[$docs])*
|
||||
pub const $upcase: Standard = Standard::$konst;
|
||||
)+
|
||||
|
||||
impl Standard {
|
||||
#[inline]
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match *self {
|
||||
$(
|
||||
Self::$konst => $name,
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn url(&self) -> &str {
|
||||
match *self {
|
||||
$(
|
||||
Self::$konst => concat!("https://www.rfc-editor.org/rfc/", $lowername, ".html"),
|
||||
)+
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// invalid clippy lint match here
|
||||
#[allow(clippy::string_lit_as_bytes)]
|
||||
pub fn from_bytes(name_bytes: &[u8]) -> Option<Self> {
|
||||
match name_bytes {
|
||||
$(
|
||||
_ if name_bytes.eq_ignore_ascii_case($name.as_bytes()) => Some(Self::$konst),
|
||||
)+
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
standards! {
|
||||
(RFC0850, RFC0850, "RFC0850", "rfc0850");
|
||||
(RFC1808, RFC1808, "RFC1808", "rfc1808");
|
||||
(RFC1849, RFC1849, "RFC1849", "rfc1849");
|
||||
(RFC2068, RFC2068, "RFC2068", "rfc2068");
|
||||
(RFC2076, RFC2076, "RFC2076", "rfc2076");
|
||||
(RFC2110, RFC2110, "RFC2110", "rfc2110");
|
||||
(RFC2156, RFC2156, "RFC2156", "rfc2156");
|
||||
(RFC2557, RFC2557, "RFC2557", "rfc2557");
|
||||
(RFC2616, RFC2616, "RFC2616", "rfc2616");
|
||||
(RFC2980, RFC2980, "RFC2980", "rfc2980");
|
||||
(RFC3798, RFC3798, "RFC3798", "rfc3798");
|
||||
(RFC3834, RFC3834, "RFC3834", "rfc3834");
|
||||
(RFC3865, RFC3865, "RFC3865", "rfc3865");
|
||||
(RFC3977, RFC3977, "RFC3977", "rfc3977");
|
||||
(RFC4021, RFC4021, "RFC4021", "rfc4021");
|
||||
(RFC5064, RFC5064, "RFC5064", "rfc5064");
|
||||
(RFC5321, RFC5321, "RFC5321", "rfc5321");
|
||||
(RFC5322, RFC5322, "RFC5322", "rfc5322");
|
||||
(RFC5337, RFC5337, "RFC5337", "rfc5337");
|
||||
(RFC5504, RFC5504, "RFC5504", "rfc5504");
|
||||
(RFC5518, RFC5518, "RFC5518", "rfc5518");
|
||||
(RFC5536, RFC5536, "RFC5536", "rfc5536");
|
||||
(RFC5537, RFC5537, "RFC5537", "rfc5537");
|
||||
(RFC5703, RFC5703, "RFC5703", "rfc5703");
|
||||
(RFC6017, RFC6017, "RFC6017", "rfc6017");
|
||||
(RFC6068, RFC6068, "RFC6068", "rfc6068");
|
||||
(RFC6109, RFC6109, "RFC6109", "rfc6109");
|
||||
(RFC6376, RFC6376, "RFC6376", "rfc6376");
|
||||
(RFC6477, RFC6477, "RFC6477", "rfc6477");
|
||||
(RFC6758, RFC6758, "RFC6758", "rfc6758");
|
||||
(RFC6854, RFC6854, "RFC6854", "rfc6854");
|
||||
(RFC6857, RFC6857, "RFC6857", "rfc6857");
|
||||
(RFC7208, RFC7208, "RFC7208", "rfc7208");
|
||||
(RFC7259, RFC7259, "RFC7259", "rfc7259");
|
||||
(RFC7293, RFC7293, "RFC7293", "rfc7293");
|
||||
(RFC7444, RFC7444, "RFC7444", "rfc7444");
|
||||
(RFC7681, RFC7681, "RFC7681", "rfc7681");
|
||||
(RFC8058, RFC8058, "RFC8058", "rfc8058");
|
||||
(RFC8255, RFC8255, "RFC8255", "rfc8255");
|
||||
(RFC8315, RFC8315, "RFC8315", "rfc8315");
|
||||
(RFC8460, RFC8460, "RFC8460", "rfc8460");
|
||||
(RFC8601, RFC8601, "RFC8601", "rfc8601");
|
||||
(RFC8617, RFC8617, "RFC8617", "rfc8617");
|
||||
(RFC8689, RFC8689, "RFC8689", "rfc8689");
|
||||
(RFC9057, RFC9057, "RFC9057", "rfc9057");
|
||||
(RFC9228, RFC9228, "RFC9228", "rfc9228");
|
||||
}
|
||||
|
||||
/// Status of field at the moment of writing.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum Status {
|
||||
/// Deprecated,
|
||||
Deprecated,
|
||||
/// Experimental,
|
||||
Experimental,
|
||||
/// Informational,
|
||||
Informational,
|
||||
/// None,
|
||||
None,
|
||||
/// Obsoleted,
|
||||
Obsoleted,
|
||||
/// Reserved,
|
||||
Reserved,
|
||||
/// Standard,
|
||||
Standard,
|
||||
}
|
||||
|
||||
// Generate constants for all standard e-mail field headers.
|
||||
standard_headers! {
|
||||
/* Unit Variant |Constant ident |Actual field value |Template value |Protocols |Status |Standards */
|
||||
/* -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */
|
||||
(Subject, SUBJECT, "Subject", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5536, Standard::RFC5322]);
|
||||
(ReplyTo, REPLY_TO, "Reply-To", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5536, Standard::RFC5322]);
|
||||
(InReplyTo, IN_REPLY_TO, "In-Reply-To", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]);
|
||||
(References, REFERENCES, "References", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5536, Standard::RFC5322]);
|
||||
(MailReplyTo, MAIL_REPLY_TO, "Mail-Reply-To", None, Protocol::Mail, Status::None, &[]);
|
||||
(MailFollowupTo, MAIL_FOLLOWUP_TO, "Mail-Followup-To", None, Protocol::Mail, Status::None, &[]);
|
||||
(DeliveredTo, DELIVERED_TO, "Delivered-To", None, Protocol::Mail, Status::None, &[Standard::RFC9228]);
|
||||
(Comments, COMMENTS, "Comments", None, Protocol::Mail, Status::None, &[]);
|
||||
(Keywords, KEYWORDS, "Keywords", None, Protocol::Mail, Status::None, &[]);
|
||||
(Received, RECEIVED, "Received", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322, Standard::RFC5321]);
|
||||
(ContentLanguage, CONTENT_LANGUAGE, "Content-Language", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(ContentLength, CONTENT_LENGTH, "Content-Length", None, Protocol::Mail, Status::None, &[]);
|
||||
(Forwarded, FORWARDED, "Forwarded", None, Protocol::Mail, Status::None, &[]);
|
||||
(AcceptLanguage, ACCEPT_LANGUAGE, "Accept-Language", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(AlsoControl, ALSO_CONTROL, "Also-Control", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC1849, Standard::RFC5536]);
|
||||
(AlternateRecipient, ALTERNATE_RECIPIENT, "Alternate-Recipient", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Approved, APPROVED, "Approved", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(ArcAuthenticationResults, ARC_AUTHENTICATION_RESULTS, "ARC-Authentication-Results", None, Protocol::Mail, Status::Experimental, &[Standard::RFC8617]);
|
||||
(ArcMessageSignature, ARC_MESSAGE_SIGNATURE, "ARC-Message-Signature", None, Protocol::Mail, Status::Experimental, &[Standard::RFC8617]);
|
||||
(ArcSeal, ARC_SEAL, "ARC-Seal", None, Protocol::Mail, Status::Experimental, &[Standard::RFC8617]);
|
||||
(Archive, ARCHIVE, "Archive", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(ArchivedAt, ARCHIVED_AT, "Archived-At", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5064]);
|
||||
(ArticleNames, ARTICLE_NAMES, "Article-Names", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC1849, Standard::RFC5536]);
|
||||
(ArticleUpdates, ARTICLE_UPDATES, "Article-Updates", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC1849, Standard::RFC5536]);
|
||||
(AuthenticationResults, AUTHENTICATION_RESULTS, "Authentication-Results", None, Protocol::Mail, Status::Standard, &[Standard::RFC8601]);
|
||||
(AutoSubmitted, AUTO_SUBMITTED, "Auto-Submitted", None, Protocol::Mail, Status::Standard, &[Standard::RFC3834]);
|
||||
(Autoforwarded, AUTOFORWARDED, "Autoforwarded", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Autosubmitted, AUTOSUBMITTED, "Autosubmitted", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Base, BASE, "Base", None, Protocol::MIME, Status::Obsoleted, &[Standard::RFC1808, Standard::RFC2068]);
|
||||
(Bcc, BCC, "Bcc", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]);
|
||||
(Body, BODY, "Body", None, Protocol::None, Status::Reserved, &[Standard::RFC6068]);
|
||||
(CancelKey, CANCEL_KEY, "Cancel-Key", None, Protocol::NNTP, Status::Standard, &[Standard::RFC8315]);
|
||||
(CancelLock, CANCEL_LOCK, "Cancel-Lock", None, Protocol::NNTP, Status::Standard, &[Standard::RFC8315]);
|
||||
(Cc, CC, "Cc", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]);
|
||||
(ContentAlternative, CONTENT_ALTERNATIVE, "Content-Alternative", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(ContentBase, CONTENT_BASE, "Content-Base", None, Protocol::MIME, Status::Obsoleted, &[Standard::RFC2110, Standard::RFC2557]);
|
||||
(ContentDescription, CONTENT_DESCRIPTION, "Content-Description", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(ContentDisposition, CONTENT_DISPOSITION, "Content-Disposition", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(ContentDuration, CONTENT_DURATION, "Content-Duration", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(ContentFeatures, CONTENT_FEATURES, "Content-Features", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(ContentId, CONTENT_ID, "Content-ID", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(ContentIdentifier, CONTENT_IDENTIFIER, "Content-Identifier", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ContentLocation, CONTENT_LOCATION, "Content-Location", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(ContentMd5, CONTENT_MD5, "Content-MD5", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(ContentReturn, CONTENT_RETURN, "Content-Return", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ContentTransferEncoding, CONTENT_TRANSFER_ENCODING, "Content-Transfer-Encoding", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(ContentTranslationType, CONTENT_TRANSLATION_TYPE, "Content-Translation-Type", None, Protocol::MIME, Status::Standard, &[Standard::RFC8255]);
|
||||
(ContentType, CONTENT_TYPE, "Content-Type", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(Control, CONTROL, "Control", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(Conversion, CONVERSION, "Conversion", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ConversionWithLoss, CONVERSION_WITH_LOSS, "Conversion-With-Loss", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(DlExpansionHistory, DL_EXPANSION_HISTORY, "DL-Expansion-History", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Date, DATE, "Date", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5536, Standard::RFC5322]);
|
||||
(DateReceived, DATE_RECEIVED, "Date-Received", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC0850, Standard::RFC5536]);
|
||||
(DeferredDelivery, DEFERRED_DELIVERY, "Deferred-Delivery", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(DeliveryDate, DELIVERY_DATE, "Delivery-Date", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(DiscardedX400IpmsExtensions, DISCARDED_X400_IPMS_EXTENSIONS, "Discarded-X400-IPMS-Extensions", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(DiscardedX400MtsExtensions, DISCARDED_X400_MTS_EXTENSIONS, "Discarded-X400-MTS-Extensions", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(DiscloseRecipients, DISCLOSE_RECIPIENTS, "Disclose-Recipients", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(DispositionNotificationOptions, DISPOSITION_NOTIFICATION_OPTIONS, "Disposition-Notification-Options", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(DispositionNotificationTo, DISPOSITION_NOTIFICATION_TO, "Disposition-Notification-To", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Distribution, DISTRIBUTION, "Distribution", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(DkimSignature, DKIM_SIGNATURE, "DKIM-Signature", None, Protocol::Mail, Status::Standard, &[Standard::RFC6376]);
|
||||
(DowngradedBcc, DOWNGRADED_BCC, "Downgraded-Bcc", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedCc, DOWNGRADED_CC, "Downgraded-Cc", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedDispositionNotificationTo, DOWNGRADED_DISPOSITION_NOTIFICATION_TO, "Downgraded-Disposition-Notification-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedFinalRecipient, DOWNGRADED_FINAL_RECIPIENT, "Downgraded-Final-Recipient", None, Protocol::Mail, Status::Standard, &[Standard::RFC6857]);
|
||||
(DowngradedFrom, DOWNGRADED_FROM, "Downgraded-From", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedInReplyTo, DOWNGRADED_IN_REPLY_TO, "Downgraded-In-Reply-To", None, Protocol::Mail, Status::Standard, &[Standard::RFC6857]);
|
||||
(DowngradedMailFrom, DOWNGRADED_MAIL_FROM, "Downgraded-Mail-From", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedMessageId, DOWNGRADED_MESSAGE_ID, "Downgraded-Message-Id", None, Protocol::Mail, Status::Standard, &[Standard::RFC6857]);
|
||||
(DowngradedOriginalRecipient, DOWNGRADED_ORIGINAL_RECIPIENT, "Downgraded-Original-Recipient", None, Protocol::Mail, Status::Standard, &[Standard::RFC6857]);
|
||||
(DowngradedRcptTo, DOWNGRADED_RCPT_TO, "Downgraded-Rcpt-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedReferences, DOWNGRADED_REFERENCES, "Downgraded-References", None, Protocol::Mail, Status::Standard, &[Standard::RFC6857]);
|
||||
(DowngradedReplyTo, DOWNGRADED_REPLY_TO, "Downgraded-Reply-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedResentBcc, DOWNGRADED_RESENT_BCC, "Downgraded-Resent-Bcc", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedResentCc, DOWNGRADED_RESENT_CC, "Downgraded-Resent-Cc", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedResentFrom, DOWNGRADED_RESENT_FROM, "Downgraded-Resent-From", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedResentReplyTo, DOWNGRADED_RESENT_REPLY_TO, "Downgraded-Resent-Reply-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedResentSender, DOWNGRADED_RESENT_SENDER, "Downgraded-Resent-Sender", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedResentTo, DOWNGRADED_RESENT_TO, "Downgraded-Resent-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedReturnPath, DOWNGRADED_RETURN_PATH, "Downgraded-Return-Path", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedSender, DOWNGRADED_SENDER, "Downgraded-Sender", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(DowngradedTo, DOWNGRADED_TO, "Downgraded-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5504, Standard::RFC6857]);
|
||||
(Encoding, ENCODING, "Encoding", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Encrypted, ENCRYPTED, "Encrypted", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Expires, EXPIRES, "Expires", None, Protocol::Mail | Protocol::NNTP, Status::None, &[Standard::RFC4021, Standard::RFC5536]);
|
||||
(ExpiryDate, EXPIRY_DATE, "Expiry-Date", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(FollowupTo, FOLLOWUP_TO, "Followup-To", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(From, FROM, "From", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5322, Standard::RFC6854]);
|
||||
(GenerateDeliveryReport, GENERATE_DELIVERY_REPORT, "Generate-Delivery-Report", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Importance, IMPORTANCE, "Importance", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(IncompleteCopy, INCOMPLETE_COPY, "Incomplete-Copy", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(InjectionDate, INJECTION_DATE, "Injection-Date", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(InjectionInfo, INJECTION_INFO, "Injection-Info", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(Language, LANGUAGE, "Language", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(LatestDeliveryTime, LATEST_DELIVERY_TIME, "Latest-Delivery-Time", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Lines, LINES, "Lines", None, Protocol::NNTP, Status::Deprecated, &[Standard::RFC5536, Standard::RFC3977]);
|
||||
(ListArchive, LIST_ARCHIVE, "List-Archive", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ListHelp, LIST_HELP, "List-Help", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ListId, LIST_ID, "List-ID", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ListOwner, LIST_OWNER, "List-Owner", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ListPost, LIST_POST, "List-Post", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ListSubscribe, LIST_SUBSCRIBE, "List-Subscribe", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ListUnsubscribe, LIST_UNSUBSCRIBE, "List-Unsubscribe", Some("perm/list-unsubscribe"), Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ListUnsubscribePost, LIST_UNSUBSCRIBE_POST, "List-Unsubscribe-Post", None, Protocol::Mail, Status::Standard, &[Standard::RFC8058]);
|
||||
(MessageContext, MESSAGE_CONTEXT, "Message-Context", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(MessageId, MESSAGE_ID, "Message-ID", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5322, Standard::RFC5536]);
|
||||
(MessageType, MESSAGE_TYPE, "Message-Type", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(MimeVersion, MIME_VERSION, "MIME-Version", None, Protocol::MIME, Status::None, &[Standard::RFC4021]);
|
||||
(MtPriority, MT_PRIORITY, "MT-Priority", None, Protocol::Mail, Status::Standard, &[Standard::RFC6758]);
|
||||
(Newsgroups, NEWSGROUPS, "Newsgroups", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(NntpPostingDate, NNTP_POSTING_DATE, "NNTP-Posting-Date", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC5536]);
|
||||
(NntpPostingHost, NNTP_POSTING_HOST, "NNTP-Posting-Host", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC2980, Standard::RFC5536]);
|
||||
(Obsoletes, OBSOLETES, "Obsoletes", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Organization, ORGANIZATION, "Organization", None, Protocol::Mail | Protocol::NNTP, Status::Informational, &[Standard::RFC7681, Standard::RFC5536]);
|
||||
(OriginalEncodedInformationTypes, ORIGINAL_ENCODED_INFORMATION_TYPES, "Original-Encoded-Information-Types", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(OriginalFrom, ORIGINAL_FROM, "Original-From", None, Protocol::Mail, Status::Standard, &[Standard::RFC5703]);
|
||||
(OriginalMessageId, ORIGINAL_MESSAGE_ID, "Original-Message-ID", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(OriginalRecipient, ORIGINAL_RECIPIENT, "Original-Recipient", Some("perm/original-recipient"),Protocol::Mail, Status::Standard, &[Standard::RFC3798, Standard::RFC5337]);
|
||||
(OriginalSender, ORIGINAL_SENDER, "Original-Sender", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5537]);
|
||||
(OriginatorReturnAddress, ORIGINATOR_RETURN_ADDRESS, "Originator-Return-Address", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(OriginalSubject, ORIGINAL_SUBJECT, "Original-Subject", None, Protocol::Mail, Status::Standard, &[Standard::RFC5703]);
|
||||
(Path, PATH, "Path", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(PicsLabel, PICS_LABEL, "PICS-Label", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(PostingVersion, POSTING_VERSION, "Posting-Version", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC0850, Standard::RFC5536]);
|
||||
(PreventNondeliveryReport, PREVENT_NONDELIVERY_REPORT, "Prevent-NonDelivery-Report", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Priority, PRIORITY, "Priority", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(ReceivedSpf, RECEIVED_SPF, "Received-SPF", None, Protocol::Mail, Status::Standard, &[Standard::RFC7208]);
|
||||
(RelayVersion, RELAY_VERSION, "Relay-Version", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC0850, Standard::RFC5536]);
|
||||
(ReplyBy, REPLY_BY, "Reply-By", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(RequireRecipientValidSince, REQUIRE_RECIPIENT_VALID_SINCE, "Require-Recipient-Valid-Since", None, Protocol::Mail, Status::Standard, &[Standard::RFC7293]);
|
||||
(ResentBcc, RESENT_BCC, "Resent-Bcc", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]);
|
||||
(ResentCc, RESENT_CC, "Resent-Cc", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]);
|
||||
(ResentDate, RESENT_DATE, "Resent-Date", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]);
|
||||
(ResentFrom, RESENT_FROM, "Resent-From", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322, Standard::RFC6854]);
|
||||
(ResentMessageId, RESENT_MESSAGE_ID, "Resent-Message-ID", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]);
|
||||
(ResentReplyTo, RESENT_REPLY_TO, "Resent-Reply-To", None, Protocol::Mail, Status::Obsoleted, &[Standard::RFC5322]);
|
||||
(ResentSender, RESENT_SENDER, "Resent-Sender", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322, Standard::RFC6854]);
|
||||
(ResentTo, RESENT_TO, "Resent-To", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]);
|
||||
(ReturnPath, RETURN_PATH, "Return-Path", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]);
|
||||
(SeeAlso, SEE_ALSO, "See-Also", None, Protocol::NNTP, Status::Obsoleted, &[Standard::RFC1849, Standard::RFC5536]);
|
||||
(Sender, SENDER, "Sender", None, Protocol::Mail | Protocol::NNTP, Status::Standard, &[Standard::RFC5322, Standard::RFC6854]);
|
||||
(Sensitivity, SENSITIVITY, "Sensitivity", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Solicitation, SOLICITATION, "Solicitation", None, Protocol::Mail, Status::None, &[Standard::RFC3865]);
|
||||
(Summary, SUMMARY, "Summary", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(Supersedes, SUPERSEDES, "Supersedes", None, Protocol::Mail | Protocol::NNTP, Status::None, &[Standard::RFC5536, Standard::RFC2156]);
|
||||
(TlsReportDomain, TLS_REPORT_DOMAIN, "TLS-Report-Domain", None, Protocol::Mail, Status::Standard, &[Standard::RFC8460]);
|
||||
(TlsReportSubmitter, TLS_REPORT_SUBMITTER, "TLS-Report-Submitter", None, Protocol::Mail, Status::Standard, &[Standard::RFC8460]);
|
||||
(TlsRequired, TLS_REQUIRED, "TLS-Required", None, Protocol::Mail, Status::Standard, &[Standard::RFC8689]);
|
||||
(To, TO, "To", None, Protocol::Mail, Status::Standard, &[Standard::RFC5322]);
|
||||
(UserAgent, USER_AGENT, "User-Agent", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536, Standard::RFC2616]);
|
||||
(VbrInfo, VBR_INFO, "VBR-Info", None, Protocol::Mail, Status::Standard, &[Standard::RFC5518]);
|
||||
(X400ContentIdentifier, X400_CONTENT_IDENTIFIER, "X400-Content-Identifier", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(X400ContentReturn, X400_CONTENT_RETURN, "X400-Content-Return", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(X400ContentType, X400_CONTENT_TYPE, "X400-Content-Type", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(X400MtsIdentifier, X400_MTS_IDENTIFIER, "X400-MTS-Identifier", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(X400Originator, X400_ORIGINATOR, "X400-Originator", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(X400Received, X400_RECEIVED, "X400-Received", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(X400Recipients, X400_RECIPIENTS, "X400-Recipients", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(X400Trace, X400_TRACE, "X400-Trace", None, Protocol::Mail, Status::None, &[Standard::RFC4021]);
|
||||
(Xref, XREF, "Xref", None, Protocol::NNTP, Status::Standard, &[Standard::RFC5536]);
|
||||
(ApparentlyTo, APPARENTLY_TO, "Apparently-To", Some("prov/apparently-to"), Protocol::Mail, Status::None, &[Standard::RFC2076]);
|
||||
(Author, AUTHOR, "Author", None, Protocol::Mail, Status::None, &[Standard::RFC9057]);
|
||||
(EdiintFeatures, EDIINT_FEATURES, "EDIINT-Features", None, Protocol::Mail, Status::None, &[Standard::RFC6017]);
|
||||
(EesstVersion, EESST_VERSION, "Eesst-Version", None, Protocol::Mail, Status::None, &[Standard::RFC7681]);
|
||||
(ErrorsTo, ERRORS_TO, "Errors-To", Some("prov/errors-to"), Protocol::Mail, Status::None, &[Standard::RFC2076]);
|
||||
(JabberId, JABBER_ID, "Jabber-ID", Some("prov/jabber-id"), Protocol::Mail | Protocol::NNTP, Status::None, &[Standard::RFC7259]);
|
||||
(SioLabel, SIO_LABEL, "SIO-Label", None, Protocol::Mail, Status::None, &[Standard::RFC7444]);
|
||||
(SioLabelHistory, SIO_LABEL_HISTORY, "SIO-Label-History", None, Protocol::Mail, Status::None, &[Standard::RFC7444]);
|
||||
(XArchivedAt, X_ARCHIVED_AT, "X-Archived-At", Some("prov/x-archived-at"), Protocol::Mail | Protocol::NNTP, Status::Deprecated, &[Standard::RFC5064]);
|
||||
(XMittente, X_MITTENTE, "X-Mittente", None, Protocol::Mail, Status::None, &[Standard::RFC6109]);
|
||||
(XRicevuta, X_RICEVUTA, "X-Ricevuta", None, Protocol::Mail, Status::None, &[Standard::RFC6109]);
|
||||
(XRiferimentoMessageId, X_RIFERIMENTO_MESSAGE_ID, "X-Riferimento-Message-ID", None, Protocol::Mail, Status::None, &[Standard::RFC6109]);
|
||||
(XTiporicevuta, X_TIPORICEVUTA, "X-TipoRicevuta", None, Protocol::Mail, Status::None, &[Standard::RFC6109]);
|
||||
(XTrasporto, X_TRASPORTO, "X-Trasporto", None, Protocol::Mail, Status::None, &[Standard::RFC6109]);
|
||||
(XVerificasicurezza, X_VERIFICASICUREZZA, "X-VerificaSicurezza", None, Protocol::Mail, Status::None, &[Standard::RFC6109]);
|
||||
}
|
||||
|
||||
/// Valid header name ASCII bytes
|
||||
///
|
||||
/// Source: [RFC5322 3.6.8.](https://datatracker.ietf.org/doc/html/rfc5322#autoid-35)
|
||||
/// ```text
|
||||
/// field-name = 1*ftext
|
||||
///
|
||||
/// ftext = %d33-57 / ; Printable US-ASCII
|
||||
/// %d59-126 ; characters not including
|
||||
/// ; ":".
|
||||
/// ```
|
||||
const HEADER_CHARS: [u8; 128] = [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // x
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1x
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2x
|
||||
0, 0, 0, b'!', b'"', b'#', b'$', b'%', b'&', b'\'', // 3x
|
||||
0, 0, b'*', b'+', 0, b'-', b'.', 0, b'0', b'1', // 4x
|
||||
b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', 0, 0, // 5x
|
||||
0, 0, 0, 0, 0, b'a', b'b', b'c', b'd', b'e', // 6x
|
||||
b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', b'n', b'o', // 7x
|
||||
b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', b'x', b'y', // 8x
|
||||
b'z', 0, 0, 0, b'^', b'_', b'`', b'a', b'b', b'c', // 9x
|
||||
b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', // 10x
|
||||
b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', // 11x
|
||||
b'x', b'y', b'z', 0, b'|', 0, b'~', 0, // 128
|
||||
];
|
||||
|
||||
impl HeaderName {
|
||||
/// Returns a `str` representation of the header.
|
||||
///
|
||||
/// The returned string will always be lower case. Use `Display` for a
|
||||
/// properly formatted representation.
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self.inner {
|
||||
Repr::Standard(v) => v.as_str(),
|
||||
Repr::Custom(ref v) => v.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a `&[u8]` representation of the header.
|
||||
///
|
||||
/// The returned string will always be lower case. Use `Display` for a
|
||||
/// properly formatted representation.
|
||||
#[inline]
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
match self.inner {
|
||||
Repr::Standard(v) => v.as_str().as_bytes(),
|
||||
Repr::Custom(ref v) => v.0.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_bytes(src: &[u8]) -> Result<Self, InvalidHeaderName> {
|
||||
if let Some(std) = StandardHeader::from_bytes(src.trim()) {
|
||||
Ok(Self {
|
||||
inner: Repr::Standard(std),
|
||||
})
|
||||
} else {
|
||||
let mut buf = SmallVec::<[u8; 32]>::new();
|
||||
for b in src {
|
||||
if let Some(b) = HEADER_CHARS.get(*b as usize).filter(|b| **b != 0) {
|
||||
buf.push(*b);
|
||||
} else {
|
||||
return Err(InvalidHeaderName::new());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
inner: Repr::Custom(Custom(buf)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn is_standard(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self {
|
||||
inner: Repr::Standard(_)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for HeaderName {
|
||||
type Err = InvalidHeaderName;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, InvalidHeaderName> {
|
||||
Self::from_bytes(s.as_bytes()).map_err(|_| InvalidHeaderName::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for HeaderName {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for HeaderName {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.as_str().as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<str> for HeaderName {
|
||||
fn borrow(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for HeaderName {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(fmt, "{}", &self.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for HeaderName {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Helper {
|
||||
S(String),
|
||||
B(Vec<u8>),
|
||||
}
|
||||
<Helper>::deserialize(deserializer)
|
||||
.map_err(|_| de::Error::custom("invalid header name value"))
|
||||
.and_then(|s| {
|
||||
Self::from_bytes(match &s {
|
||||
Helper::S(v) => v.as_bytes(),
|
||||
Helper::B(v) => v.as_slice(),
|
||||
})
|
||||
.map_err(|_| de::Error::custom("invalid header name value"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for HeaderName {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl InvalidHeaderName {
|
||||
const fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Self> for HeaderName {
|
||||
fn from(src: &'a Self) -> Self {
|
||||
src.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&HeaderName> for Cow<'static, str> {
|
||||
fn from(src: &HeaderName) -> Self {
|
||||
match src.inner {
|
||||
Repr::Standard(s) => Cow::Borrowed(s.as_str()),
|
||||
Repr::Custom(_) => Cow::Owned(src.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for HeaderName {
|
||||
type Error = InvalidHeaderName;
|
||||
#[inline]
|
||||
fn try_from(s: &'a str) -> Result<Self, Self::Error> {
|
||||
Self::from_bytes(s.as_bytes()).map_err(|_| InvalidHeaderName::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a String> for HeaderName {
|
||||
type Error = InvalidHeaderName;
|
||||
#[inline]
|
||||
fn try_from(s: &'a String) -> Result<Self, Self::Error> {
|
||||
Self::from_bytes(s.as_bytes()).map_err(|_| InvalidHeaderName::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a [u8]> for HeaderName {
|
||||
type Error = InvalidHeaderName;
|
||||
#[inline]
|
||||
fn try_from(s: &'a [u8]) -> Result<Self, Self::Error> {
|
||||
Self::from_bytes(s).map_err(|_| InvalidHeaderName::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for HeaderName {
|
||||
type Error = InvalidHeaderName;
|
||||
|
||||
#[inline]
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
Self::from_bytes(s.as_bytes()).map_err(|_| InvalidHeaderName::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for HeaderName {
|
||||
type Error = InvalidHeaderName;
|
||||
|
||||
#[inline]
|
||||
fn try_from(vec: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
Self::from_bytes(&vec).map_err(|_| InvalidHeaderName::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
impl From<StandardHeader> for HeaderName {
|
||||
fn from(src: StandardHeader) -> Self {
|
||||
Self {
|
||||
inner: Repr::Standard(src),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
impl From<Custom> for HeaderName {
|
||||
fn from(src: Custom) -> Self {
|
||||
Self {
|
||||
inner: Repr::Custom(src),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialEq<&'a Self> for HeaderName {
|
||||
#[inline]
|
||||
fn eq(&self, other: &&'a Self) -> bool {
|
||||
*self == **other
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialEq<HeaderName> for &'a HeaderName {
|
||||
#[inline]
|
||||
fn eq(&self, other: &HeaderName) -> bool {
|
||||
*other == *self
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<str> for HeaderName {
|
||||
/// Performs a case-insensitive comparison of the string against the header
|
||||
/// name
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use melib::email::headers::HeaderName;
|
||||
///
|
||||
/// assert_eq!(HeaderName::CONTENT_LENGTH, "content-length");
|
||||
/// assert_eq!(HeaderName::CONTENT_LENGTH, "Content-Length");
|
||||
/// assert_ne!(HeaderName::CONTENT_LENGTH, "content length");
|
||||
/// ```
|
||||
#[inline]
|
||||
fn eq(&self, other: &str) -> bool {
|
||||
self.as_str().eq_ignore_ascii_case(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<HeaderName> for str {
|
||||
/// Performs a case-insensitive comparison of the string against the header
|
||||
/// name
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::convert::TryFrom;
|
||||
///
|
||||
/// use melib::email::headers::HeaderName;
|
||||
///
|
||||
/// assert_eq!(HeaderName::CONTENT_LENGTH, "content-length");
|
||||
/// assert_eq!(HeaderName::CONTENT_LENGTH, "Content-Length");
|
||||
/// assert_ne!(HeaderName::CONTENT_LENGTH, "content length");
|
||||
/// assert_eq!(
|
||||
/// HeaderName::CONTENT_LENGTH,
|
||||
/// HeaderName::try_from("content-length").unwrap()
|
||||
/// );
|
||||
/// ```
|
||||
#[inline]
|
||||
fn eq(&self, other: &HeaderName) -> bool {
|
||||
*other == *self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialEq<&'a str> for HeaderName {
|
||||
/// Performs a case-insensitive comparison of the string against the header
|
||||
/// name
|
||||
#[inline]
|
||||
fn eq(&self, other: &&'a str) -> bool {
|
||||
*self == **other
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialEq<HeaderName> for &'a str {
|
||||
/// Performs a case-insensitive comparison of the string against the header
|
||||
/// name
|
||||
#[inline]
|
||||
fn eq(&self, other: &HeaderName) -> bool {
|
||||
*other == *self
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Custom {
|
||||
#[inline]
|
||||
fn hash<H: Hasher>(&self, hasher: &mut H) {
|
||||
for b in self.0.as_slice() {
|
||||
hasher.write_u8(b.to_ascii_lowercase())
|
||||
}
|
||||
}
|
||||
}
|
||||
const UPPERCASE_TOKENS: &[&str] = &[
|
||||
"ARC", "DKIM", "DL", "EDIINT", "ID", "IPMS", "MD5", "MIME", "MT", "MTS", "NNTP", "PICS", "RSS",
|
||||
"SIO", "SPF", "TLS", "VBR",
|
||||
];
|
||||
|
||||
impl std::fmt::Display for Custom {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let as_str = self.as_str();
|
||||
let len = as_str.len();
|
||||
let mut bytes_count = 0;
|
||||
for chunk in as_str.split('-') {
|
||||
if let Ok(tok) = UPPERCASE_TOKENS.binary_search_by(|probe| {
|
||||
if probe.eq_ignore_ascii_case(chunk) {
|
||||
Ordering::Equal
|
||||
} else {
|
||||
let mut iter = AsciiIgnoreCaseCmp {
|
||||
ord: Ordering::Equal,
|
||||
a: probe.as_bytes(),
|
||||
b: chunk.as_bytes(),
|
||||
};
|
||||
let _cnt: usize = iter.by_ref().fuse().count();
|
||||
debug_assert!(
|
||||
_cnt <= probe.len(),
|
||||
"_cnt {} should be lte probe.len() {}, for probe {} and chunk {}",
|
||||
_cnt,
|
||||
probe.len(),
|
||||
probe,
|
||||
chunk
|
||||
);
|
||||
debug_assert!(
|
||||
_cnt <= chunk.len(),
|
||||
"_cnt {} should be lte chunk.len() {}, for probe {} and chunk {}",
|
||||
_cnt,
|
||||
chunk.len(),
|
||||
probe,
|
||||
chunk
|
||||
);
|
||||
iter.ord
|
||||
}
|
||||
}) {
|
||||
write!(fmt, "{}", UPPERCASE_TOKENS[tok])?;
|
||||
} else {
|
||||
if let Some(first) = chunk.chars().next() {
|
||||
write!(fmt, "{}", first.to_ascii_uppercase())?;
|
||||
}
|
||||
for ch in chunk.chars().skip(1) {
|
||||
write!(fmt, "{}", ch.to_ascii_lowercase())?
|
||||
}
|
||||
}
|
||||
bytes_count += chunk.len();
|
||||
if bytes_count != len {
|
||||
bytes_count += 1;
|
||||
write!(fmt, "-")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// an iterator which alternates between Some and None
|
||||
struct AsciiIgnoreCaseCmp<'a, 'b> {
|
||||
ord: Ordering,
|
||||
a: &'a [u8],
|
||||
b: &'b [u8],
|
||||
}
|
||||
|
||||
impl<'a, 'b> Iterator for AsciiIgnoreCaseCmp<'a, 'b> {
|
||||
type Item = ();
|
||||
|
||||
fn next(&mut self) -> Option<()> {
|
||||
match (self.a.first(), self.b.first()) {
|
||||
(Some(a_char), Some(b_char)) => {
|
||||
self.ord = a_char
|
||||
.to_ascii_lowercase()
|
||||
.cmp(&b_char.to_ascii_lowercase());
|
||||
self.a = &self.a[1..];
|
||||
self.b = &self.b[1..];
|
||||
if self.ord == Ordering::Equal {
|
||||
Some(())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
(Some(_), None) => {
|
||||
self.ord = Ordering::Greater;
|
||||
None
|
||||
}
|
||||
(None, Some(_)) => {
|
||||
self.ord = Ordering::Less;
|
||||
None
|
||||
}
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_email_headers_headername_display() {
|
||||
assert_eq!(&HeaderName::SUBJECT.to_string(), "Subject");
|
||||
assert_eq!(&HeaderName::CC.to_string(), "Cc");
|
||||
assert_eq!(&HeaderName::IN_REPLY_TO.to_string(), "In-Reply-To");
|
||||
assert_eq!(
|
||||
&HeaderName::ORIGINAL_MESSAGE_ID.to_string(),
|
||||
"Original-Message-ID"
|
||||
);
|
||||
assert_eq!(
|
||||
&HeaderName::try_from("x-user-agent").unwrap().to_string(),
|
||||
"X-User-Agent"
|
||||
);
|
||||
assert_eq!(
|
||||
&HeaderName::try_from("arc-foobar").unwrap().to_string(),
|
||||
"ARC-Foobar"
|
||||
);
|
||||
assert_eq!(
|
||||
&HeaderName::try_from("x-rss-feed").unwrap().to_string(),
|
||||
"X-RSS-Feed"
|
||||
);
|
||||
assert_eq!(
|
||||
&HeaderName::try_from("With-regards-to").unwrap().to_string(),
|
||||
"With-Regards-To"
|
||||
);
|
||||
assert_eq!(
|
||||
&HeaderName::try_from("in-response-to-id")
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
"In-Response-To-ID"
|
||||
);
|
||||
assert_eq!(
|
||||
&HeaderName::try_from("something-dKim").unwrap().to_string(),
|
||||
"Something-DKIM"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email_headers_parse_standard_headers() {
|
||||
for &(std, name) in TEST_HEADERS {
|
||||
// Test lower case
|
||||
assert_eq!(
|
||||
HeaderName::from_bytes(name.to_ascii_lowercase().as_bytes()).unwrap(),
|
||||
HeaderName::from(std)
|
||||
);
|
||||
|
||||
// Test upper case
|
||||
let upper = std::str::from_utf8(name.as_bytes())
|
||||
.expect("byte string constants are all utf-8")
|
||||
.to_uppercase();
|
||||
assert_eq!(
|
||||
HeaderName::from_bytes(upper.as_bytes()).unwrap(),
|
||||
HeaderName::from(std)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,15 +19,13 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Parsing of `RFC2369` and `RFC2919` `List-*` headers.
|
||||
|
||||
/*! Parsing of rfc2369/rfc2919 `List-*` headers */
|
||||
use super::parser;
|
||||
use super::Envelope;
|
||||
use smallvec::SmallVec;
|
||||
use std::convert::From;
|
||||
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::{parser, Envelope};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum ListAction<'a> {
|
||||
Url(&'a [u8]),
|
||||
Email(&'a [u8]),
|
||||
|
@ -45,8 +43,8 @@ impl<'a> From<&'a [u8]> for ListAction<'a> {
|
|||
} else if value.starts_with(b"NO") {
|
||||
ListAction::No
|
||||
} else {
|
||||
/* Otherwise treat it as url. There's no foolproof way to check if this is
|
||||
* valid, so postpone it until we try an HTTP request.
|
||||
/* Otherwise treat it as url. There's no foolproof way to check if this is valid, so
|
||||
* postpone it until we try an HTTP request.
|
||||
*/
|
||||
ListAction::Url(value)
|
||||
}
|
||||
|
@ -57,8 +55,8 @@ impl<'a> ListAction<'a> {
|
|||
pub fn parse_options_list(input: &'a [u8]) -> Option<SmallVec<[ListAction<'a>; 4]>> {
|
||||
parser::mailing_lists::rfc_2369_list_headers_action_list(input)
|
||||
.map(|(_, mut vec)| {
|
||||
/* Prefer email options first, since this _is_ a mail client after all and
|
||||
* it's more automated */
|
||||
/* Prefer email options first, since this _is_ a mail client after all and it's
|
||||
* more automated */
|
||||
vec.sort_unstable_by(|a, b| {
|
||||
match (a.starts_with(b"mailto:"), b.starts_with(b"mailto:")) {
|
||||
(true, false) => std::cmp::Ordering::Less,
|
||||
|
@ -88,6 +86,7 @@ pub fn list_id_header(envelope: &'_ Envelope) -> Option<&'_ str> {
|
|||
.other_headers()
|
||||
.get("List-ID")
|
||||
.or_else(|| envelope.other_headers().get("List-Id"))
|
||||
.map(String::as_str)
|
||||
}
|
||||
|
||||
pub fn list_id(header: Option<&'_ str>) -> Option<&'_ str> {
|
||||
|
@ -107,10 +106,9 @@ pub fn list_id(header: Option<&'_ str>) -> Option<&'_ str> {
|
|||
|
||||
impl<'a> ListActions<'a> {
|
||||
pub fn detect(envelope: &'a Envelope) -> Option<ListActions<'a>> {
|
||||
let mut ret = Self {
|
||||
id: list_id_header(envelope),
|
||||
..Self::default()
|
||||
};
|
||||
let mut ret = ListActions::default();
|
||||
|
||||
ret.id = list_id_header(envelope);
|
||||
|
||||
if let Some(archive) = envelope.other_headers().get("List-Archive") {
|
||||
if archive.starts_with('<') {
|
||||
|
|
|
@ -19,118 +19,35 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Parsing of `mailto` addresses.
|
||||
//!
|
||||
//! Conforming to [RFC6068](https://www.rfc-editor.org/rfc/rfc6068) which obsoletes
|
||||
//! [RFC2368](https://www.rfc-editor.org/rfc/rfc2368).
|
||||
|
||||
/*! Parsing of `mailto` addresses */
|
||||
use super::*;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
email::headers::HeaderMap,
|
||||
utils::percent_encoding::{AsciiSet, CONTROLS},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub struct Mailto {
|
||||
pub address: Vec<Address>,
|
||||
pub address: Address,
|
||||
pub subject: Option<String>,
|
||||
pub cc: Option<String>,
|
||||
pub bcc: Option<String>,
|
||||
pub body: Option<String>,
|
||||
pub headers: HeaderMap,
|
||||
}
|
||||
|
||||
impl Mailto {
|
||||
pub const IGNORE_HEADERS: &[HeaderName] = &[
|
||||
HeaderName::FROM,
|
||||
HeaderName::DATE,
|
||||
HeaderName::MESSAGE_ID,
|
||||
HeaderName::APPARENTLY_TO,
|
||||
HeaderName::ARC_AUTHENTICATION_RESULTS,
|
||||
HeaderName::ARC_MESSAGE_SIGNATURE,
|
||||
HeaderName::ARC_SEAL,
|
||||
HeaderName::AUTHENTICATION_RESULTS,
|
||||
HeaderName::AUTOFORWARDED,
|
||||
HeaderName::AUTO_SUBMITTED,
|
||||
HeaderName::AUTOSUBMITTED,
|
||||
HeaderName::BASE,
|
||||
HeaderName::CONTENT_ALTERNATIVE,
|
||||
HeaderName::CONTENT_BASE,
|
||||
HeaderName::CONTENT_DESCRIPTION,
|
||||
HeaderName::CONTENT_DISPOSITION,
|
||||
HeaderName::CONTENT_DURATION,
|
||||
HeaderName::CONTENT_FEATURES,
|
||||
HeaderName::CONTENT_ID,
|
||||
HeaderName::CONTENT_IDENTIFIER,
|
||||
HeaderName::CONTENT_LANGUAGE,
|
||||
HeaderName::CONTENT_LENGTH,
|
||||
HeaderName::CONTENT_LOCATION,
|
||||
HeaderName::CONTENT_MD5,
|
||||
HeaderName::CONTENT_RETURN,
|
||||
HeaderName::CONTENT_TRANSFER_ENCODING,
|
||||
HeaderName::CONTENT_TRANSLATION_TYPE,
|
||||
HeaderName::CONTENT_TYPE,
|
||||
HeaderName::DELIVERED_TO,
|
||||
HeaderName::DKIM_SIGNATURE,
|
||||
HeaderName::ENCRYPTED,
|
||||
HeaderName::FORWARDED,
|
||||
HeaderName::MAIL_FOLLOWUP_TO,
|
||||
HeaderName::MAIL_REPLY_TO,
|
||||
HeaderName::MIME_VERSION,
|
||||
HeaderName::ORIGINAL_ENCODED_INFORMATION_TYPES,
|
||||
HeaderName::ORIGINAL_FROM,
|
||||
HeaderName::ORIGINAL_MESSAGE_ID,
|
||||
HeaderName::ORIGINAL_RECIPIENT,
|
||||
HeaderName::ORIGINAL_SUBJECT,
|
||||
HeaderName::ORIGINATOR_RETURN_ADDRESS,
|
||||
HeaderName::RECEIVED,
|
||||
HeaderName::RECEIVED_SPF,
|
||||
HeaderName::RESENT_BCC,
|
||||
HeaderName::RESENT_CC,
|
||||
HeaderName::RESENT_DATE,
|
||||
HeaderName::RESENT_FROM,
|
||||
HeaderName::RESENT_MESSAGE_ID,
|
||||
HeaderName::RESENT_REPLY_TO,
|
||||
HeaderName::RESENT_SENDER,
|
||||
HeaderName::RESENT_TO,
|
||||
HeaderName::RETURN_PATH,
|
||||
HeaderName::SENDER,
|
||||
HeaderName::USER_AGENT,
|
||||
];
|
||||
|
||||
pub const MAILTO_CHARSET: &AsciiSet = &CONTROLS
|
||||
.add(b' ')
|
||||
.add(b'"')
|
||||
.add(b'"')
|
||||
.add(b'#')
|
||||
.add(b'%')
|
||||
.add(b'/')
|
||||
.add(b'<')
|
||||
.add(b'>')
|
||||
.add(b'?')
|
||||
.add(b'`')
|
||||
.add(b'{')
|
||||
.add(b'}');
|
||||
}
|
||||
|
||||
impl From<Mailto> for Draft {
|
||||
fn from(val: Mailto) -> Self {
|
||||
let mut ret = Self::default();
|
||||
let mut ret = Draft::default();
|
||||
let Mailto {
|
||||
address: _,
|
||||
address,
|
||||
subject,
|
||||
cc,
|
||||
bcc,
|
||||
body,
|
||||
headers,
|
||||
} = val;
|
||||
for (hdr, val) in headers.into_inner() {
|
||||
ret.set_header(hdr, val);
|
||||
}
|
||||
ret.set_header("Subject", subject.unwrap_or_default());
|
||||
ret.set_header("Cc", cc.unwrap_or_default());
|
||||
ret.set_header("Bcc", bcc.unwrap_or_default());
|
||||
ret.set_body(body.unwrap_or_default());
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Mailto> for Draft {
|
||||
fn from(val: &Mailto) -> Self {
|
||||
Self::from(val.clone())
|
||||
ret.set_header("To", address.to_string());
|
||||
debug!(ret)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,316 +55,95 @@ impl TryFrom<&[u8]> for Mailto {
|
|||
type Error = String;
|
||||
|
||||
fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
|
||||
super::parser::generic::mailto(value)
|
||||
.map(|(_, v)| v)
|
||||
.map_err(|err| {
|
||||
log::debug!(
|
||||
"parser::mailto returned error while parsing {}:\n{:?}",
|
||||
String::from_utf8_lossy(value),
|
||||
&err,
|
||||
);
|
||||
format!("{:?}", err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Mailto {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
|
||||
super::parser::generic::mailto(value.as_bytes())
|
||||
.map(|(_, v)| v)
|
||||
.map_err(|err| {
|
||||
log::debug!(
|
||||
"parser::mailto returned error while parsing {}:\n{:?}",
|
||||
value,
|
||||
&err
|
||||
);
|
||||
format!("{:?}", err)
|
||||
})
|
||||
let parse_res = super::parser::generic::mailto(value).map(|(_, v)| v);
|
||||
if let Ok(res) = parse_res {
|
||||
Ok(res)
|
||||
} else {
|
||||
debug!(
|
||||
"parser::mailto returned error while parsing {}:\n{:?}",
|
||||
String::from_utf8_lossy(value),
|
||||
parse_res.as_ref().err().unwrap()
|
||||
);
|
||||
Err(format!("{:?}", parse_res.err().unwrap()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use HeaderName as HDR;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_mailto() {
|
||||
macro_rules! addr {
|
||||
($lit:literal) => {
|
||||
Address::try_from($lit).unwrap()
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! mlt {
|
||||
($lit:literal) => {
|
||||
Mailto::try_from($lit).expect("Could not parse mailto link.")
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! hdr {
|
||||
($lit:literal) => {
|
||||
HeaderName::try_from($lit).expect("Could not parse header name.")
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! hdrmap {
|
||||
($(($field:literal, $val:literal)),+) => {{
|
||||
let mut m = HeaderMap::empty();
|
||||
$(
|
||||
m.insert(hdr!($field), $val.into());
|
||||
)+
|
||||
|
||||
m
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! test_case {
|
||||
($mailto:literal, addresses => $($addr:literal),*; body => $body:expr; $(($field:literal, $val:literal)),+) => {{
|
||||
let addresses = &[
|
||||
$(
|
||||
addr!($addr)
|
||||
),*
|
||||
];
|
||||
let Mailto {
|
||||
address,
|
||||
body,
|
||||
headers,
|
||||
} = mlt!($mailto);
|
||||
assert_eq!(
|
||||
(address.as_slice(), body.as_ref().map(|b| b.as_str()), headers),
|
||||
(addresses.as_slice(), $body, hdrmap!($(($field, $val)),*))
|
||||
);
|
||||
}}
|
||||
}
|
||||
|
||||
test_case!("mailto:info@example.com?subject=email%20subject",
|
||||
addresses=> "info@example.com";
|
||||
body => None;
|
||||
("To", "info@example.com"), ("Subject", "email subject")
|
||||
);
|
||||
test_case!("mailto:info@example.com?cc=8cc9@example.com",
|
||||
addresses=> "info@example.com";
|
||||
body => None;
|
||||
("To", "info@example.com"), ("Cc", "8cc9@example.com")
|
||||
);
|
||||
test_case!("mailto:info@example.com?bcc=7bcc8@example.com&body=line%20first%0Abut%20not%0Alast",
|
||||
addresses=> "info@example.com";
|
||||
body => Some("line first\nbut not\nlast");
|
||||
("To", "info@example.com"), ("Bcc", "7bcc8@example.com")
|
||||
);
|
||||
|
||||
test_case!("mailto:info@example.com?In-Reply-To=%3C20230526204845.673031-1-manos.pitsidianakis@linaro.org%3E&Cc=kraxel%40redhat.com%2Cqemu-devel%40nongnu.org&Subject=Re%3A%20%5BPATCH%5D%20Add%20virtio-sound%20and%20virtio-sound-pci%20devices",
|
||||
addresses=> "info@example.com";
|
||||
body => None;
|
||||
("To", "info@example.com"), ("Subject", "Re: [PATCH] Add virtio-sound and virtio-sound-pci devices"), ("Cc", "kraxel@redhat.com,qemu-devel@nongnu.org"), ("In-Reply-To", "<20230526204845.673031-1-manos.pitsidianakis@linaro.org>")
|
||||
);
|
||||
let test_address = super::parser::address::address(b"info@example.com")
|
||||
.map(|(_, v)| v)
|
||||
.unwrap();
|
||||
let mailto = Mailto::try_from(&b"mailto:info@example.com?subject=email%20subject"[0..])
|
||||
.expect("Could not parse mailto link.");
|
||||
let Mailto {
|
||||
ref address,
|
||||
ref subject,
|
||||
ref cc,
|
||||
ref bcc,
|
||||
ref body,
|
||||
} = mailto;
|
||||
|
||||
assert_eq!(
|
||||
mlt!("mailto:chris@example.com%2C%20tony@example.com"),
|
||||
mlt!("mailto:?to=chris@example.com%2C%20tony@example.com")
|
||||
(
|
||||
address,
|
||||
subject.as_ref().map(String::as_str),
|
||||
cc.as_ref().map(String::as_str),
|
||||
bcc.as_ref().map(String::as_str),
|
||||
body.as_ref().map(String::as_str),
|
||||
),
|
||||
(&test_address, Some("email%20subject"), None, None, None)
|
||||
);
|
||||
|
||||
/* address plus to= should be ignored */
|
||||
assert!(
|
||||
Mailto::try_from("mailto:?to=chris@example.com%2C%20tony@example.com")
|
||||
!= Mailto::try_from("mailto:chris@example.com?to=tony@example.com"),
|
||||
"{:?} == {:?}",
|
||||
Mailto::try_from("mailto:?to=chris@example.com%2C%20tony@example.com"),
|
||||
Mailto::try_from("mailto:chris@example.com?to=tony@example.com")
|
||||
);
|
||||
|
||||
// URLs for an ordinary individual mailing address:
|
||||
test_case!("mailto:chris@example.com",
|
||||
addresses=> "chris@example.com";
|
||||
body => None;
|
||||
("To", "chris@example.com")
|
||||
);
|
||||
|
||||
// A URL for a mail response system that requires the name of the file in the
|
||||
// subject:
|
||||
|
||||
test_case!("mailto:infobot@example.com?subject=current-issue",
|
||||
addresses => "infobot@example.com";
|
||||
body => None;
|
||||
("To", "infobot@example.com"), ("Subject", "current-issue")
|
||||
);
|
||||
|
||||
// A mail response system that requires a "send" request in the body:
|
||||
|
||||
test_case!("mailto:infobot@example.com?body=send%20current-issue",
|
||||
addresses => "infobot@example.com";
|
||||
body => Some("send current-issue");
|
||||
("To", "infobot@example.com")
|
||||
);
|
||||
|
||||
//A similar URL could have two lines with different "send" requests (in this
|
||||
// case, "send current-issue" and, on the next line, "send index".)
|
||||
|
||||
test_case!("mailto:infobot@example.com?body=send%20current-issue%0D%0Asend%20index",
|
||||
addresses => "infobot@example.com";
|
||||
body => Some("send current-issue\r\nsend index");
|
||||
("To", "infobot@example.com")
|
||||
);
|
||||
// An interesting use of your mailto URL is when browsing archives of messages.
|
||||
// Each browsed message might contain a mailto URL like:
|
||||
|
||||
test_case!("mailto:foobar@example.com?In-Reply-To=%3c3469A91.D10AF4C@example.com%3e",
|
||||
addresses => "foobar@example.com";
|
||||
body => None;
|
||||
("To", "foobar@example.com"), ("In-Reply-To", "<3469A91.D10AF4C@example.com>")
|
||||
);
|
||||
|
||||
// A request to subscribe to a mailing list:
|
||||
|
||||
test_case!("mailto:majordomo@example.com?body=subscribe%20bamboo-l",
|
||||
addresses => "majordomo@example.com";
|
||||
body => Some("subscribe bamboo-l");
|
||||
("To", "majordomo@example.com")
|
||||
);
|
||||
|
||||
// A URL for a single user which includes a CC of another user:
|
||||
|
||||
test_case!("mailto:joe@example.com?cc=bob@example.com&body=hello",
|
||||
addresses => "joe@example.com";
|
||||
body => Some("hello");
|
||||
("To", "joe@example.com"), ("Cc", "bob@example.com")
|
||||
);
|
||||
|
||||
// Another way of expressing the same thing:
|
||||
|
||||
test_case!("mailto:?to=joe@example.com&cc=bob@example.com&body=hello",
|
||||
addresses => "joe@example.com";
|
||||
body => Some("hello");
|
||||
("To", "joe@example.com"), ("Cc", "bob@example.com")
|
||||
);
|
||||
|
||||
// Note the use of the "&" reserved character, above. The following example,
|
||||
// by using "?" twice, is incorrect: <mailto:joe@example.com?cc=bob@
|
||||
// example.com?body=hello> ; WRONG!
|
||||
|
||||
Mailto::try_from("mailto:joe@example.com?cc=bob@example.com?body=hello").unwrap_err();
|
||||
|
||||
// <a href="mailto:?to=joe@xyz.com&cc=bob@xyz.com&body=hello"> assert
|
||||
// these are equal
|
||||
|
||||
test_case!("mailto:?to=joe@example.com&cc=bob@example.com&body=hello",
|
||||
addresses => "joe@example.com";
|
||||
body => Some("hello");
|
||||
("To", "joe@example.com"), ("Cc", "bob@example.com")
|
||||
);
|
||||
|
||||
// To indicate the address "gorby%kremvax@example.com" one would do:
|
||||
// <mailto:gorby%25kremvax@example.com>
|
||||
|
||||
test_case!("mailto:gorby%25kremvax@example.com",
|
||||
addresses => "gorby%kremvax@example.com";
|
||||
body => None;
|
||||
("To", "gorby%kremvax@example.com")
|
||||
);
|
||||
|
||||
// Custom header is ignored
|
||||
// <mailto:address@example.com?blat=foop>
|
||||
|
||||
test_case!("mailto:address@example.com?blat=foop",
|
||||
addresses => "address@example.com";
|
||||
body => None;
|
||||
("To", "address@example.com")
|
||||
);
|
||||
|
||||
// 6.2. Examples of Complicated Email Addresses
|
||||
|
||||
let mailto = Mailto::try_from(&b"mailto:info@example.com?cc=8cc9@example.com"[0..])
|
||||
.expect("Could not parse mailto link.");
|
||||
let Mailto {
|
||||
ref address,
|
||||
ref subject,
|
||||
ref cc,
|
||||
ref bcc,
|
||||
ref body,
|
||||
} = mailto;
|
||||
assert_eq!(
|
||||
mlt!("mailto:%22not%40me%22@example.org").address,
|
||||
vec![addr!(r#""not@me"@example.org"#)]
|
||||
(
|
||||
address,
|
||||
subject.as_ref().map(String::as_str),
|
||||
cc.as_ref().map(String::as_str),
|
||||
bcc.as_ref().map(String::as_str),
|
||||
body.as_ref().map(String::as_str),
|
||||
),
|
||||
(&test_address, None, Some("8cc9@example.com"), None, None)
|
||||
);
|
||||
|
||||
// Email address: "oh\\no"@example.org; corresponding 'mailto' URI:
|
||||
|
||||
// <mailto:%22oh%5C%5Cno%22@example.org>.
|
||||
|
||||
// Email address: "\\\"it's\ ugly\\\""@example.org; corresponding
|
||||
// 'mailto' URI:
|
||||
|
||||
// <mailto:%22%5C%5C%5C%22it's%5C%20ugly%5C%5C%5C%22%22@example.org>.
|
||||
// [tag:FIXME]
|
||||
//assert_eq!(
|
||||
// mlt!("mailto:%22%5C%5C%5C%22it's%5C%20ugly%5C%5C%5C%22%22@example.org").
|
||||
// address, vec![addr!(r#"\"it's ugly\"@example.org"#)]
|
||||
//);
|
||||
|
||||
// When an email address itself includes an "&" (ampersand) character, that
|
||||
// character has to be percent-encoded. For example, the 'mailto' URI
|
||||
// to send mail to "Mike&family@example.org" is
|
||||
// <mailto:Mike%26family@example.org>.
|
||||
let mailto = Mailto::try_from(
|
||||
&b"mailto:info@example.com?bcc=7bcc8@example.com&body=line%20first%0Abut%20not%0Alast"
|
||||
[0..],
|
||||
)
|
||||
.expect("Could not parse mailto link.");
|
||||
let Mailto {
|
||||
ref address,
|
||||
ref subject,
|
||||
ref cc,
|
||||
ref bcc,
|
||||
ref body,
|
||||
} = mailto;
|
||||
assert_eq!(
|
||||
mlt!("mailto:Mike%26family@example.org").address,
|
||||
vec![addr!("Mike&family@example.org")]
|
||||
);
|
||||
|
||||
// Sending a mail with the subject "coffee" in French, i.e., "cafe" where the
|
||||
// final e is an e-acute, using UTF-8 and percent-encoding:
|
||||
// <mailto:user@example.org?subject=caf%C3%A9>
|
||||
assert_eq!(
|
||||
&mlt!("mailto:user@example.org?subject=caf%C3%A9").headers[HDR::SUBJECT],
|
||||
"café"
|
||||
);
|
||||
|
||||
// The same subject, this time using an encoded-word (escaping the "="
|
||||
// and "?" characters used in the encoded-word syntax, because they are
|
||||
// reserved):
|
||||
// [tag:FIXME]
|
||||
// <mailto:user@example.org?subject=%3D%3Futf-8%3FQ%3Fcaf%3DC3%3DA9%3F%3D>
|
||||
assert_eq!(
|
||||
&mlt!("mailto:user@example.org?subject=%3D%3Futf-8%3FQ%3Fcaf%3DC3%3DA9%3F%3D").headers
|
||||
[HDR::SUBJECT],
|
||||
"=?utf-8?Q?caf=C3=A9?="
|
||||
);
|
||||
|
||||
// The same subject, this time encoded as iso-8859-1:
|
||||
|
||||
// <mailto:user@example.org?subject=%3D%3Fiso-8859-1%3FQ%3Fcaf%3DE9%3F%3D>
|
||||
// [tag:FIXME]
|
||||
assert_eq!(
|
||||
&mlt!("mailto:user@example.org?subject=%3D%3Fiso-8859-1%3FQ%3Fcaf%3DE9%3F%3D").headers
|
||||
[HDR::SUBJECT],
|
||||
"=?iso-8859-1?Q?caf=E9?="
|
||||
);
|
||||
|
||||
// Going back to straight UTF-8 and adding a body with the same value:
|
||||
//
|
||||
// <mailto:user@example.org?subject=caf%C3%A9&body=caf%C3%A9>
|
||||
test_case!("mailto:user@example.org?subject=caf%C3%A9&body=caf%C3%A9",
|
||||
addresses => "user@example.org";
|
||||
body => Some("café");
|
||||
("To", "user@example.org"),
|
||||
("Subject", "café")
|
||||
);
|
||||
|
||||
// The following example uses the Japanese word "natto" (Unicode
|
||||
// characters U+7D0D U+8C46) as a domain name label, sending a mail to a
|
||||
// user at "natto".example.org:
|
||||
|
||||
// <mailto:user@%E7%B4%8D%E8%B1%86.example.org?subject=Test&body=NATTO>
|
||||
|
||||
// When constructing the email, the domain name label is converted to
|
||||
// punycode. The resulting message may look as follows:
|
||||
|
||||
// From: sender@example.net
|
||||
// To: user@xn--99zt52a.example.org
|
||||
// Subject: Test
|
||||
// Content-Type: text/plain
|
||||
// Content-Transfer-Encoding: 7bit
|
||||
//
|
||||
// NATTO
|
||||
test_case!("mailto:user@%E7%B4%8D%E8%B1%86.example.org?subject=Test&body=NATTO",
|
||||
addresses => "user@納豆.example.org";
|
||||
body => Some("NATTO");
|
||||
("To", "user@納豆.example.org"),
|
||||
("Subject", "Test")
|
||||
(
|
||||
address,
|
||||
subject.as_ref().map(String::as_str),
|
||||
cc.as_ref().map(String::as_str),
|
||||
bcc.as_ref().map(String::as_str),
|
||||
body.as_ref().map(String::as_str),
|
||||
),
|
||||
(
|
||||
&test_address,
|
||||
None,
|
||||
None,
|
||||
Some("7bcc8@example.com"),
|
||||
Some("line first\nbut not\nlast")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -19,19 +19,16 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Verification of OpenPGP signatures.
|
||||
use crate::{
|
||||
email::{
|
||||
attachment_types::{ContentType, MultipartType},
|
||||
attachments::Attachment,
|
||||
},
|
||||
Error, Result,
|
||||
/*! Verification of OpenPGP signatures */
|
||||
use crate::email::{
|
||||
attachment_types::{ContentType, MultipartType},
|
||||
attachments::Attachment,
|
||||
};
|
||||
use crate::{MeliError, Result};
|
||||
|
||||
/// Convert raw attachment to the form needed for signature verification ([RFC3156](https://tools.ietf.org/html/rfc3156))
|
||||
///
|
||||
/// ## RFC3156
|
||||
/// Convert raw attachment to the form needed for signature verification ([rfc3156](https://tools.ietf.org/html/rfc3156))
|
||||
///
|
||||
/// ## rfc3156
|
||||
/// ```text
|
||||
/// Upon receipt of a signed message, an application MUST:
|
||||
///
|
||||
|
@ -96,10 +93,9 @@ pub fn verify_signature(a: &Attachment) -> Result<(Vec<u8>, &Attachment)> {
|
|||
kind: MultipartType::Signed,
|
||||
ref parts,
|
||||
boundary: _,
|
||||
parameters: _,
|
||||
} => {
|
||||
if parts.len() != 2 {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"Illegal number of parts in multipart/signed. Expected 2 got {}",
|
||||
parts.len()
|
||||
)));
|
||||
|
@ -118,7 +114,7 @@ pub fn verify_signature(a: &Attachment) -> Result<(Vec<u8>, &Attachment)> {
|
|||
{
|
||||
v
|
||||
} else {
|
||||
return Err(Error::new(
|
||||
return Err(MeliError::new(
|
||||
"multipart/signed attachment without a signed part".to_string(),
|
||||
));
|
||||
};
|
||||
|
@ -128,13 +124,13 @@ pub fn verify_signature(a: &Attachment) -> Result<(Vec<u8>, &Attachment)> {
|
|||
}) {
|
||||
sig
|
||||
} else {
|
||||
return Err(Error::new(
|
||||
return Err(MeliError::new(
|
||||
"multipart/signed attachment without a signature part".to_string(),
|
||||
));
|
||||
};
|
||||
Ok((signed_part, signature))
|
||||
}
|
||||
_ => Err(Error::new(
|
||||
_ => Err(MeliError::new(
|
||||
"Should not give non-signed attachments to this function",
|
||||
)),
|
||||
}
|
||||
|
|
|
@ -19,324 +19,29 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! Library error type.
|
||||
/*!
|
||||
* An error object for `melib`
|
||||
*/
|
||||
|
||||
use std::{borrow::Cow, fmt, io, result, str, string, sync::Arc};
|
||||
use std::borrow::Cow;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::result;
|
||||
use std::str;
|
||||
use std::string;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub type Result<T> = result::Result<T, Error>;
|
||||
pub type Result<T> = result::Result<T, MeliError>;
|
||||
|
||||
#[derive(Debug, Copy, PartialEq, Eq, 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 const 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",
|
||||
}
|
||||
}
|
||||
|
||||
/// Error kind means network is certainly down.
|
||||
pub const fn is_network_down(&self) -> bool {
|
||||
use NetworkErrorKind::*;
|
||||
matches!(
|
||||
self,
|
||||
BadGateway
|
||||
| ServiceUnavailable
|
||||
| GatewayTimeout
|
||||
| NetworkAuthenticationRequired
|
||||
| ConnectionFailed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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, Eq, Clone)]
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Copy, PartialEq, Clone)]
|
||||
pub enum ErrorKind {
|
||||
None,
|
||||
External,
|
||||
Authentication,
|
||||
Configuration,
|
||||
/// Protocol error.
|
||||
///
|
||||
/// `EPROTO 71 Protocol error`
|
||||
ProtocolError,
|
||||
/// Protocol is not supported.
|
||||
/// It could be the wrong type or version.
|
||||
ProtocolNotSupported,
|
||||
Bug,
|
||||
Network(NetworkErrorKind),
|
||||
Network,
|
||||
Timeout,
|
||||
OSError,
|
||||
NotImplemented,
|
||||
NotSupported,
|
||||
ValueError,
|
||||
}
|
||||
|
||||
impl fmt::Display for ErrorKind {
|
||||
|
@ -345,70 +50,56 @@ impl fmt::Display for ErrorKind {
|
|||
fmt,
|
||||
"{}",
|
||||
match self {
|
||||
Self::None => "None",
|
||||
Self::External => "External",
|
||||
Self::Authentication => "Authentication",
|
||||
Self::Bug => "Bug, please report this!",
|
||||
Self::Network(ref inner) => inner.as_str(),
|
||||
Self::ProtocolError => "Protocol error",
|
||||
Self::ProtocolNotSupported =>
|
||||
"Protocol is not supported. It could be the wrong type or version.",
|
||||
Self::Timeout => "Timeout",
|
||||
Self::OSError => "OS Error",
|
||||
Self::Configuration => "Configuration",
|
||||
Self::NotImplemented => "Not implemented",
|
||||
Self::NotSupported => "Not supported",
|
||||
Self::ValueError => "Invalid value",
|
||||
ErrorKind::None => "None",
|
||||
ErrorKind::External => "External",
|
||||
ErrorKind::Authentication => "Authentication",
|
||||
ErrorKind::Bug => "Bug, please report this!",
|
||||
ErrorKind::Network => "Network",
|
||||
ErrorKind::Timeout => "Timeout",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! is_variant {
|
||||
($n:ident, $($var:tt)+) => {
|
||||
#[inline]
|
||||
pub fn $n(&self) -> bool {
|
||||
matches!(self, Self::$($var)*)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl ErrorKind {
|
||||
is_variant! { is_authentication, Authentication }
|
||||
is_variant! { is_bug, Bug }
|
||||
is_variant! { is_configuration, Configuration }
|
||||
is_variant! { is_external, External }
|
||||
is_variant! { is_network, Network(_) }
|
||||
is_variant! { is_network_down, Network(ref k) if k.is_network_down() }
|
||||
is_variant! { is_not_implemented, NotImplemented }
|
||||
is_variant! { is_not_supported, NotSupported }
|
||||
is_variant! { is_oserror, OSError }
|
||||
is_variant! { is_protocol_error, ProtocolError }
|
||||
is_variant! { is_protocol_not_supported, ProtocolNotSupported }
|
||||
is_variant! { is_timeout, Timeout }
|
||||
is_variant! { is_value_error, ValueError }
|
||||
pub fn is_network(&self) -> bool {
|
||||
match self {
|
||||
ErrorKind::Network => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_timeout(&self) -> bool {
|
||||
match self {
|
||||
ErrorKind::Timeout => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_authentication(&self) -> bool {
|
||||
match self {
|
||||
ErrorKind::Authentication => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Error {
|
||||
pub summary: Cow<'static, str>,
|
||||
pub details: Option<Cow<'static, str>>,
|
||||
pub source: Option<std::sync::Arc<dyn std::error::Error + Send + Sync + 'static>>,
|
||||
pub struct MeliError {
|
||||
pub summary: Option<Cow<'static, str>>,
|
||||
pub details: Cow<'static, str>,
|
||||
pub source: Option<std::sync::Arc<dyn Error + Send + Sync + 'static>>,
|
||||
pub kind: ErrorKind,
|
||||
}
|
||||
|
||||
pub trait IntoError {
|
||||
fn set_err_summary<M>(self, msg: M) -> Error
|
||||
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) -> Error
|
||||
where
|
||||
M: Into<Cow<'static, str>>;
|
||||
fn set_err_kind(self, kind: ErrorKind) -> Error;
|
||||
fn set_err_kind(self, kind: ErrorKind) -> MeliError;
|
||||
}
|
||||
|
||||
pub trait ResultIntoError<T> {
|
||||
pub trait ResultIntoMeliError<T> {
|
||||
fn chain_err_summary<M, F>(self, msg_fn: F) -> Result<T>
|
||||
where
|
||||
F: Fn() -> M,
|
||||
|
@ -417,33 +108,24 @@ pub trait ResultIntoError<T> {
|
|||
fn chain_err_kind(self, kind: ErrorKind) -> Result<T>;
|
||||
}
|
||||
|
||||
impl<I: Into<Error>> IntoError for I {
|
||||
impl<I: Into<MeliError>> IntoMeliError for I {
|
||||
#[inline]
|
||||
fn set_err_summary<M>(self, msg: M) -> Error
|
||||
fn set_err_summary<M>(self, msg: M) -> MeliError
|
||||
where
|
||||
M: Into<Cow<'static, str>>,
|
||||
{
|
||||
let err: Error = self.into();
|
||||
let err: MeliError = self.into();
|
||||
err.set_summary(msg)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_err_details<M>(self, msg: M) -> Error
|
||||
where
|
||||
M: Into<Cow<'static, str>>,
|
||||
{
|
||||
let err: Error = self.into();
|
||||
err.set_details(msg)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_err_kind(self, kind: ErrorKind) -> Error {
|
||||
let err: Error = self.into();
|
||||
fn set_err_kind(self, kind: ErrorKind) -> MeliError {
|
||||
let err: MeliError = self.into();
|
||||
err.set_kind(kind)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, I: Into<Error>> ResultIntoError<T> for std::result::Result<T, I> {
|
||||
impl<T, I: Into<MeliError>> ResultIntoMeliError<T> for std::result::Result<T, I> {
|
||||
#[inline]
|
||||
fn chain_err_summary<M, F>(self, msg_fn: F) -> Result<T>
|
||||
where
|
||||
|
@ -459,63 +141,51 @@ impl<T, I: Into<Error>> ResultIntoError<T> for std::result::Result<T, I> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new<M>(msg: M) -> Self
|
||||
impl MeliError {
|
||||
pub fn new<M>(msg: M) -> MeliError
|
||||
where
|
||||
M: Into<Cow<'static, str>>,
|
||||
{
|
||||
Self {
|
||||
summary: msg.into(),
|
||||
details: None,
|
||||
MeliError {
|
||||
summary: None,
|
||||
details: msg.into(),
|
||||
source: None,
|
||||
kind: ErrorKind::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_details<M>(mut self, details: M) -> Self
|
||||
pub fn set_summary<M>(mut self, summary: 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());
|
||||
if let Some(old_summary) = self.summary.take() {
|
||||
self.summary = Some(format!("{}. {}", old_summary, summary.into()).into());
|
||||
} else {
|
||||
self.details = Some(details.into());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_summary<M>(mut self, summary: M) -> Self
|
||||
where
|
||||
M: Into<Cow<'static, str>>,
|
||||
{
|
||||
if self.summary.is_empty() {
|
||||
self.summary = summary.into();
|
||||
} else {
|
||||
self.summary = format!("{}. {}", self.summary, summary.into()).into();
|
||||
self.summary = Some(summary.into());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_source(
|
||||
mut self,
|
||||
new_val: Option<std::sync::Arc<dyn std::error::Error + Send + Sync + 'static>>,
|
||||
) -> Self {
|
||||
new_val: Option<std::sync::Arc<dyn Error + Send + Sync + 'static>>,
|
||||
) -> MeliError {
|
||||
self.source = new_val;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_kind(mut self, new_val: ErrorKind) -> Self {
|
||||
pub fn set_kind(mut self, new_val: ErrorKind) -> MeliError {
|
||||
self.kind = new_val;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
impl fmt::Display for MeliError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.summary)?;
|
||||
if let Some(details) = self.details.as_ref() {
|
||||
write!(f, "\n{}", details)?;
|
||||
if let Some(summary) = self.summary.as_ref() {
|
||||
writeln!(f, "Summary: {}", summary)?;
|
||||
}
|
||||
write!(f, "{}", self.details)?;
|
||||
if let Some(source) = self.source.as_ref() {
|
||||
write!(f, "\nCaused by: {}", source)?;
|
||||
}
|
||||
|
@ -526,229 +196,180 @@ impl fmt::Display for Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
self.source
|
||||
.as_ref()
|
||||
.map(|s| &(*(*s)) as &(dyn std::error::Error + 'static))
|
||||
impl Error for MeliError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
self.source.as_ref().map(|s| &(*(*s)) as _)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
impl From<io::Error> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: io::Error) -> Self {
|
||||
Self::new(kind.to_string())
|
||||
.set_details(kind.kind().to_string())
|
||||
fn from(kind: io::Error) -> MeliError {
|
||||
MeliError::new(kind.to_string())
|
||||
.set_summary(format!("{:?}", kind.kind()))
|
||||
.set_source(Some(Arc::new(kind)))
|
||||
.set_kind(ErrorKind::OSError)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Cow<'a, str>> for Error {
|
||||
impl<'a> From<Cow<'a, str>> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: Cow<'_, str>) -> Self {
|
||||
Self::new(kind.to_string())
|
||||
fn from(kind: Cow<'_, str>) -> MeliError {
|
||||
MeliError::new(format!("{:?}", kind))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<string::FromUtf8Error> for Error {
|
||||
impl From<string::FromUtf8Error> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: string::FromUtf8Error) -> Self {
|
||||
Self::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
fn from(kind: string::FromUtf8Error) -> MeliError {
|
||||
MeliError::new(format!("{:?}", kind)).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<str::Utf8Error> for Error {
|
||||
impl From<str::Utf8Error> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: str::Utf8Error) -> Self {
|
||||
Self::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
fn from(kind: str::Utf8Error) -> MeliError {
|
||||
MeliError::new(format!("{:?}", kind)).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
//use std::option;
|
||||
//impl From<option::NoneError> for Error {
|
||||
//impl From<option::NoneError> for MeliError {
|
||||
// #[inline]
|
||||
// fn from(kind: option::NoneError) -> Error {
|
||||
// Error::new(format!("{:?}", kind))
|
||||
// fn from(kind: option::NoneError) -> MeliError {
|
||||
// MeliError::new(format!("{:?}", kind))
|
||||
// }
|
||||
//}
|
||||
|
||||
impl<T> From<std::sync::PoisonError<T>> for Error {
|
||||
impl<T> From<std::sync::PoisonError<T>> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: std::sync::PoisonError<T>) -> Self {
|
||||
Self::new(kind.to_string()).set_kind(ErrorKind::Bug)
|
||||
fn from(kind: std::sync::PoisonError<T>) -> MeliError {
|
||||
MeliError::new(format!("{}", kind))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tls")]
|
||||
impl<T: Sync + Send + 'static + core::fmt::Debug> From<native_tls::HandshakeError<T>> for Error {
|
||||
impl<T: Sync + Send + 'static + core::fmt::Debug> From<native_tls::HandshakeError<T>>
|
||||
for MeliError
|
||||
{
|
||||
#[inline]
|
||||
fn from(kind: native_tls::HandshakeError<T>) -> Self {
|
||||
Self::new(kind.to_string())
|
||||
.set_source(Some(Arc::new(kind)))
|
||||
.set_kind(ErrorKind::Network(NetworkErrorKind::InvalidTLSConnection))
|
||||
fn from(kind: native_tls::HandshakeError<T>) -> MeliError {
|
||||
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tls")]
|
||||
impl From<native_tls::Error> for Error {
|
||||
impl From<native_tls::Error> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: native_tls::Error) -> Self {
|
||||
Self::new(kind.to_string())
|
||||
.set_source(Some(Arc::new(kind)))
|
||||
.set_kind(ErrorKind::Network(NetworkErrorKind::InvalidTLSConnection))
|
||||
fn from(kind: native_tls::Error) -> MeliError {
|
||||
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::num::ParseIntError> for Error {
|
||||
impl From<std::num::ParseIntError> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: std::num::ParseIntError) -> Self {
|
||||
Self::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
impl From<&isahc::error::ErrorKind> for NetworkErrorKind {
|
||||
#[inline]
|
||||
fn from(val: &isahc::error::ErrorKind) -> Self {
|
||||
use isahc::error::ErrorKind::*;
|
||||
match val {
|
||||
BadClientCertificate => Self::BadClientCertificate,
|
||||
BadServerCertificate => Self::BadServerCertificate,
|
||||
ClientInitialization => Self::ClientInitialization,
|
||||
ConnectionFailed => Self::ConnectionFailed,
|
||||
InvalidContentEncoding => Self::InvalidContentEncoding,
|
||||
InvalidCredentials => Self::InvalidCredentials,
|
||||
InvalidRequest => Self::BadRequest,
|
||||
Io => Self::Io,
|
||||
NameResolution => Self::HostLookupFailed,
|
||||
ProtocolViolation => Self::ProtocolViolation,
|
||||
RequestBodyNotRewindable => Self::RequestBodyNotRewindable,
|
||||
Timeout => Self::Timeout,
|
||||
TlsEngine => Self::InvalidTLSConnection,
|
||||
TooManyRedirects => Self::TooManyRedirects,
|
||||
_ => Self::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NetworkErrorKind> for ErrorKind {
|
||||
#[inline]
|
||||
fn from(kind: NetworkErrorKind) -> Self {
|
||||
Self::Network(kind)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
impl From<isahc::Error> for Error {
|
||||
#[inline]
|
||||
fn from(val: isahc::Error) -> Self {
|
||||
let kind: NetworkErrorKind = val.kind().into();
|
||||
Self::new(val.to_string())
|
||||
.set_source(Some(Arc::new(val)))
|
||||
.set_kind(ErrorKind::Network(kind))
|
||||
fn from(kind: std::num::ParseIntError) -> MeliError {
|
||||
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "jmap_backend")]
|
||||
impl From<serde_json::error::Error> for Error {
|
||||
impl From<isahc::Error> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: serde_json::error::Error) -> Self {
|
||||
Self::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
fn from(kind: isahc::Error) -> MeliError {
|
||||
MeliError::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<dyn std::error::Error + Sync + Send + 'static>> for Error {
|
||||
#[cfg(feature = "jmap_backend")]
|
||||
impl From<serde_json::error::Error> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: Box<dyn std::error::Error + Sync + Send + 'static>) -> Self {
|
||||
Self::new(kind.to_string()).set_source(Some(kind.into()))
|
||||
fn from(kind: serde_json::error::Error) -> MeliError {
|
||||
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::ffi::NulError> for Error {
|
||||
impl From<Box<dyn Error + Sync + Send + 'static>> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: std::ffi::NulError) -> Self {
|
||||
Self::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
fn from(kind: Box<dyn Error + Sync + Send + 'static>) -> MeliError {
|
||||
MeliError::new(format!("{}", kind)).set_source(Some(kind.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nix::Error> for Error {
|
||||
impl From<std::ffi::NulError> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: nix::Error) -> Self {
|
||||
Self::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
fn from(kind: std::ffi::NulError) -> MeliError {
|
||||
MeliError::new(format!("{}", kind)).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)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nix::Error> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: nix::Error) -> MeliError {
|
||||
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sqlite3")]
|
||||
impl From<rusqlite::Error> for Error {
|
||||
impl From<rusqlite::Error> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: rusqlite::Error) -> Self {
|
||||
Self::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
fn from(kind: rusqlite::Error) -> MeliError {
|
||||
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<libloading::Error> for Error {
|
||||
impl From<libloading::Error> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: libloading::Error) -> Self {
|
||||
Self::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
fn from(kind: libloading::Error) -> MeliError {
|
||||
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Error {
|
||||
impl From<&str> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: &str) -> Self {
|
||||
Self::new(kind.to_string())
|
||||
fn from(kind: &str) -> MeliError {
|
||||
MeliError::new(kind.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
impl From<String> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: String) -> Self {
|
||||
Self::new(kind)
|
||||
fn from(kind: String) -> MeliError {
|
||||
MeliError::new(kind)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nom::Err<(&[u8], nom::error::ErrorKind)>> for Error {
|
||||
impl From<nom::Err<(&[u8], nom::error::ErrorKind)>> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: nom::Err<(&[u8], nom::error::ErrorKind)>) -> Self {
|
||||
Self::new("Parsing error").set_source(Some(Arc::new(Self::new(kind.to_string()))))
|
||||
fn from(kind: nom::Err<(&[u8], nom::error::ErrorKind)>) -> MeliError {
|
||||
MeliError::new("Parsing error")
|
||||
.set_source(Some(Arc::new(MeliError::new(format!("{}", kind)))))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nom::Err<(&str, nom::error::ErrorKind)>> for Error {
|
||||
impl From<nom::Err<(&str, nom::error::ErrorKind)>> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: nom::Err<(&str, nom::error::ErrorKind)>) -> Self {
|
||||
Self::new("Parsing error").set_details(kind.to_string())
|
||||
fn from(kind: nom::Err<(&str, nom::error::ErrorKind)>) -> MeliError {
|
||||
MeliError::new("Parsing error")
|
||||
.set_source(Some(Arc::new(MeliError::new(format!("{}", kind)))))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::email::InvalidHeaderName> for Error {
|
||||
impl<'a> From<&'a mut MeliError> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: crate::email::InvalidHeaderName) -> Self {
|
||||
Self::new(kind.to_string())
|
||||
.set_source(Some(Arc::new(kind)))
|
||||
.set_kind(ErrorKind::Network(NetworkErrorKind::InvalidTLSConnection))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a mut Self> for Error {
|
||||
#[inline]
|
||||
fn from(kind: &'a mut Self) -> Self {
|
||||
fn from(kind: &'a mut MeliError) -> MeliError {
|
||||
kind.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Self> for Error {
|
||||
impl<'a> From<&'a MeliError> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: &'a Self) -> Self {
|
||||
fn from(kind: &'a MeliError) -> MeliError {
|
||||
kind.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<base64::DecodeError> for Error {
|
||||
#[inline]
|
||||
fn from(kind: base64::DecodeError) -> Self {
|
||||
Self::new("base64 decoding failed")
|
||||
.set_source(Some(Arc::new(kind)))
|
||||
.set_kind(ErrorKind::ValueError)
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -19,9 +19,8 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::io::{self, Read, Seek, Write};
|
||||
|
||||
use super::*;
|
||||
use std::io::{self, Read, Seek, Write};
|
||||
|
||||
#[repr(C)]
|
||||
struct TagData {
|
||||
|
@ -112,7 +111,7 @@ impl Read for Data {
|
|||
if result >= 0 {
|
||||
Ok(result as usize)
|
||||
} else {
|
||||
Err(io::Error::last_os_error())
|
||||
Err(io::Error::last_os_error().into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -127,7 +126,7 @@ impl Write for Data {
|
|||
if result >= 0 {
|
||||
Ok(result as usize)
|
||||
} else {
|
||||
Err(io::Error::last_os_error())
|
||||
Err(io::Error::last_os_error().into())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -150,7 +149,7 @@ impl Seek for Data {
|
|||
if result >= 0 {
|
||||
Ok(result as u64)
|
||||
} else {
|
||||
Err(io::Error::last_os_error())
|
||||
Err(io::Error::last_os_error().into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,34 +19,26 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashMap,
|
||||
ffi::{CStr, CString, OsStr},
|
||||
future::Future,
|
||||
io::Seek,
|
||||
os::unix::{
|
||||
ffi::OsStrExt,
|
||||
io::{AsRawFd, RawFd},
|
||||
},
|
||||
path::Path,
|
||||
sync::{Arc, Mutex},
|
||||
use crate::email::{
|
||||
pgp::{DecryptionMetadata, Recipient},
|
||||
Address,
|
||||
};
|
||||
|
||||
use crate::error::{ErrorKind, IntoMeliError, MeliError, Result, ResultIntoMeliError};
|
||||
use futures::FutureExt;
|
||||
use serde::{
|
||||
de::{self, Deserialize},
|
||||
Deserializer, Serialize, Serializer,
|
||||
};
|
||||
use smol::Async;
|
||||
|
||||
use crate::{
|
||||
email::{
|
||||
pgp::{DecryptionMetadata, Recipient},
|
||||
Address,
|
||||
},
|
||||
error::{Error, ErrorKind, IntoError, Result, ResultIntoError},
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{CStr, CString, OsStr};
|
||||
use std::future::Future;
|
||||
use std::io::Seek;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
macro_rules! call {
|
||||
($lib:expr, $func:ty) => {{
|
||||
|
@ -72,7 +64,7 @@ mod bindings;
|
|||
use bindings::*;
|
||||
mod io;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum GpgmeFlag {
|
||||
///"auto-key-retrieve"
|
||||
AutoKeyRetrieve,
|
||||
|
@ -108,10 +100,11 @@ impl<'de> Deserialize<'de> for LocateKey {
|
|||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
<String>::deserialize(deserializer).map_or_else(
|
||||
|_| Err(de::Error::custom("LocateKey value must be a string.")),
|
||||
|s| Self::from_string_de::<'de, D, String>(s),
|
||||
)
|
||||
if let Ok(s) = <String>::deserialize(deserializer) {
|
||||
LocateKey::from_string_de::<'de, D, String>(s)
|
||||
} else {
|
||||
Err(de::Error::custom("LocateKey value must be a string."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,17 +123,17 @@ impl LocateKey {
|
|||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(match s.as_ref().trim() {
|
||||
s if s.eq_ignore_ascii_case("cert") => Self::CERT,
|
||||
s if s.eq_ignore_ascii_case("pka") => Self::PKA,
|
||||
s if s.eq_ignore_ascii_case("dane") => Self::DANE,
|
||||
s if s.eq_ignore_ascii_case("wkd") => Self::WKD,
|
||||
s if s.eq_ignore_ascii_case("ldap") => Self::LDAP,
|
||||
s if s.eq_ignore_ascii_case("keyserver") => Self::KEYSERVER,
|
||||
s if s.eq_ignore_ascii_case("keyserver-url") => Self::KEYSERVER_URL,
|
||||
s if s.eq_ignore_ascii_case("local") => Self::LOCAL,
|
||||
combination if combination.contains(',') => {
|
||||
let mut ret = Self::NODEFAULT;
|
||||
for c in combination.trim().split(',') {
|
||||
s if s.eq_ignore_ascii_case("cert") => LocateKey::CERT,
|
||||
s if s.eq_ignore_ascii_case("pka") => LocateKey::PKA,
|
||||
s if s.eq_ignore_ascii_case("dane") => LocateKey::DANE,
|
||||
s if s.eq_ignore_ascii_case("wkd") => LocateKey::WKD,
|
||||
s if s.eq_ignore_ascii_case("ldap") => LocateKey::LDAP,
|
||||
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(",") => {
|
||||
let mut ret = LocateKey::NODEFAULT;
|
||||
for c in combination.trim().split(",") {
|
||||
ret |= Self::from_string_de::<'de, D, &str>(c.trim())?;
|
||||
}
|
||||
ret
|
||||
|
@ -156,7 +149,7 @@ impl LocateKey {
|
|||
|
||||
impl std::fmt::Display for LocateKey {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
if *self == Self::NODEFAULT {
|
||||
if *self == LocateKey::NODEFAULT {
|
||||
write!(fmt, "clear,nodefault")
|
||||
} else {
|
||||
let mut accum = String::new();
|
||||
|
@ -164,17 +157,17 @@ impl std::fmt::Display for LocateKey {
|
|||
($flag:expr, $string:literal) => {{
|
||||
if self.intersects($flag) {
|
||||
accum.push_str($string);
|
||||
accum.push(',');
|
||||
accum.push_str(",");
|
||||
}
|
||||
}};
|
||||
}
|
||||
is_set!(Self::CERT, "cert");
|
||||
is_set!(Self::PKA, "pka");
|
||||
is_set!(Self::WKD, "wkd");
|
||||
is_set!(Self::LDAP, "ldap");
|
||||
is_set!(Self::KEYSERVER, "keyserver");
|
||||
is_set!(Self::KEYSERVER_URL, "keyserver-url");
|
||||
is_set!(Self::LOCAL, "local");
|
||||
is_set!(LocateKey::CERT, "cert");
|
||||
is_set!(LocateKey::PKA, "pka");
|
||||
is_set!(LocateKey::WKD, "wkd");
|
||||
is_set!(LocateKey::LDAP, "ldap");
|
||||
is_set!(LocateKey::KEYSERVER, "keyserver");
|
||||
is_set!(LocateKey::KEYSERVER_URL, "keyserver-url");
|
||||
is_set!(LocateKey::LOCAL, "local");
|
||||
accum.pop();
|
||||
write!(fmt, "{}", accum)
|
||||
}
|
||||
|
@ -220,11 +213,13 @@ impl Drop for ContextInner {
|
|||
|
||||
impl Context {
|
||||
pub fn new() -> Result<Self> {
|
||||
let lib =
|
||||
Arc::new(unsafe { libloading::Library::new(libloading::library_filename("gpgme")) }?);
|
||||
if unsafe { call!(&lib, gpgme_check_version)(GPGME_VERSION.as_bytes().as_ptr()) }.is_null()
|
||||
let lib = Arc::new(libloading::Library::new(libloading::library_filename(
|
||||
"gpgme",
|
||||
))?);
|
||||
if unsafe { call!(&lib, gpgme_check_version)(GPGME_VERSION.as_bytes().as_ptr() as *mut _) }
|
||||
.is_null()
|
||||
{
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"Could not use libgpgme: requested version compatible with {} but got {}",
|
||||
GPGME_VERSION,
|
||||
unsafe {
|
||||
|
@ -253,20 +248,21 @@ impl Context {
|
|||
|
||||
let mut io_cbs = gpgme_io_cbs {
|
||||
add: Some(io::gpgme_register_io_cb),
|
||||
add_priv: Arc::into_raw(add_priv_data) as *mut ::std::os::raw::c_void, /* add_priv: *mut ::std::os::raw::c_void, */
|
||||
add_priv: Arc::into_raw(add_priv_data) as *mut ::std::os::raw::c_void, //add_priv: *mut ::std::os::raw::c_void,
|
||||
remove: Some(io::gpgme_remove_io_cb),
|
||||
event: Some(io::gpgme_event_io_cb),
|
||||
event_priv: Arc::into_raw(event_priv_data) as *mut ::std::os::raw::c_void, /* pub event_priv: *mut ::std::os::raw::c_void, */
|
||||
event_priv: Arc::into_raw(event_priv_data) as *mut ::std::os::raw::c_void, //pub event_priv: *mut ::std::os::raw::c_void,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
gpgme_error_try(&lib, call!(&lib, gpgme_new)(&mut ptr))?;
|
||||
call!(&lib, gpgme_set_io_cbs)(ptr, &mut io_cbs);
|
||||
}
|
||||
let ret = Self {
|
||||
let ret = Context {
|
||||
inner: Arc::new(ContextInner {
|
||||
inner: core::ptr::NonNull::new(ptr)
|
||||
.ok_or_else(|| Error::new("Could not use libgpgme").set_kind(ErrorKind::Bug))?,
|
||||
inner: core::ptr::NonNull::new(ptr).ok_or_else(|| {
|
||||
MeliError::new("Could not use libgpgme").set_kind(ErrorKind::Bug)
|
||||
})?,
|
||||
lib,
|
||||
}),
|
||||
io_state,
|
||||
|
@ -365,7 +361,7 @@ impl Context {
|
|||
self.set_flag_inner(
|
||||
auto_key_locate,
|
||||
CStr::from_bytes_with_nul(accum.as_bytes())
|
||||
.map_err(|err| format!("Expected `{}`: {}", accum.as_str(), err))?
|
||||
.expect(accum.as_str())
|
||||
.as_ptr() as *const _,
|
||||
)
|
||||
}
|
||||
|
@ -377,7 +373,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" => {
|
||||
|
@ -425,7 +421,7 @@ impl Context {
|
|||
lib: self.inner.lib.clone(),
|
||||
kind: DataKind::Memory,
|
||||
inner: core::ptr::NonNull::new(ptr).ok_or_else(|| {
|
||||
Error::new("Could not create libgpgme data").set_kind(ErrorKind::Bug)
|
||||
MeliError::new("Could not create libgpgme data").set_kind(ErrorKind::Bug)
|
||||
})?,
|
||||
})
|
||||
}
|
||||
|
@ -433,7 +429,7 @@ impl Context {
|
|||
pub fn new_data_file<P: AsRef<Path>>(&self, r: P) -> Result<Data> {
|
||||
let path: &Path = r.as_ref();
|
||||
if !path.exists() {
|
||||
return Err(Error::new(format!(
|
||||
return Err(MeliError::new(format!(
|
||||
"File `{}` doesn't exist.",
|
||||
path.display()
|
||||
)));
|
||||
|
@ -454,7 +450,7 @@ impl Context {
|
|||
lib: self.inner.lib.clone(),
|
||||
kind: DataKind::Memory,
|
||||
inner: core::ptr::NonNull::new(ptr).ok_or_else(|| {
|
||||
Error::new("Could not create libgpgme data").set_kind(ErrorKind::Bug)
|
||||
MeliError::new("Could not create libgpgme data").set_kind(ErrorKind::Bug)
|
||||
})?,
|
||||
})
|
||||
}
|
||||
|
@ -543,15 +539,15 @@ impl Context {
|
|||
};
|
||||
let _ = rcv.recv().await;
|
||||
{
|
||||
let verify_result: gpgme_verify_result_t =
|
||||
let verify_result =
|
||||
unsafe { call!(&ctx.lib, gpgme_op_verify_result)(ctx.inner.as_ptr()) };
|
||||
if verify_result.is_null() {
|
||||
return Err(Error::new(
|
||||
return Err(MeliError::new(
|
||||
"Unspecified libgpgme error: gpgme_op_verify_result returned NULL.",
|
||||
)
|
||||
.set_err_kind(ErrorKind::External));
|
||||
}
|
||||
unsafe { call!(&ctx.lib, gpgme_free)(verify_result as *mut ::libc::c_void) };
|
||||
drop(verify_result);
|
||||
}
|
||||
let io_state_lck = io_state.lock().unwrap();
|
||||
let ret = io_state_lck
|
||||
|
@ -559,7 +555,7 @@ impl Context {
|
|||
.lock()
|
||||
.unwrap()
|
||||
.take()
|
||||
.unwrap_or_else(|| Err(Error::new("Unspecified libgpgme error")));
|
||||
.unwrap_or_else(|| Err(MeliError::new("Unspecified libgpgme error")));
|
||||
ret
|
||||
})
|
||||
}
|
||||
|
@ -584,7 +580,7 @@ impl Context {
|
|||
.map(|cs| cs.as_ptr())
|
||||
.unwrap_or(std::ptr::null_mut())
|
||||
as *const ::std::os::raw::c_char,
|
||||
secret.into(),
|
||||
if secret { 1 } else { 0 },
|
||||
),
|
||||
)?;
|
||||
}
|
||||
|
@ -661,14 +657,13 @@ impl Context {
|
|||
call!(&ctx.lib, gpgme_op_keylist_end)(ctx.inner.as_ptr()),
|
||||
)?;
|
||||
}
|
||||
io_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
let io_state_lck = io_state.lock().unwrap();
|
||||
io_state_lck
|
||||
.done
|
||||
.lock()
|
||||
.unwrap()
|
||||
.take()
|
||||
.unwrap_or_else(|| Err(Error::new("Unspecified libgpgme error")))?;
|
||||
.unwrap_or_else(|| Err(MeliError::new("Unspecified libgpgme error")))?;
|
||||
let mut keys = vec![];
|
||||
while let Ok(inner) = key_receiver.try_recv() {
|
||||
let key = Key::new(inner, ctx.lib.clone());
|
||||
|
@ -685,7 +680,7 @@ impl Context {
|
|||
) -> Result<impl Future<Output = Result<Vec<u8>>>> {
|
||||
if sign_keys.is_empty() {
|
||||
return Err(
|
||||
Error::new("gpgme: Call to sign() with zero keys.").set_kind(ErrorKind::Bug)
|
||||
MeliError::new("gpgme: Call to sign() with zero keys.").set_kind(ErrorKind::Bug)
|
||||
);
|
||||
}
|
||||
let mut sig: gpgme_data_t = std::ptr::null_mut();
|
||||
|
@ -720,8 +715,9 @@ impl Context {
|
|||
let mut sig = Data {
|
||||
lib: self.inner.lib.clone(),
|
||||
kind: DataKind::Memory,
|
||||
inner: core::ptr::NonNull::new(sig)
|
||||
.ok_or_else(|| Error::new("internal libgpgme error").set_kind(ErrorKind::Bug))?,
|
||||
inner: core::ptr::NonNull::new(sig).ok_or_else(|| {
|
||||
MeliError::new("internal libgpgme error").set_kind(ErrorKind::Bug)
|
||||
})?,
|
||||
};
|
||||
|
||||
let io_state = self.io_state.clone();
|
||||
|
@ -781,23 +777,23 @@ impl Context {
|
|||
}
|
||||
}))
|
||||
.await;
|
||||
{
|
||||
let rcv = io_state.lock().unwrap().receiver.clone();
|
||||
let _ = rcv.recv().await;
|
||||
}
|
||||
io_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
let rcv = {
|
||||
let io_state_lck = io_state.lock().unwrap();
|
||||
io_state_lck.receiver.clone()
|
||||
};
|
||||
let _ = rcv.recv().await;
|
||||
let io_state_lck = io_state.lock().unwrap();
|
||||
io_state_lck
|
||||
.done
|
||||
.lock()
|
||||
.unwrap()
|
||||
.take()
|
||||
.unwrap_or_else(|| Err(Error::new("Unspecified libgpgme error")))?;
|
||||
.unwrap_or_else(|| Err(MeliError::new("Unspecified libgpgme error")))?;
|
||||
sig.seek(std::io::SeekFrom::Start(0))
|
||||
.chain_err_summary(|| {
|
||||
"libgpgme error: could not perform seek on signature data object"
|
||||
})?;
|
||||
sig.into_bytes()
|
||||
Ok(sig.into_bytes()?)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -823,8 +819,9 @@ impl Context {
|
|||
let mut plain = Data {
|
||||
lib: self.inner.lib.clone(),
|
||||
kind: DataKind::Memory,
|
||||
inner: core::ptr::NonNull::new(plain)
|
||||
.ok_or_else(|| Error::new("internal libgpgme error").set_kind(ErrorKind::Bug))?,
|
||||
inner: core::ptr::NonNull::new(plain).ok_or_else(|| {
|
||||
MeliError::new("internal libgpgme error").set_kind(ErrorKind::Bug)
|
||||
})?,
|
||||
};
|
||||
|
||||
let ctx = self.inner.clone();
|
||||
|
@ -886,21 +883,23 @@ impl Context {
|
|||
}
|
||||
}))
|
||||
.await;
|
||||
let rcv = { io_state.lock().unwrap().receiver.clone() };
|
||||
let rcv = {
|
||||
let io_state_lck = io_state.lock().unwrap();
|
||||
io_state_lck.receiver.clone()
|
||||
};
|
||||
let _ = rcv.recv().await;
|
||||
io_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
let io_state_lck = io_state.lock().unwrap();
|
||||
io_state_lck
|
||||
.done
|
||||
.lock()
|
||||
.unwrap()
|
||||
.take()
|
||||
.unwrap_or_else(|| Err(Error::new("Unspecified libgpgme error")))?;
|
||||
.unwrap_or_else(|| Err(MeliError::new("Unspecified libgpgme error")))?;
|
||||
|
||||
let decrypt_result =
|
||||
unsafe { call!(&ctx.lib, gpgme_op_decrypt_result)(ctx.inner.as_ptr()) };
|
||||
if decrypt_result.is_null() {
|
||||
return Err(Error::new(
|
||||
return Err(MeliError::new(
|
||||
"Unspecified libgpgme error: gpgme_op_decrypt_result returned NULL.",
|
||||
)
|
||||
.set_err_kind(ErrorKind::External));
|
||||
|
@ -970,7 +969,7 @@ impl Context {
|
|||
) -> Result<impl Future<Output = Result<Vec<u8>>> + Send> {
|
||||
if encrypt_keys.is_empty() {
|
||||
return Err(
|
||||
Error::new("gpgme: Call to encrypt() with zero keys.").set_kind(ErrorKind::Bug)
|
||||
MeliError::new("gpgme: Call to encrypt() with zero keys.").set_kind(ErrorKind::Bug)
|
||||
);
|
||||
}
|
||||
unsafe {
|
||||
|
@ -1032,8 +1031,9 @@ impl Context {
|
|||
let mut cipher = Data {
|
||||
lib: self.inner.lib.clone(),
|
||||
kind: DataKind::Memory,
|
||||
inner: core::ptr::NonNull::new(cipher)
|
||||
.ok_or_else(|| Error::new("internal libgpgme error").set_kind(ErrorKind::Bug))?,
|
||||
inner: core::ptr::NonNull::new(cipher).ok_or_else(|| {
|
||||
MeliError::new("internal libgpgme error").set_kind(ErrorKind::Bug)
|
||||
})?,
|
||||
};
|
||||
|
||||
let ctx = self.inner.clone();
|
||||
|
@ -1094,21 +1094,23 @@ impl Context {
|
|||
}
|
||||
}))
|
||||
.await;
|
||||
let rcv = { io_state.lock().unwrap().receiver.clone() };
|
||||
let rcv = {
|
||||
let io_state_lck = io_state.lock().unwrap();
|
||||
io_state_lck.receiver.clone()
|
||||
};
|
||||
let _ = rcv.recv().await;
|
||||
io_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
let io_state_lck = io_state.lock().unwrap();
|
||||
io_state_lck
|
||||
.done
|
||||
.lock()
|
||||
.unwrap()
|
||||
.take()
|
||||
.unwrap_or_else(|| Err(Error::new("Unspecified libgpgme error")))?;
|
||||
.unwrap_or_else(|| Err(MeliError::new("Unspecified libgpgme error")))?;
|
||||
|
||||
let encrypt_result =
|
||||
unsafe { call!(&ctx.lib, gpgme_op_encrypt_result)(ctx.inner.as_ptr()) };
|
||||
if encrypt_result.is_null() {
|
||||
return Err(Error::new(
|
||||
return Err(MeliError::new(
|
||||
"Unspecified libgpgme error: gpgme_op_encrypt_result returned NULL.",
|
||||
)
|
||||
.set_err_kind(ErrorKind::External));
|
||||
|
@ -1117,7 +1119,7 @@ impl Context {
|
|||
cipher
|
||||
.seek(std::io::SeekFrom::Start(0))
|
||||
.chain_err_summary(|| "libgpgme error: could not perform seek on plain text")?;
|
||||
cipher.into_bytes()
|
||||
Ok(cipher.into_bytes()?)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1138,7 +1140,7 @@ fn gpgme_error_try(lib: &libloading::Library, error_code: GpgmeError) -> Result<
|
|||
while buf.ends_with(&b"\0"[..]) {
|
||||
buf.pop();
|
||||
}
|
||||
Err(Error::from(
|
||||
Err(MeliError::from(
|
||||
String::from_utf8(buf)
|
||||
.unwrap_or_else(|err| String::from_utf8_lossy(&err.into_bytes()).to_string()),
|
||||
)
|
||||
|
@ -1225,7 +1227,7 @@ impl Clone for Key {
|
|||
unsafe {
|
||||
call!(&self.lib, gpgme_key_ref)(self.inner.inner.as_ptr());
|
||||
}
|
||||
Self {
|
||||
Key {
|
||||
inner: self.inner.clone(),
|
||||
lib,
|
||||
}
|
||||
|
@ -1235,7 +1237,7 @@ impl Clone for Key {
|
|||
impl Key {
|
||||
#[inline(always)]
|
||||
fn new(inner: KeyInner, lib: Arc<libloading::Library>) -> Self {
|
||||
Self { inner, lib }
|
||||
Key { inner, lib }
|
||||
}
|
||||
|
||||
pub fn primary_uid(&self) -> Option<Address> {
|
||||
|
@ -1313,14 +1315,12 @@ impl std::fmt::Debug for Key {
|
|||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Key {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
impl std::cmp::PartialEq for Key {
|
||||
fn eq(&self, other: &Key) -> bool {
|
||||
self.fingerprint() == other.fingerprint()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Key {}
|
||||
|
||||
impl Drop for Key {
|
||||
#[inline]
|
||||
fn drop(&mut self) {
|
||||
|
@ -1348,8 +1348,7 @@ impl Drop for Key {
|
|||
// futures::executor::block_on(ctx.keylist().unwrap()).unwrap()
|
||||
// );
|
||||
// let cipher = ctx.new_data_file("/tmp/msg.asc").unwrap();
|
||||
// let plain =
|
||||
// futures::executor::block_on(ctx.decrypt(cipher).unwrap()).unwrap();
|
||||
// let plain = futures::executor::block_on(ctx.decrypt(cipher).unwrap()).unwrap();
|
||||
// println!(
|
||||
// "buf: {}",
|
||||
// String::from_utf8_lossy(&plain.into_bytes().unwrap())
|
||||
|
|
347
melib/src/lib.rs
347
melib/src/lib.rs
|
@ -19,77 +19,51 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#![deny(
|
||||
/* groups */
|
||||
clippy::correctness,
|
||||
clippy::suspicious,
|
||||
clippy::complexity,
|
||||
clippy::perf,
|
||||
clippy::cargo,
|
||||
clippy::nursery,
|
||||
clippy::style,
|
||||
/* restriction */
|
||||
clippy::dbg_macro,
|
||||
clippy::rc_buffer,
|
||||
clippy::as_underscore,
|
||||
clippy::assertions_on_result_states,
|
||||
/* rustdoc */
|
||||
rustdoc::broken_intra_doc_links,
|
||||
/* pedantic */
|
||||
//clippy::cast_lossless,
|
||||
//clippy::cast_possible_wrap,
|
||||
//clippy::ptr_as_ptr,
|
||||
//clippy::bool_to_int_with_if,
|
||||
clippy::borrow_as_ptr,
|
||||
clippy::case_sensitive_file_extension_comparisons,
|
||||
//clippy::cast_lossless,
|
||||
//clippy::cast_ptr_alignment,
|
||||
)]
|
||||
#![allow(
|
||||
clippy::option_if_let_else,
|
||||
clippy::missing_const_for_fn,
|
||||
clippy::significant_drop_tightening,
|
||||
clippy::multiple_crate_versions,
|
||||
clippy::significant_drop_in_scrutinee,
|
||||
clippy::cognitive_complexity
|
||||
)]
|
||||
|
||||
//! A crate that performs mail client operations such as
|
||||
//! - Hold an [`Envelope`](./email/struct.Envelope.html) with methods convenient
|
||||
//! for mail client use. (see module [`email`](./email/index.html))
|
||||
//! - Abstract through mail storages through the
|
||||
//! [`MailBackend`](./backends/trait.MailBackend.html) trait, and handle
|
||||
//! read/writes/updates through it. (see module
|
||||
//! [`backends`](./backends/index.html))
|
||||
//! - Decode attachments (see module
|
||||
//! [`email::attachments`](./email/attachments/index.html))
|
||||
//! - Hold an [`Envelope`](./email/struct.Envelope.html) with methods convenient for mail client use. (see module [`email`](./email/index.html))
|
||||
//! - Abstract through mail storages through the [`MailBackend`](./backends/trait.MailBackend.html) trait, and handle read/writes/updates through it. (see module [`backends`](./backends/index.html))
|
||||
//! - Decode attachments (see module [`email::attachments`](./email/attachments/index.html))
|
||||
//! - Create new mail (see [`email::Draft`](./email/compose/struct.Draft.html))
|
||||
//! - Send mail with an SMTP client (see module [`smtp`](./smtp/index.html))
|
||||
//! - Manage an `addressbook` i.e. have contacts (see module
|
||||
//! [`addressbook`](./addressbook/index.html))
|
||||
//! - Build thread structures out of a list of mail via their `In-Reply-To` and
|
||||
//! `References` header values (see module [`thread`](./thread/index.html))
|
||||
//! - Manage an `addressbook` i.e. have contacts (see module [`addressbook`](./addressbook/index.html))
|
||||
//! - Build thread structures out of a list of mail via their `In-Reply-To` and `References` header values (see module [`thread`](./thread/index.html))
|
||||
//!
|
||||
//! Other exports are
|
||||
//! - Basic mail account configuration to use with
|
||||
//! [`backends`](./backends/index.html) (see module
|
||||
//! [`conf`](./conf/index.html))
|
||||
//! - A `debug` macro that works like `std::dbg` but for multiple threads. (see
|
||||
//! [`debug` macro](./macro.debug.html))
|
||||
|
||||
//! - Basic mail account configuration to use with [`backends`](./backends/index.html) (see module [`conf`](./conf/index.html))
|
||||
//! - Parser combinators (see module [`parsec`](./parsec/index.html))
|
||||
//! - A `ShellExpandTrait` to expand paths like a shell.
|
||||
//! - A `debug` macro that works like `std::dbg` but for multiple threads. (see [`debug` macro](./macro.debug.html))
|
||||
#[macro_use]
|
||||
pub mod dbg {
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_tag {
|
||||
() => {
|
||||
eprint!(
|
||||
"[{}][{:?}] {}:{}_{}: ",
|
||||
crate::datetime::timestamp_to_string(crate::datetime::now(), Some("%Y-%m-%d %T")),
|
||||
std::thread::current()
|
||||
.name()
|
||||
.map(std::string::ToString::to_string)
|
||||
.unwrap_or_else(|| format!("{:?}", std::thread::current().id())),
|
||||
file!(),
|
||||
line!(),
|
||||
column!()
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(clippy::redundant_closure)]
|
||||
#[macro_export]
|
||||
macro_rules! debug {
|
||||
($val:literal) => {
|
||||
{
|
||||
if cfg!(feature="debug-tracing") {
|
||||
$crate::log::debug!($val);
|
||||
}
|
||||
$val
|
||||
if cfg!(feature="debug-tracing") {
|
||||
log_tag!();
|
||||
eprintln!($val);
|
||||
}
|
||||
$val
|
||||
}
|
||||
};
|
||||
($val:expr) => {
|
||||
if cfg!(feature="debug-tracing") {
|
||||
|
@ -98,7 +72,8 @@ pub mod dbg {
|
|||
// of temporaries - https://stackoverflow.com/a/48732525/1063961
|
||||
match $val {
|
||||
tmp => {
|
||||
$crate::log::debug!("{} = {:?}", stringify, tmp);
|
||||
log_tag!();
|
||||
eprintln!("{} = {:?}", stringify, tmp);
|
||||
tmp
|
||||
}
|
||||
}
|
||||
|
@ -108,7 +83,8 @@ pub mod dbg {
|
|||
};
|
||||
($fmt:literal, $($arg:tt)*) => {
|
||||
if cfg!(feature="debug-tracing") {
|
||||
$crate::log::debug!($fmt, $($arg)*);
|
||||
log_tag!();
|
||||
eprintln!($fmt, $($arg)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -117,39 +93,41 @@ pub mod dbg {
|
|||
#[cfg(feature = "unicode_algorithms")]
|
||||
pub mod text_processing;
|
||||
|
||||
pub use utils::{
|
||||
datetime::UnixTimestamp,
|
||||
logging::{LogLevel, StderrLogger},
|
||||
};
|
||||
pub mod datetime;
|
||||
pub use datetime::UnixTimestamp;
|
||||
|
||||
#[macro_use]
|
||||
mod logging;
|
||||
pub use self::logging::LoggingLevel::*;
|
||||
pub use self::logging::*;
|
||||
|
||||
pub mod addressbook;
|
||||
pub use addressbook::*;
|
||||
pub mod backends;
|
||||
pub use backends::*;
|
||||
mod collection;
|
||||
pub mod sieve;
|
||||
pub use collection::*;
|
||||
pub mod conf;
|
||||
pub use conf::*;
|
||||
pub mod email;
|
||||
pub use email::*;
|
||||
pub mod error;
|
||||
pub use error::*;
|
||||
pub use crate::error::*;
|
||||
pub mod thread;
|
||||
pub use thread::*;
|
||||
pub mod connections;
|
||||
pub mod parsec;
|
||||
pub mod search;
|
||||
|
||||
#[macro_use]
|
||||
pub mod utils;
|
||||
|
||||
#[cfg(feature = "gpgme")]
|
||||
pub mod gpgme;
|
||||
#[cfg(feature = "smtp")]
|
||||
pub mod smtp;
|
||||
#[cfg(feature = "sqlite3")]
|
||||
pub mod sqlite3;
|
||||
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
pub extern crate log;
|
||||
/* parser */
|
||||
extern crate data_encoding;
|
||||
extern crate encoding;
|
||||
|
@ -165,17 +143,16 @@ pub extern crate uuid;
|
|||
pub extern crate xdg_utils;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[repr(transparent)]
|
||||
pub struct BytesDisplay(pub usize);
|
||||
pub struct Bytes(pub usize);
|
||||
|
||||
impl BytesDisplay {
|
||||
impl Bytes {
|
||||
pub const KILOBYTE: f64 = 1024.0;
|
||||
pub const MEGABYTE: f64 = Self::KILOBYTE * 1024.0;
|
||||
pub const GIGABYTE: f64 = Self::MEGABYTE * 1024.0;
|
||||
pub const PETABYTE: f64 = Self::GIGABYTE * 1024.0;
|
||||
}
|
||||
|
||||
impl core::fmt::Display for BytesDisplay {
|
||||
impl core::fmt::Display for Bytes {
|
||||
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
let bytes: f64 = self.0 as f64;
|
||||
if bytes == 0.0 {
|
||||
|
@ -194,4 +171,226 @@ impl core::fmt::Display for BytesDisplay {
|
|||
}
|
||||
}
|
||||
|
||||
pub use utils::shellexpand::ShellExpandTrait;
|
||||
pub use shellexpand::ShellExpandTrait;
|
||||
pub mod shellexpand {
|
||||
|
||||
use smallvec::SmallVec;
|
||||
use std::ffi::OsStr;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub trait ShellExpandTrait {
|
||||
fn expand(&self) -> PathBuf;
|
||||
fn complete(&self, force: bool) -> SmallVec<[String; 128]>;
|
||||
}
|
||||
|
||||
impl ShellExpandTrait for Path {
|
||||
fn expand(&self) -> PathBuf {
|
||||
let mut ret = PathBuf::new();
|
||||
for c in self.components() {
|
||||
let c_to_str = c.as_os_str().to_str();
|
||||
match c_to_str {
|
||||
Some("~") => {
|
||||
if let Ok(home_dir) = std::env::var("HOME") {
|
||||
ret.push(home_dir)
|
||||
} else {
|
||||
return PathBuf::new();
|
||||
}
|
||||
}
|
||||
Some(var) if var.starts_with('$') => {
|
||||
let env_name = var.split_at(1).1;
|
||||
if env_name.chars().all(char::is_uppercase) {
|
||||
ret.push(std::env::var(env_name).unwrap_or_default());
|
||||
} else {
|
||||
ret.push(c);
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
ret.push(c);
|
||||
}
|
||||
None => {
|
||||
/* path is invalid */
|
||||
return PathBuf::new();
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn complete(&self, force: bool) -> SmallVec<[String; 128]> {
|
||||
use libc::dirent64;
|
||||
use nix::fcntl::OFlag;
|
||||
const BUF_SIZE: ::libc::size_t = 8 << 10;
|
||||
|
||||
let (prefix, _match) = if self.as_os_str().as_bytes().ends_with(b"/.") {
|
||||
(self.components().as_path(), OsStr::from_bytes(b"."))
|
||||
} else {
|
||||
if self.exists() && (!force || self.as_os_str().as_bytes().ends_with(b"/")) {
|
||||
// println!("{} {:?}", self.display(), self.components().last());
|
||||
return SmallVec::new();
|
||||
} else {
|
||||
let last_component = self
|
||||
.components()
|
||||
.last()
|
||||
.map(|c| c.as_os_str())
|
||||
.unwrap_or(OsStr::from_bytes(b""));
|
||||
let prefix = if let Some(p) = self.parent() {
|
||||
p
|
||||
} else {
|
||||
return SmallVec::new();
|
||||
};
|
||||
(prefix, last_component)
|
||||
}
|
||||
};
|
||||
|
||||
let dir = match ::nix::dir::Dir::openat(
|
||||
::libc::AT_FDCWD,
|
||||
prefix,
|
||||
OFlag::O_DIRECTORY | OFlag::O_NOATIME | OFlag::O_RDONLY | OFlag::O_CLOEXEC,
|
||||
::nix::sys::stat::Mode::S_IRUSR | ::nix::sys::stat::Mode::S_IXUSR,
|
||||
)
|
||||
.or_else(|_| {
|
||||
::nix::dir::Dir::openat(
|
||||
::libc::AT_FDCWD,
|
||||
prefix,
|
||||
OFlag::O_DIRECTORY | OFlag::O_RDONLY | OFlag::O_CLOEXEC,
|
||||
::nix::sys::stat::Mode::S_IRUSR | ::nix::sys::stat::Mode::S_IXUSR,
|
||||
)
|
||||
}) {
|
||||
Ok(dir) => dir,
|
||||
Err(err) => {
|
||||
debug!(prefix);
|
||||
debug!(err);
|
||||
return SmallVec::new();
|
||||
}
|
||||
};
|
||||
|
||||
let mut buf: Vec<u8> = Vec::with_capacity(BUF_SIZE);
|
||||
let mut entries = SmallVec::new();
|
||||
loop {
|
||||
let n: i64 = unsafe {
|
||||
::libc::syscall(
|
||||
::libc::SYS_getdents64,
|
||||
dir.as_raw_fd(),
|
||||
buf.as_ptr(),
|
||||
BUF_SIZE - 256,
|
||||
)
|
||||
};
|
||||
if n < 0 {
|
||||
return SmallVec::new();
|
||||
} else if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let n = n as usize;
|
||||
unsafe {
|
||||
buf.set_len(n);
|
||||
}
|
||||
let mut pos = 0;
|
||||
while pos < n {
|
||||
let dir = unsafe { std::mem::transmute::<&[u8], &[dirent64]>(&buf[pos..]) };
|
||||
let entry = unsafe { std::ffi::CStr::from_ptr(dir[0].d_name.as_ptr()) };
|
||||
if entry.to_bytes() != b"." && entry.to_bytes() != b".." {
|
||||
if entry.to_bytes().starts_with(_match.as_bytes()) {
|
||||
if dir[0].d_type == ::libc::DT_DIR && !entry.to_bytes().ends_with(b"/")
|
||||
{
|
||||
let mut s = unsafe {
|
||||
String::from_utf8_unchecked(
|
||||
entry.to_bytes()[_match.as_bytes().len()..].to_vec(),
|
||||
)
|
||||
};
|
||||
s.push('/');
|
||||
entries.push(s);
|
||||
} else {
|
||||
entries.push(unsafe {
|
||||
String::from_utf8_unchecked(
|
||||
entry.to_bytes()[_match.as_bytes().len()..].to_vec(),
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
pos += dir[0].d_reclen as usize;
|
||||
}
|
||||
// https://github.com/romkatv/gitstatus/blob/caf44f7aaf33d0f46e6749e50595323c277e0908/src/dir.cc
|
||||
// "It's tempting to bail here if n + sizeof(linux_dirent64) + 512 <= n. After all, there
|
||||
// was enough space for another entry but SYS_getdents64 didn't write it, so this must be
|
||||
// the end of the directory listing, right? Unfortunately, no. SYS_getdents64 is finicky.
|
||||
// It sometimes writes a partial list of entries even if the full list would fit."
|
||||
}
|
||||
entries
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn complete(&self, force: bool) -> SmallVec<[String; 128]> {
|
||||
let mut entries = SmallVec::new();
|
||||
let (prefix, _match) = {
|
||||
if self.exists() && (!force || self.as_os_str().as_bytes().ends_with(b"/")) {
|
||||
// println!("{} {:?}", self.display(), self.components().last());
|
||||
return entries;
|
||||
} else {
|
||||
let last_component = self
|
||||
.components()
|
||||
.last()
|
||||
.map(|c| c.as_os_str())
|
||||
.unwrap_or(OsStr::from_bytes(b""));
|
||||
let prefix = if let Some(p) = self.parent() {
|
||||
p
|
||||
} else {
|
||||
return entries;
|
||||
};
|
||||
(prefix, last_component)
|
||||
}
|
||||
};
|
||||
if force && self.is_dir() && !self.as_os_str().as_bytes().ends_with(b"/") {
|
||||
entries.push("/".to_string());
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
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(),
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
entries
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shellexpandtrait() {
|
||||
assert!(Path::new("~").expand().complete(false).is_empty());
|
||||
assert!(!Path::new("~").expand().complete(true).is_empty());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* melib
|
||||
*
|
||||
* Copyright 2019 Manos Pitsidianakis
|
||||
*
|
||||
* This file is part of meli.
|
||||
*
|
||||
* meli is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* meli is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::shellexpand::ShellExpandTrait;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{BufWriter, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, PartialOrd, Hash, Debug, Serialize, Deserialize)]
|
||||
pub enum LoggingLevel {
|
||||
OFF,
|
||||
FATAL,
|
||||
ERROR,
|
||||
WARN,
|
||||
INFO,
|
||||
DEBUG,
|
||||
TRACE,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for LoggingLevel {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
OFF => "OFF",
|
||||
FATAL => "FATAL",
|
||||
ERROR => "ERROR",
|
||||
WARN => "WARN",
|
||||
INFO => "INFO",
|
||||
DEBUG => "DEBUG",
|
||||
TRACE => "TRACE",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LoggingLevel {
|
||||
fn default() -> Self {
|
||||
LoggingLevel::INFO
|
||||
}
|
||||
}
|
||||
|
||||
use LoggingLevel::*;
|
||||
|
||||
struct LoggingBackend {
|
||||
dest: BufWriter<std::fs::File>,
|
||||
level: LoggingLevel,
|
||||
}
|
||||
|
||||
thread_local!(static LOG: Arc<Mutex<LoggingBackend>> = Arc::new(Mutex::new({
|
||||
let data_dir = xdg::BaseDirectories::with_prefix("meli").unwrap();
|
||||
let log_file = OpenOptions::new().append(true) /* writes will append to a file instead of overwriting previous contents */
|
||||
.create(true) /* a new file will be created if the file does not yet already exist.*/
|
||||
.read(true)
|
||||
.open(data_dir.place_data_file("meli.log").unwrap()).unwrap();
|
||||
LoggingBackend {
|
||||
dest: BufWriter::new(log_file),
|
||||
level: LoggingLevel::default(),
|
||||
}}))
|
||||
);
|
||||
|
||||
pub fn log<S: AsRef<str>>(val: S, level: LoggingLevel) {
|
||||
LOG.with(|f| {
|
||||
let mut b = f.lock().unwrap();
|
||||
if level <= b.level {
|
||||
b.dest
|
||||
.write_all(
|
||||
crate::datetime::timestamp_to_string(crate::datetime::now(), None).as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
b.dest.write_all(b" [").unwrap();
|
||||
b.dest.write_all(level.to_string().as_bytes()).unwrap();
|
||||
b.dest.write_all(b"]: ").unwrap();
|
||||
b.dest.write_all(val.as_ref().as_bytes()).unwrap();
|
||||
b.dest.write_all(b"\n").unwrap();
|
||||
b.dest.flush().unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_log_level() -> LoggingLevel {
|
||||
let mut level = INFO;
|
||||
LOG.with(|f| {
|
||||
level = f.lock().unwrap().level;
|
||||
});
|
||||
level
|
||||
}
|
||||
|
||||
pub fn change_log_dest(path: PathBuf) {
|
||||
LOG.with(|f| {
|
||||
let path = path.expand(); // expand shell stuff
|
||||
let mut backend = f.lock().unwrap();
|
||||
backend.dest = BufWriter::new(OpenOptions::new().append(true) /* writes will append to a file instead of overwriting previous contents */
|
||||
.create(true) /* a new file will be created if the file does not yet already exist.*/
|
||||
.read(true)
|
||||
.open(path).unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
pub fn change_log_level(new_val: LoggingLevel) {
|
||||
LOG.with(|f| {
|
||||
let mut backend = f.lock().unwrap();
|
||||
backend.level = new_val;
|
||||
});
|
||||
}
|
|
@ -0,0 +1,347 @@
|
|||
/*
|
||||
* meli - melib crate.
|
||||
*
|
||||
* Copyright 2019 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/>.
|
||||
*/
|
||||
|
||||
//! Parser combinators.
|
||||
|
||||
pub type Result<'a, Output> = std::result::Result<(&'a str, Output), &'a str>;
|
||||
|
||||
pub trait Parser<'a, Output> {
|
||||
fn parse(&self, input: &'a str) -> Result<'a, Output>;
|
||||
|
||||
fn parse_complete(&self, input: &'a str) -> Result<'a, Output> {
|
||||
match self.parse(input) {
|
||||
r @ Ok(("", _)) => r,
|
||||
r @ Err(_) => r,
|
||||
Ok(_) => Err(input),
|
||||
}
|
||||
}
|
||||
|
||||
fn map<F, NewOutput>(self, map_fn: F) -> BoxedParser<'a, NewOutput>
|
||||
where
|
||||
Self: Sized + 'a,
|
||||
Output: 'a,
|
||||
NewOutput: 'a,
|
||||
F: Fn(Output) -> NewOutput + 'a,
|
||||
{
|
||||
BoxedParser::new(map(self, map_fn))
|
||||
}
|
||||
|
||||
fn and_then<F, NextParser, NewOutput>(self, f: F) -> BoxedParser<'a, NewOutput>
|
||||
where
|
||||
Self: Sized + 'a,
|
||||
Output: 'a,
|
||||
NewOutput: 'a,
|
||||
NextParser: Parser<'a, NewOutput> + 'a,
|
||||
F: Fn(Output) -> NextParser + 'a,
|
||||
{
|
||||
BoxedParser::new(and_then(self, f))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn left<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, R1>
|
||||
where
|
||||
P1: Parser<'a, R1>,
|
||||
P2: Parser<'a, R2>,
|
||||
{
|
||||
map(pair(parser1, parser2), |(left, _right)| left)
|
||||
}
|
||||
|
||||
pub fn right<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, R2>
|
||||
where
|
||||
P1: Parser<'a, R1>,
|
||||
P2: Parser<'a, R2>,
|
||||
{
|
||||
map(pair(parser1, parser2), |(_left, right)| right)
|
||||
}
|
||||
|
||||
impl<'a, F, Output> Parser<'a, Output> for F
|
||||
where
|
||||
F: Fn(&'a str) -> Result<Output>,
|
||||
{
|
||||
fn parse(&self, input: &'a str) -> Result<'a, Output> {
|
||||
self(input)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map<'a, P, F, A, B>(parser: P, map_fn: F) -> impl Parser<'a, B>
|
||||
where
|
||||
P: Parser<'a, A>,
|
||||
F: Fn(A) -> B,
|
||||
{
|
||||
move |input| {
|
||||
parser
|
||||
.parse(input)
|
||||
.map(|(next_input, result)| (next_input, map_fn(result)))
|
||||
}
|
||||
}
|
||||
|
||||
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()..], ())),
|
||||
_ => Err(input),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_literal_anycase<'a>(expected: &'static str) -> impl Parser<'a, ()> {
|
||||
move |input: &'a str| match input.get(0..expected.len()) {
|
||||
Some(next) if next.eq_ignore_ascii_case(expected) => Ok((&input[expected.len()..], ())),
|
||||
_ => Err(input),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn one_or_more<'a, P, A>(parser: P) -> impl Parser<'a, Vec<A>>
|
||||
where
|
||||
P: Parser<'a, A>,
|
||||
{
|
||||
move |mut input| {
|
||||
let mut result = Vec::new();
|
||||
|
||||
if let Ok((next_input, first_item)) = parser.parse(input) {
|
||||
input = next_input;
|
||||
result.push(first_item);
|
||||
} else {
|
||||
return Err(input);
|
||||
}
|
||||
|
||||
while let Ok((next_input, next_item)) = parser.parse(input) {
|
||||
input = next_input;
|
||||
result.push(next_item);
|
||||
}
|
||||
|
||||
Ok((input, result))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn zero_or_more<'a, P, A>(parser: P) -> impl Parser<'a, Vec<A>>
|
||||
where
|
||||
P: Parser<'a, A>,
|
||||
{
|
||||
move |mut input| {
|
||||
let mut result = Vec::new();
|
||||
|
||||
while let Ok((next_input, next_item)) = parser.parse(input) {
|
||||
input = next_input;
|
||||
result.push(next_item);
|
||||
}
|
||||
|
||||
Ok((input, result))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pred<'a, P, A, F>(parser: P, predicate: F) -> impl Parser<'a, A>
|
||||
where
|
||||
P: Parser<'a, A>,
|
||||
F: Fn(&A) -> bool,
|
||||
{
|
||||
move |input| {
|
||||
if let Ok((next_input, value)) = parser.parse(input) {
|
||||
if predicate(&value) {
|
||||
return Ok((next_input, value));
|
||||
}
|
||||
}
|
||||
Err(input)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn whitespace_char<'a>() -> impl Parser<'a, char> {
|
||||
pred(any_char, |c| c.is_whitespace())
|
||||
}
|
||||
|
||||
pub fn quoted_string<'a>() -> impl Parser<'a, String> {
|
||||
map(
|
||||
right(
|
||||
match_literal("\""),
|
||||
left(
|
||||
zero_or_more(pred(any_char, |c| *c != '"')),
|
||||
match_literal("\""),
|
||||
),
|
||||
),
|
||||
|chars| chars.into_iter().collect(),
|
||||
)
|
||||
}
|
||||
|
||||
pub struct BoxedParser<'a, Output> {
|
||||
parser: Box<dyn Parser<'a, Output> + 'a>,
|
||||
}
|
||||
|
||||
impl<'a, Output> BoxedParser<'a, Output> {
|
||||
fn new<P>(parser: P) -> Self
|
||||
where
|
||||
P: Parser<'a, Output> + 'a,
|
||||
{
|
||||
BoxedParser {
|
||||
parser: Box::new(parser),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Output> Parser<'a, Output> for BoxedParser<'a, Output> {
|
||||
fn parse(&self, input: &'a str) -> Result<'a, Output> {
|
||||
self.parser.parse(input)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn either<'a, P1, P2, A>(parser1: P1, parser2: P2) -> impl Parser<'a, A>
|
||||
where
|
||||
P1: Parser<'a, A>,
|
||||
P2: Parser<'a, A>,
|
||||
{
|
||||
move |input| match parser1.parse(input) {
|
||||
ok @ Ok(_) => ok,
|
||||
Err(_) => parser2.parse(input),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn whitespace_wrap<'a, P, A>(parser: P) -> impl Parser<'a, A>
|
||||
where
|
||||
P: Parser<'a, A>,
|
||||
{
|
||||
right(space0(), left(parser, space0()))
|
||||
}
|
||||
|
||||
pub fn pair<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, (R1, R2)>
|
||||
where
|
||||
P1: Parser<'a, R1>,
|
||||
P2: Parser<'a, R2>,
|
||||
{
|
||||
move |input| {
|
||||
parser1.parse(input).and_then(|(next_input, result1)| {
|
||||
parser2
|
||||
.parse(next_input)
|
||||
.map(|(last_input, result2)| (last_input, (result1, result2)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prefix<'a, PN, P, R, RN>(pre: PN, parser: P) -> impl Parser<'a, R>
|
||||
where
|
||||
PN: Parser<'a, RN>,
|
||||
P: Parser<'a, R>,
|
||||
{
|
||||
move |input| {
|
||||
pre.parse(input).and_then(|(last_input, _)| {
|
||||
parser
|
||||
.parse(last_input)
|
||||
.map(|(rest, result)| (rest, result))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn suffix<'a, PN, P, R, RN>(parser: P, suf: PN) -> impl Parser<'a, R>
|
||||
where
|
||||
PN: Parser<'a, RN>,
|
||||
P: Parser<'a, R>,
|
||||
{
|
||||
move |input| {
|
||||
parser
|
||||
.parse(input)
|
||||
.and_then(|(last_input, result)| suf.parse(last_input).map(|(rest, _)| (rest, result)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delimited<'a, PN, RN, P, R>(lparser: PN, mid: P, rparser: PN) -> impl Parser<'a, R>
|
||||
where
|
||||
PN: Parser<'a, RN>,
|
||||
P: Parser<'a, R>,
|
||||
{
|
||||
move |input| {
|
||||
lparser.parse(input).and_then(|(next_input, _)| {
|
||||
mid.parse(next_input).and_then(|(last_input, result)| {
|
||||
rparser.parse(last_input).map(|(rest, _)| (rest, result))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn any_char(input: &str) -> Result<char> {
|
||||
match input.chars().next() {
|
||||
Some(next) => Ok((&input[next.len_utf8()..], next)),
|
||||
_ => Err(input),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn string<'a>() -> impl Parser<'a, String> {
|
||||
one_or_more(pred(any_char, |c| c.is_alphanumeric())).map(|r| r.into_iter().collect())
|
||||
}
|
||||
|
||||
pub fn space1<'a>() -> impl Parser<'a, Vec<char>> {
|
||||
one_or_more(whitespace_char())
|
||||
}
|
||||
|
||||
pub fn space0<'a>() -> impl Parser<'a, Vec<char>> {
|
||||
zero_or_more(whitespace_char())
|
||||
}
|
||||
|
||||
pub fn and_then<'a, P, F, A, B, NextP>(parser: P, f: F) -> impl Parser<'a, B>
|
||||
where
|
||||
P: Parser<'a, A>,
|
||||
NextP: Parser<'a, B>,
|
||||
F: Fn(A) -> NextP,
|
||||
{
|
||||
move |input| match parser.parse(input) {
|
||||
Ok((next_input, result)) => f(result).parse(next_input),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn opt<'a, P, A>(opt_parser: P) -> impl Parser<'a, Option<A>>
|
||||
where
|
||||
P: Parser<'a, A>,
|
||||
{
|
||||
move |input| match opt_parser.parse(input) {
|
||||
Ok((next_input, result)) => Ok((next_input, Some(result))),
|
||||
Err(_) => Ok((input, None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn peek<'a, P, A>(parser: P) -> impl Parser<'a, A>
|
||||
where
|
||||
P: Parser<'a, A>,
|
||||
{
|
||||
move |input| match parser.parse(input) {
|
||||
Ok((_, result)) => Ok((input, result)),
|
||||
e @ Err(_) => e,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_until<'a, A, P>(end: P) -> impl Parser<'a, &'a str>
|
||||
where
|
||||
P: Parser<'a, A>,
|
||||
{
|
||||
move |input: &'a str| {
|
||||
let mut offset = 0;
|
||||
while !input[offset..].is_empty() {
|
||||
if let Ok((rest, _)) = end.parse(&input[offset..]) {
|
||||
return Ok((
|
||||
rest,
|
||||
&input[..(offset + input[offset..].len() - rest.len())],
|
||||
));
|
||||
}
|
||||
while offset != input.len() {
|
||||
offset += 1;
|
||||
if input.is_char_boundary(offset) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok((&input[offset..], input))
|
||||
}
|
||||
}
|
|
@ -19,16 +19,14 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::{borrow::Cow, convert::TryFrom};
|
||||
use crate::parsec::*;
|
||||
use crate::UnixTimestamp;
|
||||
use std::borrow::Cow;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
pub use query_parser::query;
|
||||
use Query::*;
|
||||
|
||||
use crate::utils::{
|
||||
datetime::{formats, UnixTimestamp},
|
||||
parsec::*,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize)]
|
||||
pub enum Query {
|
||||
Before(UnixTimestamp),
|
||||
|
@ -53,18 +51,6 @@ pub enum Query {
|
|||
And(Box<Query>, Box<Query>),
|
||||
Or(Box<Query>, Box<Query>),
|
||||
Not(Box<Query>),
|
||||
/// By us.
|
||||
Answered,
|
||||
/// By an address/name.
|
||||
AnsweredBy {
|
||||
by: String,
|
||||
},
|
||||
Larger {
|
||||
than: usize,
|
||||
},
|
||||
Smaller {
|
||||
than: usize,
|
||||
},
|
||||
}
|
||||
|
||||
pub trait QueryTrait {
|
||||
|
@ -100,45 +86,14 @@ impl QueryTrait for crate::Envelope {
|
|||
And(q_a, q_b) => self.is_match(q_a) && self.is_match(q_b),
|
||||
Or(q_a, q_b) => self.is_match(q_a) || self.is_match(q_b),
|
||||
Not(q) => !self.is_match(q),
|
||||
InReplyTo(_) => {
|
||||
log::warn!("Filtering with InReplyTo is unimplemented.");
|
||||
false
|
||||
}
|
||||
References(_) => {
|
||||
log::warn!("Filtering with References is unimplemented.");
|
||||
false
|
||||
}
|
||||
AllText(_) => {
|
||||
log::warn!("Filtering with AllText is unimplemented.");
|
||||
false
|
||||
}
|
||||
Body(_) => {
|
||||
log::warn!("Filtering with Body is unimplemented.");
|
||||
false
|
||||
}
|
||||
Answered => {
|
||||
log::warn!("Filtering with Answered is unimplemented.");
|
||||
false
|
||||
}
|
||||
AnsweredBy { .. } => {
|
||||
log::warn!("Filtering with AnsweredBy is unimplemented.");
|
||||
false
|
||||
}
|
||||
Larger { .. } => {
|
||||
log::warn!("Filtering with Larger is unimplemented.");
|
||||
false
|
||||
}
|
||||
Smaller { .. } => {
|
||||
log::warn!("Filtering with Smaller is unimplemented.");
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Query {
|
||||
type Error = crate::error::Error;
|
||||
fn try_from(t: &str) -> crate::error::Result<Self> {
|
||||
type Error = crate::error::MeliError;
|
||||
fn try_from(t: &str) -> crate::error::Result<Query> {
|
||||
query()
|
||||
.parse_complete(t)
|
||||
.map(|(_, q)| q)
|
||||
|
@ -149,88 +104,6 @@ impl TryFrom<&str> for Query {
|
|||
pub mod query_parser {
|
||||
use super::*;
|
||||
|
||||
fn date<'a>() -> impl Parser<'a, UnixTimestamp> {
|
||||
move |input| {
|
||||
literal().parse(input).and_then(|(next_input, result)| {
|
||||
if let Ok((_, t)) = crate::utils::datetime::parse_timestamp_from_string(
|
||||
result,
|
||||
formats::RFC3339_DATE,
|
||||
) {
|
||||
Ok((next_input, t))
|
||||
} else {
|
||||
Err(next_input)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn before<'a>() -> impl Parser<'a, Query> {
|
||||
prefix(
|
||||
whitespace_wrap(match_literal("before:")),
|
||||
whitespace_wrap(date()),
|
||||
)
|
||||
.map(Query::Before)
|
||||
}
|
||||
|
||||
fn after<'a>() -> impl Parser<'a, Query> {
|
||||
prefix(
|
||||
whitespace_wrap(match_literal("after:")),
|
||||
whitespace_wrap(date()),
|
||||
)
|
||||
.map(Query::After)
|
||||
}
|
||||
|
||||
fn between<'a>() -> impl Parser<'a, Query> {
|
||||
prefix(
|
||||
whitespace_wrap(match_literal("between:")),
|
||||
pair(
|
||||
suffix(whitespace_wrap(date()), whitespace_wrap(match_literal(","))),
|
||||
whitespace_wrap(date()),
|
||||
),
|
||||
)
|
||||
.map(|(t1, t2)| Query::Between(t1, t2))
|
||||
}
|
||||
|
||||
fn on<'a>() -> impl Parser<'a, Query> {
|
||||
prefix(
|
||||
whitespace_wrap(match_literal("on:")),
|
||||
whitespace_wrap(date()),
|
||||
)
|
||||
.map(Query::After)
|
||||
}
|
||||
|
||||
fn smaller<'a>() -> impl Parser<'a, Query> {
|
||||
prefix(
|
||||
whitespace_wrap(match_literal("smaller:")),
|
||||
whitespace_wrap(integer()),
|
||||
)
|
||||
.map(|than| Query::Smaller { than })
|
||||
}
|
||||
|
||||
fn larger<'a>() -> impl Parser<'a, Query> {
|
||||
prefix(
|
||||
whitespace_wrap(match_literal("larger:")),
|
||||
whitespace_wrap(integer()),
|
||||
)
|
||||
.map(|than| Query::Larger { than })
|
||||
}
|
||||
|
||||
fn answered_by<'a>() -> impl Parser<'a, Query> {
|
||||
prefix(
|
||||
whitespace_wrap(match_literal("answered-by:")),
|
||||
whitespace_wrap(literal()),
|
||||
)
|
||||
.map(|by| Query::AnsweredBy { by })
|
||||
}
|
||||
|
||||
fn answered<'a>() -> impl Parser<'a, Query> {
|
||||
move |input| {
|
||||
whitespace_wrap(match_literal_anycase("answered"))
|
||||
.map(|()| Query::Answered)
|
||||
.parse(input)
|
||||
}
|
||||
}
|
||||
|
||||
fn subject<'a>() -> impl Parser<'a, Query> {
|
||||
prefix(
|
||||
whitespace_wrap(match_literal("subject:")),
|
||||
|
@ -271,14 +144,6 @@ pub mod query_parser {
|
|||
.map(Query::Bcc)
|
||||
}
|
||||
|
||||
fn all_addresses<'a>() -> impl Parser<'a, Query> {
|
||||
prefix(
|
||||
whitespace_wrap(match_literal("all-addresses:")),
|
||||
whitespace_wrap(literal()),
|
||||
)
|
||||
.map(Query::AllAddresses)
|
||||
}
|
||||
|
||||
fn or<'a>() -> impl Parser<'a, Query> {
|
||||
move |input| {
|
||||
whitespace_wrap(match_literal_anycase("or"))
|
||||
|
@ -368,10 +233,9 @@ pub mod query_parser {
|
|||
///
|
||||
/// # Invocation
|
||||
/// ```
|
||||
/// use melib::{
|
||||
/// search::{query, Query},
|
||||
/// utils::parsec::Parser,
|
||||
/// };
|
||||
/// use melib::search::query;
|
||||
/// use melib::search::Query;
|
||||
/// use melib::parsec::Parser;
|
||||
///
|
||||
/// let input = "test";
|
||||
/// let query = query().parse(input);
|
||||
|
@ -385,17 +249,8 @@ pub mod query_parser {
|
|||
.or_else(|_| to().parse(input))
|
||||
.or_else(|_| cc().parse(input))
|
||||
.or_else(|_| bcc().parse(input))
|
||||
.or_else(|_| all_addresses().parse(input))
|
||||
.or_else(|_| subject().parse(input))
|
||||
.or_else(|_| before().parse(input))
|
||||
.or_else(|_| after().parse(input))
|
||||
.or_else(|_| on().parse(input))
|
||||
.or_else(|_| between().parse(input))
|
||||
.or_else(|_| flags().parse(input))
|
||||
.or_else(|_| answered().parse(input))
|
||||
.or_else(|_| answered_by().parse(input))
|
||||
.or_else(|_| larger().parse(input))
|
||||
.or_else(|_| smaller().parse(input))
|
||||
.or_else(|_| has_attachment().parse(input))
|
||||
{
|
||||
Ok(q)
|
||||
|
@ -427,12 +282,105 @@ pub mod query_parser {
|
|||
} else if let Ok((rest, query_b)) = or().parse(rest) {
|
||||
Ok((rest, Or(Box::new(query_a), Box::new(query_b))))
|
||||
} else if let Ok((rest, query_b)) = query().parse(rest) {
|
||||
Ok((rest, And(Box::new(query_a), Box::new(query_b))))
|
||||
Ok((rest, Or(Box::new(query_a), Box::new(query_b))))
|
||||
} else {
|
||||
Ok((rest, query_a))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_parsing() {
|
||||
assert_eq!(
|
||||
Err("subject: test and"),
|
||||
query().parse_complete("subject: test and")
|
||||
);
|
||||
assert_eq!(
|
||||
Ok((
|
||||
"",
|
||||
And(
|
||||
Box::new(Subject("test".to_string())),
|
||||
Box::new(AllText("i".to_string()))
|
||||
)
|
||||
)),
|
||||
query().parse_complete("subject: test and i")
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(("", AllText("test".to_string()))),
|
||||
query().parse_complete("test")
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(("", Subject("test".to_string()))),
|
||||
query().parse_complete("subject: test")
|
||||
);
|
||||
assert_eq!(
|
||||
Ok((
|
||||
"",
|
||||
Or(
|
||||
Box::new(Subject("wah ah ah".to_string())),
|
||||
Box::new(And(
|
||||
Box::new(From("Manos".to_string())),
|
||||
Box::new(From("Sia".to_string()))
|
||||
))
|
||||
)
|
||||
)),
|
||||
query().parse_complete("subject: \"wah ah ah\" or (from: Manos and from: Sia)")
|
||||
);
|
||||
assert_eq!(
|
||||
Ok((
|
||||
"",
|
||||
Or(
|
||||
Box::new(Subject("wah".to_string())),
|
||||
Box::new(And(
|
||||
Box::new(From("Manos".to_string())),
|
||||
Box::new(Or(
|
||||
Box::new(Subject("foo".to_string())),
|
||||
Box::new(Subject("bar".to_string())),
|
||||
))
|
||||
))
|
||||
)
|
||||
)),
|
||||
query()
|
||||
.parse_complete("subject: wah or (from: Manos and (subject:foo or subject: bar))")
|
||||
);
|
||||
assert_eq!(
|
||||
Ok((
|
||||
"",
|
||||
And(
|
||||
Box::new(From("Manos".to_string())),
|
||||
Box::new(And(
|
||||
Box::new(Or(
|
||||
Box::new(Subject("foo".to_string())),
|
||||
Box::new(Subject("bar".to_string()))
|
||||
)),
|
||||
Box::new(Or(
|
||||
Box::new(From("woo".to_string())),
|
||||
Box::new(From("my".to_string()))
|
||||
))
|
||||
))
|
||||
)
|
||||
)),
|
||||
query().parse_complete(
|
||||
"(from: Manos and (subject:foo or subject: bar) and (from:woo or from:my))"
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(("", Flags(vec!["test".to_string(), "testtest".to_string()]))),
|
||||
query().parse_complete("flags:test,testtest")
|
||||
);
|
||||
assert_eq!(
|
||||
query().parse_complete("flags:test,testtest"),
|
||||
query().parse_complete("tags:test,testtest")
|
||||
);
|
||||
assert_eq!(
|
||||
query().parse_complete("flags:seen"),
|
||||
query().parse_complete("tags:seen")
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(("", Flags(vec!["f".to_string()]))),
|
||||
query().parse_complete("tags:f")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
|
@ -458,114 +406,3 @@ impl<'de> Deserialize<'de> for Query {
|
|||
ret
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_query_parsing() {
|
||||
assert_eq!(
|
||||
Err("subject:test and"),
|
||||
query().parse_complete("subject:test and")
|
||||
);
|
||||
assert_eq!(
|
||||
Ok((
|
||||
"",
|
||||
And(
|
||||
Box::new(Subject("test".to_string())),
|
||||
Box::new(AllText("i".to_string()))
|
||||
)
|
||||
)),
|
||||
query().parse_complete("subject:test and i")
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(("", AllText("test".to_string()))),
|
||||
query().parse_complete("test")
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(("", Subject("test".to_string()))),
|
||||
query().parse_complete("subject:test")
|
||||
);
|
||||
assert_eq!(
|
||||
Ok((
|
||||
"",
|
||||
And(
|
||||
Box::new(From("Manos".to_string())),
|
||||
Box::new(From("Sia".to_string()))
|
||||
)
|
||||
)),
|
||||
query().parse_complete("from:Manos and from:Sia")
|
||||
);
|
||||
assert_eq!(
|
||||
Ok((
|
||||
"",
|
||||
Or(
|
||||
Box::new(Subject("wah ah ah".to_string())),
|
||||
Box::new(And(
|
||||
Box::new(From("Manos".to_string())),
|
||||
Box::new(From("Sia".to_string()))
|
||||
))
|
||||
)
|
||||
)),
|
||||
query().parse_complete("subject:\"wah ah ah\" or (from:Manos and from:Sia)")
|
||||
);
|
||||
assert_eq!(
|
||||
Ok((
|
||||
"",
|
||||
Or(
|
||||
Box::new(Subject("wah".to_string())),
|
||||
Box::new(And(
|
||||
Box::new(From("Manos".to_string())),
|
||||
Box::new(Or(
|
||||
Box::new(Subject("foo".to_string())),
|
||||
Box::new(Subject("bar".to_string())),
|
||||
))
|
||||
))
|
||||
)
|
||||
)),
|
||||
query().parse_complete("subject:wah or (from:Manos and (subject:foo or subject:bar))")
|
||||
);
|
||||
assert_eq!(
|
||||
Ok((
|
||||
"",
|
||||
And(
|
||||
Box::new(From("Manos".to_string())),
|
||||
Box::new(And(
|
||||
Box::new(Or(
|
||||
Box::new(Subject("foo".to_string())),
|
||||
Box::new(Subject("bar".to_string()))
|
||||
)),
|
||||
Box::new(Or(
|
||||
Box::new(From("woo".to_string())),
|
||||
Box::new(From("my".to_string()))
|
||||
))
|
||||
))
|
||||
)
|
||||
)),
|
||||
query().parse_complete(
|
||||
"(from:Manos and (subject:foo or subject:bar) and (from:woo or from:my))"
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(("", Flags(vec!["test".to_string(), "testtest".to_string()]))),
|
||||
query().parse_complete("flags:test,testtest")
|
||||
);
|
||||
assert_eq!(
|
||||
query().parse_complete("flags:test,testtest"),
|
||||
query().parse_complete("tags:test,testtest")
|
||||
);
|
||||
assert_eq!(
|
||||
query().parse_complete("flags:seen"),
|
||||
query().parse_complete("tags:seen")
|
||||
);
|
||||
assert_eq!(
|
||||
query().parse_complete("is:unseen"),
|
||||
query().parse_complete("tags:unseen")
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(("", Flags(vec!["f".to_string()]))),
|
||||
query().parse_complete("tags:f")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
1011
melib/src/sieve.rs
1011
melib/src/sieve.rs
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -19,12 +19,10 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{error::*, logging::log, Envelope};
|
||||
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput};
|
||||
pub use rusqlite::{self, params, Connection};
|
||||
|
||||
use crate::{error::*, log, Envelope};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct DatabaseDescription {
|
||||
|
@ -35,17 +33,17 @@ pub struct DatabaseDescription {
|
|||
|
||||
pub fn db_path(name: &str) -> Result<PathBuf> {
|
||||
let data_dir =
|
||||
xdg::BaseDirectories::with_prefix("meli").map_err(|e| Error::new(e.to_string()))?;
|
||||
data_dir
|
||||
xdg::BaseDirectories::with_prefix("meli").map_err(|e| MeliError::new(e.to_string()))?;
|
||||
Ok(data_dir
|
||||
.place_data_file(name)
|
||||
.map_err(|err| Error::new(err.to_string()))
|
||||
.map_err(|e| MeliError::new(e.to_string()))?)
|
||||
}
|
||||
|
||||
pub fn open_db(db_path: PathBuf) -> Result<Connection> {
|
||||
if !db_path.exists() {
|
||||
return Err(Error::new("Database doesn't exist"));
|
||||
return Err(MeliError::new("Database doesn't exist"));
|
||||
}
|
||||
Connection::open(&db_path).map_err(|e| Error::new(e.to_string()))
|
||||
Connection::open(&db_path).map_err(|e| MeliError::new(e.to_string()))
|
||||
}
|
||||
|
||||
pub fn open_or_create_db(
|
||||
|
@ -54,19 +52,24 @@ pub fn open_or_create_db(
|
|||
) -> Result<Connection> {
|
||||
let mut second_try: bool = false;
|
||||
loop {
|
||||
let db_path = identifier.map_or_else(
|
||||
|| db_path(description.name),
|
||||
|id| db_path(&format!("{}_{}", id, description.name)),
|
||||
)?;
|
||||
let set_mode = !db_path.exists();
|
||||
if set_mode {
|
||||
log::info!(
|
||||
"Creating {} database in {}",
|
||||
description.name,
|
||||
db_path.display()
|
||||
let db_path = if let Some(id) = identifier {
|
||||
db_path(&format!("{}_{}", id, description.name))
|
||||
} else {
|
||||
db_path(description.name)
|
||||
}?;
|
||||
let mut set_mode = false;
|
||||
if !db_path.exists() {
|
||||
log(
|
||||
format!(
|
||||
"Creating {} database in {}",
|
||||
description.name,
|
||||
db_path.display()
|
||||
),
|
||||
crate::INFO,
|
||||
);
|
||||
set_mode = true;
|
||||
}
|
||||
let conn = Connection::open(&db_path).map_err(|e| Error::new(e.to_string()))?;
|
||||
let conn = Connection::open(&db_path).map_err(|e| MeliError::new(e.to_string()))?;
|
||||
if set_mode {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let file = std::fs::File::open(&db_path)?;
|
||||
|
@ -78,17 +81,17 @@ pub fn open_or_create_db(
|
|||
}
|
||||
let version: i32 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
|
||||
if version != 0_i32 && version as u32 != description.version {
|
||||
log::info!(
|
||||
"Database version mismatch, is {} but expected {}. Attempting to recreate \
|
||||
database.",
|
||||
version,
|
||||
description.version
|
||||
log(
|
||||
format!(
|
||||
"Database version mismatch, is {} but expected {}",
|
||||
version, description.version
|
||||
),
|
||||
crate::INFO,
|
||||
);
|
||||
if second_try {
|
||||
return Err(Error::new(format!(
|
||||
"Database version mismatch, is {} but expected {}. Could not recreate \
|
||||
database.",
|
||||
version, description.version
|
||||
return Err(MeliError::new(format!(
|
||||
"Database version mismatch, is {} but expected {}. Could not recreate database.",
|
||||
version, description.version
|
||||
)));
|
||||
}
|
||||
reset_db(description, identifier)?;
|
||||
|
@ -97,11 +100,11 @@ pub fn open_or_create_db(
|
|||
}
|
||||
|
||||
if version == 0 {
|
||||
conn.pragma_update(None, "user_version", description.version)?;
|
||||
conn.pragma_update(None, "user_version", &description.version)?;
|
||||
}
|
||||
if let Some(s) = description.init_script {
|
||||
conn.execute_batch(s)
|
||||
.map_err(|e| Error::new(e.to_string()))?;
|
||||
.map_err(|e| MeliError::new(e.to_string()))?;
|
||||
}
|
||||
|
||||
return Ok(conn);
|
||||
|
@ -110,17 +113,21 @@ pub fn open_or_create_db(
|
|||
|
||||
/// Return database to a clean slate.
|
||||
pub fn reset_db(description: &DatabaseDescription, identifier: Option<&str>) -> Result<()> {
|
||||
let db_path = identifier.map_or_else(
|
||||
|| db_path(description.name),
|
||||
|id| db_path(&format!("{}_{}", id, description.name)),
|
||||
)?;
|
||||
let db_path = if let Some(id) = identifier {
|
||||
db_path(&format!("{}_{}", id, description.name))
|
||||
} else {
|
||||
db_path(description.name)
|
||||
}?;
|
||||
if !db_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
log::info!(
|
||||
"Resetting {} database in {}",
|
||||
description.name,
|
||||
db_path.display()
|
||||
log(
|
||||
format!(
|
||||
"Resetting {} database in {}",
|
||||
description.name,
|
||||
db_path.display()
|
||||
),
|
||||
crate::INFO,
|
||||
);
|
||||
std::fs::remove_file(&db_path)?;
|
||||
Ok(())
|
||||
|
@ -128,17 +135,27 @@ pub fn reset_db(description: &DatabaseDescription, identifier: Option<&str>) ->
|
|||
|
||||
impl ToSql for Envelope {
|
||||
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
|
||||
let v: Vec<u8> = serde_json::to_vec(self).map_err(|e| {
|
||||
rusqlite::Error::ToSqlConversionFailure(Box::new(Error::new(e.to_string())))
|
||||
})?;
|
||||
let v: Vec<u8> = bincode::Options::serialize(bincode::config::DefaultOptions::new(), self)
|
||||
.map_err(|e| {
|
||||
rusqlite::Error::ToSqlConversionFailure(Box::new(MeliError::new(e.to_string())))
|
||||
})?;
|
||||
Ok(ToSqlOutput::from(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql for Envelope {
|
||||
fn column_result(value: rusqlite::types::ValueRef) -> FromSqlResult<Self> {
|
||||
use std::convert::TryFrom;
|
||||
|
||||
let b: Vec<u8> = FromSql::column_result(value)?;
|
||||
|
||||
serde_json::from_slice(&b).map_err(|e| FromSqlError::Other(Box::new(e)))
|
||||
Ok(bincode::Options::deserialize(
|
||||
bincode::Options::with_limit(
|
||||
bincode::config::DefaultOptions::new(),
|
||||
2 * u64::try_from(b.len()).map_err(|e| FromSqlError::Other(Box::new(e)))?,
|
||||
),
|
||||
&b,
|
||||
)
|
||||
.map_err(|e| FromSqlError::Other(Box::new(e)))?)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -20,26 +20,30 @@
|
|||
*/
|
||||
|
||||
extern crate unicode_segmentation;
|
||||
use core::{cmp::Ordering, iter::Peekable, str::FromStr};
|
||||
use self::unicode_segmentation::UnicodeSegmentation;
|
||||
use super::grapheme_clusters::TextProcessing;
|
||||
use super::tables::LINE_BREAK_RULES;
|
||||
use super::types::LineBreakClass;
|
||||
use super::types::Reflow;
|
||||
use core::cmp::Ordering;
|
||||
use core::iter::Peekable;
|
||||
use core::str::FromStr;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use LineBreakClass::*;
|
||||
|
||||
use self::unicode_segmentation::UnicodeSegmentation;
|
||||
use super::{
|
||||
grapheme_clusters::TextProcessing,
|
||||
tables::LINE_BREAK_RULES,
|
||||
types::{LineBreakClass, Reflow},
|
||||
};
|
||||
|
||||
#[derive(Default, Debug, Eq, PartialEq, Copy, Clone)]
|
||||
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||
pub enum LineBreakCandidate {
|
||||
MandatoryBreak,
|
||||
BreakAllowed,
|
||||
#[default]
|
||||
NoBreak, // Not used.
|
||||
}
|
||||
|
||||
impl Default for LineBreakCandidate {
|
||||
fn default() -> Self {
|
||||
LineBreakCandidate::NoBreak
|
||||
}
|
||||
}
|
||||
|
||||
use LineBreakCandidate::*;
|
||||
|
||||
pub struct LineBreakCandidateIter<'a> {
|
||||
|
@ -124,7 +128,7 @@ trait EvenAfterSpaces {
|
|||
impl EvenAfterSpaces for str {
|
||||
fn even_after_spaces(&self) -> &Self {
|
||||
let mut ret = self;
|
||||
while !ret.is_empty() && get_class!(ret) != SP {
|
||||
while !ret.is_empty() && get_class!(&ret) != SP {
|
||||
ret = &ret[get_base_character!(ret).unwrap().len_utf8()..];
|
||||
}
|
||||
ret
|
||||
|
@ -134,32 +138,14 @@ impl EvenAfterSpaces for str {
|
|||
/// Returns positions where breaks can happen
|
||||
/// Examples:
|
||||
/// ```
|
||||
/// use melib::text_processing::{
|
||||
/// self,
|
||||
/// line_break::LineBreakCandidateIter,
|
||||
/// LineBreakCandidate::{self, *},
|
||||
/// };
|
||||
/// use melib::text_processing::{self, LineBreakCandidate::{self, *}};
|
||||
/// use melib::text_processing::line_break::LineBreakCandidateIter;
|
||||
///
|
||||
/// assert!(LineBreakCandidateIter::new("")
|
||||
/// .collect::<Vec<(usize, LineBreakCandidate)>>()
|
||||
/// .is_empty());
|
||||
/// assert_eq!(
|
||||
/// &[(7, BreakAllowed), (12, MandatoryBreak)],
|
||||
/// LineBreakCandidateIter::new("Sample Text.")
|
||||
/// .collect::<Vec<(usize, LineBreakCandidate)>>()
|
||||
/// .as_slice()
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// &[
|
||||
/// (3, MandatoryBreak),
|
||||
/// (7, MandatoryBreak),
|
||||
/// (10, BreakAllowed),
|
||||
/// (17, MandatoryBreak)
|
||||
/// ],
|
||||
/// LineBreakCandidateIter::new("Sa\nmp\r\nle T(e)xt.")
|
||||
/// .collect::<Vec<(usize, LineBreakCandidate)>>()
|
||||
/// .as_slice()
|
||||
/// );
|
||||
/// assert!(LineBreakCandidateIter::new("").collect::<Vec<(usize, LineBreakCandidate)>>().is_empty());
|
||||
/// assert_eq!(&[(7, BreakAllowed), (12, MandatoryBreak)],
|
||||
/// LineBreakCandidateIter::new("Sample Text.").collect::<Vec<(usize, LineBreakCandidate)>>().as_slice());
|
||||
/// assert_eq!(&[(3, MandatoryBreak), (7, MandatoryBreak), (10, BreakAllowed), (17, MandatoryBreak)],
|
||||
/// LineBreakCandidateIter::new("Sa\nmp\r\nle T(e)xt.").collect::<Vec<(usize, LineBreakCandidate)>>().as_slice());
|
||||
/// ```
|
||||
impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
||||
type Item = (usize, LineBreakCandidate);
|
||||
|
@ -172,7 +158,7 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
}
|
||||
$last_break = $pos;
|
||||
};
|
||||
}
|
||||
};
|
||||
// After end of text, there are no breaks.
|
||||
if self.pos > self.text.len() {
|
||||
return None;
|
||||
|
@ -187,7 +173,7 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
|
||||
let LineBreakCandidateIter {
|
||||
ref mut iter,
|
||||
text,
|
||||
ref text,
|
||||
ref mut reg_ind_streak,
|
||||
ref mut break_now,
|
||||
ref mut last_break,
|
||||
|
@ -204,13 +190,13 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
*reg_ind_streak = 0;
|
||||
}
|
||||
|
||||
/* LB1 Assign a line breaking class to each code point of the input. Resolve
|
||||
* AI, CB, CJ, SA, SG, and XX into other line breaking classes
|
||||
* depending on criteria outside the scope of this algorithm.
|
||||
/* LB1 Assign a line breaking class to each code point of the input. Resolve AI, CB, CJ,
|
||||
* SA, SG, and XX into other line breaking classes depending on criteria outside the scope
|
||||
* of this algorithm.
|
||||
*
|
||||
* In the absence of such criteria all characters with a specific combination
|
||||
* of original class and General_Category property value are
|
||||
* resolved as follows: Resolved Original General_Category
|
||||
* In the absence of such criteria all characters with a specific combination of original
|
||||
* class and General_Category property value are resolved as follows:
|
||||
* Resolved Original General_Category
|
||||
* AL AI, SG, XX Any
|
||||
* CM SA Only Mn or Mc
|
||||
* AL SA Any except Mn and Mc
|
||||
|
@ -259,8 +245,7 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
continue;
|
||||
}
|
||||
WJ => {
|
||||
/* : LB11 Do not break before or after Word joiner and related
|
||||
* characters. */
|
||||
/*: LB11 Do not break before or after Word joiner and related characters.*/
|
||||
*pos += grapheme.len();
|
||||
continue;
|
||||
}
|
||||
|
@ -281,8 +266,8 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
}
|
||||
match class {
|
||||
ZW => {
|
||||
// LB8 Break before any character following a zero-width space, even if one or
|
||||
// more spaces intervene
|
||||
// LB8 Break before any character following a zero-width space, even if one or more
|
||||
// spaces intervene
|
||||
// ZW SP* ÷
|
||||
*pos += grapheme.len();
|
||||
while next_grapheme_class!((next_char is SP)) {
|
||||
|
@ -301,9 +286,9 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
}
|
||||
|
||||
CM => {
|
||||
// LB9 Do not break a combining character sequence; treat it as if it has the
|
||||
// line breaking class of the base character in all of the
|
||||
// following rules. Treat ZWJ as if it were CM.
|
||||
// LB9 Do not break a combining character sequence; treat it as if it has the line
|
||||
// breaking class of the base character in all of the following rules. Treat ZWJ as
|
||||
// if it were CM.
|
||||
// Treat X (CM | ZWJ)* as if it were X.
|
||||
// where X is any line break class except BK, CR, LF, NL, SP, or ZW.
|
||||
|
||||
|
@ -311,7 +296,7 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
continue;
|
||||
}
|
||||
WJ => {
|
||||
/* : LB11 Do not break before or after Word joiner and related characters. */
|
||||
/*: LB11 Do not break before or after Word joiner and related characters.*/
|
||||
*pos += grapheme.len();
|
||||
/* Get next grapheme */
|
||||
if next_grapheme_class!(iter, grapheme).is_some() {
|
||||
|
@ -320,8 +305,7 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
continue;
|
||||
}
|
||||
GL => {
|
||||
/* LB12 Non-breaking characters: LB12 Do not break after NBSP and related
|
||||
* characters. */
|
||||
/*LB12 Non-breaking characters: LB12 Do not break after NBSP and related characters.*/
|
||||
*pos += grapheme.len();
|
||||
continue;
|
||||
}
|
||||
|
@ -331,8 +315,8 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
let next_class = get_class!(next_grapheme);
|
||||
match next_class {
|
||||
GL if ![SP, BA, HY].contains(&class) => {
|
||||
/* LB12a Do not break before NBSP and related characters, except after
|
||||
* spaces and hyphens. [^SP BA HY] × GL
|
||||
/* LB12a Do not break before NBSP and related characters, except after spaces and
|
||||
* hyphens. [^SP BA HY] × GL
|
||||
* Also LB12 Do not break after NBSP and related characters */
|
||||
*pos += grapheme.len();
|
||||
continue;
|
||||
|
@ -400,8 +384,8 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
if !text[idx + grapheme.len()..].even_after_spaces().is_empty()
|
||||
&& get_class!(text[idx + grapheme.len()..].even_after_spaces()) == NS =>
|
||||
{
|
||||
/* LB16 Do not break between closing punctuation and a nonstarter (lb=NS),
|
||||
* even with intervening spaces.
|
||||
/* LB16 Do not break between closing punctuation and a nonstarter (lb=NS), even with
|
||||
* intervening spaces.
|
||||
* (CL | CP) SP* × NS */
|
||||
*pos += grapheme.len();
|
||||
while Some(SP) == next_grapheme_class!(iter, grapheme) {
|
||||
|
@ -413,7 +397,7 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
&& get_class!(text[idx + grapheme.len()..].even_after_spaces()) == B2 =>
|
||||
{
|
||||
/* LB17 Do not break within ‘——’, even with intervening spaces.
|
||||
* B2 SP* × B2 */
|
||||
* B2 SP* × B2*/
|
||||
*pos += grapheme.len();
|
||||
continue;
|
||||
}
|
||||
|
@ -450,9 +434,8 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
set_last_break!(*last_break, ret);
|
||||
return Some((ret, BreakAllowed));
|
||||
}
|
||||
/* LB21 Do not break before hyphen-minus, other hyphens, fixed-width spaces,
|
||||
* small kana, and other non-starters, or after acute accents. ×
|
||||
* BA, × HY, × NS, BB × */
|
||||
/* LB21 Do not break before hyphen-minus, other hyphens, fixed-width spaces, small
|
||||
* kana, and other non-starters, or after acute accents. × BA, × HY, × NS, BB × */
|
||||
BB if !*break_now => {
|
||||
*pos += grapheme.len();
|
||||
continue;
|
||||
|
@ -464,9 +447,8 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
let next_class = get_class!(next_grapheme);
|
||||
match next_class {
|
||||
BA | HY | NS => {
|
||||
/* LB21 Do not break before hyphen-minus, other hyphens, fixed-width
|
||||
* spaces, small kana, and other non-starters, or
|
||||
* after acute accents. × BA, × HY, × NS, BB × */
|
||||
/* LB21 Do not break before hyphen-minus, other hyphens, fixed-width spaces, small
|
||||
* kana, and other non-starters, or after acute accents. × BA, × HY, × NS, BB × */
|
||||
*pos += grapheme.len();
|
||||
//*pos += next_grapheme.len();
|
||||
continue;
|
||||
|
@ -503,7 +485,7 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
self.iter.next();
|
||||
continue;
|
||||
}
|
||||
/* EX × IN */
|
||||
/* EX × IN */
|
||||
EX if next_grapheme_class!((next_char is IN)) => {
|
||||
let (idx, next_grapheme) = next_char.unwrap();
|
||||
*pos = idx + next_grapheme.len();
|
||||
|
@ -515,21 +497,21 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
*pos += grapheme.len();
|
||||
continue;
|
||||
}
|
||||
/* (ID | EB | EM) × IN */
|
||||
/* (ID | EB | EM) × IN */
|
||||
ID | EB | EM if next_grapheme_class!((next_char is IN)) => {
|
||||
let (idx, next_grapheme) = next_char.unwrap();
|
||||
*pos = idx + next_grapheme.len();
|
||||
self.iter.next();
|
||||
continue;
|
||||
}
|
||||
/* IN × IN */
|
||||
/* IN × IN */
|
||||
IN if next_grapheme_class!((next_char is IN)) => {
|
||||
let (idx, next_grapheme) = next_char.unwrap();
|
||||
*pos = idx + next_grapheme.len();
|
||||
self.iter.next();
|
||||
continue;
|
||||
}
|
||||
/* NU × IN */
|
||||
/* NU × IN */
|
||||
NU if next_grapheme_class!((next_char is IN)) => {
|
||||
let (idx, next_grapheme) = next_char.unwrap();
|
||||
*pos = idx + next_grapheme.len();
|
||||
|
@ -551,8 +533,8 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
self.iter.next();
|
||||
continue;
|
||||
}
|
||||
/* LB23a Do not break between numeric prefixes and ideographs, or between
|
||||
* ideographs and numeric postfixes.
|
||||
/* LB23a Do not break between numeric prefixes and ideographs, or between ideographs
|
||||
* and numeric postfixes.
|
||||
* PR × (ID | EB | EM) */
|
||||
PR if next_grapheme_class!((next_char is ID, EB, EM)) => {
|
||||
let (idx, next_grapheme) = next_char.unwrap();
|
||||
|
@ -576,7 +558,7 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
self.iter.next();
|
||||
continue;
|
||||
}
|
||||
/* (AL | HL) × (PR | PO) */
|
||||
/*(AL | HL) × (PR | PO) */
|
||||
AL | HL if next_grapheme_class!((next_char is PR, PO)) => {
|
||||
let (idx, next_grapheme) = next_char.unwrap();
|
||||
*pos = idx + next_grapheme.len();
|
||||
|
@ -767,9 +749,9 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
continue;
|
||||
}
|
||||
RI => {
|
||||
/* LB30a Break between two regional indicator symbols if and only if there
|
||||
* are an even number of regional indicators preceding
|
||||
* the position of the break. sot (RI RI)* RI × RI
|
||||
/* LB30a Break between two regional indicator symbols if and only if there are an
|
||||
* even number of regional indicators preceding the position of the break.
|
||||
* sot (RI RI)* RI × RI
|
||||
* [^RI] (RI RI)* RI × RI */
|
||||
*reg_ind_streak += 1;
|
||||
*pos += grapheme.len();
|
||||
|
@ -870,7 +852,8 @@ mod tests {
|
|||
pub use alg::linear;
|
||||
|
||||
mod alg {
|
||||
use super::super::{grapheme_clusters::TextProcessing, *};
|
||||
use super::super::grapheme_clusters::TextProcessing;
|
||||
use super::super::*;
|
||||
fn cost(i: usize, j: usize, width: usize, minima: &[usize], offsets: &[usize]) -> usize {
|
||||
let w = offsets[j] + j - offsets[i] - i - 1;
|
||||
if w > width {
|
||||
|
@ -989,8 +972,7 @@ mod alg {
|
|||
);
|
||||
let x = minima[r - 1 + offset];
|
||||
let mut for_was_broken = false;
|
||||
let i_copy = i;
|
||||
for j in i_copy..(r - 1) {
|
||||
for j in i..(r - 1) {
|
||||
let y = cost(j + offset, r - 1 + offset, width, &minima, &offsets);
|
||||
if y <= x {
|
||||
n -= j;
|
||||
|
@ -1014,8 +996,8 @@ mod alg {
|
|||
let mut p_i = 0;
|
||||
while j > 0 {
|
||||
let mut line = String::new();
|
||||
for word in words.iter().take(j).skip(breaks[j]) {
|
||||
line.push_str(word);
|
||||
for i in breaks[j]..j {
|
||||
line.push_str(words[i]);
|
||||
}
|
||||
lines.push(line);
|
||||
if p_i + 1 < paragraphs {
|
||||
|
@ -1077,7 +1059,7 @@ pub fn split_lines_reflow(text: &str, reflow: Reflow, width: Option<usize>) -> V
|
|||
} else if prev_quote_depth == quote_depth {
|
||||
/* This becomes part of the paragraph we're in */
|
||||
} else {
|
||||
/* Malformed line, different quote depths can't be in the same paragraph. */
|
||||
/*Malformed line, different quote depths can't be in the same paragraph. */
|
||||
let paragraph = &text[paragraph_start..prev_index];
|
||||
reflow_helper(&mut ret, paragraph, prev_quote_depth, in_paragraph, width);
|
||||
|
||||
|
@ -1088,7 +1070,7 @@ pub fn split_lines_reflow(text: &str, reflow: Reflow, width: Option<usize>) -> V
|
|||
let paragraph = &text[paragraph_start..*i];
|
||||
reflow_helper(&mut ret, paragraph, quote_depth, in_paragraph, width);
|
||||
} else {
|
||||
/* Malformed line, different quote depths can't be in the same paragraph. */
|
||||
/*Malformed line, different quote depths can't be in the same paragraph. */
|
||||
let paragraph = &text[paragraph_start..prev_index];
|
||||
reflow_helper(&mut ret, paragraph, prev_quote_depth, in_paragraph, width);
|
||||
let paragraph = &text[prev_index..*i];
|
||||
|
@ -1128,7 +1110,7 @@ pub fn split_lines_reflow(text: &str, reflow: Reflow, width: Option<usize>) -> V
|
|||
for (idx, _g) in UnicodeSegmentation::grapheme_indices(line, true) {
|
||||
t[idx] = 1;
|
||||
}
|
||||
Box::new(segment_tree::SegmentTree::new(t))
|
||||
segment_tree::SegmentTree::new(t)
|
||||
};
|
||||
|
||||
let mut prev = 0;
|
||||
|
@ -1197,7 +1179,8 @@ fn reflow_helper(
|
|||
let paragraph = paragraph
|
||||
.trim_start_matches("es)
|
||||
.replace(&format!("\n{}", "es), "")
|
||||
.replace(['\n', '\r'], "");
|
||||
.replace("\n", "")
|
||||
.replace("\r", "");
|
||||
if in_paragraph {
|
||||
if let Some(width) = width {
|
||||
ret.extend(
|
||||
|
@ -1212,7 +1195,7 @@ fn reflow_helper(
|
|||
ret.push(format!("{}{}", "es, ¶graph));
|
||||
}
|
||||
} else {
|
||||
let paragraph = paragraph.replace(['\n', '\r'], "");
|
||||
let paragraph = paragraph.replace("\n", "").replace("\r", "");
|
||||
|
||||
if in_paragraph {
|
||||
if let Some(width) = width {
|
||||
|
@ -1263,13 +1246,14 @@ easy to take MORE than nothing.'"#;
|
|||
}
|
||||
}
|
||||
|
||||
mod segment_tree {
|
||||
//! Simple segment tree implementation for maximum in range queries. This
|
||||
//! is useful if given an array of numbers you want to get the
|
||||
//! maximum value inside an interval quickly.
|
||||
use std::{convert::TryFrom, iter::FromIterator};
|
||||
|
||||
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.
|
||||
*/
|
||||
use smallvec::SmallVec;
|
||||
use std::convert::TryFrom;
|
||||
use std::iter::FromIterator;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub(super) struct SegmentTree {
|
||||
|
@ -1278,9 +1262,9 @@ mod segment_tree {
|
|||
}
|
||||
|
||||
impl SegmentTree {
|
||||
pub(super) fn new(val: SmallVec<[usize; 1024]>) -> Self {
|
||||
pub(super) fn new(val: SmallVec<[usize; 1024]>) -> SegmentTree {
|
||||
if val.is_empty() {
|
||||
return Self {
|
||||
return SegmentTree {
|
||||
array: val.clone(),
|
||||
tree: val,
|
||||
};
|
||||
|
@ -1301,7 +1285,7 @@ mod segment_tree {
|
|||
segment_tree[i] = segment_tree[2 * i] + segment_tree[2 * i + 1];
|
||||
}
|
||||
|
||||
Self {
|
||||
SegmentTree {
|
||||
array: val,
|
||||
tree: segment_tree,
|
||||
}
|
||||
|
@ -1345,9 +1329,8 @@ mod segment_tree {
|
|||
}
|
||||
}
|
||||
|
||||
/// A lazy stateful iterator for line breaking text. Useful for very long text
|
||||
/// where you don't want to linebreak it completely before user requests
|
||||
/// specific lines.
|
||||
/// A lazy stateful iterator for line breaking text. Useful for very long text where you don't want
|
||||
/// to linebreak it completely before user requests specific lines.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LineBreakText {
|
||||
text: String,
|
||||
|
@ -1360,31 +1343,31 @@ pub struct LineBreakText {
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
enum ReflowState {
|
||||
No {
|
||||
ReflowNo {
|
||||
cur_index: usize,
|
||||
},
|
||||
AllWidth {
|
||||
ReflowAllWidth {
|
||||
width: usize,
|
||||
state: LineBreakTextState,
|
||||
},
|
||||
All {
|
||||
ReflowAll {
|
||||
cur_index: usize,
|
||||
},
|
||||
FormatFlowed {
|
||||
ReflowFormatFlowed {
|
||||
cur_index: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl ReflowState {
|
||||
fn new(reflow: Reflow, width: Option<usize>, cur_index: usize) -> Self {
|
||||
fn new(reflow: Reflow, width: Option<usize>, cur_index: usize) -> ReflowState {
|
||||
match reflow {
|
||||
Reflow::All if width.is_some() => Self::AllWidth {
|
||||
Reflow::All if width.is_some() => ReflowState::ReflowAllWidth {
|
||||
width: width.unwrap(),
|
||||
state: LineBreakTextState::AtLine { cur_index },
|
||||
},
|
||||
Reflow::All => Self::All { cur_index },
|
||||
Reflow::FormatFlowed => Self::FormatFlowed { cur_index },
|
||||
Reflow::No => Self::No { cur_index },
|
||||
Reflow::All => ReflowState::ReflowAll { cur_index },
|
||||
Reflow::FormatFlowed => ReflowState::ReflowFormatFlowed { cur_index },
|
||||
Reflow::No => ReflowState::ReflowNo { cur_index },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1400,7 +1383,7 @@ enum LineBreakTextState {
|
|||
within_line_index: usize,
|
||||
breaks: Vec<(usize, LineBreakCandidate)>,
|
||||
prev_break: usize,
|
||||
segment_tree: Box<segment_tree::SegmentTree>,
|
||||
segment_tree: segment_tree::SegmentTree,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -1412,7 +1395,7 @@ impl Default for LineBreakText {
|
|||
|
||||
impl LineBreakText {
|
||||
pub fn new(text: String, reflow: Reflow, width: Option<usize>) -> Self {
|
||||
Self {
|
||||
LineBreakText {
|
||||
text,
|
||||
state: ReflowState::new(reflow, width, 0),
|
||||
paragraph: VecDeque::new(),
|
||||
|
@ -1454,14 +1437,14 @@ impl LineBreakText {
|
|||
|
||||
pub fn is_finished(&self) -> bool {
|
||||
match self.state {
|
||||
ReflowState::No { cur_index }
|
||||
| ReflowState::All { cur_index }
|
||||
| ReflowState::FormatFlowed { cur_index }
|
||||
| ReflowState::AllWidth {
|
||||
ReflowState::ReflowNo { cur_index }
|
||||
| ReflowState::ReflowAll { cur_index }
|
||||
| ReflowState::ReflowFormatFlowed { cur_index }
|
||||
| ReflowState::ReflowAllWidth {
|
||||
width: _,
|
||||
state: LineBreakTextState::AtLine { cur_index },
|
||||
} => cur_index >= self.text.len(),
|
||||
ReflowState::AllWidth {
|
||||
ReflowState::ReflowAllWidth {
|
||||
width: _,
|
||||
state: LineBreakTextState::WithinLine { .. },
|
||||
} => false,
|
||||
|
@ -1479,7 +1462,7 @@ impl Iterator for LineBreakText {
|
|||
return None;
|
||||
}
|
||||
match self.state {
|
||||
ReflowState::FormatFlowed { ref mut cur_index } => {
|
||||
ReflowState::ReflowFormatFlowed { ref mut cur_index } => {
|
||||
/* rfc3676 - The Text/Plain Format and DelSp Parameters
|
||||
* https://tools.ietf.org/html/rfc3676 */
|
||||
|
||||
|
@ -1528,8 +1511,7 @@ impl Iterator for LineBreakText {
|
|||
} else if prev_quote_depth == quote_depth {
|
||||
/* This becomes part of the paragraph we're in */
|
||||
} else {
|
||||
/* Malformed line, different quote depths can't be in the same
|
||||
* paragraph. */
|
||||
/*Malformed line, different quote depths can't be in the same paragraph. */
|
||||
let paragraph_s = &self.text[paragraph_start..prev_index];
|
||||
reflow_helper2(
|
||||
&mut paragraph,
|
||||
|
@ -1552,8 +1534,7 @@ impl Iterator for LineBreakText {
|
|||
self.width,
|
||||
);
|
||||
} else {
|
||||
/* Malformed line, different quote depths can't be in the same
|
||||
* paragraph. */
|
||||
/*Malformed line, different quote depths can't be in the same paragraph. */
|
||||
let paragraph_s = &self.text[paragraph_start..prev_index];
|
||||
reflow_helper2(
|
||||
&mut paragraph,
|
||||
|
@ -1593,9 +1574,9 @@ impl Iterator for LineBreakText {
|
|||
);
|
||||
self.paragraph = paragraph;
|
||||
}
|
||||
self.paragraph.pop_front()
|
||||
return self.paragraph.pop_front();
|
||||
}
|
||||
ReflowState::AllWidth {
|
||||
ReflowState::ReflowAllWidth {
|
||||
width,
|
||||
ref mut state,
|
||||
} => {
|
||||
|
@ -1644,7 +1625,7 @@ impl Iterator for LineBreakText {
|
|||
{
|
||||
t[idx] = 1;
|
||||
}
|
||||
Box::new(segment_tree::SegmentTree::new(t))
|
||||
segment_tree::SegmentTree::new(t)
|
||||
},
|
||||
};
|
||||
if let LineBreakTextState::WithinLine {
|
||||
|
@ -1760,13 +1741,14 @@ impl Iterator for LineBreakText {
|
|||
};
|
||||
}
|
||||
}
|
||||
ReflowState::No { ref mut cur_index } | ReflowState::All { ref mut cur_index } => {
|
||||
if let Some(line) = self.text[*cur_index..].split('\n').next() {
|
||||
ReflowState::ReflowNo { ref mut cur_index }
|
||||
| ReflowState::ReflowAll { ref mut cur_index } => {
|
||||
for line in self.text[*cur_index..].split('\n') {
|
||||
let ret = line.to_string();
|
||||
*cur_index += line.len() + 2;
|
||||
return Some(ret);
|
||||
}
|
||||
None
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1784,7 +1766,8 @@ fn reflow_helper2(
|
|||
let paragraph = paragraph
|
||||
.trim_start_matches("es)
|
||||
.replace(&format!("\n{}", "es), "")
|
||||
.replace(['\n', '\r'], "");
|
||||
.replace("\n", "")
|
||||
.replace("\r", "");
|
||||
if in_paragraph {
|
||||
if let Some(width) = width {
|
||||
ret.extend(
|
||||
|
@ -1799,7 +1782,7 @@ fn reflow_helper2(
|
|||
ret.push_back(format!("{}{}", "es, ¶graph));
|
||||
}
|
||||
} else {
|
||||
let paragraph = paragraph.replace(['\n', '\r'], "");
|
||||
let paragraph = paragraph.replace("\n", "").replace("\r", "");
|
||||
|
||||
if in_paragraph {
|
||||
if let Some(width) = width {
|
||||
|
|
|
@ -77,7 +77,10 @@ impl Truncate for &str {
|
|||
|
||||
extern crate unicode_segmentation;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(*self, true).nth(skip_len) {
|
||||
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(*self, true)
|
||||
.skip(skip_len)
|
||||
.next()
|
||||
{
|
||||
&self[first..]
|
||||
} else {
|
||||
self
|
||||
|
@ -92,7 +95,10 @@ impl Truncate for &str {
|
|||
|
||||
extern crate unicode_segmentation;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(*self, true).nth(skip_len) {
|
||||
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(*self, true)
|
||||
.skip(skip_len)
|
||||
.next()
|
||||
{
|
||||
*self = &self[first..];
|
||||
}
|
||||
}
|
||||
|
@ -110,7 +116,7 @@ impl Truncate for String {
|
|||
.take(new_len)
|
||||
.last()
|
||||
{
|
||||
Self::truncate(self, last);
|
||||
String::truncate(self, last);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,8 +144,9 @@ impl Truncate for String {
|
|||
|
||||
extern crate unicode_segmentation;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
if let Some((first, _)) =
|
||||
UnicodeSegmentation::grapheme_indices(self.as_str(), true).nth(skip_len)
|
||||
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(self.as_str(), true)
|
||||
.skip(skip_len)
|
||||
.next()
|
||||
{
|
||||
&self[first..]
|
||||
} else {
|
||||
|
@ -155,8 +162,9 @@ impl Truncate for String {
|
|||
|
||||
extern crate unicode_segmentation;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
if let Some((first, _)) =
|
||||
UnicodeSegmentation::grapheme_indices(self.as_str(), true).nth(skip_len)
|
||||
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(self.as_str(), true)
|
||||
.skip(skip_len)
|
||||
.next()
|
||||
{
|
||||
*self = self[first..].to_string();
|
||||
}
|
||||
|
@ -170,11 +178,17 @@ pub trait GlobMatch {
|
|||
|
||||
impl GlobMatch for str {
|
||||
fn matches_glob(&self, _pattern: &str) -> bool {
|
||||
let pattern: Vec<&Self> = _pattern
|
||||
.strip_suffix('/')
|
||||
.unwrap_or(_pattern)
|
||||
.split_graphemes();
|
||||
let s: Vec<&Self> = self.strip_suffix('/').unwrap_or(self).split_graphemes();
|
||||
macro_rules! strip_slash {
|
||||
($v:expr) => {
|
||||
if $v.ends_with("/") {
|
||||
&$v[..$v.len() - 1]
|
||||
} else {
|
||||
$v
|
||||
}
|
||||
};
|
||||
}
|
||||
let pattern: Vec<&str> = strip_slash!(_pattern).split_graphemes();
|
||||
let s: Vec<&str> = strip_slash!(self).split_graphemes();
|
||||
|
||||
// Taken from https://research.swtch.com/glob
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue