Compare commits
1 Commits
Author | SHA1 | Date |
---|---|---|
Manos Pitsidianakis | 413be3f334 |
115
CHANGELOG.md
115
CHANGELOG.md
|
@ -7,117 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added listing configuration setting `thread_subject_pack` (see meli.conf.5)
|
||||
- Added shortcuts for focusing to sidebar menu and back to the e-mail view (`focus_left` and `focus_right`)
|
||||
- `f76f4ea3` A new manual page, `meli.7` which contains a general tutorial for using meli.
|
||||
- `cbe593cf` add configurable header preample suffix and prefix for editing
|
||||
- `a484b397` Added instructions and information to error shown when libnotmuch could not be found.
|
||||
- `a484b397` Added configuration setting `library_file_path` to notmuch backend if user wants to specify the library's location manually.
|
||||
- `aa99b0d7` Implement configurable subject prefix stripping when replying
|
||||
- `a73885ac` added RGB support to embedded terminal emulator.
|
||||
- `f4e0970d` added ability to kill embed process with Ctrl-C, or Ctrl-Z and pressing 'q'.
|
||||
- `9205f3b8` added a per account mail sort order parameter.
|
||||
- `d921b3c3` implemented sorting with user sort order parameter if defined.
|
||||
- `dc5afa13` use osascript/applescript for notifications on macos
|
||||
- `d0de0485` add {in,de}crease_sidebar shortcuts
|
||||
- `340d6451` add config setting for sidebar ratio
|
||||
- `36e29cb6` Add configurable mailbox sort order
|
||||
|
||||
### Changed
|
||||
|
||||
- `f76f4ea3` Shortcut `open_thread` and `exit_thread` renamed to `open_entry` and `exit_entry`.
|
||||
- `7650805c` Binary size reduced significantly.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `a42a6ca8` show notifications in terminal if there is no other alternative.
|
||||
|
||||
## [alpha-0.7.2] - 2021-10-15
|
||||
|
||||
### Added
|
||||
|
||||
- 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
|
||||
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
|
||||
|
||||
### Added
|
||||
- Add customizable mailbox tree in sidebar
|
||||
- Make `dbus` dependency opt-out (feature is `dbus-notifications`)
|
||||
- Implemented JMAP async, search, tagging, syncing
|
||||
- Preserve account order from configuration file
|
||||
- Implemented IMAP `CONDSTORE` support for IMAP cache
|
||||
- Add `timeout` setting for IMAP
|
||||
- Implement TCP keepalive for IMAP
|
||||
- Rewrote email address parsers.
|
||||
- Implement `copy_messages` for maildir
|
||||
- Implement selection with motions
|
||||
|
||||
### Fixed
|
||||
- Fixed various problems with IMAP cache
|
||||
- Fixed various problems with IMAP message counts
|
||||
- Fixed various problems with IMAP connection hanging
|
||||
- Fixed IMAP not reconnecting on dropped IDLE connections
|
||||
- Fixed various problems with notmuch backend
|
||||
|
||||
## [alpha-0.6.1] - 2020-08-02
|
||||
|
||||
### Added
|
||||
|
@ -193,7 +82,3 @@ Notable changes:
|
|||
[alpha-0.5.1]: https://github.com/meli/meli/releases/tag/alpha-0.5.1
|
||||
[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
82
Cargo.toml
82
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "meli"
|
||||
version = "0.7.2"
|
||||
version = "0.6.1"
|
||||
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
|
||||
edition = "2018"
|
||||
|
||||
|
@ -15,82 +15,72 @@ default-run = "meli"
|
|||
|
||||
[[bin]]
|
||||
name = "meli"
|
||||
path = "src/main.rs"
|
||||
path = "src/bin.rs"
|
||||
|
||||
[lib]
|
||||
name = "meli"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "managesieve-client"
|
||||
path = "src/managesieve.rs"
|
||||
#[[bin]]
|
||||
#name = "managesieve-meli"
|
||||
#path = "src/managesieve.rs"
|
||||
|
||||
#[[bin]]
|
||||
#name = "async"
|
||||
#path = "src/async.rs"
|
||||
|
||||
[dependencies]
|
||||
async-task = "^4.2.0"
|
||||
bincode = { version = "^1.3.0", default-features = false }
|
||||
bitflags = "1.0"
|
||||
crossbeam = { version = "^0.8" }
|
||||
flate2 = { version = "1.0.16", 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.1" }
|
||||
|
||||
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.10", 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
|
||||
notify-rust = { version = "^4", optional = true }
|
||||
termion = "1.5.1"
|
||||
bincode = "1.2.0"
|
||||
uuid = { version = "0.8.1", features = ["serde", "v4"] }
|
||||
unicode-segmentation = "1.2.1" # >:c
|
||||
xdg = "2.1.0"
|
||||
|
||||
[target.'cfg(target_os="linux")'.dependencies]
|
||||
notify-rust = { version = "^4", default-features = false, features = ["dbus", ], optional = true }
|
||||
libc = {version = "0.2.59", features = ["extra_traits",]}
|
||||
rmp = "^0.8"
|
||||
rmpv = { version = "^0.4.2", features=["with-serde",] }
|
||||
rmp-serde = "^0.14.0"
|
||||
smallvec = { version = "^1.4.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"
|
||||
|
||||
[build-dependencies]
|
||||
flate2 = { version = "1.0.16", optional = true }
|
||||
proc-macro2 = "1.0.37"
|
||||
syn = { version = "1.0.31", features = [] }
|
||||
quote = "^1.0"
|
||||
syn = { version = "1.0.92", features = [] }
|
||||
|
||||
[dev-dependencies]
|
||||
regex = "1"
|
||||
proc-macro2 = "1.0.18"
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
opt-level = "s"
|
||||
opt-level = "z"
|
||||
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"]
|
||||
notmuch = ["melib/notmuch_backend", ]
|
||||
jmap = ["melib/jmap_backend",]
|
||||
sqlite3 = ["melib/sqlite3"]
|
||||
smtp = ["melib/smtp"]
|
||||
regexp = ["pcre2"]
|
||||
dbus-notifications = ["notify-rust",]
|
||||
cli-docs = ["flate2"]
|
||||
cli-docs = []
|
||||
svgscreenshot = ["svg_crate"]
|
||||
gpgme = ["melib/gpgme"]
|
||||
|
||||
# Print tracing logs as meli runs in stderr
|
||||
# enable for debug tracing logs: build with --features=debug-tracing
|
||||
|
|
32
Makefile
32
Makefile
|
@ -16,8 +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:
|
||||
|
||||
# Options
|
||||
PREFIX ?= /usr/local
|
||||
|
@ -28,10 +26,8 @@ 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
|
||||
FEATURES ?= --features "${MELI_FEATURES}"
|
||||
|
||||
|
@ -48,11 +44,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
|
||||
@${CARGO_BIN} build ${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:"
|
||||
|
@ -87,18 +83,15 @@ help:
|
|||
@echo -n "* NO_COLOR ${UNDERLINE}"
|
||||
@[ $${NO_COLOR+x} ] && echo -n "set" || echo -n "unset"
|
||||
@echo ${ANSI_RESET}
|
||||
@echo "* CARGO_BIN = ${UNDERLINE}${CARGO_BIN}${ANSI_RESET}"
|
||||
@echo "* CARGO_ARGS = ${UNDERLINE}${CARGO_ARGS}${ANSI_RESET}"
|
||||
@echo "* MIN_RUSTC = ${UNDERLINE}${MIN_RUSTC}${ANSI_RESET}"
|
||||
@#@echo "* CARGO_COLOR = ${CARGO_COLOR}"
|
||||
|
||||
.PHONY: check
|
||||
check:
|
||||
@${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --workspace
|
||||
@${CARGO_BIN} test ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --workspace
|
||||
|
||||
.PHONY: check-deps
|
||||
check-deps:
|
||||
@(if ! echo ${MIN_RUSTC}\\n`${CARGO_BIN} --version | grep ^cargo | cut -d ' ' -f 2` | sort -CV; then echo "rust version >= ${RED}${MIN_RUSTC}${ANSI_RESET} required, found: `which ${CARGO_BIN}` `${CARGO_BIN} --version | cut -d ' ' -f 2`" \
|
||||
@(if ! echo ${MIN_RUSTC}\\n`${CARGO_BIN} --version | cut -d ' ' -f 2` | sort -CV; then echo "rust version >= ${RED}${MIN_RUSTC}${ANSI_RESET} required, found: `which ${CARGO_BIN}` `${CARGO_BIN} --version | cut -d ' ' -f 2`" \
|
||||
"\nYour options:\n - Set CARGO_BIN to a supported version\n - Install a supported version from your distribution's package manager\n - Install a supported version from ${UNDERLINE}https://rustup.rs/${ANSI_RESET}" ; exit 1; fi)
|
||||
|
||||
|
||||
|
@ -127,10 +120,10 @@ install-doc:
|
|||
SECTION=`echo $${MANPAGE} | rev | cut -d "." -f 1`; \
|
||||
MANPAGEPATH=${DESTDIR}${MANDIR}/man$${SECTION}/$${MANPAGE}.gz; \
|
||||
echo " * installing $${MANPAGE} → ${GREEN}$${MANPAGEPATH}${ANSI_RESET}"; \
|
||||
gzip -n < ${DOCS_SUBDIR}$${MANPAGE} > $${MANPAGEPATH} \
|
||||
; done ; \
|
||||
gzip -n < $${MANPAGE} > $${MANPAGEPATH} \
|
||||
; 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)
|
||||
|
@ -140,13 +133,10 @@ 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}
|
||||
@rm -f $(DESTDIR)${BINDIR}/meli
|
||||
@cp ./${CARGO_TARGET_DIR}/release/meli $(DESTDIR)${BINDIR}/meli
|
||||
@chmod 755 $(DESTDIR)${BINDIR}/meli
|
||||
@install -D ./${CARGO_TARGET_DIR}/release/meli $(DESTDIR)${BINDIR}/meli
|
||||
|
||||
|
||||
.PHONY: install
|
||||
|
@ -170,4 +160,4 @@ deb-dist:
|
|||
|
||||
.PHONY: build-rustdoc
|
||||
build-rustdoc:
|
||||
@RUSTDOCFLAGS="--crate-version ${VERSION}_${GIT_COMMIT}_${DATE}" ${CARGO_BIN} doc ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all-features --no-deps --workspace --document-private-items --open
|
||||
@RUSTDOCFLAGS="--crate-version ${VERSION}_${GIT_COMMIT}_${DATE}" ${CARGO_BIN} doc ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all-features --no-deps --workspace --document-private-items --open
|
||||
|
|
146
README.md
146
README.md
|
@ -1,89 +1,92 @@
|
|||
# meli [![GitHub license](https://img.shields.io/github/license/meli/meli)](https://github.com/meli/meli/blob/master/COPYING) [![Crates.io](https://img.shields.io/crates/v/meli)](https://crates.io/crates/meli)
|
||||
|
||||
**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")
|
||||
|
||||
| | | |
|
||||
:---:|:---:|:---:
|
||||
![Main view screenshot](./docs/screenshots/main.webp "mail meli view screenshot") | ![Compact main view screenshot](./docs/screenshots/compact.webp "compact main view screenshot") | ![Compose with embed terminal editor screenshot](./docs/screenshots/compose.webp "composing view screenshot")
|
||||
Main view | Compact main view | Compose with embed terminal editor
|
||||
|
||||
Main repository:
|
||||
* https://git.meli.delivery/meli/meli
|
||||
|
||||
Official mirrors:
|
||||
* https://github.com/meli/meli
|
||||
|
||||
## Install
|
||||
- Try an [online interactive web demo](https://meli.delivery/wasm2.html "online interactive web demo") powered by WebAssembly
|
||||
- [`cargo install meli`](https://crates.io/crates/meli "crates.io meli package")
|
||||
- [Download and install pre-built debian package, static linux binary](https://github.com/meli/meli/releases/ "github releases for meli"), or
|
||||
- Install with [Nix](https://search.nixos.org/packages?show=meli&query=meli&from=0&size=30&sort=relevance&channel=unstable#disabled "nixos package search results for 'meli'")
|
||||
|
||||
## Documentation
|
||||
|
||||
See a comprehensive tour of `meli` in the manual page [`meli(7)`](./docs/meli.7).
|
||||
|
||||
See also the [Quickstart tutorial](https://meli.delivery/documentation.html#quick-start) online.
|
||||
|
||||
After installing `meli`, see `meli(1)`, `meli.conf(5)`, `meli(7)` and `meli-themes(5)` for documentation. Sample configuration and theme files can be found in the `docs/samples/` subdirectory. Manual pages are also [hosted online](https://meli.delivery/documentation.html "meli documentation").
|
||||
|
||||
`meli` by default looks for a configuration file in this location: `$XDG_CONFIG_HOME/meli/config.toml`
|
||||
|
||||
You can run meli with arbitrary configuration files by setting the `$MELI_CONFIG`
|
||||
environment variable to their locations, i.e.:
|
||||
|
||||
```sh
|
||||
MELI_CONFIG=./test_config cargo run
|
||||
```
|
||||
|
||||
## Build
|
||||
# meli
|
||||
For a quick start, build and install locally:
|
||||
|
||||
```sh
|
||||
PREFIX=~/.local make install
|
||||
```
|
||||
|
||||
Available subcommands for `make` are listed with `make help`. The Makefile *should* be POSIX portable and not require a specific `make` version.
|
||||
Available subcommands:
|
||||
- meli (builds meli with optimizations in `$CARGO_TARGET_DIR`)
|
||||
- install (installs binary in `$BINDIR` and documentation to `$MANDIR`)
|
||||
- uninstall
|
||||
Secondary subcommands:
|
||||
- clean (cleans build artifacts)
|
||||
- check-deps (checks dependencies)
|
||||
- install-bin (installs binary to `$BINDIR`)
|
||||
- install-doc (installs manpages to `$MANDIR`)
|
||||
- help (prints this information)
|
||||
- dist (creates release tarball named `meli-VERSION.tar.gz` in this directory)
|
||||
- deb-dist (builds debian package in the parent directory)
|
||||
- distclean (cleans distribution build artifacts)
|
||||
- build-rustdoc (builds rustdoc documentation for all packages in `$CARGO_TARGET_DIR`)
|
||||
|
||||
`meli` requires rust 1.39 and rust's package manager, Cargo. Information on how
|
||||
The Makefile *should* be portable and not require a specific `make` version.
|
||||
|
||||
# Documentation
|
||||
|
||||
After installing meli, see `meli(1)` and `meli.conf(5)` for documentation. Sample configuration and theme files can be found in the `samples/` subdirectory.
|
||||
|
||||
# Building
|
||||
|
||||
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`.
|
||||
With Cargo available, the project can be built with
|
||||
|
||||
You can build and run `meli` with one command: `cargo run --release`.
|
||||
```sh
|
||||
make
|
||||
```
|
||||
|
||||
### Build features
|
||||
The resulting binary will then be found under `target/release/meli`
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
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`:
|
||||
|
||||
```sh
|
||||
make PREFIX=$HOME/.local install
|
||||
```
|
||||
|
||||
See `meli(1)` and `meli.conf(5)` for documentation.
|
||||
|
||||
You can build and run meli with one command:
|
||||
|
||||
```sh
|
||||
cargo run --release
|
||||
```
|
||||
|
||||
While the project is in early development, meli will only be developed for the
|
||||
linux kernel and respected linux distributions. Support for more UNIX-like OSes
|
||||
is on the roadmap.
|
||||
|
||||
## Features
|
||||
|
||||
Some functionality is held behind "feature gates", or compile-time flags. The following list explains each feature's purpose:
|
||||
|
||||
- `gpgme` enables GPG support via `libgpgme` (on by default)
|
||||
- `dbus-notifications` enables showing notifications using `dbus` (on by default)
|
||||
- `notmuch` provides support for using a notmuch database as a mail backend (on by default)
|
||||
- `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)
|
||||
- `dbus-notifications` enables showing notifications using `dbus`.
|
||||
- `notmuch` provides support for using a notmuch database as a mail backend
|
||||
- `jmap` provides support for connecting to a jmap server and use it as a mail backend
|
||||
- `sqlite3` provides support for builting fast search indexes in local sqlite3 databases
|
||||
- `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.
|
||||
- `debug-tracing` enables various trace debug logs from various places around the meli code base. The trace log is printed in `stderr`.
|
||||
|
||||
### Build Debian package (*deb*)
|
||||
## Building in Debian
|
||||
|
||||
Building with Debian's packaged cargo might require the installation of these
|
||||
two packages: `librust-openssl-sys-dev librust-libdbus-sys-dev`
|
||||
|
||||
A `*.deb` package can be built with `make deb-dist`
|
||||
|
||||
### Using notmuch
|
||||
# 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.
|
||||
|
||||
### Building with JMAP
|
||||
# Building with JMAP
|
||||
|
||||
To build with JMAP support, prepend the environment variable `MELI_FEATURES='jmap'` to your make invocation:
|
||||
|
||||
|
@ -104,11 +107,22 @@ 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.
|
||||
|
||||
## Testing
|
||||
# Configuration
|
||||
|
||||
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, ie:
|
||||
|
||||
```sh
|
||||
MELI_CONFIG=./test_config cargo run
|
||||
```
|
||||
|
||||
# Testing
|
||||
|
||||
How to run specific tests:
|
||||
|
||||
|
@ -116,14 +130,14 @@ How to run specific tests:
|
|||
cargo test -p {melib, meli} (-- --nocapture) (--test test_name)
|
||||
```
|
||||
|
||||
## Profiling
|
||||
# Profiling
|
||||
|
||||
```sh
|
||||
perf record -g target/debug/bin
|
||||
perf script | stackcollapse-perf | rust-unmangle | flamegraph > perf.svg
|
||||
```
|
||||
|
||||
## Running fuzz targets
|
||||
# Running fuzz targets
|
||||
|
||||
Note: `cargo-fuzz` requires the nightly toolchain.
|
||||
|
||||
|
|
55
build.rs
55
build.rs
|
@ -37,39 +37,46 @@ fn main() {
|
|||
]);
|
||||
#[cfg(feature = "cli-docs")]
|
||||
{
|
||||
use flate2::Compression;
|
||||
use flate2::GzBuilder;
|
||||
const MANDOC_OPTS: &[&str] = &["-T", "utf8", "-I", "os=Generated by mandoc(1)"];
|
||||
const MANDOC_OPTS: &[&'static str] = &["-T", "utf8", "-I", "os=Generated by mandoc(1)"];
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let mut out_dir_path = Path::new(&out_dir).to_path_buf();
|
||||
out_dir_path.push("meli.txt");
|
||||
|
||||
let mut cl = |filepath: &str, output: &str| {
|
||||
out_dir_path.push(output);
|
||||
let output = Command::new("mandoc")
|
||||
.args(MANDOC_OPTS)
|
||||
.arg(filepath)
|
||||
.output()
|
||||
.or_else(|_| Command::new("man").arg("-l").arg(filepath).output())
|
||||
.unwrap();
|
||||
let output = Command::new("mandoc")
|
||||
.args(MANDOC_OPTS)
|
||||
.arg("meli.1")
|
||||
.output()
|
||||
.or_else(|_| Command::new("man").arg("-l").arg("meli.1").output())
|
||||
.unwrap();
|
||||
|
||||
let file = File::create(&out_dir_path).unwrap();
|
||||
let mut gz = GzBuilder::new()
|
||||
.comment(output.stdout.len().to_string().into_bytes())
|
||||
.write(file, Compression::default());
|
||||
gz.write_all(&output.stdout).unwrap();
|
||||
gz.finish().unwrap();
|
||||
out_dir_path.pop();
|
||||
};
|
||||
let mut file = File::create(&out_dir_path).unwrap();
|
||||
file.write_all(&output.stdout).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");
|
||||
let output = Command::new("mandoc")
|
||||
.args(MANDOC_OPTS)
|
||||
.arg("meli.conf.5")
|
||||
.output()
|
||||
.or_else(|_| Command::new("man").arg("-l").arg("meli.conf.5").output())
|
||||
.unwrap();
|
||||
let mut file = File::create(&out_dir_path).unwrap();
|
||||
file.write_all(&output.stdout).unwrap();
|
||||
out_dir_path.pop();
|
||||
|
||||
out_dir_path.push("meli-themes.txt");
|
||||
let output = Command::new("mandoc")
|
||||
.args(MANDOC_OPTS)
|
||||
.arg("meli-themes.5")
|
||||
.output()
|
||||
.or_else(|_| Command::new("man").arg("-l").arg("meli-themes.5").output())
|
||||
.unwrap();
|
||||
let mut file = File::create(&out_dir_path).unwrap();
|
||||
file.write_all(&output.stdout).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,9 +50,7 @@ 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::*;
|
||||
|
||||
"##
|
||||
|
@ -89,14 +87,6 @@ use super::*;
|
|||
}
|
||||
let override_ident: syn::Ident = format_ident!("{}Override", s.ident);
|
||||
let mut field_tokentrees = vec![];
|
||||
let mut attrs_tokens = vec![];
|
||||
for attr in &s.attrs {
|
||||
if let Ok(syn::Meta::List(ml)) = attr.parse_meta() {
|
||||
if ml.path.get_ident().is_some() && ml.path.get_ident().unwrap() == "cfg" {
|
||||
attrs_tokens.push(attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut field_idents = vec![];
|
||||
for f in &s.fields {
|
||||
let ident = &f.ident;
|
||||
|
@ -156,7 +146,6 @@ use super::*;
|
|||
//let fields = &s.fields;
|
||||
|
||||
let literal_struct = quote! {
|
||||
#(#attrs_tokens)*
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct #override_ident {
|
||||
|
@ -164,7 +153,6 @@ use super::*;
|
|||
}
|
||||
|
||||
|
||||
#(#attrs_tokens)*
|
||||
impl Default for #override_ident {
|
||||
fn default() -> Self {
|
||||
#override_ident {
|
||||
|
|
|
@ -1,348 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright 2012 Google Inc.
|
||||
# Copyright 2020 Manos Pitsidianakis
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Performs client tasks for testing IMAP OAuth2 authentication.
|
||||
|
||||
To use this script, you'll need to have registered with Google as an OAuth
|
||||
application and obtained an OAuth client ID and client secret.
|
||||
See https://developers.google.com/identity/protocols/OAuth2 for instructions on
|
||||
registering and for documentation of the APIs invoked by this code.
|
||||
|
||||
This script has 3 modes of operation.
|
||||
|
||||
1. The first mode is used to generate and authorize an OAuth2 token, the
|
||||
first step in logging in via OAuth2.
|
||||
|
||||
oauth2 --user=xxx@gmail.com \
|
||||
--client_id=1038[...].apps.googleusercontent.com \
|
||||
--client_secret=VWFn8LIKAMC-MsjBMhJeOplZ \
|
||||
--generate_oauth2_token
|
||||
|
||||
The script will converse with Google and generate an oauth request
|
||||
token, then present you with a URL you should visit in your browser to
|
||||
authorize the token. Once you get the verification code from the Google
|
||||
website, enter it into the script to get your OAuth access token. The output
|
||||
from this command will contain the access token, a refresh token, and some
|
||||
metadata about the tokens. The access token can be used until it expires, and
|
||||
the refresh token lasts indefinitely, so you should record these values for
|
||||
reuse.
|
||||
|
||||
2. The script will generate new access tokens using a refresh token.
|
||||
|
||||
oauth2 --user=xxx@gmail.com \
|
||||
--client_id=1038[...].apps.googleusercontent.com \
|
||||
--client_secret=VWFn8LIKAMC-MsjBMhJeOplZ \
|
||||
--refresh_token=1/Yzm6MRy4q1xi7Dx2DuWXNgT6s37OrP_DW_IoyTum4YA
|
||||
|
||||
3. The script will generate an OAuth2 string that can be fed
|
||||
directly to IMAP or SMTP. This is triggered with the --generate_oauth2_string
|
||||
option.
|
||||
|
||||
oauth2 --generate_oauth2_string --user=xxx@gmail.com \
|
||||
--access_token=ya29.AGy[...]ezLg
|
||||
|
||||
The output of this mode will be a base64-encoded string. To use it, connect to a
|
||||
IMAPFE and pass it as the second argument to the AUTHENTICATE command.
|
||||
|
||||
a AUTHENTICATE XOAUTH2 a9sha9sfs[...]9dfja929dk==
|
||||
"""
|
||||
|
||||
import base64
|
||||
import imaplib
|
||||
import json
|
||||
from optparse import OptionParser
|
||||
import smtplib
|
||||
import sys
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
|
||||
|
||||
def SetupOptionParser():
|
||||
# Usage message is the module's docstring.
|
||||
parser = OptionParser(usage=__doc__)
|
||||
parser.add_option('--generate_oauth2_token',
|
||||
action='store_true',
|
||||
dest='generate_oauth2_token',
|
||||
help='generates an OAuth2 token for testing')
|
||||
parser.add_option('--generate_oauth2_string',
|
||||
action='store_true',
|
||||
dest='generate_oauth2_string',
|
||||
help='generates an initial client response string for '
|
||||
'OAuth2')
|
||||
parser.add_option('--client_id',
|
||||
default=None,
|
||||
help='Client ID of the application that is authenticating. '
|
||||
'See OAuth2 documentation for details.')
|
||||
parser.add_option('--client_secret',
|
||||
default=None,
|
||||
help='Client secret of the application that is '
|
||||
'authenticating. See OAuth2 documentation for '
|
||||
'details.')
|
||||
parser.add_option('--access_token',
|
||||
default=None,
|
||||
help='OAuth2 access token')
|
||||
parser.add_option('--refresh_token',
|
||||
default=None,
|
||||
help='OAuth2 refresh token')
|
||||
parser.add_option('--scope',
|
||||
default='https://mail.google.com/',
|
||||
help='scope for the access token. Multiple scopes can be '
|
||||
'listed separated by spaces with the whole argument '
|
||||
'quoted.')
|
||||
parser.add_option('--test_imap_authentication',
|
||||
action='store_true',
|
||||
dest='test_imap_authentication',
|
||||
help='attempts to authenticate to IMAP')
|
||||
parser.add_option('--test_smtp_authentication',
|
||||
action='store_true',
|
||||
dest='test_smtp_authentication',
|
||||
help='attempts to authenticate to SMTP')
|
||||
parser.add_option('--user',
|
||||
default=None,
|
||||
help='email address of user whose account is being '
|
||||
'accessed')
|
||||
parser.add_option('--quiet',
|
||||
action='store_true',
|
||||
default=False,
|
||||
dest='quiet',
|
||||
help='Omit verbose descriptions and only print '
|
||||
'machine-readable outputs.')
|
||||
return parser
|
||||
|
||||
|
||||
# The URL root for accessing Google Accounts.
|
||||
GOOGLE_ACCOUNTS_BASE_URL = 'https://accounts.google.com'
|
||||
|
||||
|
||||
# Hardcoded dummy redirect URI for non-web apps.
|
||||
REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob'
|
||||
|
||||
|
||||
def AccountsUrl(command):
|
||||
"""Generates the Google Accounts URL.
|
||||
|
||||
Args:
|
||||
command: The command to execute.
|
||||
|
||||
Returns:
|
||||
A URL for the given command.
|
||||
"""
|
||||
return '%s/%s' % (GOOGLE_ACCOUNTS_BASE_URL, command)
|
||||
|
||||
|
||||
def UrlEscape(text):
|
||||
# See OAUTH 5.1 for a definition of which characters need to be escaped.
|
||||
return urllib.parse.quote(text, safe='~-._')
|
||||
|
||||
|
||||
def UrlUnescape(text):
|
||||
# See OAUTH 5.1 for a definition of which characters need to be escaped.
|
||||
return urllib.parse.unquote(text)
|
||||
|
||||
|
||||
def FormatUrlParams(params):
|
||||
"""Formats parameters into a URL query string.
|
||||
|
||||
Args:
|
||||
params: A key-value map.
|
||||
|
||||
Returns:
|
||||
A URL query string version of the given parameters.
|
||||
"""
|
||||
param_fragments = []
|
||||
for param in sorted(iter(params.items()), key=lambda x: x[0]):
|
||||
param_fragments.append('%s=%s' % (param[0], UrlEscape(param[1])))
|
||||
return '&'.join(param_fragments)
|
||||
|
||||
|
||||
def GeneratePermissionUrl(client_id, scope='https://mail.google.com/'):
|
||||
"""Generates the URL for authorizing access.
|
||||
|
||||
This uses the "OAuth2 for Installed Applications" flow described at
|
||||
https://developers.google.com/accounts/docs/OAuth2InstalledApp
|
||||
|
||||
Args:
|
||||
client_id: Client ID obtained by registering your app.
|
||||
scope: scope for access token, e.g. 'https://mail.google.com'
|
||||
Returns:
|
||||
A URL that the user should visit in their browser.
|
||||
"""
|
||||
params = {}
|
||||
params['client_id'] = client_id
|
||||
params['redirect_uri'] = REDIRECT_URI
|
||||
params['scope'] = scope
|
||||
params['response_type'] = 'code'
|
||||
return '%s?%s' % (AccountsUrl('o/oauth2/auth'),
|
||||
FormatUrlParams(params))
|
||||
|
||||
|
||||
def AuthorizeTokens(client_id, client_secret, authorization_code):
|
||||
"""Obtains OAuth access token and refresh token.
|
||||
|
||||
This uses the application portion of the "OAuth2 for Installed Applications"
|
||||
flow at https://developers.google.com/accounts/docs/OAuth2InstalledApp#handlingtheresponse
|
||||
|
||||
Args:
|
||||
client_id: Client ID obtained by registering your app.
|
||||
client_secret: Client secret obtained by registering your app.
|
||||
authorization_code: code generated by Google Accounts after user grants
|
||||
permission.
|
||||
Returns:
|
||||
The decoded response from the Google Accounts server, as a dict. Expected
|
||||
fields include 'access_token', 'expires_in', and 'refresh_token'.
|
||||
"""
|
||||
params = {}
|
||||
params['client_id'] = client_id
|
||||
params['client_secret'] = client_secret
|
||||
params['code'] = authorization_code
|
||||
params['redirect_uri'] = REDIRECT_URI
|
||||
params['grant_type'] = 'authorization_code'
|
||||
request_url = AccountsUrl('o/oauth2/token')
|
||||
|
||||
response = urllib.request.urlopen(request_url, urllib.parse.urlencode(params).encode()).read()
|
||||
return json.loads(response)
|
||||
|
||||
|
||||
def RefreshToken(client_id, client_secret, refresh_token):
|
||||
"""Obtains a new token given a refresh token.
|
||||
|
||||
See https://developers.google.com/accounts/docs/OAuth2InstalledApp#refresh
|
||||
|
||||
Args:
|
||||
client_id: Client ID obtained by registering your app.
|
||||
client_secret: Client secret obtained by registering your app.
|
||||
refresh_token: A previously-obtained refresh token.
|
||||
Returns:
|
||||
The decoded response from the Google Accounts server, as a dict. Expected
|
||||
fields include 'access_token', 'expires_in', and 'refresh_token'.
|
||||
"""
|
||||
params = {}
|
||||
params['client_id'] = client_id
|
||||
params['client_secret'] = client_secret
|
||||
params['refresh_token'] = refresh_token
|
||||
params['grant_type'] = 'refresh_token'
|
||||
request_url = AccountsUrl('o/oauth2/token')
|
||||
|
||||
response = urllib.request.urlopen(request_url, urllib.parse.urlencode(params).encode()).read()
|
||||
return json.loads(response)
|
||||
|
||||
|
||||
def GenerateOAuth2String(username, access_token, base64_encode=True):
|
||||
"""Generates an IMAP OAuth2 authentication string.
|
||||
|
||||
See https://developers.google.com/google-apps/gmail/oauth2_overview
|
||||
|
||||
Args:
|
||||
username: the username (email address) of the account to authenticate
|
||||
access_token: An OAuth2 access token.
|
||||
base64_encode: Whether to base64-encode the output.
|
||||
|
||||
Returns:
|
||||
The SASL argument for the OAuth2 mechanism.
|
||||
"""
|
||||
auth_string = 'user=%s\1auth=Bearer %s\1\1' % (username, access_token)
|
||||
if base64_encode:
|
||||
auth_string = base64.b64encode(bytes(auth_string, 'utf-8'))
|
||||
return auth_string
|
||||
|
||||
|
||||
def TestImapAuthentication(user, auth_string):
|
||||
"""Authenticates to IMAP with the given auth_string.
|
||||
|
||||
Prints a debug trace of the attempted IMAP connection.
|
||||
|
||||
Args:
|
||||
user: The Gmail username (full email address)
|
||||
auth_string: A valid OAuth2 string, as returned by GenerateOAuth2String.
|
||||
Must not be base64-encoded, since imaplib does its own base64-encoding.
|
||||
"""
|
||||
print()
|
||||
imap_conn = imaplib.IMAP4_SSL('imap.gmail.com')
|
||||
imap_conn.debug = 4
|
||||
imap_conn.authenticate('XOAUTH2', lambda x: auth_string)
|
||||
imap_conn.select('INBOX')
|
||||
|
||||
|
||||
def TestSmtpAuthentication(user, auth_string):
|
||||
"""Authenticates to SMTP with the given auth_string.
|
||||
|
||||
Args:
|
||||
user: The Gmail username (full email address)
|
||||
auth_string: A valid OAuth2 string, not base64-encoded, as returned by
|
||||
GenerateOAuth2String.
|
||||
"""
|
||||
print()
|
||||
smtp_conn = smtplib.SMTP('smtp.gmail.com', 587)
|
||||
smtp_conn.set_debuglevel(True)
|
||||
smtp_conn.ehlo('test')
|
||||
smtp_conn.starttls()
|
||||
smtp_conn.docmd('AUTH', 'XOAUTH2 ' + base64.b64encode(auth_string))
|
||||
|
||||
|
||||
def RequireOptions(options, *args):
|
||||
missing = [arg for arg in args if getattr(options, arg) is None]
|
||||
if missing:
|
||||
print('Missing options: %s' % ' '.join(missing), file=sys.stderr)
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
def main(argv):
|
||||
options_parser = SetupOptionParser()
|
||||
(options, args) = options_parser.parse_args()
|
||||
if options.refresh_token:
|
||||
RequireOptions(options, 'client_id', 'client_secret')
|
||||
response = RefreshToken(options.client_id, options.client_secret,
|
||||
options.refresh_token)
|
||||
if options.quiet:
|
||||
print(response['access_token'])
|
||||
else:
|
||||
print('Access Token: %s' % response['access_token'])
|
||||
print('Access Token Expiration Seconds: %s' % response['expires_in'])
|
||||
elif options.generate_oauth2_string:
|
||||
RequireOptions(options, 'user', 'access_token')
|
||||
oauth2_string = GenerateOAuth2String(options.user, options.access_token)
|
||||
if options.quiet:
|
||||
print(oauth2_string.decode('utf-8'))
|
||||
else:
|
||||
print('OAuth2 argument:\n' + oauth2_string.decode('utf-8'))
|
||||
elif options.generate_oauth2_token:
|
||||
RequireOptions(options, 'client_id', 'client_secret')
|
||||
print('To authorize token, visit this url and follow the directions:')
|
||||
print(' %s' % GeneratePermissionUrl(options.client_id, options.scope))
|
||||
authorization_code = input('Enter verification code: ')
|
||||
response = AuthorizeTokens(options.client_id, options.client_secret,
|
||||
authorization_code)
|
||||
print('Refresh Token: %s' % response['refresh_token'])
|
||||
print('Access Token: %s' % response['access_token'])
|
||||
print('Access Token Expiration Seconds: %s' % response['expires_in'])
|
||||
elif options.test_imap_authentication:
|
||||
RequireOptions(options, 'user', 'access_token')
|
||||
TestImapAuthentication(options.user,
|
||||
GenerateOAuth2String(options.user, options.access_token,
|
||||
base64_encode=False))
|
||||
elif options.test_smtp_authentication:
|
||||
RequireOptions(options, 'user', 'access_token')
|
||||
TestSmtpAuthentication(options.user,
|
||||
GenerateOAuth2String(options.user, options.access_token,
|
||||
base64_encode=False))
|
||||
else:
|
||||
options_parser.print_help()
|
||||
print('Nothing to do, exiting.')
|
||||
return
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
|
@ -1,62 +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
|
||||
- Add customizable mailbox tree in sidebar
|
||||
- Make `dbus` dependency opt-out (feature is `dbus-notifications`)
|
||||
- Implemented JMAP async, search, tagging, syncing
|
||||
- Preserve account order from configuration file
|
||||
- Implemented IMAP `CONDSTORE` support for IMAP cache
|
||||
- Add `timeout` setting for IMAP
|
||||
- Implement TCP keepalive for IMAP
|
||||
- Rewrote email address parsers.
|
||||
- Implement `copy_messages` for maildir
|
||||
- Implement selection with motions
|
||||
|
||||
Fixed
|
||||
- Fixed various problems with IMAP cache
|
||||
- Fixed various problems with IMAP message counts
|
||||
- Fixed various problems with IMAP connection hanging
|
||||
- Fixed IMAP not reconnecting on dropped IDLE connections
|
||||
- Fixed various problems with notmuch backend
|
||||
|
||||
-- Manos Pitsidianakis <epilys@nessuent.xyz> Thu, 24 Sep 2020 18:14:00 +0200
|
||||
meli (0.6.1-1) buster; urgency=low
|
||||
|
||||
* added experimental NNTP backend
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
docs/meli.1
|
||||
docs/meli.conf.5
|
||||
docs/meli-themes.5
|
||||
meli.1
|
||||
meli.conf.5
|
||||
meli-themes.5
|
||||
|
|
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 September 4, 2022
|
||||
.Dt MELI 7
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm meli
|
||||
.Nd Tutorial for the Meli Mail User Agent
|
||||
.Sh SYNOPSIS
|
||||
.Nm
|
||||
.Op ...
|
||||
.Sh DESCRIPTION
|
||||
.Nm
|
||||
is a terminal mail client aiming for extensive and user-frendly configurability.
|
||||
.Bd -literal -offset center
|
||||
^^ .-=-=-=-. ^^
|
||||
^^ (`-=-=-=-=-`) ^^
|
||||
(`-=-=-=-=-=-=-`) ^^ ^^
|
||||
^^ (`-=-=-=-=-=-=-=-`) ^^
|
||||
( `-=-=-=-(@)-=-=-` ) ^^
|
||||
(`-=-=-=-=-=-=-=-=-`) ^^
|
||||
(`-=-=-=-=-=-=-=-=-`) ^^
|
||||
(`-=-=-=-=-=-=-=-=-`)
|
||||
^^ (`-=-=-=-=-=-=-=-=-`) ^^
|
||||
^^ (`-=-=-=-=-=-=-=-`) ^^
|
||||
(`-=-=-=-=-=-=-`) ^^
|
||||
^^ (`-=-=-=-=-`)
|
||||
`-=-=-=-=-` ^^
|
||||
.Ed
|
||||
.Sh INTRODUCTION
|
||||
To quit
|
||||
.Nm
|
||||
press
|
||||
.Shortcut q general quit
|
||||
at any time.
|
||||
To go to the next tab on the right, press
|
||||
.ShortcutPeriod T general next_tab
|
||||
\&.
|
||||
.Pp
|
||||
When launched for the first time,
|
||||
.Nm
|
||||
will search for its configuration directory,
|
||||
.Pa $XDG_CONFIG_HOME/meli/ Ns
|
||||
\&.
|
||||
If it doesn't exist, you will be asked if you want to create one and presented with a sample configuration file
|
||||
.Pq Pa $XDG_CONFIG_HOME/meli/config.toml
|
||||
that includes the basic settings required for setting up accounts allowing you to copy and edit right away.
|
||||
See
|
||||
.Xr meli.conf 5
|
||||
for the available configuration options.
|
||||
.Pp
|
||||
At any time, you may press
|
||||
.Shortcut \&? general toggle_help
|
||||
for a searchable list of all available actions and shortcuts, along with every possible setting and command that your version supports.
|
||||
.Pp
|
||||
Each time a shortcut is mentioned in this document, you will find a parenthesis next to it with the name of the shortcut setting along with its section in the configuration settings so that you can modify it if you wish.
|
||||
.Pp
|
||||
For example, to set the
|
||||
.Em toggle_help
|
||||
shortcut mentioned in the previous paragraph, add the following to your configuration:
|
||||
.Bd -literal -offset center
|
||||
[shortcuts]
|
||||
general.toggle_help = 'F1'
|
||||
.Ed
|
||||
.sp
|
||||
Or alternatively:
|
||||
.Bd -literal -offset center
|
||||
[shortcuts.general]
|
||||
toggle_help = 'F1'
|
||||
.Ed
|
||||
.Sh INTERACTING WITH Nm
|
||||
You will be interacting with
|
||||
.Nm
|
||||
in four primary ways:
|
||||
.Bl -column
|
||||
.It 1.
|
||||
keyboard shortcuts in
|
||||
.Sy NORMAL
|
||||
mode.
|
||||
.It 2.
|
||||
commands with arguments in
|
||||
.Sy COMMAND
|
||||
mode.
|
||||
.It 3.
|
||||
regular text input in text input widgets in
|
||||
.Sy INSERT
|
||||
mode.
|
||||
.It 4.
|
||||
any kind of input that gets passed directly into an embedded terminal in
|
||||
.Sy EMBED
|
||||
mode.
|
||||
.El
|
||||
.Sh MODES
|
||||
.Nm
|
||||
is a modal application, just like
|
||||
.Xr vi 1 Ns
|
||||
\&.
|
||||
This means that pressing the same keys in different modes would yield different results.
|
||||
This allows you to separate how the input is interpreted without the need to focus your input with a mouse.
|
||||
.Bl -tag -width 8n
|
||||
.It NORMAL
|
||||
This is the default mode of
|
||||
.Nm Ns
|
||||
\&.
|
||||
All keyboard shortcuts work in this mode.
|
||||
.It COMMAND
|
||||
Commands are issued in
|
||||
.Sy COMMAND
|
||||
mode, by default started with
|
||||
.Shortcut \&: general enter_command_mode
|
||||
and exited with
|
||||
.Aq Esc
|
||||
key.
|
||||
.It EMBED
|
||||
This is the mode of the embed terminal emulator.
|
||||
To exit an embedded application, issue
|
||||
.Aq Ctrl-C
|
||||
to kill it or
|
||||
.Aq Ctrl-Z
|
||||
to stop the program and follow the instructions on
|
||||
.Nm
|
||||
to exit.
|
||||
.It INSERT
|
||||
This mode is entered when pressing
|
||||
.Aq Enter
|
||||
on a cursor selected text input field, and it captures all input as text input.
|
||||
It is exited with the
|
||||
.Aq Esc
|
||||
key.
|
||||
.El
|
||||
.Sh ACTIVE SHORTCUTS POPUP
|
||||
By pressing
|
||||
.Shortcut \&? general toggle_help
|
||||
at any time, the shortcuts popup display status gets toggled.
|
||||
You can find all valid shortcuts for the current UI state you are in.
|
||||
.Bd -literal -offset center
|
||||
┌─shortcuts──Press ? to close────────────────────────────────┐
|
||||
│ ▀│
|
||||
│ use COMMAND "search" to find shortcuts █│
|
||||
│ Use Up, Down, Left, Right to scroll. █│
|
||||
│ █│
|
||||
│ pager █│
|
||||
│ █│
|
||||
│ PageDown page_down █│
|
||||
│ PageUp page_up │
|
||||
│ j scroll_down │
|
||||
│ k scroll_up │
|
||||
│ │
|
||||
│ view mail │
|
||||
│ │
|
||||
│ c add_addresses_to_contacts │
|
||||
│ e edit │
|
||||
│ u toggle_url_mode │
|
||||
│ a open_attachment │
|
||||
│ m open_mailcap │
|
||||
│ R reply │
|
||||
│ C-r reply_to_author │
|
||||
│ C-g reply_to_all │
|
||||
│ C-f forward │
|
||||
│ M-r view_raw_source │
|
||||
│ h toggle_expand_headers ▄│
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
.Ed
|
||||
.Bd -ragged -offset 3n
|
||||
.Em Shows\ active\ shortcuts\ in\ order\ of\ the\ widget\ hierarchy\&.
|
||||
.Ed
|
||||
.Sh MAIN VIEW
|
||||
.Bd -literal -offset center
|
||||
┌───────────────────────┐
|
||||
├────┼──────────────────┤
|
||||
│___ │ ___________ │
|
||||
│ _ │ _______________ │
|
||||
│ _ │__________________│
|
||||
│ _ │ ___________ │
|
||||
│ │ _____ │
|
||||
│ │ │
|
||||
└────┴──────────────────┘
|
||||
.Ed
|
||||
.Bd -ragged -offset 3n
|
||||
.Em The\ main\ view's\ layout\&.
|
||||
.Ed
|
||||
.sp
|
||||
This is the view you will spend more time with in
|
||||
.Nm Ns
|
||||
\&.
|
||||
.Pp
|
||||
Press
|
||||
.Shortcut ` listing toggle_menu_visibility
|
||||
to toggle the sidebars visibility.
|
||||
.Pp
|
||||
Press
|
||||
.Shortcut Left listing focus_right
|
||||
to switch focus on the sidebar menu.
|
||||
Press
|
||||
.Shortcut Right listing focus_left
|
||||
to switch focus on the e-mail list.
|
||||
.Pp
|
||||
On the e-mail list, press
|
||||
.Shortcut k listing scroll_up
|
||||
to scroll up, and
|
||||
.Shortcut j listing scroll_down
|
||||
to scroll down.
|
||||
Press
|
||||
.Shortcut Enter listing open_entry
|
||||
to open an e-mail entry and
|
||||
.Shortcut i listing exit_entry
|
||||
to exit it.
|
||||
.Bd -ragged
|
||||
.Sy The sidebar\&.
|
||||
.Ed
|
||||
.Bd -literal -offset center
|
||||
┌─────────────┉┉┉┉┉✂
|
||||
│ mail▐ contact li✂
|
||||
│personal account ✂
|
||||
│ 0 INBOX ✂
|
||||
│ 1 ┣━Sent ✂
|
||||
│ 2 ┣━Lists ✂
|
||||
│ 3 ┃ ┣━meli-dev ✂
|
||||
│ 4 ┃ ┗━meli ✂
|
||||
│ 5 ┣━Drafts ✂
|
||||
│ 6 ┣━Trash ✂
|
||||
│ 7 ┗━foobar ✂
|
||||
┇ 8 Trash ✂
|
||||
✂ ✂ ✂ ✂ ✂ ✂ ✂ ✂ ✂ ✂
|
||||
.Ed
|
||||
.sp
|
||||
Press
|
||||
.Shortcut k listing scroll_up
|
||||
to scroll up, and
|
||||
.Shortcut j listing scroll_down
|
||||
to scroll down.
|
||||
.Pp
|
||||
Press
|
||||
.Shortcut Enter listing open_mailbox
|
||||
to open an entry (either a mailbox or an account name).
|
||||
Entering an account name will show you a page with details about the account and its network connection, depending on the backend.
|
||||
.Pp
|
||||
While focused in the sidebar, you can
|
||||
.Dq collapse
|
||||
a mailbox tree, if it has children, and you can open it with
|
||||
.ShortcutPeriod Space listing toggle_mailbox_collapse
|
||||
\&.
|
||||
You can have mailbox trees collapsed on startup by default by setting a mailbox's
|
||||
.Ic collapsed
|
||||
setting to
|
||||
.Em true Ns
|
||||
\&.
|
||||
See
|
||||
.Xr meli.conf 5 section MAILBOXES
|
||||
for details.
|
||||
.Pp
|
||||
You can increase the sidebar's width with
|
||||
.Shortcut Ctrl-p listing increase_sidebar
|
||||
and decrease with
|
||||
.ShortcutPeriod Ctrl-o listing decrease_sidebar
|
||||
\&.
|
||||
.Bd -ragged
|
||||
.Sy The status bar.
|
||||
.Ed
|
||||
.Bd -literal -offset center
|
||||
┌────────────────────────────────────────────────────┈┈
|
||||
│NORMAL | Mailbox: Inbox, Messages: 25772, New: 3006
|
||||
└────────────────────────────────────────────────────┈┈
|
||||
.Ed
|
||||
.Pp
|
||||
The status bar shows which mode you are, and the status message of the current view.
|
||||
In the pictured example, it shows the status of a mailbox called
|
||||
.Dq Inbox
|
||||
with lots of e-mails.
|
||||
.Bd -ragged
|
||||
.Sy The number modifier buffer.
|
||||
.Ed
|
||||
.Bd -literal -offset center
|
||||
┈┈────────────┐
|
||||
12 │
|
||||
┈┈────────────┘
|
||||
.Ed
|
||||
.Pp
|
||||
Some commands may accept a number modifier.
|
||||
.Tg number-modifier
|
||||
For example, scroll down commands can receive a multiplier
|
||||
.Em n
|
||||
to scroll down
|
||||
.Em n
|
||||
entries.
|
||||
Another use of the number buffer is opening URLs inside the pager.
|
||||
See
|
||||
.Sx PAGER
|
||||
for an explanation of interacting with URLs in e-mails.
|
||||
.Pp
|
||||
Pressing numbers in
|
||||
.Sy NORMAL
|
||||
mode will populate this buffer.
|
||||
To erase it, press the
|
||||
.Aq Esc
|
||||
key.
|
||||
.Sh MAIL LIST
|
||||
There are four different list styles:
|
||||
.Bl -hyphen -compact
|
||||
.It
|
||||
.Qq plain
|
||||
which shows one line per e-mail.
|
||||
.It
|
||||
.Qq threaded
|
||||
which shows a threaded view with drawn tree structure.
|
||||
.It
|
||||
.Qq compact
|
||||
which shows one line per thread which can include multiple e-mails.
|
||||
.It
|
||||
.Qq conversations
|
||||
which shows more than one line per thread which can include multiple e-mails with more details about the thread.
|
||||
.El
|
||||
.Bd -ragged
|
||||
.Sy Plain view\&.
|
||||
.Ed
|
||||
.Bd -literal -offset center
|
||||
│42 Fri, 02 Sep 2022 19:51 xxxxxxxxxxxxx < [PATCH 3/8] │
|
||||
│43 Fri, 02 Sep 2022 19:51 xxxxxxxxxxxxx < [PATCH 2/8] │
|
||||
│44 Fri, 02 Sep 2022 19:51 xxxxxxxxxxxxx < [PATCH 1/8] │
|
||||
|45 Fri, 02 Sep 2022 19:51 xxxxxxxxxxxxx < [PATCH 0/8] |
|
||||
│46 Fri, 02 Sep 2022 18:18 xxxxxxxx <xxxxx Re: [PATCH 3│
|
||||
.Ed
|
||||
.Bd -ragged
|
||||
.Sy Threaded view\&.
|
||||
.Ed
|
||||
.Bd -literal -offset center
|
||||
│12 9 hours ago xxxxxxxxxxxxxxx [PATCH v3 0│
|
||||
│13 9 hours ago xxxxxxxxxxxxxxx ├─>[PATCH │
|
||||
│14 9 hours ago xxxxxxxxxxxxxxx ├─>[PATCH │
|
||||
|15 9 hours ago xxxxxxxxxxxxxxx ├─>[PATCH |
|
||||
│16 9 hours ago xxxxxxxxxxxxxxx ├─>[PATCH │
|
||||
│17 9 hours ago xxxxxxxxxxxxxxx └─>[PATCH │
|
||||
│18 2022-08-23 01:23:51 xxxxxxxxxxxxxxx [RFC v4 00/│
|
||||
│19 2022-08-23 01:23:52 xxxxxxxxxxxxxxx ├─>[RFC v4│
|
||||
|20 2022-08-30 10:30:16 xxxxxxxxxxxxxxx │ └─> |
|
||||
│21 6 days ago xxxxxxxxxxxxxxx │ └─> │
|
||||
│22 2022-08-23 01:23:53 xxxxxxxxxxxxxxx ├─>[RFC v4│
|
||||
.Ed
|
||||
.Bd -ragged
|
||||
.Sy Compact view\&.
|
||||
.Ed
|
||||
.Bd -literal -offset center
|
||||
│18 2022-…:38 xxxxxxxxxxxxxxx [PATCH v3 3/3] u…_l() (2) │
|
||||
|19 2022-…:49 xxxxxxxxxxxxxxx [PATCH v8 0/7] A…e (3) |
|
||||
│20 2022-…:10 xxxxxxxxxxxxxxx [PATCH v8 2/7] f…s (2) │
|
||||
│21 2022-…:38 xxxxxxxxxxxxxxx [PATCH v8 3/7] b…s (2) │
|
||||
│22 2022-…:53 xxxxxxxxxxxxxxx [PATCH v6 00/10] p…g (31) │
|
||||
.Ed
|
||||
.Bd -ragged
|
||||
.Sy Conversations view\&.
|
||||
.Ed
|
||||
.Bd -literal -offset center
|
||||
│[PATCH v2] xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (5) │
|
||||
|1 day ago▁▁▁▁xxxxxxxxxxxxx <xxxxxxxxxxxxx@xxxxxxxxxx>, xxxxx│
|
||||
│ |
|
||||
│[PATCH v2 0/8] xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx│
|
||||
│1 day ago▁▁▁▁xxxxxxxxxxxxxxx <xxxxxxxxxx@xxxxxxxxxxxxxx>, xx│
|
||||
| │
|
||||
│[PATCH 0/2] xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (4) |
|
||||
│2 days ago▁▁▁▁xxxxxxxxxxxxxxxx <xxxxxxxx@xxxxxxxxxxx>, xxxxx│
|
||||
│ │
|
||||
│[PATCH 0/8] xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (12) │
|
||||
│2 days ago▁▁▁▁xxxxxxxxxxxxx <xxxxxxxx@xxxxxxxxxx>, xxxxxxxxx│
|
||||
.Ed
|
||||
.sp
|
||||
.sp
|
||||
.Sy Performing actions on entries and/or selections\&.
|
||||
.Pp
|
||||
Press
|
||||
.Shortcut v listing select_entry
|
||||
to toggle the selection of a single entry.
|
||||
.Qq select_entry
|
||||
can be prefixed by a number modifier and affixed by a scrolling motion (up or down) to select multiple entries.
|
||||
.Tg number-modifier
|
||||
Simple set operations can be performed on a selection with these shortcut modifiers:
|
||||
.sp
|
||||
.Bl -hyphen -compact
|
||||
.It
|
||||
Union modifier:
|
||||
.Shortcut Ctrl-u listing union_modifier
|
||||
.It
|
||||
Difference modifier:
|
||||
.Shortcut Ctrl-d listing diff_modifier
|
||||
.It
|
||||
Intersection modifier:
|
||||
.Shortcut Ctrl-i listing intersection_modifier
|
||||
.El
|
||||
.Pp
|
||||
To set an entry as
|
||||
.Qq read
|
||||
\&, use the
|
||||
.Shortcut n listing set_seen
|
||||
shortcut.
|
||||
To set an entry as
|
||||
.Qq unread
|
||||
\&, use the command
|
||||
.Command set unseen
|
||||
.sp
|
||||
which also has its complement
|
||||
.Command set seen
|
||||
.sp
|
||||
action.
|
||||
.Pp
|
||||
For e-mail backends that support tags
|
||||
.Po
|
||||
like
|
||||
.Qq IMAP
|
||||
or
|
||||
.Qq notmuch Ns
|
||||
.Pc
|
||||
you can use the following commands on entries and selections to modify them:
|
||||
.Command tag add TAG
|
||||
.Command tag remove TAG
|
||||
.sp
|
||||
(see
|
||||
.Xr meli.conf 5 TAGS Ns
|
||||
, settings
|
||||
.Ic colors
|
||||
and
|
||||
.Ic ignore_tags
|
||||
for how to set tag colors and tag visibility)
|
||||
.Sh PAGER
|
||||
You can open an e-mail entry by pressing
|
||||
.ShortcutPeriod Enter listing open_entry
|
||||
\&. This brings up the e-mail view with the e-mail content inside a pager.
|
||||
.Bd -literal -offset center
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│Date: Sat, 21 May 2022 16:16:11 +0300 ▀│
|
||||
│From: Narrator <narrator@example.com> █│
|
||||
│To: Stanley <427@example.com> █│
|
||||
│Subject: The e-mail ending █│
|
||||
│Message-ID: <gambheerata@example.com> █│
|
||||
│ █│
|
||||
│The story, and the choices, or what have you, and therefore█│
|
||||
│by becoming it is! So on and so forth, until inevitably, we │
|
||||
│all until the end of time. At which time, everything all at │
|
||||
│once, so now you see? Blah, blah, blah, rah, rah, rah... │
|
||||
│We've eaten too much and it can't be just yet. No, no! │
|
||||
│Until two-hundred and forty-five! But the logic of │
|
||||
│elimination, working backwards, the deduction therefore │
|
||||
│becomes impossible to manufacture. It went on for nearly │
|
||||
│ten thousand years, until just yesterday. Here and there, │
|
||||
│forward and back, and never a moment before lunchtime. It │
|
||||
│can't be! It's the only thing there is! How many billions │
|
||||
│left until so much more than forever ago! Which is why I │
|
||||
│say: │
|
||||
│ │
|
||||
│The story, and the choices, or what have you, and therefore │
|
||||
│by becoming it is! So on and so forth, until inevitably, we▄│
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
.Ed
|
||||
.Bd -ragged -offset 3n
|
||||
.Em The\ pager\ displaying\ an\ e-mail\&.
|
||||
.Ed
|
||||
.Pp
|
||||
The pager is simple to use.
|
||||
Scroll with the following:
|
||||
.Bl -hang -width 27n
|
||||
.It Go to next pager page
|
||||
.Shortcut PageDown pager page_down
|
||||
.It Go to previous pager page
|
||||
.Shortcut PageUp pager page_up
|
||||
.It Scroll down pager.
|
||||
.Shortcut j pager scroll_down
|
||||
.It Scroll up pager.
|
||||
.Shortcut k pager scroll_up
|
||||
.El
|
||||
.sp
|
||||
All scrolling shortcuts can be prefixed with a number modifier
|
||||
.Tg number-modifier
|
||||
which will act as a multiplier.
|
||||
.Pp
|
||||
The pager can enter a special
|
||||
.Em url
|
||||
mode which will prefix all detected hyperlinks and e-mail addresses with a number inside square brackets
|
||||
.ShortcutPeriod u pager toggle_url_mode
|
||||
\&.
|
||||
Writing down a chosen number as a number modifier
|
||||
.Tg number-modifier
|
||||
and pressing
|
||||
.Shortcut g envelope_view go_to_url
|
||||
will attempt to open the link with the system's default open command
|
||||
.Po
|
||||
.Xr xdg-open 1
|
||||
in supported OSes,
|
||||
and
|
||||
.Xr open 1
|
||||
on MacOS
|
||||
.Pc Ns
|
||||
\&.
|
||||
To override with a custom launcher, see
|
||||
.Qo
|
||||
.Li pager
|
||||
.Qc
|
||||
configuration setting
|
||||
.Qo
|
||||
.Li url_launcher
|
||||
.Qc
|
||||
.Po
|
||||
see
|
||||
.Xr meli.conf 5 PAGER
|
||||
for more details
|
||||
.Pc Ns
|
||||
\&.
|
||||
.Sh MAIL VIEW
|
||||
Other things you can do when viewing e-mail:
|
||||
.Bl -bullet -compact
|
||||
.It
|
||||
Most importantly, you can exit the mail view with:
|
||||
.Shortcut i listing exit_entry
|
||||
.It
|
||||
Add addresses from the e-mail headers to contacts:
|
||||
.Shortcut c envelope_view add_addresses_to_contacts
|
||||
.It
|
||||
Open an attachment by entering its index as a number modifier and pressing:
|
||||
.Tg number-modifier
|
||||
.Shortcut a envelope_view open_attachment
|
||||
.It
|
||||
Open an attachment by its
|
||||
.Xr mailcap 4
|
||||
entry by entering its index as a number modifier and pressing:
|
||||
.Shortcut m envelope_view open_mailcap
|
||||
.It
|
||||
Reply to envelope:
|
||||
.Shortcut R envelope_view reply
|
||||
.It
|
||||
Reply to author:
|
||||
.Shortcut Ctrl-r envelope_view reply_to_author
|
||||
.It
|
||||
Reply to all/Reply to list/Follow up:
|
||||
.Shortcut Ctrl-g envelope_view reply_to_all
|
||||
.It
|
||||
Forward email:
|
||||
.Shortcut Ctrl-f envelope_view forward
|
||||
.It
|
||||
Expand extra headers: (References and others)
|
||||
.Shortcut h envelope_view toggle_expand_headerk
|
||||
.It
|
||||
View envelope source in a pager: (toggles between raw and decoded source)
|
||||
.Shortcut M-r envelope_view view_raw_source
|
||||
.It
|
||||
Return to envelope_view if viewing raw source or attachment:
|
||||
.Shortcut r envelope_view return_to_normal_view
|
||||
.El
|
||||
.Sh COMPOSING
|
||||
To compose an e-mail, you can either start with an empty draft by pressing
|
||||
.Shortcut m listing new_mail
|
||||
which opens a composer view in a new tab.
|
||||
To reply to a specific e-mail, when in envelope view you can select the specific action you want to take:
|
||||
.sp
|
||||
.Bl -bullet -compact
|
||||
.It
|
||||
Reply to envelope.
|
||||
.Shortcut R envelope_view reply
|
||||
.It
|
||||
Reply to author.
|
||||
.Shortcut Ctrl-r envelope_view reply_to_author
|
||||
.It
|
||||
Reply to all.
|
||||
.Shortcut Ctrl-g envelope_view reply_to_all
|
||||
.El
|
||||
.sp
|
||||
To launch your editor, press
|
||||
.ShortcutPeriod e composing edit_mail
|
||||
\&.
|
||||
To send your draft, press
|
||||
.ShortcutPeriod s composing send_mail
|
||||
\&.
|
||||
To save the draft without submission, enter the command
|
||||
.Command close
|
||||
.sp
|
||||
and select
|
||||
.Qq save as draft Ns
|
||||
\&.
|
||||
You can return to the draft by going to your
|
||||
.Qq Drafts
|
||||
mailbox and selecting
|
||||
.ShortcutPeriod e envelope_view edit_mail
|
||||
\&.
|
||||
.Bd -literal -offset center
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ mail▐ contact list ▐ composing ▍███████████████████████│
|
||||
│ COMPOSING MESSAGE │
|
||||
│ Date Mon, 05 Sep 2022 17:49:19 +0300 │
|
||||
│ From myself <myself@example.com>░░░░ │
|
||||
│ To friend <myfriend@example.com>░░ │
|
||||
│ Cc ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
|
||||
│ Bcc ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
|
||||
│ Subject This is my subject!░░░░░░░░░░░░ │
|
||||
│ │
|
||||
│ Hello friend!░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
|
||||
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
|
||||
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
|
||||
│ │
|
||||
│ ☐ don't sign │
|
||||
│ ☐ don't encrypt │
|
||||
│ no attachments │
|
||||
│ │
|
||||
│NORMAL | Mailbox: Inbox, Messages: 25772, New: 3006 │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
.Ed
|
||||
.Bd -ragged -offset 3n
|
||||
.Em The\ lightly\ highlighted\ cells\ represent\ text\ input\ fields\&.
|
||||
.Ed
|
||||
.sp
|
||||
If you enable the embed terminal option, you can launch your terminal editor of choice when you press
|
||||
.Ic edit_mail Ns
|
||||
\&.
|
||||
.Bd -literal -offset center
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ mail▐ contact list ▐ composing ▍███████████████████████│
|
||||
│ ╓COMPOSING MESSAGE┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╖ │
|
||||
│ ║ p/v/f/h/5/T/m/07f56b6e-ec09-49d9-b8d8-f0c5a81e7826 ║ │
|
||||
│ ║ 7 Date: Mon, 05 Sep 2022 18:43:10 +0300 ║ │
|
||||
│ ║ 6 From: Mister Cardholder <mrholder@example.com> ║ │
|
||||
│ ║ 5 To: ║ │
|
||||
│ ║ 4 Cc: ║ │
|
||||
│ ║ 3 Bcc: ║ │
|
||||
│ ║ 2 Subject: ║ │
|
||||
│ ║ 1 User-Agent: meli 0.7.2 ║ │
|
||||
│ ║8 █ ║ │
|
||||
│ ║~ ║ │
|
||||
│ ║~ ║ │
|
||||
│ ║~ ║ │
|
||||
│ ║~ ║ │
|
||||
│ ║ N… <6e-ec09-49d9-b8d8-f0c5a81e7826 100% ㏑:8 ℅:1║ │
|
||||
│ ╚════════════════════════════════════════════════════╝ │
|
||||
│ │
|
||||
│ │
|
||||
│ ☐ don't sign │
|
||||
│ ☐ don't encrypt │
|
||||
│ no attachments │
|
||||
│ │
|
||||
│EMBED | Mailbox: Inbox, Messages: 25772, New: 3006 │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
.Ed
|
||||
.Bd -ragged -offset 3n
|
||||
.Bf -emphasis
|
||||
.Xr neovim 1 Ns
|
||||
\ running\ inside\ the\ composing\ tab\&.
|
||||
.Ef
|
||||
The\ double\ line\ border\ annotates\ the\ area\ of\ the\ embedded\ terminal,
|
||||
the\ actual\ embedding\ is\ seamless\&.
|
||||
.Ed
|
||||
.Ss composing mail commands
|
||||
.Bl -tag -width 36n
|
||||
.It Cm add-attachment Ar PATH
|
||||
in composer, add
|
||||
.Ar PATH
|
||||
as an attachment
|
||||
.It Cm add-attachment < Ar CMD Ar ARGS
|
||||
in composer, pipe
|
||||
.Ar CMD Ar ARGS
|
||||
output into an attachment
|
||||
.It Cm add-attachment-file-picker
|
||||
Launch command defined in the configuration value
|
||||
.Ic file_picker_command
|
||||
in
|
||||
.Xr meli.conf 5 TERMINAL
|
||||
.It Cm add-attachment-file-picker < Ar CMD Ar ARGS
|
||||
Launch command
|
||||
.Ar CMD Ar ARGS Ns
|
||||
\&.
|
||||
The command should print file paths in stderr, separated by NULL bytes.
|
||||
.It Cm remove-attachment Ar INDEX
|
||||
remove attachment with given index
|
||||
.It Cm toggle sign
|
||||
toggle between signing and not signing this message.
|
||||
If the gpg invocation fails then the mail won't be sent.
|
||||
See
|
||||
.Xr meli.conf 5 PGP
|
||||
for PGP configuration.
|
||||
.It Cm save-draft
|
||||
saves a copy of the draft in the Draft folder
|
||||
.El
|
||||
.\" TODO add contacts section
|
||||
.Sh THEMES
|
||||
See
|
||||
.Xr meli-themes 5
|
||||
for documentation on how to theme
|
||||
.Nm Ns
|
||||
\&.
|
||||
.Sh SEE ALSO
|
||||
.Xr meli 1 ,
|
||||
.Xr meli.conf 5 ,
|
||||
.Xr meli-themes 5 ,
|
||||
.Xr xdg-open 1 ,
|
||||
.Xr mailcap 5
|
||||
.Sh AUTHORS
|
||||
Copyright 2017-2022
|
||||
.An Manos Pitsidianakis Mt manos@pitsidianak.is
|
||||
Released under the GPL, version 3 or greater.
|
||||
This software carries no warranty of any kind.
|
||||
(See COPYING for full copyright and warranty notices.)
|
||||
.Pp
|
||||
.Lk https://meli.delivery
|
||||
.Lk https://github.com/meli/meli
|
||||
.Lk https://crates.io/crates/meli
|
|
@ -1,70 +0,0 @@
|
|||
[terminal.themes.nord]
|
||||
"theme_default" = { fg = "$nord6", bg = "$nord0", attrs = "Default" }
|
||||
"mail.listing.compact.even" = { fg = "theme_default", bg = "$nord1", attrs = "theme_default" }
|
||||
"mail.listing.compact.odd" = { fg = "theme_default", bg = "$nord2", attrs = "theme_default" }
|
||||
"mail.listing.plain.even" = { fg = "theme_default", bg = "$nord1", attrs = "theme_default" }
|
||||
"mail.listing.plain.odd" = { fg = "theme_default", bg = "$nord2", attrs = "theme_default" }
|
||||
"mail.listing.compact.even_highlighted" = { fg = "$nord0", bg = "$focused_bg", attrs = "theme_default" }
|
||||
"mail.listing.compact.odd_highlighted" = { fg = "$nord0", bg = "$focused_bg", attrs = "theme_default" }
|
||||
"mail.listing.conversations.highlighted" = { fg = "$nord0", bg = "$focused_bg", 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 = "theme_default", bg = "$nord2", attrs = "theme_default" }
|
||||
"mail.listing.plain.even_highlighted" = { fg = "$nord0", bg = "$focused_bg", attrs = "theme_default" }
|
||||
"mail.listing.plain.odd_highlighted" = { fg = "$nord0", bg = "$focused_bg", attrs = "theme_default" }
|
||||
"mail.listing.tag_default" = { fg = "theme_default", bg = "$nord8", attrs = "theme_default" }
|
||||
"mail.sidebar_highlighted" = { fg = "$nord1", bg = "$focused_bg", attrs = "theme_default" }
|
||||
"mail.sidebar_highlighted_account" = { fg = "$nord5", bg = "$nord1", attrs = "theme_default" }
|
||||
"mail.sidebar_highlighted_account_name" = { fg = "mail.sidebar_highlighted_account", bg = "mail.sidebar_highlighted_account", attrs = "theme_default" }
|
||||
"mail.sidebar_highlighted_account_index" = { fg = "mail.sidebar_highlighted_account", bg = "mail.sidebar_highlighted_account", attrs = "theme_default" }
|
||||
"mail.sidebar_highlighted_account_unread_count" = { fg = "mail.sidebar_highlighted_account", bg = "mail.sidebar_highlighted_account", attrs = "theme_default" }
|
||||
"mail.sidebar_highlighted_index" = { fg = "mail.sidebar_index", bg = "mail.sidebar_highlighted", attrs = "theme_default" }
|
||||
"mail.sidebar_highlighted_unread_count" = { fg = "mail.sidebar_highlighted", bg = "mail.sidebar_highlighted", attrs = "theme_default" }
|
||||
"mail.sidebar" = { fg = "$nord5", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.sidebar_account_name" = { fg = "$nord5", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.sidebar_index" = { fg = "$nord1", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.sidebar_unread_count" = { fg = "$nord1", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.view.body" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.view.headers" = { fg = "$nord9", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.a" = { fg = "theme_default", bg = "$nord11", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.b" = { fg = "theme_default", bg = "$nord12", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.c" = { fg = "theme_default", bg = "$nord13", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.d" = { fg = "theme_default", bg = "$nord14", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.e" = { fg = "theme_default", bg = "$nord15", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.f" = { fg = "theme_default", bg = "$nord13", attrs = "theme_default" }
|
||||
"pager.highlight_search" = { fg = "$nord5", bg = "$nord7", attrs = "Bold" }
|
||||
"pager.highlight_search_current" = { fg = "$nord7", bg = "$nord10", attrs = "Bold" }
|
||||
"status.bar" = { fg = "$nord5", bg = "$nord3", attrs = "theme_default" }
|
||||
"status.notification" = { fg = "$nord5", bg = "$nord3", attrs = "theme_default" }
|
||||
"tab.bar" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"tab.focused" = { fg = "$nord1", bg = "$focused_bg", attrs = "theme_default" }
|
||||
"tab.unfocused" = { fg = "$nord4", bg = "$unfocused_bg", attrs = "theme_default" }
|
||||
"widgets.form.field" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"widgets.form.highlighted" = { fg = "theme_default", bg = "$nord2", attrs = "theme_default" }
|
||||
"widgets.form.label" = { fg = "theme_default", bg = "theme_default", attrs = "Bold" }
|
||||
"widgets.list.header" = { fg = "theme_default", bg = "theme_default", attrs = "Bold" }
|
||||
"widgets.options.highlighted" = { fg = "theme_default", bg = "$nord2", attrs = "theme_default" }
|
||||
|
||||
[terminal.themes.nord.color_aliases]
|
||||
nord0 = "#2e3440"
|
||||
nord1 = "#3b4252"
|
||||
nord2 = "#434c5e"
|
||||
nord3 = "#4c566a"
|
||||
# snow storm
|
||||
nord4 = "#d8dee9"
|
||||
nord5 = "#e5e9f0"
|
||||
nord6 = "#eceff4"
|
||||
# frost
|
||||
nord7 = "#8fbcbb"
|
||||
nord8 = "#88c0d0"
|
||||
nord9 = "#81a1c1"
|
||||
nord10 = "#5e81ac"
|
||||
# aurora
|
||||
nord11 = "#bf616a"
|
||||
nord12 = "#d08770"
|
||||
nord13 = "#ebcb8b"
|
||||
nord14 = "#a3be8c"
|
||||
nord15 = "#b48ead"
|
||||
# semantics
|
||||
focused_bg = "$nord8"
|
||||
unfocused_bg = "$nord3"
|
|
@ -1,69 +0,0 @@
|
|||
[terminal.themes.sail]
|
||||
color_aliases = { "unseen_fg" = "theme_default", "unseen_bg" = "theme_default", "sea" = "#91C7FF", "dimmed_text" = "#afbec5", "dimmed_bg" = "Grey78", "header" = "#edeff1" }
|
||||
"theme_default" = { fg = "#37474f", bg = "White", attrs = "Default" }
|
||||
"mail.listing.attachment_flag" = { fg = "Blue", bg = "theme_default", attrs = "theme_default" }
|
||||
|
||||
"mail.listing.compact.even" = { fg = "$dimmed_text", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.compact.even_highlighted" = { fg = "theme_default", bg = "$dimmed_bg", attrs = "theme_default" }
|
||||
"mail.listing.compact.even_selected" = { fg = "$dimmed_text", bg = "LightCoral", attrs = "theme_default" }
|
||||
"mail.listing.compact.even_unseen" = { fg = "$unseen_fg", bg = "$unseen_bg", attrs = "theme_default" }
|
||||
|
||||
"mail.listing.compact.odd" = { fg = "$dimmed_text", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.listing.compact.odd_highlighted" = { fg = "theme_default", bg = "$dimmed_bg", attrs = "theme_default" }
|
||||
"mail.listing.compact.odd_selected" = { fg = "$dimmed_text", bg = "LightCoral", attrs = "theme_default" }
|
||||
"mail.listing.compact.odd_unseen" = { fg = "$unseen_fg", bg = "$unseen_bg", attrs = "theme_default" }
|
||||
|
||||
"mail.listing.plain.even" = { fg = "mail.listing.compact.even", bg = "mail.listing.compact.even", attrs = "theme_default" }
|
||||
"mail.listing.plain.even_highlighted" = { fg = "mail.listing.compact.even_highlighted", bg = "mail.listing.compact.even_highlighted", attrs = "theme_default" }
|
||||
"mail.listing.plain.even_selected" = { fg = "mail.listing.compact.even_selected", bg = "mail.listing.compact.even_selected", attrs = "theme_default" }
|
||||
"mail.listing.plain.even_unseen" = { fg = "mail.listing.compact.even_unseen", bg = "mail.listing.compact.even_unseen", attrs = "theme_default" }
|
||||
"mail.listing.plain.odd" = { fg = "mail.listing.compact.odd", bg = "mail.listing.compact.odd", attrs = "theme_default" }
|
||||
"mail.listing.plain.odd_highlighted" = { fg = "mail.listing.compact.odd_highlighted", bg = "mail.listing.compact.odd_highlighted", attrs = "theme_default" }
|
||||
"mail.listing.plain.odd_selected" = { fg = "mail.listing.compact.odd_selected", bg = "mail.listing.compact.odd_selected", attrs = "theme_default" }
|
||||
"mail.listing.plain.odd_unseen" = { fg = "mail.listing.compact.odd_unseen", bg = "mail.listing.compact.odd_unseen", attrs = "theme_default" }
|
||||
|
||||
"mail.listing.conversations" = { fg = "$dimmed_text", bg = "theme_default", attrs = "Default" }
|
||||
"mail.listing.conversations.date" = { fg = "mail.listing.conversations", bg = "mail.listing.conversations", attrs = "theme_default" }
|
||||
"mail.listing.conversations.from" = { fg = "mail.listing.conversations", bg = "mail.listing.conversations", attrs = "theme_default" }
|
||||
"mail.listing.conversations.subject" = { fg = "mail.listing.conversations", bg = "mail.listing.conversations", attrs = "theme_default" }
|
||||
"mail.listing.conversations.highlighted" = { fg = "theme_default", bg = "$dimmed_bg", attrs = "theme_default" }
|
||||
"mail.listing.conversations.selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
|
||||
"mail.listing.conversations.unseen" = { fg = "$unseen_fg", bg = "$unseen_bg", attrs = "theme_default" }
|
||||
|
||||
"mail.listing.tag_default" = { fg = "Black", bg = "$dimmed_text", attrs = "theme_default" }
|
||||
"mail.listing.thread_snooze_flag" = { fg = "Red", bg = "theme_default", attrs = "theme_default" }
|
||||
|
||||
"mail.sidebar" = { fg = "$dimmed_text", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.sidebar_highlighted" = { fg = "theme_default", bg = "$dimmed_bg", attrs = "theme_default" }
|
||||
"mail.sidebar_highlighted_account" = { fg = "$dimmed_text", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.sidebar_highlighted_account_name" = { fg = "theme_default", bg = "$header", attrs = "Bold" }
|
||||
"mail.sidebar_account_name" = { fg = "mail.sidebar", bg = "mail.sidebar", attrs = "theme_default" }
|
||||
"mail.sidebar_highlighted_account_index" = { fg = "mail.sidebar_index", bg = "mail.sidebar_highlighted_account", attrs = "theme_default" }
|
||||
"mail.sidebar_highlighted_account_unread_count" = { fg = "mail.sidebar_unread_count", bg = "mail.sidebar_highlighted_account", attrs = "theme_default" }
|
||||
"mail.sidebar_highlighted_index" = { fg = "mail.sidebar_index", bg = "mail.sidebar_highlighted", attrs = "theme_default" }
|
||||
"mail.sidebar_highlighted_unread_count" = { fg = "mail.sidebar_highlighted", bg = "mail.sidebar_highlighted", attrs = "theme_default" }
|
||||
"mail.sidebar_index" = { fg = "mail.sidebar", bg = "mail.sidebar", attrs = "theme_default" }
|
||||
"mail.sidebar_unread_count" = { fg = "$dimmed_text", bg = "theme_default", attrs = "theme_default" }
|
||||
|
||||
"mail.view.body" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.view.headers" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"mail.view.headers_names" = { fg = "theme_default", bg = "theme_default", attrs = "Bold" }
|
||||
"mail.view.thread.indentation.a" = { fg = "theme_default", bg = "#EC633D", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.b" = { fg = "theme_default", bg = "#D347F9", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.c" = { fg = "theme_default", bg = "#317EFB", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.d" = { fg = "theme_default", bg = "#06B8CD", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.e" = { fg = "theme_default", bg = "#93DDB6", attrs = "theme_default" }
|
||||
"mail.view.thread.indentation.f" = { fg = "theme_default", bg = "#68A033", attrs = "theme_default" }
|
||||
|
||||
"pager.highlight_search" = { fg = "White", bg = "Teal", attrs = "Bold" }
|
||||
"pager.highlight_search_current" = { fg = "White", bg = "NavyBlue", attrs = "Bold" }
|
||||
"status.bar" = { fg = "theme_default", bg = "$sea", attrs = "theme_default" }
|
||||
"status.notification" = { fg = "Plum1", bg = "theme_default", attrs = "theme_default" }
|
||||
"tab.bar" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"tab.focused" = { fg = "theme_default", bg = "theme_default", attrs = "Bold" }
|
||||
"tab.unfocused" = { fg = "White", bg = "$sea", attrs = "Bold" }
|
||||
"widgets.form.field" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"widgets.form.highlighted" = { fg = "theme_default", bg = "$dimmed_bg", attrs = "theme_default" }
|
||||
"widgets.form.label" = { fg = "theme_default", bg = "theme_default", attrs = "Bold" }
|
||||
"widgets.list.header" = { fg = "Black", bg = "White", attrs = "Bold" }
|
||||
"widgets.options.highlighted" = { fg = "theme_default", bg = "$dimmed_bg", attrs = "theme_default" }
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 33 KiB |
Binary file not shown.
Before Width: | Height: | Size: 204 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
Before Width: | Height: | Size: 19 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 20 KiB |
Binary file not shown.
Before Width: | Height: | Size: 50 KiB |
File diff suppressed because it is too large
Load Diff
|
@ -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,20 +171,8 @@ In this mode, cursor locations (i.e., currently selected entries/items) will use
|
|||
.It
|
||||
theme_default
|
||||
.It
|
||||
error_message
|
||||
.It
|
||||
email_header
|
||||
.It
|
||||
highlight
|
||||
.It
|
||||
status.bar
|
||||
.It
|
||||
status.command_bar
|
||||
.It
|
||||
status.history
|
||||
.It
|
||||
status.history.hints
|
||||
.It
|
||||
status.notification
|
||||
.It
|
||||
tab.focused
|
||||
|
@ -205,8 +193,6 @@ widgets.options.highlighted
|
|||
.It
|
||||
mail.sidebar
|
||||
.It
|
||||
mail.sidebar_divider
|
||||
.It
|
||||
mail.sidebar_unread_count
|
||||
.It
|
||||
mail.sidebar_index
|
||||
|
@ -263,18 +249,18 @@ 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
|
||||
.It
|
||||
mail.view.headers
|
||||
.It
|
||||
mail.view.headers_names
|
||||
.It
|
||||
mail.view.headers_area
|
||||
.It
|
||||
mail.view.body
|
||||
.It
|
||||
mail.view.thread.indentation.a
|
||||
|
@ -614,7 +600,7 @@ Yellow6:148:_:Grey93:255
|
|||
.Xr meli 1 ,
|
||||
.Xr meli.conf 5
|
||||
.Sh CONFORMING TO
|
||||
TOML Standard v.0.5.0 https://toml.io/en/v0.5.0
|
||||
TOML Standard v.0.5.0 https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.5.0.md
|
||||
.sp
|
||||
https://no-color.org/
|
||||
.Sh AUTHORS
|
|
@ -17,29 +17,6 @@
|
|||
.\" You should have received a copy of the GNU General Public License
|
||||
.\" along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
.\"
|
||||
.de Shortcut
|
||||
.Sm
|
||||
.Aq \\$1
|
||||
\
|
||||
.Po
|
||||
.Em shortcuts.\\$2\&. Ns
|
||||
.Em \\$3
|
||||
.Pc
|
||||
.Sm
|
||||
..
|
||||
.de ShortcutPeriod
|
||||
.Aq \\$1
|
||||
.Po
|
||||
.Em shortcuts.\\$2\&. Ns
|
||||
.Em \\$3
|
||||
.Pc Ns
|
||||
..
|
||||
.de Command
|
||||
.Bd -ragged
|
||||
.Cm \\$*
|
||||
.Ed
|
||||
.sp
|
||||
..
|
||||
.Dd July 29, 2019
|
||||
.Dt MELI 1
|
||||
.Os
|
||||
|
@ -66,13 +43,11 @@ if given, or at
|
|||
.It Cm test-config Op Ar path
|
||||
Test a configuration file for syntax issues or missing options.
|
||||
.It Cm man Op Ar page
|
||||
Print documentation page and exit (Piping to a pager is recommended).
|
||||
Print documentation page and exit (Piping to a pager is recommended.)
|
||||
.It Cm print-default-theme
|
||||
Print default theme keys and values in TOML syntax, to be used as a blueprint.
|
||||
.It Cm print-loaded-themes
|
||||
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
|
||||
|
@ -108,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.
|
||||
|
@ -126,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
|
||||
|
@ -149,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.
|
||||
|
@ -183,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
|
||||
|
@ -192,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
|
||||
|
@ -253,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
|
||||
|
@ -284,8 +268,10 @@ mode and
|
|||
key to exit.
|
||||
.It
|
||||
At any time you may press
|
||||
.Shortcut e composing edit_mail Ns
|
||||
to launch your editor (see
|
||||
.Cm e
|
||||
(shortcut
|
||||
.Ic edit_mail Ns
|
||||
) to launch your editor (see
|
||||
.Xr meli.conf 5 COMPOSING Ns
|
||||
, setting
|
||||
.Ic editor_command
|
||||
|
@ -297,23 +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
|
||||
press Ctrl-z and to resume editing press the
|
||||
.Ic edit_mail
|
||||
command again.
|
||||
command again
|
||||
.Po
|
||||
default
|
||||
.Em e
|
||||
.Pc .
|
||||
.El
|
||||
.Ss Attachments
|
||||
Attachments may be handled with the
|
||||
|
@ -323,12 +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
|
||||
\&.
|
||||
|
@ -376,10 +360,8 @@ is the default mode
|
|||
.It COMMAND
|
||||
commands are issued in
|
||||
.Em COMMAND
|
||||
mode, by default started with
|
||||
.Shortcut \&: general enter_command_mode
|
||||
and exited with
|
||||
.Aq Esc
|
||||
mode, by default started with Space and exited with
|
||||
.Cm Esc
|
||||
key.
|
||||
.It EMBED
|
||||
is the mode of the embed terminal emulator
|
||||
|
@ -413,29 +395,15 @@ where
|
|||
is a mailbox prefixed with the
|
||||
.Ar n
|
||||
number in the side menu for the current account
|
||||
.It Cm toggle thread_snooze
|
||||
.It Cm toggle_thread_snooze
|
||||
don't issue notifications for thread under cursor in thread listing
|
||||
.It Cm search Ar STRING
|
||||
search mailbox with
|
||||
.Ar STRING
|
||||
query.
|
||||
Escape exits search results.
|
||||
.It Cm select Ar STRING
|
||||
select threads matching
|
||||
.Ar STRING
|
||||
query.
|
||||
.It Cm set seen, set unseen
|
||||
Set seen status of message.
|
||||
.It Cm import Ar FILEPATH Ar MAILBOX_PATH
|
||||
Import mail from file into given mailbox.
|
||||
.It Cm copyto, moveto Ar MAILBOX_PATH
|
||||
Copy or move to other mailbox.
|
||||
.It Cm copyto, moveto Ar ACCOUNT Ar MAILBOX_PATH
|
||||
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.
|
||||
key.
|
||||
Escape exits search results
|
||||
.It Cm set read, set unread
|
||||
Set read status of message.
|
||||
.It Cm create-mailbox Ar ACCOUNT Ar MAILBOX_PATH
|
||||
create mailbox with given path.
|
||||
Be careful with backends and separator sensitivity (eg IMAP)
|
||||
|
@ -453,8 +421,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
|
||||
|
@ -473,16 +439,6 @@ as an attachment
|
|||
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
|
||||
|
@ -508,15 +464,6 @@ to
|
|||
.It Cm printenv Ar KEY
|
||||
print environment variable
|
||||
.Ar KEY
|
||||
.It Cm quit
|
||||
Quits
|
||||
.Nm Ns
|
||||
\&.
|
||||
.It Cm reload-config
|
||||
Reloads configuration but only if account configuration is unchanged.
|
||||
Useful if you want to reload some settings without restarting
|
||||
.Nm Ns
|
||||
\&.
|
||||
.El
|
||||
.Sh SHORTCUTS
|
||||
See
|
||||
|
@ -599,72 +546,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
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "melib"
|
||||
version = "0.7.2"
|
||||
version = "0.6.1"
|
||||
authors = ["Manos Pitsidianakis <epilys@nessuent.xyz>"]
|
||||
workspace = ".."
|
||||
edition = "2018"
|
||||
|
@ -8,66 +8,62 @@ build = "build.rs"
|
|||
|
||||
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"]
|
||||
description = "backend mail client library"
|
||||
keywords = ["mail", "mua", "maildir", "imap"]
|
||||
categories = [ "email"]
|
||||
license = "GPL-3.0-or-later"
|
||||
readme = "README.md"
|
||||
|
||||
[lib]
|
||||
name = "melib"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
async-stream = "^0.3"
|
||||
base64 = { version = "^0.13", optional = true }
|
||||
bincode = { version = "^1.3.0", default-features = false }
|
||||
bitflags = "1.0"
|
||||
data-encoding = { version = "2.1.1" }
|
||||
encoding = { version = "0.2.33", default-features = false }
|
||||
flate2 = { version = "1.0.16", optional = true }
|
||||
futures = "0.3.5"
|
||||
crossbeam = "0.7.2"
|
||||
data-encoding = "2.1.1"
|
||||
encoding = "0.2.33"
|
||||
memmap = { version = "0.5.2", optional = true }
|
||||
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"
|
||||
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 }
|
||||
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"
|
||||
bincode = "1.2.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", ] }
|
||||
smol = "1.0.0"
|
||||
smallvec = { version = "^1.4.0", features = ["serde", ] }
|
||||
nix = "0.17.0"
|
||||
rusqlite = {version = "0.24.0", optional = true }
|
||||
|
||||
unicode-segmentation = { version = "1.2.1", default-features = false, optional = true }
|
||||
uuid = { version = "^1", features = ["serde", "v4", "v5"] }
|
||||
xdg = "2.1.0"
|
||||
xdg-utils = "^0.4.0"
|
||||
|
||||
[dev-dependencies]
|
||||
mailin-embedded = { version = "0.7", features = ["rtls"] }
|
||||
stderrlog = "^0.5"
|
||||
libloading = "0.6.2"
|
||||
futures = "0.3.5"
|
||||
smol = "0.1.18"
|
||||
async-stream = "0.2.1"
|
||||
base64 = { version = "0.12.3", optional = true }
|
||||
flate2 = { version = "1.0.16", optional = true }
|
||||
xdg-utils = "0.3.0"
|
||||
|
||||
[features]
|
||||
default = ["unicode_algorithms", "imap_backend", "maildir_backend", "mbox_backend", "vcard", "sqlite3", "smtp", "deflate_compression"]
|
||||
|
||||
debug-tracing = []
|
||||
deflate_compression = ["flate2", ]
|
||||
gpgme = []
|
||||
http = ["isahc"]
|
||||
http-static = ["isahc", "isahc/static-curl"]
|
||||
tls = ["native-tls"]
|
||||
imap_backend = ["tls"]
|
||||
jmap_backend = ["http", "serde_json"]
|
||||
maildir_backend = ["notify"]
|
||||
mbox_backend = ["notify"]
|
||||
maildir_backend = ["notify", "memmap"]
|
||||
mbox_backend = ["notify", "memmap"]
|
||||
notmuch_backend = []
|
||||
smtp = ["tls", "base64"]
|
||||
sqlite3 = ["rusqlite", ]
|
||||
tls = ["native-tls"]
|
||||
unicode_algorithms = ["unicode-segmentation"]
|
||||
vcard = []
|
||||
|
|
|
@ -1,88 +0,0 @@
|
|||
# melib
|
||||
|
||||
[![GitHub license](https://img.shields.io/github/license/meli/meli)](https://github.com/meli/meli/blob/master/COPYING) [![Crates.io](https://img.shields.io/crates/v/melib)](https://crates.io/crates/melib) [![docs.rs](https://docs.rs/melib/badge.svg)](https://docs.rs/melib)
|
||||
|
||||
Library for handling mail.
|
||||
|
||||
## optional features
|
||||
|
||||
| feature flag | dependencies | notes |
|
||||
| ---------------------- | ----------------------------------- | ------------------------ |
|
||||
| `imap_backend` | `native-tls` | |
|
||||
| `deflate_compression` | `flate2` | for use with IMAP |
|
||||
| `jmap_backend` | `isahc`, `native-tls`, `serde_json` | |
|
||||
| `maildir_backend` | `notify` | |
|
||||
| `mbox_backend` | `notify` | |
|
||||
| `notmuch_backend` | `notify` | |
|
||||
| `sqlite` | `rusqlite` | used in IMAP cache |
|
||||
| `unicode_algorithms` | `unicode-segmentation` | linebreaking algo etc |
|
||||
| `vcard` | | vcard parsing |
|
||||
| `gpgme` | | GPG use with libgpgme |
|
||||
| `smtp` | `native-tls`, `base64` | async SMTP communication |
|
||||
|
||||
## Example: Parsing bytes into an `Envelope`
|
||||
|
||||
An `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` types.
|
||||
|
||||
```rust
|
||||
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");
|
||||
```
|
379
melib/build.rs
379
melib/build.rs
|
@ -25,25 +25,15 @@ include!("src/text_processing/types.rs");
|
|||
fn main() -> Result<(), std::io::Error> {
|
||||
#[cfg(feature = "unicode_algorithms")]
|
||||
{
|
||||
const MOD_PATH: &str = "src/text_processing/tables.rs";
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed={}", MOD_PATH);
|
||||
/* Line break tables */
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::io::BufReader;
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
const LINE_BREAK_TABLE_URL: &str =
|
||||
"http://www.unicode.org/Public/UCD/latest/ucd/LineBreak.txt";
|
||||
/* Grapheme width tables */
|
||||
const UNICODE_DATA_URL: &str =
|
||||
"http://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt";
|
||||
const EAW_URL: &str = "http://www.unicode.org/Public/UCD/latest/ucd/EastAsianWidth.txt";
|
||||
const EMOJI_DATA_URL: &str =
|
||||
"https://www.unicode.org/Public/UCD/latest/ucd/emoji/emoji-data.txt";
|
||||
|
||||
let mod_path = Path::new(MOD_PATH);
|
||||
let mod_path = Path::new("src/text_processing/tables.rs");
|
||||
if mod_path.exists() {
|
||||
eprintln!(
|
||||
"{} already exists, delete it if you want to replace it.",
|
||||
|
@ -51,14 +41,18 @@ fn main() -> Result<(), std::io::Error> {
|
|||
);
|
||||
std::process::exit(0);
|
||||
}
|
||||
let mut child = Command::new("curl")
|
||||
.args(&["-o", "-", LINE_BREAK_TABLE_URL])
|
||||
.stdout(Stdio::piped())
|
||||
.stdin(Stdio::null())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()?;
|
||||
let mut tmpdir_path = PathBuf::from(
|
||||
std::str::from_utf8(&Command::new("mktemp").arg("-d").output()?.stdout)
|
||||
.unwrap()
|
||||
.trim(),
|
||||
);
|
||||
tmpdir_path.push("LineBreak.txt");
|
||||
Command::new("curl")
|
||||
.args(&["-o", tmpdir_path.to_str().unwrap(), LINE_BREAK_TABLE_URL])
|
||||
.output()?;
|
||||
|
||||
let buf_reader = BufReader::new(child.stdout.take().unwrap());
|
||||
let file = File::open(&tmpdir_path)?;
|
||||
let buf_reader = BufReader::new(file);
|
||||
|
||||
let mut line_break_table: Vec<(u32, u32, LineBreakClass)> = Vec::with_capacity(3800);
|
||||
for line in buf_reader.lines() {
|
||||
|
@ -75,352 +69,31 @@ fn main() -> Result<(), std::io::Error> {
|
|||
let mut codepoint_iter = chars_str.split("..");
|
||||
|
||||
let first_codepoint: u32 =
|
||||
u32::from_str_radix(codepoint_iter.next().unwrap(), 16).unwrap();
|
||||
u32::from_str_radix(std::dbg!(codepoint_iter.next().unwrap()), 16).unwrap();
|
||||
|
||||
let sec_codepoint: u32 = codepoint_iter
|
||||
.next()
|
||||
.map(|v| u32::from_str_radix(v, 16).unwrap())
|
||||
.map(|v| u32::from_str_radix(std::dbg!(v), 16).unwrap())
|
||||
.unwrap_or(first_codepoint);
|
||||
let class = &tokens[semicolon_idx + 1..semicolon_idx + 1 + 2];
|
||||
line_break_table.push((first_codepoint, sec_codepoint, LineBreakClass::from(class)));
|
||||
}
|
||||
child.wait()?;
|
||||
|
||||
let child = Command::new("curl")
|
||||
.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])
|
||||
.stdout(Stdio::piped())
|
||||
.output()?;
|
||||
|
||||
let eaw_data = String::from_utf8_lossy(&child.stdout);
|
||||
|
||||
let child = Command::new("curl")
|
||||
.args(&["-o", "-", EMOJI_DATA_URL])
|
||||
.stdout(Stdio::piped())
|
||||
.output()?;
|
||||
|
||||
let emoji_data = String::from_utf8_lossy(&child.stdout);
|
||||
|
||||
const MAX_CODEPOINT: usize = 0x110000;
|
||||
// See https://www.unicode.org/L2/L1999/UnicodeData.html
|
||||
const FIELD_CODEPOINT: usize = 0;
|
||||
const FIELD_CATEGORY: usize = 2;
|
||||
// Ambiguous East Asian characters
|
||||
const WIDTH_AMBIGUOUS_EASTASIAN: isize = -3;
|
||||
|
||||
// Width changed from 1 to 2 in Unicode 9.0
|
||||
const WIDTH_WIDENED_IN_9: isize = -6;
|
||||
// Category for unassigned codepoints.
|
||||
const CAT_UNASSIGNED: &str = "Cn";
|
||||
|
||||
// Category for private use codepoints.
|
||||
const CAT_PRIVATE_USE: &str = "Co";
|
||||
|
||||
// Category for surrogates.
|
||||
const CAT_SURROGATE: &str = "Cs";
|
||||
|
||||
struct Codepoint<'cat> {
|
||||
raw: u32,
|
||||
width: Option<isize>,
|
||||
category: &'cat str,
|
||||
}
|
||||
|
||||
let mut codepoints: Vec<Codepoint> = Vec::with_capacity(MAX_CODEPOINT + 1);
|
||||
for i in 0..=MAX_CODEPOINT {
|
||||
codepoints.push(Codepoint {
|
||||
raw: i as u32,
|
||||
width: None,
|
||||
category: CAT_UNASSIGNED,
|
||||
});
|
||||
}
|
||||
|
||||
set_general_categories(&mut codepoints, &unicode_data);
|
||||
set_eaw_widths(&mut codepoints, &eaw_data);
|
||||
set_emoji_widths(&mut codepoints, &emoji_data);
|
||||
set_hardcoded_ranges(&mut codepoints);
|
||||
fn hexrange_to_range(hexrange: &str) -> std::ops::Range<usize> {
|
||||
/* Given a string like 1F300..1F320 representing an inclusive range,
|
||||
return the range of codepoints.
|
||||
If the string is like 1F321, return a range of just that element.
|
||||
*/
|
||||
let hexrange = hexrange.trim();
|
||||
let fields = hexrange
|
||||
.split("..")
|
||||
.map(|h| usize::from_str_radix(h.trim(), 16).unwrap())
|
||||
.collect::<Vec<usize>>();
|
||||
if fields.len() == 1 {
|
||||
fields[0]..(fields[0] + 1)
|
||||
} else {
|
||||
fields[0]..(fields[1] + 1)
|
||||
}
|
||||
}
|
||||
|
||||
fn set_general_categories<'u>(codepoints: &mut [Codepoint<'u>], unicode_data: &'u str) {
|
||||
for line in unicode_data.lines() {
|
||||
let fields = line.trim().split(';').collect::<Vec<_>>();
|
||||
if fields.len() > FIELD_CATEGORY {
|
||||
for idx in hexrange_to_range(fields[FIELD_CODEPOINT]) {
|
||||
codepoints[idx].category = fields[FIELD_CATEGORY];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_eaw_widths(codepoints: &mut [Codepoint<'_>], eaw_data_lines: &str) {
|
||||
// Read from EastAsianWidth.txt, set width values on the codepoints
|
||||
for line in eaw_data_lines.lines() {
|
||||
let line = line.trim().split('#').next().unwrap_or(line);
|
||||
let fields = line.trim().split(';').collect::<Vec<_>>();
|
||||
if fields.len() != 2 {
|
||||
continue;
|
||||
}
|
||||
let hexrange = fields[0];
|
||||
let width_type = fields[1];
|
||||
// width_types:
|
||||
// A: ambiguous, F: fullwidth, H: halfwidth,
|
||||
// . N: neutral, Na: east-asian Narrow
|
||||
let width: isize = if width_type == "A" {
|
||||
WIDTH_AMBIGUOUS_EASTASIAN
|
||||
} else if width_type == "F" || width_type == "W" {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
for cp in hexrange_to_range(hexrange) {
|
||||
codepoints[cp].width = Some(width);
|
||||
}
|
||||
}
|
||||
// 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
|
||||
const WIDE_RANGES: [(usize, usize); 5] = [
|
||||
(0x3400, 0x4DBF),
|
||||
(0x4E00, 0x9FFF),
|
||||
(0xF900, 0xFAFF),
|
||||
(0x20000, 0x2FFFD),
|
||||
(0x30000, 0x3FFFD),
|
||||
];
|
||||
for &wr in WIDE_RANGES.iter() {
|
||||
for cp in wr.0..(wr.1 + 1) {
|
||||
if codepoints[cp].width.is_none() {
|
||||
codepoints[cp].width = Some(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_emoji_widths(codepoints: &mut [Codepoint<'_>], emoji_data_lines: &str) {
|
||||
// Read from emoji-data.txt, set codepoint widths
|
||||
for line in emoji_data_lines.lines() {
|
||||
if !line.contains('#') || line.trim().starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let mut fields = line.trim().split('#').collect::<Vec<_>>();
|
||||
if fields.len() != 2 {
|
||||
continue;
|
||||
}
|
||||
let comment = fields.pop().unwrap();
|
||||
let fields = fields.pop().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
|
||||
// of first use, they will now return a zero version instead of
|
||||
// crashing the script
|
||||
if comment.trim().starts_with("NA") {
|
||||
continue;
|
||||
}
|
||||
|
||||
use std::str::FromStr;
|
||||
let mut v = comment.trim().split_whitespace().next().unwrap();
|
||||
if v.starts_with('E') {
|
||||
v = &v[1..];
|
||||
}
|
||||
if v.as_bytes()
|
||||
.get(0)
|
||||
.map(|c| !c.is_ascii_digit())
|
||||
.unwrap_or(true)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let mut idx = 1;
|
||||
while v
|
||||
.as_bytes()
|
||||
.get(idx)
|
||||
.map(|c| c.is_ascii_digit())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
idx += 1;
|
||||
}
|
||||
if v.as_bytes().get(idx).map(|&c| c != b'.').unwrap_or(true) {
|
||||
continue;
|
||||
}
|
||||
idx += 1;
|
||||
while v
|
||||
.as_bytes()
|
||||
.get(idx)
|
||||
.map(|c| c.is_ascii_digit())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
idx += 1;
|
||||
}
|
||||
v = &v[0..idx];
|
||||
|
||||
let version = f32::from_str(v).unwrap();
|
||||
for cp in hexrange_to_range(hexrange) {
|
||||
// Don't consider <=1F000 values as emoji. These can only be made
|
||||
// emoji through the variation selector which interacts terribly
|
||||
// with wcwidth().
|
||||
if cp < 0x1F000 {
|
||||
continue;
|
||||
}
|
||||
// Skip codepoints that are explicitly not wide.
|
||||
// For example U+1F336 ("Hot Pepper") renders like any emoji but is
|
||||
// marked as neutral in EAW so has width 1 for some reason.
|
||||
//if codepoints[cp].width == Some(1) {
|
||||
// continue;
|
||||
//}
|
||||
|
||||
// If this emoji was introduced before Unicode 9, then it was widened in 9.
|
||||
codepoints[cp].width = if version >= 9.0 {
|
||||
Some(2)
|
||||
} else {
|
||||
Some(WIDTH_WIDENED_IN_9)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
fn set_hardcoded_ranges(codepoints: &mut [Codepoint<'_>]) {
|
||||
// Mark private use and surrogate codepoints
|
||||
// Private use can be determined awkwardly from UnicodeData.txt,
|
||||
// but we just hard-code them.
|
||||
// We do not treat "private use high surrogate" as private use
|
||||
// so as to match wcwidth9().
|
||||
const PRIVATE_RANGES: [(usize, usize); 3] =
|
||||
[(0xE000, 0xF8FF), (0xF0000, 0xFFFFD), (0x100000, 0x10FFFD)];
|
||||
for &(first, last) in PRIVATE_RANGES.iter() {
|
||||
for idx in first..=last {
|
||||
codepoints[idx].category = CAT_PRIVATE_USE;
|
||||
}
|
||||
}
|
||||
|
||||
const SURROGATE_RANGES: [(usize, usize); 2] = [(0xD800, 0xDBFF), (0xDC00, 0xDFFF)];
|
||||
for &(first, last) in SURROGATE_RANGES.iter() {
|
||||
for idx in first..=last {
|
||||
codepoints[idx].category = CAT_SURROGATE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut file = File::create(&mod_path)?;
|
||||
file.write_all(
|
||||
br#"/*
|
||||
* meli - text_processing crate.
|
||||
*
|
||||
* Copyright 2017-2020 Manos Pitsidianakis
|
||||
*
|
||||
* This file is part of meli.
|
||||
*
|
||||
* meli is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* meli is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use super::types::LineBreakClass::{self, *};
|
||||
|
||||
pub const LINE_BREAK_RULES: &[(u32, u32, LineBreakClass)] = &[
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
file.write_all(b"use crate::types::LineBreakClass::*;\n")
|
||||
.unwrap();
|
||||
file.write_all(b"use crate::types::LineBreakClass;\n\n")
|
||||
.unwrap();
|
||||
file.write_all(b"const LINE_BREAK_RULES: &[(u32, u32, LineBreakClass)] = &[\n")
|
||||
.unwrap();
|
||||
for l in &line_break_table {
|
||||
file.write_all(format!(" (0x{:X}, 0x{:X}, {:?}),\n", l.0, l.1, l.2).as_bytes())
|
||||
.unwrap();
|
||||
}
|
||||
file.write_all(b"];\n").unwrap();
|
||||
|
||||
for (name, filter) in [
|
||||
(
|
||||
"ASCII",
|
||||
Box::new(|c: &&Codepoint| c.raw < 0x7f && c.raw >= 0x20)
|
||||
as Box<dyn Fn(&&Codepoint) -> bool>,
|
||||
),
|
||||
(
|
||||
"PRIVATE",
|
||||
Box::new(|c: &&Codepoint| c.category == CAT_PRIVATE_USE),
|
||||
),
|
||||
(
|
||||
"NONPRINT",
|
||||
Box::new(|c: &&Codepoint| {
|
||||
["Cc", "Cf", "Zl", "Zp", CAT_SURROGATE].contains(&c.category)
|
||||
}),
|
||||
),
|
||||
(
|
||||
"COMBINING",
|
||||
Box::new(|c: &&Codepoint| ["Mn", "Mc", "Me"].contains(&c.category)),
|
||||
),
|
||||
("DOUBLEWIDE", Box::new(|c: &&Codepoint| c.width == Some(2))),
|
||||
(
|
||||
"UNASSIGNED",
|
||||
Box::new(|c: &&Codepoint| c.category == CAT_UNASSIGNED),
|
||||
),
|
||||
(
|
||||
"AMBIGUOUS",
|
||||
Box::new(|c: &&Codepoint| c.width == Some(WIDTH_AMBIGUOUS_EASTASIAN)),
|
||||
),
|
||||
(
|
||||
"WIDENEDIN9",
|
||||
Box::new(|c: &&Codepoint| c.width == Some(WIDTH_WIDENED_IN_9)),
|
||||
),
|
||||
]
|
||||
.iter()
|
||||
{
|
||||
file.write_all(
|
||||
format!(
|
||||
r#"
|
||||
pub const {}: &[(u32, u32)] = &[
|
||||
"#,
|
||||
name
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut iter = codepoints.iter().filter(filter);
|
||||
let mut prev = iter.next().unwrap().raw;
|
||||
let mut a = prev;
|
||||
for cp in iter {
|
||||
if prev + 1 != cp.raw {
|
||||
file.write_all(format!(" (0x{:X}, 0x{:X}),\n", a, prev).as_bytes())
|
||||
.unwrap();
|
||||
a = cp.raw;
|
||||
}
|
||||
prev = cp.raw;
|
||||
}
|
||||
file.write_all(format!(" (0x{:X}, 0x{:X}),\n", a, prev).as_bytes())
|
||||
.unwrap();
|
||||
file.write_all(b"];\n").unwrap();
|
||||
}
|
||||
file.write_all(b"];").unwrap();
|
||||
std::fs::remove_file(&tmpdir_path).unwrap();
|
||||
tmpdir_path.pop();
|
||||
std::fs::remove_dir(&tmpdir_path).unwrap();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -105,17 +105,9 @@ impl AddressBook {
|
|||
{
|
||||
let mut ret = AddressBook::new(s.name.clone());
|
||||
if let Some(vcard_path) = s.vcard_folder() {
|
||||
match vcard::load_cards(std::path::Path::new(vcard_path)) {
|
||||
Ok(cards) => {
|
||||
for c in cards {
|
||||
ret.add_card(c);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
crate::log(
|
||||
format!("Could not load vcards from {:?}: {}", vcard_path, err),
|
||||
crate::WARN,
|
||||
);
|
||||
if let Ok(cards) = vcard::load_cards(&std::path::Path::new(vcard_path)) {
|
||||
for c in cards {
|
||||
ret.add_card(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -200,7 +192,7 @@ 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) {
|
||||
|
|
|
@ -19,14 +19,7 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! # vCard format
|
||||
//!
|
||||
//! This module implements the standards:
|
||||
//!
|
||||
//! - Version 3 (read-only) [RFC 2426: vCard MIME Directory Profile](https://datatracker.ietf.org/doc/2426)
|
||||
//! - Version 4 [RFC 6350: vCard Format Specification](https://datatracker.ietf.org/doc/rfc6350/)
|
||||
//! - Parameter escaping [RFC 6868 Parameter Value Encoding in iCalendar and vCard](https://datatracker.ietf.org/doc/rfc6868/)
|
||||
|
||||
/// Convert VCard strings to meli Cards (contacts).
|
||||
use super::*;
|
||||
use crate::error::{MeliError, Result};
|
||||
use crate::parsec::{match_literal_anycase, one_or_more, peek, prefix, take_until, Parser};
|
||||
|
@ -40,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>(
|
||||
|
@ -83,14 +72,10 @@ pub struct ContentLine {
|
|||
|
||||
impl CardDeserializer {
|
||||
pub fn from_str(mut input: &str) -> Result<VCard<impl VCardVersion>> {
|
||||
input = if (!input.starts_with(HEADER_CRLF) || !input.ends_with(FOOTER_CRLF))
|
||||
&& (!input.starts_with(HEADER_LF) || !input.ends_with(FOOTER_LF))
|
||||
{
|
||||
input = if !input.starts_with(HEADER) || !input.ends_with(FOOTER) {
|
||||
return Err(MeliError::new(format!("Error while parsing vcard: input does not start or end with correct header and footer. input is:\n{:?}", input)));
|
||||
} else if input.starts_with(HEADER_CRLF) {
|
||||
&input[HEADER_CRLF.len()..input.len() - FOOTER_CRLF.len()]
|
||||
} else {
|
||||
&input[HEADER_LF.len()..input.len() - FOOTER_LF.len()]
|
||||
&input[HEADER.len()..input.len() - FOOTER.len()]
|
||||
};
|
||||
|
||||
let mut ret = HashMap::default();
|
||||
|
@ -216,7 +201,7 @@ impl<V: VCardVersion> TryInto<Card> for VCard<V> {
|
|||
T102200Z
|
||||
T102200-0800
|
||||
*/
|
||||
card.birthday = crate::datetime::timestamp_from_string(val.value.as_str(), "%Y%m%d\0")
|
||||
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") {
|
||||
|
@ -284,24 +269,16 @@ pub fn load_cards(p: &std::path::Path) -> Result<Vec<Card>> {
|
|||
use std::io::Read;
|
||||
contents.clear();
|
||||
std::fs::File::open(&f)?.read_to_string(&mut contents)?;
|
||||
match parse_card().parse(contents.as_str()) {
|
||||
Ok((_, c)) => {
|
||||
for s in c {
|
||||
ret.push(
|
||||
CardDeserializer::from_str(s)
|
||||
.and_then(TryInto::try_into)
|
||||
.map(|mut card| {
|
||||
Card::set_external_resource(&mut card, true);
|
||||
is_any_valid = true;
|
||||
card
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
crate::log(
|
||||
format!("Could not parse vcard from {}: {}", f.display(), err),
|
||||
crate::WARN,
|
||||
if let Ok((_, c)) = parse_card().parse(contents.as_str()) {
|
||||
for s in c {
|
||||
ret.push(
|
||||
CardDeserializer::from_str(s)
|
||||
.and_then(TryInto::try_into)
|
||||
.and_then(|mut card| {
|
||||
Card::set_external_resource(&mut card, true);
|
||||
is_any_valid = true;
|
||||
Ok(card)
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -313,16 +290,16 @@ pub fn load_cards(p: &std::path::Path) -> Result<Vec<Card>> {
|
|||
debug!(&c);
|
||||
}
|
||||
}
|
||||
if is_any_valid {
|
||||
if !is_any_valid {
|
||||
ret.into_iter().collect::<Result<Vec<Card>>>()
|
||||
} else {
|
||||
ret.retain(Result::is_ok);
|
||||
ret.into_iter().collect::<Result<Vec<Card>>>()
|
||||
}
|
||||
ret.into_iter().collect::<Result<Vec<Card>>>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_card() {
|
||||
let j = "BEGIN:VCARD\r\nVERSION:4.0\r\nN:Gump;Forrest;;Mr.;\r\nFN:Forrest Gump\r\nORG:Bubba Gump Shrimp Co.\r\nTITLE:Shrimp Man\r\nPHOTO;MEDIATYPE=image/gif:http://www.example.com/dir_photos/my_photo.gif\r\nTEL;TYPE=work,voice;VALUE=uri:tel:+1-111-555-1212\r\nTEL;TYPE=home,voice;VALUE=uri:tel:+1-404-555-1212\r\nADR;TYPE=WORK;PREF=1;LABEL=\"100 Waters Edge\\nBaytown\\, LA 30314\\nUnited States of America\":;;100 Waters Edge;Baytown;LA;30314;United States of America\r\nADR;TYPE=HOME;LABEL=\"42 Plantation St.\\nBaytown\\, LA 30314\\nUnited States of America\":;;42 Plantation St.;Baytown;LA;30314;United States of America\r\nEMAIL:forrestgump@example.com\r\nREV:20080424T195243Z\r\nx-qq:21588891\r\nEND:VCARD\r\n";
|
||||
println!("results = {:#?}", CardDeserializer::from_str(j).unwrap());
|
||||
let j = "BEGIN:VCARD\nVERSION:4.0\nN:Gump;Forrest;;Mr.;\nFN:Forrest Gump\nORG:Bubba Gump Shrimp Co.\nTITLE:Shrimp Man\nPHOTO;MEDIATYPE=image/gif:http://www.example.com/dir_photos/my_photo.gif\nTEL;TYPE=work,voice;VALUE=uri:tel:+1-111-555-1212\nTEL;TYPE=home,voice;VALUE=uri:tel:+1-404-555-1212\nADR;TYPE=WORK;PREF=1;LABEL=\"100 Waters Edge\\nBaytown\\, LA 30314\\nUnited States of America\":;;100 Waters Edge;Baytown;LA;30314;United States of America\nADR;TYPE=HOME;LABEL=\"42 Plantation St.\\nBaytown\\, LA 30314\\nUnited States of America\":;;42 Plantation St.;Baytown;LA;30314;United States of America\nEMAIL:forrestgump@example.com\nREV:20080424T195243Z\nx-qq:21588891\nEND:VCARD\n";
|
||||
println!("results = {:#?}", CardDeserializer::from_str(j).unwrap());
|
||||
}
|
||||
|
|
|
@ -50,25 +50,26 @@ pub use self::imap::ImapType;
|
|||
#[cfg(feature = "imap_backend")]
|
||||
pub use self::nntp::NntpType;
|
||||
use crate::conf::AccountSettings;
|
||||
use crate::error::{ErrorKind, MeliError, Result};
|
||||
use crate::error::{MeliError, Result};
|
||||
|
||||
#[cfg(feature = "maildir_backend")]
|
||||
use self::maildir::MaildirType;
|
||||
#[cfg(feature = "mbox_backend")]
|
||||
use self::mbox::MboxType;
|
||||
use super::email::{Envelope, EnvelopeHash, Flag};
|
||||
use futures::stream::Stream;
|
||||
use std::any::Any;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::fmt::Debug;
|
||||
use std::future::Future;
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
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) => {{
|
||||
|
@ -96,7 +97,7 @@ pub struct Backends {
|
|||
|
||||
pub struct Backend {
|
||||
pub create_fn: Box<dyn Fn() -> BackendCreator>,
|
||||
pub validate_conf_fn: Box<dyn Fn(&mut AccountSettings) -> Result<()>>,
|
||||
pub validate_conf_fn: Box<dyn Fn(&AccountSettings) -> Result<()>>,
|
||||
}
|
||||
|
||||
impl Default for Backends {
|
||||
|
@ -107,45 +108,10 @@ impl Default for Backends {
|
|||
|
||||
#[cfg(feature = "notmuch_backend")]
|
||||
pub const NOTMUCH_ERROR_MSG: &str =
|
||||
"libnotmuch5 was not found in your system. Make sure it is installed and in the library paths. For a custom file path, use `library_file_path` setting in your notmuch account.\n";
|
||||
"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."#;
|
||||
|
||||
impl Backends {
|
||||
pub fn new() -> Self {
|
||||
let mut b = Backends {
|
||||
|
@ -190,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")]
|
||||
{
|
||||
|
@ -215,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);
|
||||
}
|
||||
|
@ -232,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(|| {
|
||||
MeliError::new(format!(
|
||||
"{}{} is not a valid mail backend. {}",
|
||||
"{}{} 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)
|
||||
|
@ -259,22 +218,19 @@ impl Backends {
|
|||
#[derive(Debug, Clone)]
|
||||
pub enum BackendEvent {
|
||||
Notice {
|
||||
description: String,
|
||||
content: Option<String>,
|
||||
description: Option<String>,
|
||||
content: String,
|
||||
level: crate::LoggingLevel,
|
||||
},
|
||||
Refresh(RefreshEvent),
|
||||
AccountStateChange {
|
||||
message: Cow<'static, str>,
|
||||
},
|
||||
//Job(Box<Future<Output = Result<()>> + Send + 'static>)
|
||||
}
|
||||
|
||||
impl From<MeliError> for BackendEvent {
|
||||
fn from(val: MeliError) -> BackendEvent {
|
||||
BackendEvent::Notice {
|
||||
description: val.summary.to_string(),
|
||||
content: Some(val.to_string()),
|
||||
description: val.summary.as_ref().map(|s| s.to_string()),
|
||||
content: val.to_string(),
|
||||
level: crate::LoggingLevel::ERROR,
|
||||
}
|
||||
}
|
||||
|
@ -290,14 +246,6 @@ pub enum RefreshEventKind {
|
|||
NewFlags(EnvelopeHash, (Flag, Vec<String>)),
|
||||
Rescan,
|
||||
Failure(MeliError),
|
||||
MailboxCreate(Mailbox),
|
||||
MailboxDelete(MailboxHash),
|
||||
MailboxRename {
|
||||
old_mailbox_hash: MailboxHash,
|
||||
new_mailbox: Mailbox,
|
||||
},
|
||||
MailboxSubscribe(MailboxHash),
|
||||
MailboxUnsubscribe(MailboxHash),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -387,12 +335,18 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
|
|||
) -> ResultFuture<()>;
|
||||
|
||||
fn delete_messages(
|
||||
&mut self,
|
||||
env_hashes: EnvelopeHashBatch,
|
||||
mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<()>;
|
||||
|
||||
fn collection(&self) -> crate::Collection;
|
||||
&self,
|
||||
_env_hashes: EnvelopeHashBatch,
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
fn delete(&self, _env_hash: EnvelopeHash, _mailbox_hash: MailboxHash) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
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;
|
||||
|
||||
|
@ -400,14 +354,14 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
|
|||
&mut self,
|
||||
_path: String,
|
||||
) -> ResultFuture<(MailboxHash, HashMap<MailboxHash, Mailbox>)> {
|
||||
Err(MeliError::new("Unimplemented.").set_kind(ErrorKind::NotImplemented))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn delete_mailbox(
|
||||
&mut self,
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
|
||||
Err(MeliError::new("Unimplemented.").set_kind(ErrorKind::NotImplemented))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn set_mailbox_subscription(
|
||||
|
@ -415,7 +369,7 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_val: bool,
|
||||
) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Unimplemented.").set_kind(ErrorKind::NotImplemented))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn rename_mailbox(
|
||||
|
@ -423,7 +377,7 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_new_path: String,
|
||||
) -> ResultFuture<Mailbox> {
|
||||
Err(MeliError::new("Unimplemented.").set_kind(ErrorKind::NotImplemented))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn set_mailbox_permissions(
|
||||
|
@ -431,7 +385,7 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_val: MailboxPermissions,
|
||||
) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Unimplemented.").set_kind(ErrorKind::NotImplemented))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn search(
|
||||
|
@ -439,16 +393,7 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
|
|||
_query: crate::search::Query,
|
||||
_mailbox_hash: Option<MailboxHash>,
|
||||
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>> {
|
||||
Err(MeliError::new("Unimplemented.").set_kind(ErrorKind::NotImplemented))
|
||||
}
|
||||
|
||||
fn submit(
|
||||
&self,
|
||||
_bytes: Vec<u8>,
|
||||
_mailbox_hash: Option<MailboxHash>,
|
||||
_flags: Option<Flag>,
|
||||
) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Not supported in this backend.").set_kind(ErrorKind::NotSupported))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -663,12 +608,6 @@ impl std::convert::TryFrom<&[EnvelopeHash]> for EnvelopeHashBatch {
|
|||
}
|
||||
}
|
||||
|
||||
impl Into<BTreeSet<EnvelopeHash>> for &EnvelopeHashBatch {
|
||||
fn into(self) -> BTreeSet<EnvelopeHash> {
|
||||
self.iter().collect::<BTreeSet<EnvelopeHash>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl EnvelopeHashBatch {
|
||||
pub fn iter(&self) -> impl std::iter::Iterator<Item = EnvelopeHash> + '_ {
|
||||
std::iter::once(self.first).chain(self.rest.iter().cloned())
|
||||
|
@ -677,100 +616,4 @@ impl EnvelopeHashBatch {
|
|||
pub fn len(&self) -> usize {
|
||||
1 + self.rest.len()
|
||||
}
|
||||
|
||||
pub fn to_set(&self) -> BTreeSet<EnvelopeHash> {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct LazyCountSet {
|
||||
not_yet_seen: usize,
|
||||
set: BTreeSet<EnvelopeHash>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for LazyCountSet {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.debug_struct("LazyCountSet")
|
||||
.field("not_yet_seen", &self.not_yet_seen)
|
||||
.field("set", &self.set.len())
|
||||
.field("total_len", &self.len())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl LazyCountSet {
|
||||
pub fn set_not_yet_seen(&mut self, new_val: usize) {
|
||||
self.not_yet_seen = new_val;
|
||||
}
|
||||
|
||||
pub fn insert_existing(&mut self, new_val: EnvelopeHash) -> bool {
|
||||
if self.not_yet_seen == 0 {
|
||||
false
|
||||
} else {
|
||||
if !self.set.contains(&new_val) {
|
||||
self.not_yet_seen -= 1;
|
||||
}
|
||||
self.set.insert(new_val);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_existing_set(&mut self, set: BTreeSet<EnvelopeHash>) {
|
||||
let old_len = self.set.len();
|
||||
self.set.extend(set.into_iter());
|
||||
self.not_yet_seen = self.not_yet_seen.saturating_sub(self.set.len() - old_len);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.set.len() + self.not_yet_seen
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn clear(&mut self) {
|
||||
self.set.clear();
|
||||
self.not_yet_seen = 0;
|
||||
}
|
||||
|
||||
pub fn insert_new(&mut self, new_val: EnvelopeHash) {
|
||||
self.set.insert(new_val);
|
||||
}
|
||||
|
||||
pub fn insert_set(&mut self, set: BTreeSet<EnvelopeHash>) {
|
||||
self.set.extend(set.into_iter());
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, env_hash: EnvelopeHash) -> bool {
|
||||
self.set.remove(&env_hash)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lazy_count_set() {
|
||||
let mut new = LazyCountSet::default();
|
||||
assert_eq!(new.len(), 0);
|
||||
new.set_not_yet_seen(10);
|
||||
assert_eq!(new.len(), 10);
|
||||
for i in 0..10 {
|
||||
assert!(new.insert_existing(i));
|
||||
}
|
||||
assert_eq!(new.len(), 10);
|
||||
assert!(!new.insert_existing(10));
|
||||
assert_eq!(new.len(), 10);
|
||||
}
|
||||
|
||||
pub struct IsSubscribedFn(Box<dyn Fn(&str) -> bool + Send + Sync>);
|
||||
|
||||
impl std::fmt::Debug for IsSubscribedFn {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "IsSubscribedFn Box")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for IsSubscribedFn {
|
||||
type Target = Box<dyn Fn(&str) -> bool + Send + Sync>;
|
||||
fn deref(&self) -> &Box<dyn Fn(&str) -> bool + Send + Sync> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,20 +42,20 @@ use crate::backends::{
|
|||
*,
|
||||
};
|
||||
|
||||
use crate::collection::Collection;
|
||||
use crate::conf::AccountSettings;
|
||||
use crate::connections::timeout;
|
||||
use crate::email::{parser::BytesExt, *};
|
||||
use crate::error::{MeliError, Result, ResultIntoMeliError};
|
||||
use futures::lock::Mutex as FutureMutex;
|
||||
use futures::stream::Stream;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::{hash_map::DefaultHasher, BTreeMap};
|
||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
use std::hash::Hasher;
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
pub type ImapNum = usize;
|
||||
|
@ -64,7 +64,6 @@ pub type UIDVALIDITY = UID;
|
|||
pub type MessageSequenceNumber = ImapNum;
|
||||
|
||||
pub static SUPPORTED_CAPABILITIES: &[&str] = &[
|
||||
"AUTH=OAUTH2",
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
"COMPRESS=DEFLATE",
|
||||
"CONDSTORE",
|
||||
|
@ -83,7 +82,9 @@ pub static SUPPORTED_CAPABILITIES: &[&str] = &[
|
|||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct EnvelopeCache {
|
||||
bytes: Option<Vec<u8>>,
|
||||
bytes: Option<String>,
|
||||
headers: Option<String>,
|
||||
body: Option<String>,
|
||||
flags: Option<Flag>,
|
||||
}
|
||||
|
||||
|
@ -100,6 +101,20 @@ pub struct ImapServerConf {
|
|||
pub timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
struct IsSubscribedFn(Box<dyn Fn(&str) -> bool + Send + Sync>);
|
||||
|
||||
impl std::fmt::Debug for IsSubscribedFn {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "IsSubscribedFn Box")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for IsSubscribedFn {
|
||||
type Target = Box<dyn Fn(&str) -> bool + Send + Sync>;
|
||||
fn deref(&self) -> &Box<dyn Fn(&str) -> bool + Send + Sync> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
type Capabilities = HashSet<Vec<u8>>;
|
||||
|
||||
#[macro_export]
|
||||
|
@ -142,13 +157,14 @@ pub struct UIDStore {
|
|||
msn_index: Arc<Mutex<HashMap<MailboxHash, Vec<UID>>>>,
|
||||
|
||||
byte_cache: Arc<Mutex<HashMap<UID, EnvelopeCache>>>,
|
||||
collection: Collection,
|
||||
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
|
||||
|
||||
/* Offline caching */
|
||||
uidvalidity: Arc<Mutex<HashMap<MailboxHash, UID>>>,
|
||||
envelopes: Arc<Mutex<HashMap<EnvelopeHash, cache::CachedEnvelope>>>,
|
||||
max_uids: Arc<Mutex<HashMap<MailboxHash, UID>>>,
|
||||
modseq: Arc<Mutex<HashMap<EnvelopeHash, ModSequence>>>,
|
||||
reverse_modseq: Arc<Mutex<HashMap<MailboxHash, BTreeMap<ModSequence, EnvelopeHash>>>>,
|
||||
highestmodseqs: Arc<Mutex<HashMap<MailboxHash, std::result::Result<ModSequence, ()>>>>,
|
||||
mailboxes: Arc<FutureMutex<HashMap<MailboxHash, ImapMailbox>>>,
|
||||
is_online: Arc<Mutex<(SystemTime, Result<()>)>>,
|
||||
|
@ -172,13 +188,14 @@ impl UIDStore {
|
|||
envelopes: Default::default(),
|
||||
max_uids: Default::default(),
|
||||
modseq: Default::default(),
|
||||
reverse_modseq: Default::default(),
|
||||
highestmodseqs: Default::default(),
|
||||
hash_index: Default::default(),
|
||||
uid_index: Default::default(),
|
||||
msn_index: Default::default(),
|
||||
byte_cache: Default::default(),
|
||||
mailboxes: Arc::new(FutureMutex::new(Default::default())),
|
||||
collection: Default::default(),
|
||||
tag_index: Arc::new(RwLock::new(Default::default())),
|
||||
is_online: Arc::new(Mutex::new((
|
||||
SystemTime::now(),
|
||||
Err(MeliError::new("Account is uninitialised.")),
|
||||
|
@ -191,7 +208,7 @@ impl UIDStore {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct ImapType {
|
||||
_is_subscribed: Arc<IsSubscribedFn>,
|
||||
is_subscribed: Arc<IsSubscribedFn>,
|
||||
connection: Arc<FutureMutex<ImapConnection>>,
|
||||
server_conf: ImapServerConf,
|
||||
uid_store: Arc<UIDStore>,
|
||||
|
@ -219,7 +236,6 @@ impl MailBackend for ImapType {
|
|||
#[cfg(feature = "deflate_compression")]
|
||||
deflate,
|
||||
condstore,
|
||||
oauth2,
|
||||
},
|
||||
} = self.server_conf.protocol
|
||||
{
|
||||
|
@ -261,19 +277,10 @@ impl MailBackend for ImapType {
|
|||
};
|
||||
}
|
||||
}
|
||||
"AUTH=OAUTH2" => {
|
||||
if oauth2 {
|
||||
*status = MailBackendExtensionStatus::Enabled { comment: None };
|
||||
} else {
|
||||
*status = MailBackendExtensionStatus::Supported {
|
||||
comment: Some("Disabled by user configuration"),
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if SUPPORTED_CAPABILITIES
|
||||
.iter()
|
||||
.any(|c| c.eq_ignore_ascii_case(name.as_str()))
|
||||
.any(|c| c.eq_ignore_ascii_case(&name.as_str()))
|
||||
{
|
||||
*status = MailBackendExtensionStatus::Enabled { comment: None };
|
||||
}
|
||||
|
@ -318,7 +325,7 @@ impl MailBackend for ImapType {
|
|||
None
|
||||
};
|
||||
let mut state = FetchState {
|
||||
stage: if self.uid_store.keep_offline_cache && cache_handle.is_some() {
|
||||
stage: if self.uid_store.keep_offline_cache {
|
||||
FetchStage::InitialCache
|
||||
} else {
|
||||
FetchStage::InitialFresh
|
||||
|
@ -329,24 +336,11 @@ impl MailBackend for ImapType {
|
|||
cache_handle,
|
||||
};
|
||||
|
||||
/* do this in a closure to prevent recursion limit error in async_stream macro */
|
||||
let prepare_cl = |f: &ImapMailbox| {
|
||||
f.set_warm(true);
|
||||
if let Ok(mut exists) = f.exists.lock() {
|
||||
let total = exists.len();
|
||||
exists.clear();
|
||||
exists.set_not_yet_seen(total);
|
||||
}
|
||||
if let Ok(mut unseen) = f.unseen.lock() {
|
||||
let total = unseen.len();
|
||||
unseen.clear();
|
||||
unseen.set_not_yet_seen(total);
|
||||
}
|
||||
};
|
||||
Ok(Box::pin(async_stream::try_stream! {
|
||||
{
|
||||
let f = &state.uid_store.mailboxes.lock().await[&mailbox_hash];
|
||||
prepare_cl(f);
|
||||
f.exists.lock().unwrap().clear();
|
||||
f.unseen.lock().unwrap().clear();
|
||||
if f.no_select {
|
||||
yield vec![];
|
||||
return;
|
||||
|
@ -436,11 +430,11 @@ impl MailBackend for ImapType {
|
|||
match timeout(timeout_dur, connection.lock()).await {
|
||||
Ok(mut conn) => {
|
||||
debug!("is_online");
|
||||
match timeout(timeout_dur, conn.connect()).await {
|
||||
match debug!(timeout(timeout_dur, conn.connect()).await) {
|
||||
Ok(Ok(())) => Ok(()),
|
||||
Err(err) | Ok(Err(err)) => {
|
||||
conn.stream = Err(err.clone());
|
||||
conn.connect().await
|
||||
debug!(conn.connect().await)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -453,20 +447,21 @@ impl MailBackend for ImapType {
|
|||
let server_conf = self.server_conf.clone();
|
||||
let main_conn = self.connection.clone();
|
||||
let uid_store = self.uid_store.clone();
|
||||
let has_idle: bool = match self.server_conf.protocol {
|
||||
ImapProtocol::IMAP {
|
||||
extension_use: ImapExtensionUse { idle, .. },
|
||||
} => {
|
||||
idle && uid_store
|
||||
.capabilities
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"IDLE"))
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
Ok(Box::pin(async move {
|
||||
let has_idle: bool = match server_conf.protocol {
|
||||
ImapProtocol::IMAP {
|
||||
extension_use: ImapExtensionUse { idle, .. },
|
||||
} => {
|
||||
idle && uid_store
|
||||
.capabilities
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"IDLE"))
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
debug!(has_idle);
|
||||
while let Err(err) = if has_idle {
|
||||
idle(ImapWatchKit {
|
||||
conn: ImapConnection::new_connection(&server_conf, uid_store.clone()),
|
||||
|
@ -485,19 +480,17 @@ impl MailBackend for ImapType {
|
|||
let mut main_conn_lck = timeout(uid_store.timeout, main_conn.lock()).await?;
|
||||
if err.kind.is_network() {
|
||||
uid_store.is_online.lock().unwrap().1 = Err(err.clone());
|
||||
} else {
|
||||
return Err(err);
|
||||
}
|
||||
debug!("Watch failure: {}", err.to_string());
|
||||
debug!("failure: {}", err.to_string());
|
||||
match timeout(uid_store.timeout, main_conn_lck.connect())
|
||||
.await
|
||||
.and_then(|res| res)
|
||||
{
|
||||
Err(err2) => {
|
||||
debug!("Watch reconnect attempt failed: {}", err2.to_string());
|
||||
debug!("reconnect attempt failed: {}", err2.to_string());
|
||||
}
|
||||
Ok(()) => {
|
||||
debug!("Watch reconnect attempt succesful");
|
||||
debug!("reconnect attempt succesful");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
@ -631,9 +624,25 @@ impl MailBackend for ImapType {
|
|||
}
|
||||
let dest_path = {
|
||||
let mailboxes = uid_store.mailboxes.lock().await;
|
||||
let mailbox = mailboxes
|
||||
.get(&source_mailbox_hash)
|
||||
.ok_or_else(|| MeliError::new("Source mailbox not found"))?;
|
||||
if move_ && !mailbox.permissions.lock().unwrap().delete_messages {
|
||||
return Err(MeliError::new(format!(
|
||||
"You are not allowed to delete messages from mailbox {}",
|
||||
mailbox.path()
|
||||
)));
|
||||
}
|
||||
let mailbox = mailboxes
|
||||
.get(&destination_mailbox_hash)
|
||||
.ok_or_else(|| MeliError::new("Destination mailbox not found"))?;
|
||||
if !mailbox.permissions.lock().unwrap().create_messages {
|
||||
return Err(MeliError::new(format!(
|
||||
"You are not allowed to create messages in mailbox {}",
|
||||
mailbox.path()
|
||||
)));
|
||||
}
|
||||
|
||||
mailbox.imap_path().to_string()
|
||||
};
|
||||
let mut response = Vec::with_capacity(8 * 1024);
|
||||
|
@ -708,9 +717,8 @@ impl MailBackend for ImapType {
|
|||
.await?;
|
||||
if flags.iter().any(|(_, b)| *b) {
|
||||
/* Set flags/tags to true */
|
||||
let mut set_seen = false;
|
||||
let command = {
|
||||
let mut tag_lck = uid_store.collection.tag_index.write().unwrap();
|
||||
let mut tag_lck = uid_store.tag_index.write().unwrap();
|
||||
let mut cmd = format!("UID STORE {}", uids[0]);
|
||||
for uid in uids.iter().skip(1) {
|
||||
cmd = format!("{},{}", cmd, uid);
|
||||
|
@ -732,7 +740,6 @@ impl MailBackend for ImapType {
|
|||
}
|
||||
Ok(flag) if *flag == Flag::SEEN => {
|
||||
cmd.push_str("\\Seen ");
|
||||
set_seen = true;
|
||||
}
|
||||
Ok(flag) if *flag == Flag::DRAFT => {
|
||||
cmd.push_str("\\Draft ");
|
||||
|
@ -759,17 +766,8 @@ impl MailBackend for ImapType {
|
|||
conn.send_command(command.as_bytes()).await?;
|
||||
conn.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
if set_seen {
|
||||
let f = &uid_store.mailboxes.lock().await[&mailbox_hash];
|
||||
if let Ok(mut unseen) = f.unseen.lock() {
|
||||
for env_hash in env_hashes.iter() {
|
||||
unseen.remove(env_hash);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
if flags.iter().any(|(_, b)| !*b) {
|
||||
let mut set_unseen = false;
|
||||
/* Set flags/tags to false */
|
||||
let command = {
|
||||
let mut cmd = format!("UID STORE {}", uids[0]);
|
||||
|
@ -793,7 +791,6 @@ impl MailBackend for ImapType {
|
|||
}
|
||||
Ok(flag) if *flag == Flag::SEEN => {
|
||||
cmd.push_str("\\Seen ");
|
||||
set_unseen = true;
|
||||
}
|
||||
Ok(flag) if *flag == Flag::DRAFT => {
|
||||
cmd.push_str("\\Draft ");
|
||||
|
@ -823,40 +820,13 @@ impl MailBackend for ImapType {
|
|||
conn.send_command(command.as_bytes()).await?;
|
||||
conn.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
if set_unseen {
|
||||
let f = &uid_store.mailboxes.lock().await[&mailbox_hash];
|
||||
if let Ok(mut unseen) = f.unseen.lock() {
|
||||
for env_hash in env_hashes.iter() {
|
||||
unseen.insert_new(env_hash);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn delete_messages(
|
||||
&mut self,
|
||||
env_hashes: EnvelopeHashBatch,
|
||||
mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<()> {
|
||||
let flag_future = self.set_flags(
|
||||
env_hashes,
|
||||
mailbox_hash,
|
||||
smallvec::smallvec![(Ok(Flag::TRASHED), true)],
|
||||
)?;
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
flag_future.await?;
|
||||
let mut response = Vec::with_capacity(8 * 1024);
|
||||
let mut conn = connection.lock().await;
|
||||
conn.send_command("EXPUNGE".as_bytes()).await?;
|
||||
conn.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
debug!("EXPUNGE response: {}", &String::from_utf8_lossy(&response));
|
||||
Ok(())
|
||||
}))
|
||||
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
|
||||
Some(self.uid_store.tag_index.clone())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
|
@ -867,10 +837,6 @@ impl MailBackend for ImapType {
|
|||
self
|
||||
}
|
||||
|
||||
fn collection(&self) -> Collection {
|
||||
self.uid_store.collection.clone()
|
||||
}
|
||||
|
||||
fn create_mailbox(
|
||||
&mut self,
|
||||
mut path: String,
|
||||
|
@ -1120,32 +1086,32 @@ impl MailBackend for ImapType {
|
|||
Subject(t) => {
|
||||
s.push_str(" SUBJECT \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
s.push_str("\"");
|
||||
}
|
||||
From(t) => {
|
||||
s.push_str(" FROM \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
s.push_str("\"");
|
||||
}
|
||||
To(t) => {
|
||||
s.push_str(" TO \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
s.push_str("\"");
|
||||
}
|
||||
Cc(t) => {
|
||||
s.push_str(" CC \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
s.push_str("\"");
|
||||
}
|
||||
Bcc(t) => {
|
||||
s.push_str(" BCC \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
s.push_str("\"");
|
||||
}
|
||||
AllText(t) => {
|
||||
s.push_str(" TEXT \"");
|
||||
s.extend(escape_double_quote(t).chars());
|
||||
s.push('"');
|
||||
s.push_str("\"");
|
||||
}
|
||||
Flags(v) => {
|
||||
for f in v {
|
||||
|
@ -1177,20 +1143,20 @@ impl MailBackend for ImapType {
|
|||
keyword => {
|
||||
s.push_str(" KEYWORD ");
|
||||
s.push_str(keyword);
|
||||
s.push(' ');
|
||||
s.push_str(" ");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
And(q1, q2) => {
|
||||
rec(q1, s);
|
||||
s.push(' ');
|
||||
s.push_str(" ");
|
||||
rec(q2, s);
|
||||
}
|
||||
Or(q1, q2) => {
|
||||
s.push_str(" OR ");
|
||||
rec(q1, s);
|
||||
s.push(' ');
|
||||
s.push_str(" ");
|
||||
rec(q2, s);
|
||||
}
|
||||
Not(q) => {
|
||||
|
@ -1210,7 +1176,7 @@ impl MailBackend for ImapType {
|
|||
let mut conn = connection.lock().await;
|
||||
conn.examine_mailbox(mailbox_hash, &mut response, false)
|
||||
.await?;
|
||||
conn.send_command(format!("UID SEARCH CHARSET UTF-8 {}", query_str.trim()).as_bytes())
|
||||
conn.send_command(format!("UID SEARCH CHARSET UTF-8 {}", query_str).as_bytes())
|
||||
.await?;
|
||||
conn.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await?;
|
||||
|
@ -1249,14 +1215,7 @@ impl ImapType {
|
|||
) -> Result<Box<dyn MailBackend>> {
|
||||
let server_hostname = get_conf_val!(s["server_hostname"])?;
|
||||
let server_username = get_conf_val!(s["server_username"])?;
|
||||
let use_oauth2: bool = get_conf_val!(s["use_oauth2"], false)?;
|
||||
let server_password = if !s.extra.contains_key("server_password_command") {
|
||||
if use_oauth2 {
|
||||
return Err(MeliError::new(format!(
|
||||
"({}) `use_oauth2` use requires `server_password_command` set with a command that returns an OAUTH2 token. Consult documentation for guidance.",
|
||||
s.name,
|
||||
)));
|
||||
}
|
||||
get_conf_val!(s["server_password"])?.to_string()
|
||||
} else {
|
||||
let invocation = get_conf_val!(s["server_password_command"])?;
|
||||
|
@ -1279,7 +1238,7 @@ impl ImapType {
|
|||
};
|
||||
let server_port = get_conf_val!(s["server_port"], 143)?;
|
||||
let use_tls = get_conf_val!(s["use_tls"], true)?;
|
||||
let use_starttls = use_tls && get_conf_val!(s["use_starttls"], server_port != 993)?;
|
||||
let use_starttls = use_tls && get_conf_val!(s["use_starttls"], !(server_port == 993))?;
|
||||
let danger_accept_invalid_certs: bool =
|
||||
get_conf_val!(s["danger_accept_invalid_certs"], false)?;
|
||||
#[cfg(feature = "sqlite3")]
|
||||
|
@ -1313,7 +1272,6 @@ impl ImapType {
|
|||
condstore: get_conf_val!(s["use_condstore"], true)?,
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
deflate: get_conf_val!(s["use_deflate"], true)?,
|
||||
oauth2: use_oauth2,
|
||||
},
|
||||
},
|
||||
timeout,
|
||||
|
@ -1337,7 +1295,7 @@ impl ImapType {
|
|||
|
||||
Ok(Box::new(ImapType {
|
||||
server_conf,
|
||||
_is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)),
|
||||
is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)),
|
||||
connection: Arc::new(FutureMutex::new(connection)),
|
||||
uid_store,
|
||||
}))
|
||||
|
@ -1427,12 +1385,12 @@ impl ImapType {
|
|||
conn.read_response(&mut res, RequiredResponses::LIST_REQUIRED)
|
||||
.await?;
|
||||
}
|
||||
debug!("LIST reply: {}", String::from_utf8_lossy(&res));
|
||||
for l in res.split_rn() {
|
||||
if !l.starts_with(b"*") {
|
||||
continue;
|
||||
}
|
||||
if let Ok(mut mailbox) = protocol_parser::list_mailbox_result(l).map(|(_, v)| v) {
|
||||
debug!("out: {}", String::from_utf8_lossy(&res));
|
||||
let mut lines = res.split_rn();
|
||||
/* Remove "M__ OK .." line */
|
||||
lines.next_back();
|
||||
for l in lines {
|
||||
if let Ok(mut mailbox) = protocol_parser::list_mailbox_result(&l).map(|(_, v)| v) {
|
||||
if let Some(parent) = mailbox.parent {
|
||||
if mailboxes.contains_key(&parent) {
|
||||
mailboxes
|
||||
|
@ -1458,7 +1416,7 @@ impl ImapType {
|
|||
} else {
|
||||
mailboxes.insert(mailbox.hash, mailbox);
|
||||
}
|
||||
} else if let Ok(status) = protocol_parser::status_response(l).map(|(_, v)| v) {
|
||||
} else if let Ok(status) = protocol_parser::status_response(&l).map(|(_, v)| v) {
|
||||
if let Some(mailbox_hash) = status.mailbox {
|
||||
if mailboxes.contains_key(&mailbox_hash) {
|
||||
let entry = mailboxes.entry(mailbox_hash).or_default();
|
||||
|
@ -1478,69 +1436,26 @@ impl ImapType {
|
|||
conn.send_command(b"LSUB \"\" \"*\"").await?;
|
||||
conn.read_response(&mut res, RequiredResponses::LSUB_REQUIRED)
|
||||
.await?;
|
||||
debug!("LSUB reply: {}", String::from_utf8_lossy(&res));
|
||||
for l in res.split_rn() {
|
||||
if !l.starts_with(b"*") {
|
||||
continue;
|
||||
}
|
||||
if let Ok(subscription) = protocol_parser::list_mailbox_result(l).map(|(_, v)| v) {
|
||||
let mut lines = res.split_rn();
|
||||
debug!("out: {}", String::from_utf8_lossy(&res));
|
||||
/* Remove "M__ OK .." line */
|
||||
lines.next_back();
|
||||
for l in lines {
|
||||
if let Ok(subscription) = protocol_parser::list_mailbox_result(&l).map(|(_, v)| v) {
|
||||
if let Some(f) = mailboxes.get_mut(&subscription.hash()) {
|
||||
if f.special_usage() == SpecialUsageMailbox::Normal
|
||||
&& subscription.special_usage() != SpecialUsageMailbox::Normal
|
||||
{
|
||||
f.set_special_usage(subscription.special_usage())?;
|
||||
}
|
||||
f.is_subscribed = true;
|
||||
}
|
||||
} else {
|
||||
debug!("parse error for {:?}", l);
|
||||
}
|
||||
}
|
||||
Ok(mailboxes)
|
||||
Ok(debug!(mailboxes))
|
||||
}
|
||||
|
||||
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(|| {
|
||||
MeliError::new(format!(
|
||||
"Configuration error ({}): IMAP connection requires the field `{}` set",
|
||||
$s.name.as_str(),
|
||||
$var
|
||||
))
|
||||
})
|
||||
}};
|
||||
($s:ident[$var:literal], $default:expr) => {{
|
||||
keys.insert($var);
|
||||
$s.extra
|
||||
.remove($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))
|
||||
}};
|
||||
}
|
||||
pub fn validate_config(s: &AccountSettings) -> Result<()> {
|
||||
get_conf_val!(s["server_hostname"])?;
|
||||
get_conf_val!(s["server_username"])?;
|
||||
let use_oauth2: bool = get_conf_val!(s["use_oauth2"], false)?;
|
||||
keys.insert("server_password_command");
|
||||
if !s.extra.contains_key("server_password_command") {
|
||||
if use_oauth2 {
|
||||
return Err(MeliError::new(format!(
|
||||
"({}) `use_oauth2` use requires `server_password_command` set with a command that returns an OAUTH2 token. Consult documentation for guidance.",
|
||||
s.name,
|
||||
)));
|
||||
}
|
||||
get_conf_val!(s["server_password"])?;
|
||||
} else if s.extra.contains_key("server_password") {
|
||||
return Err(MeliError::new(format!(
|
||||
|
@ -1548,10 +1463,9 @@ impl ImapType {
|
|||
s.name.as_str(),
|
||||
)));
|
||||
}
|
||||
let _ = get_conf_val!(s["server_password_command"]);
|
||||
get_conf_val!(s["server_port"], 143)?;
|
||||
let server_port = get_conf_val!(s["server_port"], 143)?;
|
||||
let use_tls = get_conf_val!(s["use_tls"], true)?;
|
||||
let use_starttls = get_conf_val!(s["use_starttls"], false)?;
|
||||
let use_starttls = get_conf_val!(s["use_starttls"], !(server_port == 993))?;
|
||||
if !use_tls && use_starttls {
|
||||
return Err(MeliError::new(format!(
|
||||
"Configuration error ({}): incompatible use_tls and use_starttls values: use_tls = false, use_starttls = true",
|
||||
|
@ -1583,18 +1497,6 @@ impl ImapType {
|
|||
)));
|
||||
}
|
||||
let _timeout = get_conf_val!(s["timeout"], 16_u64)?;
|
||||
let extra_keys = s
|
||||
.extra
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect::<HashSet<&str>>();
|
||||
let diff = extra_keys.difference(&keys).collect::<Vec<&&str>>();
|
||||
if !diff.is_empty() {
|
||||
return Err(MeliError::new(format!(
|
||||
"Configuration error ({}): the following flags are set but are not recognized: {:?}.",
|
||||
s.name.as_str(), diff
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -1714,7 +1616,7 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
|||
FetchStage::ResyncCache => {
|
||||
let mailbox_hash = state.mailbox_hash;
|
||||
let mut conn = state.connection.lock().await;
|
||||
let res = conn.resync(mailbox_hash).await;
|
||||
let res = debug!(conn.resync(mailbox_hash).await);
|
||||
if let Ok(Some(payload)) = res {
|
||||
state.stage = FetchStage::Finished;
|
||||
return Ok(payload);
|
||||
|
@ -1746,21 +1648,26 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
|||
return Ok(Vec::new());
|
||||
}
|
||||
let mut conn = connection.lock().await;
|
||||
debug!("locked for fetch {}", mailbox_path);
|
||||
let mut response = Vec::with_capacity(8 * 1024);
|
||||
let max_uid_left = max_uid;
|
||||
let chunk_size = 250;
|
||||
|
||||
let mut envelopes = Vec::with_capacity(chunk_size);
|
||||
let mut payload = vec![];
|
||||
conn.examine_mailbox(mailbox_hash, &mut response, false)
|
||||
.await?;
|
||||
if max_uid_left > 0 {
|
||||
let mut envelopes = vec![];
|
||||
debug!("{} max_uid_left= {}", mailbox_hash, max_uid_left);
|
||||
let command = if max_uid_left == 1 {
|
||||
"UID FETCH 1 (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)".to_string()
|
||||
"UID FETCH 1 (UID FLAGS ENVELOPE BODYSTRUCTURE)".to_string()
|
||||
} else {
|
||||
format!(
|
||||
"UID FETCH {}:{} (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)",
|
||||
"UID FETCH {}:{} (UID FLAGS ENVELOPE BODYSTRUCTURE)",
|
||||
std::cmp::max(
|
||||
std::cmp::max(max_uid_left.saturating_sub(chunk_size), 1),
|
||||
1
|
||||
),
|
||||
max_uid_left
|
||||
)
|
||||
};
|
||||
|
@ -1774,43 +1681,29 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
|||
mailbox_path
|
||||
)
|
||||
})?;
|
||||
let (_, mut v, _) = protocol_parser::fetch_responses(&response)?;
|
||||
debug!(
|
||||
"fetch response is {} bytes and {} lines and has {} parsed Envelopes",
|
||||
"fetch response is {} bytes and {} lines",
|
||||
response.len(),
|
||||
String::from_utf8_lossy(&response).lines().count(),
|
||||
v.len()
|
||||
String::from_utf8_lossy(&response).lines().count()
|
||||
);
|
||||
let (_, mut v, _) = protocol_parser::fetch_responses(&response)?;
|
||||
debug!("responses len is {}", v.len());
|
||||
for FetchResponse {
|
||||
ref uid,
|
||||
ref mut envelope,
|
||||
ref mut flags,
|
||||
raw_fetch_value,
|
||||
ref references,
|
||||
..
|
||||
} in v.iter_mut()
|
||||
{
|
||||
if uid.is_none() || envelope.is_none() || flags.is_none() {
|
||||
debug!("BUG? in fetch is none");
|
||||
debug!(uid);
|
||||
debug!(envelope);
|
||||
debug!(flags);
|
||||
debug!("response was: {}", String::from_utf8_lossy(&response));
|
||||
debug!(conn.process_untagged(raw_fetch_value).await)?;
|
||||
continue;
|
||||
}
|
||||
let uid = uid.unwrap();
|
||||
let env = envelope.as_mut().unwrap();
|
||||
env.set_hash(generate_envelope_hash(&mailbox_path, &uid));
|
||||
if let Some(value) = references {
|
||||
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() {
|
||||
if !flags.intersects(Flag::SEEN) {
|
||||
our_unseen.insert(env.hash());
|
||||
}
|
||||
env.set_flags(*flags);
|
||||
for f in keywords {
|
||||
let hash = tag_hash!(f);
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
|
@ -1821,14 +1714,14 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
|||
}
|
||||
}
|
||||
if let Some(ref mut cache_handle) = cache_handle {
|
||||
if let Err(err) = cache_handle
|
||||
if let Err(err) = debug!(cache_handle
|
||||
.insert_envelopes(mailbox_hash, &v)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not save envelopes in cache for mailbox {}",
|
||||
mailbox_path
|
||||
)
|
||||
})
|
||||
}))
|
||||
{
|
||||
(state.uid_store.event_consumer)(
|
||||
state.uid_store.account_hash,
|
||||
|
@ -1861,7 +1754,7 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
|||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.or_default()
|
||||
.insert(message_sequence_number - 1, uid);
|
||||
.insert((message_sequence_number - 1).try_into().unwrap(), uid);
|
||||
uid_store
|
||||
.hash_index
|
||||
.lock()
|
||||
|
@ -1872,25 +1765,30 @@ async fn fetch_hlpr(state: &mut FetchState) -> Result<Vec<Envelope>> {
|
|||
.lock()
|
||||
.unwrap()
|
||||
.insert((mailbox_hash, uid), env.hash());
|
||||
envelopes.push(env);
|
||||
envelopes.push((uid, env));
|
||||
}
|
||||
unseen.lock().unwrap().insert_existing_set(our_unseen);
|
||||
mailbox_exists
|
||||
debug!("sending payload for {}", mailbox_hash);
|
||||
unseen
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert_existing_set(envelopes.iter().map(|env| env.hash()).collect::<_>());
|
||||
.insert_existing_set(our_unseen.iter().cloned().collect());
|
||||
mailbox_exists.lock().unwrap().insert_existing_set(
|
||||
envelopes.iter().map(|(_, env)| env.hash()).collect::<_>(),
|
||||
);
|
||||
drop(conn);
|
||||
payload.extend(envelopes.into_iter().map(|(_, env)| env));
|
||||
}
|
||||
if max_uid_left <= 1 {
|
||||
unseen.lock().unwrap().set_not_yet_seen(0);
|
||||
mailbox_exists.lock().unwrap().set_not_yet_seen(0);
|
||||
*stage = FetchStage::Finished;
|
||||
} else {
|
||||
*stage = FetchStage::FreshFetch {
|
||||
max_uid: std::cmp::max(max_uid_left.saturating_sub(chunk_size + 1), 1),
|
||||
max_uid: std::cmp::max(
|
||||
std::cmp::max(max_uid_left.saturating_sub(chunk_size), 1),
|
||||
1,
|
||||
),
|
||||
};
|
||||
}
|
||||
return Ok(envelopes);
|
||||
return Ok(payload);
|
||||
}
|
||||
FetchStage::Finished => {
|
||||
return Ok(vec![]);
|
||||
|
|
|
@ -140,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: 2,
|
||||
version: 1,
|
||||
};
|
||||
|
||||
impl ToSql for ModSequence {
|
||||
|
@ -178,7 +178,7 @@ mod sqlite3_m {
|
|||
|
||||
let mut ret: Vec<UID> = stmt
|
||||
.query_map(sqlite3::params![mailbox_hash as i64], |row| {
|
||||
row.get(0).map(|i: Sqlite3UID| i as UID)
|
||||
Ok(row.get(0).map(|i: Sqlite3UID| i as UID)?)
|
||||
})?
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
Ok(ret.pop().unwrap_or(0))
|
||||
|
@ -231,7 +231,7 @@ mod sqlite3_m {
|
|||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.and_modify(|entry| *entry = highestmodseq.ok_or(()))
|
||||
.or_insert_with(|| highestmodseq.ok_or(()));
|
||||
.or_insert(highestmodseq.ok_or(()));
|
||||
self.uid_store
|
||||
.uidvalidity
|
||||
.lock()
|
||||
|
@ -239,7 +239,7 @@ 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 = tag_hash!(f);
|
||||
//debug!("hash {} flag {}", hash, &f);
|
||||
|
@ -365,7 +365,7 @@ mod sqlite3_m {
|
|||
|
||||
fn envelopes(&mut self, mailbox_hash: MailboxHash) -> Result<Option<Vec<EnvelopeHash>>> {
|
||||
debug!("envelopes mailbox_hash {}", mailbox_hash);
|
||||
if self.mailbox_state(mailbox_hash)?.is_none() {
|
||||
if debug!(self.mailbox_state(mailbox_hash)?.is_none()) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
|
@ -429,6 +429,7 @@ mod sqlite3_m {
|
|||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if self.mailbox_state(mailbox_hash)?.is_none() {
|
||||
debug!(self.mailbox_state(mailbox_hash)?.is_none());
|
||||
return Err(MeliError::new("Mailbox is not in cache").set_kind(ErrorKind::Bug));
|
||||
}
|
||||
let Self {
|
||||
|
@ -444,9 +445,7 @@ mod sqlite3_m {
|
|||
modseq,
|
||||
flags: _,
|
||||
body: _,
|
||||
references: _,
|
||||
envelope: Some(envelope),
|
||||
raw_fetch_value: _,
|
||||
} = item
|
||||
{
|
||||
max_uid = std::cmp::max(max_uid, *uid);
|
||||
|
@ -470,7 +469,13 @@ mod sqlite3_m {
|
|||
mailbox_hash: MailboxHash,
|
||||
refresh_events: &[(UID, RefreshEvent)],
|
||||
) -> Result<()> {
|
||||
debug!(
|
||||
"update with refresh_events mailbox_hash {} len {}",
|
||||
mailbox_hash,
|
||||
refresh_events.len()
|
||||
);
|
||||
if self.mailbox_state(mailbox_hash)?.is_none() {
|
||||
debug!(self.mailbox_state(mailbox_hash)?.is_none());
|
||||
return Err(MeliError::new("Mailbox is not in cache").set_kind(ErrorKind::Bug));
|
||||
}
|
||||
let Self {
|
||||
|
@ -481,9 +486,9 @@ mod sqlite3_m {
|
|||
let tx = connection.transaction()?;
|
||||
let mut hash_index_lck = uid_store.hash_index.lock().unwrap();
|
||||
for (uid, event) in refresh_events {
|
||||
match &event.kind {
|
||||
match debug!(&event.kind) {
|
||||
RefreshEventKind::Remove(env_hash) => {
|
||||
hash_index_lck.remove(env_hash);
|
||||
hash_index_lck.remove(&env_hash);
|
||||
tx.execute(
|
||||
"DELETE FROM envelopes WHERE mailbox_hash = ?1 AND uid = ?2;",
|
||||
sqlite3::params![mailbox_hash as i64, *uid as Sqlite3UID],
|
||||
|
@ -503,7 +508,7 @@ mod sqlite3_m {
|
|||
let mut ret: Vec<Envelope> = stmt
|
||||
.query_map(
|
||||
sqlite3::params![mailbox_hash as i64, *uid as Sqlite3UID],
|
||||
|row| row.get(0),
|
||||
|row| Ok(row.get(0)?),
|
||||
)?
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
if let Some(mut env) = ret.pop() {
|
||||
|
@ -592,12 +597,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(
|
||||
|
@ -613,7 +618,7 @@ mod sqlite3_m {
|
|||
let x = stmt
|
||||
.query_map(
|
||||
sqlite3::params![mailbox_hash as i64, uid as Sqlite3UID],
|
||||
|row| row.get(0),
|
||||
|row| Ok(row.get(0)?),
|
||||
)?
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
x
|
||||
|
@ -625,7 +630,7 @@ mod sqlite3_m {
|
|||
let x = stmt
|
||||
.query_map(
|
||||
sqlite3::params![mailbox_hash as i64, env_hash as i64],
|
||||
|row| row.get(0),
|
||||
|row| Ok(row.get(0)?),
|
||||
)?
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
x
|
||||
|
@ -648,26 +653,53 @@ pub(super) async fn fetch_cached_envs(state: &mut FetchState) -> Result<Option<V
|
|||
ref uid_store,
|
||||
cache_handle: _,
|
||||
} = state;
|
||||
debug!(uid_store.keep_offline_cache);
|
||||
let mailbox_hash = *mailbox_hash;
|
||||
if !uid_store.keep_offline_cache {
|
||||
return Ok(None);
|
||||
}
|
||||
{
|
||||
let mut conn = connection.lock().await;
|
||||
match conn.load_cache(mailbox_hash).await {
|
||||
None => Ok(None),
|
||||
match debug!(conn.load_cache(mailbox_hash).await) {
|
||||
None => return Ok(None),
|
||||
Some(Ok(env_hashes)) => {
|
||||
uid_store
|
||||
.mailboxes
|
||||
.lock()
|
||||
.await
|
||||
.entry(mailbox_hash)
|
||||
.and_modify(|entry| {
|
||||
entry
|
||||
.exists
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert_set(env_hashes.iter().cloned().collect());
|
||||
let env_lck = uid_store.envelopes.lock().unwrap();
|
||||
entry.unseen.lock().unwrap().insert_set(
|
||||
env_hashes
|
||||
.iter()
|
||||
.filter_map(|h| {
|
||||
if !env_lck[h].inner.is_seen() {
|
||||
Some(*h)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
});
|
||||
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 debug!(Err(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,20 +63,54 @@ impl ImapConnection {
|
|||
Ok(v) => v,
|
||||
Err(err) => return Some(Err(err)),
|
||||
};
|
||||
match cache_handle.mailbox_state(mailbox_hash) {
|
||||
match debug!(cache_handle.mailbox_state(mailbox_hash)) {
|
||||
Err(err) => return Some(Err(err)),
|
||||
Ok(Some(())) => {}
|
||||
Ok(None) => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
match cache_handle.envelopes(mailbox_hash) {
|
||||
match debug!(cache_handle.envelopes(mailbox_hash)) {
|
||||
Ok(Some(envs)) => Some(Ok(envs)),
|
||||
Ok(None) => None,
|
||||
Err(err) => Some(Err(err)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn build_cache(
|
||||
&mut self,
|
||||
cache_handle: &mut Box<dyn ImapCache>,
|
||||
mailbox_hash: MailboxHash,
|
||||
) -> Result<()> {
|
||||
debug!("build_cache {}", mailbox_hash);
|
||||
let mut response = Vec::with_capacity(8 * 1024);
|
||||
// 1 get uidvalidity, highestmodseq
|
||||
let select_response = self
|
||||
.select_mailbox(mailbox_hash, &mut response, true)
|
||||
.await?
|
||||
.unwrap();
|
||||
self.uid_store
|
||||
.uidvalidity
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(mailbox_hash, select_response.uidvalidity);
|
||||
if let Some(v) = select_response.highestmodseq {
|
||||
self.uid_store
|
||||
.highestmodseqs
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(mailbox_hash, v);
|
||||
}
|
||||
cache_handle.clear(mailbox_hash, &select_response)?;
|
||||
self.send_command(b"UID FETCH 1:* (UID FLAGS ENVELOPE BODYSTRUCTURE)")
|
||||
.await?;
|
||||
self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
|
||||
.await?;
|
||||
let fetches = protocol_parser::fetch_responses(&response)?.1;
|
||||
cache_handle.insert_envelopes(mailbox_hash, &fetches)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
//rfc4549_Synchronization_Operations_for_Disconnected_IMAP4_Clients
|
||||
pub async fn resync_basic(
|
||||
&mut self,
|
||||
|
@ -116,10 +150,16 @@ impl ImapConnection {
|
|||
)
|
||||
};
|
||||
let mut new_unseen = BTreeSet::default();
|
||||
debug!("current_uidvalidity is {}", current_uidvalidity);
|
||||
debug!("max_uid is {}", max_uid);
|
||||
let select_response = self
|
||||
.select_mailbox(mailbox_hash, &mut response, true)
|
||||
.await?
|
||||
.unwrap();
|
||||
debug!(
|
||||
"select_response.uidvalidity is {}",
|
||||
select_response.uidvalidity
|
||||
);
|
||||
// 1. check UIDVALIDITY. If fail, discard cache and rebuild
|
||||
if select_response.uidvalidity != current_uidvalidity {
|
||||
cache_handle.clear(mailbox_hash, &select_response)?;
|
||||
|
@ -130,7 +170,7 @@ impl ImapConnection {
|
|||
// 2. tag1 UID FETCH <lastseenuid+1>:* <descriptors>
|
||||
self.send_command(
|
||||
format!(
|
||||
"UID FETCH {}:* (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)",
|
||||
"UID FETCH {}:* (UID FLAGS ENVELOPE BODYSTRUCTURE)",
|
||||
max_uid + 1
|
||||
)
|
||||
.as_bytes(),
|
||||
|
@ -149,22 +189,18 @@ impl ImapConnection {
|
|||
ref uid,
|
||||
ref mut envelope,
|
||||
ref mut flags,
|
||||
ref references,
|
||||
..
|
||||
} in v.iter_mut()
|
||||
{
|
||||
let uid = uid.unwrap();
|
||||
let env = envelope.as_mut().unwrap();
|
||||
env.set_hash(generate_envelope_hash(&mailbox_path, &uid));
|
||||
if let Some(value) = references {
|
||||
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() {
|
||||
if !flags.intersects(Flag::SEEN) {
|
||||
new_unseen.insert(env.hash());
|
||||
}
|
||||
env.set_flags(*flags);
|
||||
for f in keywords {
|
||||
let hash = tag_hash!(f);
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
|
@ -175,14 +211,14 @@ impl ImapConnection {
|
|||
}
|
||||
}
|
||||
{
|
||||
cache_handle
|
||||
debug!(cache_handle
|
||||
.insert_envelopes(mailbox_hash, &v)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not save envelopes in cache for mailbox {}",
|
||||
mailbox_path
|
||||
)
|
||||
})?;
|
||||
}))?;
|
||||
}
|
||||
|
||||
for FetchResponse {
|
||||
|
@ -216,17 +252,14 @@ impl ImapConnection {
|
|||
payload.push((uid, env));
|
||||
}
|
||||
debug!("sending payload for {}", mailbox_hash);
|
||||
let payload_hash_set: BTreeSet<_> =
|
||||
payload.iter().map(|(_, env)| env.hash()).collect::<_>();
|
||||
{
|
||||
let mut unseen_lck = unseen.lock().unwrap();
|
||||
for &seen_env_hash in payload_hash_set.difference(&new_unseen) {
|
||||
unseen_lck.remove(seen_env_hash);
|
||||
}
|
||||
|
||||
unseen_lck.insert_set(new_unseen);
|
||||
}
|
||||
mailbox_exists.lock().unwrap().insert_set(payload_hash_set);
|
||||
unseen
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert_existing_set(new_unseen.iter().cloned().collect());
|
||||
mailbox_exists
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert_existing_set(payload.iter().map(|(_, env)| env.hash()).collect::<_>());
|
||||
// 3. tag2 UID FETCH 1:<lastseenuid> FLAGS
|
||||
if max_uid == 0 {
|
||||
self.send_command("UID FETCH 1:* FLAGS".as_bytes()).await?;
|
||||
|
@ -337,6 +370,9 @@ impl ImapConnection {
|
|||
.unwrap()
|
||||
.get(&mailbox_hash)
|
||||
.cloned();
|
||||
debug!(&cached_uidvalidity);
|
||||
debug!(&cached_max_uid);
|
||||
debug!(&cached_highestmodseq);
|
||||
if cached_uidvalidity.is_none()
|
||||
|| cached_max_uid.is_none()
|
||||
|| cached_highestmodseq.is_none()
|
||||
|
@ -363,11 +399,17 @@ impl ImapConnection {
|
|||
)
|
||||
};
|
||||
let mut new_unseen = BTreeSet::default();
|
||||
debug!("current_uidvalidity is {}", cached_uidvalidity);
|
||||
debug!("max_uid is {}", cached_max_uid);
|
||||
// 1. check UIDVALIDITY. If fail, discard cache and rebuild
|
||||
let select_response = self
|
||||
.select_mailbox(mailbox_hash, &mut response, true)
|
||||
.await?
|
||||
.unwrap();
|
||||
debug!(
|
||||
"select_response.uidvalidity is {}",
|
||||
select_response.uidvalidity
|
||||
);
|
||||
if select_response.uidvalidity != cached_uidvalidity {
|
||||
// 1a) Check the mailbox UIDVALIDITY (see section 4.1 for more
|
||||
//details) with SELECT/EXAMINE/STATUS.
|
||||
|
@ -415,7 +457,7 @@ impl ImapConnection {
|
|||
// 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 BODYSTRUCTURE) (CHANGEDSINCE {})",
|
||||
cached_max_uid + 1,
|
||||
cached_highestmodseq,
|
||||
)
|
||||
|
@ -435,22 +477,18 @@ impl ImapConnection {
|
|||
ref uid,
|
||||
ref mut envelope,
|
||||
ref mut flags,
|
||||
ref references,
|
||||
..
|
||||
} in v.iter_mut()
|
||||
{
|
||||
let uid = uid.unwrap();
|
||||
let env = envelope.as_mut().unwrap();
|
||||
env.set_hash(generate_envelope_hash(&mailbox_path, &uid));
|
||||
if let Some(value) = references {
|
||||
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() {
|
||||
if !flags.intersects(Flag::SEEN) {
|
||||
new_unseen.insert(env.hash());
|
||||
}
|
||||
env.set_flags(*flags);
|
||||
for f in keywords {
|
||||
let hash = tag_hash!(f);
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
|
@ -461,14 +499,14 @@ impl ImapConnection {
|
|||
}
|
||||
}
|
||||
{
|
||||
cache_handle
|
||||
debug!(cache_handle
|
||||
.insert_envelopes(mailbox_hash, &v)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not save envelopes in cache for mailbox {}",
|
||||
mailbox_path
|
||||
)
|
||||
})?;
|
||||
}))?;
|
||||
}
|
||||
|
||||
for FetchResponse { uid, envelope, .. } in v {
|
||||
|
@ -496,17 +534,14 @@ impl ImapConnection {
|
|||
payload.push((uid, env));
|
||||
}
|
||||
debug!("sending payload for {}", mailbox_hash);
|
||||
let payload_hash_set: BTreeSet<_> =
|
||||
payload.iter().map(|(_, env)| env.hash()).collect::<_>();
|
||||
{
|
||||
let mut unseen_lck = unseen.lock().unwrap();
|
||||
for &seen_env_hash in payload_hash_set.difference(&new_unseen) {
|
||||
unseen_lck.remove(seen_env_hash);
|
||||
}
|
||||
|
||||
unseen_lck.insert_set(new_unseen);
|
||||
}
|
||||
mailbox_exists.lock().unwrap().insert_set(payload_hash_set);
|
||||
unseen
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert_existing_set(new_unseen.iter().cloned().collect());
|
||||
mailbox_exists
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert_existing_set(payload.iter().map(|(_, env)| env.hash()).collect::<_>());
|
||||
// 3. tag2 UID FETCH 1:<lastseenuid> FLAGS
|
||||
if cached_max_uid == 0 {
|
||||
self.send_command(
|
||||
|
@ -625,11 +660,12 @@ impl ImapConnection {
|
|||
|
||||
pub async fn init_mailbox(&mut self, mailbox_hash: MailboxHash) -> Result<SelectResponse> {
|
||||
let mut response = Vec::with_capacity(8 * 1024);
|
||||
let (mailbox_path, mailbox_exists, permissions) = {
|
||||
let (mailbox_path, mailbox_exists, unseen, permissions) = {
|
||||
let f = &self.uid_store.mailboxes.lock().await[&mailbox_hash];
|
||||
(
|
||||
f.imap_path().to_string(),
|
||||
f.exists.clone(),
|
||||
f.unseen.clone(),
|
||||
f.permissions.clone(),
|
||||
)
|
||||
};
|
||||
|
@ -666,11 +702,14 @@ impl ImapConnection {
|
|||
permissions.set_flags = !select_response.read_only;
|
||||
permissions.rename_messages = !select_response.read_only;
|
||||
permissions.delete_messages = !select_response.read_only;
|
||||
{
|
||||
let mut mailbox_exists_lck = mailbox_exists.lock().unwrap();
|
||||
mailbox_exists_lck.clear();
|
||||
mailbox_exists_lck.set_not_yet_seen(select_response.exists);
|
||||
}
|
||||
mailbox_exists
|
||||
.lock()
|
||||
.unwrap()
|
||||
.set_not_yet_seen(select_response.exists);
|
||||
unseen
|
||||
.lock()
|
||||
.unwrap()
|
||||
.set_not_yet_seen(select_response.unseen);
|
||||
}
|
||||
if select_response.exists == 0 {
|
||||
return Ok(select_response);
|
||||
|
|
|
@ -44,7 +44,7 @@ use super::{Capabilities, ImapServerConf, UIDStore};
|
|||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum SyncPolicy {
|
||||
None,
|
||||
///rfc4549 `Synch Ops for Disconnected IMAP4 Clients` <https://tools.ietf.org/html/rfc4549>
|
||||
///rfc4549 `Synch Ops for Disconnected IMAP4 Clients` https://tools.ietf.org/html/rfc4549
|
||||
Basic,
|
||||
///rfc7162 `IMAP Extensions: Quick Flag Changes Resynchronization (CONDSTORE) and Quick Mailbox Resynchronization (QRESYNC)`
|
||||
Condstore,
|
||||
|
@ -63,7 +63,6 @@ pub struct ImapExtensionUse {
|
|||
pub idle: bool,
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
pub deflate: bool,
|
||||
pub oauth2: bool,
|
||||
}
|
||||
|
||||
impl Default for ImapExtensionUse {
|
||||
|
@ -73,7 +72,6 @@ impl Default for ImapExtensionUse {
|
|||
idle: true,
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
deflate: true,
|
||||
oauth2: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -115,67 +113,69 @@ pub struct ImapConnection {
|
|||
impl ImapStream {
|
||||
pub async fn new_connection(
|
||||
server_conf: &ImapServerConf,
|
||||
uid_store: &UIDStore,
|
||||
) -> Result<(Capabilities, ImapStream)> {
|
||||
use std::net::TcpStream;
|
||||
let path = &server_conf.server_hostname;
|
||||
|
||||
let cmd_id = 1;
|
||||
let stream = if server_conf.use_tls {
|
||||
(uid_store.event_consumer)(
|
||||
uid_store.account_hash,
|
||||
crate::backends::BackendEvent::AccountStateChange {
|
||||
message: "Establishing TLS connection.".into(),
|
||||
},
|
||||
);
|
||||
let mut connector = TlsConnector::builder();
|
||||
if server_conf.danger_accept_invalid_certs {
|
||||
connector.danger_accept_invalid_certs(true);
|
||||
}
|
||||
let connector = connector
|
||||
.build()
|
||||
.chain_err_kind(crate::error::ErrorKind::Network(
|
||||
crate::error::NetworkErrorKind::InvalidTLSConnection,
|
||||
))?;
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
|
||||
let addr = lookup_ipv4(path, server_conf.server_port)?;
|
||||
let addr = if let Ok(a) = lookup_ipv4(path, server_conf.server_port) {
|
||||
a
|
||||
} else {
|
||||
return Err(MeliError::new(format!(
|
||||
"Could not lookup address {}",
|
||||
&path
|
||||
)));
|
||||
};
|
||||
|
||||
let mut socket = AsyncWrapper::new(Connection::Tcp(
|
||||
if let Some(timeout) = server_conf.timeout {
|
||||
TcpStream::connect_timeout(&addr, timeout)?
|
||||
TcpStream::connect_timeout(&addr, timeout)
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?
|
||||
} else {
|
||||
TcpStream::connect(&addr)?
|
||||
TcpStream::connect(&addr).chain_err_kind(crate::error::ErrorKind::Network)?
|
||||
},
|
||||
))?;
|
||||
))
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
if server_conf.use_starttls {
|
||||
let err_fn = || {
|
||||
if server_conf.server_port == 993 {
|
||||
"STARTTLS failed. Server port is set to 993, which normally uses TLS. Maybe try disabling use_starttls."
|
||||
} else {
|
||||
"STARTTLS failed. Is the connection already encrypted?"
|
||||
}
|
||||
};
|
||||
let mut buf = vec![0; Connection::IO_BUF_SIZE];
|
||||
match server_conf.protocol {
|
||||
ImapProtocol::IMAP { .. } => socket
|
||||
.write_all(format!("M{} STARTTLS\r\n", cmd_id).as_bytes())
|
||||
.await
|
||||
.chain_err_summary(err_fn)?,
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?,
|
||||
ImapProtocol::ManageSieve => {
|
||||
socket.read(&mut buf).await.chain_err_summary(err_fn)?;
|
||||
socket
|
||||
.read(&mut buf)
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
socket
|
||||
.write_all(b"STARTTLS\r\n")
|
||||
.await
|
||||
.chain_err_summary(err_fn)?;
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
}
|
||||
}
|
||||
socket.flush().await.chain_err_summary(err_fn)?;
|
||||
socket
|
||||
.flush()
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
let mut response = Vec::with_capacity(1024);
|
||||
let mut broken = false;
|
||||
let now = Instant::now();
|
||||
|
||||
while now.elapsed().as_secs() < 3 {
|
||||
let len = socket.read(&mut buf).await.chain_err_summary(err_fn)?;
|
||||
let len = socket
|
||||
.read(&mut buf)
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
response.extend_from_slice(&buf[0..len]);
|
||||
match server_conf.protocol {
|
||||
ImapProtocol::IMAP { .. } => {
|
||||
|
@ -200,7 +200,7 @@ impl ImapStream {
|
|||
}
|
||||
if !broken {
|
||||
return Err(MeliError::new(format!(
|
||||
"Could not initiate STARTTLS negotiation to {}.",
|
||||
"Could not initiate TLS negotiation to {}.",
|
||||
path
|
||||
)));
|
||||
}
|
||||
|
@ -208,7 +208,9 @@ impl ImapStream {
|
|||
|
||||
{
|
||||
// FIXME: This is blocking
|
||||
let socket = socket.into_inner()?;
|
||||
let socket = socket
|
||||
.into_inner()
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
let mut conn_result = connector.connect(path, socket);
|
||||
if let Err(native_tls::HandshakeError::WouldBlock(midhandshake_stream)) =
|
||||
conn_result
|
||||
|
@ -224,17 +226,15 @@ impl ImapStream {
|
|||
midhandshake_stream = Some(stream);
|
||||
}
|
||||
p => {
|
||||
p.chain_err_kind(crate::error::ErrorKind::Network(
|
||||
crate::error::NetworkErrorKind::InvalidTLSConnection,
|
||||
))?;
|
||||
p.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AsyncWrapper::new(Connection::Tls(conn_result.chain_err_summary(|| {
|
||||
format!("Could not initiate TLS negotiation to {}.", path)
|
||||
})?))
|
||||
.chain_err_summary(|| format!("Could not initiate TLS negotiation to {}.", path))?
|
||||
AsyncWrapper::new(Connection::Tls(
|
||||
conn_result.chain_err_kind(crate::error::ErrorKind::Network)?,
|
||||
))
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?
|
||||
}
|
||||
} else {
|
||||
let addr = if let Ok(a) = lookup_ipv4(path, server_conf.server_port) {
|
||||
|
@ -247,11 +247,13 @@ impl ImapStream {
|
|||
};
|
||||
AsyncWrapper::new(Connection::Tcp(
|
||||
if let Some(timeout) = server_conf.timeout {
|
||||
TcpStream::connect_timeout(&addr, timeout)?
|
||||
TcpStream::connect_timeout(&addr, timeout)
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?
|
||||
} else {
|
||||
TcpStream::connect(&addr)?
|
||||
TcpStream::connect(&addr).chain_err_kind(crate::error::ErrorKind::Network)?
|
||||
},
|
||||
))?
|
||||
))
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?
|
||||
};
|
||||
if let Err(err) = stream
|
||||
.get_ref()
|
||||
|
@ -291,12 +293,6 @@ impl ImapStream {
|
|||
return Ok((Default::default(), ret));
|
||||
}
|
||||
|
||||
(uid_store.event_consumer)(
|
||||
uid_store.account_hash,
|
||||
crate::backends::BackendEvent::AccountStateChange {
|
||||
message: "Negotiating server capabilities.".into(),
|
||||
},
|
||||
);
|
||||
ret.send_command(b"CAPABILITY").await?;
|
||||
ret.read_response(&mut res).await?;
|
||||
let capabilities: std::result::Result<Vec<&[u8]>, _> = res
|
||||
|
@ -304,7 +300,7 @@ impl ImapStream {
|
|||
.find(|l| l.starts_with(b"* CAPABILITY"))
|
||||
.ok_or_else(|| MeliError::new(""))
|
||||
.and_then(|res| {
|
||||
protocol_parser::capabilities(res)
|
||||
protocol_parser::capabilities(&res)
|
||||
.map_err(|_| MeliError::new(""))
|
||||
.map(|(_, v)| v)
|
||||
});
|
||||
|
@ -338,62 +334,23 @@ impl ImapStream {
|
|||
.set_err_kind(crate::error::ErrorKind::Authentication));
|
||||
}
|
||||
|
||||
(uid_store.event_consumer)(
|
||||
uid_store.account_hash,
|
||||
crate::backends::BackendEvent::AccountStateChange {
|
||||
message: "Attempting authentication.".into(),
|
||||
},
|
||||
);
|
||||
match server_conf.protocol {
|
||||
ImapProtocol::IMAP {
|
||||
extension_use: ImapExtensionUse { oauth2, .. },
|
||||
} if oauth2 => {
|
||||
if !capabilities
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"AUTH=XOAUTH2"))
|
||||
{
|
||||
return Err(MeliError::new(format!(
|
||||
"Could not connect to {}: OAUTH2 is enabled but server did not return AUTH=XOAUTH2 capability. Returned capabilities were: {}",
|
||||
&server_conf.server_hostname,
|
||||
capabilities.iter().map(|capability|
|
||||
String::from_utf8_lossy(capability).to_string()).collect::<Vec<String>>().join(" ")
|
||||
)));
|
||||
}
|
||||
ret.send_command(
|
||||
format!("AUTHENTICATE XOAUTH2 {}", &server_conf.server_password).as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
_ => {
|
||||
ret.send_command(
|
||||
format!(
|
||||
r#"LOGIN "{}" {{{}}}"#,
|
||||
&server_conf
|
||||
.server_username
|
||||
.replace('\\', r#"\\"#)
|
||||
.replace('"', r#"\""#)
|
||||
.replace('{', r#"\{"#)
|
||||
.replace('}', r#"\}"#),
|
||||
&server_conf.server_password.as_bytes().len()
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
// wait for "+ Ready for literal data" reply
|
||||
ret.wait_for_continuation_request().await?;
|
||||
ret.send_literal(server_conf.server_password.as_bytes())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
let tag_start = format!("M{} ", (ret.cmd_id - 1));
|
||||
let mut capabilities = None;
|
||||
ret.send_command(
|
||||
format!(
|
||||
"LOGIN \"{}\" \"{}\"",
|
||||
&server_conf.server_username, &server_conf.server_password
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
let tag_start = format!("M{} ", (ret.cmd_id - 1));
|
||||
|
||||
loop {
|
||||
ret.read_lines(&mut res, &[], false).await?;
|
||||
let mut should_break = false;
|
||||
for l in res.split_rn() {
|
||||
if l.starts_with(b"* CAPABILITY") {
|
||||
capabilities = protocol_parser::capabilities(l)
|
||||
capabilities = protocol_parser::capabilities(&l)
|
||||
.map(|(_, capabilities)| {
|
||||
HashSet::from_iter(capabilities.into_iter().map(|s: &[u8]| s.to_vec()))
|
||||
})
|
||||
|
@ -468,6 +425,7 @@ impl ImapStream {
|
|||
if !termination_string.is_empty()
|
||||
&& ret[last_line_idx..].starts_with(termination_string)
|
||||
{
|
||||
debug!(&ret[last_line_idx..]);
|
||||
if !keep_termination_string {
|
||||
ret.splice(last_line_idx.., std::iter::empty::<u8>());
|
||||
}
|
||||
|
@ -479,8 +437,8 @@ impl ImapStream {
|
|||
last_line_idx += pos + b"\r\n".len();
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(MeliError::from(err));
|
||||
Err(e) => {
|
||||
return Err(MeliError::from(e).set_err_kind(crate::error::ErrorKind::Network));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -496,7 +454,7 @@ impl ImapStream {
|
|||
}
|
||||
|
||||
pub async fn send_command(&mut self, command: &[u8]) -> Result<()> {
|
||||
_ = timeout(
|
||||
if let Err(err) = timeout(
|
||||
self.timeout,
|
||||
try_await(async move {
|
||||
let command = command.trim();
|
||||
|
@ -517,35 +475,51 @@ impl ImapStream {
|
|||
self.stream.flush().await?;
|
||||
match self.protocol {
|
||||
ImapProtocol::IMAP { .. } => {
|
||||
if !command.starts_with(b"LOGIN") {
|
||||
debug!("sent: M{} {}", self.cmd_id - 1, unsafe {
|
||||
std::str::from_utf8_unchecked(command)
|
||||
});
|
||||
} else {
|
||||
debug!("sent: M{} LOGIN ..", self.cmd_id - 1);
|
||||
}
|
||||
debug!("sent: M{} {}", self.cmd_id - 1, unsafe {
|
||||
std::str::from_utf8_unchecked(command)
|
||||
});
|
||||
}
|
||||
ImapProtocol::ManageSieve => {}
|
||||
}
|
||||
Ok(())
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
.await
|
||||
{
|
||||
Err(err.set_err_kind(crate::error::ErrorKind::Network))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_literal(&mut self, data: &[u8]) -> Result<()> {
|
||||
self.stream.write_all(data).await?;
|
||||
self.stream.write_all(b"\r\n").await?;
|
||||
self.stream.flush().await?;
|
||||
Ok(())
|
||||
if let Err(err) = try_await(async move {
|
||||
self.stream.write_all(data).await?;
|
||||
self.stream.write_all(b"\r\n").await?;
|
||||
self.stream.flush().await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
{
|
||||
Err(err.set_err_kind(crate::error::ErrorKind::Network))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_raw(&mut self, raw: &[u8]) -> Result<()> {
|
||||
self.stream.write_all(raw).await?;
|
||||
self.stream.write_all(b"\r\n").await?;
|
||||
self.stream.flush().await?;
|
||||
Ok(())
|
||||
if let Err(err) = try_await(async move {
|
||||
self.stream.write_all(raw).await?;
|
||||
self.stream.write_all(b"\r\n").await?;
|
||||
self.stream.flush().await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
{
|
||||
Err(err.set_err_kind(crate::error::ErrorKind::Network))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -569,19 +543,13 @@ impl ImapConnection {
|
|||
pub fn connect<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
if let (time, ref mut status @ Ok(())) = *self.uid_store.is_online.lock().unwrap() {
|
||||
if SystemTime::now().duration_since(time).unwrap_or_default()
|
||||
>= IMAP_PROTOCOL_TIMEOUT
|
||||
{
|
||||
let err = MeliError::new(format!(
|
||||
"Connection timed out after {} seconds",
|
||||
IMAP_PROTOCOL_TIMEOUT.as_secs()
|
||||
))
|
||||
.set_kind(ErrorKind::Timeout);
|
||||
if SystemTime::now().duration_since(time).unwrap_or_default() >= IMAP_PROTOCOL_TIMEOUT {
|
||||
let err = MeliError::new("Connection timed out").set_kind(ErrorKind::Timeout);
|
||||
*status = Err(err.clone());
|
||||
self.stream = Err(err);
|
||||
}
|
||||
}
|
||||
if self.stream.is_ok() {
|
||||
if debug!(self.stream.is_ok()) {
|
||||
let mut ret = Vec::new();
|
||||
if let Err(err) = try_await(async {
|
||||
self.send_command(b"NOOP").await?;
|
||||
|
@ -594,12 +562,12 @@ impl ImapConnection {
|
|||
} else {
|
||||
debug!(
|
||||
"connect(): connection is probably alive, NOOP returned {:?}",
|
||||
&String::from_utf8_lossy(&ret)
|
||||
&ret
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let new_stream = ImapStream::new_connection(&self.server_conf, &self.uid_store).await;
|
||||
let new_stream = debug!(ImapStream::new_connection(&self.server_conf).await);
|
||||
if let Err(err) = new_stream.as_ref() {
|
||||
self.uid_store.is_online.lock().unwrap().1 = Err(err.clone());
|
||||
} else {
|
||||
|
@ -615,7 +583,6 @@ impl ImapConnection {
|
|||
#[cfg(feature = "deflate_compression")]
|
||||
deflate,
|
||||
idle: _idle,
|
||||
oauth2: _,
|
||||
},
|
||||
} => {
|
||||
if capabilities.contains(&b"CONDSTORE"[..]) && condstore {
|
||||
|
@ -708,21 +675,17 @@ impl ImapConnection {
|
|||
{
|
||||
debug!(
|
||||
"Received expected NO response: {:?} {:?}",
|
||||
response_code,
|
||||
String::from_utf8_lossy(&response)
|
||||
response_code, response
|
||||
);
|
||||
}
|
||||
ImapResponse::No(ref response_code) => {
|
||||
debug!(
|
||||
"Received NO response: {:?} {:?}",
|
||||
response_code,
|
||||
String::from_utf8_lossy(&response)
|
||||
);
|
||||
//FIXME return error
|
||||
debug!("Received NO response: {:?} {:?}", response_code, response);
|
||||
(self.uid_store.event_consumer)(
|
||||
self.uid_store.account_hash,
|
||||
crate::backends::BackendEvent::Notice {
|
||||
description: response_code.to_string(),
|
||||
content: None,
|
||||
description: None,
|
||||
content: response_code.to_string(),
|
||||
level: crate::logging::LoggingLevel::ERROR,
|
||||
},
|
||||
);
|
||||
|
@ -730,16 +693,13 @@ impl ImapConnection {
|
|||
return r.into();
|
||||
}
|
||||
ImapResponse::Bad(ref response_code) => {
|
||||
debug!(
|
||||
"Received BAD response: {:?} {:?}",
|
||||
response_code,
|
||||
String::from_utf8_lossy(&response)
|
||||
);
|
||||
//FIXME return error
|
||||
debug!("Received BAD response: {:?} {:?}", response_code, response);
|
||||
(self.uid_store.event_consumer)(
|
||||
self.uid_store.account_hash,
|
||||
crate::backends::BackendEvent::Notice {
|
||||
description: response_code.to_string(),
|
||||
content: None,
|
||||
description: None,
|
||||
content: response_code.to_string(),
|
||||
level: crate::logging::LoggingLevel::ERROR,
|
||||
},
|
||||
);
|
||||
|
@ -860,9 +820,9 @@ impl ImapConnection {
|
|||
debug!(
|
||||
"{} select response {}",
|
||||
imap_path,
|
||||
String::from_utf8_lossy(ret)
|
||||
String::from_utf8_lossy(&ret)
|
||||
);
|
||||
let select_response = protocol_parser::select_response(ret).chain_err_summary(|| {
|
||||
let select_response = protocol_parser::select_response(&ret).chain_err_summary(|| {
|
||||
format!("Could not parse select response for mailbox {}", imap_path)
|
||||
})?;
|
||||
{
|
||||
|
@ -943,8 +903,8 @@ impl ImapConnection {
|
|||
.await?;
|
||||
self.read_response(ret, RequiredResponses::EXAMINE_REQUIRED)
|
||||
.await?;
|
||||
debug!("examine response {}", String::from_utf8_lossy(ret));
|
||||
let select_response = protocol_parser::select_response(ret).chain_err_summary(|| {
|
||||
debug!("examine response {}", String::from_utf8_lossy(&ret));
|
||||
let select_response = protocol_parser::select_response(&ret).chain_err_summary(|| {
|
||||
format!("Could not parse select response for mailbox {}", imap_path)
|
||||
})?;
|
||||
self.stream.as_mut()?.current_mailbox = MailboxSelection::Examine(mailbox_hash);
|
||||
|
@ -965,38 +925,45 @@ impl ImapConnection {
|
|||
|
||||
pub async fn unselect(&mut self) -> Result<()> {
|
||||
match self.stream.as_mut()?.current_mailbox.take() {
|
||||
MailboxSelection::Examine(_) | MailboxSelection::Select(_) => {
|
||||
let mut response = Vec::with_capacity(8 * 1024);
|
||||
if self
|
||||
.uid_store
|
||||
.capabilities
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"UNSELECT"))
|
||||
{
|
||||
self.send_command(b"UNSELECT").await?;
|
||||
self.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
} else {
|
||||
/* `RFC3691 - UNSELECT Command` states: "[..] IMAP4 provides this
|
||||
* functionality (via a SELECT command with a nonexistent mailbox name or
|
||||
* reselecting the same mailbox with EXAMINE command)[..]
|
||||
*/
|
||||
let mut nonexistent = "blurdybloop".to_string();
|
||||
MailboxSelection::Examine(_) |
|
||||
MailboxSelection::Select(_) => {
|
||||
let mut response = Vec::with_capacity(8 * 1024);
|
||||
if self
|
||||
.uid_store
|
||||
.capabilities
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"UNSELECT"))
|
||||
{
|
||||
let mailboxes = self.uid_store.mailboxes.lock().await;
|
||||
while mailboxes.values().any(|m| m.imap_path() == nonexistent) {
|
||||
nonexistent.push('p');
|
||||
self.send_command(b"UNSELECT").await?;
|
||||
self.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
} else {
|
||||
/* `RFC3691 - UNSELECT Command` states: "[..] IMAP4 provides this
|
||||
* functionality (via a SELECT command with a nonexistent mailbox name or
|
||||
* reselecting the same mailbox with EXAMINE command)[..]
|
||||
*/
|
||||
let mut nonexistent = "blurdybloop".to_string();
|
||||
{
|
||||
let mailboxes = self.uid_store.mailboxes.lock().await;
|
||||
while mailboxes.values().any(|m| m.imap_path() == nonexistent) {
|
||||
nonexistent.push('p');
|
||||
}
|
||||
}
|
||||
self.send_command(
|
||||
format!(
|
||||
"SELECT \"{}\"",
|
||||
nonexistent
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
self.read_response(&mut response, RequiredResponses::NO_REQUIRED)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
self.send_command(format!("SELECT \"{}\"", nonexistent).as_bytes())
|
||||
.await?;
|
||||
self.read_response(&mut response, RequiredResponses::NO_REQUIRED)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
MailboxSelection::None => {}
|
||||
MailboxSelection::None => {},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1020,10 +987,15 @@ impl ImapConnection {
|
|||
.await?;
|
||||
self.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await?;
|
||||
debug!("uid search response {:?}", &response);
|
||||
let mut msn_index_lck = self.uid_store.msn_index.lock().unwrap();
|
||||
let msn_index = msn_index_lck.entry(mailbox_hash).or_default();
|
||||
let _ = msn_index.drain(low - 1..);
|
||||
msn_index.extend(protocol_parser::search_results(&response)?.1.into_iter());
|
||||
msn_index.extend(
|
||||
debug!(protocol_parser::search_results(&response))?
|
||||
.1
|
||||
.into_iter(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -1064,6 +1036,7 @@ impl ImapBlockingConnection {
|
|||
let mut prev_failure = None;
|
||||
async move {
|
||||
if self.conn.stream.is_err() {
|
||||
debug!(&self.conn.stream);
|
||||
return None;
|
||||
}
|
||||
loop {
|
||||
|
@ -1097,6 +1070,7 @@ async fn read(
|
|||
}
|
||||
Ok(b) => {
|
||||
result.extend_from_slice(&buf[0..b]);
|
||||
debug!(unsafe { std::str::from_utf8_unchecked(result) });
|
||||
if let Some(pos) = result.find(b"\r\n") {
|
||||
*prev_res_length = pos + b"\r\n".len();
|
||||
return Some(result[0..*prev_res_length].to_vec());
|
||||
|
@ -1104,7 +1078,9 @@ async fn read(
|
|||
*prev_failure = None;
|
||||
}
|
||||
Err(_err) => {
|
||||
*err = Some(Into::<MeliError>::into(_err));
|
||||
debug!(&conn.stream);
|
||||
debug!(&_err);
|
||||
*err = Some(Into::<MeliError>::into(_err).set_kind(crate::error::ErrorKind::Network));
|
||||
*break_flag = true;
|
||||
*prev_failure = Some(SystemTime::now());
|
||||
}
|
||||
|
|
|
@ -21,11 +21,80 @@
|
|||
|
||||
use super::protocol_parser::SelectResponse;
|
||||
use crate::backends::{
|
||||
BackendMailbox, LazyCountSet, Mailbox, MailboxHash, MailboxPermissions, SpecialUsageMailbox,
|
||||
BackendMailbox, Mailbox, MailboxHash, MailboxPermissions, SpecialUsageMailbox,
|
||||
};
|
||||
use crate::email::EnvelopeHash;
|
||||
use crate::error::*;
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct LazyCountSet {
|
||||
not_yet_seen: usize,
|
||||
set: BTreeSet<EnvelopeHash>,
|
||||
}
|
||||
|
||||
impl LazyCountSet {
|
||||
pub fn set_not_yet_seen(&mut self, new_val: usize) {
|
||||
self.not_yet_seen = new_val;
|
||||
}
|
||||
|
||||
pub fn insert_existing(&mut self, new_val: EnvelopeHash) -> bool {
|
||||
if self.not_yet_seen == 0 {
|
||||
false
|
||||
} else {
|
||||
self.not_yet_seen -= 1;
|
||||
self.set.insert(new_val);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_existing_set(&mut self, set: BTreeSet<EnvelopeHash>) -> bool {
|
||||
debug!("insert_existing_set {:?}", &set);
|
||||
if self.not_yet_seen < set.len() {
|
||||
false
|
||||
} else {
|
||||
self.not_yet_seen -= set.len();
|
||||
self.set.extend(set.into_iter());
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.set.len() + self.not_yet_seen
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn clear(&mut self) {
|
||||
self.set.clear();
|
||||
self.not_yet_seen = 0;
|
||||
}
|
||||
|
||||
pub fn insert_new(&mut self, new_val: EnvelopeHash) {
|
||||
self.set.insert(new_val);
|
||||
}
|
||||
|
||||
pub fn insert_set(&mut self, set: BTreeSet<EnvelopeHash>) {
|
||||
debug!("insert__set {:?}", &set);
|
||||
self.set.extend(set.into_iter());
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, new_val: EnvelopeHash) -> bool {
|
||||
self.set.remove(&new_val)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lazy_count_set() {
|
||||
let mut new = LazyCountSet::default();
|
||||
new.set_not_yet_seen(10);
|
||||
for i in 0..10 {
|
||||
assert!(new.insert_existing(i));
|
||||
}
|
||||
assert!(!new.insert_existing(10));
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ImapMailbox {
|
||||
pub hash: MailboxHash,
|
||||
|
@ -43,31 +112,12 @@ pub struct ImapMailbox {
|
|||
pub permissions: Arc<Mutex<MailboxPermissions>>,
|
||||
pub exists: Arc<Mutex<LazyCountSet>>,
|
||||
pub unseen: Arc<Mutex<LazyCountSet>>,
|
||||
pub warm: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
impl ImapMailbox {
|
||||
pub fn imap_path(&self) -> &str {
|
||||
&self.imap_path
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// Mailbox contents have been fetched at least once during this execution
|
||||
#[inline(always)]
|
||||
pub fn is_warm(&self) -> bool {
|
||||
*self.warm.lock().unwrap()
|
||||
}
|
||||
|
||||
/// Mailbox contents have not been fetched at all during this execution
|
||||
#[inline(always)]
|
||||
pub fn is_cold(&self) -> bool {
|
||||
!self.is_warm()
|
||||
}
|
||||
}
|
||||
|
||||
impl BackendMailbox for ImapMailbox {
|
||||
|
|
|
@ -21,24 +21,18 @@
|
|||
|
||||
use super::{ImapConnection, ImapProtocol, ImapServerConf, UIDStore};
|
||||
use crate::conf::AccountSettings;
|
||||
use crate::email::parser::IResult;
|
||||
use crate::error::{MeliError, Result};
|
||||
use crate::get_conf_val;
|
||||
use crate::imap::RequiredResponses;
|
||||
use nom::{
|
||||
branch::alt, bytes::complete::tag, combinator::map, multi::separated_list1,
|
||||
sequence::separated_pair,
|
||||
branch::alt, bytes::complete::tag, combinator::map, error::ErrorKind,
|
||||
multi::separated_nonempty_list, sequence::separated_pair, IResult,
|
||||
};
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::SystemTime;
|
||||
|
||||
pub struct ManageSieveConnection {
|
||||
pub inner: ImapConnection,
|
||||
}
|
||||
|
||||
pub fn managesieve_capabilities(input: &[u8]) -> Result<Vec<(&[u8], &[u8])>> {
|
||||
let (_, ret) = separated_list1(
|
||||
let (_, ret) = separated_nonempty_list(
|
||||
tag(b"\r\n"),
|
||||
alt((
|
||||
separated_pair(quoted_raw, tag(b" "), quoted_raw),
|
||||
|
@ -48,225 +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 super::*;
|
||||
use nom::bytes::complete::tag;
|
||||
pub use nom::bytes::complete::{is_not, tag_no_case};
|
||||
use nom::character::complete::crlf;
|
||||
use nom::combinator::{iterator, map, opt};
|
||||
pub use nom::sequence::{delimited, pair, preceded, terminated};
|
||||
|
||||
pub fn sieve_name(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
crate::backends::imap::protocol_parser::string_token(input)
|
||||
}
|
||||
|
||||
// *(sieve-name [SP "ACTIVE"] CRLF)
|
||||
// response-oknobye
|
||||
pub fn listscripts(input: &[u8]) -> IResult<&[u8], Vec<(&[u8], bool)>> {
|
||||
let mut it = iterator(
|
||||
input,
|
||||
alt((
|
||||
terminated(
|
||||
map(terminated(sieve_name, tag_no_case(b" ACTIVE")), |r| {
|
||||
(r, true)
|
||||
}),
|
||||
crlf,
|
||||
),
|
||||
terminated(map(sieve_name, |r| (r, false)), crlf),
|
||||
)),
|
||||
);
|
||||
|
||||
let parsed = (&mut it).collect::<Vec<(&[u8], bool)>>();
|
||||
let res: IResult<_, _> = it.finish();
|
||||
let (rest, _) = res?;
|
||||
Ok((rest, parsed))
|
||||
}
|
||||
|
||||
// response-getscript = (sieve-script CRLF response-ok) /
|
||||
// response-nobye
|
||||
pub fn getscript(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
sieve_name(input)
|
||||
}
|
||||
|
||||
pub fn response_oknobye(input: &[u8]) -> IResult<&[u8], ManageSieveResponse> {
|
||||
alt((
|
||||
map(
|
||||
terminated(
|
||||
pair(
|
||||
preceded(
|
||||
tag_no_case(b"ok"),
|
||||
opt(preceded(
|
||||
tag(b" "),
|
||||
delimited(tag(b"("), is_not(")"), tag(b")")),
|
||||
)),
|
||||
),
|
||||
opt(preceded(tag(b" "), sieve_name)),
|
||||
),
|
||||
crlf,
|
||||
),
|
||||
|(code, message)| ManageSieveResponse::Ok { code, message },
|
||||
),
|
||||
map(
|
||||
terminated(
|
||||
pair(
|
||||
preceded(
|
||||
alt((tag_no_case(b"no"), tag_no_case(b"bye"))),
|
||||
opt(preceded(
|
||||
tag(b" "),
|
||||
delimited(tag(b"("), is_not(")"), tag(b")")),
|
||||
)),
|
||||
),
|
||||
opt(preceded(tag(b" "), sieve_name)),
|
||||
),
|
||||
crlf,
|
||||
),
|
||||
|(code, message)| ManageSieveResponse::NoBye { code, message },
|
||||
),
|
||||
))(input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_managesieve_listscripts() {
|
||||
let input_1 = b"\"summer_script\"\r\n\"vacation_script\"\r\n{13}\r\nclever\"script\r\n\"main_script\" ACTIVE\r\nOK";
|
||||
assert_eq!(
|
||||
terminated(listscripts, tag_no_case(b"OK"))(input_1),
|
||||
Ok((
|
||||
&b""[..],
|
||||
vec![
|
||||
(&b"summer_script"[..], false),
|
||||
(&b"vacation_script"[..], false),
|
||||
(&b"clever\"script"[..], false),
|
||||
(&b"main_script"[..], true)
|
||||
]
|
||||
))
|
||||
);
|
||||
|
||||
let input_2 = b"\"summer_script\"\r\n\"main_script\" active\r\nok";
|
||||
assert_eq!(
|
||||
terminated(listscripts, tag_no_case(b"OK"))(input_2),
|
||||
Ok((
|
||||
&b""[..],
|
||||
vec![(&b"summer_script"[..], false), (&b"main_script"[..], true)]
|
||||
))
|
||||
);
|
||||
let input_3 = b"ok";
|
||||
assert_eq!(
|
||||
terminated(listscripts, tag_no_case(b"OK"))(input_3),
|
||||
Ok((&b""[..], vec![]))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_managesieve_general() {
|
||||
assert_eq!(managesieve_capabilities(b"\"IMPLEMENTATION\" \"Dovecot Pigeonhole\"\r\n\"SIEVE\" \"fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext\"\r\n\"NOTIFY\" \"mailto\"\r\n\"SASL\" \"PLAIN\"\r\n\"STARTTLS\"\r\n\"VERSION\" \"1.0\"\r\n").unwrap(), vec![
|
||||
(&b"IMPLEMENTATION"[..],&b"Dovecot Pigeonhole"[..]),
|
||||
(&b"SIEVE"[..],&b"fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date index ihave duplicate mime foreverypart extracttext"[..]),
|
||||
(&b"NOTIFY"[..],&b"mailto"[..]),
|
||||
(&b"SASL"[..],&b"PLAIN"[..]),
|
||||
(&b"STARTTLS"[..], &b""[..]),
|
||||
(&b"VERSION"[..],&b"1.0"[..])]
|
||||
|
||||
);
|
||||
|
||||
let response_ok = b"OK (WARNINGS) \"line 8: server redirect action limit is 2, this redirect might be ignored\"\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_ok),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::Ok {
|
||||
code: Some(&b"WARNINGS"[..]),
|
||||
message: Some(&b"line 8: server redirect action limit is 2, this redirect might be ignored"[..]),
|
||||
}
|
||||
))
|
||||
);
|
||||
let response_ok = b"OK (WARNINGS)\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_ok),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::Ok {
|
||||
code: Some(&b"WARNINGS"[..]),
|
||||
message: None,
|
||||
}
|
||||
))
|
||||
);
|
||||
let response_ok =
|
||||
b"OK \"line 8: server redirect action limit is 2, this redirect might be ignored\"\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_ok),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::Ok {
|
||||
code: None,
|
||||
message: Some(&b"line 8: server redirect action limit is 2, this redirect might be ignored"[..]),
|
||||
}
|
||||
))
|
||||
);
|
||||
let response_ok = b"Ok\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_ok),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::Ok {
|
||||
code: None,
|
||||
message: None,
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
let response_nobye = b"No (NONEXISTENT) \"There is no script by that name\"\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_nobye),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::NoBye {
|
||||
code: Some(&b"NONEXISTENT"[..]),
|
||||
message: Some(&b"There is no script by that name"[..]),
|
||||
}
|
||||
))
|
||||
);
|
||||
let response_nobye = b"No (NONEXISTENT) {31}\r\nThere is no script by that name\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_nobye),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::NoBye {
|
||||
code: Some(&b"NONEXISTENT"[..]),
|
||||
message: Some(&b"There is no script by that name"[..]),
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
let response_nobye = b"No\r\n";
|
||||
assert_eq!(
|
||||
response_oknobye(response_nobye),
|
||||
Ok((
|
||||
&b""[..],
|
||||
ManageSieveResponse::NoBye {
|
||||
code: None,
|
||||
message: None,
|
||||
}
|
||||
))
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Return a byte sequence surrounded by "s and decoded if necessary
|
||||
pub fn quoted_raw(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
if input.is_empty() || input[0] != b'"' {
|
||||
return Err(nom::Err::Error((input, "empty").into()));
|
||||
return Err(nom::Err::Error((input, ErrorKind::Tag)));
|
||||
}
|
||||
|
||||
let mut i = 1;
|
||||
|
@ -277,199 +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(MeliError::new("Account is uninitialised.")),
|
||||
))),
|
||||
..UIDStore::new(
|
||||
account_hash,
|
||||
Arc::new(account_name),
|
||||
event_consumer,
|
||||
server_conf.timeout,
|
||||
)
|
||||
});
|
||||
Ok(Self {
|
||||
inner: ImapConnection::new_connection(&server_conf, uid_store),
|
||||
})
|
||||
}
|
||||
pub trait ManageSieve {
|
||||
fn havespace(&mut self) -> Result<()>;
|
||||
fn putscript(&mut self) -> Result<()>;
|
||||
|
||||
pub async fn havespace(&mut self) -> Result<()> {
|
||||
fn listscripts(&mut self) -> Result<()>;
|
||||
fn setactive(&mut self) -> Result<()>;
|
||||
|
||||
fn getscript(&mut self) -> Result<()>;
|
||||
|
||||
fn deletescript(&mut self) -> Result<()>;
|
||||
fn renamescript(&mut self) -> Result<()>;
|
||||
}
|
||||
|
||||
pub fn new_managesieve_connection(
|
||||
account_hash: crate::backends::AccountHash,
|
||||
account_name: String,
|
||||
s: &AccountSettings,
|
||||
event_consumer: crate::backends::BackendEventConsumer,
|
||||
) -> Result<ImapConnection> {
|
||||
let server_hostname = get_conf_val!(s["server_hostname"])?;
|
||||
let server_username = get_conf_val!(s["server_username"])?;
|
||||
let server_password = get_conf_val!(s["server_password"])?;
|
||||
let server_port = get_conf_val!(s["server_port"], 4190)?;
|
||||
let danger_accept_invalid_certs: bool = get_conf_val!(s["danger_accept_invalid_certs"], false)?;
|
||||
let timeout = get_conf_val!(s["timeout"], 16_u64)?;
|
||||
let timeout = if timeout == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(std::time::Duration::from_secs(timeout))
|
||||
};
|
||||
let server_conf = ImapServerConf {
|
||||
server_hostname: server_hostname.to_string(),
|
||||
server_username: server_username.to_string(),
|
||||
server_password: server_password.to_string(),
|
||||
server_port,
|
||||
use_starttls: true,
|
||||
use_tls: true,
|
||||
danger_accept_invalid_certs,
|
||||
protocol: ImapProtocol::ManageSieve,
|
||||
timeout,
|
||||
};
|
||||
let uid_store = Arc::new(UIDStore {
|
||||
is_online: Arc::new(Mutex::new((
|
||||
SystemTime::now(),
|
||||
Err(MeliError::new("Account is uninitialised.")),
|
||||
))),
|
||||
..UIDStore::new(
|
||||
account_hash,
|
||||
Arc::new(account_name),
|
||||
event_consumer,
|
||||
server_conf.timeout,
|
||||
)
|
||||
});
|
||||
Ok(ImapConnection::new_connection(&server_conf, uid_store))
|
||||
}
|
||||
|
||||
impl ManageSieve for ImapConnection {
|
||||
fn havespace(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn putscript(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn putscript(&mut self, script_name: &[u8], script: &[u8]) -> Result<()> {
|
||||
let mut ret = Vec::new();
|
||||
self.inner
|
||||
.send_literal(format!("Putscript {{{len}+}}\r\n", len = script_name.len()).as_bytes())
|
||||
.await?;
|
||||
self.inner.send_literal(script_name).await?;
|
||||
self.inner
|
||||
.send_literal(format!(" {{{len}+}}\r\n", len = script.len()).as_bytes())
|
||||
.await?;
|
||||
self.inner.send_literal(script).await?;
|
||||
self.inner
|
||||
.read_response(&mut ret, RequiredResponses::empty())
|
||||
.await?;
|
||||
let (_rest, response) = parser::response_oknobye(&ret)?;
|
||||
match response {
|
||||
ManageSieveResponse::Ok { .. } => Ok(()),
|
||||
ManageSieveResponse::NoBye { code, message } => Err(format!(
|
||||
"Could not upload script: {} {}",
|
||||
code.map(|b| String::from_utf8_lossy(b)).unwrap_or_default(),
|
||||
message
|
||||
.map(|b| String::from_utf8_lossy(b))
|
||||
.unwrap_or_default()
|
||||
)
|
||||
.into()),
|
||||
}
|
||||
fn listscripts(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn setactive(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn listscripts(&mut self) -> Result<Vec<(Vec<u8>, bool)>> {
|
||||
let mut ret = Vec::new();
|
||||
self.inner.send_command(b"Listscripts").await?;
|
||||
self.inner
|
||||
.read_response(&mut ret, RequiredResponses::empty())
|
||||
.await?;
|
||||
let (_rest, scripts) =
|
||||
parser::terminated(parser::listscripts, parser::tag_no_case(b"OK"))(&ret)?;
|
||||
Ok(scripts
|
||||
.into_iter()
|
||||
.map(|(n, a)| (n.to_vec(), a))
|
||||
.collect::<Vec<(Vec<u8>, bool)>>())
|
||||
fn getscript(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn checkscript(&mut self, script: &[u8]) -> Result<()> {
|
||||
let mut ret = Vec::new();
|
||||
self.inner
|
||||
.send_literal(format!("Checkscript {{{len}+}}\r\n", len = script.len()).as_bytes())
|
||||
.await?;
|
||||
self.inner.send_literal(script).await?;
|
||||
self.inner
|
||||
.read_response(&mut ret, RequiredResponses::empty())
|
||||
.await?;
|
||||
let (_rest, response) = parser::response_oknobye(&ret)?;
|
||||
match response {
|
||||
ManageSieveResponse::Ok { .. } => Ok(()),
|
||||
ManageSieveResponse::NoBye { code, message } => Err(format!(
|
||||
"Checkscript reply: {} {}",
|
||||
code.map(|b| String::from_utf8_lossy(b)).unwrap_or_default(),
|
||||
message
|
||||
.map(|b| String::from_utf8_lossy(b))
|
||||
.unwrap_or_default()
|
||||
)
|
||||
.into()),
|
||||
}
|
||||
fn deletescript(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn setactive(&mut self, script_name: &[u8]) -> Result<()> {
|
||||
let mut ret = Vec::new();
|
||||
self.inner
|
||||
.send_literal(format!("Setactive {{{len}+}}\r\n", len = script_name.len()).as_bytes())
|
||||
.await?;
|
||||
self.inner.send_literal(script_name).await?;
|
||||
self.inner
|
||||
.read_response(&mut ret, RequiredResponses::empty())
|
||||
.await?;
|
||||
let (_rest, response) = parser::response_oknobye(&ret)?;
|
||||
match response {
|
||||
ManageSieveResponse::Ok { .. } => Ok(()),
|
||||
ManageSieveResponse::NoBye { code, message } => Err(format!(
|
||||
"Could not set active script: {} {}",
|
||||
code.map(|b| String::from_utf8_lossy(b)).unwrap_or_default(),
|
||||
message
|
||||
.map(|b| String::from_utf8_lossy(b))
|
||||
.unwrap_or_default()
|
||||
)
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn getscript(&mut self, script_name: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut ret = Vec::new();
|
||||
self.inner
|
||||
.send_literal(format!("Getscript {{{len}+}}\r\n", len = script_name.len()).as_bytes())
|
||||
.await?;
|
||||
self.inner.send_literal(script_name).await?;
|
||||
self.inner
|
||||
.read_response(&mut ret, RequiredResponses::empty())
|
||||
.await?;
|
||||
if let Ok((_, ManageSieveResponse::NoBye { code, message })) =
|
||||
parser::response_oknobye(&ret)
|
||||
{
|
||||
return Err(format!(
|
||||
"Could not set active script: {} {}",
|
||||
code.map(|b| String::from_utf8_lossy(b)).unwrap_or_default(),
|
||||
message
|
||||
.map(|b| String::from_utf8_lossy(b))
|
||||
.unwrap_or_default()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
let (_rest, script) =
|
||||
parser::terminated(parser::getscript, parser::tag_no_case(b"OK"))(&ret)?;
|
||||
Ok(script.to_vec())
|
||||
}
|
||||
|
||||
pub async fn deletescript(&mut self, script_name: &[u8]) -> Result<()> {
|
||||
let mut ret = Vec::new();
|
||||
self.inner
|
||||
.send_literal(
|
||||
format!("Deletescript {{{len}+}}\r\n", len = script_name.len()).as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
self.inner.send_literal(script_name).await?;
|
||||
self.inner
|
||||
.read_response(&mut ret, RequiredResponses::empty())
|
||||
.await?;
|
||||
let (_rest, response) = parser::response_oknobye(&ret)?;
|
||||
match response {
|
||||
ManageSieveResponse::Ok { .. } => Ok(()),
|
||||
ManageSieveResponse::NoBye { code, message } => Err(format!(
|
||||
"Could not delete script: {} {}",
|
||||
code.map(|b| String::from_utf8_lossy(b)).unwrap_or_default(),
|
||||
message
|
||||
.map(|b| String::from_utf8_lossy(b))
|
||||
.unwrap_or_default()
|
||||
)
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn renamescript(&mut self) -> Result<()> {
|
||||
fn renamescript(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,11 +103,12 @@ impl BackendOp for ImapOp {
|
|||
//flags.lock().await.set(Some(_flags));
|
||||
cache.flags = Some(_flags);
|
||||
}
|
||||
cache.bytes = Some(body.unwrap().to_vec());
|
||||
cache.bytes =
|
||||
Some(unsafe { std::str::from_utf8_unchecked(body.unwrap()).to_string() });
|
||||
}
|
||||
let mut bytes_cache = uid_store.byte_cache.lock()?;
|
||||
let cache = bytes_cache.entry(uid).or_default();
|
||||
let ret = cache.bytes.clone().unwrap();
|
||||
let ret = cache.bytes.clone().unwrap().into_bytes();
|
||||
Ok(ret)
|
||||
}))
|
||||
}
|
||||
|
@ -144,9 +145,9 @@ impl BackendOp for ImapOp {
|
|||
.map_err(MeliError::from)?;
|
||||
if v.len() != 1 {
|
||||
debug!("responses len is {}", v.len());
|
||||
debug!(String::from_utf8_lossy(&response));
|
||||
debug!(&response);
|
||||
/* TODO: Trigger cache invalidation here. */
|
||||
debug!("message with UID {} was not found", uid);
|
||||
debug!(format!("message with UID {} was not found", uid));
|
||||
return Err(MeliError::new(format!(
|
||||
"Invalid/unexpected response: {:?}",
|
||||
response
|
||||
|
@ -154,7 +155,7 @@ impl BackendOp for ImapOp {
|
|||
.set_summary(format!("message with UID {} was not found?", uid)));
|
||||
}
|
||||
let (_uid, (_flags, _)) = v[0];
|
||||
assert_eq!(_uid, uid);
|
||||
assert_eq!(uid, uid);
|
||||
let mut bytes_cache = uid_store.byte_cache.lock()?;
|
||||
let cache = bytes_cache.entry(uid).or_default();
|
||||
cache.flags = Some(_flags);
|
||||
|
|
|
@ -33,7 +33,7 @@ use nom::{
|
|||
character::complete::digit1,
|
||||
character::is_digit,
|
||||
combinator::{map, map_res, opt},
|
||||
multi::{fold_many1, length_data, many0, many1, separated_list1},
|
||||
multi::{fold_many1, length_data, many0, many1, separated_nonempty_list},
|
||||
sequence::{delimited, preceded},
|
||||
};
|
||||
use std::convert::TryFrom;
|
||||
|
@ -119,8 +119,8 @@ impl RequiredResponses {
|
|||
}
|
||||
if self.intersects(RequiredResponses::FETCH) {
|
||||
let mut ptr = 0;
|
||||
for (i, l) in line.iter().enumerate() {
|
||||
if !l.is_ascii_digit() {
|
||||
for i in 0..line.len() {
|
||||
if !line[i].is_ascii_digit() {
|
||||
ptr = i;
|
||||
break;
|
||||
}
|
||||
|
@ -150,9 +150,8 @@ fn test_imap_required_responses() {
|
|||
assert_eq!(v.len(), 1);
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub struct Alert(String);
|
||||
|
||||
pub type ImapParseResult<'a, T> = Result<(&'a [u8], T, Option<Alert>)>;
|
||||
pub struct ImapLineIterator<'a> {
|
||||
slice: &'a [u8],
|
||||
|
@ -258,7 +257,7 @@ pub enum ImapResponse {
|
|||
impl TryFrom<&'_ [u8]> for ImapResponse {
|
||||
type Error = MeliError;
|
||||
fn try_from(val: &'_ [u8]) -> Result<ImapResponse> {
|
||||
let val: &[u8] = val.split_rn().last().unwrap_or(val);
|
||||
let val: &[u8] = val.split_rn().last().unwrap_or(val.as_ref());
|
||||
let mut val = val[val.find(b" ").ok_or_else(|| {
|
||||
MeliError::new(format!(
|
||||
"Expected tagged IMAP response (OK,NO,BAD, etc) but found {:?}",
|
||||
|
@ -315,30 +314,46 @@ fn test_imap_response() {
|
|||
assert_eq!(ImapResponse::try_from(&b"M12 NO [CANNOT] Invalid mailbox name: Name must not have \'/\' characters (0.000 + 0.098 + 0.097 secs).\r\n"[..]).unwrap(), ImapResponse::No(ResponseCode::Alert("Invalid mailbox name: Name must not have '/' characters".to_string())));
|
||||
}
|
||||
|
||||
impl<'a> std::iter::DoubleEndedIterator for ImapLineIterator<'a> {
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
if self.slice.is_empty() {
|
||||
None
|
||||
} else if let Some(pos) = self.slice.rfind(b"\r\n") {
|
||||
if self.slice.get(..pos).unwrap_or_default().is_empty() {
|
||||
self.slice = self.slice.get(..pos).unwrap_or_default();
|
||||
None
|
||||
} else if let Some(prev_pos) = self.slice.get(..pos).unwrap_or_default().rfind(b"\r\n")
|
||||
{
|
||||
let ret = self.slice.get(prev_pos + 2..pos + 2).unwrap_or_default();
|
||||
self.slice = self.slice.get(..prev_pos + 2).unwrap_or_default();
|
||||
Some(ret)
|
||||
} else {
|
||||
let ret = self.slice;
|
||||
self.slice = self.slice.get(ret.len()..).unwrap_or_default();
|
||||
Some(ret)
|
||||
}
|
||||
} else {
|
||||
let ret = self.slice;
|
||||
self.slice = self.slice.get(ret.len()..).unwrap_or_default();
|
||||
Some(ret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for ImapLineIterator<'a> {
|
||||
type Item = &'a [u8];
|
||||
|
||||
fn next(&mut self) -> Option<&'a [u8]> {
|
||||
if self.slice.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut i = 0;
|
||||
loop {
|
||||
let cur_slice = &self.slice[i..];
|
||||
if let Some(pos) = cur_slice.find(b"\r\n") {
|
||||
/* Skip literal continuation line */
|
||||
if cur_slice.get(pos.saturating_sub(1)) == Some(&b'}') {
|
||||
i += pos + 2;
|
||||
continue;
|
||||
}
|
||||
let ret = self.slice.get(..i + pos + 2).unwrap_or_default();
|
||||
self.slice = self.slice.get(i + pos + 2..).unwrap_or_default();
|
||||
return Some(ret);
|
||||
} else {
|
||||
let ret = self.slice;
|
||||
self.slice = self.slice.get(ret.len()..).unwrap_or_default();
|
||||
return Some(ret);
|
||||
}
|
||||
None
|
||||
} else if let Some(pos) = self.slice.find(b"\r\n") {
|
||||
let ret = self.slice.get(..pos + 2).unwrap_or_default();
|
||||
self.slice = self.slice.get(pos + 2..).unwrap_or_default();
|
||||
Some(ret)
|
||||
} else {
|
||||
let ret = self.slice;
|
||||
self.slice = self.slice.get(ret.len()..).unwrap_or_default();
|
||||
Some(ret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -357,57 +372,6 @@ macro_rules! to_str (
|
|||
($v:expr) => (unsafe{ std::str::from_utf8_unchecked($v) })
|
||||
);
|
||||
|
||||
#[test]
|
||||
fn test_imap_line_iterator() {
|
||||
{
|
||||
let s = b"* 1429 FETCH (UID 1505 FLAGS (\\Seen) RFC822 {26}\r\nReturn-Path: <blah blah...\r\n* 1430 FETCH (UID 1506 FLAGS (\\Seen)\r\n* 1431 FETCH (UID 1507 FLAGS (\\Seen)\r\n* 1432 FETCH (UID 1500 FLAGS (\\Seen) RFC822 {4}\r\nnull\r\n";
|
||||
let line_a =
|
||||
b"* 1429 FETCH (UID 1505 FLAGS (\\Seen) RFC822 {26}\r\nReturn-Path: <blah blah...\r\n";
|
||||
let line_b = b"* 1430 FETCH (UID 1506 FLAGS (\\Seen)\r\n";
|
||||
let line_c = b"* 1431 FETCH (UID 1507 FLAGS (\\Seen)\r\n";
|
||||
let line_d = b"* 1432 FETCH (UID 1500 FLAGS (\\Seen) RFC822 {4}\r\nnull\r\n";
|
||||
|
||||
let mut iter = s.split_rn();
|
||||
|
||||
assert_eq!(to_str!(iter.next().unwrap()), to_str!(line_a));
|
||||
assert_eq!(to_str!(iter.next().unwrap()), to_str!(line_b));
|
||||
assert_eq!(to_str!(iter.next().unwrap()), to_str!(line_c));
|
||||
assert_eq!(to_str!(iter.next().unwrap()), to_str!(line_d));
|
||||
assert!(iter.next().is_none());
|
||||
}
|
||||
|
||||
{
|
||||
let s = b"* 23 FETCH (FLAGS (\\Seen) RFC822.SIZE 44827)\r\n";
|
||||
let mut iter = s.split_rn();
|
||||
assert_eq!(to_str!(iter.next().unwrap()), to_str!(s));
|
||||
assert!(iter.next().is_none());
|
||||
}
|
||||
|
||||
{
|
||||
let s = b"";
|
||||
let mut iter = s.split_rn();
|
||||
assert!(iter.next().is_none());
|
||||
}
|
||||
{
|
||||
let s = b"* 172 EXISTS\r\n* 1 RECENT\r\n* OK [UNSEEN 12] Message 12 is first unseen\r\n* OK [UIDVALIDITY 3857529045] UIDs valid\r\n* OK [UIDNEXT 4392] Predicted next UID\r\n* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n* OK [PERMANENTFLAGS (\\Deleted \\Seen \\*)] Limited\r\n* OK [NOMODSEQ] Sorry, this mailbox format doesn't support modsequences\r\n* A142 OK [READ-WRITE] SELECT completed\r\n";
|
||||
let mut iter = s.split_rn();
|
||||
for l in &[
|
||||
&b"* 172 EXISTS\r\n"[..],
|
||||
&b"* 1 RECENT\r\n"[..],
|
||||
&b"* OK [UNSEEN 12] Message 12 is first unseen\r\n"[..],
|
||||
&b"* OK [UIDVALIDITY 3857529045] UIDs valid\r\n"[..],
|
||||
&b"* OK [UIDNEXT 4392] Predicted next UID\r\n"[..],
|
||||
&b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n"[..],
|
||||
&b"* OK [PERMANENTFLAGS (\\Deleted \\Seen \\*)] Limited\r\n"[..],
|
||||
&b"* OK [NOMODSEQ] Sorry, this mailbox format doesn't support modsequences\r\n"[..],
|
||||
&b"* A142 OK [READ-WRITE] SELECT completed\r\n"[..],
|
||||
] {
|
||||
assert_eq!(to_str!(iter.next().unwrap()), to_str!(l));
|
||||
}
|
||||
assert!(iter.next().is_none());
|
||||
}
|
||||
}
|
||||
|
||||
/*macro_rules! dbg_dmp (
|
||||
($i: expr, $submac:ident!( $($args:tt)* )) => (
|
||||
{
|
||||
|
@ -451,13 +415,7 @@ pub fn list_mailbox_result(input: &[u8]) -> IResult<&[u8], ImapMailbox> {
|
|||
let separator: u8 = separator[0];
|
||||
let mut f = ImapMailbox::default();
|
||||
f.no_select = false;
|
||||
f.is_subscribed = false;
|
||||
|
||||
if path.eq_ignore_ascii_case("INBOX") {
|
||||
f.is_subscribed = true;
|
||||
let _ = f.set_special_usage(SpecialUsageMailbox::Inbox);
|
||||
}
|
||||
|
||||
f.is_subscribed = path.eq_ignore_ascii_case("INBOX");
|
||||
for p in properties.split(|&b| b == b' ') {
|
||||
if p.eq_ignore_ascii_case(b"\\NoSelect") || p.eq_ignore_ascii_case(b"\\NonExistent")
|
||||
{
|
||||
|
@ -467,15 +425,9 @@ pub fn list_mailbox_result(input: &[u8]) -> IResult<&[u8], ImapMailbox> {
|
|||
} else if p.eq_ignore_ascii_case(b"\\Sent") {
|
||||
let _ = f.set_special_usage(SpecialUsageMailbox::Sent);
|
||||
} else if p.eq_ignore_ascii_case(b"\\Junk") {
|
||||
let _ = f.set_special_usage(SpecialUsageMailbox::Junk);
|
||||
} else if p.eq_ignore_ascii_case(b"\\Trash") {
|
||||
let _ = f.set_special_usage(SpecialUsageMailbox::Trash);
|
||||
} else if p.eq_ignore_ascii_case(b"\\Drafts") {
|
||||
let _ = f.set_special_usage(SpecialUsageMailbox::Drafts);
|
||||
} else if p.eq_ignore_ascii_case(b"\\Flagged") {
|
||||
let _ = f.set_special_usage(SpecialUsageMailbox::Flagged);
|
||||
} else if p.eq_ignore_ascii_case(b"\\Archive") {
|
||||
let _ = f.set_special_usage(SpecialUsageMailbox::Archive);
|
||||
}
|
||||
}
|
||||
f.imap_path = path.to_string();
|
||||
|
@ -506,9 +458,7 @@ pub struct FetchResponse<'a> {
|
|||
pub modseq: Option<ModSequence>,
|
||||
pub flags: Option<(Flag, Vec<String>)>,
|
||||
pub body: Option<&'a [u8]>,
|
||||
pub references: Option<&'a [u8]>,
|
||||
pub envelope: Option<Envelope>,
|
||||
pub raw_fetch_value: &'a [u8],
|
||||
}
|
||||
|
||||
pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
||||
|
@ -563,9 +513,7 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
modseq: None,
|
||||
flags: None,
|
||||
body: None,
|
||||
references: None,
|
||||
envelope: None,
|
||||
raw_fetch_value: &[],
|
||||
};
|
||||
|
||||
while input[i].is_ascii_digit() {
|
||||
|
@ -595,7 +543,7 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
} else {
|
||||
return debug!(Err(MeliError::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
|
||||
String::from_utf8_lossy(input)
|
||||
String::from_utf8_lossy(&input)
|
||||
))));
|
||||
}
|
||||
} else if input[i..].starts_with(b"FLAGS (") {
|
||||
|
@ -605,8 +553,8 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
i += (input.len() - i - rest.len()) + 1;
|
||||
} else {
|
||||
return debug!(Err(MeliError::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse FLAGS: {:.40}.",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
|
||||
String::from_utf8_lossy(&input)
|
||||
))));
|
||||
}
|
||||
} else if input[i..].starts_with(b"MODSEQ (") {
|
||||
|
@ -622,7 +570,7 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
} else {
|
||||
return debug!(Err(MeliError::new(format!(
|
||||
"Unexpected input while parsing MODSEQ in UID FETCH response. Got: `{:.40}`",
|
||||
String::from_utf8_lossy(input)
|
||||
String::from_utf8_lossy(&input)
|
||||
))));
|
||||
}
|
||||
} else if input[i..].starts_with(b"RFC822 {") {
|
||||
|
@ -640,8 +588,8 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
i += input.len() - i - rest.len();
|
||||
} else {
|
||||
return debug!(Err(MeliError::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse RFC822: {:.40}",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
|
||||
String::from_utf8_lossy(&input)
|
||||
))));
|
||||
}
|
||||
} else if input[i..].starts_with(b"ENVELOPE (") {
|
||||
|
@ -651,7 +599,7 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
i += input.len() - i - rest.len();
|
||||
} else {
|
||||
return debug!(Err(MeliError::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse ENVELOPE: {:.40}",
|
||||
"Unexpected input while parsing UID FETCH response. Got: `{:.40}`",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
))));
|
||||
}
|
||||
|
@ -661,45 +609,13 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
let (rest, _has_attachments) = bodystructure_has_attachments(&input[i..])?;
|
||||
has_attachments = _has_attachments;
|
||||
i += input[i..].len() - rest.len();
|
||||
} else if input[i..].starts_with(b"BODY[HEADER.FIELDS (REFERENCES)] ") {
|
||||
i += b"BODY[HEADER.FIELDS (REFERENCES)] ".len();
|
||||
if let Ok((rest, mut references)) = astring_token(&input[i..]) {
|
||||
if !references.trim().is_empty() {
|
||||
if let Ok((_, (_, v))) = crate::email::parser::headers::header(references) {
|
||||
references = v;
|
||||
}
|
||||
ret.references = Some(references);
|
||||
}
|
||||
i += input.len() - i - rest.len();
|
||||
} else {
|
||||
return debug!(Err(MeliError::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse BODY[HEADER.FIELDS (REFERENCES)]: {:.40}",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
))));
|
||||
}
|
||||
} else if input[i..].starts_with(b"BODY[HEADER.FIELDS (\"REFERENCES\")] ") {
|
||||
i += b"BODY[HEADER.FIELDS (\"REFERENCES\")] ".len();
|
||||
if let Ok((rest, mut references)) = astring_token(&input[i..]) {
|
||||
if !references.trim().is_empty() {
|
||||
if let Ok((_, (_, v))) = crate::email::parser::headers::header(references) {
|
||||
references = v;
|
||||
}
|
||||
ret.references = Some(references);
|
||||
}
|
||||
i += input.len() - i - rest.len();
|
||||
} else {
|
||||
return debug!(Err(MeliError::new(format!(
|
||||
"Unexpected input while parsing UID FETCH response. Could not parse BODY[HEADER.FIELDS (\"REFERENCES\"): {:.40}",
|
||||
String::from_utf8_lossy(&input[i..])
|
||||
))));
|
||||
}
|
||||
} else if input[i..].starts_with(b")\r\n") {
|
||||
i += b")\r\n".len();
|
||||
break;
|
||||
} else {
|
||||
debug!(
|
||||
"Got unexpected token while parsing UID FETCH response:\n`{}`\n",
|
||||
String::from_utf8_lossy(input)
|
||||
String::from_utf8_lossy(&input)
|
||||
);
|
||||
return debug!(Err(MeliError::new(format!(
|
||||
"Got unexpected token while parsing UID FETCH response: `{:.40}`",
|
||||
|
@ -707,7 +623,6 @@ pub fn fetch_response(input: &[u8]) -> ImapParseResult<FetchResponse<'_>> {
|
|||
))));
|
||||
}
|
||||
}
|
||||
ret.raw_fetch_value = &input[..i];
|
||||
|
||||
if let Some(env) = ret.envelope.as_mut() {
|
||||
env.set_has_attachments(has_attachments);
|
||||
|
@ -737,9 +652,9 @@ pub fn fetch_responses(mut input: &[u8]) -> ImapParseResult<Vec<FetchResponse<'_
|
|||
}
|
||||
Err(err) => {
|
||||
return Err(MeliError::new(format!(
|
||||
"Unexpected input while parsing UID FETCH responses: {} `{:.40}`",
|
||||
err,
|
||||
String::from_utf8_lossy(input),
|
||||
"Unexpected input while parsing UID FETCH responses: `{:.40}`, {}",
|
||||
String::from_utf8_lossy(&input),
|
||||
err
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
@ -750,7 +665,7 @@ pub fn fetch_responses(mut input: &[u8]) -> ImapParseResult<Vec<FetchResponse<'_
|
|||
} else {
|
||||
return Err(MeliError::new(format!(
|
||||
"310Unexpected input while parsing UID FETCH responses: `{:.40}`",
|
||||
String::from_utf8_lossy(input)
|
||||
String::from_utf8_lossy(&input)
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
@ -824,7 +739,7 @@ macro_rules! flags_to_imap_list {
|
|||
pub fn capabilities(input: &[u8]) -> IResult<&[u8], Vec<&[u8]>> {
|
||||
let (input, _) = take_until("CAPABILITY ")(input)?;
|
||||
let (input, _) = tag("CAPABILITY ")(input)?;
|
||||
let (input, ret) = separated_list1(tag(" "), is_not(" ]\r\n"))(input)?;
|
||||
let (input, ret) = separated_nonempty_list(tag(" "), is_not(" ]\r\n"))(input)?;
|
||||
let (input, _) = take_until("\r\n")(input)?;
|
||||
let (input, _) = tag("\r\n")(input)?;
|
||||
Ok((input, ret))
|
||||
|
@ -908,10 +823,7 @@ pub fn untagged_responses(input: &[u8]) -> ImapParseResult<Option<UntaggedRespon
|
|||
let (input, _tag) =
|
||||
take_until::<_, &[u8], (&[u8], nom::error::ErrorKind)>(&b"\r\n"[..])(input)?;
|
||||
let (input, _) = tag::<_, &[u8], (&[u8], nom::error::ErrorKind)>(b"\r\n")(input)?;
|
||||
debug!(
|
||||
"Parse untagged response from {:?}",
|
||||
String::from_utf8_lossy(orig_input)
|
||||
);
|
||||
debug!("Parse untagged response from {:?}", orig_input);
|
||||
Ok((
|
||||
input,
|
||||
{
|
||||
|
@ -924,7 +836,7 @@ pub fn untagged_responses(input: &[u8]) -> ImapParseResult<Option<UntaggedRespon
|
|||
_ => {
|
||||
debug!(
|
||||
"unknown untagged_response: {}",
|
||||
String::from_utf8_lossy(_tag)
|
||||
String::from_utf8_lossy(&_tag)
|
||||
);
|
||||
None
|
||||
}
|
||||
|
@ -935,7 +847,8 @@ pub fn untagged_responses(input: &[u8]) -> ImapParseResult<Option<UntaggedRespon
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_imap_untagged_responses() {
|
||||
fn test_untagged_responses() {
|
||||
use std::convert::TryInto;
|
||||
use UntaggedResponse::*;
|
||||
assert_eq!(
|
||||
untagged_responses(b"* 2 EXISTS\r\n")
|
||||
|
@ -955,9 +868,7 @@ fn test_imap_untagged_responses() {
|
|||
modseq: Some(ModSequence(std::num::NonZeroU64::new(1365_u64).unwrap())),
|
||||
flags: Some((Flag::SEEN, vec![])),
|
||||
body: None,
|
||||
references: None,
|
||||
envelope: None,
|
||||
raw_fetch_value: &b"* 1079 FETCH (UID 1103 MODSEQ (1365) FLAGS (\\Seen))\r\n"[..],
|
||||
envelope: None
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
|
@ -971,49 +882,16 @@ fn test_imap_untagged_responses() {
|
|||
modseq: None,
|
||||
flags: Some((Flag::SEEN, vec![])),
|
||||
body: None,
|
||||
references: None,
|
||||
envelope: None,
|
||||
raw_fetch_value: &b"* 1 FETCH (FLAGS (\\Seen))\r\n"[..],
|
||||
envelope: None
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_imap_fetch_response() {
|
||||
let input: &[u8] = b"* 198 FETCH (UID 7608 FLAGS (\\Seen) ENVELOPE (\"Fri, 24 Jun 2011 10:09:10 +0000\" \"xxxx/xxxx\" ((\"xx@xx.com\" NIL \"xx\" \"xx.com\")) NIL NIL ((\"xx@xx\" NIL \"xx\" \"xx.com\")) ((\"'xx, xx'\" NIL \"xx.xx\" \"xx.com\") (\"xx.xx@xx.com\" NIL \"xx.xx\" \"xx.com\") (\"'xx'\" NIL \"xx.xx\" \"xx.com\") (\"'xx xx'\" NIL \"xx.xx\" \"xx.com\") (\"xx.xx@xx.com\" NIL \"xx.xx\" \"xx.com\")) NIL NIL \"<xx@xx.com>\") BODY[HEADER.FIELDS (REFERENCES)] {2}\r\n\r\nBODYSTRUCTURE ((\"text\" \"html\" (\"charset\" \"us-ascii\") \"<xx@xx>\" NIL \"7BIT\" 17236 232 NIL NIL NIL NIL)(\"image\" \"jpeg\" (\"name\" \"image001.jpg\") \"<image001.jpg@xx.xx>\" \"image001.jpg\" \"base64\" 1918 NIL (\"inline\" (\"filename\" \"image001.jpg\" \"size\" \"1650\" \"creation-date\" \"Sun, 09 Aug 2015 20:56:04 GMT\" \"modification-date\" \"Sun, 14 Aug 2022 22:11:45 GMT\")) NIL NIL) \"related\" (\"boundary\" \"xx--xx\" \"type\" \"text/html\") NIL \"en-US\"))\r\n";
|
||||
|
||||
let mut address = SmallVec::new();
|
||||
address.push(Address::new(None, "xx@xx.com".to_string()));
|
||||
let mut env = Envelope::new(0);
|
||||
env.set_subject("xxxx/xxxx".as_bytes().to_vec());
|
||||
env.set_date("Fri, 24 Jun 2011 10:09:10 +0000".as_bytes());
|
||||
env.set_from(address.clone());
|
||||
env.set_to(address);
|
||||
env.set_message_id("<xx@xx.com>".as_bytes());
|
||||
assert_eq!(
|
||||
fetch_response(input).unwrap(),
|
||||
(
|
||||
&b""[..],
|
||||
FetchResponse {
|
||||
uid: Some(7608),
|
||||
message_sequence_number: 198,
|
||||
flags: Some((Flag::SEEN, vec![])),
|
||||
modseq: None,
|
||||
body: None,
|
||||
references: None,
|
||||
envelope: Some(env),
|
||||
raw_fetch_value: input,
|
||||
},
|
||||
None
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
pub fn search_results<'a>(input: &'a [u8]) -> IResult<&'a [u8], Vec<ImapNum>> {
|
||||
alt((
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], Vec<ImapNum>> {
|
||||
let (input, _) = tag("* SEARCH ")(input)?;
|
||||
let (input, list) = separated_list1(
|
||||
let (input, list) = separated_nonempty_list(
|
||||
tag(b" "),
|
||||
map_res(is_not(" \r\n"), |s: &[u8]| {
|
||||
ImapNum::from_str(unsafe { std::str::from_utf8_unchecked(s) })
|
||||
|
@ -1066,7 +944,7 @@ pub struct SelectResponse {
|
|||
pub exists: ImapNum,
|
||||
pub recent: ImapNum,
|
||||
pub flags: (Flag, Vec<String>),
|
||||
pub first_unseen: MessageSequenceNumber,
|
||||
pub unseen: MessageSequenceNumber,
|
||||
pub uidvalidity: UIDVALIDITY,
|
||||
pub uidnext: UID,
|
||||
pub permanentflags: (Flag, Vec<String>),
|
||||
|
@ -1113,7 +991,7 @@ pub fn select_response(input: &[u8]) -> Result<SelectResponse> {
|
|||
} else if l.starts_with(b"* FLAGS (") {
|
||||
ret.flags = flags(&l[b"* FLAGS (".len()..l.len() - b")".len()]).map(|(_, v)| v)?;
|
||||
} else if l.starts_with(b"* OK [UNSEEN ") {
|
||||
ret.first_unseen = MessageSequenceNumber::from_str(&String::from_utf8_lossy(
|
||||
ret.unseen = MessageSequenceNumber::from_str(&String::from_utf8_lossy(
|
||||
&l[b"* OK [UNSEEN ".len()..l.find(b"]").unwrap()],
|
||||
))?;
|
||||
} else if l.starts_with(b"* OK [UIDVALIDITY ") {
|
||||
|
@ -1139,7 +1017,7 @@ pub fn select_response(input: &[u8]) -> Result<SelectResponse> {
|
|||
let (_, highestmodseq) = res?;
|
||||
ret.highestmodseq = Some(
|
||||
std::num::NonZeroU64::new(u64::from_str(&String::from_utf8_lossy(
|
||||
highestmodseq,
|
||||
&highestmodseq,
|
||||
))?)
|
||||
.map(|u| Ok(ModSequence(u)))
|
||||
.unwrap_or(Err(())),
|
||||
|
@ -1147,19 +1025,20 @@ pub fn select_response(input: &[u8]) -> Result<SelectResponse> {
|
|||
} else if l.starts_with(b"* OK [NOMODSEQ") {
|
||||
ret.highestmodseq = Some(Err(()));
|
||||
} else if !l.is_empty() {
|
||||
debug!("select response: {}", String::from_utf8_lossy(l));
|
||||
debug!("select response: {}", String::from_utf8_lossy(&l));
|
||||
}
|
||||
}
|
||||
Ok(ret)
|
||||
} else {
|
||||
let ret = String::from_utf8_lossy(input).to_string();
|
||||
let ret = String::from_utf8_lossy(&input).to_string();
|
||||
debug!("BAD/NO response in select: {}", &ret);
|
||||
Err(MeliError::new(ret))
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_imap_select_response() {
|
||||
fn test_select_response() {
|
||||
use std::convert::TryInto;
|
||||
let r = b"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\*)] Flags permitted.\r\n* 45 EXISTS\r\n* 0 RECENT\r\n* OK [UNSEEN 16] First unseen.\r\n* OK [UIDVALIDITY 1554422056] UIDs valid\r\n* OK [UIDNEXT 50] Predicted next UID\r\n";
|
||||
|
||||
assert_eq!(
|
||||
|
@ -1171,7 +1050,7 @@ fn test_imap_select_response() {
|
|||
Flag::REPLIED | Flag::SEEN | Flag::TRASHED | Flag::DRAFT | Flag::FLAGGED,
|
||||
Vec::new()
|
||||
),
|
||||
first_unseen: 16,
|
||||
unseen: 16,
|
||||
uidvalidity: 1554422056,
|
||||
uidnext: 50,
|
||||
permanentflags: (
|
||||
|
@ -1194,7 +1073,7 @@ fn test_imap_select_response() {
|
|||
Flag::REPLIED | Flag::SEEN | Flag::TRASHED | Flag::DRAFT | Flag::FLAGGED,
|
||||
Vec::new()
|
||||
),
|
||||
first_unseen: 12,
|
||||
unseen: 12,
|
||||
uidvalidity: 3857529045,
|
||||
uidnext: 4392,
|
||||
permanentflags: (Flag::SEEN | Flag::TRASHED, vec!["*".into()]),
|
||||
|
@ -1216,7 +1095,7 @@ fn test_imap_select_response() {
|
|||
Flag::REPLIED | Flag::SEEN | Flag::TRASHED | Flag::DRAFT | Flag::FLAGGED,
|
||||
Vec::new()
|
||||
),
|
||||
first_unseen: 12,
|
||||
unseen: 12,
|
||||
uidvalidity: 3857529045,
|
||||
uidnext: 4392,
|
||||
permanentflags: (Flag::SEEN | Flag::TRASHED, vec!["*".into()]),
|
||||
|
@ -1233,8 +1112,7 @@ pub fn flags(input: &[u8]) -> IResult<&[u8], (Flag, Vec<String>)> {
|
|||
|
||||
let mut input = input;
|
||||
while !input.starts_with(b")") && !input.is_empty() {
|
||||
let is_system_flag = input.starts_with(b"\\");
|
||||
if is_system_flag {
|
||||
if input.starts_with(b"\\") {
|
||||
input = &input[1..];
|
||||
}
|
||||
let mut match_end = 0;
|
||||
|
@ -1245,25 +1123,24 @@ pub fn flags(input: &[u8]) -> IResult<&[u8], (Flag, Vec<String>)> {
|
|||
match_end += 1;
|
||||
}
|
||||
|
||||
match (is_system_flag, &input[..match_end]) {
|
||||
(true, t) if t.eq_ignore_ascii_case(b"Answered") => {
|
||||
match &input[..match_end] {
|
||||
b"Answered" => {
|
||||
ret.set(Flag::REPLIED, true);
|
||||
}
|
||||
(true, t) if t.eq_ignore_ascii_case(b"Flagged") => {
|
||||
b"Flagged" => {
|
||||
ret.set(Flag::FLAGGED, true);
|
||||
}
|
||||
(true, t) if t.eq_ignore_ascii_case(b"Deleted") => {
|
||||
b"Deleted" => {
|
||||
ret.set(Flag::TRASHED, true);
|
||||
}
|
||||
(true, t) if t.eq_ignore_ascii_case(b"Seen") => {
|
||||
b"Seen" => {
|
||||
ret.set(Flag::SEEN, true);
|
||||
}
|
||||
(true, t) if t.eq_ignore_ascii_case(b"Draft") => {
|
||||
b"Draft" => {
|
||||
ret.set(Flag::DRAFT, true);
|
||||
}
|
||||
(true, t) if t.eq_ignore_ascii_case(b"Recent") => { /* ignore */ }
|
||||
(_, f) => {
|
||||
keywords.push(String::from_utf8_lossy(f).into());
|
||||
f => {
|
||||
keywords.push(String::from_utf8_lossy(&f).into());
|
||||
}
|
||||
}
|
||||
input = &input[match_end..];
|
||||
|
@ -1366,9 +1243,7 @@ pub fn envelope(input: &[u8]) -> IResult<&[u8], Envelope> {
|
|||
}
|
||||
if let Some(in_reply_to) = in_reply_to {
|
||||
env.set_in_reply_to(&in_reply_to);
|
||||
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(message_id) = message_id {
|
||||
|
@ -1379,12 +1254,6 @@ pub fn envelope(input: &[u8]) -> IResult<&[u8], Envelope> {
|
|||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_imap_envelope() {
|
||||
let input: &[u8] = b"(\"Fri, 24 Jun 2011 10:09:10 +0000\" \"xxxx/xxxx\" ((\"xx@xx.com\" NIL \"xx\" \"xx.com\")) NIL NIL ((\"xx@xx\" NIL \"xx\" \"xx.com\")) ((\"'xx, xx'\" NIL \"xx.xx\" \"xx.com\") (\"xx.xx@xx.com\" NIL \"xx.xx\" \"xx.com\") (\"'xx'\" NIL \"xx.xx\" \"xx.com\") (\"'xx xx'\" NIL \"xx.xx\" \"xx.com\") (\"xx.xx@xx.com\" NIL \"xx.xx\" \"xx.com\")) NIL NIL \"<xx@xx.com>\")";
|
||||
_ = envelope(input).unwrap();
|
||||
}
|
||||
|
||||
/* Helper to build StrBuilder for Address structs */
|
||||
macro_rules! str_builder {
|
||||
($offset:expr, $length:expr) => {
|
||||
|
@ -1404,8 +1273,8 @@ pub fn envelope_addresses<'a>(
|
|||
|input: &'a [u8]| -> IResult<&'a [u8], Option<SmallVec<[Address; 1]>>> {
|
||||
let (input, _) = tag("(")(input)?;
|
||||
let (input, envelopes) = fold_many1(
|
||||
delimited(tag("("), envelope_address, alt((tag(") "), tag(")")))),
|
||||
SmallVec::new,
|
||||
delimited(tag("("), envelope_address, tag(")")),
|
||||
SmallVec::new(),
|
||||
|mut acc, item| {
|
||||
acc.push(item);
|
||||
acc
|
||||
|
@ -1439,7 +1308,7 @@ pub fn envelope_address(input: &[u8]) -> IResult<&[u8], Address> {
|
|||
to_str!(&name),
|
||||
if name.is_empty() { "" } else { " " },
|
||||
to_str!(&mailbox_name),
|
||||
to_str!(host_name)
|
||||
to_str!(&host_name)
|
||||
)
|
||||
.into_bytes()
|
||||
} else {
|
||||
|
@ -1507,14 +1376,14 @@ pub fn quoted(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
|
|||
}
|
||||
|
||||
pub fn quoted_or_nil(input: &[u8]) -> IResult<&[u8], Option<Vec<u8>>> {
|
||||
alt((map(tag("NIL"), |_| None), map(quoted, Some)))(input.ltrim())
|
||||
alt((map(tag("NIL"), |_| None), map(quoted, |v| Some(v))))(input.ltrim())
|
||||
}
|
||||
|
||||
pub fn uid_fetch_envelopes_response<'a>(
|
||||
input: &'a [u8],
|
||||
) -> IResult<&'a [u8], Vec<(UID, Option<(Flag, Vec<String>)>, Envelope)>> {
|
||||
pub fn uid_fetch_envelopes_response(
|
||||
input: &[u8],
|
||||
) -> IResult<&[u8], Vec<(UID, Option<(Flag, Vec<String>)>, Envelope)>> {
|
||||
many0(
|
||||
|input: &'a [u8]| -> IResult<&'a [u8], (UID, Option<(Flag, Vec<String>)>, Envelope)> {
|
||||
|input: &[u8]| -> IResult<&[u8], (UID, Option<(Flag, Vec<String>)>, Envelope)> {
|
||||
let (input, _) = tag("* ")(input)?;
|
||||
let (input, _) = take_while(is_digit)(input)?;
|
||||
let (input, _) = tag(" FETCH (")(input)?;
|
||||
|
@ -1551,7 +1420,7 @@ pub fn bodystructure_has_attachments(input: &[u8]) -> IResult<&[u8], bool> {
|
|||
let mut has_attachments = false;
|
||||
let mut first_in_line = true;
|
||||
while !input.is_empty() && !input.starts_with(b")") {
|
||||
if input.starts_with(b"\"") || input[0].is_ascii_alphanumeric() || input[0] == b'{' {
|
||||
if input.starts_with(b"\"") || input[0].is_ascii_alphanumeric() {
|
||||
let (_input, token) = astring_token(input)?;
|
||||
input = _input;
|
||||
if first_in_line {
|
||||
|
@ -1580,7 +1449,7 @@ fn eat_whitespace(mut input: &[u8]) -> IResult<&[u8], ()> {
|
|||
break;
|
||||
}
|
||||
}
|
||||
Ok((input, ()))
|
||||
return Ok((input, ()));
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
|
@ -1655,7 +1524,7 @@ pub fn status_response(input: &[u8]) -> IResult<&[u8], StatusResponse> {
|
|||
// ; is considered to be INBOX and not an astring.
|
||||
// ; Refer to section 5.1 for further
|
||||
// ; semantic details of mailbox names.
|
||||
pub fn mailbox_token(input: &'_ [u8]) -> IResult<&'_ [u8], std::borrow::Cow<'_, str>> {
|
||||
pub fn mailbox_token<'i>(input: &'i [u8]) -> IResult<&'i [u8], std::borrow::Cow<'i, str>> {
|
||||
let (input, astring) = astring_token(input)?;
|
||||
if astring.eq_ignore_ascii_case(b"INBOX") {
|
||||
return Ok((input, "INBOX".into()));
|
||||
|
@ -1664,12 +1533,12 @@ pub fn mailbox_token(input: &'_ [u8]) -> IResult<&'_ [u8], std::borrow::Cow<'_,
|
|||
}
|
||||
|
||||
// astring = 1*ASTRING-CHAR / string
|
||||
pub fn astring_token(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
fn astring_token(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
alt((string_token, astring_char))(input)
|
||||
}
|
||||
|
||||
// string = quoted / literal
|
||||
pub fn string_token(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
fn string_token(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
if let Ok((r, o)) = literal(input) {
|
||||
return Ok((r, o));
|
||||
}
|
||||
|
|
|
@ -28,13 +28,14 @@ use crate::backends::{
|
|||
RefreshEvent,
|
||||
RefreshEventKind::{self, *},
|
||||
};
|
||||
use crate::email::Envelope;
|
||||
use crate::error::*;
|
||||
use std::convert::TryInto;
|
||||
|
||||
impl ImapConnection {
|
||||
pub async fn process_untagged(&mut self, line: &[u8]) -> Result<bool> {
|
||||
macro_rules! try_fail {
|
||||
($mailbox_hash: expr, $($result:expr $(,)*)+) => {
|
||||
($mailbox_hash: expr, $($result:expr)+) => {
|
||||
$(if let Err(err) = $result {
|
||||
self.uid_store.is_online.lock().unwrap().1 = Err(err.clone());
|
||||
debug!("failure: {}", err.to_string());
|
||||
|
@ -77,7 +78,7 @@ impl ImapConnection {
|
|||
.lock()
|
||||
.unwrap()
|
||||
.get(&mailbox_hash)
|
||||
.map(|i| i.len() < TryInto::<usize>::try_into(n).unwrap())
|
||||
.map(|i| i.len() < n.try_into().unwrap())
|
||||
.unwrap_or(true)
|
||||
{
|
||||
debug!(
|
||||
|
@ -85,64 +86,6 @@ impl ImapConnection {
|
|||
n,
|
||||
self.uid_store.msn_index.lock().unwrap().get(&mailbox_hash)
|
||||
);
|
||||
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)?
|
||||
.1
|
||||
.into_iter()
|
||||
.collect::<std::collections::BTreeSet<UID>>();
|
||||
{
|
||||
let mut lck = self.uid_store.msn_index.lock().unwrap();
|
||||
let msn_index = lck.entry(mailbox_hash).or_default();
|
||||
msn_index.clear();
|
||||
msn_index.extend(
|
||||
super::protocol_parser::search_results(&response)?
|
||||
.1
|
||||
.into_iter(),
|
||||
);
|
||||
}
|
||||
let mut events = vec![];
|
||||
for (deleted_uid, deleted_hash) in self
|
||||
.uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|((mailbox_hash_, u), _)| {
|
||||
*mailbox_hash_ == mailbox_hash && !results.contains(u)
|
||||
})
|
||||
.map(|((_, uid), hash)| (*uid, *hash))
|
||||
.collect::<Vec<(UID, crate::email::EnvelopeHash)>>()
|
||||
{
|
||||
mailbox.exists.lock().unwrap().remove(deleted_hash);
|
||||
mailbox.unseen.lock().unwrap().remove(deleted_hash);
|
||||
self.uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&(mailbox_hash, deleted_uid));
|
||||
self.uid_store
|
||||
.hash_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&deleted_hash);
|
||||
events.push((
|
||||
deleted_uid,
|
||||
RefreshEvent {
|
||||
account_hash: self.uid_store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: Remove(deleted_hash),
|
||||
},
|
||||
));
|
||||
}
|
||||
if self.uid_store.keep_offline_cache {
|
||||
cache_handle.update(mailbox_hash, &events)?;
|
||||
}
|
||||
for (_, event) in events {
|
||||
self.add_refresh_event(event);
|
||||
}
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
let deleted_uid = self
|
||||
|
@ -152,7 +95,7 @@ impl ImapConnection {
|
|||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.or_default()
|
||||
.remove(TryInto::<usize>::try_into(n).unwrap().saturating_sub(1));
|
||||
.remove(n.try_into().unwrap());
|
||||
debug!("expunge {}, UID = {}", n, deleted_uid);
|
||||
let deleted_hash: crate::email::EnvelopeHash = match self
|
||||
.uid_store
|
||||
|
@ -164,8 +107,6 @@ impl ImapConnection {
|
|||
Some(v) => v,
|
||||
None => return Ok(true),
|
||||
};
|
||||
mailbox.exists.lock().unwrap().remove(deleted_hash);
|
||||
mailbox.unseen.lock().unwrap().remove(deleted_hash);
|
||||
self.uid_store
|
||||
.hash_index
|
||||
.lock()
|
||||
|
@ -195,108 +136,94 @@ impl ImapConnection {
|
|||
debug!("exists {}", n);
|
||||
try_fail!(
|
||||
mailbox_hash,
|
||||
self.send_command(format!("FETCH {} (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)", n).as_bytes()).await
|
||||
self.send_command(format!("FETCH {} (UID FLAGS RFC822)", n).as_bytes()).await
|
||||
self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED).await
|
||||
);
|
||||
let mut v = match super::protocol_parser::fetch_responses(&response) {
|
||||
Ok((_, v, _)) => v,
|
||||
Err(err) => {
|
||||
debug!(
|
||||
"Error when parsing FETCH response after untagged exists {:?}",
|
||||
err
|
||||
);
|
||||
return Ok(true);
|
||||
}
|
||||
};
|
||||
debug!("responses len is {}", v.len());
|
||||
for FetchResponse {
|
||||
ref uid,
|
||||
ref mut envelope,
|
||||
ref mut flags,
|
||||
ref references,
|
||||
..
|
||||
} in &mut v
|
||||
{
|
||||
if uid.is_none() || flags.is_none() || envelope.is_none() {
|
||||
continue;
|
||||
}
|
||||
let uid = uid.unwrap();
|
||||
let env = envelope.as_mut().unwrap();
|
||||
env.set_hash(generate_envelope_hash(mailbox.imap_path(), &uid));
|
||||
if let Some(value) = references {
|
||||
env.set_references(value);
|
||||
}
|
||||
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
|
||||
if let Some((flags, keywords)) = flags {
|
||||
env.set_flags(*flags);
|
||||
if !env.is_seen() {
|
||||
mailbox.unseen.lock().unwrap().insert_new(env.hash());
|
||||
}
|
||||
for f in keywords {
|
||||
let hash = tag_hash!(f);
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f.to_string());
|
||||
match super::protocol_parser::fetch_responses(&response) {
|
||||
Ok((_, v, _)) => {
|
||||
'fetch_responses: for FetchResponse {
|
||||
uid, flags, body, ..
|
||||
} in v
|
||||
{
|
||||
if uid.is_none() || flags.is_none() || body.is_none() {
|
||||
continue;
|
||||
}
|
||||
let uid = uid.unwrap();
|
||||
if self
|
||||
.uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains_key(&(mailbox_hash, uid))
|
||||
{
|
||||
continue 'fetch_responses;
|
||||
}
|
||||
let env_hash = generate_envelope_hash(&mailbox.imap_path(), &uid);
|
||||
self.uid_store
|
||||
.msn_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.or_default()
|
||||
.push(uid);
|
||||
if let Ok(mut env) =
|
||||
Envelope::from_bytes(body.unwrap(), flags.as_ref().map(|&(f, _)| f))
|
||||
{
|
||||
env.set_hash(env_hash);
|
||||
self.uid_store
|
||||
.hash_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(env_hash, (uid, mailbox_hash));
|
||||
self.uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert((mailbox_hash, uid), env_hash);
|
||||
if let Some((_, keywords)) = flags {
|
||||
let mut tag_lck = self.uid_store.tag_index.write().unwrap();
|
||||
for f in keywords {
|
||||
let hash = tag_hash!(f);
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f);
|
||||
}
|
||||
env.labels_mut().push(hash);
|
||||
}
|
||||
}
|
||||
debug!(
|
||||
"Create event {} {} {}",
|
||||
env.hash(),
|
||||
env.subject(),
|
||||
mailbox.path(),
|
||||
);
|
||||
if !env.is_seen() {
|
||||
mailbox.unseen.lock().unwrap().insert_new(env.hash());
|
||||
}
|
||||
mailbox.exists.lock().unwrap().insert_new(env.hash());
|
||||
let mut event: [(UID, RefreshEvent); 1] = [(
|
||||
uid,
|
||||
RefreshEvent {
|
||||
account_hash: self.uid_store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: Create(Box::new(env)),
|
||||
},
|
||||
)];
|
||||
if self.uid_store.keep_offline_cache {
|
||||
cache_handle.update(mailbox_hash, &event)?;
|
||||
}
|
||||
self.add_refresh_event(std::mem::replace(
|
||||
&mut event[0].1,
|
||||
RefreshEvent {
|
||||
account_hash: self.uid_store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: Rescan,
|
||||
},
|
||||
));
|
||||
}
|
||||
env.labels_mut().push(hash);
|
||||
}
|
||||
}
|
||||
mailbox.exists.lock().unwrap().insert_new(env.hash());
|
||||
if !self
|
||||
.uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains_key(&(mailbox_hash, uid))
|
||||
{
|
||||
self.uid_store
|
||||
.msn_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.or_default()
|
||||
.push(uid);
|
||||
}
|
||||
self.uid_store
|
||||
.hash_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(env.hash(), (uid, mailbox_hash));
|
||||
self.uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert((mailbox_hash, uid), env.hash());
|
||||
debug!(
|
||||
"Create event {} {} {}",
|
||||
env.hash(),
|
||||
env.subject(),
|
||||
mailbox.path(),
|
||||
);
|
||||
}
|
||||
if self.uid_store.keep_offline_cache {
|
||||
if let Err(err) = cache_handle
|
||||
.insert_envelopes(mailbox_hash, &v)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not save envelopes in cache for mailbox {}",
|
||||
&mailbox.imap_path()
|
||||
)
|
||||
})
|
||||
{
|
||||
crate::log(err.to_string(), crate::INFO);
|
||||
}
|
||||
}
|
||||
for response in v {
|
||||
if let FetchResponse {
|
||||
envelope: Some(envelope),
|
||||
..
|
||||
} = response
|
||||
{
|
||||
self.add_refresh_event(RefreshEvent {
|
||||
account_hash: self.uid_store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: Create(Box::new(envelope)),
|
||||
});
|
||||
Err(e) => {
|
||||
debug!(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -314,121 +241,97 @@ impl ImapConnection {
|
|||
debug!("UID SEARCH RECENT returned no results");
|
||||
}
|
||||
Ok(v) => {
|
||||
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();
|
||||
for ms in iter {
|
||||
accum.push(',');
|
||||
accum.push_str(to_str!(ms).trim());
|
||||
}
|
||||
format!("UID FETCH {} (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)", accum)
|
||||
};
|
||||
try_fail!(
|
||||
mailbox_hash,
|
||||
self.send_command(command.as_bytes()).await
|
||||
self.send_command(
|
||||
&[b"UID FETCH", v, b"(FLAGS RFC822)"]
|
||||
.join(&b' '),
|
||||
).await
|
||||
self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED).await
|
||||
);
|
||||
let mut v = match super::protocol_parser::fetch_responses(&response) {
|
||||
Ok((_, v, _)) => v,
|
||||
Err(err) => {
|
||||
debug!(
|
||||
"Error when parsing FETCH response after untagged recent {:?}",
|
||||
err
|
||||
);
|
||||
return Ok(true);
|
||||
}
|
||||
};
|
||||
debug!("responses len is {}", v.len());
|
||||
for FetchResponse {
|
||||
ref uid,
|
||||
ref mut envelope,
|
||||
ref mut flags,
|
||||
ref references,
|
||||
..
|
||||
} in &mut v
|
||||
{
|
||||
if uid.is_none() || flags.is_none() || envelope.is_none() {
|
||||
continue;
|
||||
}
|
||||
let uid = uid.unwrap();
|
||||
let env = envelope.as_mut().unwrap();
|
||||
env.set_hash(generate_envelope_hash(mailbox.imap_path(), &uid));
|
||||
if let Some(value) = references {
|
||||
env.set_references(value);
|
||||
}
|
||||
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
|
||||
if let Some((flags, keywords)) = flags {
|
||||
env.set_flags(*flags);
|
||||
if !env.is_seen() {
|
||||
mailbox.unseen.lock().unwrap().insert_new(env.hash());
|
||||
}
|
||||
for f in keywords {
|
||||
let hash = tag_hash!(f);
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f.to_string());
|
||||
}
|
||||
env.labels_mut().push(hash);
|
||||
}
|
||||
}
|
||||
mailbox.exists.lock().unwrap().insert_new(env.hash());
|
||||
}
|
||||
if self.uid_store.keep_offline_cache {
|
||||
if let Err(err) = cache_handle
|
||||
.insert_envelopes(mailbox_hash, &v)
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Could not save envelopes in cache for mailbox {}",
|
||||
&mailbox.imap_path()
|
||||
)
|
||||
})
|
||||
{
|
||||
crate::log(err.to_string(), crate::INFO);
|
||||
}
|
||||
}
|
||||
for response in v {
|
||||
if let FetchResponse {
|
||||
envelope: Some(envelope),
|
||||
uid: Some(uid),
|
||||
..
|
||||
} = response
|
||||
{
|
||||
if !self
|
||||
.uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains_key(&(mailbox_hash, uid))
|
||||
debug!(&response);
|
||||
match super::protocol_parser::fetch_responses(&response) {
|
||||
Ok((_, v, _)) => {
|
||||
for FetchResponse {
|
||||
uid, flags, body, ..
|
||||
} in v
|
||||
{
|
||||
self.uid_store
|
||||
.msn_index
|
||||
if uid.is_none() || flags.is_none() || body.is_none() {
|
||||
continue;
|
||||
}
|
||||
let uid = uid.unwrap();
|
||||
if !self
|
||||
.uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.or_default()
|
||||
.push(uid);
|
||||
.contains_key(&(mailbox_hash, uid))
|
||||
{
|
||||
if let Ok(mut env) = Envelope::from_bytes(
|
||||
body.unwrap(),
|
||||
flags.as_ref().map(|&(f, _)| f),
|
||||
) {
|
||||
self.uid_store
|
||||
.hash_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(env.hash(), (uid, mailbox_hash));
|
||||
self.uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert((mailbox_hash, uid), env.hash());
|
||||
debug!(
|
||||
"Create event {} {} {}",
|
||||
env.hash(),
|
||||
env.subject(),
|
||||
mailbox.path(),
|
||||
);
|
||||
if let Some((_, keywords)) = flags {
|
||||
let mut tag_lck =
|
||||
self.uid_store.tag_index.write().unwrap();
|
||||
for f in keywords {
|
||||
let hash = tag_hash!(f);
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f);
|
||||
}
|
||||
env.labels_mut().push(hash);
|
||||
}
|
||||
}
|
||||
if !env.is_seen() {
|
||||
mailbox
|
||||
.unseen
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert_new(env.hash());
|
||||
}
|
||||
|
||||
mailbox.exists.lock().unwrap().insert_new(env.hash());
|
||||
let mut event: [(UID, RefreshEvent); 1] = [(
|
||||
uid,
|
||||
RefreshEvent {
|
||||
account_hash: self.uid_store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: Create(Box::new(env)),
|
||||
},
|
||||
)];
|
||||
if self.uid_store.keep_offline_cache {
|
||||
cache_handle.update(mailbox_hash, &event)?;
|
||||
}
|
||||
self.add_refresh_event(std::mem::replace(
|
||||
&mut event[0].1,
|
||||
RefreshEvent {
|
||||
account_hash: self.uid_store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: Rescan,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.uid_store
|
||||
.hash_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(envelope.hash(), (uid, mailbox_hash));
|
||||
self.uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert((mailbox_hash, uid), envelope.hash());
|
||||
debug!(
|
||||
"Create event {} {} {}",
|
||||
envelope.hash(),
|
||||
envelope.subject(),
|
||||
mailbox.path(),
|
||||
);
|
||||
self.add_refresh_event(RefreshEvent {
|
||||
account_hash: self.uid_store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: Create(Box::new(envelope)),
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
debug!(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -447,21 +350,38 @@ impl ImapConnection {
|
|||
modseq,
|
||||
flags,
|
||||
body: _,
|
||||
references: _,
|
||||
envelope: _,
|
||||
raw_fetch_value: _,
|
||||
}) => {
|
||||
if let Some(modseq) = modseq {
|
||||
if self
|
||||
.uid_store
|
||||
.reverse_modseq
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.or_default()
|
||||
.contains_key(&modseq)
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(flags) = flags {
|
||||
let uid = if let Some(uid) = uid {
|
||||
uid
|
||||
} else {
|
||||
try_fail!(
|
||||
mailbox_hash,
|
||||
self.send_command(format!("UID SEARCH {}", msg_seq).as_bytes())
|
||||
.await,
|
||||
self.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await,
|
||||
);
|
||||
mailbox_hash,
|
||||
self.send_command(
|
||||
&[
|
||||
b"UID SEARCH",
|
||||
format!("{}", msg_seq).as_bytes(),
|
||||
]
|
||||
.join(&b' '),
|
||||
).await
|
||||
self.read_response(&mut response, RequiredResponses::SEARCH).await
|
||||
);
|
||||
debug!(to_str!(&response));
|
||||
match super::protocol_parser::search_results(
|
||||
response.split_rn().next().unwrap_or(b""),
|
||||
)
|
||||
|
@ -472,30 +392,34 @@ impl ImapConnection {
|
|||
return Ok(false);
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("SEARCH error failed: {}", e);
|
||||
debug!(to_str!(&response));
|
||||
debug!(e);
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
debug!("fetch uid {} {:?}", uid, flags);
|
||||
if let Some(env_hash) = {
|
||||
let temp = self
|
||||
.uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&(mailbox_hash, uid))
|
||||
.copied();
|
||||
temp
|
||||
} {
|
||||
let env_hash = self
|
||||
.uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&(mailbox_hash, uid))
|
||||
.copied();
|
||||
if let Some(env_hash) = env_hash {
|
||||
if !flags.0.intersects(crate::email::Flag::SEEN) {
|
||||
mailbox.unseen.lock().unwrap().insert_new(env_hash);
|
||||
} else {
|
||||
mailbox.unseen.lock().unwrap().remove(env_hash);
|
||||
}
|
||||
mailbox.exists.lock().unwrap().insert_new(env_hash);
|
||||
if let Some(modseq) = modseq {
|
||||
self.uid_store
|
||||
.reverse_modseq
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.or_default()
|
||||
.insert(modseq, env_hash);
|
||||
self.uid_store
|
||||
.modseq
|
||||
.lock()
|
||||
|
|
|
@ -76,9 +76,10 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> {
|
|||
let mailbox_hash = mailbox.hash();
|
||||
let mut response = Vec::with_capacity(8 * 1024);
|
||||
let select_response = conn
|
||||
.examine_mailbox(mailbox_hash, &mut response, true)
|
||||
.select_mailbox(mailbox_hash, &mut response, true)
|
||||
.await?
|
||||
.unwrap();
|
||||
debug!("select response {}", String::from_utf8_lossy(&response));
|
||||
{
|
||||
let mut uidvalidities = uid_store.uidvalidity.lock().unwrap();
|
||||
|
||||
|
@ -110,12 +111,6 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> {
|
|||
let mailboxes_lck = timeout(uid_store.timeout, uid_store.mailboxes.lock()).await?;
|
||||
mailboxes_lck.clone()
|
||||
};
|
||||
for (h, mailbox) in mailboxes.clone() {
|
||||
if mailbox_hash == h {
|
||||
continue;
|
||||
}
|
||||
examine_updates(mailbox, &mut conn, &uid_store).await?;
|
||||
}
|
||||
conn.send_command(b"IDLE").await?;
|
||||
let mut blockn = ImapBlockingConnection::from(conn);
|
||||
let mut watch = std::time::Instant::now();
|
||||
|
@ -150,7 +145,10 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> {
|
|||
if now.duration_since(watch) >= _5_MINS {
|
||||
/* Time to poll all inboxes */
|
||||
let mut conn = timeout(uid_store.timeout, main_conn.lock()).await?;
|
||||
for (_h, mailbox) in mailboxes.clone() {
|
||||
for (h, mailbox) in mailboxes.clone() {
|
||||
if mailbox_hash == h {
|
||||
continue;
|
||||
}
|
||||
examine_updates(mailbox, &mut conn, &uid_store).await?;
|
||||
}
|
||||
watch = now;
|
||||
|
@ -175,7 +173,7 @@ pub async fn idle(kit: ImapWatchKit) -> Result<()> {
|
|||
.conn
|
||||
.read_response(&mut response, RequiredResponses::empty())
|
||||
.await?;
|
||||
for l in line.split_rn().chain(response.split_rn()) {
|
||||
for l in line.split_rn() {
|
||||
debug!("process_untagged {:?}", &l);
|
||||
if l.starts_with(b"+ ")
|
||||
|| l.starts_with(b"* ok")
|
||||
|
@ -221,6 +219,7 @@ pub async fn examine_updates(
|
|||
.examine_mailbox(mailbox_hash, &mut response, true)
|
||||
.await?
|
||||
.unwrap();
|
||||
debug!(&select_response);
|
||||
{
|
||||
let mut uidvalidities = uid_store.uidvalidity.lock().unwrap();
|
||||
|
||||
|
@ -245,74 +244,7 @@ pub async fn examine_updates(
|
|||
uidvalidities.insert(mailbox_hash, select_response.uidvalidity);
|
||||
}
|
||||
}
|
||||
if mailbox.is_cold() {
|
||||
/* Mailbox hasn't been loaded yet */
|
||||
let has_list_status: bool = conn
|
||||
.uid_store
|
||||
.capabilities
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case(b"LIST-STATUS"));
|
||||
if has_list_status {
|
||||
conn.send_command(
|
||||
format!(
|
||||
"LIST \"{}\" \"\" RETURN (STATUS (MESSAGES UNSEEN))",
|
||||
mailbox.imap_path()
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
conn.read_response(
|
||||
&mut response,
|
||||
RequiredResponses::LIST_REQUIRED | RequiredResponses::STATUS,
|
||||
)
|
||||
.await?;
|
||||
debug!(
|
||||
"list return status out: {}",
|
||||
String::from_utf8_lossy(&response)
|
||||
);
|
||||
for l in response.split_rn() {
|
||||
if !l.starts_with(b"*") {
|
||||
continue;
|
||||
}
|
||||
if let Ok(status) = protocol_parser::status_response(l).map(|(_, v)| v) {
|
||||
if Some(mailbox_hash) == status.mailbox {
|
||||
if let Some(total) = status.messages {
|
||||
if let Ok(mut exists_lck) = mailbox.exists.lock() {
|
||||
exists_lck.clear();
|
||||
exists_lck.set_not_yet_seen(total);
|
||||
}
|
||||
}
|
||||
if let Some(total) = status.unseen {
|
||||
if let Ok(mut unseen_lck) = mailbox.unseen.lock() {
|
||||
unseen_lck.clear();
|
||||
unseen_lck.set_not_yet_seen(total);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
conn.send_command(b"SEARCH UNSEEN").await?;
|
||||
conn.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
.await?;
|
||||
let unseen_count = protocol_parser::search_results(&response)?.1.len();
|
||||
if let Ok(mut exists_lck) = mailbox.exists.lock() {
|
||||
exists_lck.clear();
|
||||
exists_lck.set_not_yet_seen(select_response.exists);
|
||||
}
|
||||
if let Ok(mut unseen_lck) = mailbox.unseen.lock() {
|
||||
unseen_lck.clear();
|
||||
unseen_lck.set_not_yet_seen(unseen_count);
|
||||
}
|
||||
}
|
||||
mailbox.set_warm(true);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if select_response.recent > 0 {
|
||||
if debug!(select_response.recent > 0) {
|
||||
/* UID SEARCH RECENT */
|
||||
conn.send_command(b"UID SEARCH RECENT").await?;
|
||||
conn.read_response(&mut response, RequiredResponses::SEARCH)
|
||||
|
@ -326,24 +258,24 @@ pub async fn examine_updates(
|
|||
return Ok(());
|
||||
}
|
||||
let mut cmd = "UID FETCH ".to_string();
|
||||
cmd.push_str(&v[0].to_string());
|
||||
if v.len() != 1 {
|
||||
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)",
|
||||
);
|
||||
cmd.push_str(" (UID FLAGS RFC822)");
|
||||
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() {
|
||||
} else if debug!(select_response.exists > mailbox.exists.lock().unwrap().len()) {
|
||||
conn.send_command(
|
||||
format!(
|
||||
"FETCH {}:* (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] BODYSTRUCTURE)",
|
||||
std::cmp::max(mailbox.exists.lock().unwrap().len(), 1)
|
||||
"FETCH {}:* (UID FLAGS RFC822)",
|
||||
mailbox.exists.lock().unwrap().len()
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
|
@ -353,58 +285,42 @@ pub async fn examine_updates(
|
|||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
debug!(
|
||||
"fetch response is {} bytes and {} lines",
|
||||
response.len(),
|
||||
String::from_utf8_lossy(&response).lines().count()
|
||||
);
|
||||
debug!(&response);
|
||||
let (_, mut v, _) = protocol_parser::fetch_responses(&response)?;
|
||||
debug!("responses len is {}", v.len());
|
||||
for FetchResponse {
|
||||
ref uid,
|
||||
ref mut envelope,
|
||||
ref mut flags,
|
||||
ref references,
|
||||
ref mut body,
|
||||
ref mut envelope,
|
||||
..
|
||||
} in v.iter_mut()
|
||||
{
|
||||
let uid = uid.unwrap();
|
||||
let env = envelope.as_mut().unwrap();
|
||||
env.set_hash(generate_envelope_hash(mailbox.imap_path(), &uid));
|
||||
if let Some(value) = references {
|
||||
env.set_references(value);
|
||||
}
|
||||
let mut tag_lck = uid_store.collection.tag_index.write().unwrap();
|
||||
if let Some((flags, keywords)) = flags {
|
||||
env.set_flags(*flags);
|
||||
if !env.is_seen() {
|
||||
mailbox.unseen.lock().unwrap().insert_new(env.hash());
|
||||
}
|
||||
mailbox.exists.lock().unwrap().insert_new(env.hash());
|
||||
for f in keywords {
|
||||
let hash = tag_hash!(f);
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f.to_string());
|
||||
*envelope = Envelope::from_bytes(body.take().unwrap(), flags.as_ref().map(|&(f, _)| f))
|
||||
.map(|mut env| {
|
||||
env.set_hash(generate_envelope_hash(&mailbox.imap_path(), &uid));
|
||||
if let Some((_, keywords)) = flags.take() {
|
||||
let mut tag_lck = uid_store.tag_index.write().unwrap();
|
||||
for f in keywords {
|
||||
let hash = tag_hash!(f);
|
||||
if !tag_lck.contains_key(&hash) {
|
||||
tag_lck.insert(hash, f);
|
||||
}
|
||||
env.labels_mut().push(hash);
|
||||
}
|
||||
}
|
||||
env.labels_mut().push(hash);
|
||||
}
|
||||
}
|
||||
env
|
||||
})
|
||||
.map_err(|err| {
|
||||
debug!("uid {} envelope parse error {}", uid, &err);
|
||||
err
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
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 {
|
||||
cache_handle.insert_envelopes(mailbox_hash, &v)?;
|
||||
}
|
||||
|
||||
for FetchResponse { uid, envelope, .. } in v {
|
||||
if uid.is_none() || envelope.is_none() {
|
||||
continue;
|
||||
}
|
||||
'fetch_responses_c: for FetchResponse { uid, envelope, .. } in v {
|
||||
let uid = uid.unwrap();
|
||||
if uid_store
|
||||
.uid_index
|
||||
|
@ -412,37 +328,35 @@ pub async fn examine_updates(
|
|||
.unwrap()
|
||||
.contains_key(&(mailbox_hash, uid))
|
||||
{
|
||||
continue;
|
||||
continue 'fetch_responses_c;
|
||||
}
|
||||
if let Some(env) = envelope {
|
||||
uid_store
|
||||
.hash_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(env.hash(), (uid, mailbox_hash));
|
||||
uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert((mailbox_hash, uid), env.hash());
|
||||
debug!(
|
||||
"Create event {} {} {}",
|
||||
env.hash(),
|
||||
env.subject(),
|
||||
mailbox.path(),
|
||||
);
|
||||
if !env.is_seen() {
|
||||
mailbox.unseen.lock().unwrap().insert_new(env.hash());
|
||||
}
|
||||
mailbox.exists.lock().unwrap().insert_new(env.hash());
|
||||
conn.add_refresh_event(RefreshEvent {
|
||||
account_hash: uid_store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: Create(Box::new(env)),
|
||||
});
|
||||
}
|
||||
let env = envelope.unwrap();
|
||||
debug!(
|
||||
"Create event {} {} {}",
|
||||
env.hash(),
|
||||
env.subject(),
|
||||
mailbox.path(),
|
||||
);
|
||||
uid_store
|
||||
.msn_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.or_default()
|
||||
.push(uid);
|
||||
uid_store
|
||||
.hash_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(env.hash(), (uid, mailbox_hash));
|
||||
uid_store
|
||||
.uid_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert((mailbox_hash, uid), env.hash());
|
||||
conn.add_refresh_event(RefreshEvent {
|
||||
account_hash: uid_store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: Create(Box::new(env)),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
@ -21,33 +21,17 @@
|
|||
|
||||
use crate::backends::*;
|
||||
use crate::conf::AccountSettings;
|
||||
use crate::connections::timeout;
|
||||
use crate::email::*;
|
||||
use crate::error::{MeliError, Result};
|
||||
use crate::Collection;
|
||||
use futures::lock::Mutex as FutureMutex;
|
||||
use isahc::config::RedirectPolicy;
|
||||
use isahc::{AsyncReadResponseExt, HttpClient};
|
||||
use isahc::prelude::HttpClient;
|
||||
use isahc::ResponseExt;
|
||||
use serde_json::Value;
|
||||
use std::collections::{hash_map::DefaultHasher, HashMap, HashSet};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::convert::TryFrom;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
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()
|
||||
}};
|
||||
}
|
||||
use std::time::Instant;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! _impl {
|
||||
|
@ -100,11 +84,11 @@ pub struct EnvelopeCache {
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JmapServerConf {
|
||||
pub server_url: String,
|
||||
pub server_hostname: String,
|
||||
pub server_username: String,
|
||||
pub server_password: String,
|
||||
pub server_port: u16,
|
||||
pub danger_accept_invalid_certs: bool,
|
||||
pub timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
macro_rules! get_conf_val {
|
||||
|
@ -138,21 +122,29 @@ macro_rules! get_conf_val {
|
|||
impl JmapServerConf {
|
||||
pub fn new(s: &AccountSettings) -> Result<Self> {
|
||||
Ok(JmapServerConf {
|
||||
server_url: get_conf_val!(s["server_url"])?.to_string(),
|
||||
server_hostname: get_conf_val!(s["server_hostname"])?.to_string(),
|
||||
server_username: get_conf_val!(s["server_username"])?.to_string(),
|
||||
server_password: get_conf_val!(s["server_password"])?.to_string(),
|
||||
server_port: get_conf_val!(s["server_port"], 443)?,
|
||||
danger_accept_invalid_certs: get_conf_val!(s["danger_accept_invalid_certs"], false)?,
|
||||
timeout: get_conf_val!(s["timeout"], 16_u64).map(|t| {
|
||||
if t == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(Duration::from_secs(t))
|
||||
}
|
||||
})?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct IsSubscribedFn(Box<dyn Fn(&str) -> bool + Send + Sync>);
|
||||
|
||||
impl std::fmt::Debug for IsSubscribedFn {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "IsSubscribedFn Box")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for IsSubscribedFn {
|
||||
type Target = Box<dyn Fn(&str) -> bool + Send + Sync>;
|
||||
fn deref(&self) -> &Box<dyn Fn(&str) -> bool + Send + Sync> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
macro_rules! get_conf_val {
|
||||
($s:ident[$var:literal]) => {
|
||||
$s.extra.get($var).ok_or_else(|| {
|
||||
|
@ -181,115 +173,24 @@ macro_rules! get_conf_val {
|
|||
};
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Store {
|
||||
pub account_name: Arc<String>,
|
||||
pub account_hash: AccountHash,
|
||||
pub account_id: Arc<Mutex<Id<Account>>>,
|
||||
pub byte_cache: Arc<Mutex<HashMap<EnvelopeHash, EnvelopeCache>>>,
|
||||
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 mailboxes: Arc<RwLock<HashMap<MailboxHash, JmapMailbox>>>,
|
||||
pub mailboxes_index: Arc<RwLock<HashMap<MailboxHash, HashSet<EnvelopeHash>>>>,
|
||||
pub mailbox_state: Arc<Mutex<State<MailboxObject>>>,
|
||||
pub online_status: Arc<FutureMutex<(Instant, Result<()>)>>,
|
||||
pub is_subscribed: Arc<IsSubscribedFn>,
|
||||
pub event_consumer: BackendEventConsumer,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
pub fn add_envelope(&self, obj: EmailObject) -> Envelope {
|
||||
let mut tag_lck = self.collection.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());
|
||||
}
|
||||
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();
|
||||
|
||||
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();
|
||||
let mailboxes_lck = self.mailboxes.read().unwrap();
|
||||
let mut mailboxes_index_lck = self.mailboxes_index.write().unwrap();
|
||||
for (mailbox_id, _) in mailbox_ids {
|
||||
if let Some((mailbox_hash, _)) = mailboxes_lck.iter().find(|(_, m)| m.id == mailbox_id)
|
||||
{
|
||||
mailboxes_index_lck
|
||||
.entry(*mailbox_hash)
|
||||
.or_default()
|
||||
.insert(ret.hash());
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
pub fn remove_envelope(
|
||||
&self,
|
||||
obj_id: Id<EmailObject>,
|
||||
) -> Option<(EnvelopeHash, SmallVec<[MailboxHash; 8]>)> {
|
||||
let env_hash = self.reverse_id_store.lock().unwrap().remove(&obj_id)?;
|
||||
self.id_store.lock().unwrap().remove(&env_hash);
|
||||
self.blob_id_store.lock().unwrap().remove(&env_hash);
|
||||
self.byte_cache.lock().unwrap().remove(&env_hash);
|
||||
let mut mailbox_hashes = SmallVec::new();
|
||||
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))
|
||||
}
|
||||
byte_cache: HashMap<EnvelopeHash, EnvelopeCache>,
|
||||
id_store: HashMap<EnvelopeHash, Id>,
|
||||
blob_id_store: HashMap<EnvelopeHash, Id>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JmapType {
|
||||
account_name: String,
|
||||
account_hash: AccountHash,
|
||||
online: Arc<FutureMutex<(Instant, Result<()>)>>,
|
||||
is_subscribed: Arc<IsSubscribedFn>,
|
||||
server_conf: JmapServerConf,
|
||||
connection: Arc<FutureMutex<JmapConnection>>,
|
||||
store: Arc<Store>,
|
||||
store: Arc<RwLock<Store>>,
|
||||
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
|
||||
mailboxes: Arc<RwLock<HashMap<MailboxHash, JmapMailbox>>>,
|
||||
}
|
||||
|
||||
impl MailBackend for JmapType {
|
||||
|
@ -306,18 +207,16 @@ 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;
|
||||
let online = self.online.clone();
|
||||
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()
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -325,7 +224,9 @@ impl MailBackend for JmapType {
|
|||
&mut self,
|
||||
mailbox_hash: MailboxHash,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Result<Vec<Envelope>>> + Send + 'static>>> {
|
||||
let mailboxes = self.mailboxes.clone();
|
||||
let store = self.store.clone();
|
||||
let tag_index = self.tag_index.clone();
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async_stream::try_stream! {
|
||||
let mut conn = connection.lock().await;
|
||||
|
@ -333,67 +234,34 @@ impl MailBackend for JmapType {
|
|||
let res = protocol::fetch(
|
||||
&conn,
|
||||
&store,
|
||||
&tag_index,
|
||||
&mailboxes,
|
||||
mailbox_hash,
|
||||
).await?;
|
||||
if res.is_empty() {
|
||||
return;
|
||||
}
|
||||
yield res;
|
||||
}))
|
||||
}
|
||||
|
||||
fn refresh(&mut self, mailbox_hash: MailboxHash) -> ResultFuture<()> {
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
let mut conn = connection.lock().await;
|
||||
conn.connect().await?;
|
||||
conn.email_changes(mailbox_hash).await?;
|
||||
Ok(())
|
||||
}))
|
||||
fn refresh(&mut self, _mailbox_hash: MailboxHash) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
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?;
|
||||
}
|
||||
}
|
||||
crate::connections::sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
}))
|
||||
Err(MeliError::from("JMAP watch for updates is unimplemented"))
|
||||
}
|
||||
|
||||
fn mailboxes(&self) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
|
||||
let store = self.store.clone();
|
||||
let mailboxes = self.mailboxes.clone();
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
let mut conn = connection.lock().await;
|
||||
conn.connect().await?;
|
||||
if store.mailboxes.read().unwrap().is_empty() {
|
||||
if mailboxes.read().unwrap().is_empty() {
|
||||
let new_mailboxes = debug!(protocol::get_mailboxes(&conn).await)?;
|
||||
*store.mailboxes.write().unwrap() = new_mailboxes;
|
||||
*mailboxes.write().unwrap() = new_mailboxes;
|
||||
}
|
||||
|
||||
let ret = store
|
||||
.mailboxes
|
||||
let ret = mailboxes
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
|
@ -415,98 +283,15 @@ impl MailBackend for JmapType {
|
|||
|
||||
fn save(
|
||||
&self,
|
||||
bytes: Vec<u8>,
|
||||
mailbox_hash: MailboxHash,
|
||||
_bytes: Vec<u8>,
|
||||
_mailbox_hash: MailboxHash,
|
||||
_flags: Option<Flag>,
|
||||
) -> ResultFuture<()> {
|
||||
let store = self.store.clone();
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
let mut conn = connection.lock().await;
|
||||
conn.connect().await?;
|
||||
/*
|
||||
* 1. upload binary blob, get blobId
|
||||
* 2. Email/import
|
||||
*/
|
||||
let (api_url, upload_url) = {
|
||||
let lck = conn.session.lock().unwrap();
|
||||
(lck.api_url.clone(), lck.upload_url.clone())
|
||||
};
|
||||
let mut res = conn
|
||||
.client
|
||||
.post_async(
|
||||
&upload_request_format(upload_url.as_str(), &conn.mail_account_id()),
|
||||
bytes,
|
||||
)
|
||||
.await?;
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
let mailbox_id: Id<MailboxObject> = {
|
||||
let mailboxes_lck = store.mailboxes.read().unwrap();
|
||||
if let Some(mailbox) = mailboxes_lck.get(&mailbox_hash) {
|
||||
mailbox.id.clone()
|
||||
} else {
|
||||
return Err(MeliError::new(format!(
|
||||
"Mailbox with hash {} not found",
|
||||
mailbox_hash
|
||||
)));
|
||||
}
|
||||
};
|
||||
let res_text = res.text().await?;
|
||||
|
||||
let upload_response: UploadResponse = match serde_json::from_str(&res_text) {
|
||||
Err(err) => {
|
||||
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
|
||||
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
Ok(s) => s,
|
||||
};
|
||||
let mut req = Request::new(conn.request_no.clone());
|
||||
let creation_id: Id<EmailObject> = "1".to_string().into();
|
||||
let mut email_imports = HashMap::default();
|
||||
let mut mailbox_ids = HashMap::default();
|
||||
mailbox_ids.insert(mailbox_id, true);
|
||||
email_imports.insert(
|
||||
creation_id.clone(),
|
||||
EmailImport::new()
|
||||
.blob_id(upload_response.blob_id)
|
||||
.mailbox_ids(mailbox_ids),
|
||||
);
|
||||
|
||||
let import_call: ImportCall = ImportCall::new()
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.emails(email_imports);
|
||||
|
||||
req.add_call(&import_call);
|
||||
let mut res = conn
|
||||
.client
|
||||
.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 = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
|
||||
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
Ok(s) => s,
|
||||
};
|
||||
let m = ImportResponse::try_from(v.method_responses.remove(0)).or_else(|err| {
|
||||
let ierr: Result<ImportError> =
|
||||
serde_json::from_str(&res_text).map_err(|err| err.into());
|
||||
if let Ok(err) = ierr {
|
||||
Err(MeliError::new(format!("Could not save message: {:?}", err)))
|
||||
} else {
|
||||
Err(err.into())
|
||||
}
|
||||
})?;
|
||||
|
||||
if let Some(err) = m.not_created.get(&creation_id) {
|
||||
return Err(MeliError::new(format!("Could not save message: {:?}", err)));
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
|
||||
Some(self.tag_index.clone())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
|
@ -517,21 +302,14 @@ impl MailBackend for JmapType {
|
|||
self
|
||||
}
|
||||
|
||||
fn collection(&self) -> Collection {
|
||||
self.store.collection.clone()
|
||||
}
|
||||
|
||||
fn search(
|
||||
&self,
|
||||
q: crate::search::Query,
|
||||
mailbox_hash: Option<MailboxHash>,
|
||||
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>> {
|
||||
let store = self.store.clone();
|
||||
let connection = self.connection.clone();
|
||||
let filter = if let Some(mailbox_hash) = mailbox_hash {
|
||||
let mailbox_id = self.store.mailboxes.read().unwrap()[&mailbox_hash]
|
||||
.id
|
||||
.clone();
|
||||
let mailbox_id = self.mailboxes.read().unwrap()[&mailbox_hash].id.clone();
|
||||
|
||||
let mut f = Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
|
@ -549,7 +327,7 @@ impl MailBackend for JmapType {
|
|||
conn.connect().await?;
|
||||
let email_call: EmailQuery = EmailQuery::new(
|
||||
Query::new()
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.account_id(conn.mail_account_id().to_string())
|
||||
.filter(Some(filter))
|
||||
.position(0),
|
||||
)
|
||||
|
@ -557,26 +335,26 @@ impl MailBackend for JmapType {
|
|||
|
||||
let mut req = Request::new(conn.request_no.clone());
|
||||
req.add_call(&email_call);
|
||||
let api_url = conn.session.lock().unwrap().api_url.clone();
|
||||
|
||||
let mut res = conn
|
||||
.client
|
||||
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
|
||||
.post_async(&conn.session.api_url, 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 = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
|
||||
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
Ok(s) => s,
|
||||
};
|
||||
*store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
|
||||
let res_text = res.text_async().await?;
|
||||
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
|
||||
*conn.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;
|
||||
let ret = ids.into_iter().map(|id| id.into_hash()).collect();
|
||||
let ret = ids
|
||||
.into_iter()
|
||||
.map(|id| {
|
||||
use std::hash::Hasher;
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
h.write(id.as_bytes());
|
||||
h.finish()
|
||||
})
|
||||
.collect();
|
||||
Ok(ret)
|
||||
}))
|
||||
}
|
||||
|
@ -598,99 +376,12 @@ impl MailBackend for JmapType {
|
|||
|
||||
fn copy_messages(
|
||||
&mut self,
|
||||
env_hashes: EnvelopeHashBatch,
|
||||
source_mailbox_hash: MailboxHash,
|
||||
destination_mailbox_hash: MailboxHash,
|
||||
move_: bool,
|
||||
_env_hashes: EnvelopeHashBatch,
|
||||
_source_mailbox_hash: MailboxHash,
|
||||
_destination_mailbox_hash: MailboxHash,
|
||||
_move_: bool,
|
||||
) -> ResultFuture<()> {
|
||||
let store = self.store.clone();
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
let (source_mailbox_id, destination_mailbox_id) = {
|
||||
let mailboxes_lck = store.mailboxes.read().unwrap();
|
||||
if !mailboxes_lck.contains_key(&source_mailbox_hash) {
|
||||
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(MeliError::new(format!(
|
||||
"Could not find destination mailbox with hash {}",
|
||||
destination_mailbox_hash
|
||||
)));
|
||||
}
|
||||
|
||||
(
|
||||
mailboxes_lck[&source_mailbox_hash].id.clone(),
|
||||
mailboxes_lck[&destination_mailbox_hash].id.clone(),
|
||||
)
|
||||
};
|
||||
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),
|
||||
serde_json::json!(true),
|
||||
);
|
||||
if move_ {
|
||||
update_keywords.insert(
|
||||
format!("mailboxIds/{}", &source_mailbox_id),
|
||||
serde_json::json!(null),
|
||||
);
|
||||
}
|
||||
{
|
||||
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);
|
||||
update_map.insert(id.clone(), serde_json::json!(update_keywords.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
let conn = connection.lock().await;
|
||||
let api_url = conn.session.lock().unwrap().api_url.clone();
|
||||
|
||||
let email_set_call: EmailSet = EmailSet::new(
|
||||
Set::<EmailObject>::new()
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.update(Some(update_map)),
|
||||
);
|
||||
|
||||
let mut req = Request::new(conn.request_no.clone());
|
||||
let _prev_seq = req.add_call(&email_set_call);
|
||||
|
||||
let mut res = conn
|
||||
.client
|
||||
.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 = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
|
||||
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
Ok(s) => s,
|
||||
};
|
||||
*store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
|
||||
let m = SetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
|
||||
if let Some(ids) = m.not_updated {
|
||||
if !ids.is_empty() {
|
||||
return Err(MeliError::new(format!(
|
||||
"Could not update ids: {}",
|
||||
ids.into_iter()
|
||||
.map(|err| err.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn set_flags(
|
||||
|
@ -699,12 +390,16 @@ impl MailBackend for JmapType {
|
|||
mailbox_hash: MailboxHash,
|
||||
flags: SmallVec<[(std::result::Result<Flag, String>, bool); 8]>,
|
||||
) -> ResultFuture<()> {
|
||||
let mailboxes = self.mailboxes.clone();
|
||||
let store = self.store.clone();
|
||||
let account_hash = self.account_hash;
|
||||
let tag_index = self.tag_index.clone();
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
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 mailbox_id = mailboxes.read().unwrap()[&mailbox_hash].id.clone();
|
||||
let mut update_map: HashMap<String, Value> = HashMap::default();
|
||||
let mut ids: Vec<Id> = Vec::with_capacity(env_hashes.rest.len() + 1);
|
||||
let mut id_map: HashMap<Id, EnvelopeHash> = HashMap::default();
|
||||
let mut update_keywords: HashMap<String, Value> = HashMap::default();
|
||||
for (flag, value) in flags.iter() {
|
||||
match flag {
|
||||
|
@ -742,8 +437,9 @@ impl MailBackend for JmapType {
|
|||
}
|
||||
}
|
||||
{
|
||||
let store_lck = store.read().unwrap();
|
||||
for hash in env_hashes.iter() {
|
||||
if let Some(id) = store.id_store.lock().unwrap().get(&hash) {
|
||||
if let Some(id) = store_lck.id_store.get(&hash) {
|
||||
ids.push(id.clone());
|
||||
id_map.insert(id.clone(), hash);
|
||||
update_map.insert(id.clone(), serde_json::json!(update_keywords.clone()));
|
||||
|
@ -754,41 +450,33 @@ impl MailBackend for JmapType {
|
|||
|
||||
let email_set_call: EmailSet = EmailSet::new(
|
||||
Set::<EmailObject>::new()
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.account_id(conn.mail_account_id().to_string())
|
||||
.update(Some(update_map)),
|
||||
);
|
||||
|
||||
let mut req = Request::new(conn.request_no.clone());
|
||||
req.add_call(&email_set_call);
|
||||
let prev_seq = req.add_call(&email_set_call);
|
||||
let email_call: EmailGet = EmailGet::new(
|
||||
Get::new()
|
||||
.ids(Some(JmapArgument::Value(ids)))
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.account_id(conn.mail_account_id().to_string())
|
||||
.properties(Some(vec!["keywords".to_string()])),
|
||||
);
|
||||
|
||||
req.add_call(&email_call);
|
||||
let api_url = conn.session.lock().unwrap().api_url.clone();
|
||||
//debug!(serde_json::to_string(&req)?);
|
||||
let mut res = conn
|
||||
.client
|
||||
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
|
||||
.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?;
|
||||
/*
|
||||
*{"methodResponses":[["Email/set",{"notUpdated":null,"notDestroyed":null,"oldState":"86","newState":"87","accountId":"u148940c7","updated":{"M045926eed54b11423918f392":{"id":"M045926eed54b11423918f392"}},"created":null,"destroyed":null,"notCreated":null},"m3"]],"sessionState":"cyrus-0;p-5;vfs-0"}
|
||||
*/
|
||||
//debug!("res_text = {}", &res_text);
|
||||
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
|
||||
Err(err) => {
|
||||
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
|
||||
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
Ok(s) => s,
|
||||
};
|
||||
*store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
|
||||
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
|
||||
*conn.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(MeliError::new(
|
||||
|
@ -799,51 +487,24 @@ impl MailBackend for JmapType {
|
|||
));
|
||||
}
|
||||
|
||||
{
|
||||
let mut tag_index_lck = store.collection.tag_index.write().unwrap();
|
||||
for (flag, value) in flags.iter() {
|
||||
match flag {
|
||||
Ok(_) => {}
|
||||
Err(t) => {
|
||||
if *value {
|
||||
tag_index_lck.insert(tag_hash!(t), t.clone());
|
||||
}
|
||||
let mut tag_index_lck = tag_index.write().unwrap();
|
||||
for (flag, value) in flags.iter() {
|
||||
match flag {
|
||||
Ok(f) => {}
|
||||
Err(t) => {
|
||||
if *value {
|
||||
tag_index_lck.insert(tag_hash!(t), t.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(tag_index_lck);
|
||||
}
|
||||
let e = GetResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
|
||||
let GetResponse::<EmailObject> { list, state, .. } = e;
|
||||
{
|
||||
let (is_empty, is_equal) = {
|
||||
let mailboxes_lck = conn.store.mailboxes.read().unwrap();
|
||||
mailboxes_lck
|
||||
.get(&mailbox_hash)
|
||||
.map(|mbox| {
|
||||
let current_state_lck = mbox.email_state.lock().unwrap();
|
||||
(
|
||||
current_state_lck.is_some(),
|
||||
current_state_lck.as_ref() != Some(&state),
|
||||
)
|
||||
})
|
||||
.unwrap_or((true, true))
|
||||
};
|
||||
if is_empty {
|
||||
let mut mailboxes_lck = conn.store.mailboxes.write().unwrap();
|
||||
debug!("{:?}: inserting state {}", EmailObject::NAME, &state);
|
||||
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
|
||||
*mbox.email_state.lock().unwrap() = Some(state);
|
||||
});
|
||||
} else if !is_equal {
|
||||
conn.email_changes(mailbox_hash).await?;
|
||||
}
|
||||
}
|
||||
debug!(&list);
|
||||
//debug!(&list);
|
||||
for envobj in list {
|
||||
let env_hash = id_map[&envobj.id];
|
||||
conn.add_refresh_event(RefreshEvent {
|
||||
account_hash: store.account_hash,
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::NewFlags(
|
||||
env_hash,
|
||||
|
@ -854,14 +515,6 @@ impl MailBackend for JmapType {
|
|||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn delete_messages(
|
||||
&mut self,
|
||||
_env_hashes: EnvelopeHashBatch,
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapType {
|
||||
|
@ -870,76 +523,43 @@ impl JmapType {
|
|||
is_subscribed: Box<dyn Fn(&str) -> bool + Send + Sync>,
|
||||
event_consumer: BackendEventConsumer,
|
||||
) -> Result<Box<dyn MailBackend>> {
|
||||
let online_status = Arc::new(FutureMutex::new((
|
||||
let online = Arc::new(FutureMutex::new((
|
||||
std::time::Instant::now(),
|
||||
Err(MeliError::new("Account is uninitialised.")),
|
||||
)));
|
||||
let server_conf = JmapServerConf::new(s)?;
|
||||
|
||||
let account_hash = {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::Hasher;
|
||||
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,
|
||||
account_id: Arc::new(Mutex::new(Id::new())),
|
||||
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(),
|
||||
mailboxes: Default::default(),
|
||||
mailboxes_index: Default::default(),
|
||||
mailbox_state: Default::default(),
|
||||
});
|
||||
|
||||
Ok(Box::new(JmapType {
|
||||
connection: Arc::new(FutureMutex::new(JmapConnection::new(
|
||||
&server_conf,
|
||||
store.clone(),
|
||||
account_hash,
|
||||
event_consumer,
|
||||
online.clone(),
|
||||
)?)),
|
||||
store,
|
||||
store: Arc::new(RwLock::new(Store::default())),
|
||||
tag_index: Arc::new(RwLock::new(Default::default())),
|
||||
mailboxes: Arc::new(RwLock::new(HashMap::default())),
|
||||
account_name: s.name.clone(),
|
||||
account_hash,
|
||||
online,
|
||||
is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)),
|
||||
server_conf,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn validate_config(s: &mut AccountSettings) -> Result<()> {
|
||||
macro_rules! get_conf_val {
|
||||
($s:ident[$var:literal]) => {
|
||||
$s.extra.remove($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
|
||||
.remove($var)
|
||||
.map(|v| {
|
||||
<_>::from_str(&v).map_err(|e| {
|
||||
MeliError::new(format!(
|
||||
"Configuration error ({}): Invalid value for field `{}`: {}\n{}",
|
||||
$s.name.as_str(),
|
||||
$var,
|
||||
v,
|
||||
e
|
||||
))
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| Ok($default))
|
||||
};
|
||||
}
|
||||
get_conf_val!(s["server_url"])?;
|
||||
pub fn validate_config(s: &AccountSettings) -> Result<()> {
|
||||
get_conf_val!(s["server_hostname"])?;
|
||||
get_conf_val!(s["server_username"])?;
|
||||
get_conf_val!(s["server_password"])?;
|
||||
get_conf_val!(s["server_port"], 443)?;
|
||||
get_conf_val!(s["danger_accept_invalid_certs"], false)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -21,22 +21,29 @@
|
|||
|
||||
use super::*;
|
||||
use isahc::config::Configurable;
|
||||
use std::sync::MutexGuard;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JmapConnection {
|
||||
pub session: Arc<Mutex<JmapSession>>,
|
||||
pub session: JmapSession,
|
||||
pub request_no: Arc<Mutex<usize>>,
|
||||
pub client: Arc<HttpClient>,
|
||||
pub online_status: Arc<FutureMutex<(Instant, Result<()>)>>,
|
||||
pub server_conf: JmapServerConf,
|
||||
pub store: Arc<Store>,
|
||||
pub account_id: Arc<Mutex<String>>,
|
||||
pub account_hash: AccountHash,
|
||||
pub method_call_states: Arc<Mutex<HashMap<&'static str, String>>>,
|
||||
pub event_consumer: BackendEventConsumer,
|
||||
}
|
||||
|
||||
impl JmapConnection {
|
||||
pub fn new(server_conf: &JmapServerConf, store: Arc<Store>) -> Result<Self> {
|
||||
pub fn new(
|
||||
server_conf: &JmapServerConf,
|
||||
account_hash: AccountHash,
|
||||
event_consumer: BackendEventConsumer,
|
||||
online_status: Arc<FutureMutex<(Instant, Result<()>)>>,
|
||||
) -> Result<Self> {
|
||||
let client = HttpClient::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.redirect_policy(RedirectPolicy::Limit(10))
|
||||
.authentication(isahc::auth::Authentication::basic())
|
||||
.credentials(isahc::auth::Credentials::new(
|
||||
&server_conf.server_username,
|
||||
|
@ -45,45 +52,41 @@ impl JmapConnection {
|
|||
.build()?;
|
||||
let server_conf = server_conf.clone();
|
||||
Ok(JmapConnection {
|
||||
session: Arc::new(Mutex::new(Default::default())),
|
||||
session: Default::default(),
|
||||
request_no: Arc::new(Mutex::new(0)),
|
||||
client: Arc::new(client),
|
||||
online_status,
|
||||
server_conf,
|
||||
store,
|
||||
account_id: Arc::new(Mutex::new(String::new())),
|
||||
account_hash,
|
||||
event_consumer,
|
||||
method_call_states: Arc::new(Mutex::new(Default::default())),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn connect(&mut self) -> Result<()> {
|
||||
if self.store.online_status.lock().await.1.is_ok() {
|
||||
if self.online_status.lock().await.1.is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut jmap_session_resource_url = self.server_conf.server_url.to_string();
|
||||
let mut jmap_session_resource_url =
|
||||
if self.server_conf.server_hostname.starts_with("https://") {
|
||||
self.server_conf.server_hostname.to_string()
|
||||
} else {
|
||||
format!("https://{}", &self.server_conf.server_hostname)
|
||||
};
|
||||
if self.server_conf.server_port != 443 {
|
||||
jmap_session_resource_url.push(':');
|
||||
jmap_session_resource_url.push_str(&self.server_conf.server_port.to_string());
|
||||
}
|
||||
jmap_session_resource_url.push_str("/.well-known/jmap");
|
||||
|
||||
let mut req = self.client.get_async(&jmap_session_resource_url).await.map_err(|err| {
|
||||
let err = MeliError::new(format!("Could not connect to JMAP server endpoint for {}. Is your server url setting correct? (i.e. \"jmap.mailserver.org\") (Note: only session resource discovery via /.well-known/jmap is supported. DNS SRV records are not suppported.)\nError connecting to server: {}", &self.server_conf.server_url, &err)).set_source(Some(Arc::new(err)));
|
||||
//*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
err
|
||||
})?;
|
||||
|
||||
if !req.status().is_success() {
|
||||
let kind: crate::error::NetworkErrorKind = req.status().into();
|
||||
let res_text = req.text().await.unwrap_or_default();
|
||||
let err = MeliError::new(format!(
|
||||
"Could not connect to JMAP server endpoint for {}. Reply from server: {}",
|
||||
&self.server_conf.server_url, res_text
|
||||
))
|
||||
.set_kind(kind.into());
|
||||
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
let 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 = MeliError::new(format!("Could not connect to JMAP server endpoint for {}. Is your server url setting correct? (i.e. \"jmap.mailserver.org\") (Note: only session resource discovery via /.well-known/jmap is supported. DNS SRV records are not suppported.)\nReply from server: {}", &self.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err)));
|
||||
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
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.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
Ok(s) => s,
|
||||
|
@ -92,271 +95,29 @@ impl JmapConnection {
|
|||
.capabilities
|
||||
.contains_key("urn:ietf:params:jmap:core")
|
||||
{
|
||||
let err = MeliError::new(format!("Server {} did not return JMAP Core capability (urn:ietf:params:jmap:core). Returned capabilities were: {}", &self.server_conf.server_url, session.capabilities.keys().map(String::as_str).collect::<Vec<&str>>().join(", ")));
|
||||
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
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.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
if !session
|
||||
.capabilities
|
||||
.contains_key("urn:ietf:params:jmap:mail")
|
||||
{
|
||||
let err = MeliError::new(format!("Server {} does not support JMAP Mail capability (urn:ietf:params:jmap:mail). Returned capabilities were: {}", &self.server_conf.server_url, session.capabilities.keys().map(String::as_str).collect::<Vec<&str>>().join(", ")));
|
||||
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
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.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
*self.store.online_status.lock().await = (Instant::now(), Ok(()));
|
||||
*self.session.lock().unwrap() = session;
|
||||
*self.online_status.lock().await = (Instant::now(), Ok(()));
|
||||
self.session = session;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn mail_account_id(&self) -> Id<Account> {
|
||||
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 mail_account_id(&self) -> &Id {
|
||||
&self.session.primary_accounts["urn:ietf:params:jmap:mail"]
|
||||
}
|
||||
|
||||
pub fn add_refresh_event(&self, event: RefreshEvent) {
|
||||
(self.store.event_consumer)(self.store.account_hash, BackendEvent::Refresh(event));
|
||||
}
|
||||
|
||||
pub async fn email_changes(&self, mailbox_hash: MailboxHash) -> Result<()> {
|
||||
let mut current_state: State<EmailObject> = if let Some(s) = self
|
||||
.store
|
||||
.mailboxes
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(&mailbox_hash)
|
||||
.and_then(|mbox| mbox.email_state.lock().unwrap().clone())
|
||||
{
|
||||
s
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
loop {
|
||||
let email_changes_call: EmailChanges = EmailChanges::new(
|
||||
Changes::<EmailObject>::new()
|
||||
.account_id(self.mail_account_id().clone())
|
||||
.since_state(current_state.clone()),
|
||||
);
|
||||
|
||||
let mut req = Request::new(self.request_no.clone());
|
||||
let prev_seq = req.add_call(&email_changes_call);
|
||||
let email_get_call: EmailGet = EmailGet::new(
|
||||
Get::new()
|
||||
.ids(Some(JmapArgument::reference(
|
||||
prev_seq,
|
||||
ResultField::<EmailChanges, EmailObject>::new("/created"),
|
||||
)))
|
||||
.account_id(self.mail_account_id().clone()),
|
||||
);
|
||||
|
||||
req.add_call(&email_get_call);
|
||||
if let Some(mailbox) = self.store.mailboxes.read().unwrap().get(&mailbox_hash) {
|
||||
if let Some(email_query_state) = mailbox.email_query_state.lock().unwrap().clone() {
|
||||
let email_query_changes_call = EmailQueryChanges::new(
|
||||
QueryChanges::new(self.mail_account_id().clone(), email_query_state)
|
||||
.filter(Some(Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.in_mailbox(Some(mailbox.id.clone()))
|
||||
.into(),
|
||||
))),
|
||||
);
|
||||
let seq_no = req.add_call(&email_query_changes_call);
|
||||
let email_get_call: EmailGet = EmailGet::new(
|
||||
Get::new()
|
||||
.ids(Some(JmapArgument::reference(
|
||||
seq_no,
|
||||
ResultField::<EmailQueryChanges, EmailObject>::new("/removed"),
|
||||
)))
|
||||
.account_id(self.mail_account_id().clone())
|
||||
.properties(Some(vec![
|
||||
"keywords".to_string(),
|
||||
"mailboxIds".to_string(),
|
||||
])),
|
||||
);
|
||||
req.add_call(&email_get_call);
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
let api_url = self.session.lock().unwrap().api_url.clone();
|
||||
let mut res = self
|
||||
.client
|
||||
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
|
||||
.await?;
|
||||
|
||||
let res_text = res.text().await?;
|
||||
debug!(&res_text);
|
||||
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
|
||||
Err(err) => {
|
||||
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &self.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
|
||||
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
Ok(s) => s,
|
||||
};
|
||||
let changes_response =
|
||||
ChangesResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
|
||||
if changes_response.new_state == current_state {
|
||||
return Ok(());
|
||||
}
|
||||
let get_response = GetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
|
||||
|
||||
{
|
||||
/* process get response */
|
||||
let GetResponse::<EmailObject> { list, .. } = get_response;
|
||||
|
||||
let mut mailbox_hashes: Vec<SmallVec<[MailboxHash; 8]>> =
|
||||
Vec::with_capacity(list.len());
|
||||
for envobj in &list {
|
||||
let v = self
|
||||
.store
|
||||
.mailboxes
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|(_, m)| envobj.mailbox_ids.contains_key(&m.id))
|
||||
.map(|(k, _)| *k)
|
||||
.collect::<SmallVec<[MailboxHash; 8]>>();
|
||||
mailbox_hashes.push(v);
|
||||
}
|
||||
for (env, mailbox_hashes) in list
|
||||
.into_iter()
|
||||
.map(|obj| self.store.add_envelope(obj))
|
||||
.zip(mailbox_hashes)
|
||||
{
|
||||
for mailbox_hash in mailbox_hashes.iter().skip(1).cloned() {
|
||||
let mut mailboxes_lck = self.store.mailboxes.write().unwrap();
|
||||
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
|
||||
if !env.is_seen() {
|
||||
mbox.unread_emails.lock().unwrap().insert_new(env.hash());
|
||||
}
|
||||
mbox.total_emails.lock().unwrap().insert_new(env.hash());
|
||||
});
|
||||
self.add_refresh_event(RefreshEvent {
|
||||
account_hash: self.store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Create(Box::new(env.clone())),
|
||||
});
|
||||
}
|
||||
if let Some(mailbox_hash) = mailbox_hashes.first().cloned() {
|
||||
let mut mailboxes_lck = self.store.mailboxes.write().unwrap();
|
||||
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
|
||||
if !env.is_seen() {
|
||||
mbox.unread_emails.lock().unwrap().insert_new(env.hash());
|
||||
}
|
||||
mbox.total_emails.lock().unwrap().insert_new(env.hash());
|
||||
});
|
||||
self.add_refresh_event(RefreshEvent {
|
||||
account_hash: self.store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Create(Box::new(env)),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let reverse_id_store_lck = self.store.reverse_id_store.lock().unwrap();
|
||||
let response = v.method_responses.remove(0);
|
||||
match EmailQueryChangesResponse::try_from(response) {
|
||||
Ok(EmailQueryChangesResponse {
|
||||
collapse_threads: _,
|
||||
query_changes_response:
|
||||
QueryChangesResponse {
|
||||
account_id: _,
|
||||
old_query_state,
|
||||
new_query_state,
|
||||
total: _,
|
||||
removed,
|
||||
added,
|
||||
},
|
||||
}) if old_query_state != new_query_state => {
|
||||
self.store
|
||||
.mailboxes
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.and_modify(|mbox| {
|
||||
*mbox.email_query_state.lock().unwrap() = Some(new_query_state);
|
||||
});
|
||||
/* If the "filter" or "sort" includes a mutable property, the server
|
||||
MUST include all Foos in the current results for which this
|
||||
property may have changed. The position of these may have moved
|
||||
in the results, so they must be reinserted by the client to ensure
|
||||
its query cache is correct. */
|
||||
for email_obj_id in removed
|
||||
.into_iter()
|
||||
.filter(|id| !added.iter().any(|item| item.id == *id))
|
||||
{
|
||||
if let Some(env_hash) = reverse_id_store_lck.get(&email_obj_id) {
|
||||
let mut mailboxes_lck = self.store.mailboxes.write().unwrap();
|
||||
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
|
||||
mbox.unread_emails.lock().unwrap().remove(*env_hash);
|
||||
mbox.total_emails.lock().unwrap().insert_new(*env_hash);
|
||||
});
|
||||
self.add_refresh_event(RefreshEvent {
|
||||
account_hash: self.store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::Remove(*env_hash),
|
||||
});
|
||||
}
|
||||
}
|
||||
for AddedItem {
|
||||
id: _email_obj_id,
|
||||
index: _,
|
||||
} in added
|
||||
{
|
||||
// FIXME
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
debug!(mailbox_hash);
|
||||
debug!(err);
|
||||
}
|
||||
}
|
||||
let GetResponse::<EmailObject> { list, .. } =
|
||||
GetResponse::<EmailObject>::try_from(v.method_responses.remove(0))?;
|
||||
let mut mailboxes_lck = self.store.mailboxes.write().unwrap();
|
||||
for envobj in list {
|
||||
if let Some(env_hash) = reverse_id_store_lck.get(&envobj.id) {
|
||||
let new_flags =
|
||||
protocol::keywords_to_flags(envobj.keywords().keys().cloned().collect());
|
||||
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
|
||||
if new_flags.0.contains(Flag::SEEN) {
|
||||
mbox.unread_emails.lock().unwrap().remove(*env_hash);
|
||||
} else {
|
||||
mbox.unread_emails.lock().unwrap().insert_new(*env_hash);
|
||||
}
|
||||
});
|
||||
self.add_refresh_event(RefreshEvent {
|
||||
account_hash: self.store.account_hash,
|
||||
mailbox_hash,
|
||||
kind: RefreshEventKind::NewFlags(*env_hash, new_flags),
|
||||
});
|
||||
}
|
||||
}
|
||||
drop(mailboxes_lck);
|
||||
if changes_response.has_more_changes {
|
||||
current_state = changes_response.new_state;
|
||||
} else {
|
||||
self.store
|
||||
.mailboxes
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.and_modify(|mbox| {
|
||||
*mbox.email_state.lock().unwrap() = Some(changes_response.new_state);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
(self.event_consumer)(self.account_hash, BackendEvent::Refresh(event));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
*/
|
||||
|
||||
use super::*;
|
||||
use crate::backends::{LazyCountSet, MailboxPermissions, SpecialUsageMailbox};
|
||||
use crate::backends::{MailboxPermissions, SpecialUsageMailbox};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -28,21 +28,18 @@ pub struct JmapMailbox {
|
|||
pub name: String,
|
||||
pub path: String,
|
||||
pub hash: MailboxHash,
|
||||
pub children: Vec<MailboxHash>,
|
||||
pub id: Id<MailboxObject>,
|
||||
pub v: Vec<MailboxHash>,
|
||||
pub id: String,
|
||||
pub is_subscribed: bool,
|
||||
pub my_rights: JmapRights,
|
||||
pub parent_id: Option<Id<MailboxObject>>,
|
||||
pub parent_hash: Option<MailboxHash>,
|
||||
pub parent_id: Option<String>,
|
||||
pub role: Option<String>,
|
||||
pub sort_order: u64,
|
||||
pub total_emails: Arc<Mutex<LazyCountSet>>,
|
||||
pub total_emails: Arc<Mutex<u64>>,
|
||||
pub total_threads: u64,
|
||||
pub unread_emails: Arc<Mutex<LazyCountSet>>,
|
||||
pub unread_emails: Arc<Mutex<u64>>,
|
||||
pub unread_threads: u64,
|
||||
pub usage: Arc<RwLock<SpecialUsageMailbox>>,
|
||||
pub email_state: Arc<Mutex<Option<State<EmailObject>>>>,
|
||||
pub email_query_state: Arc<Mutex<Option<String>>>,
|
||||
}
|
||||
|
||||
impl BackendMailbox for JmapMailbox {
|
||||
|
@ -65,11 +62,11 @@ impl BackendMailbox for JmapMailbox {
|
|||
}
|
||||
|
||||
fn children(&self) -> &[MailboxHash] {
|
||||
&self.children
|
||||
&self.v
|
||||
}
|
||||
|
||||
fn parent(&self) -> Option<MailboxHash> {
|
||||
self.parent_hash
|
||||
None
|
||||
}
|
||||
|
||||
fn permissions(&self) -> MailboxPermissions {
|
||||
|
@ -95,11 +92,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
|
||||
|
@ -113,8 +108,8 @@ impl BackendMailbox for JmapMailbox {
|
|||
|
||||
fn count(&self) -> Result<(usize, usize)> {
|
||||
Ok((
|
||||
self.unread_emails.lock()?.len(),
|
||||
self.total_emails.lock()?.len(),
|
||||
*self.unread_emails.lock()? as usize,
|
||||
*self.total_emails.lock()? as usize,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,30 +24,11 @@ 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::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ThreadObject;
|
||||
|
||||
impl Object for ThreadObject {
|
||||
const NAME: &'static str = "Thread";
|
||||
}
|
||||
|
||||
impl Id<EmailObject> {
|
||||
pub fn into_hash(&self) -> EnvelopeHash {
|
||||
let mut h = DefaultHasher::new();
|
||||
h.write(self.inner.as_bytes());
|
||||
h.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// 4.1.1.
|
||||
// Metadata
|
||||
// These properties represent metadata about the message in the mail
|
||||
|
@ -149,58 +130,58 @@ impl Id<EmailObject> {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EmailObject {
|
||||
#[serde(default)]
|
||||
pub id: Id<EmailObject>,
|
||||
pub id: Id,
|
||||
#[serde(default)]
|
||||
pub blob_id: Id<BlobObject>,
|
||||
pub blob_id: String,
|
||||
#[serde(default)]
|
||||
pub mailbox_ids: HashMap<Id<MailboxObject>, bool>,
|
||||
mailbox_ids: HashMap<Id, bool>,
|
||||
#[serde(default)]
|
||||
pub size: u64,
|
||||
size: u64,
|
||||
#[serde(default)]
|
||||
pub received_at: String,
|
||||
received_at: String,
|
||||
#[serde(default)]
|
||||
pub message_id: Vec<String>,
|
||||
message_id: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub to: Option<SmallVec<[EmailAddress; 1]>>,
|
||||
to: SmallVec<[EmailAddress; 1]>,
|
||||
#[serde(default)]
|
||||
pub bcc: Option<Vec<EmailAddress>>,
|
||||
bcc: Option<Vec<EmailAddress>>,
|
||||
#[serde(default)]
|
||||
pub reply_to: Option<Vec<EmailAddress>>,
|
||||
reply_to: Option<Vec<EmailAddress>>,
|
||||
#[serde(default)]
|
||||
pub cc: Option<SmallVec<[EmailAddress; 1]>>,
|
||||
cc: Option<SmallVec<[EmailAddress; 1]>>,
|
||||
#[serde(default)]
|
||||
pub sender: Option<Vec<EmailAddress>>,
|
||||
sender: Option<Vec<EmailAddress>>,
|
||||
#[serde(default)]
|
||||
pub from: Option<SmallVec<[EmailAddress; 1]>>,
|
||||
from: SmallVec<[EmailAddress; 1]>,
|
||||
#[serde(default)]
|
||||
pub in_reply_to: Option<Vec<String>>,
|
||||
in_reply_to: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub references: Option<Vec<String>>,
|
||||
references: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub keywords: HashMap<String, bool>,
|
||||
keywords: HashMap<String, bool>,
|
||||
#[serde(default)]
|
||||
pub attached_emails: Option<Id<BlobObject>>,
|
||||
attached_emails: Option<Id>,
|
||||
#[serde(default)]
|
||||
pub attachments: Vec<Value>,
|
||||
attachments: Vec<Value>,
|
||||
#[serde(default)]
|
||||
pub has_attachment: bool,
|
||||
has_attachment: bool,
|
||||
#[serde(default)]
|
||||
#[serde(deserialize_with = "deserialize_header")]
|
||||
pub headers: HashMap<String, String>,
|
||||
headers: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub html_body: Vec<HtmlBody>,
|
||||
html_body: Vec<HtmlBody>,
|
||||
#[serde(default)]
|
||||
pub preview: Option<String>,
|
||||
preview: Option<String>,
|
||||
#[serde(default)]
|
||||
pub sent_at: Option<String>,
|
||||
sent_at: Option<String>,
|
||||
#[serde(default)]
|
||||
pub subject: Option<String>,
|
||||
subject: Option<String>,
|
||||
#[serde(default)]
|
||||
pub text_body: Vec<TextBody>,
|
||||
text_body: Vec<TextBody>,
|
||||
#[serde(default)]
|
||||
pub thread_id: Id<ThreadObject>,
|
||||
thread_id: Id,
|
||||
#[serde(flatten)]
|
||||
pub extra: HashMap<String, Value>,
|
||||
extra: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
impl EmailObject {
|
||||
|
@ -209,9 +190,9 @@ impl EmailObject {
|
|||
|
||||
#[derive(Deserialize, Serialize, Debug, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Header {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
struct Header {
|
||||
name: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
fn deserialize_header<'de, D>(
|
||||
|
@ -226,9 +207,9 @@ where
|
|||
|
||||
#[derive(Deserialize, Serialize, Debug, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EmailAddress {
|
||||
pub email: String,
|
||||
pub name: Option<String>,
|
||||
struct EmailAddress {
|
||||
email: String,
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
impl Into<crate::email::Address> for EmailAddress {
|
||||
|
@ -266,11 +247,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") {
|
||||
|
@ -278,30 +263,24 @@ 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::replace(subject, String::new()).into_bytes());
|
||||
}
|
||||
|
||||
if let Some(ref mut from) = t.from {
|
||||
env.set_from(
|
||||
std::mem::replace(from, SmallVec::new())
|
||||
.into_iter()
|
||||
.map(|addr| addr.into())
|
||||
.collect::<SmallVec<[crate::email::Address; 1]>>(),
|
||||
);
|
||||
}
|
||||
if let Some(ref mut to) = t.to {
|
||||
env.set_to(
|
||||
std::mem::replace(to, SmallVec::new())
|
||||
.into_iter()
|
||||
.map(|addr| addr.into())
|
||||
.collect::<SmallVec<[crate::email::Address; 1]>>(),
|
||||
);
|
||||
}
|
||||
env.set_from(
|
||||
std::mem::replace(&mut t.from, SmallVec::new())
|
||||
.into_iter()
|
||||
.map(|addr| addr.into())
|
||||
.collect::<SmallVec<[crate::email::Address; 1]>>(),
|
||||
);
|
||||
env.set_to(
|
||||
std::mem::replace(&mut t.to, SmallVec::new())
|
||||
.into_iter()
|
||||
.map(|addr| addr.into())
|
||||
.collect::<SmallVec<[crate::email::Address; 1]>>(),
|
||||
);
|
||||
|
||||
if let Some(ref mut cc) = t.cc {
|
||||
env.set_cc(
|
||||
|
@ -321,75 +300,99 @@ impl std::convert::From<EmailObject> for crate::Envelope {
|
|||
);
|
||||
}
|
||||
|
||||
if let Some(ref r) = env.references {
|
||||
if let Some(pos) = r.refs.iter().position(|r| r == env.message_id()) {
|
||||
if env.references.is_some() {
|
||||
if let Some(pos) = env
|
||||
.references
|
||||
.as_ref()
|
||||
.map(|r| &r.refs)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.position(|r| r == env.message_id())
|
||||
{
|
||||
env.references.as_mut().unwrap().refs.remove(pos);
|
||||
}
|
||||
}
|
||||
|
||||
env.set_hash(t.id.into_hash());
|
||||
let mut h = DefaultHasher::new();
|
||||
h.write(t.id.as_bytes());
|
||||
env.set_hash(h.finish());
|
||||
env
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HtmlBody {
|
||||
pub blob_id: Id<BlobObject>,
|
||||
struct HtmlBody {
|
||||
blob_id: Id,
|
||||
#[serde(default)]
|
||||
pub charset: String,
|
||||
charset: String,
|
||||
#[serde(default)]
|
||||
pub cid: Option<String>,
|
||||
cid: Option<String>,
|
||||
#[serde(default)]
|
||||
pub disposition: Option<String>,
|
||||
disposition: Option<String>,
|
||||
#[serde(default)]
|
||||
pub headers: Value,
|
||||
headers: Value,
|
||||
#[serde(default)]
|
||||
pub language: Option<Vec<String>>,
|
||||
language: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub location: Option<String>,
|
||||
location: Option<String>,
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub part_id: Option<String>,
|
||||
pub size: u64,
|
||||
part_id: Option<String>,
|
||||
size: u64,
|
||||
#[serde(alias = "type")]
|
||||
pub content_type: String,
|
||||
content_type: String,
|
||||
#[serde(default)]
|
||||
pub sub_parts: Vec<Value>,
|
||||
sub_parts: Vec<Value>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TextBody {
|
||||
pub blob_id: Id<BlobObject>,
|
||||
struct TextBody {
|
||||
blob_id: Id,
|
||||
#[serde(default)]
|
||||
pub charset: String,
|
||||
charset: String,
|
||||
#[serde(default)]
|
||||
pub cid: Option<String>,
|
||||
cid: Option<String>,
|
||||
#[serde(default)]
|
||||
pub disposition: Option<String>,
|
||||
disposition: Option<String>,
|
||||
#[serde(default)]
|
||||
pub headers: Value,
|
||||
headers: Value,
|
||||
#[serde(default)]
|
||||
pub language: Option<Vec<String>>,
|
||||
language: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub location: Option<String>,
|
||||
location: Option<String>,
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub part_id: Option<String>,
|
||||
pub size: u64,
|
||||
part_id: Option<String>,
|
||||
size: u64,
|
||||
#[serde(alias = "type")]
|
||||
pub content_type: String,
|
||||
content_type: String,
|
||||
#[serde(default)]
|
||||
pub sub_parts: Vec<Value>,
|
||||
sub_parts: Vec<Value>,
|
||||
}
|
||||
|
||||
impl Object for EmailObject {
|
||||
const NAME: &'static str = "Email";
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EmailQueryResponse {
|
||||
pub account_id: Id,
|
||||
pub can_calculate_changes: bool,
|
||||
pub collapse_threads: bool,
|
||||
// FIXME
|
||||
pub filter: String,
|
||||
pub ids: Vec<Id>,
|
||||
pub position: u64,
|
||||
pub query_state: String,
|
||||
pub sort: Option<String>,
|
||||
pub total: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EmailQuery {
|
||||
|
@ -466,9 +469,9 @@ impl EmailGet {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EmailFilterCondition {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub in_mailbox: Option<Id<MailboxObject>>,
|
||||
pub in_mailbox: Option<Id>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub in_mailbox_other_than: Vec<Id<MailboxObject>>,
|
||||
pub in_mailbox_other_than: Vec<Id>,
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
pub before: UtcDate,
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
|
@ -514,8 +517,8 @@ impl EmailFilterCondition {
|
|||
Self::default()
|
||||
}
|
||||
|
||||
_impl!(in_mailbox: Option<Id<MailboxObject>>);
|
||||
_impl!(in_mailbox_other_than: Vec<Id<MailboxObject>>);
|
||||
_impl!(in_mailbox: Option<Id>);
|
||||
_impl!(in_mailbox_other_than: Vec<Id>);
|
||||
_impl!(before: UtcDate);
|
||||
_impl!(after: UtcDate);
|
||||
_impl!(min_size: Option<u64>);
|
||||
|
@ -579,7 +582,6 @@ impl From<crate::search::Query> for Filter<EmailFilterCondition, EmailObject> {
|
|||
fn from(val: crate::search::Query) -> Self {
|
||||
let mut ret = Filter::Condition(EmailFilterCondition::new().into());
|
||||
fn rec(q: &crate::search::Query, f: &mut Filter<EmailFilterCondition, EmailObject>) {
|
||||
use crate::datetime::{timestamp_to_string, RFC3339_FMT};
|
||||
use crate::search::Query::*;
|
||||
match q {
|
||||
Subject(t) => {
|
||||
|
@ -603,48 +605,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_FMT), true))
|
||||
.into(),
|
||||
);
|
||||
Before(_) => {
|
||||
//TODO, convert UNIX timestamp into UtcDate
|
||||
}
|
||||
After(t) => {
|
||||
*f = Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.after(timestamp_to_string(*t, Some(RFC3339_FMT), true))
|
||||
.into(),
|
||||
);
|
||||
After(_) => {
|
||||
//TODO
|
||||
}
|
||||
Between(a, b) => {
|
||||
*f = Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.after(timestamp_to_string(*a, Some(RFC3339_FMT), true))
|
||||
.into(),
|
||||
);
|
||||
*f &= Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.before(timestamp_to_string(*b, Some(RFC3339_FMT), 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
|
||||
|
@ -751,7 +728,7 @@ fn test_jmap_query() {
|
|||
|
||||
let mut r = Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.in_mailbox(Some(mailbox_id.into()))
|
||||
.in_mailbox(Some(mailbox_id))
|
||||
.into(),
|
||||
);
|
||||
r &= f;
|
||||
|
@ -760,7 +737,7 @@ fn test_jmap_query() {
|
|||
|
||||
let email_call: EmailQuery = EmailQuery::new(
|
||||
Query::new()
|
||||
.account_id("account_id".to_string().into())
|
||||
.account_id("account_id".to_string())
|
||||
.filter(Some(filter))
|
||||
.position(0),
|
||||
)
|
||||
|
@ -793,58 +770,3 @@ impl EmailSet {
|
|||
EmailSet { set_call }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EmailChanges {
|
||||
#[serde(flatten)]
|
||||
pub changes_call: Changes<EmailObject>,
|
||||
}
|
||||
|
||||
impl Method<EmailObject> for EmailChanges {
|
||||
const NAME: &'static str = "Email/changes";
|
||||
}
|
||||
|
||||
impl EmailChanges {
|
||||
pub fn new(changes_call: Changes<EmailObject>) -> Self {
|
||||
EmailChanges { changes_call }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EmailQueryChanges {
|
||||
#[serde(flatten)]
|
||||
pub query_changes_call: QueryChanges<Filter<EmailFilterCondition, EmailObject>, EmailObject>,
|
||||
}
|
||||
|
||||
impl Method<EmailObject> for EmailQueryChanges {
|
||||
const NAME: &'static str = "Email/queryChanges";
|
||||
}
|
||||
|
||||
impl EmailQueryChanges {
|
||||
pub fn new(
|
||||
query_changes_call: QueryChanges<Filter<EmailFilterCondition, EmailObject>, EmailObject>,
|
||||
) -> Self {
|
||||
EmailQueryChanges { query_changes_call }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct EmailQueryChangesResponse {
|
||||
///o The "collapseThreads" argument that was used with "Email/query".
|
||||
#[serde(default = "bool_false")]
|
||||
pub collapse_threads: bool,
|
||||
#[serde(flatten)]
|
||||
pub query_changes_response: QueryChangesResponse<EmailObject>,
|
||||
}
|
||||
|
||||
impl std::convert::TryFrom<&RawValue> for EmailQueryChangesResponse {
|
||||
type Error = crate::error::MeliError;
|
||||
fn try_from(t: &RawValue) -> Result<EmailQueryChangesResponse> {
|
||||
let res: (String, EmailQueryChangesResponse, String) =
|
||||
serde_json::from_str(t.get()).map_err(|err| crate::error::MeliError::new(format!("BUG: Could not deserialize server JSON response properly, please report this!\nReply from server: {}", &t)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug))?;
|
||||
assert_eq!(&res.0, "Email/queryChanges");
|
||||
Ok(res.1)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,201 +0,0 @@
|
|||
/*
|
||||
* meli -
|
||||
*
|
||||
* Copyright 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 serde_json::value::RawValue;
|
||||
|
||||
/// #`import`
|
||||
///
|
||||
/// Objects of type `Foo` are imported via a call to `Foo/import`.
|
||||
///
|
||||
/// It takes the following arguments:
|
||||
///
|
||||
/// - `account_id`: "Id"
|
||||
///
|
||||
/// The id of the account to use.
|
||||
///
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ImportCall {
|
||||
///accountId: "Id"
|
||||
///The id of the account to use.
|
||||
pub account_id: Id<Account>,
|
||||
///ifInState: "String|null"
|
||||
///This is a state string as returned by the "Email/get" method. If
|
||||
///supplied, the string must match the current state of the account
|
||||
///referenced by the accountId; otherwise, the method will be aborted
|
||||
///and a "stateMismatch" error returned. If null, any changes will
|
||||
///be applied to the current state.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub if_in_state: Option<State<EmailObject>>,
|
||||
///o emails: "Id[EmailImport]"
|
||||
///A map of creation id (client specified) to EmailImport objects.
|
||||
pub emails: HashMap<Id<EmailObject>, EmailImport>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EmailImport {
|
||||
///o blobId: "Id"
|
||||
///The id of the blob containing the raw message [RFC5322].
|
||||
pub blob_id: Id<BlobObject>,
|
||||
///o mailboxIds: "Id[Boolean]"
|
||||
///The ids of the Mailboxes to assign this Email to. At least one
|
||||
///Mailbox MUST be given.
|
||||
pub mailbox_ids: HashMap<Id<MailboxObject>, bool>,
|
||||
///o keywords: "String[Boolean]" (default: {})
|
||||
///The keywords to apply to the Email.
|
||||
pub keywords: HashMap<String, bool>,
|
||||
|
||||
///o receivedAt: "UTCDate" (default: time of most recent Received
|
||||
///header, or time of import on server if none)
|
||||
///The "receivedAt" date to set on the Email.
|
||||
pub received_at: Option<String>,
|
||||
}
|
||||
|
||||
impl ImportCall {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
account_id: Id::new(),
|
||||
if_in_state: None,
|
||||
emails: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
_impl!(
|
||||
/// - 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 Method<EmailObject> for ImportCall {
|
||||
const NAME: &'static str = "Email/import";
|
||||
}
|
||||
|
||||
impl EmailImport {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
blob_id: Id::new(),
|
||||
mailbox_ids: HashMap::default(),
|
||||
keywords: HashMap::default(),
|
||||
received_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
_impl!(blob_id: Id<BlobObject>);
|
||||
_impl!(mailbox_ids: HashMap<Id<MailboxObject>, bool>);
|
||||
_impl!(keywords: HashMap<String, bool>);
|
||||
_impl!(received_at: Option<String>);
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ImportError {
|
||||
///The server MAY forbid two Email objects with the same exact content
|
||||
/// [RFC5322], or even just with the same Message-ID [RFC5322], to
|
||||
/// coexist within an account. In this case, it MUST reject attempts to
|
||||
/// import an Email considered to be a duplicate with an "alreadyExists"
|
||||
/// SetError.
|
||||
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.
|
||||
existing_id: Id<EmailObject>,
|
||||
},
|
||||
///If the "blobId", "mailboxIds", or "keywords" properties are invalid
|
||||
///(e.g., missing, wrong type, id not found), the server MUST reject the
|
||||
///import with an "invalidProperties" SetError.
|
||||
InvalidProperties {
|
||||
description: Option<String>,
|
||||
properties: Vec<String>,
|
||||
},
|
||||
///If the Email cannot be imported because it would take the account
|
||||
///over quota, the import should be rejected with an "overQuota"
|
||||
///SetError.
|
||||
OverQuota { description: Option<String> },
|
||||
///If the blob referenced is not a valid message [RFC5322], the server
|
||||
///MAY modify the message to fix errors (such as removing NUL octets or
|
||||
///fixing invalid headers). If it does this, the "blobId" on the
|
||||
///response MUST represent the new representation and therefore be
|
||||
///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.
|
||||
StateMismatch,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ImportResponse {
|
||||
///o accountId: "Id"
|
||||
///The id of the account used for this call.
|
||||
pub account_id: Id<Account>,
|
||||
|
||||
///o oldState: "String|null"
|
||||
///The state string that would have been returned by "Email/get" on
|
||||
///this account before making the requested changes, or null if the
|
||||
///server doesn't know what the previous state string was.
|
||||
pub old_state: Option<State<EmailObject>>,
|
||||
|
||||
///o newState: "String"
|
||||
///The state string that will now be returned by "Email/get" on this
|
||||
///account.
|
||||
pub new_state: Option<State<EmailObject>>,
|
||||
|
||||
///o created: "Id[Email]|null"
|
||||
///A map of the creation id to an object containing the "id",
|
||||
///"blobId", "threadId", and "size" properties for each successfully
|
||||
///imported Email, or null if none.
|
||||
pub created: HashMap<Id<EmailObject>, ImportEmailResult>,
|
||||
|
||||
///o notCreated: "Id[SetError]|null"
|
||||
///A map of the creation id to a SetError object for each Email that
|
||||
///failed to be created, or null if all successful. The possible
|
||||
///errors are defined above.
|
||||
pub not_created: HashMap<Id<EmailObject>, ImportError>,
|
||||
}
|
||||
|
||||
impl std::convert::TryFrom<&RawValue> for ImportResponse {
|
||||
type Error = crate::error::MeliError;
|
||||
fn try_from(t: &RawValue) -> Result<ImportResponse> {
|
||||
let res: (String, ImportResponse, String) =
|
||||
serde_json::from_str(t.get()).map_err(|err| crate::error::MeliError::new(format!("BUG: Could not deserialize server JSON response properly, please report this!\nReply from server: {}", &t)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug))?;
|
||||
assert_eq!(&res.0, &ImportCall::NAME);
|
||||
Ok(res.1)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ImportEmailResult {
|
||||
pub id: Id<EmailObject>,
|
||||
pub blob_id: Id<BlobObject>,
|
||||
pub thread_id: Id<ThreadObject>,
|
||||
pub size: usize,
|
||||
}
|
|
@ -21,22 +21,14 @@
|
|||
|
||||
use super::*;
|
||||
|
||||
impl Id<MailboxObject> {
|
||||
pub fn into_hash(&self) -> MailboxHash {
|
||||
let mut h = DefaultHasher::new();
|
||||
h.write(self.inner.as_bytes());
|
||||
h.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MailboxObject {
|
||||
pub id: Id<MailboxObject>,
|
||||
pub id: String,
|
||||
pub is_subscribed: bool,
|
||||
pub my_rights: JmapRights,
|
||||
pub name: String,
|
||||
pub parent_id: Option<Id<MailboxObject>>,
|
||||
pub parent_id: Option<String>,
|
||||
pub role: Option<String>,
|
||||
pub sort_order: u64,
|
||||
pub total_emails: u64,
|
||||
|
|
|
@ -20,21 +20,21 @@
|
|||
*/
|
||||
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
/// `BackendOp` implementor for Imap
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JmapOp {
|
||||
hash: EnvelopeHash,
|
||||
connection: Arc<FutureMutex<JmapConnection>>,
|
||||
store: Arc<Store>,
|
||||
store: Arc<RwLock<Store>>,
|
||||
}
|
||||
|
||||
impl JmapOp {
|
||||
pub fn new(
|
||||
hash: EnvelopeHash,
|
||||
connection: Arc<FutureMutex<JmapConnection>>,
|
||||
store: Arc<Store>,
|
||||
store: Arc<RwLock<Store>>,
|
||||
) -> Self {
|
||||
JmapOp {
|
||||
hash,
|
||||
|
@ -47,9 +47,11 @@ impl JmapOp {
|
|||
impl BackendOp for JmapOp {
|
||||
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
|
||||
{
|
||||
let byte_lck = self.store.byte_cache.lock().unwrap();
|
||||
if byte_lck.contains_key(&self.hash) && byte_lck[&self.hash].bytes.is_some() {
|
||||
let ret = byte_lck[&self.hash].bytes.clone().unwrap();
|
||||
let store_lck = self.store.read().unwrap();
|
||||
if store_lck.byte_cache.contains_key(&self.hash)
|
||||
&& store_lck.byte_cache[&self.hash].bytes.is_some()
|
||||
{
|
||||
let ret = store_lck.byte_cache[&self.hash].bytes.clone().unwrap();
|
||||
return Ok(Box::pin(async move { Ok(ret.into_bytes()) }));
|
||||
}
|
||||
}
|
||||
|
@ -57,26 +59,25 @@ impl BackendOp for JmapOp {
|
|||
let hash = self.hash;
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
let blob_id = store.blob_id_store.lock().unwrap()[&hash].clone();
|
||||
let blob_id = store.read().unwrap().blob_id_store[&hash].clone();
|
||||
let mut conn = connection.lock().await;
|
||||
conn.connect().await?;
|
||||
let download_url = conn.session.lock().unwrap().download_url.clone();
|
||||
let mut res = conn
|
||||
.client
|
||||
.get_async(&download_request_format(
|
||||
download_url.as_str(),
|
||||
&conn.mail_account_id(),
|
||||
&conn.session,
|
||||
conn.mail_account_id(),
|
||||
&blob_id,
|
||||
None,
|
||||
))
|
||||
.await?;
|
||||
|
||||
let res_text = res.text().await?;
|
||||
let res_text = res.text_async().await?;
|
||||
|
||||
store
|
||||
.byte_cache
|
||||
.lock()
|
||||
.write()
|
||||
.unwrap()
|
||||
.byte_cache
|
||||
.entry(hash)
|
||||
.or_default()
|
||||
.bytes = Some(res_text.clone());
|
||||
|
|
|
@ -23,8 +23,13 @@ use super::mailbox::JmapMailbox;
|
|||
use super::*;
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, Value};
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use smallvec::SmallVec;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
pub type Id = String;
|
||||
pub type UtcDate = String;
|
||||
|
||||
use super::rfc8620::Object;
|
||||
|
@ -38,6 +43,19 @@ macro_rules! get_request_no {
|
|||
}};
|
||||
}
|
||||
|
||||
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()
|
||||
}};
|
||||
}
|
||||
|
||||
pub trait Response<OBJ: Object> {
|
||||
const NAME: &'static str;
|
||||
}
|
||||
|
@ -86,11 +104,10 @@ pub struct JsonResponse<'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();
|
||||
let mut res = conn
|
||||
.client
|
||||
.post_async(
|
||||
api_url.as_str(),
|
||||
&conn.session.api_url,
|
||||
serde_json::to_string(&json!({
|
||||
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
|
||||
"methodCalls": [["Mailbox/get", {
|
||||
|
@ -101,32 +118,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 = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
|
||||
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
Ok(s) => s,
|
||||
};
|
||||
*conn.store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
|
||||
let res_text = res.text_async().await?;
|
||||
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
|
||||
*conn.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
|
||||
*conn.account_id.lock().unwrap() = account_id;
|
||||
Ok(list
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
let MailboxObject {
|
||||
|
@ -142,26 +142,18 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
|
|||
unread_emails,
|
||||
unread_threads,
|
||||
} = r;
|
||||
let mut total_emails_set = LazyCountSet::default();
|
||||
total_emails_set.set_not_yet_seen(total_emails.try_into().unwrap_or(0));
|
||||
let total_emails = total_emails_set;
|
||||
let mut unread_emails_set = LazyCountSet::default();
|
||||
unread_emails_set.set_not_yet_seen(unread_emails.try_into().unwrap_or(0));
|
||||
let unread_emails = unread_emails_set;
|
||||
let hash = id.into_hash();
|
||||
let parent_hash = parent_id.clone().map(|id| id.into_hash());
|
||||
let hash = crate::get_path_hash!(&name);
|
||||
(
|
||||
hash,
|
||||
JmapMailbox {
|
||||
name: name.clone(),
|
||||
hash,
|
||||
path: name,
|
||||
children: Vec::new(),
|
||||
v: Vec::new(),
|
||||
id,
|
||||
is_subscribed: is_subscribed || is_personal,
|
||||
is_subscribed,
|
||||
my_rights,
|
||||
parent_id,
|
||||
parent_hash,
|
||||
role,
|
||||
usage: Default::default(),
|
||||
sort_order,
|
||||
|
@ -169,27 +161,16 @@ pub async fn get_mailboxes(conn: &JmapConnection) -> Result<HashMap<MailboxHash,
|
|||
total_threads,
|
||||
unread_emails: Arc::new(Mutex::new(unread_emails)),
|
||||
unread_threads,
|
||||
email_state: Arc::new(Mutex::new(None)),
|
||||
email_query_state: Arc::new(Mutex::new(None)),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
for key in ret.keys().cloned().collect::<SmallVec<[MailboxHash; 24]>>() {
|
||||
if let Some(parent_hash) = ret[&key].parent_hash.clone() {
|
||||
ret.entry(parent_hash).and_modify(|e| e.children.push(key));
|
||||
}
|
||||
}
|
||||
Ok(ret)
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn get_message_list(
|
||||
conn: &JmapConnection,
|
||||
mailbox: &JmapMailbox,
|
||||
) -> Result<Vec<Id<EmailObject>>> {
|
||||
pub async fn get_message_list(conn: &JmapConnection, mailbox: &JmapMailbox) -> Result<Vec<String>> {
|
||||
let email_call: EmailQuery = EmailQuery::new(
|
||||
Query::new()
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.account_id(conn.mail_account_id().to_string())
|
||||
.filter(Some(Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.in_mailbox(Some(mailbox.id.clone()))
|
||||
|
@ -202,28 +183,19 @@ pub async fn get_message_list(
|
|||
let mut req = Request::new(conn.request_no.clone());
|
||||
req.add_call(&email_call);
|
||||
|
||||
let api_url = conn.session.lock().unwrap().api_url.clone();
|
||||
let mut res = conn
|
||||
.client
|
||||
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
|
||||
.post_async(&conn.session.api_url, 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 = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
|
||||
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
Ok(s) => s,
|
||||
};
|
||||
*conn.store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
|
||||
let res_text = res.text_async().await?;
|
||||
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
|
||||
*conn.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;
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
/*
|
||||
pub async fn get_message(conn: &JmapConnection, ids: &[String]) -> Result<Vec<Envelope>> {
|
||||
let email_call: EmailGet = EmailGet::new(
|
||||
Get::new()
|
||||
|
@ -238,7 +210,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;
|
||||
|
@ -247,17 +219,18 @@ pub async fn get_message(conn: &JmapConnection, ids: &[String]) -> Result<Vec<En
|
|||
.map(std::convert::Into::into)
|
||||
.collect::<Vec<Envelope>>())
|
||||
}
|
||||
*/
|
||||
|
||||
pub async fn fetch(
|
||||
conn: &JmapConnection,
|
||||
store: &Store,
|
||||
store: &Arc<RwLock<Store>>,
|
||||
tag_index: &Arc<RwLock<BTreeMap<u64, String>>>,
|
||||
mailboxes: &Arc<RwLock<HashMap<MailboxHash, JmapMailbox>>>,
|
||||
mailbox_hash: MailboxHash,
|
||||
) -> Result<Vec<Envelope>> {
|
||||
let mailbox_id = store.mailboxes.read().unwrap()[&mailbox_hash].id.clone();
|
||||
let mailbox_id = mailboxes.read().unwrap()[&mailbox_hash].id.clone();
|
||||
let email_query_call: EmailQuery = EmailQuery::new(
|
||||
Query::new()
|
||||
.account_id(conn.mail_account_id().clone())
|
||||
.account_id(conn.mail_account_id().to_string())
|
||||
.filter(Some(Filter::Condition(
|
||||
EmailFilterCondition::new()
|
||||
.in_mailbox(Some(mailbox_id))
|
||||
|
@ -276,81 +249,93 @@ pub async fn fetch(
|
|||
prev_seq,
|
||||
EmailQuery::RESULT_FIELD_IDS,
|
||||
)))
|
||||
.account_id(conn.mail_account_id().clone()),
|
||||
.account_id(conn.mail_account_id().to_string()),
|
||||
);
|
||||
|
||||
req.add_call(&email_call);
|
||||
|
||||
let api_url = conn.session.lock().unwrap().api_url.clone();
|
||||
let mut res = conn
|
||||
.client
|
||||
.post_async(api_url.as_str(), serde_json::to_string(&req)?)
|
||||
.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 = match serde_json::from_str(&res_text) {
|
||||
Err(err) => {
|
||||
let err = MeliError::new(format!("BUG: Could not deserialize {} server JSON response properly, please report this!\nReply from server: {}", &conn.server_conf.server_url, &res_text)).set_source(Some(Arc::new(err))).set_kind(ErrorKind::Bug);
|
||||
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
|
||||
return Err(err);
|
||||
}
|
||||
Ok(s) => s,
|
||||
};
|
||||
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
|
||||
let e = GetResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
|
||||
let query_response = QueryResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
|
||||
store
|
||||
.mailboxes
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(mailbox_hash)
|
||||
.and_modify(|mbox| {
|
||||
*mbox.email_query_state.lock().unwrap() = Some(query_response.query_state);
|
||||
});
|
||||
let GetResponse::<EmailObject> { list, state, .. } = e;
|
||||
{
|
||||
let (is_empty, is_equal) = {
|
||||
let mailboxes_lck = conn.store.mailboxes.read().unwrap();
|
||||
mailboxes_lck
|
||||
.get(&mailbox_hash)
|
||||
.map(|mbox| {
|
||||
let current_state_lck = mbox.email_state.lock().unwrap();
|
||||
(
|
||||
current_state_lck.is_none(),
|
||||
current_state_lck.as_ref() != Some(&state),
|
||||
)
|
||||
let mut states_lck = conn.method_call_states.lock().unwrap();
|
||||
|
||||
if let Some(prev_state) = states_lck.get_mut(&EmailGet::NAME) {
|
||||
debug!("{:?}: prev_state was {}", EmailGet::NAME, prev_state);
|
||||
|
||||
if *prev_state != state { /* FIXME Query Changes. */ }
|
||||
|
||||
*prev_state = state;
|
||||
debug!("{:?}: curr state is {}", EmailGet::NAME, prev_state);
|
||||
} else {
|
||||
debug!("{:?}: inserting state {}", EmailGet::NAME, &state);
|
||||
states_lck.insert(EmailGet::NAME, state);
|
||||
}
|
||||
}
|
||||
let mut tag_lck = tag_index.write().unwrap();
|
||||
let ids = list
|
||||
.iter()
|
||||
.map(|obj| {
|
||||
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());
|
||||
}
|
||||
tag_hash
|
||||
})
|
||||
.unwrap_or((true, true))
|
||||
};
|
||||
if is_empty {
|
||||
let mut mailboxes_lck = conn.store.mailboxes.write().unwrap();
|
||||
debug!("{:?}: inserting state {}", EmailObject::NAME, &state);
|
||||
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
|
||||
*mbox.email_state.lock().unwrap() = Some(state);
|
||||
});
|
||||
} else if !is_equal {
|
||||
conn.email_changes(mailbox_hash).await?;
|
||||
.collect::<SmallVec<[u64; 1024]>>();
|
||||
(tags, obj.id.clone(), obj.blob_id.clone())
|
||||
})
|
||||
.collect::<Vec<(SmallVec<[u64; 1024]>, Id, Id)>>();
|
||||
drop(tag_lck);
|
||||
let mut ret = list
|
||||
.into_iter()
|
||||
.map(std::convert::Into::into)
|
||||
.collect::<Vec<Envelope>>();
|
||||
|
||||
let mut store_lck = store.write().unwrap();
|
||||
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);
|
||||
for (env, (tags, id, blob_id)) in ret.iter_mut().zip(ids.into_iter()) {
|
||||
store_lck.id_store.insert(env.hash(), id);
|
||||
store_lck.blob_id_store.insert(env.hash(), blob_id);
|
||||
for t in tags {
|
||||
match t {
|
||||
6613915297903591176 => {
|
||||
env.set_flags(env.flags() | Flag::DRAFT);
|
||||
}
|
||||
1683863812294339685 => {
|
||||
env.set_flags(env.flags() | Flag::SEEN);
|
||||
}
|
||||
2714010747478170100 => {
|
||||
env.set_flags(env.flags() | Flag::FLAGGED);
|
||||
}
|
||||
8940855303929342213 => {
|
||||
env.set_flags(env.flags() | Flag::REPLIED);
|
||||
}
|
||||
2656839745430720464 | 4091323799684325059 => { /* ignore */ }
|
||||
_ => env.labels_mut().push(t),
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut total = BTreeSet::default();
|
||||
let mut unread = BTreeSet::default();
|
||||
let mut ret = Vec::with_capacity(list.len());
|
||||
for obj in list {
|
||||
let env = store.add_envelope(obj);
|
||||
total.insert(env.hash());
|
||||
if !env.is_seen() {
|
||||
unread.insert(env.hash());
|
||||
}
|
||||
ret.push(env);
|
||||
}
|
||||
let mut mailboxes_lck = store.mailboxes.write().unwrap();
|
||||
mailboxes_lck.entry(mailbox_hash).and_modify(|mbox| {
|
||||
mbox.total_emails.lock().unwrap().insert_existing_set(total);
|
||||
mbox.unread_emails
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert_existing_set(unread);
|
||||
});
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
|
|
|
@ -19,13 +19,12 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use super::Id;
|
||||
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::*;
|
||||
|
@ -40,230 +39,53 @@ pub trait Object {
|
|||
const NAME: &'static str;
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Id<OBJ> {
|
||||
pub inner: String,
|
||||
#[serde(skip)]
|
||||
pub _ph: PhantomData<fn() -> OBJ>,
|
||||
}
|
||||
|
||||
impl<OBJ: Object> core::fmt::Debug for Id<OBJ> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.debug_tuple(&format!("Id<{}>", OBJ::NAME))
|
||||
.field(&self.inner)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for Id<String> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.debug_tuple("Id<Any>").field(&self.inner).finish()
|
||||
}
|
||||
}
|
||||
|
||||
//, Hash, Eq, PartialEq, Default)]
|
||||
impl<OBJ> Clone for Id<OBJ> {
|
||||
fn clone(&self) -> Self {
|
||||
Id {
|
||||
inner: self.inner.clone(),
|
||||
_ph: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<OBJ> std::cmp::Eq for Id<OBJ> {}
|
||||
|
||||
impl<OBJ> std::cmp::PartialEq for Id<OBJ> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.inner == other.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<OBJ> Hash for Id<OBJ> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.inner.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<OBJ> Default for Id<OBJ> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<OBJ> From<String> for Id<OBJ> {
|
||||
fn from(inner: String) -> Self {
|
||||
Id {
|
||||
inner,
|
||||
_ph: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<OBJ> core::fmt::Display for Id<OBJ> {
|
||||
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
core::fmt::Display::fmt(&self.inner, fmt)
|
||||
}
|
||||
}
|
||||
|
||||
impl<OBJ> Id<OBJ> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: String::new(),
|
||||
_ph: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.inner.as_str()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(transparent)]
|
||||
pub struct State<OBJ> {
|
||||
pub inner: String,
|
||||
#[serde(skip)]
|
||||
pub _ph: PhantomData<fn() -> OBJ>,
|
||||
}
|
||||
|
||||
//, Hash, Eq, PartialEq, Default)]
|
||||
impl<OBJ> Clone for State<OBJ> {
|
||||
fn clone(&self) -> Self {
|
||||
State {
|
||||
inner: self.inner.clone(),
|
||||
_ph: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<OBJ> std::cmp::Eq for State<OBJ> {}
|
||||
|
||||
impl<OBJ> std::cmp::PartialEq for State<OBJ> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.inner == other.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<OBJ> Hash for State<OBJ> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.inner.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<OBJ> Default for State<OBJ> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<OBJ> From<String> for State<OBJ> {
|
||||
fn from(inner: String) -> Self {
|
||||
State {
|
||||
inner,
|
||||
_ph: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<OBJ> core::fmt::Display for State<OBJ> {
|
||||
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
core::fmt::Display::fmt(&self.inner, fmt)
|
||||
}
|
||||
}
|
||||
|
||||
impl<OBJ> State<OBJ> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: String::new(),
|
||||
_ph: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.inner.as_str()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct JmapSession {
|
||||
pub capabilities: HashMap<String, CapabilitiesObject>,
|
||||
pub accounts: HashMap<Id<Account>, Account>,
|
||||
pub primary_accounts: HashMap<String, Id<Account>>,
|
||||
pub accounts: HashMap<Id, Account>,
|
||||
pub primary_accounts: HashMap<String, Id>,
|
||||
pub username: String,
|
||||
pub api_url: Arc<String>,
|
||||
pub download_url: Arc<String>,
|
||||
pub api_url: String,
|
||||
pub download_url: String,
|
||||
|
||||
pub upload_url: Arc<String>,
|
||||
pub event_source_url: Arc<String>,
|
||||
pub state: State<JmapSession>,
|
||||
pub upload_url: String,
|
||||
pub event_source_url: String,
|
||||
pub state: String,
|
||||
#[serde(flatten)]
|
||||
pub extra_properties: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
impl Object for JmapSession {
|
||||
const NAME: &'static str = "Session";
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[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>,
|
||||
}
|
||||
|
||||
impl Object for Account {
|
||||
const NAME: &'static str = "Account";
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BlobObject;
|
||||
|
||||
impl Object for BlobObject {
|
||||
const NAME: &'static str = "Blob";
|
||||
extra_properties: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
/// #`get`
|
||||
|
@ -282,10 +104,11 @@ pub struct Get<OBJ: Object>
|
|||
where
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub account_id: Id<Account>,
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
pub account_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(flatten)]
|
||||
pub ids: Option<JmapArgument<Vec<Id<OBJ>>>>,
|
||||
pub ids: Option<JmapArgument<Vec<String>>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub properties: Option<Vec<String>>,
|
||||
#[serde(skip)]
|
||||
|
@ -298,7 +121,7 @@ where
|
|||
{
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
account_id: Id::new(),
|
||||
account_id: String::new(),
|
||||
ids: None,
|
||||
properties: None,
|
||||
_ph: PhantomData,
|
||||
|
@ -309,7 +132,7 @@ where
|
|||
///
|
||||
/// The id of the account to use.
|
||||
///
|
||||
account_id: Id<Account>
|
||||
account_id: String
|
||||
);
|
||||
_impl!(
|
||||
/// - ids: `Option<JmapArgument<Vec<String>>>`
|
||||
|
@ -319,7 +142,7 @@ where
|
|||
/// type and the number of records does not exceed the
|
||||
/// "max_objects_in_get" limit.
|
||||
///
|
||||
ids: Option<JmapArgument<Vec<Id<OBJ>>>>
|
||||
ids: Option<JmapArgument<Vec<String>>>
|
||||
);
|
||||
_impl!(
|
||||
/// - properties: Option<Vec<String>>
|
||||
|
@ -395,36 +218,36 @@ pub struct MethodResponse<'a> {
|
|||
#[serde(borrow)]
|
||||
pub method_responses: Vec<&'a RawValue>,
|
||||
#[serde(default)]
|
||||
pub created_ids: HashMap<Id<String>, Id<String>>,
|
||||
pub created_ids: HashMap<Id, Id>,
|
||||
#[serde(default)]
|
||||
pub session_state: State<JmapSession>,
|
||||
pub session_state: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetResponse<OBJ: Object> {
|
||||
pub account_id: Id<Account>,
|
||||
#[serde(default = "State::default")]
|
||||
pub state: State<OBJ>,
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
pub account_id: String,
|
||||
#[serde(default)]
|
||||
pub state: String,
|
||||
pub list: Vec<OBJ>,
|
||||
pub not_found: Vec<Id<OBJ>>,
|
||||
pub not_found: Vec<String>,
|
||||
}
|
||||
|
||||
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for GetResponse<OBJ> {
|
||||
type Error = crate::error::MeliError;
|
||||
fn try_from(t: &RawValue) -> Result<GetResponse<OBJ>, crate::error::MeliError> {
|
||||
let res: (String, GetResponse<OBJ>, String) =
|
||||
serde_json::from_str(t.get()).map_err(|err| crate::error::MeliError::new(format!("BUG: Could not deserialize server JSON response properly, please report this!\nReply from server: {}", &t)).set_source(Some(Arc::new(err))).set_kind(crate::error::ErrorKind::Bug))?;
|
||||
let res: (String, GetResponse<OBJ>, String) = serde_json::from_str(t.get())?;
|
||||
assert_eq!(&res.0, &format!("{}/get", OBJ::NAME));
|
||||
Ok(res.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl<OBJ: Object> GetResponse<OBJ> {
|
||||
_impl!(get_mut account_id_mut, account_id: Id<Account>);
|
||||
_impl!(get_mut state_mut, state: State<OBJ>);
|
||||
_impl!(get_mut account_id_mut, account_id: String);
|
||||
_impl!(get_mut state_mut, state: String);
|
||||
_impl!(get_mut list_mut, list: Vec<OBJ>);
|
||||
_impl!(get_mut not_found_mut, not_found: Vec<Id<OBJ>>);
|
||||
_impl!(get_mut not_found_mut, not_found: Vec<String>);
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
|
@ -441,20 +264,20 @@ pub struct Query<F: FilterTrait<OBJ>, OBJ: Object>
|
|||
where
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub account_id: Id<Account>,
|
||||
pub filter: Option<F>,
|
||||
pub sort: Option<Comparator<OBJ>>,
|
||||
account_id: String,
|
||||
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>,
|
||||
}
|
||||
|
@ -465,7 +288,7 @@ where
|
|||
{
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
account_id: Id::new(),
|
||||
account_id: String::new(),
|
||||
filter: None,
|
||||
sort: None,
|
||||
position: 0,
|
||||
|
@ -477,7 +300,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
_impl!(account_id: Id<Account>);
|
||||
_impl!(account_id: String);
|
||||
_impl!(filter: Option<F>);
|
||||
_impl!(sort: Option<Comparator<OBJ>>);
|
||||
_impl!(position: u64);
|
||||
|
@ -502,11 +325,12 @@ pub fn bool_true() -> bool {
|
|||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryResponse<OBJ: Object> {
|
||||
pub account_id: Id<Account>,
|
||||
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||
pub account_id: String,
|
||||
pub query_state: String,
|
||||
pub can_calculate_changes: bool,
|
||||
pub position: u64,
|
||||
pub ids: Vec<Id<OBJ>>,
|
||||
pub ids: Vec<Id>,
|
||||
#[serde(default)]
|
||||
pub total: u64,
|
||||
#[serde(default)]
|
||||
|
@ -518,15 +342,14 @@ pub struct QueryResponse<OBJ: Object> {
|
|||
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for QueryResponse<OBJ> {
|
||||
type Error = crate::error::MeliError;
|
||||
fn try_from(t: &RawValue) -> Result<QueryResponse<OBJ>, crate::error::MeliError> {
|
||||
let res: (String, QueryResponse<OBJ>, String) =
|
||||
serde_json::from_str(t.get()).map_err(|err| crate::error::MeliError::new(format!("BUG: Could not deserialize server JSON response properly, please report this!\nReply from server: {}", &t)).set_source(Some(Arc::new(err))).set_kind(crate::error::ErrorKind::Bug))?;
|
||||
let res: (String, QueryResponse<OBJ>, String) = serde_json::from_str(t.get())?;
|
||||
assert_eq!(&res.0, &format!("{}/query", OBJ::NAME));
|
||||
Ok(res.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl<OBJ: Object> QueryResponse<OBJ> {
|
||||
_impl!(get_mut ids_mut, ids: Vec<Id<OBJ>>);
|
||||
_impl!(get_mut ids_mut, ids: Vec<Id>);
|
||||
}
|
||||
|
||||
pub struct ResultField<M: Method<OBJ>, OBJ: Object> {
|
||||
|
@ -587,8 +410,9 @@ pub struct Changes<OBJ: Object>
|
|||
where
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub account_id: Id<Account>,
|
||||
pub since_state: State<OBJ>,
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
pub account_id: String,
|
||||
pub since_state: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_changes: Option<u64>,
|
||||
#[serde(skip)]
|
||||
|
@ -601,8 +425,8 @@ where
|
|||
{
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
account_id: Id::new(),
|
||||
since_state: State::new(),
|
||||
account_id: String::new(),
|
||||
since_state: String::new(),
|
||||
max_changes: None,
|
||||
_ph: PhantomData,
|
||||
}
|
||||
|
@ -612,7 +436,7 @@ where
|
|||
///
|
||||
/// The id of the account to use.
|
||||
///
|
||||
account_id: Id<Account>
|
||||
account_id: String
|
||||
);
|
||||
_impl!(
|
||||
/// - since_state: "String"
|
||||
|
@ -622,7 +446,7 @@ where
|
|||
/// state.
|
||||
///
|
||||
///
|
||||
since_state: State<OBJ>
|
||||
since_state: String
|
||||
);
|
||||
_impl!(
|
||||
/// - max_changes: "UnsignedInt|null"
|
||||
|
@ -639,35 +463,35 @@ where
|
|||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChangesResponse<OBJ: Object> {
|
||||
pub account_id: Id<Account>,
|
||||
pub old_state: State<OBJ>,
|
||||
pub new_state: State<OBJ>,
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
pub account_id: String,
|
||||
pub old_state: String,
|
||||
pub new_state: String,
|
||||
pub has_more_changes: bool,
|
||||
pub created: Vec<Id<OBJ>>,
|
||||
pub updated: Vec<Id<OBJ>>,
|
||||
pub destroyed: Vec<Id<OBJ>>,
|
||||
pub created: Vec<Id>,
|
||||
pub updated: Vec<Id>,
|
||||
pub destroyed: Vec<Id>,
|
||||
#[serde(skip)]
|
||||
pub _ph: PhantomData<fn() -> OBJ>,
|
||||
_ph: PhantomData<fn() -> OBJ>,
|
||||
}
|
||||
|
||||
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for ChangesResponse<OBJ> {
|
||||
type Error = crate::error::MeliError;
|
||||
fn try_from(t: &RawValue) -> Result<ChangesResponse<OBJ>, crate::error::MeliError> {
|
||||
let res: (String, ChangesResponse<OBJ>, String) =
|
||||
serde_json::from_str(t.get()).map_err(|err| crate::error::MeliError::new(format!("BUG: Could not deserialize server JSON response properly, please report this!\nReply from server: {}", &t)).set_source(Some(Arc::new(err))).set_kind(crate::error::ErrorKind::Bug))?;
|
||||
let res: (String, ChangesResponse<OBJ>, String) = serde_json::from_str(t.get())?;
|
||||
assert_eq!(&res.0, &format!("{}/changes", OBJ::NAME));
|
||||
Ok(res.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl<OBJ: Object> ChangesResponse<OBJ> {
|
||||
_impl!(get_mut account_id_mut, account_id: Id<Account>);
|
||||
_impl!(get_mut old_state_mut, old_state: State<OBJ>);
|
||||
_impl!(get_mut new_state_mut, new_state: State<OBJ>);
|
||||
_impl!(get_mut account_id_mut, account_id: String);
|
||||
_impl!(get_mut old_state_mut, old_state: String);
|
||||
_impl!(get_mut new_state_mut, new_state: String);
|
||||
_impl!(get has_more_changes, has_more_changes: bool);
|
||||
_impl!(get_mut created_mut, created: Vec<Id<OBJ>>);
|
||||
_impl!(get_mut updated_mut, updated: Vec<Id<OBJ>>);
|
||||
_impl!(get_mut destroyed_mut, destroyed: Vec<Id<OBJ>>);
|
||||
_impl!(get_mut created_mut, created: Vec<String>);
|
||||
_impl!(get_mut updated_mut, updated: Vec<String>);
|
||||
_impl!(get_mut destroyed_mut, destroyed: Vec<String>);
|
||||
}
|
||||
|
||||
///#`set`
|
||||
|
@ -684,10 +508,11 @@ pub struct Set<OBJ: Object>
|
|||
where
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
///o accountId: "Id"
|
||||
///
|
||||
/// The id of the account to use.
|
||||
pub account_id: Id<Account>,
|
||||
pub account_id: String,
|
||||
///o ifInState: "String|null"
|
||||
///
|
||||
/// This is a state string as returned by the "Foo/get" method
|
||||
|
@ -696,7 +521,7 @@ where
|
|||
/// otherwise, the method will be aborted and a "stateMismatch" error
|
||||
/// returned. If null, any changes will be applied to the current
|
||||
/// state.
|
||||
pub if_in_state: Option<State<OBJ>>,
|
||||
pub if_in_state: Option<String>,
|
||||
///o create: "Id[Foo]|null"
|
||||
///
|
||||
/// A map of a *creation id* (a temporary id set by the client) to Foo
|
||||
|
@ -708,7 +533,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>>,
|
||||
pub create: Option<HashMap<Id, OBJ>>,
|
||||
///o update: "Id[PatchObject]|null"
|
||||
///
|
||||
/// A map of an id to a Patch object to apply to the current Foo
|
||||
|
@ -752,12 +577,12 @@ where
|
|||
/// is also a valid PatchObject. The client may choose to optimise
|
||||
/// network usage by just sending the diff or may send the whole
|
||||
/// object; the server processes it the same either way.
|
||||
pub update: Option<HashMap<Id<OBJ>, Value>>,
|
||||
pub update: Option<HashMap<Id, Value>>,
|
||||
///o destroy: "Id[]|null"
|
||||
///
|
||||
/// A list of ids for Foo objects to permanently delete, or null if no
|
||||
/// objects are to be destroyed.
|
||||
pub destroy: Option<Vec<Id<OBJ>>>,
|
||||
pub destroy: Option<Vec<Id>>,
|
||||
}
|
||||
|
||||
impl<OBJ: Object> Set<OBJ>
|
||||
|
@ -766,14 +591,14 @@ where
|
|||
{
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
account_id: Id::new(),
|
||||
account_id: String::new(),
|
||||
if_in_state: None,
|
||||
create: None,
|
||||
update: None,
|
||||
destroy: None,
|
||||
}
|
||||
}
|
||||
_impl!(account_id: Id<Account>);
|
||||
_impl!(account_id: String);
|
||||
_impl!(
|
||||
///o ifInState: "String|null"
|
||||
///
|
||||
|
@ -783,9 +608,9 @@ where
|
|||
/// otherwise, the method will be aborted and a "stateMismatch" error
|
||||
/// returned. If null, any changes will be applied to the current
|
||||
/// state.
|
||||
if_in_state: Option<State<OBJ>>
|
||||
if_in_state: Option<String>
|
||||
);
|
||||
_impl!(update: Option<HashMap<Id<OBJ>, Value>>);
|
||||
_impl!(update: Option<HashMap<Id, Value>>);
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
@ -794,17 +619,17 @@ pub struct SetResponse<OBJ: Object> {
|
|||
///o accountId: "Id"
|
||||
///
|
||||
/// The id of the account used for the call.
|
||||
pub account_id: Id<Account>,
|
||||
pub account_id: String,
|
||||
///o oldState: "String|null"
|
||||
///
|
||||
/// The state string that would have been returned by "Foo/get" before
|
||||
/// making the requested changes, or null if the server doesn't know
|
||||
/// what the previous state string was.
|
||||
pub old_state: State<OBJ>,
|
||||
pub old_state: String,
|
||||
///o newState: "String"
|
||||
///
|
||||
/// The state string that will now be returned by "Foo/get".
|
||||
pub new_state: State<OBJ>,
|
||||
pub new_state: String,
|
||||
///o created: "Id[Foo]|null"
|
||||
///
|
||||
/// A map of the creation id to an object containing any properties of
|
||||
|
@ -814,7 +639,7 @@ pub struct SetResponse<OBJ: Object> {
|
|||
/// and thus set to a default by the server.
|
||||
///
|
||||
/// This argument is null if no Foo objects were successfully created.
|
||||
pub created: Option<HashMap<Id<OBJ>, OBJ>>,
|
||||
pub created: Option<HashMap<Id, OBJ>>,
|
||||
///o updated: "Id[Foo|null]|null"
|
||||
///
|
||||
/// The keys in this map are the ids of all Foos that were
|
||||
|
@ -826,12 +651,12 @@ pub struct SetResponse<OBJ: Object> {
|
|||
/// any changes to server-set or computed properties.
|
||||
///
|
||||
/// This argument is null if no Foo objects were successfully updated.
|
||||
pub updated: Option<HashMap<Id<OBJ>, Option<OBJ>>>,
|
||||
pub updated: Option<HashMap<Id, Option<OBJ>>>,
|
||||
///o destroyed: "Id[]|null"
|
||||
///
|
||||
/// A list of Foo ids for records that were successfully destroyed, or
|
||||
/// null if none.
|
||||
pub destroyed: Option<Vec<Id<OBJ>>>,
|
||||
pub destroyed: Option<Vec<Id>>,
|
||||
///o notCreated: "Id[SetError]|null"
|
||||
///
|
||||
/// A map of the creation id to a SetError object for each record that
|
||||
|
@ -852,8 +677,7 @@ pub struct SetResponse<OBJ: Object> {
|
|||
impl<OBJ: Object + DeserializeOwned> std::convert::TryFrom<&RawValue> for SetResponse<OBJ> {
|
||||
type Error = crate::error::MeliError;
|
||||
fn try_from(t: &RawValue) -> Result<SetResponse<OBJ>, crate::error::MeliError> {
|
||||
let res: (String, SetResponse<OBJ>, String) =
|
||||
serde_json::from_str(t.get()).map_err(|err| crate::error::MeliError::new(format!("BUG: Could not deserialize server JSON response properly, please report this!\nReply from server: {}", &t)).set_source(Some(Arc::new(err))).set_kind(crate::error::ErrorKind::Bug))?;
|
||||
let res: (String, SetResponse<OBJ>, String) = serde_json::from_str(t.get())?;
|
||||
assert_eq!(&res.0, &format!("{}/set", OBJ::NAME));
|
||||
Ok(res.1)
|
||||
}
|
||||
|
@ -935,50 +759,50 @@ impl core::fmt::Display for SetError {
|
|||
}
|
||||
|
||||
pub fn download_request_format(
|
||||
download_url: &str,
|
||||
account_id: &Id<Account>,
|
||||
blob_id: &Id<BlobObject>,
|
||||
session: &JmapSession,
|
||||
account_id: &Id,
|
||||
blob_id: &Id,
|
||||
name: Option<String>,
|
||||
) -> String {
|
||||
// https://jmap.fastmail.com/download/{accountId}/{blobId}/{name}
|
||||
let mut ret = String::with_capacity(
|
||||
download_url.len()
|
||||
session.download_url.len()
|
||||
+ blob_id.len()
|
||||
+ name.as_ref().map(|n| n.len()).unwrap_or(0)
|
||||
+ account_id.len(),
|
||||
);
|
||||
let mut prev_pos = 0;
|
||||
|
||||
while let Some(pos) = download_url.as_bytes()[prev_pos..].find(b"{") {
|
||||
ret.push_str(&download_url[prev_pos..prev_pos + pos]);
|
||||
while let Some(pos) = session.download_url.as_bytes()[prev_pos..].find(b"{") {
|
||||
ret.push_str(&session.download_url[prev_pos..prev_pos + pos]);
|
||||
prev_pos += pos;
|
||||
if download_url[prev_pos..].starts_with("{accountId}") {
|
||||
ret.push_str(account_id.as_str());
|
||||
if session.download_url[prev_pos..].starts_with("{accountId}") {
|
||||
ret.push_str(account_id);
|
||||
prev_pos += "{accountId}".len();
|
||||
} else if download_url[prev_pos..].starts_with("{blobId}") {
|
||||
ret.push_str(blob_id.as_str());
|
||||
} else if session.download_url[prev_pos..].starts_with("{blobId}") {
|
||||
ret.push_str(blob_id);
|
||||
prev_pos += "{blobId}".len();
|
||||
} else if download_url[prev_pos..].starts_with("{name}") {
|
||||
} else if session.download_url[prev_pos..].starts_with("{name}") {
|
||||
ret.push_str(name.as_ref().map(String::as_str).unwrap_or(""));
|
||||
prev_pos += "{name}".len();
|
||||
}
|
||||
}
|
||||
if prev_pos != download_url.len() {
|
||||
ret.push_str(&download_url[prev_pos..]);
|
||||
if prev_pos != session.download_url.len() {
|
||||
ret.push_str(&session.download_url[prev_pos..]);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn upload_request_format(upload_url: &str, account_id: &Id<Account>) -> String {
|
||||
pub fn upload_request_format(session: &JmapSession, account_id: &Id) -> String {
|
||||
//"uploadUrl": "https://jmap.fastmail.com/upload/{accountId}/",
|
||||
let mut ret = String::with_capacity(upload_url.len() + account_id.len());
|
||||
let mut ret = String::with_capacity(session.upload_url.len() + account_id.len());
|
||||
let mut prev_pos = 0;
|
||||
|
||||
while let Some(pos) = upload_url.as_bytes()[prev_pos..].find(b"{") {
|
||||
ret.push_str(&upload_url[prev_pos..prev_pos + pos]);
|
||||
while let Some(pos) = session.upload_url.as_bytes()[prev_pos..].find(b"{") {
|
||||
ret.push_str(&session.upload_url[prev_pos..prev_pos + pos]);
|
||||
prev_pos += pos;
|
||||
if upload_url[prev_pos..].starts_with("{accountId}") {
|
||||
ret.push_str(account_id.as_str());
|
||||
if session.upload_url[prev_pos..].starts_with("{accountId}") {
|
||||
ret.push_str(account_id);
|
||||
prev_pos += "{accountId}".len();
|
||||
break;
|
||||
} else {
|
||||
|
@ -986,193 +810,8 @@ pub fn upload_request_format(upload_url: &str, account_id: &Id<Account>) -> Stri
|
|||
prev_pos += 1;
|
||||
}
|
||||
}
|
||||
if prev_pos != upload_url.len() {
|
||||
ret.push_str(&upload_url[prev_pos..]);
|
||||
if prev_pos != session.upload_url.len() {
|
||||
ret.push_str(&session.upload_url[prev_pos..]);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UploadResponse {
|
||||
///o accountId: "Id"
|
||||
///
|
||||
/// The id of the account used for the call.
|
||||
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.
|
||||
pub blob_id: Id<BlobObject>,
|
||||
///o type: "String"
|
||||
///
|
||||
///The media type of the file (as specified in [RFC6838],
|
||||
///Section 4.2) as set in the Content-Type header of the upload HTTP
|
||||
///request.
|
||||
|
||||
#[serde(rename = "type")]
|
||||
pub _type: String,
|
||||
|
||||
///o size: "UnsignedInt"
|
||||
///
|
||||
/// The size of the file in octets.
|
||||
pub size: usize,
|
||||
}
|
||||
|
||||
/// #`queryChanges`
|
||||
///
|
||||
/// The "Foo/queryChanges" method allows a client to efficiently update
|
||||
/// the state of a cached query to match the new state on the server. It
|
||||
/// takes the following arguments:
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryChanges<F: FilterTrait<OBJ>, OBJ: Object>
|
||||
where
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub account_id: Id<Account>,
|
||||
pub filter: Option<F>,
|
||||
pub sort: Option<Comparator<OBJ>>,
|
||||
///sinceQueryState: "String"
|
||||
///
|
||||
///The current state of the query in the client. This is the string
|
||||
///that was returned as the "queryState" argument in the "Foo/query"
|
||||
///response with the same sort/filter. The server will return the
|
||||
///changes made to the query since this state.
|
||||
pub since_query_state: String,
|
||||
///o maxChanges: "UnsignedInt|null"
|
||||
///
|
||||
///The maximum number of changes to return in the response. See
|
||||
///error descriptions below for more details.
|
||||
pub max_changes: Option<usize>,
|
||||
///o upToId: "Id|null"
|
||||
///
|
||||
///The last (highest-index) id the client currently has cached from
|
||||
///the query results. When there are a large number of results, in a
|
||||
///common case, the client may have only downloaded and cached a
|
||||
///small subset from the beginning of the results. If the sort and
|
||||
///filter are both only on immutable properties, this allows the
|
||||
///server to omit changes after this point in the results, which can
|
||||
///significantly increase efficiency. If they are not immutable,
|
||||
///this argument is ignored.
|
||||
pub up_to_id: Option<Id<OBJ>>,
|
||||
|
||||
///o calculateTotal: "Boolean" (default: false)
|
||||
///
|
||||
///Does the client wish to know the total number of results now in
|
||||
///the query? This may be slow and expensive for servers to
|
||||
///calculate, particularly with complex filters, so clients should
|
||||
///take care to only request the total when needed.
|
||||
#[serde(default = "bool_false")]
|
||||
pub calculate_total: bool,
|
||||
|
||||
#[serde(skip)]
|
||||
_ph: PhantomData<fn() -> OBJ>,
|
||||
}
|
||||
|
||||
impl<F: FilterTrait<OBJ>, OBJ: Object> QueryChanges<F, OBJ>
|
||||
where
|
||||
OBJ: std::fmt::Debug + Serialize,
|
||||
{
|
||||
pub fn new(account_id: Id<Account>, since_query_state: String) -> Self {
|
||||
Self {
|
||||
account_id,
|
||||
filter: None,
|
||||
sort: None,
|
||||
since_query_state,
|
||||
max_changes: None,
|
||||
up_to_id: None,
|
||||
calculate_total: false,
|
||||
_ph: PhantomData,
|
||||
}
|
||||
}
|
||||
_impl!(filter: Option<F>);
|
||||
_impl!(sort: Option<Comparator<OBJ>>);
|
||||
_impl!(max_changes: Option<usize>);
|
||||
_impl!(up_to_id: Option<Id<OBJ>>);
|
||||
_impl!(calculate_total: bool);
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
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.
|
||||
pub old_query_state: String,
|
||||
///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.
|
||||
#[serde(default)]
|
||||
pub total: Option<usize>,
|
||||
///The "id" for every Foo that was in the query results in the old
|
||||
///state and that is not in the results in the new state.
|
||||
|
||||
///If the server cannot calculate this exactly, the server MAY return
|
||||
///the ids of extra Foos in addition that may have been in the old
|
||||
///results but are not in the new results.
|
||||
|
||||
///If the sort and filter are both only on immutable properties and
|
||||
///an "upToId" is supplied and exists in the results, any ids that
|
||||
///were removed but have a higher index than "upToId" SHOULD be
|
||||
///omitted.
|
||||
|
||||
///If the "filter" or "sort" includes a mutable property, the server
|
||||
///MUST include all Foos in the current results for which this
|
||||
///property may have changed. The position of these may have moved
|
||||
///in the results, so they must be reinserted by the client to ensure
|
||||
///its query cache is correct.
|
||||
pub removed: Vec<Id<OBJ>>,
|
||||
///The id and index in the query results (in the new state) for every
|
||||
///Foo that has been added to the results since the old state AND
|
||||
///every Foo in the current results that was included in the
|
||||
///"removed" array (due to a filter or sort based upon a mutable
|
||||
///property).
|
||||
|
||||
///If the sort and filter are both only on immutable properties and
|
||||
///an "upToId" is supplied and exists in the results, any ids that
|
||||
///were added but have a higher index than "upToId" SHOULD be
|
||||
///omitted.
|
||||
|
||||
///The array MUST be sorted in order of index, with the lowest index
|
||||
///first.
|
||||
|
||||
///An *AddedItem* object has the following properties:
|
||||
|
||||
///* id: "Id"
|
||||
|
||||
///* 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:
|
||||
|
||||
///fooIds = [ "id1", "id2", null, null, "id3", "id4", null, null, null ]
|
||||
|
||||
///If it *splices out* all ids in the removed array that it has in its
|
||||
///cached results, then:
|
||||
|
||||
/// removed = [ "id2", "id31", ... ];
|
||||
/// fooIds => [ "id1", null, null, "id3", "id4", null, null, null ]
|
||||
|
||||
///and *splices in* (one by one in order, starting with the lowest
|
||||
///index) all of the ids in the added array:
|
||||
|
||||
///added = [{ id: "id5", index: 0, ... }];
|
||||
///fooIds => [ "id5", "id1", null, null, "id3", "id4", null, null, null ]
|
||||
|
||||
///and *truncates* or *extends* to the new total length, then the
|
||||
///results will now be in the new state.
|
||||
|
||||
///Note: splicing in adds the item at the given index, incrementing the
|
||||
///index of all items previously at that or a higher index. Splicing
|
||||
///out is the inverse, removing the item and decrementing the index of
|
||||
///every item after it in the array.
|
||||
pub added: Vec<AddedItem<OBJ>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddedItem<OBJ: Object> {
|
||||
pub id: Id<OBJ>,
|
||||
pub index: usize,
|
||||
}
|
||||
|
|
|
@ -30,11 +30,12 @@ use crate::backends::*;
|
|||
use crate::email::Flag;
|
||||
use crate::error::{MeliError, Result};
|
||||
use crate::shellexpand::ShellExpandTrait;
|
||||
use futures::stream::Stream;
|
||||
pub use futures::stream::Stream;
|
||||
|
||||
use memmap::{Mmap, Protection};
|
||||
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};
|
||||
|
||||
|
@ -44,7 +45,7 @@ pub struct MaildirOp {
|
|||
hash_index: HashIndexes,
|
||||
mailbox_hash: MailboxHash,
|
||||
hash: EnvelopeHash,
|
||||
slice: Option<Vec<u8>>,
|
||||
slice: Option<Mmap>,
|
||||
}
|
||||
|
||||
impl Clone for MaildirOp {
|
||||
|
@ -67,7 +68,7 @@ impl MaildirOp {
|
|||
slice: None,
|
||||
}
|
||||
}
|
||||
fn path(&self) -> Result<PathBuf> {
|
||||
fn path(&self) -> PathBuf {
|
||||
let map = self.hash_index.lock().unwrap();
|
||||
let map = &map[&self.mailbox_hash];
|
||||
debug!("looking for {} in {} map", self.hash, self.mailbox_hash);
|
||||
|
@ -76,38 +77,30 @@ impl MaildirOp {
|
|||
for e in map.iter() {
|
||||
debug!("{:#?}", e);
|
||||
}
|
||||
return Err(MeliError::new("File not found"));
|
||||
}
|
||||
|
||||
Ok(if let Some(modif) = &map[&self.hash].modified {
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()?)?;
|
||||
let mut buf_reader = BufReader::new(file);
|
||||
let mut contents = Vec::new();
|
||||
buf_reader.read_to_end(&mut contents)?;
|
||||
self.slice = Some(contents);
|
||||
self.slice = Some(Mmap::open_path(self.path(), Protection::Read)?);
|
||||
}
|
||||
let ret = Ok(self.slice.as_ref().unwrap().as_slice().to_vec());
|
||||
/* Unwrap is safe since we use ? above. */
|
||||
let ret = Ok((unsafe { self.slice.as_ref().unwrap().as_slice() }).to_vec());
|
||||
Ok(Box::pin(async move { ret }))
|
||||
}
|
||||
|
||||
fn fetch_flags(&self) -> ResultFuture<Flag> {
|
||||
let path = self.path()?;
|
||||
let path = self.path();
|
||||
let ret = Ok(path.flags());
|
||||
Ok(Box::pin(async move { ret }))
|
||||
}
|
||||
|
@ -148,7 +141,7 @@ impl MaildirMailbox {
|
|||
PathBuf::from(&settings.root_mailbox)
|
||||
.expand()
|
||||
.parent()
|
||||
.unwrap_or_else(|| Path::new("/")),
|
||||
.unwrap_or_else(|| &Path::new("/")),
|
||||
)
|
||||
.ok();
|
||||
|
||||
|
@ -217,7 +210,7 @@ impl BackendMailbox for MaildirMailbox {
|
|||
}
|
||||
|
||||
fn path(&self) -> &str {
|
||||
self.path.to_str().unwrap_or_else(|| self.name())
|
||||
self.path.to_str().unwrap_or(self.name())
|
||||
}
|
||||
|
||||
fn change_name(&mut self, s: &str) {
|
||||
|
|
|
@ -19,20 +19,15 @@
|
|||
* 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 crate::Collection;
|
||||
use futures::prelude::Stream;
|
||||
|
||||
use memmap::{Mmap, Protection};
|
||||
extern crate notify;
|
||||
use self::notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
|
||||
use std::time::Duration;
|
||||
|
@ -89,7 +84,7 @@ impl From<PathBuf> for MaildirPath {
|
|||
#[derive(Debug, Default)]
|
||||
pub struct HashIndex {
|
||||
index: HashMap<EnvelopeHash, MaildirPath>,
|
||||
_hash: MailboxHash,
|
||||
hash: MailboxHash,
|
||||
}
|
||||
|
||||
impl Deref for HashIndex {
|
||||
|
@ -107,7 +102,7 @@ impl DerefMut for HashIndex {
|
|||
|
||||
pub type HashIndexes = Arc<Mutex<HashMap<MailboxHash, HashIndex>>>;
|
||||
|
||||
/// The maildir backend instance type.
|
||||
/// Maildir backend https://cr.yp.to/proto/maildir.html
|
||||
#[derive(Debug)]
|
||||
pub struct MaildirType {
|
||||
name: String,
|
||||
|
@ -115,7 +110,6 @@ pub struct MaildirType {
|
|||
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
|
||||
hash_indexes: HashIndexes,
|
||||
event_consumer: BackendEventConsumer,
|
||||
collection: Collection,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
|
@ -147,7 +141,16 @@ macro_rules! get_path_hash {
|
|||
}};
|
||||
}
|
||||
|
||||
pub fn get_file_hash(file: &Path) -> EnvelopeHash {
|
||||
pub(super) fn get_file_hash(file: &Path) -> EnvelopeHash {
|
||||
/*
|
||||
let mut buf = Vec::with_capacity(2048);
|
||||
let mut f = fs::File::open(&file).unwrap_or_else(|_| panic!("Can't open {}", file.display()));
|
||||
f.read_to_end(&mut buf)
|
||||
.unwrap_or_else(|_| panic!("Can't read {}", file.display()));
|
||||
let mut hasher = DefaultHasher::default();
|
||||
hasher.write(&buf);
|
||||
hasher.finish()
|
||||
*/
|
||||
let mut hasher = DefaultHasher::default();
|
||||
file.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
|
@ -205,7 +208,7 @@ impl MailBackend for MaildirType {
|
|||
let unseen = mailbox.unseen.clone();
|
||||
let total = mailbox.total.clone();
|
||||
let path: PathBuf = mailbox.fs_path().into();
|
||||
let root_mailbox = self.path.to_path_buf();
|
||||
let root_path = self.path.to_path_buf();
|
||||
let map = self.hash_indexes.clone();
|
||||
let mailbox_index = self.mailbox_index.clone();
|
||||
super::stream::MaildirStream::new(
|
||||
|
@ -214,7 +217,7 @@ impl MailBackend for MaildirType {
|
|||
unseen,
|
||||
total,
|
||||
path,
|
||||
root_mailbox,
|
||||
root_path,
|
||||
map,
|
||||
mailbox_index,
|
||||
)
|
||||
|
@ -231,15 +234,33 @@ impl MailBackend for MaildirType {
|
|||
|
||||
let mailbox: &MaildirMailbox = &self.mailboxes[&mailbox_hash];
|
||||
let path: PathBuf = mailbox.fs_path().into();
|
||||
let root_mailbox = self.path.to_path_buf();
|
||||
let root_path = self.path.to_path_buf();
|
||||
let map = self.hash_indexes.clone();
|
||||
let mailbox_index = self.mailbox_index.clone();
|
||||
|
||||
Ok(Box::pin(async move {
|
||||
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();
|
||||
|
@ -257,16 +278,16 @@ impl MailBackend for MaildirType {
|
|||
}
|
||||
(*map).insert(hash, PathBuf::from(&file).into());
|
||||
}
|
||||
let mut reader = io::BufReader::new(fs::File::open(&file)?);
|
||||
buf.clear();
|
||||
reader.read_to_end(&mut buf)?;
|
||||
if let Ok(mut env) = Envelope::from_bytes(buf.as_slice(), Some(file.flags())) {
|
||||
if let Ok(mut env) = Envelope::from_bytes(
|
||||
unsafe { &Mmap::open_path(&file, Protection::Read)?.as_slice() },
|
||||
Some(file.flags()),
|
||||
) {
|
||||
env.set_hash(hash);
|
||||
mailbox_index
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(env.hash(), mailbox_hash);
|
||||
let file_name = file.strip_prefix(&root_mailbox).unwrap().to_path_buf();
|
||||
let file_name = file.strip_prefix(&root_path).unwrap().to_path_buf();
|
||||
if let Ok(cached) = cache_dir.place_cache_file(file_name) {
|
||||
/* place result in cache directory */
|
||||
let f = fs::File::create(cached)?;
|
||||
|
@ -277,11 +298,7 @@ impl MailBackend for MaildirType {
|
|||
f.set_permissions(permissions)?;
|
||||
|
||||
let writer = io::BufWriter::new(f);
|
||||
bincode::Options::serialize_into(
|
||||
bincode::config::DefaultOptions::new(),
|
||||
writer,
|
||||
&env,
|
||||
)?;
|
||||
bincode::serialize_into(writer, &env)?;
|
||||
}
|
||||
(sender)(
|
||||
account_hash,
|
||||
|
@ -334,12 +351,10 @@ impl MailBackend for MaildirType {
|
|||
hasher.write(self.name.as_bytes());
|
||||
hasher.finish()
|
||||
};
|
||||
let root_mailbox = self.path.to_path_buf();
|
||||
watcher
|
||||
.watch(&root_mailbox, RecursiveMode::Recursive)
|
||||
.unwrap();
|
||||
let root_path = self.path.to_path_buf();
|
||||
watcher.watch(&root_path, RecursiveMode::Recursive).unwrap();
|
||||
let cache_dir = xdg::BaseDirectories::with_profile("meli", &self.name).unwrap();
|
||||
debug!("watching {:?}", root_mailbox);
|
||||
debug!("watching {:?}", root_path);
|
||||
let hash_indexes = self.hash_indexes.clone();
|
||||
let mailbox_index = self.mailbox_index.clone();
|
||||
let root_mailbox_hash: MailboxHash = self
|
||||
|
@ -356,7 +371,6 @@ impl MailBackend for MaildirType {
|
|||
Ok(Box::pin(async move {
|
||||
// Move `watcher` in the closure's scope so that it doesn't get dropped.
|
||||
let _watcher = watcher;
|
||||
let mut buf = Vec::with_capacity(4096);
|
||||
loop {
|
||||
match rx.recv() {
|
||||
/*
|
||||
|
@ -387,7 +401,7 @@ impl MailBackend for MaildirType {
|
|||
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(
|
||||
|
@ -396,7 +410,6 @@ impl MailBackend for MaildirType {
|
|||
pathbuf.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
mailbox_index
|
||||
.lock()
|
||||
|
@ -431,7 +444,7 @@ impl MailBackend for MaildirType {
|
|||
&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 */
|
||||
|
@ -451,7 +464,6 @@ impl MailBackend for MaildirType {
|
|||
pathbuf.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
mailbox_index
|
||||
.lock()
|
||||
|
@ -470,14 +482,14 @@ impl MailBackend for MaildirType {
|
|||
}
|
||||
};
|
||||
let new_hash: EnvelopeHash = get_file_hash(pathbuf.as_path());
|
||||
let mut reader = io::BufReader::new(fs::File::open(&pathbuf)?);
|
||||
buf.clear();
|
||||
reader.read_to_end(&mut buf)?;
|
||||
if index_lock.get_mut(&new_hash).is_none() {
|
||||
debug!("write notice");
|
||||
if let Ok(mut env) =
|
||||
Envelope::from_bytes(buf.as_slice(), Some(pathbuf.flags()))
|
||||
{
|
||||
if let Ok(mut env) = Envelope::from_bytes(
|
||||
unsafe {
|
||||
&Mmap::open_path(&pathbuf, Protection::Read)?.as_slice()
|
||||
},
|
||||
Some(pathbuf.flags()),
|
||||
) {
|
||||
env.set_hash(new_hash);
|
||||
debug!("{}\t{:?}", new_hash, &pathbuf);
|
||||
debug!(
|
||||
|
@ -523,7 +535,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| {
|
||||
|
@ -531,13 +543,9 @@ impl MailBackend for MaildirType {
|
|||
});
|
||||
continue;
|
||||
}
|
||||
{
|
||||
let mut lck = mailbox_counts[&mailbox_hash].1.lock().unwrap();
|
||||
*lck = lck.saturating_sub(1);
|
||||
}
|
||||
*mailbox_counts[&mailbox_hash].1.lock().unwrap() -= 1;
|
||||
if !pathbuf.flags().contains(Flag::SEEN) {
|
||||
let mut lck = mailbox_counts[&mailbox_hash].0.lock().unwrap();
|
||||
*lck = lck.saturating_sub(1);
|
||||
*mailbox_counts[&mailbox_hash].0.lock().unwrap() -= 1;
|
||||
}
|
||||
|
||||
index_lock.entry(hash).and_modify(|e| {
|
||||
|
@ -593,7 +601,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);
|
||||
|
@ -603,7 +611,6 @@ impl MailBackend for MaildirType {
|
|||
dest.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
mailbox_index
|
||||
.lock()
|
||||
|
@ -683,7 +690,7 @@ impl MailBackend for MaildirType {
|
|||
}
|
||||
let file_name = dest
|
||||
.as_path()
|
||||
.strip_prefix(&root_mailbox)
|
||||
.strip_prefix(&root_path)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
debug!("filename = {:?}", file_name);
|
||||
|
@ -694,7 +701,6 @@ impl MailBackend for MaildirType {
|
|||
dest.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
mailbox_index
|
||||
.lock()
|
||||
|
@ -732,7 +738,7 @@ impl MailBackend for MaildirType {
|
|||
drop(hash_indexes_lock);
|
||||
let file_name = dest
|
||||
.as_path()
|
||||
.strip_prefix(&root_mailbox)
|
||||
.strip_prefix(&root_path)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
if let Ok(env) = add_path_to_index(
|
||||
|
@ -741,7 +747,6 @@ impl MailBackend for MaildirType {
|
|||
dest.as_path(),
|
||||
&cache_dir,
|
||||
file_name,
|
||||
&mut buf,
|
||||
) {
|
||||
mailbox_index
|
||||
.lock()
|
||||
|
@ -861,7 +866,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()
|
||||
|
@ -908,37 +913,6 @@ impl MailBackend for MaildirType {
|
|||
}))
|
||||
}
|
||||
|
||||
fn delete_messages(
|
||||
&mut self,
|
||||
env_hashes: EnvelopeHashBatch,
|
||||
mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<()> {
|
||||
let hash_index = self.hash_indexes.clone();
|
||||
Ok(Box::pin(async move {
|
||||
let mut hash_indexes_lck = hash_index.lock().unwrap();
|
||||
let hash_index = hash_indexes_lck.entry(mailbox_hash).or_default();
|
||||
|
||||
for env_hash in env_hashes.iter() {
|
||||
let _path = {
|
||||
if !hash_index.contains_key(&env_hash) {
|
||||
continue;
|
||||
}
|
||||
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(),
|
||||
}
|
||||
} else {
|
||||
hash_index[&env_hash].to_path_buf()
|
||||
}
|
||||
};
|
||||
|
||||
fs::remove_file(&_path)?;
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn copy_messages(
|
||||
&mut self,
|
||||
env_hashes: EnvelopeHashBatch,
|
||||
|
@ -966,15 +940,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()));
|
||||
|
@ -993,10 +967,6 @@ impl MailBackend for MaildirType {
|
|||
}))
|
||||
}
|
||||
|
||||
fn collection(&self) -> Collection {
|
||||
self.collection.clone()
|
||||
}
|
||||
|
||||
fn create_mailbox(
|
||||
&mut self,
|
||||
new_path: String,
|
||||
|
@ -1121,7 +1091,7 @@ impl MaildirType {
|
|||
None,
|
||||
Vec::new(),
|
||||
false,
|
||||
settings,
|
||||
&settings,
|
||||
) {
|
||||
f.children = recurse_mailboxes(mailboxes, settings, &path)?;
|
||||
for c in &f.children {
|
||||
|
@ -1144,7 +1114,7 @@ impl MaildirType {
|
|||
None,
|
||||
subdirs,
|
||||
true,
|
||||
settings,
|
||||
&settings,
|
||||
) {
|
||||
for c in &f.children {
|
||||
if let Some(f) = mailboxes.get_mut(c) {
|
||||
|
@ -1161,30 +1131,25 @@ impl MaildirType {
|
|||
}
|
||||
}
|
||||
Ok(children)
|
||||
}
|
||||
let root_mailbox = PathBuf::from(settings.root_mailbox()).expand();
|
||||
if !root_mailbox.exists() {
|
||||
};
|
||||
let root_path = PathBuf::from(settings.root_mailbox()).expand();
|
||||
if !root_path.exists() {
|
||||
return Err(MeliError::new(format!(
|
||||
"Configuration error ({}): root_mailbox `{}` is not a valid directory.",
|
||||
"Configuration error ({}): root_path `{}` is not a valid directory.",
|
||||
settings.name(),
|
||||
settings.root_mailbox.as_str()
|
||||
)));
|
||||
} else if !root_mailbox.is_dir() {
|
||||
} else if !root_path.is_dir() {
|
||||
return Err(MeliError::new(format!(
|
||||
"Configuration error ({}): root_mailbox `{}` is not a directory.",
|
||||
"Configuration error ({}): root_path `{}` is not a directory.",
|
||||
settings.name(),
|
||||
settings.root_mailbox.as_str()
|
||||
)));
|
||||
}
|
||||
|
||||
if let Ok(f) = MaildirMailbox::new(
|
||||
root_mailbox.to_str().unwrap().to_string(),
|
||||
root_mailbox
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
root_path.to_str().unwrap().to_string(),
|
||||
root_path.file_name().unwrap().to_str().unwrap().to_string(),
|
||||
None,
|
||||
Vec::with_capacity(0),
|
||||
false,
|
||||
|
@ -1194,7 +1159,7 @@ impl MaildirType {
|
|||
}
|
||||
|
||||
if mailboxes.is_empty() {
|
||||
let children = recurse_mailboxes(&mut mailboxes, settings, &root_mailbox)?;
|
||||
let children = recurse_mailboxes(&mut mailboxes, settings, &root_path)?;
|
||||
for c in &children {
|
||||
if let Some(f) = mailboxes.get_mut(c) {
|
||||
f.parent = None;
|
||||
|
@ -1202,7 +1167,7 @@ impl MaildirType {
|
|||
}
|
||||
} else {
|
||||
let root_hash = *mailboxes.keys().next().unwrap();
|
||||
let children = recurse_mailboxes(&mut mailboxes, settings, &root_mailbox)?;
|
||||
let children = recurse_mailboxes(&mut mailboxes, settings, &root_path)?;
|
||||
for c in &children {
|
||||
if let Some(f) = mailboxes.get_mut(c) {
|
||||
f.parent = Some(root_hash);
|
||||
|
@ -1225,7 +1190,7 @@ impl MaildirType {
|
|||
fh,
|
||||
HashIndex {
|
||||
index: HashMap::with_capacity_and_hasher(0, Default::default()),
|
||||
_hash: fh,
|
||||
hash: fh,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -1235,8 +1200,7 @@ impl MaildirType {
|
|||
hash_indexes: Arc::new(Mutex::new(hash_indexes)),
|
||||
mailbox_index: Default::default(),
|
||||
event_consumer,
|
||||
collection: Default::default(),
|
||||
path: root_mailbox,
|
||||
path: root_path,
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -1309,17 +1273,17 @@ impl MaildirType {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_config(s: &mut AccountSettings) -> Result<()> {
|
||||
let root_mailbox = PathBuf::from(s.root_mailbox()).expand();
|
||||
if !root_mailbox.exists() {
|
||||
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_mailbox `{}` is not a valid directory.",
|
||||
"Configuration error ({}): root_path `{}` is not a valid directory.",
|
||||
s.name(),
|
||||
s.root_mailbox.as_str()
|
||||
)));
|
||||
} else if !root_mailbox.is_dir() {
|
||||
} else if !root_path.is_dir() {
|
||||
return Err(MeliError::new(format!(
|
||||
"Configuration error ({}): root_mailbox `{}` is not a directory.",
|
||||
"Configuration error ({}): root_path `{}` is not a directory.",
|
||||
s.name(),
|
||||
s.root_mailbox.as_str()
|
||||
)));
|
||||
|
@ -1327,24 +1291,6 @@ impl MaildirType {
|
|||
|
||||
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(
|
||||
|
@ -1353,7 +1299,6 @@ fn add_path_to_index(
|
|||
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);
|
||||
|
@ -1368,10 +1313,11 @@ fn add_path_to_index(
|
|||
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()))?;
|
||||
//Mmap::open_path(self.path(), Protection::Read)?
|
||||
let mut env = Envelope::from_bytes(
|
||||
unsafe { &Mmap::open_path(path, Protection::Read)?.as_slice() },
|
||||
Some(path.flags()),
|
||||
)?;
|
||||
env.set_hash(env_hash);
|
||||
debug!(
|
||||
"add_path_to_index gen {}\t{}",
|
||||
|
@ -1388,7 +1334,7 @@ fn add_path_to_index(
|
|||
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)?;
|
||||
bincode::serialize_into(writer, &env)?;
|
||||
}
|
||||
Ok(env)
|
||||
}
|
||||
|
|
|
@ -25,7 +25,8 @@ use core::future::Future;
|
|||
use core::pin::Pin;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use futures::task::{Context, Poll};
|
||||
use std::io::{self, Read};
|
||||
use memmap::{Mmap, Protection};
|
||||
use std::io::{self};
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
use std::result;
|
||||
|
@ -46,26 +47,40 @@ impl MaildirStream {
|
|||
unseen: Arc<Mutex<usize>>,
|
||||
total: Arc<Mutex<usize>>,
|
||||
mut path: PathBuf,
|
||||
root_mailbox: PathBuf,
|
||||
root_path: PathBuf,
|
||||
map: HashIndexes,
|
||||
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
|
||||
) -> Result<Pin<Box<dyn Stream<Item = Result<Vec<Envelope>>> + Send + 'static>>> {
|
||||
let chunk_size = 2048;
|
||||
path.push("new");
|
||||
for p in path.read_dir()?.flatten() {
|
||||
move_to_cur(p.path()).ok().take();
|
||||
for d in path.read_dir()? {
|
||||
if let Ok(p) = d {
|
||||
move_to_cur(p.path()).ok().take();
|
||||
}
|
||||
}
|
||||
path.pop();
|
||||
|
||||
path.push("cur");
|
||||
let files: Vec<PathBuf> = path
|
||||
.read_dir()?
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.collect::<Vec<_>>();
|
||||
let iter = path.read_dir()?;
|
||||
let count = path.read_dir()?.count();
|
||||
let mut files: Vec<PathBuf> = Vec::with_capacity(count);
|
||||
for e in iter {
|
||||
let e = e.and_then(|x| {
|
||||
let path = x.path();
|
||||
Ok(path)
|
||||
})?;
|
||||
files.push(e);
|
||||
}
|
||||
let payloads = Box::pin(if !files.is_empty() {
|
||||
let cores = 4_usize;
|
||||
let chunk_size = if count / cores > 0 {
|
||||
count / cores
|
||||
} else {
|
||||
count
|
||||
};
|
||||
files
|
||||
.chunks(chunk_size)
|
||||
.map(|chunk| {
|
||||
//Self::chunk(chunk, name, mailbox_hash, unseen, total, path, root_path, map, mailbox_index)})
|
||||
let cache_dir = xdg::BaseDirectories::with_profile("meli", &name).unwrap();
|
||||
Box::pin(Self::chunk(
|
||||
SmallVec::from(chunk),
|
||||
|
@ -73,7 +88,7 @@ impl MaildirStream {
|
|||
mailbox_hash,
|
||||
unseen.clone(),
|
||||
total.clone(),
|
||||
root_mailbox.clone(),
|
||||
root_path.clone(),
|
||||
map.clone(),
|
||||
mailbox_index.clone(),
|
||||
)) as Pin<Box<dyn Future<Output = _> + Send + 'static>>
|
||||
|
@ -91,94 +106,83 @@ impl MaildirStream {
|
|||
mailbox_hash: MailboxHash,
|
||||
unseen: Arc<Mutex<usize>>,
|
||||
total: Arc<Mutex<usize>>,
|
||||
root_mailbox: PathBuf,
|
||||
root_path: PathBuf,
|
||||
map: HashIndexes,
|
||||
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
|
||||
) -> Result<Vec<Envelope>> {
|
||||
let len = chunk.len();
|
||||
let size = if len <= 100 { 100 } else { (len / 100) * 100 };
|
||||
let mut local_r: Vec<Envelope> = Vec::with_capacity(chunk.len());
|
||||
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_mailbox)
|
||||
.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 {
|
||||
for c in chunk.chunks(size) {
|
||||
let map = map.clone();
|
||||
for file in c {
|
||||
/* 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 reader = io::BufReader::new(fs::File::open(&cached).unwrap());
|
||||
let result: result::Result<Envelope, _> = bincode::deserialize_from(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.lock().unwrap() += 1;
|
||||
}
|
||||
*total.lock().unwrap() += 1;
|
||||
local_r.push(env);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let env_hash = get_file_hash(file);
|
||||
{
|
||||
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;
|
||||
(*map).insert(env_hash, PathBuf::from(file).into());
|
||||
}
|
||||
/* Try delete invalid file */
|
||||
let _ = fs::remove_file(&cached);
|
||||
};
|
||||
let env_hash = get_file_hash(&file);
|
||||
{
|
||||
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();
|
||||
reader.read_to_end(&mut buf)?;
|
||||
match Envelope::from_bytes(buf.as_slice(), Some(file.flags())) {
|
||||
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();
|
||||
match Envelope::from_bytes(
|
||||
unsafe { &Mmap::open_path(&file, Protection::Read)?.as_slice() },
|
||||
Some(file.flags()),
|
||||
) {
|
||||
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)?;
|
||||
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,
|
||||
)?;
|
||||
let writer = io::BufWriter::new(f);
|
||||
bincode::serialize_into(writer, &env)?;
|
||||
}
|
||||
if !env.is_seen() {
|
||||
*unseen.lock().unwrap() += 1;
|
||||
}
|
||||
*total.lock().unwrap() += 1;
|
||||
local_r.push(env);
|
||||
}
|
||||
if !env.is_seen() {
|
||||
unseen_total += 1;
|
||||
Err(err) => {
|
||||
debug!(
|
||||
"DEBUG: hash {}, path: {} couldn't be parsed, {}",
|
||||
env_hash,
|
||||
file.as_path().display(),
|
||||
err,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
local_r.push(env);
|
||||
}
|
||||
Err(err) => {
|
||||
debug!(
|
||||
"DEBUG: hash {}, path: {} couldn't be parsed, {}",
|
||||
env_hash,
|
||||
file.as_path().display(),
|
||||
err,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
*total.lock().unwrap() += local_r.len();
|
||||
*unseen.lock().unwrap() += unseen_total;
|
||||
Ok(local_r)
|
||||
}
|
||||
}
|
||||
|
@ -186,6 +190,7 @@ impl MaildirStream {
|
|||
impl Stream for MaildirStream {
|
||||
type Item = Result<Vec<Envelope>>;
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
//todo!()
|
||||
let payloads = self.payloads.as_mut();
|
||||
payloads.poll_next(cx)
|
||||
}
|
||||
|
|
|
@ -19,144 +19,44 @@
|
|||
* 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::MeliError>(())
|
||||
//! ```
|
||||
/*!
|
||||
* https://wiki2.dovecot.org/MailboxFormat/mbox
|
||||
*/
|
||||
|
||||
use crate::backends::*;
|
||||
use crate::collection::Collection;
|
||||
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 memmap::{Mmap, Protection};
|
||||
use nom::bytes::complete::tag;
|
||||
use nom::character::complete::digit1;
|
||||
use nom::combinator::map_res;
|
||||
use nom::{self, error::Error as NomError, error::ErrorKind, IResult};
|
||||
use nom::{self, error::ErrorKind, IResult};
|
||||
|
||||
extern crate notify;
|
||||
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::io::BufReader;
|
||||
use std::io::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;
|
||||
type Offset = usize;
|
||||
type Length = usize;
|
||||
|
||||
pub type Offset = usize;
|
||||
pub type Length = usize;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
const F_OFD_SETLKW: libc::c_int = 38;
|
||||
|
||||
// Open file description locking
|
||||
// # man fcntl
|
||||
fn get_rw_lock_blocking(f: &File, path: &Path) -> Result<()> {
|
||||
fn get_rw_lock_blocking(f: &File) {
|
||||
let fd: libc::c_int = f.as_raw_fd();
|
||||
let mut flock: libc::flock = libc::flock {
|
||||
l_type: libc::F_WRLCK as libc::c_short,
|
||||
|
@ -165,23 +65,11 @@ fn get_rw_lock_blocking(f: &File, path: &Path) -> Result<()> {
|
|||
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." */
|
||||
#[cfg(target_os = "freebsd")]
|
||||
l_sysid: 0,
|
||||
};
|
||||
let ptr: *mut libc::flock = &mut flock;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let ret_val = unsafe { libc::fcntl(fd, libc::F_SETLKW, ptr as *mut libc::c_void) };
|
||||
#[cfg(target_os = "linux")]
|
||||
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(MeliError::new(format!(
|
||||
"Could not lock {}: fcntl() returned {}",
|
||||
path.display(),
|
||||
err.desc()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
debug!(&ret_val);
|
||||
assert!(-1 != ret_val);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -272,19 +160,19 @@ impl BackendMailbox for MboxMailbox {
|
|||
/// `BackendOp` implementor for Mbox
|
||||
#[derive(Debug, Default)]
|
||||
pub struct MboxOp {
|
||||
_hash: EnvelopeHash,
|
||||
hash: EnvelopeHash,
|
||||
path: PathBuf,
|
||||
offset: Offset,
|
||||
length: Length,
|
||||
slice: std::cell::RefCell<Option<Vec<u8>>>,
|
||||
slice: Option<Mmap>,
|
||||
}
|
||||
|
||||
impl MboxOp {
|
||||
pub fn new(_hash: EnvelopeHash, path: &Path, offset: Offset, length: Length) -> Self {
|
||||
pub fn new(hash: EnvelopeHash, path: &Path, offset: Offset, length: Length) -> Self {
|
||||
MboxOp {
|
||||
_hash,
|
||||
hash,
|
||||
path: path.to_path_buf(),
|
||||
slice: std::cell::RefCell::new(None),
|
||||
slice: None,
|
||||
offset,
|
||||
length,
|
||||
}
|
||||
|
@ -293,38 +181,28 @@ impl MboxOp {
|
|||
|
||||
impl BackendOp for MboxOp {
|
||||
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
|
||||
if self.slice.get_mut().is_none() {
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(&self.path)?;
|
||||
get_rw_lock_blocking(&file, &self.path)?;
|
||||
let mut buf_reader = BufReader::new(file);
|
||||
let mut contents = Vec::new();
|
||||
buf_reader.read_to_end(&mut contents)?;
|
||||
*self.slice.get_mut() = Some(contents);
|
||||
if self.slice.is_none() {
|
||||
self.slice = Some(Mmap::open_path(&self.path, Protection::Read)?);
|
||||
}
|
||||
let ret = Ok(self.slice.get_mut().as_ref().unwrap().as_slice()
|
||||
[self.offset..self.offset + self.length]
|
||||
.to_vec());
|
||||
/* Unwrap is safe since we use ? above. */
|
||||
let ret = Ok((unsafe {
|
||||
&self.slice.as_ref().unwrap().as_slice()[self.offset..self.offset + self.length]
|
||||
})
|
||||
.to_vec());
|
||||
Ok(Box::pin(async move { ret }))
|
||||
}
|
||||
|
||||
fn fetch_flags(&self) -> ResultFuture<Flag> {
|
||||
let mut flags = Flag::empty();
|
||||
if self.slice.borrow().is_none() {
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(&self.path)?;
|
||||
get_rw_lock_blocking(&file, &self.path)?;
|
||||
let mut buf_reader = BufReader::new(file);
|
||||
let mut contents = Vec::new();
|
||||
buf_reader.read_to_end(&mut contents)?;
|
||||
*self.slice.borrow_mut() = Some(contents);
|
||||
}
|
||||
let slice_ref = self.slice.borrow();
|
||||
let (_, headers) = parser::headers::headers_raw(slice_ref.as_ref().unwrap().as_slice())?;
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(&self.path)?;
|
||||
get_rw_lock_blocking(&file);
|
||||
let mut buf_reader = BufReader::new(file);
|
||||
let mut contents = Vec::new();
|
||||
buf_reader.read_to_end(&mut contents)?;
|
||||
let (_, headers) = parser::headers::headers_raw(contents.as_slice())?;
|
||||
if let Some(start) = headers.find(b"Status:") {
|
||||
if let Some(end) = headers[start..].find(b"\n") {
|
||||
let start = start + b"Status:".len();
|
||||
|
@ -372,30 +250,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
|
||||
}
|
||||
|
@ -438,8 +300,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 {
|
||||
|
@ -492,10 +354,7 @@ impl MboxFormat {
|
|||
}
|
||||
Err(err) => {
|
||||
debug!("Could not parse mail {:?}", err);
|
||||
Err(nom::Err::Error(NomError {
|
||||
input,
|
||||
code: ErrorKind::Tag,
|
||||
}))
|
||||
Err(nom::Err::Error((input, ErrorKind::Tag)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -539,10 +398,7 @@ impl MboxFormat {
|
|||
}
|
||||
Err(err) => {
|
||||
debug!("Could not parse mail at {:?}", err);
|
||||
Err(nom::Err::Error(NomError {
|
||||
input,
|
||||
code: ErrorKind::Tag,
|
||||
}))
|
||||
Err(nom::Err::Error((input, ErrorKind::Tag)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -596,10 +452,7 @@ impl MboxFormat {
|
|||
}
|
||||
Err(err) => {
|
||||
debug!("Could not parse mail {:?}", err);
|
||||
Err(nom::Err::Error(NomError {
|
||||
input,
|
||||
code: ErrorKind::Tag,
|
||||
}))
|
||||
Err(nom::Err::Error((input, ErrorKind::Tag)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -643,10 +496,7 @@ impl MboxFormat {
|
|||
}
|
||||
Err(err) => {
|
||||
debug!("Could not parse mail {:?}", err);
|
||||
Err(nom::Err::Error(NomError {
|
||||
input,
|
||||
code: ErrorKind::Tag,
|
||||
}))
|
||||
Err(nom::Err::Error((input, ErrorKind::Tag)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -734,21 +584,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: ErrorKind::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
|
||||
|
@ -780,12 +627,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> {
|
||||
|
@ -796,10 +643,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
|
||||
|
@ -840,10 +687,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,
|
||||
}
|
||||
|
||||
|
@ -872,7 +718,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>,
|
||||
|
@ -884,10 +730,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;
|
||||
|
@ -936,7 +782,7 @@ impl MailBackend for MboxType {
|
|||
.read(true)
|
||||
.write(true)
|
||||
.open(&mailbox_path)?;
|
||||
get_rw_lock_blocking(&file, &mailbox_path)?;
|
||||
get_rw_lock_blocking(&file);
|
||||
let mut buf_reader = BufReader::new(file);
|
||||
let mut contents = Vec::new();
|
||||
buf_reader.read_to_end(&mut contents)?;
|
||||
|
@ -1014,7 +860,7 @@ impl MailBackend for MboxType {
|
|||
continue;
|
||||
}
|
||||
};
|
||||
get_rw_lock_blocking(&file, &pathbuf)?;
|
||||
get_rw_lock_blocking(&file);
|
||||
let mut mailbox_lock = mailboxes.lock().unwrap();
|
||||
let mut buf_reader = BufReader::new(file);
|
||||
let mut contents = Vec::new();
|
||||
|
@ -1166,14 +1012,6 @@ impl MailBackend for MboxType {
|
|||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn delete_messages(
|
||||
&mut self,
|
||||
_env_hashes: EnvelopeHashBatch,
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn save(
|
||||
&self,
|
||||
_bytes: Vec<u8>,
|
||||
|
@ -1190,10 +1028,6 @@ impl MailBackend for MboxType {
|
|||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn collection(&self) -> Collection {
|
||||
self.collection.clone()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! get_conf_val {
|
||||
|
@ -1245,10 +1079,10 @@ impl MboxType {
|
|||
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(MeliError::new(format!(
|
||||
"{} invalid `prefer_mbox_type` value: `{}`",
|
||||
|
@ -1257,7 +1091,6 @@ impl MboxType {
|
|||
)))
|
||||
}
|
||||
},
|
||||
collection: Collection::default(),
|
||||
mailbox_index: Default::default(),
|
||||
mailboxes: Default::default(),
|
||||
};
|
||||
|
@ -1355,34 +1188,7 @@ 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(|| {
|
||||
MeliError::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| {
|
||||
MeliError::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(MeliError::new(format!(
|
||||
|
|
|
@ -1,260 +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::*;
|
||||
|
||||
impl MboxFormat {
|
||||
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(MeliError::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(
|
||||
crate::datetime::timestamp_to_string(
|
||||
delivery_date.unwrap_or_else(crate::datetime::now),
|
||||
Some(crate::datetime::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::<(), MeliError>(())
|
||||
};
|
||||
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::<(), MeliError>(())
|
||||
}
|
||||
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 {
|
||||
MboxFormat::MboxO | MboxFormat::MboxRd => Err(MeliError::new("Unimplemented.")),
|
||||
MboxFormat::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(())
|
||||
}
|
||||
MboxFormat::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,13 +19,6 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! # NNTP backend / client
|
||||
//!
|
||||
//! Implements an NNTP client as specified by [RFC 3977: Network News Transfer Protocol
|
||||
//! (NNTP)](https://datatracker.ietf.org/doc/html/rfc3977). Also implements [RFC 6048: Network News
|
||||
//! Transfer Protocol (NNTP) Additions to LIST
|
||||
//! Command](https://datatracker.ietf.org/doc/html/rfc6048).
|
||||
|
||||
use crate::get_conf_val;
|
||||
use crate::get_path_hash;
|
||||
use smallvec::SmallVec;
|
||||
|
@ -39,61 +32,25 @@ pub use operations::*;
|
|||
mod connection;
|
||||
pub use connection::*;
|
||||
|
||||
use crate::backends::*;
|
||||
use crate::conf::AccountSettings;
|
||||
use crate::connections::timeout;
|
||||
use crate::email::*;
|
||||
use crate::error::{MeliError, Result, ResultIntoMeliError};
|
||||
use crate::{backends::*, Collection};
|
||||
use futures::lock::Mutex as FutureMutex;
|
||||
use futures::stream::Stream;
|
||||
use std::collections::{hash_map::DefaultHasher, BTreeSet, HashMap, HashSet};
|
||||
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};
|
||||
use std::time::{Duration, Instant};
|
||||
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(|| {
|
||||
MeliError::new(format!(
|
||||
"Configuration error ({}): NNTP connection requires the field `{}` set",
|
||||
$s.name.as_str(),
|
||||
$var
|
||||
))
|
||||
})
|
||||
};
|
||||
($s:ident[$var:literal], $default:expr) => {
|
||||
$s.extra
|
||||
.get($var)
|
||||
.map(|v| {
|
||||
<_>::from_str(v).map_err(|e| {
|
||||
MeliError::new(format!(
|
||||
"Configuration error ({}) NNTP: Invalid value for field `{}`: {}\n{}",
|
||||
$s.name.as_str(),
|
||||
$var,
|
||||
v,
|
||||
e
|
||||
))
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| Ok($default))
|
||||
};
|
||||
}
|
||||
|
||||
pub static SUPPORTED_CAPABILITIES: &[&str] = &[
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
"COMPRESS DEFLATE",
|
||||
"VERSION 2",
|
||||
"NEWNEWS",
|
||||
"POST",
|
||||
"OVER",
|
||||
"OVER MSGID",
|
||||
"READER",
|
||||
"STARTTLS",
|
||||
"HDR",
|
||||
"AUTHINFO USER",
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -109,18 +66,31 @@ pub struct NntpServerConf {
|
|||
pub extension_use: NntpExtensionUse,
|
||||
}
|
||||
|
||||
pub struct IsSubscribedFn(Box<dyn Fn(&str) -> bool + Send + Sync>);
|
||||
|
||||
impl std::fmt::Debug for IsSubscribedFn {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "IsSubscribedFn Box")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for IsSubscribedFn {
|
||||
type Target = Box<dyn Fn(&str) -> bool + Send + Sync>;
|
||||
fn deref(&self) -> &Box<dyn Fn(&str) -> bool + Send + Sync> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
type Capabilities = HashSet<String>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UIDStore {
|
||||
account_hash: AccountHash,
|
||||
account_name: Arc<String>,
|
||||
offline_cache: bool,
|
||||
capabilities: Arc<Mutex<Capabilities>>,
|
||||
message_id_index: Arc<Mutex<HashMap<String, EnvelopeHash>>>,
|
||||
hash_index: Arc<Mutex<HashMap<EnvelopeHash, (UID, MailboxHash)>>>,
|
||||
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,
|
||||
|
@ -136,12 +106,11 @@ impl 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(MeliError::new("Account is uninitialised.")),
|
||||
|
@ -152,11 +121,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 {
|
||||
|
@ -174,7 +143,6 @@ impl MailBackend for NntpType {
|
|||
)
|
||||
})
|
||||
.collect::<Vec<(String, MailBackendExtensionStatus)>>();
|
||||
let mut supports_submission = false;
|
||||
let NntpExtensionUse {
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
deflate,
|
||||
|
@ -182,10 +150,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")]
|
||||
{
|
||||
|
@ -219,7 +183,7 @@ impl MailBackend for NntpType {
|
|||
supports_search: false,
|
||||
extensions: Some(extensions),
|
||||
supports_tags: false,
|
||||
supports_submission,
|
||||
supports_submission: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -249,87 +213,8 @@ impl MailBackend for NntpType {
|
|||
}))
|
||||
}
|
||||
|
||||
fn refresh(&mut self, mailbox_hash: MailboxHash) -> ResultFuture<()> {
|
||||
let uid_store = self.uid_store.clone();
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
/* To get updates, either issue NEWNEWS if it's supported by the server, and fallback
|
||||
* to OVER otherwise */
|
||||
let mbox: NntpMailbox = uid_store.mailboxes.lock().await.get(&mailbox_hash).map(std::clone::Clone::clone).ok_or_else(|| MeliError::new(format!("Mailbox with hash {} not found in NNTP connection, this could possibly be a bug or it was deleted.", mailbox_hash)))?;
|
||||
let latest_article: Option<crate::UnixTimestamp> = *mbox.latest_article.lock().unwrap();
|
||||
let (over_msgid_support, newnews_support): (bool, bool) = {
|
||||
let caps = uid_store.capabilities.lock().unwrap();
|
||||
|
||||
(
|
||||
caps.iter().any(|c| c.eq_ignore_ascii_case("OVER MSGID")),
|
||||
caps.iter().any(|c| c.eq_ignore_ascii_case("NEWNEWS")),
|
||||
)
|
||||
};
|
||||
let mut res = String::with_capacity(8 * 1024);
|
||||
let mut conn = timeout(Some(Duration::from_secs(60 * 16)), connection.lock()).await?;
|
||||
if let Some(mut latest_article) = latest_article {
|
||||
let timestamp = latest_article - 10 * 60;
|
||||
let datetime_str =
|
||||
crate::datetime::timestamp_to_string(timestamp, Some("%Y%m%d %H%M%S"), true);
|
||||
|
||||
if newnews_support {
|
||||
conn.send_command(
|
||||
format!("NEWNEWS {} {}", &mbox.nntp_path, datetime_str).as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
conn.read_response(&mut res, true, &["230 "]).await?;
|
||||
let message_ids = {
|
||||
let message_id_lck = uid_store.message_id_index.lock().unwrap();
|
||||
res.split_rn()
|
||||
.skip(1)
|
||||
.map(|s| s.trim())
|
||||
.filter(|msg_id| !message_id_lck.contains_key(*msg_id))
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<String>>()
|
||||
};
|
||||
if message_ids.is_empty() || !over_msgid_support {
|
||||
return Ok(());
|
||||
}
|
||||
let mut env_hash_set: BTreeSet<EnvelopeHash> = Default::default();
|
||||
for msg_id in message_ids {
|
||||
conn.send_command(format!("OVER {}", msg_id).as_bytes())
|
||||
.await?;
|
||||
conn.read_response(&mut res, true, &["224 "]).await?;
|
||||
let mut message_id_lck = uid_store.message_id_index.lock().unwrap();
|
||||
let mut hash_index_lck = uid_store.hash_index.lock().unwrap();
|
||||
let mut uid_index_lck = uid_store.uid_index.lock().unwrap();
|
||||
for l in res.split_rn().skip(1) {
|
||||
let (_, (num, env)) = protocol_parser::over_article(l)?;
|
||||
env_hash_set.insert(env.hash());
|
||||
message_id_lck.insert(env.message_id_display().to_string(), env.hash());
|
||||
hash_index_lck.insert(env.hash(), (num, mailbox_hash));
|
||||
uid_index_lck.insert((mailbox_hash, num), env.hash());
|
||||
latest_article = std::cmp::max(latest_article, env.timestamp);
|
||||
(uid_store.event_consumer)(
|
||||
uid_store.account_hash,
|
||||
crate::backends::BackendEvent::Refresh(RefreshEvent {
|
||||
mailbox_hash,
|
||||
account_hash: uid_store.account_hash,
|
||||
kind: RefreshEventKind::Create(Box::new(env)),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
{
|
||||
let f = &uid_store.mailboxes.lock().await[&mailbox_hash];
|
||||
*f.latest_article.lock().unwrap() = Some(latest_article);
|
||||
f.exists
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert_existing_set(env_hash_set.clone());
|
||||
f.unseen.lock().unwrap().insert_existing_set(env_hash_set);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
//conn.select_group(mailbox_hash, false, &mut res).await?;
|
||||
Ok(())
|
||||
}))
|
||||
fn refresh(&mut self, _mailbox_hash: MailboxHash) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn mailboxes(&self) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
|
||||
|
@ -349,11 +234,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());
|
||||
|
@ -367,7 +251,7 @@ impl MailBackend for NntpType {
|
|||
}
|
||||
|
||||
fn watch(&self) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Unimplemented.").set_kind(ErrorKind::NotImplemented))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn operation(&self, env_hash: EnvelopeHash) -> Result<Box<dyn BackendOp>> {
|
||||
|
@ -394,7 +278,7 @@ impl MailBackend for NntpType {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_flags: Option<Flag>,
|
||||
) -> ResultFuture<()> {
|
||||
Err(MeliError::new("NNTP doesn't support saving."))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn copy_messages(
|
||||
|
@ -404,7 +288,7 @@ impl MailBackend for NntpType {
|
|||
_destination_mailbox_hash: MailboxHash,
|
||||
_move_: bool,
|
||||
) -> ResultFuture<()> {
|
||||
Err(MeliError::new("NNTP doesn't support copying/moving."))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn set_flags(
|
||||
|
@ -413,15 +297,11 @@ impl MailBackend for NntpType {
|
|||
_mailbox_hash: MailboxHash,
|
||||
_flags: SmallVec<[(std::result::Result<Flag, String>, bool); 8]>,
|
||||
) -> ResultFuture<()> {
|
||||
Err(MeliError::new("NNTP doesn't support flags."))
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn delete_messages(
|
||||
&mut self,
|
||||
_env_hashes: EnvelopeHashBatch,
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<()> {
|
||||
Err(MeliError::new("NNTP doesn't support deletion."))
|
||||
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
|
@ -432,10 +312,6 @@ impl MailBackend for NntpType {
|
|||
self
|
||||
}
|
||||
|
||||
fn collection(&self) -> Collection {
|
||||
self.uid_store.collection.clone()
|
||||
}
|
||||
|
||||
fn create_mailbox(
|
||||
&mut self,
|
||||
_path: String,
|
||||
|
@ -481,39 +357,6 @@ impl MailBackend for NntpType {
|
|||
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>> {
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn submit(
|
||||
&self,
|
||||
bytes: Vec<u8>,
|
||||
mailbox_hash: Option<MailboxHash>,
|
||||
_flags: Option<Flag>,
|
||||
) -> ResultFuture<()> {
|
||||
let connection = self.connection.clone();
|
||||
Ok(Box::pin(async move {
|
||||
match timeout(Some(Duration::from_secs(60 * 16)), connection.lock()).await {
|
||||
Ok(mut conn) => {
|
||||
match &conn.stream {
|
||||
Ok(stream) => {
|
||||
if !stream.supports_submission {
|
||||
return Err(MeliError::new("Server prohibits posting."));
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(err.clone()),
|
||||
}
|
||||
let mut res = String::with_capacity(8 * 1024);
|
||||
if let Some(mailbox_hash) = mailbox_hash {
|
||||
conn.select_group(mailbox_hash, false, &mut res).await?;
|
||||
}
|
||||
conn.send_command(b"POST").await?;
|
||||
conn.read_response(&mut res, false, &["340 "]).await?;
|
||||
conn.send_multiline_data_block(&bytes).await?;
|
||||
conn.read_response(&mut res, false, &["240 "]).await?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl NntpType {
|
||||
|
@ -548,10 +391,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 {
|
||||
|
@ -571,7 +414,7 @@ impl NntpType {
|
|||
danger_accept_invalid_certs,
|
||||
extension_use: NntpExtensionUse {
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
deflate: get_conf_val!(s["use_deflate"], false)?,
|
||||
deflate: get_conf_val!(s["use_deflate"], true)?,
|
||||
},
|
||||
};
|
||||
let account_hash = {
|
||||
|
@ -590,7 +433,6 @@ impl NntpType {
|
|||
nntp_path: k.to_string(),
|
||||
high_watermark: Arc::new(Mutex::new(0)),
|
||||
low_watermark: Arc::new(Mutex::new(0)),
|
||||
latest_article: Arc::new(Mutex::new(None)),
|
||||
exists: Default::default(),
|
||||
unseen: Default::default(),
|
||||
},
|
||||
|
@ -603,6 +445,7 @@ impl NntpType {
|
|||
)));
|
||||
}
|
||||
let uid_store: Arc<UIDStore> = Arc::new(UIDStore {
|
||||
offline_cache: false, //get_conf_val!(s["X_header_caching"], false)?,
|
||||
mailboxes: Arc::new(FutureMutex::new(mailboxes)),
|
||||
..UIDStore::new(account_hash, account_name, event_consumer)
|
||||
});
|
||||
|
@ -610,8 +453,8 @@ impl NntpType {
|
|||
|
||||
Ok(Box::new(NntpType {
|
||||
server_conf,
|
||||
_is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)),
|
||||
_can_create_flags: Arc::new(Mutex::new(false)),
|
||||
is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)),
|
||||
can_create_flags: Arc::new(Mutex::new(false)),
|
||||
connection: Arc::new(FutureMutex::new(connection)),
|
||||
uid_store,
|
||||
}))
|
||||
|
@ -657,38 +500,7 @@ 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(|| {
|
||||
MeliError::new(format!(
|
||||
"Configuration error ({}): NNTP connection requires the field `{}` set",
|
||||
$s.name.as_str(),
|
||||
$var
|
||||
))
|
||||
})
|
||||
}};
|
||||
($s:ident[$var:literal], $default:expr) => {{
|
||||
keys.insert($var);
|
||||
$s.extra
|
||||
.remove($var)
|
||||
.map(|v| {
|
||||
<_>::from_str(&v).map_err(|e| {
|
||||
MeliError::new(format!(
|
||||
"Configuration error ({}) NNTP: Invalid value for field `{}`: {}\n{}",
|
||||
$s.name.as_str(),
|
||||
$var,
|
||||
v,
|
||||
e
|
||||
))
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| Ok($default))
|
||||
}};
|
||||
}
|
||||
get_conf_val!(s["require_auth"], false)?;
|
||||
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") {
|
||||
|
@ -699,10 +511,9 @@ impl NntpType {
|
|||
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(MeliError::new(format!(
|
||||
"Configuration error ({}): incompatible use_tls and use_starttls values: use_tls = false, use_starttls = true",
|
||||
|
@ -710,7 +521,7 @@ impl NntpType {
|
|||
)));
|
||||
}
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
get_conf_val!(s["use_deflate"], false)?;
|
||||
get_conf_val!(s["use_deflate"], true)?;
|
||||
#[cfg(not(feature = "deflate_compression"))]
|
||||
if s.extra.contains_key("use_deflate") {
|
||||
return Err(MeliError::new(format!(
|
||||
|
@ -719,18 +530,6 @@ impl NntpType {
|
|||
)));
|
||||
}
|
||||
get_conf_val!(s["danger_accept_invalid_certs"], false)?;
|
||||
let extra_keys = s
|
||||
.extra
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect::<HashSet<&str>>();
|
||||
let diff = extra_keys.difference(&keys).collect::<Vec<&&str>>();
|
||||
if !diff.is_empty() {
|
||||
return Err(MeliError::new(format!(
|
||||
"Configuration error ({}) NNTP: the following flags are set but are not recognized: {:?}.",
|
||||
s.name.as_str(), diff
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -740,7 +539,7 @@ impl NntpType {
|
|||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|c| c.clone())
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
}
|
||||
|
@ -782,9 +581,9 @@ impl FetchState {
|
|||
&uid_store.account_name, path, res
|
||||
)));
|
||||
}
|
||||
let total = usize::from_str(s[1]).unwrap_or(0);
|
||||
let _low = usize::from_str(s[2]).unwrap_or(0);
|
||||
let high = usize::from_str(s[3]).unwrap_or(0);
|
||||
let total = usize::from_str(&s[1]).unwrap_or(0);
|
||||
let _low = usize::from_str(&s[2]).unwrap_or(0);
|
||||
let high = usize::from_str(&s[3]).unwrap_or(0);
|
||||
*high_low_total = Some((high, _low, total));
|
||||
{
|
||||
let f = &uid_store.mailboxes.lock().await[&mailbox_hash];
|
||||
|
@ -796,11 +595,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"))
|
||||
|
@ -814,28 +612,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()
|
||||
|
@ -845,3 +634,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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ impl Default for NntpExtensionUse {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "deflate_compression")]
|
||||
deflate: false,
|
||||
deflate: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,6 @@ pub struct NntpStream {
|
|||
pub stream: AsyncWrapper<Connection>,
|
||||
pub extension_use: NntpExtensionUse,
|
||||
pub current_mailbox: MailboxSelection,
|
||||
pub supports_submission: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
|
@ -90,17 +89,17 @@ impl NntpStream {
|
|||
|
||||
let stream = {
|
||||
let addr = lookup_ipv4(path, server_conf.server_port)?;
|
||||
AsyncWrapper::new(Connection::Tcp(TcpStream::connect_timeout(
|
||||
&addr,
|
||||
std::time::Duration::new(16, 0),
|
||||
)?))?
|
||||
AsyncWrapper::new(Connection::Tcp(
|
||||
TcpStream::connect_timeout(&addr, std::time::Duration::new(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 = NntpStream {
|
||||
stream,
|
||||
extension_use: server_conf.extension_use,
|
||||
current_mailbox: MailboxSelection::None,
|
||||
supports_submission: false,
|
||||
};
|
||||
|
||||
if server_conf.use_tls {
|
||||
|
@ -108,13 +107,13 @@ 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?;
|
||||
|
@ -131,7 +130,7 @@ impl NntpStream {
|
|||
.any(|cap| cap.eq_ignore_ascii_case("VERSION 2"))
|
||||
{
|
||||
return Err(MeliError::new(format!(
|
||||
"Could not connect to {}: server is not NNTP VERSION 2 compliant",
|
||||
"Could not connect to {}: server is not NNTP compliant",
|
||||
&server_conf.server_hostname
|
||||
)));
|
||||
}
|
||||
|
@ -144,8 +143,14 @@ impl NntpStream {
|
|||
&server_conf.server_hostname
|
||||
)));
|
||||
}
|
||||
ret.stream.write_all(b"STARTTLS\r\n").await?;
|
||||
ret.stream.flush().await?;
|
||||
ret.stream
|
||||
.write_all(b"STARTTLS\r\n")
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
ret.stream
|
||||
.flush()
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
ret.read_response(&mut res, false, command_to_replycodes("STARTTLS"))
|
||||
.await?;
|
||||
if !res.starts_with("382 ") {
|
||||
|
@ -158,7 +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
|
||||
|
@ -174,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(
|
||||
|
@ -195,20 +201,9 @@ impl NntpStream {
|
|||
// .as_bytes(),
|
||||
//)
|
||||
//.await?;
|
||||
if let Err(err) = ret
|
||||
.stream
|
||||
.get_ref()
|
||||
.set_keepalive(Some(std::time::Duration::new(60 * 9, 0)))
|
||||
{
|
||||
crate::log(
|
||||
format!("Could not set TCP keepalive in NNTP connection: {}", err),
|
||||
crate::LoggingLevel::WARN,
|
||||
);
|
||||
}
|
||||
|
||||
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?;
|
||||
|
@ -218,8 +213,7 @@ impl NntpStream {
|
|||
&server_conf.server_hostname, res
|
||||
)));
|
||||
}
|
||||
let capabilities: HashSet<String> =
|
||||
res.lines().skip(1).map(|l| l.trim().to_string()).collect();
|
||||
let capabilities: HashSet<String> = res.lines().skip(1).map(|l| l.to_string()).collect();
|
||||
if !capabilities
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case("VERSION 2"))
|
||||
|
@ -229,12 +223,6 @@ impl NntpStream {
|
|||
&server_conf.server_hostname
|
||||
)));
|
||||
}
|
||||
if !capabilities
|
||||
.iter()
|
||||
.any(|cap| cap.eq_ignore_ascii_case("POST"))
|
||||
{
|
||||
ret.supports_submission = false;
|
||||
}
|
||||
|
||||
if server_conf.require_auth {
|
||||
if capabilities.iter().any(|c| c.starts_with("AUTHINFO USER")) {
|
||||
|
@ -280,7 +268,6 @@ impl NntpStream {
|
|||
stream,
|
||||
extension_use,
|
||||
current_mailbox,
|
||||
supports_submission,
|
||||
} = ret;
|
||||
let stream = stream.into_inner()?;
|
||||
return Ok((
|
||||
|
@ -289,7 +276,6 @@ impl NntpStream {
|
|||
stream: AsyncWrapper::new(stream.deflate())?,
|
||||
extension_use,
|
||||
current_mailbox,
|
||||
supports_submission,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
@ -356,8 +342,8 @@ impl NntpStream {
|
|||
last_line_idx += pos + "\r\n".len();
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(MeliError::from(err));
|
||||
Err(e) => {
|
||||
return Err(MeliError::from(e).set_err_kind(crate::error::ErrorKind::Network));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -366,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?;
|
||||
|
@ -382,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?;
|
||||
|
@ -416,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(())
|
||||
}
|
||||
|
@ -539,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,11 +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 crate::backends::imap::LazyCountSet;
|
||||
use crate::backends::{
|
||||
BackendMailbox, LazyCountSet, Mailbox, MailboxHash, MailboxPermissions, SpecialUsageMailbox,
|
||||
BackendMailbox, Mailbox, MailboxHash, MailboxPermissions, SpecialUsageMailbox,
|
||||
};
|
||||
use crate::error::*;
|
||||
use crate::UnixTimestamp;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
|
@ -35,8 +35,6 @@ pub struct NntpMailbox {
|
|||
|
||||
pub exists: Arc<Mutex<LazyCountSet>>,
|
||||
pub unseen: Arc<Mutex<LazyCountSet>>,
|
||||
|
||||
pub latest_article: Arc<Mutex<Option<UnixTimestamp>>>,
|
||||
}
|
||||
|
||||
impl NntpMailbox {
|
||||
|
|
|
@ -144,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,11 +19,11 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::backends::*;
|
||||
use crate::conf::AccountSettings;
|
||||
use crate::email::{Envelope, EnvelopeHash, Flag};
|
||||
use crate::error::{MeliError, Result};
|
||||
use crate::error::{MeliError, Result, ResultIntoMeliError};
|
||||
use crate::shellexpand::ShellExpandTrait;
|
||||
use crate::{backends::*, Collection};
|
||||
use smallvec::SmallVec;
|
||||
use std::collections::{
|
||||
hash_map::{DefaultHasher, HashMap},
|
||||
|
@ -69,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>>,
|
||||
|
@ -102,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.lib.clone(), &self, &query_str)?;
|
||||
let iter = query.search()?;
|
||||
let mailbox_index_lck = mailbox_index.write().unwrap();
|
||||
let mailboxes_lck = mailboxes.read().unwrap();
|
||||
|
@ -131,32 +130,38 @@ impl DbConnection {
|
|||
}
|
||||
} else {
|
||||
let message_id = message.msg_id_cstr().to_string_lossy().to_string();
|
||||
let env = message.into_envelope(&index, &tag_index);
|
||||
for (&mailbox_hash, m) in mailboxes_lck.iter() {
|
||||
let query_str = format!("{} id:{}", m.query_str.as_str(), &message_id);
|
||||
let query: Query = Query::new(self, &query_str)?;
|
||||
if query.count().unwrap_or(0) > 0 {
|
||||
let mut total_lck = m.total.lock().unwrap();
|
||||
let mut unseen_lck = m.unseen.lock().unwrap();
|
||||
*total_lck += 1;
|
||||
if !env.is_seen() {
|
||||
*unseen_lck += 1;
|
||||
match message.into_envelope(index.clone(), tag_index.clone()) {
|
||||
Ok(env) => {
|
||||
for (&mailbox_hash, m) in mailboxes_lck.iter() {
|
||||
let query_str = format!("{} id:{}", m.query_str.as_str(), &message_id);
|
||||
let query: Query = Query::new(self.lib.clone(), self, &query_str)?;
|
||||
if query.count().unwrap_or(0) > 0 {
|
||||
let mut total_lck = m.total.lock().unwrap();
|
||||
let mut unseen_lck = m.unseen.lock().unwrap();
|
||||
*total_lck += 1;
|
||||
if !env.is_seen() {
|
||||
*unseen_lck += 1;
|
||||
}
|
||||
(event_consumer)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: Create(Box::new(env.clone())),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
(event_consumer)(
|
||||
account_hash,
|
||||
BackendEvent::Refresh(RefreshEvent {
|
||||
account_hash,
|
||||
mailbox_hash,
|
||||
kind: Create(Box::new(env.clone())),
|
||||
}),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("could not parse message {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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];
|
||||
|
@ -216,16 +221,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<String>,
|
||||
account_hash: AccountHash,
|
||||
account_name: Arc<String>,
|
||||
event_consumer: BackendEventConsumer,
|
||||
save_messages_to: Option<PathBuf>,
|
||||
}
|
||||
|
@ -309,57 +312,15 @@ impl NotmuchDb {
|
|||
_is_subscribed: Box<dyn Fn(&str) -> bool>,
|
||||
event_consumer: BackendEventConsumer,
|
||||
) -> Result<Box<dyn MailBackend>> {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let mut dlpath = "libnotmuch.so.5";
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut dlpath = "libnotmuch.5.dylib";
|
||||
let mut custom_dlpath = false;
|
||||
if let Some(lib_path) = s.extra.get("library_file_path") {
|
||||
dlpath = lib_path.as_str();
|
||||
custom_dlpath = true;
|
||||
}
|
||||
let lib = Arc::new(unsafe {
|
||||
match libloading::Library::new(dlpath) {
|
||||
Ok(l) => l,
|
||||
Err(err) => {
|
||||
if custom_dlpath {
|
||||
return Err(MeliError::new(format!("Notmuch `library_file_path` setting value `{}` for account {} does not exist or is a directory or not a valid library file.",dlpath, s.name()))
|
||||
.set_kind(ErrorKind::Configuration)
|
||||
.set_source(Some(Arc::new(err))));
|
||||
} else {
|
||||
return Err(MeliError::new("Could not load libnotmuch!")
|
||||
.set_details(super::NOTMUCH_ERROR_DETAILS)
|
||||
.set_source(Some(Arc::new(err))));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut path = Path::new(s.root_mailbox.as_str()).expand();
|
||||
let lib = Arc::new(libloading::Library::new("libnotmuch.so.5")?);
|
||||
let path = Path::new(s.root_mailbox.as_str()).expand();
|
||||
if !path.exists() {
|
||||
return Err(MeliError::new(format!(
|
||||
"Notmuch `root_mailbox` {} for account {} does not exist.",
|
||||
"\"root_mailbox\" {} for account {} is not a valid path.",
|
||||
s.root_mailbox.as_str(),
|
||||
s.name()
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
)));
|
||||
}
|
||||
if !path.is_dir() {
|
||||
return Err(MeliError::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(MeliError::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::default();
|
||||
for (k, f) in s.mailboxes.iter() {
|
||||
|
@ -385,81 +346,42 @@ impl NotmuchDb {
|
|||
);
|
||||
} else {
|
||||
return Err(MeliError::new(format!(
|
||||
"notmuch mailbox configuration entry `{}` for account {} should have a `query` value set.",
|
||||
k,
|
||||
s.name(),
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
"notmuch mailbox configuration entry \"{}\" should have a \"query\" value set.",
|
||||
k
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
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: Arc::new(s.name().to_string()),
|
||||
account_hash,
|
||||
account_name: Arc::new(s.name().to_string()),
|
||||
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(MeliError::new(format!(
|
||||
"Notmuch `root_mailbox` {} for account {} does not exist.",
|
||||
"\"root_mailbox\" {} for account {} is not a valid path.",
|
||||
s.root_mailbox.as_str(),
|
||||
s.name()
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
)));
|
||||
}
|
||||
if !path.is_dir() {
|
||||
return Err(MeliError::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(MeliError::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() {
|
||||
for (k, f) in s.mailboxes.iter() {
|
||||
if f.extra.get("query").is_none() {
|
||||
return Err(MeliError::new(format!(
|
||||
"Notmuch `library_file_path` setting value `{}` for account {} does not exist or is a directory.",
|
||||
&lib_path,
|
||||
s.name()
|
||||
)).set_kind(ErrorKind::Configuration));
|
||||
}
|
||||
}
|
||||
for (k, f) in s.mailboxes.iter_mut() {
|
||||
if f.extra.remove("query").is_none() {
|
||||
return Err(MeliError::new(format!(
|
||||
"notmuch mailbox configuration entry `{}` for account {} should have a `query` value set.",
|
||||
k,
|
||||
account_name,
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
"notmuch mailbox configuration entry \"{}\" should have a \"query\" value set.",
|
||||
k
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
@ -552,15 +474,21 @@ impl MailBackend for NotmuchDb {
|
|||
} else {
|
||||
continue;
|
||||
};
|
||||
let env = message.into_envelope(&self.index, &self.tag_index);
|
||||
mailbox_index_lck
|
||||
.entry(env.hash())
|
||||
.or_default()
|
||||
.push(self.mailbox_hash);
|
||||
if !env.is_seen() {
|
||||
unseen_count += 1;
|
||||
match message.into_envelope(self.index.clone(), self.tag_index.clone()) {
|
||||
Ok(env) => {
|
||||
mailbox_index_lck
|
||||
.entry(env.hash())
|
||||
.or_default()
|
||||
.push(self.mailbox_hash);
|
||||
if !env.is_seen() {
|
||||
unseen_count += 1;
|
||||
}
|
||||
ret.push(env);
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("could not parse message {:?}", err);
|
||||
}
|
||||
}
|
||||
ret.push(env);
|
||||
} else {
|
||||
done = true;
|
||||
break;
|
||||
|
@ -587,13 +515,13 @@ 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>;
|
||||
{
|
||||
let mailboxes_lck = mailboxes.read().unwrap();
|
||||
let mailbox = mailboxes_lck.get(&mailbox_hash).unwrap();
|
||||
let query: Query = Query::new(&database, mailbox.query_str.as_str())?;
|
||||
let query: Query = Query::new(self.lib.clone(), &database, mailbox.query_str.as_str())?;
|
||||
{
|
||||
let mut total_lck = mailbox.total.lock().unwrap();
|
||||
let mut unseen_lck = mailbox.unseen.lock().unwrap();
|
||||
|
@ -628,7 +556,11 @@ impl MailBackend for NotmuchDb {
|
|||
}
|
||||
|
||||
fn refresh(&mut self, _mailbox_hash: MailboxHash) -> ResultFuture<()> {
|
||||
let account_hash = self.account_hash;
|
||||
let account_hash = {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
hasher.write(self.account_name.as_bytes());
|
||||
hasher.finish()
|
||||
};
|
||||
let mut database = NotmuchDb::new_connection(
|
||||
self.path.as_path(),
|
||||
self.revision_uuid.clone(),
|
||||
|
@ -638,7 +570,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();
|
||||
|
@ -662,14 +594,18 @@ impl MailBackend for NotmuchDb {
|
|||
extern crate notify;
|
||||
use notify::{watcher, RecursiveMode, Watcher};
|
||||
|
||||
let account_hash = self.account_hash;
|
||||
let collection = self.collection.clone();
|
||||
let account_hash = {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
hasher.write(self.account_name.as_bytes());
|
||||
hasher.finish()
|
||||
};
|
||||
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();
|
||||
|
@ -693,8 +629,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,
|
||||
)?;
|
||||
|
@ -728,6 +664,7 @@ impl MailBackend for NotmuchDb {
|
|||
hash,
|
||||
index: self.index.clone(),
|
||||
bytes: None,
|
||||
tag_index: self.tag_index.clone(),
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -769,7 +706,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 {
|
||||
|
@ -841,7 +778,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -857,11 +794,7 @@ impl MailBackend for NotmuchDb {
|
|||
for (f, v) in flags.iter() {
|
||||
if let (Err(tag), true) = (f, v) {
|
||||
let hash = tag_hash!(tag);
|
||||
collection
|
||||
.tag_index
|
||||
.write()
|
||||
.unwrap()
|
||||
.insert(hash, tag.to_string());
|
||||
tag_index.write().unwrap().insert(hash, tag.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -869,14 +802,6 @@ impl MailBackend for NotmuchDb {
|
|||
}))
|
||||
}
|
||||
|
||||
fn delete_messages(
|
||||
&mut self,
|
||||
_env_hashes: EnvelopeHashBatch,
|
||||
_mailbox_hash: MailboxHash,
|
||||
) -> ResultFuture<()> {
|
||||
Err(MeliError::new("Unimplemented."))
|
||||
}
|
||||
|
||||
fn search(
|
||||
&self,
|
||||
melib_query: crate::search::Query,
|
||||
|
@ -888,6 +813,7 @@ impl MailBackend for NotmuchDb {
|
|||
self.lib.clone(),
|
||||
false,
|
||||
)?;
|
||||
let lib = self.lib.clone();
|
||||
let mailboxes = self.mailboxes.clone();
|
||||
Ok(Box::pin(async move {
|
||||
let mut ret = SmallVec::new();
|
||||
|
@ -904,7 +830,7 @@ impl MailBackend for NotmuchDb {
|
|||
String::new()
|
||||
};
|
||||
melib_query.query_to_string(&mut query_s);
|
||||
let query: Query = Query::new(&database, &query_s)?;
|
||||
let query: Query = Query::new(lib.clone(), &database, &query_s)?;
|
||||
let iter = query.search()?;
|
||||
for message in iter {
|
||||
ret.push(message.env_hash());
|
||||
|
@ -914,8 +840,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 {
|
||||
|
@ -931,9 +857,9 @@ impl MailBackend for NotmuchDb {
|
|||
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>,
|
||||
}
|
||||
|
||||
|
@ -958,15 +884,17 @@ impl BackendOp for NotmuchOp {
|
|||
}
|
||||
|
||||
pub struct Query<'s> {
|
||||
#[allow(dead_code)]
|
||||
lib: Arc<libloading::Library>,
|
||||
ptr: *mut notmuch_query_t,
|
||||
query_str: &'s str,
|
||||
}
|
||||
|
||||
impl<'s> Query<'s> {
|
||||
fn new(database: &DbConnection, query_str: &'s str) -> Result<Self> {
|
||||
let lib: Arc<libloading::Library> = database.lib.clone();
|
||||
fn new(
|
||||
lib: Arc<libloading::Library>,
|
||||
database: &DbConnection,
|
||||
query_str: &'s str,
|
||||
) -> Result<Self> {
|
||||
let query_cstr = std::ffi::CString::new(query_str)?;
|
||||
let query: *mut notmuch_query_t = unsafe {
|
||||
call!(lib, notmuch_query_create)(*database.inner.read().unwrap(), query_cstr.as_ptr())
|
||||
|
@ -1059,7 +987,7 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
|
|||
ret.push(c);
|
||||
}
|
||||
}
|
||||
ret.push('"');
|
||||
ret.push_str("\"");
|
||||
}
|
||||
To(s) | Cc(s) | Bcc(s) => {
|
||||
ret.push_str("to:\"");
|
||||
|
@ -1070,7 +998,7 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
|
|||
ret.push(c);
|
||||
}
|
||||
}
|
||||
ret.push('"');
|
||||
ret.push_str("\"");
|
||||
}
|
||||
InReplyTo(_s) | References(_s) | AllAddresses(_s) => {}
|
||||
/* * * * */
|
||||
|
@ -1083,7 +1011,7 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
|
|||
ret.push(c);
|
||||
}
|
||||
}
|
||||
ret.push('"');
|
||||
ret.push_str("\"");
|
||||
}
|
||||
Subject(s) => {
|
||||
ret.push_str("subject:\"");
|
||||
|
@ -1094,10 +1022,10 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
|
|||
ret.push(c);
|
||||
}
|
||||
}
|
||||
ret.push('"');
|
||||
ret.push_str("\"");
|
||||
}
|
||||
AllText(s) => {
|
||||
ret.push('"');
|
||||
ret.push_str("\"");
|
||||
for c in s.chars() {
|
||||
if c == '"' {
|
||||
ret.push_str("\\\"");
|
||||
|
@ -1105,7 +1033,7 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
|
|||
ret.push(c);
|
||||
}
|
||||
}
|
||||
ret.push('"');
|
||||
ret.push_str("\"");
|
||||
}
|
||||
/* * * * */
|
||||
Flags(v) => {
|
||||
|
@ -1128,18 +1056,18 @@ impl MelibQueryToNotmuchQuery for crate::search::Query {
|
|||
ret.push_str("tag:attachment");
|
||||
}
|
||||
And(q1, q2) => {
|
||||
ret.push('(');
|
||||
ret.push_str("(");
|
||||
q1.query_to_string(ret);
|
||||
ret.push_str(") AND (");
|
||||
q2.query_to_string(ret);
|
||||
ret.push(')');
|
||||
ret.push_str(")");
|
||||
}
|
||||
Or(q1, q2) => {
|
||||
ret.push('(');
|
||||
ret.push_str("(");
|
||||
q1.query_to_string(ret);
|
||||
ret.push_str(") OR (");
|
||||
q2.query_to_string(ret);
|
||||
ret.push(')');
|
||||
ret.push_str(")");
|
||||
}
|
||||
Not(q) => {
|
||||
ret.push_str("(NOT (");
|
||||
|
|
|
@ -16,11 +16,9 @@ pub const _notmuch_status_NOTMUCH_STATUS_OUT_OF_MEMORY: _notmuch_status = 1;
|
|||
pub const _notmuch_status_NOTMUCH_STATUS_READ_ONLY_DATABASE: _notmuch_status = 2;
|
||||
/// A Xapian exception occurred.
|
||||
///
|
||||
/// ```text
|
||||
/// @todo We don't really want to expose this lame XAPIAN_EXCEPTION
|
||||
/// value. Instead we should map to things like DATABASE_LOCKED or
|
||||
/// whatever.
|
||||
/// ```
|
||||
pub const _notmuch_status_NOTMUCH_STATUS_XAPIAN_EXCEPTION: _notmuch_status = 3;
|
||||
/// An error occurred trying to read or write to a file (this could
|
||||
/// be file not found, permission denied, etc.)
|
||||
|
@ -468,7 +466,6 @@ pub type notmuch_database_get_directory = unsafe extern "C" fn(
|
|||
///
|
||||
/// Return value:
|
||||
///
|
||||
/// ```text
|
||||
/// NOTMUCH_STATUS_SUCCESS: Message successfully added to database.
|
||||
///
|
||||
/// NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred,
|
||||
|
@ -493,7 +490,6 @@ pub type notmuch_database_get_directory = unsafe extern "C" fn(
|
|||
/// database to use this function.
|
||||
///
|
||||
/// @since libnotmuch 5.1 (notmuch 0.26)
|
||||
/// ```
|
||||
pub type notmuch_database_index_file = unsafe extern "C" fn(
|
||||
database: *mut notmuch_database_t,
|
||||
filename: *const ::std::os::raw::c_char,
|
||||
|
@ -505,10 +501,8 @@ extern "C" {
|
|||
/// Deprecated alias for notmuch_database_index_file called with
|
||||
/// NULL indexopts.
|
||||
///
|
||||
/// ```text
|
||||
/// @deprecated Deprecated as of libnotmuch 5.1 (notmuch 0.26). Please
|
||||
/// use notmuch_database_index_file instead.
|
||||
/// ```
|
||||
///
|
||||
pub fn notmuch_database_add_message(
|
||||
database: *mut notmuch_database_t,
|
||||
|
@ -622,7 +616,7 @@ pub type notmuch_database_get_all_tags =
|
|||
/// completely in the future, but it's likely to be a specialized
|
||||
/// version of the general Xapian query syntax:
|
||||
///
|
||||
/// <https://xapian.org/docs/queryparser.html>
|
||||
/// https://xapian.org/docs/queryparser.html
|
||||
///
|
||||
/// As a special case, passing either a length-zero string, (that is ""),
|
||||
/// or a string consisting of a single asterisk (that is "*"), will
|
||||
|
@ -709,9 +703,7 @@ pub type notmuch_query_get_sort =
|
|||
/// This exclusion will be ignored if this tag appears explicitly in
|
||||
/// the query.
|
||||
///
|
||||
/// ```text
|
||||
/// @returns
|
||||
/// ```
|
||||
///
|
||||
/// NOTMUCH_STATUS_SUCCESS: excluded was added successfully.
|
||||
///
|
||||
|
@ -765,9 +757,7 @@ pub type notmuch_query_add_tag_exclude = unsafe extern "C" fn(
|
|||
/// notmuch_threads_destroy function, but there's no good reason
|
||||
/// to call it if the query is about to be destroyed).
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 5.0 (notmuch 0.25)
|
||||
/// ```
|
||||
pub type notmuch_query_search_threads = unsafe extern "C" fn(
|
||||
query: *mut notmuch_query_t,
|
||||
out: *mut *mut notmuch_threads_t,
|
||||
|
@ -775,9 +765,7 @@ pub type notmuch_query_search_threads = unsafe extern "C" fn(
|
|||
|
||||
/// Deprecated alias for notmuch_query_search_threads.
|
||||
///
|
||||
/// ```text
|
||||
/// @deprecated Deprecated as of libnotmuch 5 (notmuch 0.25). Please
|
||||
/// ```
|
||||
/// use notmuch_query_search_threads instead.
|
||||
///
|
||||
pub type notmuch_query_search_threads_st = unsafe extern "C" fn(
|
||||
|
@ -825,9 +813,7 @@ pub type notmuch_query_search_threads_st = unsafe extern "C" fn(
|
|||
///
|
||||
/// If a Xapian exception occurs this function will return NULL.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 5 (notmuch 0.25)
|
||||
/// ```
|
||||
pub type notmuch_query_search_messages = unsafe extern "C" fn(
|
||||
query: *mut notmuch_query_t,
|
||||
out: *mut *mut notmuch_messages_t,
|
||||
|
@ -835,9 +821,7 @@ pub type notmuch_query_search_messages = unsafe extern "C" fn(
|
|||
|
||||
/// Deprecated alias for notmuch_query_search_messages
|
||||
///
|
||||
/// ```text
|
||||
/// @deprecated Deprecated as of libnotmuch 5 (notmuch 0.25). Please use
|
||||
/// ```
|
||||
/// notmuch_query_search_messages instead.
|
||||
///
|
||||
pub type notmuch_query_search_messages_st = unsafe extern "C" fn(
|
||||
|
@ -903,7 +887,6 @@ pub type notmuch_threads_destroy = unsafe extern "C" fn(threads: *mut notmuch_th
|
|||
/// This function performs a search and returns the number of matching
|
||||
/// messages.
|
||||
///
|
||||
/// ```text
|
||||
/// @returns
|
||||
///
|
||||
/// NOTMUCH_STATUS_SUCCESS: query completed successfully.
|
||||
|
@ -912,7 +895,6 @@ pub type notmuch_threads_destroy = unsafe extern "C" fn(threads: *mut notmuch_th
|
|||
/// value of *count is not defined.
|
||||
///
|
||||
/// @since libnotmuch 5 (notmuch 0.25)
|
||||
/// ```
|
||||
pub type notmuch_query_count_messages = unsafe extern "C" fn(
|
||||
query: *mut notmuch_query_t,
|
||||
count: *mut ::std::os::raw::c_uint,
|
||||
|
@ -920,10 +902,9 @@ pub type notmuch_query_count_messages = unsafe extern "C" fn(
|
|||
|
||||
/// Deprecated alias for notmuch_query_count_messages
|
||||
///
|
||||
/// ```text
|
||||
///
|
||||
/// @deprecated Deprecated since libnotmuch 5.0 (notmuch 0.25). Please
|
||||
/// use notmuch_query_count_messages instead.
|
||||
/// ```
|
||||
pub type notmuch_query_count_messages_st = unsafe extern "C" fn(
|
||||
query: *mut notmuch_query_t,
|
||||
count: *mut ::std::os::raw::c_uint,
|
||||
|
@ -938,7 +919,6 @@ pub type notmuch_query_count_messages_st = unsafe extern "C" fn(
|
|||
/// Note that this is a significantly heavier operation than
|
||||
/// notmuch_query_count_messages{_st}().
|
||||
///
|
||||
/// ```text
|
||||
/// @returns
|
||||
///
|
||||
/// NOTMUCH_STATUS_OUT_OF_MEMORY: Memory allocation failed. The value
|
||||
|
@ -950,7 +930,6 @@ pub type notmuch_query_count_messages_st = unsafe extern "C" fn(
|
|||
/// value of *count is not defined.
|
||||
///
|
||||
/// @since libnotmuch 5 (notmuch 0.25)
|
||||
/// ```
|
||||
pub type notmuch_query_count_threads = unsafe extern "C" fn(
|
||||
query: *mut notmuch_query_t,
|
||||
count: *mut ::std::os::raw::c_uint,
|
||||
|
@ -958,10 +937,8 @@ pub type notmuch_query_count_threads = unsafe extern "C" fn(
|
|||
|
||||
/// Deprecated alias for notmuch_query_count_threads
|
||||
///
|
||||
/// ```text
|
||||
/// @deprecated Deprecated as of libnotmuch 5.0 (notmuch 0.25). Please
|
||||
/// use notmuch_query_count_threads_st instead.
|
||||
/// ```
|
||||
pub type notmuch_query_count_threads_st = unsafe extern "C" fn(
|
||||
query: *mut notmuch_query_t,
|
||||
count: *mut ::std::os::raw::c_uint,
|
||||
|
@ -987,10 +964,8 @@ pub type notmuch_thread_get_total_messages =
|
|||
///
|
||||
/// This sums notmuch_message_count_files over all messages in the
|
||||
/// thread
|
||||
/// ```text
|
||||
/// @returns Non-negative integer
|
||||
/// @since libnotmuch 5.0 (notmuch 0.25)
|
||||
/// ```
|
||||
pub type notmuch_thread_get_total_files =
|
||||
unsafe extern "C" fn(thread: *mut notmuch_thread_t) -> ::std::os::raw::c_int;
|
||||
|
||||
|
@ -1161,9 +1136,7 @@ pub type notmuch_messages_collect_tags =
|
|||
|
||||
/// Get the database associated with this message.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 5.2 (notmuch 0.27)
|
||||
/// ```
|
||||
pub type notmuch_message_get_database =
|
||||
unsafe extern "C" fn(message: *const notmuch_message_t) -> *mut notmuch_database_t;
|
||||
|
||||
|
@ -1215,10 +1188,8 @@ pub type notmuch_message_get_replies =
|
|||
unsafe extern "C" fn(message: *mut notmuch_message_t) -> *mut notmuch_messages_t;
|
||||
|
||||
/// Get the total number of files associated with a message.
|
||||
/// ```text
|
||||
/// @returns Non-negative integer
|
||||
/// @since libnotmuch 5.0 (notmuch 0.25)
|
||||
/// ```
|
||||
pub type notmuch_message_count_files =
|
||||
unsafe extern "C" fn(message: *mut notmuch_message_t) -> ::std::os::raw::c_int;
|
||||
|
||||
|
@ -1479,7 +1450,6 @@ pub type notmuch_message_tags_to_maildir_flags =
|
|||
/// change tag values. For example, explicitly setting a message to
|
||||
/// have a given set of tags might look like this:
|
||||
///
|
||||
/// ```c
|
||||
/// notmuch_message_freeze (message);
|
||||
///
|
||||
/// notmuch_message_remove_all_tags (message);
|
||||
|
@ -1488,7 +1458,6 @@ pub type notmuch_message_tags_to_maildir_flags =
|
|||
/// notmuch_message_add_tag (message, tags[i]);
|
||||
///
|
||||
/// notmuch_message_thaw (message);
|
||||
/// ```
|
||||
///
|
||||
/// With freeze/thaw used like this, the message in the database is
|
||||
/// guaranteed to have either the full set of original tag values, or
|
||||
|
@ -1539,9 +1508,7 @@ pub type notmuch_message_thaw =
|
|||
/// the messages get reclaimed when the containing query is destroyed.)
|
||||
pub type notmuch_message_destroy = unsafe extern "C" fn(message: *mut notmuch_message_t);
|
||||
|
||||
/// ```text
|
||||
/// @name Message Properties
|
||||
/// ```
|
||||
///
|
||||
/// This interface provides the ability to attach arbitrary (key,value)
|
||||
/// string pairs to a message, to remove such pairs, and to iterate
|
||||
|
@ -1558,12 +1525,10 @@ pub type notmuch_message_destroy = unsafe extern "C" fn(message: *mut notmuch_me
|
|||
/// no such key. In the case of multiple values for the given key, the
|
||||
/// first one is retrieved.
|
||||
///
|
||||
/// ```text
|
||||
/// @returns
|
||||
/// - NOTMUCH_STATUS_NULL_POINTER: *value* may not be NULL.
|
||||
/// - NOTMUCH_STATUS_SUCCESS: No error occurred.
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_message_get_property = unsafe extern "C" fn(
|
||||
message: *mut notmuch_message_t,
|
||||
key: *const ::std::os::raw::c_char,
|
||||
|
@ -1572,13 +1537,11 @@ pub type notmuch_message_get_property = unsafe extern "C" fn(
|
|||
|
||||
/// Add a (key,value) pair to a message
|
||||
///
|
||||
/// ```text
|
||||
/// @returns
|
||||
/// - NOTMUCH_STATUS_ILLEGAL_ARGUMENT: *key* may not contain an '=' character.
|
||||
/// - NOTMUCH_STATUS_NULL_POINTER: Neither *key* nor *value* may be NULL.
|
||||
/// - NOTMUCH_STATUS_SUCCESS: No error occurred.
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_message_add_property = unsafe extern "C" fn(
|
||||
message: *mut notmuch_message_t,
|
||||
key: *const ::std::os::raw::c_char,
|
||||
|
@ -1589,13 +1552,11 @@ pub type notmuch_message_add_property = unsafe extern "C" fn(
|
|||
///
|
||||
/// It is not an error to remove a non-existant (key,value) pair
|
||||
///
|
||||
/// ```text
|
||||
/// @returns
|
||||
/// - NOTMUCH_STATUS_ILLEGAL_ARGUMENT: *key* may not contain an '=' character.
|
||||
/// - NOTMUCH_STATUS_NULL_POINTER: Neither *key* nor *value* may be NULL.
|
||||
/// - NOTMUCH_STATUS_SUCCESS: No error occurred.
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_message_remove_property = unsafe extern "C" fn(
|
||||
message: *mut notmuch_message_t,
|
||||
key: *const ::std::os::raw::c_char,
|
||||
|
@ -1604,7 +1565,6 @@ pub type notmuch_message_remove_property = unsafe extern "C" fn(
|
|||
|
||||
/// Remove all (key,value) pairs from the given message.
|
||||
///
|
||||
/// ```text
|
||||
/// @param[in,out] message message to operate on.
|
||||
/// @param[in] key key to delete properties for. If NULL, delete
|
||||
/// properties for all keys
|
||||
|
@ -1614,7 +1574,6 @@ pub type notmuch_message_remove_property = unsafe extern "C" fn(
|
|||
/// - NOTMUCH_STATUS_SUCCESS: No error occurred.
|
||||
///
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_message_remove_all_properties = unsafe extern "C" fn(
|
||||
message: *mut notmuch_message_t,
|
||||
key: *const ::std::os::raw::c_char,
|
||||
|
@ -1622,7 +1581,6 @@ pub type notmuch_message_remove_all_properties = unsafe extern "C" fn(
|
|||
|
||||
/// Remove all (prefix*,value) pairs from the given message
|
||||
///
|
||||
/// ```text
|
||||
/// @param[in,out] message message to operate on.
|
||||
/// @param[in] prefix delete properties with keys that start with prefix.
|
||||
/// If NULL, delete all properties
|
||||
|
@ -1632,7 +1590,6 @@ pub type notmuch_message_remove_all_properties = unsafe extern "C" fn(
|
|||
/// - NOTMUCH_STATUS_SUCCESS: No error occurred.
|
||||
///
|
||||
/// @since libnotmuch 5.1 (notmuch 0.26)
|
||||
/// ```
|
||||
pub type notmuch_message_remove_all_properties_with_prefix =
|
||||
unsafe extern "C" fn(
|
||||
message: *mut notmuch_message_t,
|
||||
|
@ -1655,12 +1612,10 @@ extern "C" {
|
|||
/// as such, will only be valid for as long as the message is valid,
|
||||
/// (which is until the query from which it derived is destroyed).
|
||||
///
|
||||
/// ```text
|
||||
/// @param[in] message The message to examine
|
||||
/// @param[in] key key or key prefix
|
||||
/// @param[in] exact if TRUE, require exact match with key. Otherwise
|
||||
/// treat as prefix.
|
||||
/// ```
|
||||
///
|
||||
/// Typical usage might be:
|
||||
///
|
||||
|
@ -1680,9 +1635,7 @@ extern "C" {
|
|||
/// provide a notmuch_message_properities_destroy function, but there's
|
||||
/// no good reason to call it if the message is about to be destroyed).
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub fn notmuch_message_get_properties(
|
||||
message: *mut notmuch_message_t,
|
||||
key: *const ::std::os::raw::c_char,
|
||||
|
@ -1691,7 +1644,6 @@ extern "C" {
|
|||
}
|
||||
/// Return the number of properties named "key" belonging to the specific message.
|
||||
///
|
||||
/// ```text
|
||||
/// @param[in] message The message to examine
|
||||
/// @param[in] key key to count
|
||||
/// @param[out] count The number of matching properties associated with this message.
|
||||
|
@ -1701,7 +1653,6 @@ extern "C" {
|
|||
/// NOTMUCH_STATUS_SUCCESS: successful count, possibly some other error.
|
||||
///
|
||||
/// @since libnotmuch 5.2 (notmuch 0.27)
|
||||
/// ```
|
||||
pub type notmuch_message_count_properties = unsafe extern "C" fn(
|
||||
message: *mut notmuch_message_t,
|
||||
key: *const ::std::os::raw::c_char,
|
||||
|
@ -1721,9 +1672,7 @@ pub type notmuch_message_count_properties = unsafe extern "C" fn(
|
|||
/// code showing how to iterate over a notmuch_message_properties_t
|
||||
/// object.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_message_properties_valid =
|
||||
unsafe extern "C" fn(properties: *mut notmuch_message_properties_t) -> notmuch_bool_t;
|
||||
|
||||
|
@ -1736,9 +1685,7 @@ pub type notmuch_message_properties_valid =
|
|||
/// See the documentation of notmuch_message_get_properties for example
|
||||
/// code showing how to iterate over a notmuch_message_properties_t object.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_message_properties_move_to_next =
|
||||
unsafe extern "C" fn(properties: *mut notmuch_message_properties_t);
|
||||
|
||||
|
@ -1746,9 +1693,7 @@ pub type notmuch_message_properties_move_to_next =
|
|||
///
|
||||
/// this could be useful if iterating for a prefix
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_message_properties_key = unsafe extern "C" fn(
|
||||
properties: *mut notmuch_message_properties_t,
|
||||
) -> *const ::std::os::raw::c_char;
|
||||
|
@ -1757,9 +1702,7 @@ pub type notmuch_message_properties_key = unsafe extern "C" fn(
|
|||
///
|
||||
/// This could be useful if iterating for a prefix.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_message_properties_value = unsafe extern "C" fn(
|
||||
properties: *mut notmuch_message_properties_t,
|
||||
) -> *const ::std::os::raw::c_char;
|
||||
|
@ -1770,9 +1713,7 @@ pub type notmuch_message_properties_value = unsafe extern "C" fn(
|
|||
/// the notmuch_message_properties_t object will be reclaimed when the
|
||||
/// containing message object is destroyed.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_message_properties_destroy =
|
||||
unsafe extern "C" fn(properties: *mut notmuch_message_properties_t);
|
||||
|
||||
|
@ -1880,9 +1821,7 @@ pub type notmuch_directory_get_child_directories =
|
|||
/// notmuch_directory_t object. Assumes any child directories and files
|
||||
/// have been deleted by the caller.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.3 (notmuch 0.21)
|
||||
/// ```
|
||||
pub type notmuch_directory_delete =
|
||||
unsafe extern "C" fn(directory: *mut notmuch_directory_t) -> notmuch_status_t;
|
||||
|
||||
|
@ -1933,9 +1872,7 @@ pub type notmuch_filenames_destroy = unsafe extern "C" fn(filenames: *mut notmuc
|
|||
|
||||
/// set config 'key' to 'value'
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_database_set_config = unsafe extern "C" fn(
|
||||
db: *mut notmuch_database_t,
|
||||
key: *const ::std::os::raw::c_char,
|
||||
|
@ -1950,9 +1887,7 @@ pub type notmuch_database_set_config = unsafe extern "C" fn(
|
|||
/// return value is allocated by malloc and should be freed by the
|
||||
/// caller.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_database_get_config = unsafe extern "C" fn(
|
||||
db: *mut notmuch_database_t,
|
||||
key: *const ::std::os::raw::c_char,
|
||||
|
@ -1961,9 +1896,7 @@ pub type notmuch_database_get_config = unsafe extern "C" fn(
|
|||
|
||||
/// Create an iterator for all config items with keys matching a given prefix
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_database_get_config_list = unsafe extern "C" fn(
|
||||
db: *mut notmuch_database_t,
|
||||
prefix: *const ::std::os::raw::c_char,
|
||||
|
@ -1972,9 +1905,7 @@ pub type notmuch_database_get_config_list = unsafe extern "C" fn(
|
|||
|
||||
/// Is 'config_list' iterator valid (i.e. _key, _value, _move_to_next can be called).
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_config_list_valid =
|
||||
unsafe extern "C" fn(config_list: *mut notmuch_config_list_t) -> notmuch_bool_t;
|
||||
|
||||
|
@ -1983,9 +1914,7 @@ pub type notmuch_config_list_valid =
|
|||
/// return value is owned by the iterator, and will be destroyed by the
|
||||
/// next call to notmuch_config_list_key or notmuch_config_list_destroy.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_config_list_key =
|
||||
unsafe extern "C" fn(config_list: *mut notmuch_config_list_t) -> *const ::std::os::raw::c_char;
|
||||
|
||||
|
@ -1994,25 +1923,19 @@ pub type notmuch_config_list_key =
|
|||
/// return value is owned by the iterator, and will be destroyed by the
|
||||
/// next call to notmuch_config_list_value or notmuch config_list_destroy
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_config_list_value =
|
||||
unsafe extern "C" fn(config_list: *mut notmuch_config_list_t) -> *const ::std::os::raw::c_char;
|
||||
|
||||
/// move 'config_list' iterator to the next pair
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_config_list_move_to_next =
|
||||
unsafe extern "C" fn(config_list: *mut notmuch_config_list_t);
|
||||
|
||||
/// free any resources held by 'config_list'
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_config_list_destroy =
|
||||
unsafe extern "C" fn(config_list: *mut notmuch_config_list_t);
|
||||
|
||||
|
@ -2025,9 +1948,7 @@ pub type notmuch_config_list_destroy =
|
|||
/// This object represents a set of options on how a message can be
|
||||
/// added to the index. At the moment it is a featureless stub.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 5.1 (notmuch 0.26)
|
||||
/// ```
|
||||
pub type notmuch_database_get_default_indexopts =
|
||||
unsafe extern "C" fn(db: *mut notmuch_database_t) -> *mut notmuch_indexopts_t;
|
||||
|
||||
|
@ -2046,9 +1967,7 @@ pub type notmuch_decryption_policy_t = u32;
|
|||
/// message index is adequately protected. DO NOT SET THIS FLAG TO TRUE
|
||||
/// without considering the security of your index.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 5.1 (notmuch 0.26)
|
||||
/// ```
|
||||
pub type notmuch_indexopts_set_decrypt_policy = unsafe extern "C" fn(
|
||||
indexopts: *mut notmuch_indexopts_t,
|
||||
decrypt_policy: notmuch_decryption_policy_t,
|
||||
|
@ -2057,23 +1976,17 @@ pub type notmuch_indexopts_set_decrypt_policy = unsafe extern "C" fn(
|
|||
/// Return whether to decrypt encrypted parts while indexing.
|
||||
/// see notmuch_indexopts_set_decrypt_policy.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 5.1 (notmuch 0.26)
|
||||
/// ```
|
||||
pub type notmuch_indexopts_get_decrypt_policy =
|
||||
unsafe extern "C" fn(indexopts: *const notmuch_indexopts_t) -> notmuch_decryption_policy_t;
|
||||
|
||||
/// Destroy a notmuch_indexopts_t object.
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 5.1 (notmuch 0.26)
|
||||
/// ```
|
||||
pub type notmuch_indexopts_destroy = unsafe extern "C" fn(options: *mut notmuch_indexopts_t);
|
||||
|
||||
/// interrogate the library for compile time features
|
||||
///
|
||||
/// ```text
|
||||
/// @since libnotmuch 4.4 (notmuch 0.23)
|
||||
/// ```
|
||||
pub type notmuch_built_with =
|
||||
unsafe extern "C" fn(name: *const ::std::os::raw::c_char) -> notmuch_bool_t;
|
||||
|
|
|
@ -65,16 +65,6 @@ impl<'m> Message<'m> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn header(&self, header: &CStr) -> Option<&[u8]> {
|
||||
let header_val =
|
||||
unsafe { call!(self.lib, notmuch_message_get_header)(self.message, header.as_ptr()) };
|
||||
if header_val.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(unsafe { CStr::from_ptr(header_val).to_bytes() })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn msg_id(&self) -> &[u8] {
|
||||
let c_str = self.msg_id_cstr();
|
||||
c_str.to_bytes()
|
||||
|
@ -91,11 +81,19 @@ impl<'m> Message<'m> {
|
|||
|
||||
pub fn into_envelope(
|
||||
self,
|
||||
index: &RwLock<HashMap<EnvelopeHash, CString>>,
|
||||
tag_index: &RwLock<BTreeMap<u64, String>>,
|
||||
) -> Envelope {
|
||||
index: Arc<RwLock<HashMap<EnvelopeHash, CString>>>,
|
||||
tag_index: Arc<RwLock<BTreeMap<u64, String>>>,
|
||||
) -> Result<Envelope> {
|
||||
let mut contents = Vec::new();
|
||||
let path = self.get_filename().to_os_string();
|
||||
let mut f = std::fs::File::open(&path)?;
|
||||
f.read_to_end(&mut contents)?;
|
||||
let env_hash = self.env_hash();
|
||||
let mut env = Envelope::new(env_hash);
|
||||
let mut env = Envelope::from_bytes(&contents, None).chain_err_summary(|| {
|
||||
index.write().unwrap().remove(&env_hash);
|
||||
format!("could not parse path {:?}", path)
|
||||
})?;
|
||||
env.set_hash(env_hash);
|
||||
index
|
||||
.write()
|
||||
.unwrap()
|
||||
|
@ -111,63 +109,8 @@ impl<'m> Message<'m> {
|
|||
}
|
||||
env.labels_mut().push(num);
|
||||
}
|
||||
unsafe {
|
||||
use crate::email::parser::address::rfc2822address_list;
|
||||
env.set_message_id(self.msg_id())
|
||||
.set_date(
|
||||
self.header(CStr::from_bytes_with_nul_unchecked(b"Date\0"))
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.set_from(
|
||||
rfc2822address_list(
|
||||
self.header(CStr::from_bytes_with_nul_unchecked(b"From\0"))
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.map(|(_, v)| v)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.set_to(
|
||||
rfc2822address_list(
|
||||
self.header(CStr::from_bytes_with_nul_unchecked(b"To\0"))
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.map(|(_, v)| v)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.set_cc(
|
||||
rfc2822address_list(
|
||||
self.header(CStr::from_bytes_with_nul_unchecked(b"Cc\0"))
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.map(|(_, v)| v)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.set_bcc(
|
||||
rfc2822address_list(
|
||||
self.header(CStr::from_bytes_with_nul_unchecked(b"Bcc\0"))
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.map(|(_, v)| v)
|
||||
.unwrap_or_default()
|
||||
.to_vec(),
|
||||
)
|
||||
.set_subject(
|
||||
self.header(CStr::from_bytes_with_nul_unchecked(b"Subject\0"))
|
||||
.unwrap_or_default()
|
||||
.to_vec(),
|
||||
)
|
||||
.set_references(
|
||||
self.header(CStr::from_bytes_with_nul_unchecked(b"References\0"))
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.set_in_reply_to(
|
||||
self.header(CStr::from_bytes_with_nul_unchecked(b"In-Reply-To\0"))
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.set_datetime(self.date())
|
||||
.set_flags(flags);
|
||||
}
|
||||
env
|
||||
env.set_flags(flags);
|
||||
Ok(env)
|
||||
}
|
||||
|
||||
pub fn replies_iter(&self) -> Option<MessageIterator> {
|
||||
|
@ -194,7 +137,6 @@ impl<'m> Message<'m> {
|
|||
ThreadNode {
|
||||
message: Some(self.env_hash()),
|
||||
parent: None,
|
||||
other_mailbox: false,
|
||||
children: vec![],
|
||||
date: self.date(),
|
||||
show_subject: true,
|
||||
|
@ -247,7 +189,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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,19 +25,47 @@ use smallvec::SmallVec;
|
|||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
pub type EnvelopeRef<'g> = RwRef<'g, EnvelopeHash, Envelope>;
|
||||
pub type EnvelopeRefMut<'g> = RwRefMut<'g, EnvelopeHash, Envelope>;
|
||||
pub struct EnvelopeRef<'g> {
|
||||
guard: RwLockReadGuard<'g, HashMap<EnvelopeHash, Envelope>>,
|
||||
env_hash: EnvelopeHash,
|
||||
}
|
||||
|
||||
impl Deref for EnvelopeRef<'_> {
|
||||
type Target = Envelope;
|
||||
|
||||
fn deref(&self) -> &Envelope {
|
||||
self.guard.get(&self.env_hash).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EnvelopeRefMut<'g> {
|
||||
guard: RwLockWriteGuard<'g, HashMap<EnvelopeHash, Envelope>>,
|
||||
env_hash: EnvelopeHash,
|
||||
}
|
||||
|
||||
impl Deref for EnvelopeRefMut<'_> {
|
||||
type Target = Envelope;
|
||||
|
||||
fn deref(&self) -> &Envelope {
|
||||
self.guard.get(&self.env_hash).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for EnvelopeRefMut<'_> {
|
||||
fn deref_mut(&mut self) -> &mut Envelope {
|
||||
self.guard.get_mut(&self.env_hash).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Collection {
|
||||
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<u64, String>>>,
|
||||
}
|
||||
|
||||
impl Default for Collection {
|
||||
|
@ -60,11 +88,7 @@ impl Drop for Collection {
|
|||
}
|
||||
};
|
||||
let writer = io::BufWriter::new(f);
|
||||
let _ = bincode::Options::serialize_into(
|
||||
bincode::config::DefaultOptions::new(),
|
||||
writer,
|
||||
&self.thread,
|
||||
);
|
||||
bincode::serialize_into(writer, &self.threads).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -87,7 +111,6 @@ impl Collection {
|
|||
|
||||
Collection {
|
||||
envelopes: Arc::new(RwLock::new(Default::default())),
|
||||
tag_index: Arc::new(RwLock::new(BTreeMap::default())),
|
||||
message_id_index,
|
||||
threads,
|
||||
mailboxes,
|
||||
|
@ -427,14 +450,14 @@ impl Collection {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_env(&'_ self, hash: EnvelopeHash) -> EnvelopeRef<'_> {
|
||||
pub fn get_env(&'_ self, env_hash: EnvelopeHash) -> EnvelopeRef<'_> {
|
||||
let guard: RwLockReadGuard<'_, _> = self.envelopes.read().unwrap();
|
||||
EnvelopeRef { guard, hash }
|
||||
EnvelopeRef { guard, env_hash }
|
||||
}
|
||||
|
||||
pub fn get_env_mut(&'_ self, hash: EnvelopeHash) -> EnvelopeRefMut<'_> {
|
||||
pub fn get_env_mut(&'_ self, env_hash: EnvelopeHash) -> EnvelopeRefMut<'_> {
|
||||
let guard = self.envelopes.write().unwrap();
|
||||
EnvelopeRefMut { guard, hash }
|
||||
EnvelopeRefMut { guard, env_hash }
|
||||
}
|
||||
|
||||
pub fn get_threads(&'_ self, hash: MailboxHash) -> RwRef<'_, MailboxHash, Threads> {
|
||||
|
@ -475,25 +498,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")
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
self.guard.get(&self.hash).unwrap()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
|
||||
//! Basic mail account configuration to use with [`backends`](./backends/index.html)
|
||||
use crate::backends::SpecialUsageMailbox;
|
||||
pub use crate::{SortField, SortOrder};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
@ -31,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>,
|
||||
|
@ -67,9 +63,6 @@ impl AccountSettings {
|
|||
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
|
||||
|
@ -81,8 +74,8 @@ impl AccountSettings {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MailboxConf {
|
||||
#[serde(alias = "rename")]
|
||||
pub alias: Option<String>,
|
||||
|
@ -94,8 +87,6 @@ pub struct MailboxConf {
|
|||
pub ignore: ToggleFlag,
|
||||
#[serde(default = "none")]
|
||||
pub usage: Option<SpecialUsageMailbox>,
|
||||
#[serde(default = "none")]
|
||||
pub sort_order: Option<usize>,
|
||||
#[serde(flatten)]
|
||||
pub extra: HashMap<String, String>,
|
||||
}
|
||||
|
@ -108,7 +99,6 @@ impl Default for MailboxConf {
|
|||
subscribe: ToggleFlag::Unset,
|
||||
ignore: ToggleFlag::Unset,
|
||||
usage: None,
|
||||
sort_order: None,
|
||||
extra: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
@ -132,53 +122,12 @@ pub fn none<T>() -> Option<T> {
|
|||
None
|
||||
}
|
||||
|
||||
macro_rules! named_unit_variant {
|
||||
($variant:ident) => {
|
||||
pub mod $variant {
|
||||
/*
|
||||
pub fn serialize<S>(serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(stringify!($variant))
|
||||
}
|
||||
*/
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<(), D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct V;
|
||||
impl<'de> serde::de::Visitor<'de> for V {
|
||||
type Value = ();
|
||||
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.write_str(concat!("\"", stringify!($variant), "\""))
|
||||
}
|
||||
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
|
||||
if value == stringify!($variant) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(E::invalid_value(serde::de::Unexpected::Str(value), &self))
|
||||
}
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_str(V)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mod strings {
|
||||
named_unit_variant!(ask);
|
||||
}
|
||||
|
||||
#[derive(Copy, Debug, Clone, PartialEq)]
|
||||
pub enum ToggleFlag {
|
||||
Unset,
|
||||
InternalVal(bool),
|
||||
False,
|
||||
True,
|
||||
Ask,
|
||||
}
|
||||
|
||||
impl From<bool> for ToggleFlag {
|
||||
|
@ -202,19 +151,17 @@ impl ToggleFlag {
|
|||
ToggleFlag::Unset == *self
|
||||
}
|
||||
pub fn is_internal(&self) -> bool {
|
||||
matches!(self, ToggleFlag::InternalVal(_))
|
||||
if let ToggleFlag::InternalVal(_) = *self {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_ask(&self) -> bool {
|
||||
matches!(self, ToggleFlag::Ask)
|
||||
}
|
||||
|
||||
pub fn is_false(&self) -> bool {
|
||||
matches!(self, ToggleFlag::False | ToggleFlag::InternalVal(false))
|
||||
ToggleFlag::False == *self || ToggleFlag::InternalVal(false) == *self
|
||||
}
|
||||
|
||||
pub fn is_true(&self) -> bool {
|
||||
matches!(self, ToggleFlag::True | ToggleFlag::InternalVal(true))
|
||||
ToggleFlag::True == *self || ToggleFlag::InternalVal(true) == *self
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -227,7 +174,6 @@ impl Serialize for ToggleFlag {
|
|||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -237,25 +183,10 @@ 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!(
|
||||
r#"expected one of "true", "false", "ask", found `{}`"#,
|
||||
err
|
||||
))
|
||||
})? {
|
||||
InnerToggleFlag::Bool(true) => ToggleFlag::True,
|
||||
InnerToggleFlag::Bool(false) => ToggleFlag::False,
|
||||
InnerToggleFlag::Ask => ToggleFlag::Ask,
|
||||
},
|
||||
)
|
||||
let s = <bool>::deserialize(deserializer);
|
||||
Ok(match s? {
|
||||
true => ToggleFlag::True,
|
||||
false => ToggleFlag::False,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")]
|
||||
|
@ -271,9 +273,7 @@ pub fn lookup_ipv4(host: &str, port: u16) -> crate::Result<std::net::SocketAddr>
|
|||
|
||||
Err(
|
||||
crate::error::MeliError::new(format!("Could not lookup address {}:{}", host, port))
|
||||
.set_kind(crate::error::ErrorKind::Network(
|
||||
crate::error::NetworkErrorKind::HostLookupFailed,
|
||||
)),
|
||||
.set_kind(crate::error::ErrorKind::Network),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -291,7 +291,3 @@ pub async fn timeout<O>(dur: Option<Duration>, f: impl Future<Output = O>) -> cr
|
|||
Ok(f.await)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sleep(dur: Duration) {
|
||||
smol::Timer::after(dur).await;
|
||||
}
|
||||
|
|
|
@ -34,199 +34,71 @@
|
|||
//! assert_eq!(timestamp, 1578509043);
|
||||
//!
|
||||
//! // Convert timestamp back to string
|
||||
//! let s = timestamp_to_string(timestamp, Some("%Y-%m-%d"), true);
|
||||
//! let s = timestamp_to_string(timestamp, Some("%Y-%m-%d"));
|
||||
//! assert_eq!(s, "2020-01-08");
|
||||
//! ```
|
||||
use crate::error::{Result, ResultIntoMeliError};
|
||||
use std::borrow::Cow;
|
||||
use crate::error::Result;
|
||||
use std::convert::TryInto;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::c_int;
|
||||
|
||||
pub type UnixTimestamp = u64;
|
||||
pub const RFC3339_FMT_WITH_TIME: &str = "%Y-%m-%dT%H:%M:%S\0";
|
||||
pub const RFC3339_FMT: &str = "%Y-%m-%d\0";
|
||||
pub const RFC822_DATE: &str = "%a, %d %b %Y %H:%M:%S %z\0";
|
||||
pub const RFC822_FMT_WITH_TIME: &str = "%a, %e %h %Y %H:%M:%S \0";
|
||||
pub const RFC822_FMT: &str = "%e %h %Y %H:%M:%S \0";
|
||||
pub const DEFAULT_FMT: &str = "%a, %d %b %Y %R\0";
|
||||
//"Tue May 21 13:46:22 1991\n"
|
||||
//"Wed Sep 9 00:27:54 2020\n"
|
||||
pub const ASCTIME_FMT: &str = "%a %b %d %H:%M:%S %Y\n\0";
|
||||
|
||||
use libc::{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;
|
||||
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;
|
||||
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 mktime(tm: *const ::libc::tm) -> ::libc::time_t;
|
||||
|
||||
fn localtime_r(timep: *const libc::time_t, tm: *mut libc::tm) -> *mut libc::tm;
|
||||
fn localtime_r(timep: *const ::libc::time_t, tm: *mut ::libc::tm) -> *mut ::libc::tm;
|
||||
|
||||
fn gettimeofday(tv: *mut libc::timeval, tz: *mut libc::timezone) -> i32;
|
||||
fn gettimeofday(tv: *mut timeval, tz: *mut timezone) -> i32;
|
||||
}
|
||||
|
||||
#[repr(i32)]
|
||||
#[derive(Copy, Clone)]
|
||||
#[allow(dead_code)]
|
||||
enum LocaleCategoryMask {
|
||||
Time = libc::LC_TIME_MASK,
|
||||
All = libc::LC_ALL_MASK,
|
||||
}
|
||||
|
||||
#[repr(i32)]
|
||||
#[derive(Copy, Clone)]
|
||||
#[allow(dead_code)]
|
||||
enum LocaleCategory {
|
||||
Time = libc::LC_TIME,
|
||||
All = libc::LC_ALL,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "netbsd"))]
|
||||
#[allow(dead_code)]
|
||||
struct Locale {
|
||||
mask: LocaleCategoryMask,
|
||||
category: LocaleCategory,
|
||||
new_locale: libc::locale_t,
|
||||
old_locale: libc::locale_t,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "netbsd")]
|
||||
#[allow(dead_code)]
|
||||
struct Locale {
|
||||
mask: LocaleCategoryMask,
|
||||
category: LocaleCategory,
|
||||
old_locale: *const std::os::raw::c_char,
|
||||
}
|
||||
|
||||
impl Drop for Locale {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(not(target_os = "netbsd"))]
|
||||
unsafe {
|
||||
let _ = libc::uselocale(self.old_locale);
|
||||
libc::freelocale(self.new_locale);
|
||||
}
|
||||
#[cfg(target_os = "netbsd")]
|
||||
unsafe {
|
||||
let _ = libc::setlocale(self.category as c_int, self.old_locale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// How to unit test this? Test machine is not guaranteed to have non-english locales.
|
||||
impl Locale {
|
||||
#[cfg(not(target_os = "netbsd"))]
|
||||
fn new(
|
||||
mask: LocaleCategoryMask,
|
||||
category: LocaleCategory,
|
||||
locale: *const std::os::raw::c_char,
|
||||
base: libc::locale_t,
|
||||
) -> Result<Self> {
|
||||
let new_locale = unsafe { libc::newlocale(mask as c_int, locale, base) };
|
||||
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 {
|
||||
mask,
|
||||
category,
|
||||
new_locale,
|
||||
old_locale,
|
||||
})
|
||||
}
|
||||
#[cfg(target_os = "netbsd")]
|
||||
fn new(
|
||||
mask: LocaleCategoryMask,
|
||||
category: LocaleCategory,
|
||||
locale: *const std::os::raw::c_char,
|
||||
_base: libc::locale_t,
|
||||
) -> Result<Self> {
|
||||
let old_locale = unsafe { libc::setlocale(category as c_int, std::ptr::null_mut()) };
|
||||
if old_locale.is_null() {
|
||||
return Err(nix::Error::last().into());
|
||||
}
|
||||
let new_locale = unsafe { libc::setlocale(category as c_int, locale) };
|
||||
if new_locale.is_null() {
|
||||
return Err(nix::Error::last().into());
|
||||
}
|
||||
Ok(Locale {
|
||||
mask,
|
||||
category,
|
||||
old_locale,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn timestamp_to_string(timestamp: UnixTimestamp, fmt: Option<&str>, posix: bool) -> String {
|
||||
let mut new_tm: libc::tm = unsafe { std::mem::zeroed() };
|
||||
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);
|
||||
localtime_r(&i as *const i64, &mut new_tm as *mut ::libc::tm);
|
||||
}
|
||||
let format: Cow<'_, CStr> = if let Some(cs) = fmt
|
||||
.map(str::as_bytes)
|
||||
.map(CStr::from_bytes_with_nul)
|
||||
.and_then(|res| res.ok())
|
||||
{
|
||||
Cow::from(cs)
|
||||
} else if let Some(cstring) = fmt
|
||||
.map(str::as_bytes)
|
||||
let fmt = fmt
|
||||
.map(CString::new)
|
||||
.and_then(|res| res.ok())
|
||||
{
|
||||
Cow::from(cstring)
|
||||
.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(DEFAULT_FMT.as_bytes()).into() }
|
||||
unsafe { CStr::from_bytes_with_nul_unchecked(b"%a, %d %b %Y %T %z\0") }
|
||||
};
|
||||
|
||||
let mut vec: [u8; 256] = [0; 256];
|
||||
let ret = {
|
||||
let _with_locale: Option<Result<Locale>> = if posix {
|
||||
Some(
|
||||
Locale::new(
|
||||
LocaleCategoryMask::Time,
|
||||
LocaleCategory::Time,
|
||||
b"C\0".as_ptr() as *const std::os::raw::c_char,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
.chain_err_summary(|| "Could not set locale for datetime conversion")
|
||||
.chain_err_kind(crate::error::ErrorKind::External),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
unsafe {
|
||||
strftime(
|
||||
vec.as_mut_ptr() as *mut _,
|
||||
256,
|
||||
format.as_ptr(),
|
||||
&new_tm as *const _,
|
||||
)
|
||||
}
|
||||
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, ()> {
|
||||
fn tm_to_secs(tm: ::libc::tm) -> std::result::Result<i64, ()> {
|
||||
let mut is_leap = false;
|
||||
let mut year = tm.tm_year;
|
||||
let mut month = tm.tm_mon;
|
||||
if !(0..12).contains(&month) {
|
||||
if month >= 12 || month < 0 {
|
||||
let mut adj = month / 12;
|
||||
month %= 12;
|
||||
if month < 0 {
|
||||
|
@ -259,13 +131,17 @@ fn year_to_secs(year: i64, is_leap: &mut bool) -> std::result::Result<i64, ()> {
|
|||
} else {
|
||||
*is_leap = false;
|
||||
}
|
||||
return Ok(31536000 * (y - 70) + 86400 * leaps);
|
||||
return Ok((31536000 * (y - 70) + 86400 * leaps)
|
||||
.try_into()
|
||||
.unwrap_or(0));
|
||||
}
|
||||
|
||||
let cycles = (year - 100) / 400;
|
||||
let centuries;
|
||||
let mut leaps;
|
||||
let mut rem = (year - 100) % 400;
|
||||
let mut rem;
|
||||
|
||||
rem = (year - 100) % 400;
|
||||
|
||||
if rem == 0 {
|
||||
*is_leap = true;
|
||||
|
@ -331,59 +207,52 @@ where
|
|||
T: Into<Vec<u8>>,
|
||||
{
|
||||
let s = CString::new(s)?;
|
||||
let mut new_tm: libc::tm = unsafe { std::mem::zeroed() };
|
||||
for fmt in &[RFC822_FMT_WITH_TIME, RFC822_FMT, ASCTIME_FMT] {
|
||||
let fmt = unsafe { CStr::from_bytes_with_nul_unchecked(fmt.as_bytes()) };
|
||||
let ret = {
|
||||
let _with_locale = Locale::new(
|
||||
LocaleCategoryMask::Time,
|
||||
LocaleCategory::Time,
|
||||
b"C\0".as_ptr() as *const std::os::raw::c_char,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
.chain_err_summary(|| "Could not set locale for datetime conversion")
|
||||
.chain_err_kind(crate::error::ErrorKind::External)?;
|
||||
unsafe { strptime(s.as_ptr(), fmt.as_ptr(), &mut new_tm as *mut _) }
|
||||
};
|
||||
|
||||
if ret.is_null() {
|
||||
continue;
|
||||
}
|
||||
let rest = unsafe { 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)
|
||||
{
|
||||
// safe since rest.to_bytes().is_ascii()
|
||||
let offset = unsafe { 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>())
|
||||
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 = 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)
|
||||
{
|
||||
if rest.to_bytes()[0] == b'-' {
|
||||
hr_offset = -hr_offset;
|
||||
min_offset = -min_offset;
|
||||
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
|
||||
}
|
||||
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()
|
||||
};
|
||||
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));
|
||||
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)
|
||||
}
|
||||
|
@ -393,59 +262,50 @@ where
|
|||
T: Into<Vec<u8>>,
|
||||
{
|
||||
let s = CString::new(s)?;
|
||||
let mut new_tm: libc::tm = unsafe { std::mem::zeroed() };
|
||||
for fmt in &[RFC3339_FMT_WITH_TIME, RFC3339_FMT] {
|
||||
let fmt = unsafe { CStr::from_bytes_with_nul_unchecked(fmt.as_bytes()) };
|
||||
let ret = {
|
||||
let _with_locale = Locale::new(
|
||||
LocaleCategoryMask::Time,
|
||||
LocaleCategory::Time,
|
||||
b"C\0".as_ptr() as *const std::os::raw::c_char,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
.chain_err_summary(|| "Could not set locale for datetime conversion")
|
||||
.chain_err_kind(crate::error::ErrorKind::External)?;
|
||||
unsafe { strptime(s.as_ptr(), fmt.as_ptr(), &mut new_tm as *mut _) }
|
||||
};
|
||||
if ret.is_null() {
|
||||
continue;
|
||||
}
|
||||
let rest = unsafe { 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)
|
||||
{
|
||||
// safe since rest.to_bytes().is_ascii()
|
||||
let offset = unsafe { 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>())
|
||||
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 = 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)
|
||||
{
|
||||
if rest.to_bytes()[0] == b'-' {
|
||||
hr_offset = -hr_offset;
|
||||
min_offset = -min_offset;
|
||||
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
|
||||
}
|
||||
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()
|
||||
};
|
||||
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));
|
||||
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)
|
||||
}
|
||||
|
@ -455,12 +315,8 @@ 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: Cow<'_, CStr> = if let Ok(cs) = CStr::from_bytes_with_nul(fmt.as_bytes()) {
|
||||
Cow::from(cs)
|
||||
} else {
|
||||
Cow::from(CString::new(fmt.as_bytes())?)
|
||||
};
|
||||
let mut new_tm: ::libc::tm = unsafe { std::mem::zeroed() };
|
||||
let fmt = CString::new(fmt)?;
|
||||
unsafe {
|
||||
let ret = strptime(
|
||||
CString::new(s)?.as_ptr(),
|
||||
|
@ -476,8 +332,8 @@ where
|
|||
|
||||
pub fn now() -> UnixTimestamp {
|
||||
use std::mem::MaybeUninit;
|
||||
let mut tv = MaybeUninit::<libc::timeval>::uninit();
|
||||
let mut tz = MaybeUninit::<libc::timezone>::uninit();
|
||||
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 {
|
||||
|
@ -488,15 +344,12 @@ pub fn now() -> UnixTimestamp {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_datetime_timestamp() {
|
||||
timestamp_to_string(0, None, false);
|
||||
fn test_timestamp() {
|
||||
timestamp_to_string(0, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datetime_rfcs() {
|
||||
if unsafe { libc::setlocale(libc::LC_ALL, b"\0".as_ptr() as _) }.is_null() {
|
||||
println!("Unable to set locale.");
|
||||
}
|
||||
fn test_rfcs() {
|
||||
/* Some tests were lazily stolen from https://rachelbythebay.com/w/2013/06/11/time/ */
|
||||
|
||||
assert_eq!(
|
||||
|
@ -507,7 +360,7 @@ fn test_datetime_rfcs() {
|
|||
/*
|
||||
macro_rules! mkt {
|
||||
($year:literal, $month:literal, $day:literal, $hour:literal, $minute:literal, $second:literal) => {
|
||||
libc::tm {
|
||||
::libc::tm {
|
||||
tm_sec: $second,
|
||||
tm_min: $minute,
|
||||
tm_hour: $hour,
|
||||
|
|
|
@ -20,75 +20,8 @@
|
|||
*/
|
||||
|
||||
/*!
|
||||
* 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, handling, sending etc.
|
||||
*/
|
||||
|
||||
pub mod address;
|
||||
pub mod attachment_types;
|
||||
pub mod attachments;
|
||||
|
@ -97,7 +30,7 @@ pub mod headers;
|
|||
pub mod list_management;
|
||||
pub mod mailto;
|
||||
pub mod parser;
|
||||
pub mod pgp;
|
||||
pub mod signatures;
|
||||
|
||||
pub use address::{Address, MessageID, References, StrBuild, StrBuilder};
|
||||
pub use attachments::{Attachment, AttachmentBuilder};
|
||||
|
@ -143,23 +76,6 @@ impl PartialEq<&str> for Flag {
|
|||
}
|
||||
}
|
||||
|
||||
macro_rules! flag_impl {
|
||||
(fn $name:ident, $val:expr) => {
|
||||
pub const fn $name(&self) -> bool {
|
||||
self.contains($val)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Flag {
|
||||
flag_impl!(fn is_seen, Flag::SEEN);
|
||||
flag_impl!(fn is_draft, Flag::DRAFT);
|
||||
flag_impl!(fn is_trashed, Flag::TRASHED);
|
||||
flag_impl!(fn is_passed, Flag::PASSED);
|
||||
flag_impl!(fn is_replied, Flag::REPLIED);
|
||||
flag_impl!(fn is_flagged, Flag::FLAGGED);
|
||||
}
|
||||
|
||||
///`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.
|
||||
|
@ -236,7 +152,6 @@ impl core::fmt::Debug for Envelope {
|
|||
.field("Message-ID", &self.message_id_display())
|
||||
.field("In-Reply-To", &self.in_reply_to_display())
|
||||
.field("References", &self.references)
|
||||
.field("Flags", &self.flags)
|
||||
.field("Hash", &self.hash)
|
||||
.finish()
|
||||
}
|
||||
|
@ -270,9 +185,8 @@ impl Envelope {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_hash(&mut self, new_hash: EnvelopeHash) -> &mut Self {
|
||||
pub fn set_hash(&mut self, new_hash: EnvelopeHash) {
|
||||
self.hash = new_hash;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8], flags: Option<Flag>) -> Result<Envelope> {
|
||||
|
@ -338,6 +252,14 @@ impl Envelope {
|
|||
} else if name == "message-id" {
|
||||
self.set_message_id(value);
|
||||
} else if name == "references" {
|
||||
{
|
||||
let parse_result = parser::address::msg_id_list(value);
|
||||
if let Ok((_, value)) = parse_result {
|
||||
for v in value {
|
||||
self.push_references(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.set_references(value);
|
||||
} else if name == "in-reply-to" {
|
||||
self.set_in_reply_to(value);
|
||||
|
@ -391,7 +313,7 @@ impl Envelope {
|
|||
if let Some(x) = self.in_reply_to.clone() {
|
||||
self.push_references(x);
|
||||
}
|
||||
if let Ok(d) = parser::dates::rfc5322_date(self.date.as_bytes()) {
|
||||
if let Ok(d) = parser::dates::rfc5322_date(&self.date.as_bytes()) {
|
||||
self.set_datetime(d);
|
||||
}
|
||||
if self.message_id.raw().is_empty() {
|
||||
|
@ -488,14 +410,6 @@ impl Envelope {
|
|||
self.to.as_slice()
|
||||
}
|
||||
|
||||
pub fn cc(&self) -> &[Address] {
|
||||
self.cc.as_slice()
|
||||
}
|
||||
|
||||
pub fn bcc(&self) -> &[Address] {
|
||||
self.bcc.as_slice()
|
||||
}
|
||||
|
||||
pub fn field_to_to_string(&self) -> String {
|
||||
if self.to.is_empty() {
|
||||
self.other_headers
|
||||
|
@ -592,51 +506,41 @@ impl Envelope {
|
|||
String::from_utf8_lossy(self.message_id.raw())
|
||||
}
|
||||
|
||||
pub fn set_date(&mut self, new_val: &[u8]) -> &mut Self {
|
||||
pub fn set_date(&mut self, new_val: &[u8]) {
|
||||
let new_val = new_val.trim();
|
||||
self.date = String::from_utf8_lossy(new_val).into_owned();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_bcc(&mut self, new_val: Vec<Address>) -> &mut Self {
|
||||
pub fn set_bcc(&mut self, new_val: Vec<Address>) {
|
||||
self.bcc = new_val;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_cc(&mut self, new_val: SmallVec<[Address; 1]>) -> &mut Self {
|
||||
pub fn set_cc(&mut self, new_val: SmallVec<[Address; 1]>) {
|
||||
self.cc = new_val;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_from(&mut self, new_val: SmallVec<[Address; 1]>) -> &mut Self {
|
||||
pub fn set_from(&mut self, new_val: SmallVec<[Address; 1]>) {
|
||||
self.from = new_val;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_to(&mut self, new_val: SmallVec<[Address; 1]>) -> &mut Self {
|
||||
pub fn set_to(&mut self, new_val: SmallVec<[Address; 1]>) {
|
||||
self.to = new_val;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_in_reply_to(&mut self, new_val: &[u8]) -> &mut Self {
|
||||
pub fn set_in_reply_to(&mut self, new_val: &[u8]) {
|
||||
// FIXME msg_id_list
|
||||
let new_val = new_val.trim();
|
||||
if !new_val.is_empty() {
|
||||
let val = match parser::address::msg_id(new_val) {
|
||||
Ok(v) => v.1,
|
||||
Err(_) => {
|
||||
self.in_reply_to = Some(MessageID::new(new_val, new_val));
|
||||
return self;
|
||||
}
|
||||
};
|
||||
self.in_reply_to = Some(val);
|
||||
} else {
|
||||
self.in_reply_to = None;
|
||||
}
|
||||
self
|
||||
let val = match parser::address::msg_id(new_val) {
|
||||
Ok(v) => v.1,
|
||||
Err(_) => {
|
||||
self.in_reply_to = Some(MessageID::new(new_val, new_val));
|
||||
return;
|
||||
}
|
||||
};
|
||||
self.in_reply_to = Some(val);
|
||||
}
|
||||
|
||||
pub fn set_subject(&mut self, new_val: Vec<u8>) -> &mut Self {
|
||||
pub fn set_subject(&mut self, new_val: Vec<u8>) {
|
||||
let mut new_val = String::from_utf8(new_val)
|
||||
.unwrap_or_else(|err| String::from_utf8_lossy(&err.into_bytes()).into());
|
||||
while new_val
|
||||
|
@ -649,10 +553,9 @@ impl Envelope {
|
|||
}
|
||||
|
||||
self.subject = Some(new_val);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_message_id(&mut self, new_val: &[u8]) -> &mut Self {
|
||||
pub fn set_message_id(&mut self, new_val: &[u8]) {
|
||||
let new_val = new_val.trim();
|
||||
match parser::address::msg_id(new_val) {
|
||||
Ok((_, val)) => {
|
||||
|
@ -662,7 +565,6 @@ impl Envelope {
|
|||
self.message_id = MessageID::new(new_val, new_val);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn push_references(&mut self, new_ref: MessageID) {
|
||||
|
@ -691,31 +593,19 @@ impl Envelope {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_references(&mut self, new_val: &[u8]) -> &mut Self {
|
||||
pub fn set_references(&mut self, new_val: &[u8]) {
|
||||
let new_val = new_val.trim();
|
||||
if !new_val.is_empty() {
|
||||
self.references = None;
|
||||
{
|
||||
let parse_result = parser::address::msg_id_list(new_val);
|
||||
if let Ok((_, value)) = parse_result {
|
||||
for v in value {
|
||||
self.push_references(v);
|
||||
}
|
||||
}
|
||||
match self.references {
|
||||
Some(ref mut s) => {
|
||||
s.raw = new_val.into();
|
||||
}
|
||||
match self.references {
|
||||
Some(ref mut s) => {
|
||||
s.raw = new_val.into();
|
||||
}
|
||||
None => {
|
||||
self.references = Some(References {
|
||||
raw: new_val.into(),
|
||||
refs: Vec::new(),
|
||||
});
|
||||
}
|
||||
None => {
|
||||
self.references = Some(References {
|
||||
raw: new_val.into(),
|
||||
refs: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn references(&self) -> SmallVec<[&MessageID; 8]> {
|
||||
|
@ -740,47 +630,40 @@ impl Envelope {
|
|||
self.thread
|
||||
}
|
||||
|
||||
pub fn set_thread(&mut self, new_val: ThreadNodeHash) -> &mut Self {
|
||||
pub fn set_thread(&mut self, new_val: ThreadNodeHash) {
|
||||
self.thread = new_val;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_datetime(&mut self, new_val: UnixTimestamp) -> &mut Self {
|
||||
pub fn set_datetime(&mut self, new_val: UnixTimestamp) {
|
||||
self.timestamp = new_val;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_flag(&mut self, f: Flag, value: bool) -> &mut Self {
|
||||
pub fn set_flag(&mut self, f: Flag, value: bool) {
|
||||
self.flags.set(f, value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_flags(&mut self, f: Flag) -> &mut Self {
|
||||
pub fn set_flags(&mut self, f: Flag) {
|
||||
self.flags = f;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn flags(&self) -> Flag {
|
||||
self.flags
|
||||
}
|
||||
|
||||
pub fn set_seen(&mut self) -> &mut Self {
|
||||
self.set_flag(Flag::SEEN, true);
|
||||
self
|
||||
pub fn set_seen(&mut self) {
|
||||
self.set_flag(Flag::SEEN, true)
|
||||
}
|
||||
|
||||
pub fn set_unseen(&mut self) -> &mut Self {
|
||||
self.set_flag(Flag::SEEN, false);
|
||||
self
|
||||
pub fn set_unseen(&mut self) {
|
||||
self.set_flag(Flag::SEEN, false)
|
||||
}
|
||||
|
||||
pub fn is_seen(&self) -> bool {
|
||||
self.flags.contains(Flag::SEEN)
|
||||
}
|
||||
|
||||
pub fn set_has_attachments(&mut self, new_val: bool) -> &mut Self {
|
||||
pub fn set_has_attachments(&mut self, new_val: bool) {
|
||||
self.has_attachments = new_val;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn has_attachments(&self) -> bool {
|
||||
|
|
|
@ -112,7 +112,7 @@ impl Address {
|
|||
}
|
||||
} else {
|
||||
MailboxAddress {
|
||||
raw: address.to_string().into_bytes(),
|
||||
raw: format!("{}", address).into_bytes(),
|
||||
display_name: StrBuilder {
|
||||
offset: 0,
|
||||
length: 0,
|
||||
|
@ -262,7 +262,7 @@ impl Address {
|
|||
let email = self.get_email();
|
||||
let (local_part, domain) =
|
||||
match super::parser::address::addr_spec_raw(email.as_bytes())
|
||||
.map_err(Into::<MeliError>::into)
|
||||
.map_err(|err| Into::<MeliError>::into(err))
|
||||
.and_then(|(_, (l, d))| {
|
||||
Ok((String::from_utf8(l.into())?, String::from_utf8(d.into())?))
|
||||
}) {
|
||||
|
@ -323,14 +323,12 @@ impl Hash for Address {
|
|||
impl core::fmt::Display for Address {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
match self {
|
||||
Address::Mailbox(m) if m.display_name.length > 0 => {
|
||||
match m.display_name.display(&m.raw) {
|
||||
d if d.contains('.') || d.contains(',') => {
|
||||
write!(f, "\"{}\" <{}>", d, m.address_spec.display(&m.raw))
|
||||
}
|
||||
d => write!(f, "{} <{}>", d, m.address_spec.display(&m.raw)),
|
||||
}
|
||||
}
|
||||
Address::Mailbox(m) if m.display_name.length > 0 => write!(
|
||||
f,
|
||||
"{} <{}>",
|
||||
m.display_name.display(&m.raw),
|
||||
m.address_spec.display(&m.raw)
|
||||
),
|
||||
Address::Group(g) => {
|
||||
let attachment_strings: Vec<String> =
|
||||
g.mailbox_list.iter().map(|a| format!("{}", a)).collect();
|
||||
|
@ -392,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()
|
||||
|
|
|
@ -31,29 +31,16 @@ pub enum Charset {
|
|||
UTF16,
|
||||
ISO8859_1,
|
||||
ISO8859_2,
|
||||
ISO8859_3,
|
||||
ISO8859_4,
|
||||
ISO8859_5,
|
||||
ISO8859_6,
|
||||
ISO8859_7,
|
||||
ISO8859_8,
|
||||
ISO8859_10,
|
||||
ISO8859_13,
|
||||
ISO8859_14,
|
||||
ISO8859_15,
|
||||
ISO8859_16,
|
||||
Windows1250,
|
||||
Windows1251,
|
||||
Windows1252,
|
||||
Windows1253,
|
||||
GBK,
|
||||
GB2312,
|
||||
GB18030,
|
||||
BIG5,
|
||||
ISO2022JP,
|
||||
EUCJP,
|
||||
KOI8R,
|
||||
KOI8U,
|
||||
}
|
||||
|
||||
impl Default for Charset {
|
||||
|
@ -80,49 +67,14 @@ impl<'a> From<&'a [u8]> for Charset {
|
|||
b if b.eq_ignore_ascii_case(b"iso-8859-2") || b.eq_ignore_ascii_case(b"iso8859-2") => {
|
||||
Charset::ISO8859_2
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-3") || b.eq_ignore_ascii_case(b"iso8859-3") => {
|
||||
Charset::ISO8859_3
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-4") || b.eq_ignore_ascii_case(b"iso8859-4") => {
|
||||
Charset::ISO8859_4
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-5") || b.eq_ignore_ascii_case(b"iso8859-5") => {
|
||||
Charset::ISO8859_5
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-6") || b.eq_ignore_ascii_case(b"iso8859-6") => {
|
||||
Charset::ISO8859_6
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-7") || b.eq_ignore_ascii_case(b"iso8859-7") => {
|
||||
Charset::ISO8859_7
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-8") || b.eq_ignore_ascii_case(b"iso8859-8") => {
|
||||
Charset::ISO8859_8
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-10")
|
||||
|| b.eq_ignore_ascii_case(b"iso8859-10") =>
|
||||
{
|
||||
Charset::ISO8859_10
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-13")
|
||||
|| b.eq_ignore_ascii_case(b"iso8859-13") =>
|
||||
{
|
||||
Charset::ISO8859_13
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-14")
|
||||
|| b.eq_ignore_ascii_case(b"iso8859-14") =>
|
||||
{
|
||||
Charset::ISO8859_14
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-15")
|
||||
|| b.eq_ignore_ascii_case(b"iso8859-15") =>
|
||||
{
|
||||
Charset::ISO8859_15
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"iso-8859-16")
|
||||
|| b.eq_ignore_ascii_case(b"iso8859-16") =>
|
||||
{
|
||||
Charset::ISO8859_16
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"windows-1250")
|
||||
|| b.eq_ignore_ascii_case(b"windows1250") =>
|
||||
{
|
||||
|
@ -139,24 +91,16 @@ impl<'a> From<&'a [u8]> for Charset {
|
|||
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") =>
|
||||
{
|
||||
Charset::Windows1253
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"gbk") => Charset::GBK,
|
||||
b if b.eq_ignore_ascii_case(b"gb18030") || b.eq_ignore_ascii_case(b"gb-18030") => {
|
||||
Charset::GB18030
|
||||
}
|
||||
b if b.eq_ignore_ascii_case(b"gb2312") || b.eq_ignore_ascii_case(b"gb-2312") => {
|
||||
Charset::GB2312
|
||||
}
|
||||
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));
|
||||
Charset::Ascii
|
||||
|
@ -173,29 +117,16 @@ impl Display for Charset {
|
|||
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::GBK => write!(f, "GBK"),
|
||||
Charset::GB2312 => write!(f, "gb2312"),
|
||||
Charset::GB18030 => write!(f, "gb18030"),
|
||||
Charset::BIG5 => write!(f, "big5"),
|
||||
Charset::ISO2022JP => write!(f, "iso-2022-jp"),
|
||||
Charset::EUCJP => write!(f, "euc-jp"),
|
||||
Charset::KOI8R => write!(f, "koi8-r"),
|
||||
Charset::KOI8U => write!(f, "koi8-u"),
|
||||
Charset::BIG5 => write!(f, "BIG5"),
|
||||
Charset::ISO2022JP => write!(f, "ISO-2022-JP"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -204,7 +135,6 @@ impl Display for Charset {
|
|||
pub enum MultipartType {
|
||||
Alternative,
|
||||
Digest,
|
||||
Encrypted,
|
||||
Mixed,
|
||||
Related,
|
||||
Signed,
|
||||
|
@ -218,18 +148,13 @@ impl Default for MultipartType {
|
|||
|
||||
impl Display for MultipartType {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
MultipartType::Alternative => "multipart/alternative",
|
||||
MultipartType::Digest => "multipart/digest",
|
||||
MultipartType::Encrypted => "multipart/encrypted",
|
||||
MultipartType::Mixed => "multipart/mixed",
|
||||
MultipartType::Related => "multipart/related",
|
||||
MultipartType::Signed => "multipart/signed",
|
||||
}
|
||||
)
|
||||
match self {
|
||||
MultipartType::Alternative => write!(f, "multipart/alternative"),
|
||||
MultipartType::Digest => write!(f, "multipart/digest"),
|
||||
MultipartType::Mixed => write!(f, "multipart/mixed"),
|
||||
MultipartType::Related => write!(f, "multipart/related"),
|
||||
MultipartType::Signed => write!(f, "multipart/signed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -241,8 +166,6 @@ impl From<&[u8]> for MultipartType {
|
|||
MultipartType::Alternative
|
||||
} else if val.eq_ignore_ascii_case(b"digest") {
|
||||
MultipartType::Digest
|
||||
} else if val.eq_ignore_ascii_case(b"encrypted") {
|
||||
MultipartType::Encrypted
|
||||
} else if val.eq_ignore_ascii_case(b"signed") {
|
||||
MultipartType::Signed
|
||||
} else if val.eq_ignore_ascii_case(b"related") {
|
||||
|
@ -267,7 +190,6 @@ pub enum ContentType {
|
|||
},
|
||||
MessageRfc822,
|
||||
PGPSignature,
|
||||
CMSSignature,
|
||||
Other {
|
||||
tag: Vec<u8>,
|
||||
name: Option<String>,
|
||||
|
@ -287,75 +209,6 @@ impl Default for ContentType {
|
|||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&str> for ContentType {
|
||||
fn eq(&self, other: &&str) -> bool {
|
||||
match (self, *other) {
|
||||
(
|
||||
ContentType::Text {
|
||||
kind: Text::Plain, ..
|
||||
},
|
||||
"text/plain",
|
||||
) => true,
|
||||
(
|
||||
ContentType::Text {
|
||||
kind: Text::Html, ..
|
||||
},
|
||||
"text/html",
|
||||
) => true,
|
||||
(
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Alternative,
|
||||
..
|
||||
},
|
||||
"multipart/alternative",
|
||||
) => true,
|
||||
(
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Digest,
|
||||
..
|
||||
},
|
||||
"multipart/digest",
|
||||
) => true,
|
||||
(
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Encrypted,
|
||||
..
|
||||
},
|
||||
"multipart/encrypted",
|
||||
) => true,
|
||||
(
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Mixed,
|
||||
..
|
||||
},
|
||||
"multipart/mixed",
|
||||
) => true,
|
||||
(
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Related,
|
||||
..
|
||||
},
|
||||
"multipart/related",
|
||||
) => true,
|
||||
(
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Signed,
|
||||
..
|
||||
},
|
||||
"multipart/signed",
|
||||
) => 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 Display for ContentType {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
match self {
|
||||
|
@ -363,7 +216,6 @@ impl Display for ContentType {
|
|||
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"),
|
||||
}
|
||||
|
@ -372,17 +224,22 @@ impl Display for ContentType {
|
|||
|
||||
impl ContentType {
|
||||
pub fn is_text(&self) -> bool {
|
||||
matches!(self, ContentType::Text { .. })
|
||||
if let ContentType::Text { .. } = self {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_text_html(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
ContentType::Text {
|
||||
kind: Text::Html,
|
||||
..
|
||||
}
|
||||
)
|
||||
if let ContentType::Text {
|
||||
kind: Text::Html, ..
|
||||
} = self
|
||||
{
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_boundary(parts: &[AttachmentBuilder]) -> String {
|
||||
|
@ -448,7 +305,11 @@ pub enum Text {
|
|||
|
||||
impl Text {
|
||||
pub fn is_html(&self) -> bool {
|
||||
matches!(self, Text::Html)
|
||||
if let Text::Html = self {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -528,11 +389,11 @@ pub enum ContentDispositionKind {
|
|||
|
||||
impl ContentDispositionKind {
|
||||
pub fn is_inline(&self) -> bool {
|
||||
matches!(self, ContentDispositionKind::Inline)
|
||||
*self == ContentDispositionKind::Inline
|
||||
}
|
||||
|
||||
pub fn is_attachment(&self) -> bool {
|
||||
matches!(self, ContentDispositionKind::Attachment)
|
||||
*self == ContentDispositionKind::Attachment
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -557,12 +418,3 @@ impl From<&[u8]> for ContentDisposition {
|
|||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ContentDispositionKind> for ContentDisposition {
|
||||
fn from(kind: ContentDispositionKind) -> ContentDisposition {
|
||||
ContentDisposition {
|
||||
kind,
|
||||
..ContentDisposition::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*! Encoding/decoding of attachments */
|
||||
use crate::email::{
|
||||
address::StrBuilder,
|
||||
parser::{self, BytesExt},
|
||||
|
@ -149,7 +148,7 @@ impl AttachmentBuilder {
|
|||
}
|
||||
}
|
||||
if let Some(boundary) = boundary {
|
||||
let parts = Self::parts(self.body(), boundary);
|
||||
let parts = Self::parts(self.body(), &boundary);
|
||||
|
||||
let boundary = boundary.to_vec();
|
||||
self.content_type = ContentType::Multipart {
|
||||
|
@ -202,10 +201,6 @@ impl AttachmentBuilder {
|
|||
&& cst.eq_ignore_ascii_case(b"pgp-signature")
|
||||
{
|
||||
self.content_type = ContentType::PGPSignature;
|
||||
} else if ct.eq_ignore_ascii_case(b"application")
|
||||
&& cst.eq_ignore_ascii_case(b"pkcs7-signature")
|
||||
{
|
||||
self.content_type = ContentType::CMSSignature;
|
||||
} else {
|
||||
let mut name: Option<String> = None;
|
||||
for (n, v) in params {
|
||||
|
@ -259,7 +254,7 @@ impl AttachmentBuilder {
|
|||
let mut vec = Vec::with_capacity(attachments.len());
|
||||
for a in attachments {
|
||||
let mut builder = AttachmentBuilder::default();
|
||||
let (headers, body) = match parser::attachments::attachment(a) {
|
||||
let (headers, body) = match parser::attachments::attachment(&a) {
|
||||
Ok((_, v)) => v,
|
||||
Err(_) => {
|
||||
debug!("error in parsing attachment");
|
||||
|
@ -355,14 +350,16 @@ pub struct Attachment {
|
|||
|
||||
impl fmt::Debug for Attachment {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
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("raw bytes length", &self.raw.len())
|
||||
.field("body", &String::from_utf8_lossy(&text))
|
||||
.finish()
|
||||
write!(f, "Attachment {{\n content_type: {:?},\n content_transfer_encoding: {:?},\n raw: Vec of {} bytes\n, body:\n{}\n}}",
|
||||
self.content_type,
|
||||
self.content_transfer_encoding,
|
||||
self.raw.len(),
|
||||
{
|
||||
let mut text = Vec::with_capacity(4096);
|
||||
self.get_text_recursive(&mut text);
|
||||
std::str::from_utf8(&text).map(std::string::ToString::to_string).unwrap_or_else(|e| format!("Unicode error {}", e))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -373,64 +370,44 @@ impl fmt::Display for Attachment {
|
|||
match Mail::new(self.body.display_bytes(&self.raw).to_vec(), None) {
|
||||
Ok(wrapper) => write!(
|
||||
f,
|
||||
"{} - {} - {} [message/rfc822] {}",
|
||||
"message/rfc822: {} - {} - {}",
|
||||
wrapper.date(),
|
||||
wrapper.field_from_to_string(),
|
||||
wrapper.subject(),
|
||||
crate::Bytes(self.raw.len()),
|
||||
),
|
||||
Err(err) => write!(
|
||||
f,
|
||||
"could not parse: {} [message/rfc822] {}",
|
||||
err,
|
||||
crate::Bytes(self.raw.len()),
|
||||
wrapper.subject()
|
||||
),
|
||||
Err(e) => write!(f, "{}", e),
|
||||
}
|
||||
}
|
||||
ContentType::PGPSignature => write!(f, "pgp signature [{}]", self.mime_type()),
|
||||
ContentType::CMSSignature => write!(f, "S/MIME signature [{}]", self.mime_type()),
|
||||
ContentType::OctetStream { .. } | ContentType::Other { .. } => {
|
||||
if let Some(name) = self.filename() {
|
||||
write!(
|
||||
f,
|
||||
"\"{}\", [{}] {}",
|
||||
name,
|
||||
self.mime_type(),
|
||||
crate::Bytes(self.raw.len())
|
||||
)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"Data attachment [{}] {}",
|
||||
self.mime_type(),
|
||||
crate::Bytes(self.raw.len())
|
||||
)
|
||||
}
|
||||
ContentType::PGPSignature => write!(f, "pgp signature {}", self.mime_type()),
|
||||
ContentType::OctetStream { ref name } => {
|
||||
write!(f, "{}", name.clone().unwrap_or_else(|| self.mime_type()))
|
||||
}
|
||||
ContentType::Text { .. } => {
|
||||
if let Some(name) = self.filename() {
|
||||
write!(
|
||||
f,
|
||||
"\"{}\", [{}] {}",
|
||||
name,
|
||||
self.mime_type(),
|
||||
crate::Bytes(self.raw.len())
|
||||
)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"Text attachment [{}] {}",
|
||||
self.mime_type(),
|
||||
crate::Bytes(self.raw.len())
|
||||
)
|
||||
}
|
||||
ContentType::Other {
|
||||
name: Some(ref name),
|
||||
..
|
||||
} => write!(f, "\"{}\", [{}]", name, self.mime_type()),
|
||||
ContentType::Other { .. } => write!(f, "Data attachment of type {}", self.mime_type()),
|
||||
ContentType::Text { ref parameters, .. }
|
||||
if parameters
|
||||
.iter()
|
||||
.any(|(name, _)| name.eq_ignore_ascii_case(b"name")) =>
|
||||
{
|
||||
let name = String::from_utf8_lossy(
|
||||
parameters
|
||||
.iter()
|
||||
.find(|(name, _)| name.eq_ignore_ascii_case(b"name"))
|
||||
.map(|(_, value)| value)
|
||||
.unwrap(),
|
||||
);
|
||||
write!(f, "\"{}\", [{}]", name, self.mime_type())
|
||||
}
|
||||
ContentType::Text { .. } => write!(f, "Text attachment of type {}", self.mime_type()),
|
||||
ContentType::Multipart {
|
||||
parts: ref sub_att_vec,
|
||||
..
|
||||
} => write!(
|
||||
f,
|
||||
"{} attachment with {} parts",
|
||||
"{} attachment with {} subs",
|
||||
self.mime_type(),
|
||||
sub_att_vec.len()
|
||||
),
|
||||
|
@ -516,19 +493,20 @@ impl Attachment {
|
|||
.iter()
|
||||
.find(|(n, _)| n.eq_ignore_ascii_case(b"content-type"))
|
||||
.and_then(|(_, v)| {
|
||||
if let Ok((_, (ct, _cst, params))) =
|
||||
parser::attachments::content_type(v)
|
||||
{
|
||||
if ct.eq_ignore_ascii_case(b"multipart") {
|
||||
let mut boundary = None;
|
||||
for (n, v) in params {
|
||||
if n.eq_ignore_ascii_case(b"boundary") {
|
||||
boundary = Some(v);
|
||||
break;
|
||||
match parser::attachments::content_type(v) {
|
||||
Ok((_, (ct, _cst, params))) => {
|
||||
if ct.eq_ignore_ascii_case(b"multipart") {
|
||||
let mut boundary = None;
|
||||
for (n, v) in params {
|
||||
if n.eq_ignore_ascii_case(b"boundary") {
|
||||
boundary = Some(v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return boundary;
|
||||
}
|
||||
return boundary;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
None
|
||||
})
|
||||
|
@ -552,8 +530,8 @@ 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()));
|
||||
ContentType::Text { .. } | ContentType::PGPSignature => {
|
||||
text.extend(decode(self, None));
|
||||
}
|
||||
ContentType::Multipart {
|
||||
ref kind,
|
||||
|
@ -584,7 +562,6 @@ impl Attachment {
|
|||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text(&self) -> String {
|
||||
let mut text = Vec::with_capacity(self.body.length);
|
||||
self.get_text_recursive(&mut text);
|
||||
|
@ -594,7 +571,6 @@ impl Attachment {
|
|||
pub fn mime_type(&self) -> String {
|
||||
self.content_type.to_string()
|
||||
}
|
||||
|
||||
pub fn attachments(&self) -> Vec<Attachment> {
|
||||
let mut ret = Vec::new();
|
||||
fn count_recursive(att: &Attachment, ret: &mut Vec<Attachment>) {
|
||||
|
@ -613,26 +589,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 {
|
||||
|
@ -652,31 +626,21 @@ impl Attachment {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
matches!(
|
||||
self.content_type,
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Encrypted,
|
||||
..
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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 {
|
||||
let mut ret = String::with_capacity(2 * self.raw.len());
|
||||
fn into_raw_helper(a: &Attachment, ret: &mut String) {
|
||||
ret.push_str(&format!(
|
||||
"Content-Transfer-Encoding: {}\r\n",
|
||||
"Content-Transfer-Encoding: {}\n",
|
||||
a.content_transfer_encoding
|
||||
));
|
||||
match &a.content_type {
|
||||
|
@ -692,17 +656,17 @@ 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("\"");
|
||||
}
|
||||
}
|
||||
|
||||
ret.push_str("\r\n\r\n");
|
||||
ret.push_str("\n\n");
|
||||
ret.push_str(&String::from_utf8_lossy(a.body()));
|
||||
}
|
||||
ContentType::Multipart {
|
||||
|
@ -715,36 +679,36 @@ impl Attachment {
|
|||
if *kind == MultipartType::Signed {
|
||||
ret.push_str("; micalg=pgp-sha512; protocol=\"application/pgp-signature\"");
|
||||
}
|
||||
ret.push_str("\r\n");
|
||||
ret.push('\n');
|
||||
|
||||
let boundary_start = format!("\r\n--{}\r\n", boundary);
|
||||
let boundary_start = format!("\n--{}\n", boundary);
|
||||
for p in parts {
|
||||
ret.push_str(&boundary_start);
|
||||
into_raw_helper(p, ret);
|
||||
}
|
||||
ret.push_str(&format!("--{}--\r\n\r\n", boundary));
|
||||
ret.push_str(&format!("--{}--\n\n", boundary));
|
||||
}
|
||||
ContentType::MessageRfc822 => {
|
||||
ret.push_str(&format!("Content-Type: {}\r\n\r\n", a.content_type));
|
||||
ret.push_str(&format!("Content-Type: {}\n\n", a.content_type));
|
||||
ret.push_str(&String::from_utf8_lossy(a.body()));
|
||||
}
|
||||
ContentType::CMSSignature | ContentType::PGPSignature => {
|
||||
ret.push_str(&format!("Content-Type: {}\r\n\r\n", a.content_type));
|
||||
ContentType::PGPSignature => {
|
||||
ret.push_str(&format!("Content-Type: {}\n\n", a.content_type));
|
||||
ret.push_str(&String::from_utf8_lossy(a.body()));
|
||||
}
|
||||
ContentType::OctetStream { ref name } => {
|
||||
if let Some(name) = name {
|
||||
ret.push_str(&format!(
|
||||
"Content-Type: {}; name={}\r\n\r\n",
|
||||
"Content-Type: {}; name={}\n\n",
|
||||
a.content_type, name
|
||||
));
|
||||
} else {
|
||||
ret.push_str(&format!("Content-Type: {}\r\n\r\n", a.content_type));
|
||||
ret.push_str(&format!("Content-Type: {}\n\n", a.content_type));
|
||||
}
|
||||
ret.push_str(BASE64_MIME.encode(a.body()).trim());
|
||||
ret.push_str(&BASE64_MIME.encode(a.body()).trim());
|
||||
}
|
||||
_ => {
|
||||
ret.push_str(&format!("Content-Type: {}\r\n\r\n", a.content_type));
|
||||
ret.push_str(&format!("Content-Type: {}\n\n", a.content_type));
|
||||
ret.push_str(&String::from_utf8_lossy(a.body()));
|
||||
}
|
||||
}
|
||||
|
@ -761,8 +725,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;
|
||||
}
|
||||
|
@ -784,143 +751,115 @@ impl Attachment {
|
|||
h.eq_ignore_ascii_case(b"name") | h.eq_ignore_ascii_case(b"filename")
|
||||
})
|
||||
.map(|(_, v)| String::from_utf8_lossy(v).to_string()),
|
||||
ContentType::Other { .. } | ContentType::OctetStream { .. } => {
|
||||
self.content_type.name().map(|s| s.to_string())
|
||||
}
|
||||
ContentType::Other { name, .. } | ContentType::OctetStream { name, .. } => name.clone(),
|
||||
_ => None,
|
||||
})
|
||||
.map(|s| {
|
||||
crate::email::parser::encodings::phrase(s.as_bytes(), false)
|
||||
.map(|(_, v)| v)
|
||||
.ok()
|
||||
.and_then(|n| String::from_utf8(n).ok())
|
||||
.unwrap_or(s)
|
||||
})
|
||||
.map(|n| n.replace(|c| std::path::is_separator(c) || c.is_ascii_control(), "_"))
|
||||
}
|
||||
|
||||
fn decode_rec_helper<'a, 'b>(&'a self, options: &mut DecodeOptions<'b>) -> Vec<u8> {
|
||||
match self.content_type {
|
||||
ContentType::Other { .. } => Vec::new(),
|
||||
ContentType::Text { .. } => self.decode_helper(options),
|
||||
ContentType::OctetStream { ref name } => name
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.mime_type())
|
||||
.into_bytes(),
|
||||
ContentType::CMSSignature | ContentType::PGPSignature => Vec::new(),
|
||||
ContentType::MessageRfc822 => {
|
||||
if self.content_disposition.kind.is_inline() {
|
||||
AttachmentBuilder::new(self.body())
|
||||
.build()
|
||||
.decode_rec_helper(options)
|
||||
} else {
|
||||
b"message/rfc822 attachment".to_vec()
|
||||
}
|
||||
}
|
||||
ContentType::Multipart {
|
||||
ref kind,
|
||||
ref parts,
|
||||
..
|
||||
} => match kind {
|
||||
MultipartType::Alternative => {
|
||||
for a in parts {
|
||||
if let ContentType::Text {
|
||||
kind: Text::Plain, ..
|
||||
} = a.content_type
|
||||
{
|
||||
return a.decode_helper(options);
|
||||
}
|
||||
}
|
||||
self.decode_helper(options)
|
||||
}
|
||||
MultipartType::Signed => {
|
||||
let mut vec = Vec::new();
|
||||
for a in parts {
|
||||
vec.extend(a.decode_rec_helper(options));
|
||||
}
|
||||
vec.extend(self.decode_helper(options));
|
||||
vec
|
||||
}
|
||||
MultipartType::Encrypted => {
|
||||
let mut vec = Vec::new();
|
||||
for a in parts {
|
||||
if a.content_type == "application/octet-stream" {
|
||||
vec.extend(a.decode_rec_helper(options));
|
||||
}
|
||||
}
|
||||
vec.extend(self.decode_helper(options));
|
||||
vec
|
||||
}
|
||||
_ => {
|
||||
let mut vec = Vec::new();
|
||||
for a in parts {
|
||||
if a.content_disposition.kind.is_inline() {
|
||||
vec.extend(a.decode_rec_helper(options));
|
||||
}
|
||||
}
|
||||
vec
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode_rec<'a, 'b>(&'a self, mut options: DecodeOptions<'b>) -> Vec<u8> {
|
||||
self.decode_rec_helper(&mut options)
|
||||
}
|
||||
|
||||
fn decode_helper<'a, 'b>(&'a self, options: &mut DecodeOptions<'b>) -> Vec<u8> {
|
||||
let charset = options
|
||||
.force_charset
|
||||
.unwrap_or_else(|| match self.content_type {
|
||||
ContentType::Text { charset, .. } => charset,
|
||||
_ => Default::default(),
|
||||
});
|
||||
|
||||
let bytes = match self.content_transfer_encoding {
|
||||
ContentTransferEncoding::Base64 => match BASE64_MIME.decode(self.body()) {
|
||||
Ok(v) => v,
|
||||
_ => self.body().to_vec(),
|
||||
},
|
||||
ContentTransferEncoding::QuotedPrintable => {
|
||||
parser::encodings::quoted_printable_bytes(self.body())
|
||||
.unwrap()
|
||||
.1
|
||||
}
|
||||
ContentTransferEncoding::_7Bit
|
||||
| ContentTransferEncoding::_8Bit
|
||||
| ContentTransferEncoding::Other { .. } => self.body().to_vec(),
|
||||
};
|
||||
|
||||
let mut ret = if self.content_type.is_text() {
|
||||
if let Ok(v) = parser::encodings::decode_charset(&bytes, charset) {
|
||||
v.into_bytes()
|
||||
} else {
|
||||
self.body().to_vec()
|
||||
}
|
||||
} else {
|
||||
bytes.to_vec()
|
||||
};
|
||||
|
||||
if let Some(filter) = options.filter.as_mut() {
|
||||
filter(self, &mut ret);
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn decode<'a, 'b>(&'a self, mut options: DecodeOptions<'b>) -> Vec<u8> {
|
||||
self.decode_helper(&mut options)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn interpret_format_flowed(_t: &str) -> String {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub type Filter<'a> = Box<dyn FnMut(&Attachment, &mut Vec<u8>) + 'a>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DecodeOptions<'att> {
|
||||
pub filter: Option<Filter<'att>>,
|
||||
pub force_charset: Option<Charset>,
|
||||
fn decode_rfc822(_raw: &[u8]) -> Attachment {
|
||||
// FIXME
|
||||
let builder = AttachmentBuilder::new(b"message/rfc822 cannot be displayed");
|
||||
builder.build()
|
||||
}
|
||||
|
||||
type Filter<'a> = Box<dyn FnMut(&'a Attachment, &mut Vec<u8>) -> () + 'a>;
|
||||
|
||||
fn decode_rec_helper<'a>(a: &'a Attachment, filter: &mut Option<Filter<'a>>) -> 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::PGPSignature => Vec::new(),
|
||||
ContentType::MessageRfc822 => {
|
||||
let temp = decode_rfc822(a.body());
|
||||
decode_rec(&temp, None)
|
||||
}
|
||||
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
|
||||
}
|
||||
_ => {
|
||||
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>(a: &'a Attachment, mut filter: Option<Filter<'a>>) -> Vec<u8> {
|
||||
decode_rec_helper(a, &mut filter)
|
||||
}
|
||||
|
||||
fn decode_helper<'a>(a: &'a Attachment, filter: &mut Option<Filter<'a>>) -> 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>(a: &'a Attachment, mut filter: Option<Filter<'a>>) -> Vec<u8> {
|
||||
decode_helper(a, &mut filter)
|
||||
}
|
||||
|
|
|
@ -19,18 +19,17 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*! Compose a `Draft`, with MIME and attachment support */
|
||||
use super::*;
|
||||
use crate::email::attachment_types::{
|
||||
Charset, ContentTransferEncoding, ContentType, MultipartType,
|
||||
};
|
||||
use crate::email::attachments::AttachmentBuilder;
|
||||
use crate::email::attachments::{decode, decode_rec, AttachmentBuilder};
|
||||
use crate::shellexpand::ShellExpandTrait;
|
||||
use data_encoding::BASE64_MIME;
|
||||
use std::ffi::OsStr;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::str;
|
||||
use xdg_utils::query_mime_info;
|
||||
|
||||
pub mod mime;
|
||||
|
@ -44,7 +43,6 @@ use super::parser;
|
|||
pub struct Draft {
|
||||
pub headers: HeaderMap,
|
||||
pub body: String,
|
||||
pub wrap_header_preamble: Option<(String, String)>,
|
||||
|
||||
pub attachments: Vec<AttachmentBuilder>,
|
||||
}
|
||||
|
@ -54,11 +52,7 @@ impl Default for Draft {
|
|||
let mut headers = HeaderMap::default();
|
||||
headers.insert(
|
||||
HeaderName::new_unchecked("Date"),
|
||||
crate::datetime::timestamp_to_string(
|
||||
crate::datetime::now(),
|
||||
Some(crate::datetime::RFC822_DATE),
|
||||
true,
|
||||
),
|
||||
crate::datetime::timestamp_to_string(crate::datetime::now(), None),
|
||||
);
|
||||
headers.insert(HeaderName::new_unchecked("From"), "".into());
|
||||
headers.insert(HeaderName::new_unchecked("To"), "".into());
|
||||
|
@ -69,14 +63,13 @@ impl Default for Draft {
|
|||
Draft {
|
||||
headers,
|
||||
body: String::new(),
|
||||
wrap_header_preamble: None,
|
||||
|
||||
attachments: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Draft {
|
||||
impl str::FromStr for Draft {
|
||||
type Err = MeliError;
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
if s.is_empty() {
|
||||
|
@ -92,7 +85,7 @@ impl FromStr for Draft {
|
|||
}
|
||||
let body = Envelope::new(0).body_bytes(s.as_bytes());
|
||||
|
||||
ret.body = String::from_utf8(body.decode(Default::default()))?;
|
||||
ret.body = String::from_utf8(decode(&body, None))?;
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
@ -101,7 +94,7 @@ impl FromStr for Draft {
|
|||
impl Draft {
|
||||
pub fn edit(envelope: &Envelope, bytes: &[u8]) -> Result<Self> {
|
||||
let mut ret = Draft::default();
|
||||
for (k, v) in envelope.headers(bytes).unwrap_or_else(|_| Vec::new()) {
|
||||
for (k, v) in envelope.headers(&bytes).unwrap_or_else(|_| Vec::new()) {
|
||||
ret.headers.insert(k.try_into()?, v.into());
|
||||
}
|
||||
|
||||
|
@ -116,39 +109,6 @@ impl Draft {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn set_wrap_header_preamble(&mut self, value: Option<(String, String)>) -> &mut Self {
|
||||
self.wrap_header_preamble = value;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn update(&mut self, value: &str) -> Result<bool> {
|
||||
let mut value: std::borrow::Cow<'_, str> = value.into();
|
||||
if let Some((pre, post)) = self.wrap_header_preamble.as_ref() {
|
||||
let mut s = value.as_ref();
|
||||
s = s.strip_prefix(pre).unwrap_or(s);
|
||||
s = s.strip_prefix('\n').unwrap_or(s);
|
||||
|
||||
if let Some(pos) = s.find(post) {
|
||||
let mut headers = &s[..pos];
|
||||
headers = headers.strip_suffix(post).unwrap_or(headers);
|
||||
if headers.ends_with('\n') {
|
||||
headers = &headers[..headers.len() - 1];
|
||||
}
|
||||
value = format!(
|
||||
"{headers}{body}",
|
||||
headers = headers,
|
||||
body = &s[pos + post.len()..]
|
||||
)
|
||||
.into();
|
||||
}
|
||||
}
|
||||
let new = Draft::from_str(value.as_ref())?;
|
||||
let changes: bool = self.headers != new.headers || self.body != new.body;
|
||||
self.headers = new.headers;
|
||||
self.body = new.body;
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
pub fn new_reply(envelope: &Envelope, bytes: &[u8], reply_to_all: bool) -> Self {
|
||||
let mut ret = Draft::default();
|
||||
ret.headers_mut().insert(
|
||||
|
@ -179,6 +139,22 @@ impl Draft {
|
|||
if let Some(reply_to) = envelope.other_headers().get("Mail-Followup-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::new_unchecked("To"), reply_to.to_string());
|
||||
} else {
|
||||
if let Some(reply_to) = envelope.other_headers().get("Reply-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::new_unchecked("To"), reply_to.to_string());
|
||||
} else {
|
||||
ret.headers_mut().insert(
|
||||
HeaderName::new_unchecked("To"),
|
||||
envelope.field_from_to_string(),
|
||||
);
|
||||
}
|
||||
// FIXME: add To/Cc
|
||||
}
|
||||
} else {
|
||||
if let Some(reply_to) = envelope.other_headers().get("Mail-Reply-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::new_unchecked("To"), reply_to.to_string());
|
||||
} else if let Some(reply_to) = envelope.other_headers().get("Reply-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::new_unchecked("To"), reply_to.to_string());
|
||||
|
@ -188,18 +164,6 @@ impl Draft {
|
|||
envelope.field_from_to_string(),
|
||||
);
|
||||
}
|
||||
// FIXME: add To/Cc
|
||||
} else if let Some(reply_to) = envelope.other_headers().get("Mail-Reply-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::new_unchecked("To"), reply_to.to_string());
|
||||
} else if let Some(reply_to) = envelope.other_headers().get("Reply-To") {
|
||||
ret.headers_mut()
|
||||
.insert(HeaderName::new_unchecked("To"), reply_to.to_string());
|
||||
} else {
|
||||
ret.headers_mut().insert(
|
||||
HeaderName::new_unchecked("To"),
|
||||
envelope.field_from_to_string(),
|
||||
);
|
||||
}
|
||||
ret.headers_mut().insert(
|
||||
HeaderName::new_unchecked("Cc"),
|
||||
|
@ -207,7 +171,7 @@ impl Draft {
|
|||
);
|
||||
let body = envelope.body_bytes(bytes);
|
||||
ret.body = {
|
||||
let reply_body_bytes = body.decode_rec(Default::default());
|
||||
let reply_body_bytes = decode_rec(&body, None);
|
||||
let reply_body = String::from_utf8_lossy(&reply_body_bytes);
|
||||
let lines: Vec<&str> = reply_body.lines().collect();
|
||||
let mut ret = format!(
|
||||
|
@ -252,36 +216,17 @@ 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.extend(format!("{}: {}\n", k, v).chars());
|
||||
}
|
||||
|
||||
ret.push('\n');
|
||||
ret.push_str(&self.body);
|
||||
|
||||
ret
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
pub fn finalise(mut self) -> Result<String> {
|
||||
|
@ -300,9 +245,9 @@ impl Draft {
|
|||
}
|
||||
for (k, v) in self.headers.deref() {
|
||||
if v.is_ascii() {
|
||||
ret.push_str(&format!("{}: {}\r\n", k, v));
|
||||
ret.extend(format!("{}: {}\r\n", k, v).chars());
|
||||
} else {
|
||||
ret.push_str(&format!("{}: {}\r\n", k, mime::encode_header(v)));
|
||||
ret.extend(format!("{}: {}\r\n", k, mime::encode_header(v)).chars());
|
||||
}
|
||||
}
|
||||
ret.push_str("MIME-Version: 1.0\r\n");
|
||||
|
@ -310,25 +255,22 @@ impl Draft {
|
|||
if self.attachments.is_empty() {
|
||||
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.extend(format!("Content-Type: {}; charset=\"utf-8\"\r\n", content_type).chars());
|
||||
ret.extend(
|
||||
format!(
|
||||
"Content-Transfer-Encoding: {}\r\n",
|
||||
content_transfer_encoding
|
||||
)
|
||||
.chars(),
|
||||
);
|
||||
ret.push_str("\r\n");
|
||||
for line in self.body.lines() {
|
||||
ret.push_str(line);
|
||||
ret.push_str("\r\n");
|
||||
}
|
||||
ret.push_str(&self.body);
|
||||
} else if self.body.is_empty() && self.attachments.len() == 1 {
|
||||
let attachment = std::mem::take(&mut self.attachments).remove(0);
|
||||
print_attachment(&mut ret, attachment);
|
||||
let attachment = std::mem::replace(&mut self.attachments, Vec::new()).remove(0);
|
||||
print_attachment(&mut ret, &Default::default(), 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());
|
||||
|
@ -344,28 +286,28 @@ impl Draft {
|
|||
|
||||
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="{}""#,
|
||||
kind, boundary
|
||||
));
|
||||
if kind == MultipartType::Encrypted {
|
||||
ret.push_str(r#"; protocol="application/pgp-encrypted""#);
|
||||
}
|
||||
ret.push_str("\r\n\r\n");
|
||||
ret.extend(
|
||||
format!(
|
||||
"Content-Type: {}; charset=\"utf-8\"; boundary=\"{}\"\r\n",
|
||||
kind, boundary
|
||||
)
|
||||
.chars(),
|
||||
);
|
||||
ret.push_str("\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");
|
||||
for sub in parts {
|
||||
ret.push_str("--");
|
||||
ret.push_str(&boundary);
|
||||
ret.push_str("\r\n");
|
||||
print_attachment(ret, sub);
|
||||
print_attachment(ret, &kind, sub);
|
||||
}
|
||||
ret.push_str("--");
|
||||
ret.push_str(&boundary);
|
||||
ret.push_str("--\r\n");
|
||||
ret.push_str("--\n");
|
||||
}
|
||||
|
||||
fn print_attachment(ret: &mut String, a: AttachmentBuilder) {
|
||||
fn print_attachment(ret: &mut String, kind: &MultipartType, a: AttachmentBuilder) {
|
||||
use ContentType::*;
|
||||
match a.content_type {
|
||||
ContentType::Text {
|
||||
|
@ -374,91 +316,63 @@ fn print_attachment(ret: &mut String, a: AttachmentBuilder) {
|
|||
parameters: ref v,
|
||||
} if v.is_empty() => {
|
||||
ret.push_str("\r\n");
|
||||
for line in String::from_utf8_lossy(a.raw()).lines() {
|
||||
ret.push_str(line);
|
||||
ret.push_str("\r\n");
|
||||
}
|
||||
ret.push_str(&String::from_utf8_lossy(a.raw()));
|
||||
ret.push_str("\r\n");
|
||||
}
|
||||
Text { .. } => {
|
||||
for line in a.build().into_raw().lines() {
|
||||
ret.push_str(line);
|
||||
ret.push_str("\r\n");
|
||||
}
|
||||
ret.push_str(&a.build().into_raw());
|
||||
ret.push_str("\r\n");
|
||||
}
|
||||
Multipart {
|
||||
boundary: _,
|
||||
boundary: _boundary,
|
||||
kind,
|
||||
parts,
|
||||
parts: subparts,
|
||||
} => {
|
||||
build_multipart(
|
||||
ret,
|
||||
kind,
|
||||
parts
|
||||
subparts
|
||||
.into_iter()
|
||||
.map(|s| s.into())
|
||||
.collect::<Vec<AttachmentBuilder>>(),
|
||||
);
|
||||
ret.push_str("\r\n");
|
||||
}
|
||||
MessageRfc822 => {
|
||||
ret.push_str(&format!(
|
||||
"Content-Type: {}; charset=\"utf-8\"\r\n",
|
||||
a.content_type
|
||||
));
|
||||
MessageRfc822 | PGPSignature => {
|
||||
ret.push_str(&format!("Content-Type: {}; charset=\"utf-8\"\r\n", kind));
|
||||
ret.push_str("Content-Disposition: attachment\r\n");
|
||||
ret.push_str("\r\n");
|
||||
for line in String::from_utf8_lossy(a.raw()).lines() {
|
||||
ret.push_str(line);
|
||||
ret.push_str("\r\n");
|
||||
}
|
||||
}
|
||||
PGPSignature => {
|
||||
ret.push_str(&format!(
|
||||
"Content-Type: {}; charset=\"utf-8\"; name=\"signature.asc\"\r\n",
|
||||
a.content_type
|
||||
));
|
||||
ret.push_str("Content-Description: Digital signature\r\n");
|
||||
ret.push_str("Content-Disposition: inline\r\n");
|
||||
ret.push_str(&String::from_utf8_lossy(a.raw()));
|
||||
ret.push_str("\r\n");
|
||||
for line in String::from_utf8_lossy(a.raw()).lines() {
|
||||
ret.push_str(line);
|
||||
ret.push_str("\r\n");
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let content_transfer_encoding: ContentTransferEncoding = if a.raw().is_ascii() {
|
||||
ContentTransferEncoding::_8Bit
|
||||
} else {
|
||||
ContentTransferEncoding::Base64
|
||||
};
|
||||
let content_transfer_encoding: ContentTransferEncoding =
|
||||
ContentTransferEncoding::Base64;
|
||||
if let Some(name) = a.content_type().name() {
|
||||
ret.push_str(&format!(
|
||||
"Content-Type: {}; name=\"{}\"; charset=\"utf-8\"\r\n",
|
||||
a.content_type(),
|
||||
name
|
||||
));
|
||||
ret.extend(
|
||||
format!(
|
||||
"Content-Type: {}; name=\"{}\"; charset=\"utf-8\"\r\n",
|
||||
a.content_type(),
|
||||
name
|
||||
)
|
||||
.chars(),
|
||||
);
|
||||
} else {
|
||||
ret.push_str(&format!(
|
||||
"Content-Type: {}; charset=\"utf-8\"\r\n",
|
||||
a.content_type()
|
||||
));
|
||||
ret.extend(
|
||||
format!("Content-Type: {}; charset=\"utf-8\"\r\n", a.content_type()).chars(),
|
||||
);
|
||||
}
|
||||
ret.push_str("Content-Disposition: attachment\r\n");
|
||||
ret.push_str(&format!(
|
||||
"Content-Transfer-Encoding: {}\r\n",
|
||||
content_transfer_encoding
|
||||
));
|
||||
ret.extend(
|
||||
format!(
|
||||
"Content-Transfer-Encoding: {}\r\n",
|
||||
content_transfer_encoding
|
||||
)
|
||||
.chars(),
|
||||
);
|
||||
ret.push_str("\r\n");
|
||||
ret.push_str(&BASE64_MIME.encode(a.raw()).trim());
|
||||
ret.push_str("\r\n");
|
||||
if content_transfer_encoding == ContentTransferEncoding::Base64 {
|
||||
for line in BASE64_MIME.encode(a.raw()).trim().lines() {
|
||||
ret.push_str(line);
|
||||
ret.push_str("\r\n");
|
||||
}
|
||||
} else {
|
||||
for line in String::from_utf8_lossy(a.raw()).lines() {
|
||||
ret.push_str(line);
|
||||
ret.push_str("\r\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -469,69 +383,27 @@ mod tests {
|
|||
use std::str::FromStr;
|
||||
|
||||
#[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("Subject", "test_update()".into())
|
||||
.set_header("Date", "Sun, 16 Jun 2013 17:56:45 +0200".into());
|
||||
|
||||
let original = default.clone();
|
||||
let s = default.to_edit_string();
|
||||
assert_eq!(s, "<!--\nDate: Sun, 16 Jun 2013 17:56:45 +0200\nFrom: \nTo: \nCc: \nBcc: \nSubject: test_update()\n-->\n\nαδφαφσαφασ");
|
||||
assert!(!default.update(&s).unwrap());
|
||||
assert_eq!(&original, &default);
|
||||
|
||||
default.set_wrap_header_preamble(Some(("".to_string(), "".to_string())));
|
||||
let original = default.clone();
|
||||
let s = default.to_edit_string();
|
||||
assert_eq!(s, "Date: Sun, 16 Jun 2013 17:56:45 +0200\nFrom: \nTo: \nCc: \nBcc: \nSubject: test_update()\n\nαδφαφσαφασ");
|
||||
assert!(!default.update(&s).unwrap());
|
||||
assert_eq!(&original, &default);
|
||||
|
||||
default.set_wrap_header_preamble(None);
|
||||
let original = default.clone();
|
||||
let s = default.to_edit_string();
|
||||
assert_eq!(s, "Date: Sun, 16 Jun 2013 17:56:45 +0200\nFrom: \nTo: \nCc: \nBcc: \nSubject: test_update()\n\nαδφαφσαφασ");
|
||||
assert!(!default.update(&s).unwrap());
|
||||
assert_eq!(&original, &default);
|
||||
|
||||
default.set_wrap_header_preamble(Some((
|
||||
"{-\n\n\n===========".to_string(),
|
||||
"</mixed>".to_string(),
|
||||
)));
|
||||
let original = default.clone();
|
||||
let s = default.to_edit_string();
|
||||
assert_eq!(s, "{-\n\n\n===========\nDate: Sun, 16 Jun 2013 17:56:45 +0200\nFrom: \nTo: \nCc: \nBcc: \nSubject: test_update()\n</mixed>\n\nαδφαφσαφασ");
|
||||
assert!(!default.update(&s).unwrap());
|
||||
assert_eq!(&original, &default);
|
||||
|
||||
default
|
||||
.set_body(
|
||||
"hellohello<!--\n<!--\n<--hellohello\nhellohello-->\n-->\n-->hello\n".to_string(),
|
||||
)
|
||||
.set_wrap_header_preamble(Some(("<!--".to_string(), "-->".to_string())));
|
||||
let original = default.clone();
|
||||
let s = default.to_edit_string();
|
||||
assert_eq!(s, "<!--\nDate: Sun, 16 Jun 2013 17:56:45 +0200\nFrom: \nTo: \nCc: \nBcc: \nSubject: test_update()\n-->\n\nhellohello<!--\n<!--\n<--hellohello\nhellohello-->\n-->\n-->hello\n");
|
||||
assert!(!default.update(&s).unwrap());
|
||||
assert_eq!(&original, &default);
|
||||
}
|
||||
|
||||
/*
|
||||
#[test]
|
||||
fn test_attachments() {
|
||||
/*
|
||||
let mut default = Draft::default();
|
||||
default.set_body("αδφαφσαφασ".to_string());
|
||||
|
||||
|
@ -549,8 +421,8 @@ mod tests {
|
|||
.set_content_transfer_encoding(ContentTransferEncoding::Base64);
|
||||
default.attachments_mut().push(attachment);
|
||||
println!("{}", default.finalise().unwrap());
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads file from given path, and returns an 'application/octet-stream' AttachmentBuilder object
|
||||
|
|
|
@ -179,7 +179,7 @@ fn test_encode_header() {
|
|||
)
|
||||
.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",
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*! Wrapper type `HeaderName` for case-insensitive comparisons */
|
||||
use crate::error::MeliError;
|
||||
use indexmap::IndexMap;
|
||||
use smallvec::SmallVec;
|
||||
|
|
|
@ -19,18 +19,15 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*! Parsing of rfc2369/rfc2919 `List-*` headers */
|
||||
use super::parser;
|
||||
use super::Envelope;
|
||||
use smallvec::SmallVec;
|
||||
use std::convert::From;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
#[derive(Debug, Copy)]
|
||||
pub enum ListAction<'a> {
|
||||
Url(&'a [u8]),
|
||||
Email(&'a [u8]),
|
||||
///`List-Post` field may contain the special value "NO".
|
||||
No,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [u8]> for ListAction<'a> {
|
||||
|
@ -40,8 +37,6 @@ impl<'a> From<&'a [u8]> for ListAction<'a> {
|
|||
* parser::mailto() will handle this if user tries to unsubscribe.
|
||||
*/
|
||||
ListAction::Email(value)
|
||||
} 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.
|
||||
|
@ -73,6 +68,15 @@ impl<'a> ListAction<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> Clone for ListAction<'a> {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
ListAction::Url(a) => ListAction::Url(<&[u8]>::clone(a)),
|
||||
ListAction::Email(a) => ListAction::Email(<&[u8]>::clone(a)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ListActions<'a> {
|
||||
pub id: Option<&'a str>,
|
||||
|
@ -124,11 +128,6 @@ impl<'a> ListActions<'a> {
|
|||
|
||||
if let Some(post) = envelope.other_headers().get("List-Post") {
|
||||
ret.post = ListAction::parse_options_list(post.as_bytes());
|
||||
if let Some(ref l) = ret.post {
|
||||
if l.starts_with(&[ListAction::No]) {
|
||||
ret.post = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(unsubscribe) = envelope.other_headers().get("List-Unsubscribe") {
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*! Parsing of `mailto` addresses */
|
||||
use super::*;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*! Parsers for email. See submodules */
|
||||
use crate::error::{MeliError, Result, ResultIntoMeliError};
|
||||
use nom::{
|
||||
branch::alt,
|
||||
|
@ -28,7 +27,7 @@ use nom::{
|
|||
combinator::peek,
|
||||
combinator::{map, opt},
|
||||
error::{context, ErrorKind},
|
||||
multi::{many0, many1, separated_list1},
|
||||
multi::{many0, many1, separated_nonempty_list},
|
||||
number::complete::le_u8,
|
||||
sequence::{delimited, pair, preceded, separated_pair, terminated},
|
||||
};
|
||||
|
@ -49,16 +48,7 @@ pub struct ParsingError<I> {
|
|||
impl core::fmt::Debug for ParsingError<&'_ [u8]> {
|
||||
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
fmt.debug_struct("ParsingError")
|
||||
.field("input", &to_str!(self.input))
|
||||
.field("error", &self.error)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for ParsingError<&'_ str> {
|
||||
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
fmt.debug_struct("ParsingError")
|
||||
.field("input", &self.input)
|
||||
.field("input", &to_str!(&self.input))
|
||||
.field("error", &self.error)
|
||||
.finish()
|
||||
}
|
||||
|
@ -121,17 +111,6 @@ impl<I> nom::error::ParseError<I> for ParsingError<I> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<I, E> nom::error::FromExternalError<I, E> for ParsingError<I> {
|
||||
fn from_external_error(input: I, kind: ErrorKind, _e: E) -> Self {
|
||||
Self {
|
||||
input,
|
||||
error: kind.description().to_string().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> nom::error::ContextError<I> for ParsingError<I> {}
|
||||
|
||||
impl<'i> From<ParsingError<&'i [u8]>> for MeliError {
|
||||
fn from(val: ParsingError<&'i [u8]>) -> MeliError {
|
||||
MeliError::new("Parsing error").set_summary(format!(
|
||||
|
@ -175,15 +154,6 @@ impl<'i> From<nom::Err<ParsingError<&'i str>>> for MeliError {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<nom::Err<nom::error::Error<&[u8]>>> for MeliError {
|
||||
fn from(val: nom::Err<nom::error::Error<&[u8]>>) -> MeliError {
|
||||
match val {
|
||||
nom::Err::Incomplete(_) => MeliError::new("Parsing Error: Incomplete"),
|
||||
nom::Err::Error(_) | nom::Err::Failure(_) => MeliError::new("Parsing Error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! is_ctl_or_space {
|
||||
($var:ident) => {
|
||||
/* <any ASCII control character and DEL> */
|
||||
|
@ -311,7 +281,6 @@ pub fn mail(input: &[u8]) -> Result<(Vec<(&[u8], &[u8])>, &[u8])> {
|
|||
}
|
||||
|
||||
pub mod dates {
|
||||
/*! Date values in headers */
|
||||
use super::generic::*;
|
||||
use super::*;
|
||||
use crate::datetime::UnixTimestamp;
|
||||
|
@ -415,22 +384,22 @@ pub mod dates {
|
|||
accum.extend_from_slice(&day_of_week);
|
||||
accum.extend_from_slice(b", ");
|
||||
}
|
||||
accum.extend_from_slice(day);
|
||||
accum.extend_from_slice(&day);
|
||||
accum.extend_from_slice(b" ");
|
||||
accum.extend_from_slice(month);
|
||||
accum.extend_from_slice(&month);
|
||||
accum.extend_from_slice(b" ");
|
||||
accum.extend_from_slice(year);
|
||||
accum.extend_from_slice(&year);
|
||||
accum.extend_from_slice(b" ");
|
||||
accum.extend_from_slice(hour);
|
||||
accum.extend_from_slice(&hour);
|
||||
accum.extend_from_slice(b":");
|
||||
accum.extend_from_slice(minute);
|
||||
accum.extend_from_slice(&minute);
|
||||
if let Some(second) = second {
|
||||
accum.extend_from_slice(b":");
|
||||
accum.extend_from_slice(second);
|
||||
accum.extend_from_slice(&second);
|
||||
}
|
||||
accum.extend_from_slice(b" ");
|
||||
accum.extend_from_slice(sign);
|
||||
accum.extend_from_slice(zone);
|
||||
accum.extend_from_slice(&sign);
|
||||
accum.extend_from_slice(&zone);
|
||||
match crate::datetime::rfc822_to_timestamp(accum.to_vec()) {
|
||||
Ok(t) => Ok((input, t)),
|
||||
Err(_err) => Err(nom::Err::Error(
|
||||
|
@ -443,67 +412,6 @@ pub mod dates {
|
|||
}
|
||||
}
|
||||
|
||||
///e.g Wed Sep 9 00:27:54 2020
|
||||
///```text
|
||||
///day-of-week month day time year
|
||||
///date-time = [ day-of-week "," ] date time [CFWS]
|
||||
///date = day month year
|
||||
///time = time-of-day zone
|
||||
///time-of-day = hour ":" minute [ ":" second ]
|
||||
///hour = 2DIGIT / obs-hour
|
||||
///minute = 2DIGIT / obs-minute
|
||||
///second = 2DIGIT / obs-second
|
||||
///```
|
||||
pub fn mbox_date_time(input: &[u8]) -> IResult<&[u8], UnixTimestamp> {
|
||||
let orig_input = input;
|
||||
let mut accum: SmallVec<[u8; 32]> = SmallVec::new();
|
||||
let (input, _) = opt(cfws)(input)?;
|
||||
let (input, day_of_week) = day_of_week(input)?;
|
||||
let (input, _) = opt(cfws)(input)?;
|
||||
let (input, month) = month(input)?;
|
||||
let (input, _) = opt(cfws)(input)?;
|
||||
let (input, day) = day(input)?;
|
||||
let (input, _) = opt(cfws)(input)?;
|
||||
let (input, hour) = take_n_digits(2)(input)?;
|
||||
let (input, _) = tag(":")(input)?;
|
||||
let (input, minute) = take_n_digits(2)(input)?;
|
||||
let (input, second) = opt(preceded(tag(":"), take_n_digits(2)))(input)?;
|
||||
let (input, _) = fws(input)?;
|
||||
let (input, zone) = opt(zone)(input)?;
|
||||
let (input, _) = opt(cfws)(input)?;
|
||||
let (input, year) = year(input)?;
|
||||
accum.extend_from_slice(&day_of_week);
|
||||
accum.extend_from_slice(b", ");
|
||||
accum.extend_from_slice(day);
|
||||
accum.extend_from_slice(b" ");
|
||||
accum.extend_from_slice(month);
|
||||
accum.extend_from_slice(b" ");
|
||||
accum.extend_from_slice(year);
|
||||
accum.extend_from_slice(b" ");
|
||||
accum.extend_from_slice(hour);
|
||||
accum.extend_from_slice(b":");
|
||||
accum.extend_from_slice(minute);
|
||||
if let Some(second) = second {
|
||||
accum.extend_from_slice(b":");
|
||||
accum.extend_from_slice(second);
|
||||
}
|
||||
if let Some((sign, zone)) = zone {
|
||||
accum.extend_from_slice(b" ");
|
||||
accum.extend_from_slice(sign);
|
||||
accum.extend_from_slice(zone);
|
||||
}
|
||||
match crate::datetime::rfc822_to_timestamp(accum.to_vec()) {
|
||||
Ok(t) => Ok((input, t)),
|
||||
Err(_err) => Err(nom::Err::Error(
|
||||
(
|
||||
orig_input,
|
||||
"mbox_date_time(): could not convert date from rfc822",
|
||||
)
|
||||
.into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
///`day-of-week = ([FWS] day-name) / obs-day-of-week`
|
||||
///day-name = "Mon" / "Tue" / "Wed" / "Thu" /
|
||||
/// "Fri" / "Sat" / "Sun"
|
||||
|
@ -551,9 +459,9 @@ pub mod dates {
|
|||
|
||||
///year = (FWS 4*DIGIT FWS) / obs-year
|
||||
fn year(input: &[u8]) -> IResult<&[u8], &[u8]> {
|
||||
let (input, _) = opt(fws)(input)?;
|
||||
let (input, _) = fws(input)?;
|
||||
let (input, ret) = take_n_digits(4)(input)?;
|
||||
let (input, _) = opt(fws)(input)?;
|
||||
let (input, _) = fws(input)?;
|
||||
Ok((input, ret))
|
||||
}
|
||||
|
||||
|
@ -572,17 +480,6 @@ pub mod dates {
|
|||
};
|
||||
Ok((rest, ret))
|
||||
})
|
||||
.or_else(|_| {
|
||||
let (rest, ret) = match mbox_date_time(input) {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
return Err(nom::Err::Error(
|
||||
(input, "rfc5322_date(): invalid input").into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
Ok((rest, ret))
|
||||
})
|
||||
.map(|(_, r)| r)
|
||||
.map_err(|err: nom::Err<ParsingError<_>>| err.into())
|
||||
/*
|
||||
|
@ -604,13 +501,10 @@ pub mod dates {
|
|||
assert_eq!(rfc5322_date(_s).unwrap(), rfc5322_date(__s).unwrap());
|
||||
let val = b"Fri, 23 Dec 0001 21:20:36 -0800 (PST)";
|
||||
assert_eq!(rfc5322_date(val).unwrap(), 0);
|
||||
let val = b"Wed Sep 9 00:27:54 2020";
|
||||
assert_eq!(rfc5322_date(val).unwrap(), 1599611274);
|
||||
}
|
||||
}
|
||||
|
||||
pub mod generic {
|
||||
/*! Generally useful parser combinators */
|
||||
use super::*;
|
||||
#[inline(always)]
|
||||
pub fn byte_in_slice<'a>(slice: &'static [u8]) -> impl Fn(&'a [u8]) -> IResult<&'a [u8], u8> {
|
||||
|
@ -863,12 +757,12 @@ pub mod generic {
|
|||
|input| {
|
||||
let (input, pr) = many1(terminated(opt(fws), comment))(input)?;
|
||||
let (input, end) = opt(fws)(input)?;
|
||||
let mut pr = pr.into_iter().flatten().fold(vec![], |mut acc, x| {
|
||||
let mut pr = pr.into_iter().filter_map(|s| s).fold(vec![], |mut acc, x| {
|
||||
acc.extend_from_slice(&x);
|
||||
acc
|
||||
});
|
||||
if pr.is_empty() {
|
||||
Ok((input, end.unwrap_or_else(|| (&b""[..]).into())))
|
||||
Ok((input, end.unwrap_or((&b""[..]).into())))
|
||||
} else {
|
||||
if let Some(end) = end {
|
||||
pr.extend_from_slice(&end);
|
||||
|
@ -1221,7 +1115,7 @@ pub mod generic {
|
|||
{
|
||||
Ok((&input[1..], input[0..1].into()))
|
||||
} else {
|
||||
Err(nom::Err::Error((input, "atext(): invalid byte").into()))
|
||||
return Err(nom::Err::Error((input, "atext(): invalid byte").into()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1229,12 +1123,12 @@ pub mod generic {
|
|||
alt((atext_ascii, utf8_non_ascii))(input)
|
||||
}
|
||||
|
||||
///`dot-atom = [CFWS] dot-atom-text [CFWS]`
|
||||
///dot-atom = [CFWS] dot-atom-text [CFWS]
|
||||
pub fn dot_atom(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> {
|
||||
let (input, _) = opt(cfws)(input)?;
|
||||
let (input, ret) = dot_atom_text(input)?;
|
||||
let (input, _) = opt(cfws)(input)?;
|
||||
Ok((input, ret))
|
||||
Ok((input, ret.into()))
|
||||
}
|
||||
|
||||
///```text
|
||||
|
@ -1261,7 +1155,7 @@ pub mod mailing_lists {
|
|||
pub fn rfc_2369_list_headers_action_list(input: &[u8]) -> IResult<&[u8], Vec<&[u8]>> {
|
||||
let (input, _) = opt(cfws)(input)?;
|
||||
let (input, ret) = alt((
|
||||
separated_list1(
|
||||
separated_nonempty_list(
|
||||
delimited(
|
||||
map(opt(cfws), |_| ()),
|
||||
map(is_a(", "), |_| ()),
|
||||
|
@ -1278,7 +1172,7 @@ pub mod mailing_lists {
|
|||
map(tag("NO"), |_| ()),
|
||||
map(opt(cfws), |_| ()),
|
||||
),
|
||||
|_| vec![&b"NO"[..]],
|
||||
|_| vec![],
|
||||
),
|
||||
))(input)?;
|
||||
let (input, _) = opt(cfws)(input)?;
|
||||
|
@ -1303,15 +1197,14 @@ List-Archive: <http://www.host.com/list/archive/> (Web Archive)
|
|||
"#;
|
||||
let (rest, headers) = headers::headers(s.as_bytes()).unwrap();
|
||||
assert!(rest.is_empty());
|
||||
for (_h, v) in headers {
|
||||
let (rest, _action_list) = rfc_2369_list_headers_action_list(v).unwrap();
|
||||
for (h, v) in headers {
|
||||
let (rest, action_list) = rfc_2369_list_headers_action_list(v).unwrap();
|
||||
assert!(rest.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod headers {
|
||||
/*! Email headers */
|
||||
use super::*;
|
||||
|
||||
pub fn headers(input: &[u8]) -> IResult<&[u8], Vec<(&[u8], &[u8])>> {
|
||||
|
@ -1572,7 +1465,6 @@ pub mod headers {
|
|||
}
|
||||
|
||||
pub mod attachments {
|
||||
/*! Email attachments */
|
||||
use super::*;
|
||||
use crate::email::address::*;
|
||||
use crate::email::attachment_types::{ContentDisposition, ContentDispositionKind};
|
||||
|
@ -1839,7 +1731,6 @@ pub mod attachments {
|
|||
}
|
||||
|
||||
pub mod encodings {
|
||||
/*! Email encodings (quoted printable, MIME) */
|
||||
use super::*;
|
||||
use crate::email::attachment_types::Charset;
|
||||
use data_encoding::BASE64_MIME;
|
||||
|
@ -1980,36 +1871,18 @@ pub mod encodings {
|
|||
Charset::UTF8 | Charset::Ascii => Ok(String::from_utf8_lossy(s).to_string()),
|
||||
Charset::ISO8859_1 => Ok(ISO_8859_1.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::ISO8859_2 => Ok(ISO_8859_2.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::ISO8859_3 => Ok(ISO_8859_3.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::ISO8859_4 => Ok(ISO_8859_4.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::ISO8859_5 => Ok(ISO_8859_5.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::ISO8859_6 => Ok(ISO_8859_6.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::ISO8859_7 => Ok(ISO_8859_7.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::ISO8859_8 => Ok(ISO_8859_8.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::ISO8859_10 => Ok(ISO_8859_10.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::ISO8859_13 => Ok(ISO_8859_13.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::ISO8859_14 => Ok(ISO_8859_14.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::ISO8859_15 => Ok(ISO_8859_15.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::ISO8859_16 => Ok(ISO_8859_16.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::GBK => Ok(GBK.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::Windows1250 => Ok(WINDOWS_1250.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::Windows1251 => Ok(WINDOWS_1251.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::Windows1252 => Ok(WINDOWS_1252.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::Windows1253 => Ok(WINDOWS_1253.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::KOI8R => Ok(KOI8_R.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::KOI8U => Ok(KOI8_U.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::BIG5 => Ok(BIG5_2003.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::GB2312 => {
|
||||
Ok(encoding::codec::simpchinese::GBK_ENCODING.decode(s, DecoderTrap::Strict)?)
|
||||
}
|
||||
Charset::GB18030 => Ok(
|
||||
encoding::codec::simpchinese::GB18030_ENCODING.decode(s, DecoderTrap::Strict)?
|
||||
),
|
||||
Charset::UTF16 => {
|
||||
Ok(encoding::codec::utf_16::UTF_16LE_ENCODING.decode(s, DecoderTrap::Strict)?)
|
||||
}
|
||||
Charset::ISO2022JP => Ok(ISO_2022_JP.decode(s, DecoderTrap::Strict)?),
|
||||
Charset::EUCJP => Ok(EUC_JP.decode(s, DecoderTrap::Strict)?),
|
||||
// Unimplemented:
|
||||
Charset::GB2312 => Ok(String::from_utf8_lossy(s).to_string()),
|
||||
Charset::UTF16 => Ok(String::from_utf8_lossy(s).to_string()),
|
||||
Charset::BIG5 => Ok(String::from_utf8_lossy(s).to_string()),
|
||||
Charset::ISO2022JP => Ok(String::from_utf8_lossy(s).to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2057,7 +1930,7 @@ pub mod encodings {
|
|||
}
|
||||
|
||||
pub fn encoded_word_list(input: &[u8]) -> IResult<&[u8], SmallVec<[u8; 64]>> {
|
||||
let (input, list) = separated_list1(space, encoded_word)(input)?;
|
||||
let (input, list) = separated_nonempty_list(space, encoded_word)(input)?;
|
||||
let list_len = list.iter().fold(0, |mut acc, x| {
|
||||
acc += x.len();
|
||||
acc
|
||||
|
@ -2066,7 +1939,7 @@ pub mod encodings {
|
|||
input,
|
||||
list.iter()
|
||||
.fold(SmallVec::with_capacity(list_len), |mut acc, x| {
|
||||
acc.extend(x.iter().cloned());
|
||||
acc.extend(x.into_iter().cloned());
|
||||
acc
|
||||
}),
|
||||
))
|
||||
|
@ -2115,7 +1988,7 @@ pub mod encodings {
|
|||
}
|
||||
let end = input[ptr..].find(b"=?");
|
||||
|
||||
let end = end.unwrap_or(input.len() - ptr) + ptr;
|
||||
let end = end.unwrap_or_else(|| input.len() - ptr) + ptr;
|
||||
let ascii_s = ptr;
|
||||
let mut ascii_e = 0;
|
||||
|
||||
|
@ -2380,7 +2253,7 @@ pub mod address {
|
|||
///`name-addr = [display-name] angle-addr`
|
||||
pub fn name_addr(input: &[u8]) -> IResult<&[u8], Address> {
|
||||
let (input, (display_name, angle_addr)) = alt((
|
||||
pair(map(display_name, Some), angle_addr),
|
||||
pair(map(display_name, |s| Some(s)), angle_addr),
|
||||
map(angle_addr, |r| (None, r)),
|
||||
))(input)?;
|
||||
Ok((
|
||||
|
@ -2477,7 +2350,7 @@ pub mod address {
|
|||
.trim(),
|
||||
);
|
||||
if i != list_len - 1 {
|
||||
acc.push(' ');
|
||||
acc.push_str(" ");
|
||||
i += 1;
|
||||
}
|
||||
acc
|
||||
|
@ -2592,7 +2465,7 @@ pub mod address {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{address::*, encodings::*, *};
|
||||
use super::{address::*, encodings::*, generic::*, *};
|
||||
use crate::email::address::*;
|
||||
use crate::make_address;
|
||||
|
||||
|
@ -2658,12 +2531,6 @@ mod tests {
|
|||
"Re: Climate crisis reality check –\u{a0}EcoHustler",
|
||||
std::str::from_utf8(&phrase(words.as_bytes(), false).unwrap().1).unwrap()
|
||||
);
|
||||
|
||||
let words = r#"=?gb18030?B?zNrRtsbz0rXTys/k19S2r9eqt6LR6dak08q8/g==?="#;
|
||||
assert_eq!(
|
||||
"腾讯企业邮箱自动转发验证邮件",
|
||||
std::str::from_utf8(&phrase(words.as_bytes(), false).unwrap().1).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -19,17 +19,14 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*! Verification of OpenPGP signatures */
|
||||
use crate::email::parser::BytesExt;
|
||||
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
|
||||
/// ```text
|
||||
/// rfc3156
|
||||
/// Upon receipt of a signed message, an application MUST:
|
||||
///
|
||||
/// (1) Convert line endings to the canonical <CR><LF> sequence before
|
||||
|
@ -38,7 +35,7 @@ use crate::{MeliError, Result};
|
|||
/// (2) Pass both the signed data and its associated content headers
|
||||
/// along with the OpenPGP signature to the signature verification
|
||||
/// service.
|
||||
/// ```
|
||||
///
|
||||
pub fn convert_attachment_to_rfc_spec(input: &[u8]) -> Vec<u8> {
|
||||
if input.is_empty() {
|
||||
return Vec::new();
|
||||
|
@ -87,7 +84,7 @@ pub fn convert_attachment_to_rfc_spec(input: &[u8]) -> Vec<u8> {
|
|||
ret
|
||||
}
|
||||
|
||||
pub fn verify_signature(a: &Attachment) -> Result<(Vec<u8>, &Attachment)> {
|
||||
pub fn verify_signature(a: &Attachment) -> Result<(Vec<u8>, &[u8])> {
|
||||
match a.content_type {
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Signed,
|
||||
|
@ -106,10 +103,7 @@ pub fn verify_signature(a: &Attachment) -> Result<(Vec<u8>, &Attachment)> {
|
|||
let signed_part: Vec<u8> = if let Some(v) = parts
|
||||
.iter()
|
||||
.zip(part_boundaries.iter())
|
||||
.find(|(p, _)| {
|
||||
p.content_type != ContentType::PGPSignature
|
||||
&& p.content_type != ContentType::CMSSignature
|
||||
})
|
||||
.find(|(p, _)| p.content_type != ContentType::PGPSignature)
|
||||
.map(|(_, s)| convert_attachment_to_rfc_spec(s.display_bytes(a.body())))
|
||||
{
|
||||
v
|
||||
|
@ -118,11 +112,12 @@ pub fn verify_signature(a: &Attachment) -> Result<(Vec<u8>, &Attachment)> {
|
|||
"multipart/signed attachment without a signed part".to_string(),
|
||||
));
|
||||
};
|
||||
let signature = if let Some(sig) = parts.iter().find(|s| {
|
||||
s.content_type == ContentType::PGPSignature
|
||||
|| s.content_type == ContentType::CMSSignature
|
||||
}) {
|
||||
sig
|
||||
let signature = if let Some(sig) = parts
|
||||
.iter()
|
||||
.find(|s| s.content_type == ContentType::PGPSignature)
|
||||
.map(|a| a.body())
|
||||
{
|
||||
sig.trim()
|
||||
} else {
|
||||
return Err(MeliError::new(
|
||||
"multipart/signed attachment without a signature part".to_string(),
|
||||
|
@ -130,29 +125,8 @@ pub fn verify_signature(a: &Attachment) -> Result<(Vec<u8>, &Attachment)> {
|
|||
};
|
||||
Ok((signed_part, signature))
|
||||
}
|
||||
_ => Err(MeliError::new(
|
||||
"Should not give non-signed attachments to this function",
|
||||
)),
|
||||
_ => {
|
||||
unreachable!("Should not give non-signed attachments to this function");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DecryptionMetadata {
|
||||
pub recipients: Vec<Recipient>,
|
||||
pub file_name: Option<String>,
|
||||
pub session_key: Option<String>,
|
||||
pub is_mime: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Recipient {
|
||||
pub keyid: Option<String>,
|
||||
pub status: Result<()>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SignatureMetadata {
|
||||
pub signatures: Vec<Recipient>,
|
||||
pub file_name: Option<String>,
|
||||
pub is_mime: bool,
|
||||
}
|
|
@ -34,296 +34,14 @@ use std::sync::Arc;
|
|||
|
||||
pub type Result<T> = result::Result<T, MeliError>;
|
||||
|
||||
#[derive(Debug, Copy, PartialEq, Clone)]
|
||||
pub enum NetworkErrorKind {
|
||||
/// Unspecified
|
||||
None,
|
||||
/// Name lookup of host failed.
|
||||
HostLookupFailed,
|
||||
/// Bad client Certificate
|
||||
BadClientCertificate,
|
||||
/// Bad server certificate
|
||||
BadServerCertificate,
|
||||
/// Client initialization
|
||||
ClientInitialization,
|
||||
/// Connection failed
|
||||
ConnectionFailed,
|
||||
/// Invalid content encoding
|
||||
InvalidContentEncoding,
|
||||
/// Invalid credentials
|
||||
InvalidCredentials,
|
||||
/// Invalid request
|
||||
InvalidRequest,
|
||||
/// IO Error
|
||||
Io,
|
||||
/// Name resolution
|
||||
NameResolution,
|
||||
/// Protocol violation
|
||||
ProtocolViolation,
|
||||
/// Request body not rewindable
|
||||
RequestBodyNotRewindable,
|
||||
/// Connection (not request) timeout.
|
||||
Timeout,
|
||||
/// TooManyRedirects
|
||||
TooManyRedirects,
|
||||
/// Invalid TLS connection
|
||||
InvalidTLSConnection,
|
||||
/// Equivalent to HTTP status code 400 Bad Request
|
||||
/// [[RFC7231, Section 6.5.1](https://tools.ietf.org/html/rfc7231#section-6.5.1)]
|
||||
BadRequest,
|
||||
/// Equivalent to HTTP status code 401 Unauthorized
|
||||
/// [[RFC7235, Section 3.1](https://tools.ietf.org/html/rfc7235#section-3.1)]
|
||||
Unauthorized,
|
||||
/// Equivalent to HTTP status code 402 Payment Required
|
||||
/// [[RFC7231, Section 6.5.2](https://tools.ietf.org/html/rfc7231#section-6.5.2)]
|
||||
PaymentRequired,
|
||||
/// Equivalent to HTTP status code 403 Forbidden
|
||||
/// [[RFC7231, Section 6.5.3](https://tools.ietf.org/html/rfc7231#section-6.5.3)]
|
||||
Forbidden,
|
||||
/// Equivalent to HTTP status code 404 Not Found
|
||||
/// [[RFC7231, Section 6.5.4](https://tools.ietf.org/html/rfc7231#section-6.5.4)]
|
||||
NotFound,
|
||||
/// Equivalent to HTTP status code 405 Method Not Allowed
|
||||
/// [[RFC7231, Section 6.5.5](https://tools.ietf.org/html/rfc7231#section-6.5.5)]
|
||||
MethodNotAllowed,
|
||||
/// Equivalent to HTTP status code 406 Not Acceptable
|
||||
/// [[RFC7231, Section 6.5.6](https://tools.ietf.org/html/rfc7231#section-6.5.6)]
|
||||
NotAcceptable,
|
||||
/// Equivalent to HTTP status code 407 Proxy Authentication Required
|
||||
/// [[RFC7235, Section 3.2](https://tools.ietf.org/html/rfc7235#section-3.2)]
|
||||
ProxyAuthenticationRequired,
|
||||
/// Equivalent to HTTP status code 408 Request Timeout
|
||||
/// [[RFC7231, Section 6.5.7](https://tools.ietf.org/html/rfc7231#section-6.5.7)]
|
||||
RequestTimeout,
|
||||
/// Equivalent to HTTP status code 409 Conflict
|
||||
/// [[RFC7231, Section 6.5.8](https://tools.ietf.org/html/rfc7231#section-6.5.8)]
|
||||
Conflict,
|
||||
/// Equivalent to HTTP status code 410 Gone
|
||||
/// [[RFC7231, Section 6.5.9](https://tools.ietf.org/html/rfc7231#section-6.5.9)]
|
||||
Gone,
|
||||
/// Equivalent to HTTP status code 411 Length Required
|
||||
/// [[RFC7231, Section 6.5.10](https://tools.ietf.org/html/rfc7231#section-6.5.10)]
|
||||
LengthRequired,
|
||||
/// Equivalent to HTTP status code 412 Precondition Failed
|
||||
/// [[RFC7232, Section 4.2](https://tools.ietf.org/html/rfc7232#section-4.2)]
|
||||
PreconditionFailed,
|
||||
/// Equivalent to HTTP status code 413 Payload Too Large
|
||||
/// [[RFC7231, Section 6.5.11](https://tools.ietf.org/html/rfc7231#section-6.5.11)]
|
||||
PayloadTooLarge,
|
||||
/// Equivalent to HTTP status code 414 URI Too Long
|
||||
/// [[RFC7231, Section 6.5.12](https://tools.ietf.org/html/rfc7231#section-6.5.12)]
|
||||
URITooLong,
|
||||
/// Equivalent to HTTP status code 415 Unsupported Media Type
|
||||
/// [[RFC7231, Section 6.5.13](https://tools.ietf.org/html/rfc7231#section-6.5.13)]
|
||||
UnsupportedMediaType,
|
||||
/// Equivalent to HTTP status code 416 Range Not Satisfiable
|
||||
/// [[RFC7233, Section 4.4](https://tools.ietf.org/html/rfc7233#section-4.4)]
|
||||
RangeNotSatisfiable,
|
||||
/// Equivalent to HTTP status code 417 Expectation Failed
|
||||
/// [[RFC7231, Section 6.5.14](https://tools.ietf.org/html/rfc7231#section-6.5.14)]
|
||||
ExpectationFailed,
|
||||
/// Equivalent to HTTP status code 421 Misdirected Request
|
||||
/// [RFC7540, Section 9.1.2](http://tools.ietf.org/html/rfc7540#section-9.1.2)
|
||||
MisdirectedRequest,
|
||||
/// Equivalent to HTTP status code 422 Unprocessable Entity
|
||||
/// [[RFC4918](https://tools.ietf.org/html/rfc4918)]
|
||||
UnprocessableEntity,
|
||||
/// Equivalent to HTTP status code 423 Locked
|
||||
/// [[RFC4918](https://tools.ietf.org/html/rfc4918)]
|
||||
Locked,
|
||||
/// Equivalent to HTTP status code 424 Failed Dependency
|
||||
/// [[RFC4918](https://tools.ietf.org/html/rfc4918)]
|
||||
FailedDependency,
|
||||
|
||||
/// Equivalent to HTTP status code 426 Upgrade Required
|
||||
/// [[RFC7231, Section 6.5.15](https://tools.ietf.org/html/rfc7231#section-6.5.15)]
|
||||
UpgradeRequired,
|
||||
|
||||
/// Equivalent to HTTP status code 428 Precondition Required
|
||||
/// [[RFC6585](https://tools.ietf.org/html/rfc6585)]
|
||||
PreconditionRequired,
|
||||
/// Equivalent to HTTP status code 429 Too Many Requests
|
||||
/// [[RFC6585](https://tools.ietf.org/html/rfc6585)]
|
||||
TooManyRequests,
|
||||
|
||||
/// Equivalent to HTTP status code 431 Request Header Fields Too Large
|
||||
/// [[RFC6585](https://tools.ietf.org/html/rfc6585)]
|
||||
RequestHeaderFieldsTooLarge,
|
||||
|
||||
/// Equivalent to HTTP status code 451 Unavailable For Legal Reasons
|
||||
/// [[RFC7725](http://tools.ietf.org/html/rfc7725)]
|
||||
UnavailableForLegalReasons,
|
||||
|
||||
/// Equivalent to HTTP status code 500 Internal Server Error
|
||||
/// [[RFC7231, Section 6.6.1](https://tools.ietf.org/html/rfc7231#section-6.6.1)]
|
||||
InternalServerError,
|
||||
/// Equivalent to HTTP status code 501 Not Implemented
|
||||
/// [[RFC7231, Section 6.6.2](https://tools.ietf.org/html/rfc7231#section-6.6.2)]
|
||||
NotImplemented,
|
||||
/// Equivalent to HTTP status code 502 Bad Gateway
|
||||
/// [[RFC7231, Section 6.6.3](https://tools.ietf.org/html/rfc7231#section-6.6.3)]
|
||||
BadGateway,
|
||||
/// Equivalent to HTTP status code 503 Service Unavailable
|
||||
/// [[RFC7231, Section 6.6.4](https://tools.ietf.org/html/rfc7231#section-6.6.4)]
|
||||
ServiceUnavailable,
|
||||
/// Equivalent to HTTP status code 504 Gateway Timeout
|
||||
/// [[RFC7231, Section 6.6.5](https://tools.ietf.org/html/rfc7231#section-6.6.5)]
|
||||
GatewayTimeout,
|
||||
/// Equivalent to HTTP status code 505 HTTP Version Not Supported
|
||||
/// [[RFC7231, Section 6.6.6](https://tools.ietf.org/html/rfc7231#section-6.6.6)]
|
||||
HTTPVersionNotSupported,
|
||||
/// Equivalent to HTTP status code 506 Variant Also Negotiates
|
||||
/// [[RFC2295](https://tools.ietf.org/html/rfc2295)]
|
||||
VariantAlsoNegotiates,
|
||||
/// Equivalent to HTTP status code 507 Insufficient Storage
|
||||
/// [[RFC4918](https://tools.ietf.org/html/rfc4918)]
|
||||
InsufficientStorage,
|
||||
/// Equivalent to HTTP status code 508 Loop Detected
|
||||
/// [[RFC5842](https://tools.ietf.org/html/rfc5842)]
|
||||
LoopDetected,
|
||||
/// Equivalent to HTTP status code 510 Not Extended
|
||||
/// [[RFC2774](https://tools.ietf.org/html/rfc2774)]
|
||||
NotExtended,
|
||||
/// Equivalent to HTTP status code 511 Network Authentication Required
|
||||
/// [[RFC6585](https://tools.ietf.org/html/rfc6585)]
|
||||
NetworkAuthenticationRequired,
|
||||
}
|
||||
|
||||
impl NetworkErrorKind {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
use NetworkErrorKind::*;
|
||||
match self {
|
||||
None => "Network",
|
||||
HostLookupFailed => "Name lookup of host failed.",
|
||||
BadClientCertificate => "Bad client Certificate",
|
||||
BadServerCertificate => "Bad server certificate",
|
||||
ClientInitialization => "Client initialization",
|
||||
ConnectionFailed => "Connection failed",
|
||||
InvalidContentEncoding => "Invalid content encoding",
|
||||
InvalidCredentials => "Invalid credentials",
|
||||
InvalidRequest => "Invalid request",
|
||||
Io => "IO Error",
|
||||
NameResolution => "Name resolution",
|
||||
ProtocolViolation => "Protocol violation",
|
||||
RequestBodyNotRewindable => "Request body not rewindable",
|
||||
Timeout => "Connection (not request) timeout.",
|
||||
TooManyRedirects => "TooManyRedirects",
|
||||
InvalidTLSConnection => "Invalid TLS connection",
|
||||
BadRequest => "Bad Request",
|
||||
Unauthorized => "Unauthorized",
|
||||
PaymentRequired => "Payment Required",
|
||||
Forbidden => "Forbidden",
|
||||
NotFound => "Not Found",
|
||||
MethodNotAllowed => "Method Not Allowed",
|
||||
NotAcceptable => "Not Acceptable",
|
||||
ProxyAuthenticationRequired => "Proxy Authentication Required",
|
||||
RequestTimeout => "Request Timeout",
|
||||
Conflict => "Conflict",
|
||||
Gone => "Gone",
|
||||
LengthRequired => "Length Required",
|
||||
PreconditionFailed => "Precondition Failed",
|
||||
PayloadTooLarge => "Payload Too Large",
|
||||
URITooLong => "URI Too Long",
|
||||
UnsupportedMediaType => "Unsupported Media Type",
|
||||
RangeNotSatisfiable => "Range Not Satisfiable",
|
||||
ExpectationFailed => "Expectation Failed",
|
||||
MisdirectedRequest => "Misdirected Request",
|
||||
UnprocessableEntity => "Unprocessable Entity",
|
||||
Locked => "Locked",
|
||||
FailedDependency => "Failed Dependency",
|
||||
UpgradeRequired => "Upgrade Required",
|
||||
PreconditionRequired => "Precondition Required",
|
||||
TooManyRequests => "Too Many Requests",
|
||||
RequestHeaderFieldsTooLarge => "Request Header Fields Too Large",
|
||||
UnavailableForLegalReasons => "Unavailable For Legal Reasons",
|
||||
InternalServerError => "Internal Server Error",
|
||||
NotImplemented => "Not Implemented",
|
||||
BadGateway => "Bad Gateway",
|
||||
ServiceUnavailable => "Service Unavailable",
|
||||
GatewayTimeout => "Gateway Timeout",
|
||||
HTTPVersionNotSupported => "HTTP Version Not Supported",
|
||||
VariantAlsoNegotiates => "Variant Also Negotiates",
|
||||
InsufficientStorage => "Insufficient Storage",
|
||||
LoopDetected => "Loop Detected",
|
||||
NotExtended => "Not Extended",
|
||||
NetworkAuthenticationRequired => "Network Authentication Required",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NetworkErrorKind {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
impl From<isahc::http::StatusCode> for NetworkErrorKind {
|
||||
fn from(val: isahc::http::StatusCode) -> Self {
|
||||
match val {
|
||||
isahc::http::StatusCode::BAD_REQUEST => Self::BadRequest,
|
||||
isahc::http::StatusCode::UNAUTHORIZED => Self::Unauthorized,
|
||||
isahc::http::StatusCode::PAYMENT_REQUIRED => Self::PaymentRequired,
|
||||
isahc::http::StatusCode::FORBIDDEN => Self::Forbidden,
|
||||
isahc::http::StatusCode::NOT_FOUND => Self::NotFound,
|
||||
isahc::http::StatusCode::METHOD_NOT_ALLOWED => Self::MethodNotAllowed,
|
||||
isahc::http::StatusCode::NOT_ACCEPTABLE => Self::NotAcceptable,
|
||||
isahc::http::StatusCode::PROXY_AUTHENTICATION_REQUIRED => {
|
||||
Self::ProxyAuthenticationRequired
|
||||
}
|
||||
isahc::http::StatusCode::REQUEST_TIMEOUT => Self::RequestTimeout,
|
||||
isahc::http::StatusCode::CONFLICT => Self::Conflict,
|
||||
isahc::http::StatusCode::GONE => Self::Gone,
|
||||
isahc::http::StatusCode::LENGTH_REQUIRED => Self::LengthRequired,
|
||||
isahc::http::StatusCode::PRECONDITION_FAILED => Self::PreconditionFailed,
|
||||
isahc::http::StatusCode::PAYLOAD_TOO_LARGE => Self::PayloadTooLarge,
|
||||
isahc::http::StatusCode::URI_TOO_LONG => Self::URITooLong,
|
||||
isahc::http::StatusCode::UNSUPPORTED_MEDIA_TYPE => Self::UnsupportedMediaType,
|
||||
isahc::http::StatusCode::RANGE_NOT_SATISFIABLE => Self::RangeNotSatisfiable,
|
||||
isahc::http::StatusCode::EXPECTATION_FAILED => Self::ExpectationFailed,
|
||||
isahc::http::StatusCode::MISDIRECTED_REQUEST => Self::MisdirectedRequest,
|
||||
isahc::http::StatusCode::UNPROCESSABLE_ENTITY => Self::UnprocessableEntity,
|
||||
isahc::http::StatusCode::LOCKED => Self::Locked,
|
||||
isahc::http::StatusCode::FAILED_DEPENDENCY => Self::FailedDependency,
|
||||
isahc::http::StatusCode::UPGRADE_REQUIRED => Self::UpgradeRequired,
|
||||
isahc::http::StatusCode::PRECONDITION_REQUIRED => Self::PreconditionRequired,
|
||||
isahc::http::StatusCode::TOO_MANY_REQUESTS => Self::TooManyRequests,
|
||||
isahc::http::StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE => {
|
||||
Self::RequestHeaderFieldsTooLarge
|
||||
}
|
||||
isahc::http::StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS => {
|
||||
Self::UnavailableForLegalReasons
|
||||
}
|
||||
isahc::http::StatusCode::INTERNAL_SERVER_ERROR => Self::InternalServerError,
|
||||
isahc::http::StatusCode::NOT_IMPLEMENTED => Self::NotImplemented,
|
||||
isahc::http::StatusCode::BAD_GATEWAY => Self::BadGateway,
|
||||
isahc::http::StatusCode::SERVICE_UNAVAILABLE => Self::ServiceUnavailable,
|
||||
isahc::http::StatusCode::GATEWAY_TIMEOUT => Self::GatewayTimeout,
|
||||
isahc::http::StatusCode::HTTP_VERSION_NOT_SUPPORTED => Self::HTTPVersionNotSupported,
|
||||
isahc::http::StatusCode::VARIANT_ALSO_NEGOTIATES => Self::VariantAlsoNegotiates,
|
||||
isahc::http::StatusCode::INSUFFICIENT_STORAGE => Self::InsufficientStorage,
|
||||
isahc::http::StatusCode::LOOP_DETECTED => Self::LoopDetected,
|
||||
isahc::http::StatusCode::NOT_EXTENDED => Self::NotExtended,
|
||||
isahc::http::StatusCode::NETWORK_AUTHENTICATION_REQUIRED => {
|
||||
Self::NetworkAuthenticationRequired
|
||||
}
|
||||
_ => Self::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, PartialEq, Clone)]
|
||||
pub enum ErrorKind {
|
||||
None,
|
||||
External,
|
||||
Authentication,
|
||||
Configuration,
|
||||
Bug,
|
||||
Network(NetworkErrorKind),
|
||||
Network,
|
||||
Timeout,
|
||||
OSError,
|
||||
NotImplemented,
|
||||
NotSupported,
|
||||
}
|
||||
|
||||
impl fmt::Display for ErrorKind {
|
||||
|
@ -336,12 +54,8 @@ impl fmt::Display for ErrorKind {
|
|||
ErrorKind::External => "External",
|
||||
ErrorKind::Authentication => "Authentication",
|
||||
ErrorKind::Bug => "Bug, please report this!",
|
||||
ErrorKind::Network(ref inner) => inner.as_str(),
|
||||
ErrorKind::Network => "Network",
|
||||
ErrorKind::Timeout => "Timeout",
|
||||
ErrorKind::OSError => "OS Error",
|
||||
ErrorKind::Configuration => "Configuration",
|
||||
ErrorKind::NotImplemented => "Not implemented",
|
||||
ErrorKind::NotSupported => "Not supported",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -349,32 +63,37 @@ impl fmt::Display for ErrorKind {
|
|||
|
||||
impl ErrorKind {
|
||||
pub fn is_network(&self) -> bool {
|
||||
matches!(self, ErrorKind::Network(_))
|
||||
match self {
|
||||
ErrorKind::Network => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_timeout(&self) -> bool {
|
||||
matches!(self, ErrorKind::Timeout)
|
||||
match self {
|
||||
ErrorKind::Timeout => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_authentication(&self) -> bool {
|
||||
matches!(self, ErrorKind::Authentication)
|
||||
match self {
|
||||
ErrorKind::Authentication => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MeliError {
|
||||
pub summary: Cow<'static, str>,
|
||||
pub details: Option<Cow<'static, str>>,
|
||||
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 IntoMeliError {
|
||||
fn set_err_summary<M>(self, msg: M) -> MeliError
|
||||
where
|
||||
M: Into<Cow<'static, str>>;
|
||||
|
||||
fn set_err_details<M>(self, msg: M) -> MeliError
|
||||
where
|
||||
M: Into<Cow<'static, str>>;
|
||||
fn set_err_kind(self, kind: ErrorKind) -> MeliError;
|
||||
|
@ -399,15 +118,6 @@ impl<I: Into<MeliError>> IntoMeliError for I {
|
|||
err.set_summary(msg)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_err_details<M>(self, msg: M) -> MeliError
|
||||
where
|
||||
M: Into<Cow<'static, str>>,
|
||||
{
|
||||
let err: MeliError = self.into();
|
||||
err.set_details(msg)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_err_kind(self, kind: ErrorKind) -> MeliError {
|
||||
let err: MeliError = self.into();
|
||||
|
@ -437,33 +147,21 @@ impl MeliError {
|
|||
M: Into<Cow<'static, str>>,
|
||||
{
|
||||
MeliError {
|
||||
summary: msg.into(),
|
||||
details: None,
|
||||
summary: None,
|
||||
details: msg.into(),
|
||||
source: None,
|
||||
kind: ErrorKind::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_details<M>(mut self, details: M) -> MeliError
|
||||
where
|
||||
M: Into<Cow<'static, str>>,
|
||||
{
|
||||
if let Some(old_details) = self.details.as_ref() {
|
||||
self.details = Some(format!("{}. {}", old_details, details.into()).into());
|
||||
} else {
|
||||
self.details = Some(details.into());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_summary<M>(mut self, summary: M) -> MeliError
|
||||
where
|
||||
M: Into<Cow<'static, str>>,
|
||||
{
|
||||
if self.summary.is_empty() {
|
||||
self.summary = summary.into();
|
||||
if let Some(old_summary) = self.summary.take() {
|
||||
self.summary = Some(format!("{}. {}", old_summary, summary.into()).into());
|
||||
} else {
|
||||
self.summary = format!("{}. {}", self.summary, summary.into()).into();
|
||||
self.summary = Some(summary.into());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
@ -484,10 +182,10 @@ impl MeliError {
|
|||
|
||||
impl fmt::Display for MeliError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f, "{}", self.summary)?;
|
||||
if let Some(details) = self.details.as_ref() {
|
||||
write!(f, "{}", 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)?;
|
||||
}
|
||||
|
@ -508,30 +206,29 @@ impl From<io::Error> for MeliError {
|
|||
#[inline]
|
||||
fn from(kind: io::Error) -> MeliError {
|
||||
MeliError::new(kind.to_string())
|
||||
.set_details(kind.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 MeliError {
|
||||
#[inline]
|
||||
fn from(kind: Cow<'_, str>) -> MeliError {
|
||||
MeliError::new(kind.to_string())
|
||||
MeliError::new(format!("{:?}", kind))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<string::FromUtf8Error> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: string::FromUtf8Error) -> MeliError {
|
||||
MeliError::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
MeliError::new(format!("{:?}", kind)).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<str::Utf8Error> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: str::Utf8Error) -> MeliError {
|
||||
MeliError::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
MeliError::new(format!("{:?}", kind)).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
//use std::option;
|
||||
|
@ -545,7 +242,7 @@ impl From<str::Utf8Error> for MeliError {
|
|||
impl<T> From<std::sync::PoisonError<T>> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: std::sync::PoisonError<T>) -> MeliError {
|
||||
MeliError::new(kind.to_string()).set_kind(ErrorKind::Bug)
|
||||
MeliError::new(format!("{}", kind))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -555,9 +252,7 @@ impl<T: Sync + Send + 'static + core::fmt::Debug> From<native_tls::HandshakeErro
|
|||
{
|
||||
#[inline]
|
||||
fn from(kind: native_tls::HandshakeError<T>) -> MeliError {
|
||||
MeliError::new(kind.to_string())
|
||||
.set_source(Some(Arc::new(kind)))
|
||||
.set_kind(ErrorKind::Network(NetworkErrorKind::InvalidTLSConnection))
|
||||
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -565,59 +260,22 @@ impl<T: Sync + Send + 'static + core::fmt::Debug> From<native_tls::HandshakeErro
|
|||
impl From<native_tls::Error> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: native_tls::Error) -> MeliError {
|
||||
MeliError::new(kind.to_string())
|
||||
.set_source(Some(Arc::new(kind)))
|
||||
.set_kind(ErrorKind::Network(NetworkErrorKind::InvalidTLSConnection))
|
||||
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::num::ParseIntError> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: std::num::ParseIntError) -> MeliError {
|
||||
MeliError::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
impl From<&isahc::error::ErrorKind> for NetworkErrorKind {
|
||||
#[inline]
|
||||
fn from(val: &isahc::error::ErrorKind) -> NetworkErrorKind {
|
||||
use isahc::error::ErrorKind::*;
|
||||
match val {
|
||||
BadClientCertificate => NetworkErrorKind::BadClientCertificate,
|
||||
BadServerCertificate => NetworkErrorKind::BadServerCertificate,
|
||||
ClientInitialization => NetworkErrorKind::ClientInitialization,
|
||||
ConnectionFailed => NetworkErrorKind::ConnectionFailed,
|
||||
InvalidContentEncoding => NetworkErrorKind::InvalidContentEncoding,
|
||||
InvalidCredentials => NetworkErrorKind::InvalidCredentials,
|
||||
InvalidRequest => NetworkErrorKind::BadRequest,
|
||||
Io => NetworkErrorKind::Io,
|
||||
NameResolution => NetworkErrorKind::HostLookupFailed,
|
||||
ProtocolViolation => NetworkErrorKind::ProtocolViolation,
|
||||
RequestBodyNotRewindable => NetworkErrorKind::RequestBodyNotRewindable,
|
||||
Timeout => NetworkErrorKind::Timeout,
|
||||
TlsEngine => NetworkErrorKind::InvalidTLSConnection,
|
||||
TooManyRedirects => NetworkErrorKind::TooManyRedirects,
|
||||
_ => NetworkErrorKind::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NetworkErrorKind> for ErrorKind {
|
||||
#[inline]
|
||||
fn from(kind: NetworkErrorKind) -> ErrorKind {
|
||||
ErrorKind::Network(kind)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
#[cfg(feature = "jmap_backend")]
|
||||
impl From<isahc::Error> for MeliError {
|
||||
#[inline]
|
||||
fn from(val: isahc::Error) -> MeliError {
|
||||
let kind: NetworkErrorKind = val.kind().into();
|
||||
MeliError::new(val.to_string())
|
||||
.set_source(Some(Arc::new(val)))
|
||||
.set_kind(ErrorKind::Network(kind))
|
||||
fn from(kind: isahc::Error) -> MeliError {
|
||||
MeliError::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -625,35 +283,35 @@ impl From<isahc::Error> for MeliError {
|
|||
impl From<serde_json::error::Error> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: serde_json::error::Error) -> MeliError {
|
||||
MeliError::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<dyn Error + Sync + Send + 'static>> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: Box<dyn Error + Sync + Send + 'static>) -> MeliError {
|
||||
MeliError::new(kind.to_string()).set_source(Some(kind.into()))
|
||||
MeliError::new(format!("{}", kind)).set_source(Some(kind.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::ffi::NulError> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: std::ffi::NulError) -> MeliError {
|
||||
MeliError::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
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(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
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(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -661,14 +319,14 @@ impl From<nix::Error> for MeliError {
|
|||
impl From<rusqlite::Error> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: rusqlite::Error) -> MeliError {
|
||||
MeliError::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<libloading::Error> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: libloading::Error) -> MeliError {
|
||||
MeliError::new(kind.to_string()).set_source(Some(Arc::new(kind)))
|
||||
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -689,14 +347,16 @@ impl From<String> for MeliError {
|
|||
impl From<nom::Err<(&[u8], nom::error::ErrorKind)>> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: nom::Err<(&[u8], nom::error::ErrorKind)>) -> MeliError {
|
||||
MeliError::new("Parsing error").set_source(Some(Arc::new(MeliError::new(kind.to_string()))))
|
||||
MeliError::new("Parsing error")
|
||||
.set_source(Some(Arc::new(MeliError::new(format!("{}", kind)))))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nom::Err<(&str, nom::error::ErrorKind)>> for MeliError {
|
||||
#[inline]
|
||||
fn from(kind: nom::Err<(&str, nom::error::ErrorKind)>) -> MeliError {
|
||||
MeliError::new("Parsing error").set_details(kind.to_string())
|
||||
MeliError::new("Parsing error")
|
||||
.set_source(Some(Arc::new(MeliError::new(format!("{}", kind)))))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,155 +0,0 @@
|
|||
/*
|
||||
* melib - gpgme module
|
||||
*
|
||||
* Copyright 2020 Manos Pitsidianakis
|
||||
*
|
||||
* This file is part of meli.
|
||||
*
|
||||
* meli is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* meli is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use super::*;
|
||||
use std::io::{self, Read, Seek, Write};
|
||||
|
||||
#[repr(C)]
|
||||
struct TagData {
|
||||
idx: usize,
|
||||
fd: ::std::os::raw::c_int,
|
||||
io_state: Arc<Mutex<IoState>>,
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn gpgme_register_io_cb(
|
||||
data: *mut ::std::os::raw::c_void,
|
||||
fd: ::std::os::raw::c_int,
|
||||
dir: ::std::os::raw::c_int,
|
||||
fnc: gpgme_io_cb_t,
|
||||
fnc_data: *mut ::std::os::raw::c_void,
|
||||
tag: *mut *mut ::std::os::raw::c_void,
|
||||
) -> gpgme_error_t {
|
||||
let io_state: Arc<Mutex<IoState>> = Arc::from_raw(data as *const _);
|
||||
let io_state_copy = io_state.clone();
|
||||
let mut io_state_lck = io_state.lock().unwrap();
|
||||
let idx = io_state_lck.max_idx;
|
||||
io_state_lck.max_idx += 1;
|
||||
let (sender, receiver) = smol::channel::unbounded();
|
||||
let gpgfd = GpgmeFd {
|
||||
fd,
|
||||
fnc,
|
||||
fnc_data,
|
||||
idx,
|
||||
write: dir == 0,
|
||||
sender,
|
||||
receiver,
|
||||
io_state: io_state_copy.clone(),
|
||||
};
|
||||
let tag_data = Arc::into_raw(Arc::new(TagData {
|
||||
idx,
|
||||
fd,
|
||||
io_state: io_state_copy,
|
||||
}));
|
||||
core::ptr::write(tag, tag_data as *mut _);
|
||||
io_state_lck.ops.insert(idx, gpgfd);
|
||||
drop(io_state_lck);
|
||||
let _ = Arc::into_raw(io_state);
|
||||
0
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn gpgme_remove_io_cb(tag: *mut ::std::os::raw::c_void) {
|
||||
let tag_data: Arc<TagData> = Arc::from_raw(tag as *const _);
|
||||
let mut io_state_lck = tag_data.io_state.lock().unwrap();
|
||||
let fd = io_state_lck.ops.remove(&tag_data.idx).unwrap();
|
||||
fd.sender.try_send(()).unwrap();
|
||||
drop(io_state_lck);
|
||||
let _ = Arc::into_raw(tag_data);
|
||||
}
|
||||
|
||||
pub unsafe extern "C" fn gpgme_event_io_cb(
|
||||
data: *mut ::std::os::raw::c_void,
|
||||
type_: gpgme_event_io_t,
|
||||
type_data: *mut ::std::os::raw::c_void,
|
||||
) {
|
||||
if type_ == gpgme_event_io_t_GPGME_EVENT_DONE {
|
||||
let err = type_data as gpgme_io_event_done_data_t;
|
||||
let io_state: Arc<Mutex<IoState>> = Arc::from_raw(data as *const _);
|
||||
let io_state_lck = io_state.lock().unwrap();
|
||||
io_state_lck.sender.try_send(()).unwrap();
|
||||
*io_state_lck.done.lock().unwrap() = Some(gpgme_error_try(&io_state_lck.lib, (*err).err));
|
||||
drop(io_state_lck);
|
||||
let _ = Arc::into_raw(io_state);
|
||||
} else if type_ == gpgme_event_io_t_GPGME_EVENT_NEXT_KEY {
|
||||
if let Some(inner) = core::ptr::NonNull::new(type_data as gpgme_key_t) {
|
||||
let io_state: Arc<Mutex<IoState>> = Arc::from_raw(data as *const _);
|
||||
let io_state_lck = io_state.lock().unwrap();
|
||||
io_state_lck
|
||||
.key_sender
|
||||
.try_send(KeyInner { inner })
|
||||
.unwrap();
|
||||
drop(io_state_lck);
|
||||
let _ = Arc::into_raw(io_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for Data {
|
||||
#[inline]
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let result = unsafe {
|
||||
let (buf, len) = (buf.as_mut_ptr() as *mut _, buf.len());
|
||||
call!(self.lib, gpgme_data_read)(self.inner.as_ptr(), buf, len)
|
||||
};
|
||||
if result >= 0 {
|
||||
Ok(result as usize)
|
||||
} else {
|
||||
Err(io::Error::last_os_error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for Data {
|
||||
#[inline]
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let result = unsafe {
|
||||
let (buf, len) = (buf.as_ptr() as *const _, buf.len());
|
||||
call!(self.lib, gpgme_data_write)(self.inner.as_ptr(), buf, len)
|
||||
};
|
||||
if result >= 0 {
|
||||
Ok(result as usize)
|
||||
} else {
|
||||
Err(io::Error::last_os_error())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for Data {
|
||||
#[inline]
|
||||
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
|
||||
use std::convert::TryInto;
|
||||
let (off, whence) = match pos {
|
||||
io::SeekFrom::Start(off) => (off.try_into().unwrap_or(i64::MAX), libc::SEEK_SET),
|
||||
io::SeekFrom::End(off) => (off.saturating_abs(), libc::SEEK_END),
|
||||
io::SeekFrom::Current(off) => (off, libc::SEEK_CUR),
|
||||
};
|
||||
let result = unsafe { call!(self.lib, gpgme_data_seek)(self.inner.as_ptr(), off, whence) };
|
||||
if result >= 0 {
|
||||
Ok(result as u64)
|
||||
} else {
|
||||
Err(io::Error::last_os_error())
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -41,11 +41,7 @@ pub mod dbg {
|
|||
() => {
|
||||
eprint!(
|
||||
"[{}][{:?}] {}:{}_{}: ",
|
||||
$crate::datetime::timestamp_to_string(
|
||||
$crate::datetime::now(),
|
||||
Some("%Y-%m-%d %T"),
|
||||
false
|
||||
),
|
||||
crate::datetime::timestamp_to_string(crate::datetime::now(), Some("%Y-%m-%d %T")),
|
||||
std::thread::current()
|
||||
.name()
|
||||
.map(std::string::ToString::to_string)
|
||||
|
@ -123,8 +119,6 @@ pub mod connections;
|
|||
pub mod parsec;
|
||||
pub mod search;
|
||||
|
||||
#[cfg(feature = "gpgme")]
|
||||
pub mod gpgme;
|
||||
#[cfg(feature = "smtp")]
|
||||
pub mod smtp;
|
||||
#[cfg(feature = "sqlite3")]
|
||||
|
@ -146,42 +140,12 @@ pub extern crate smol;
|
|||
pub extern crate uuid;
|
||||
pub extern crate xdg_utils;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Bytes(pub usize);
|
||||
|
||||
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 Bytes {
|
||||
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
let bytes: f64 = self.0 as f64;
|
||||
if bytes == 0.0 {
|
||||
write!(fmt, "0")
|
||||
} else if bytes < Self::KILOBYTE {
|
||||
write!(fmt, "{:.2} bytes", bytes)
|
||||
} else if bytes < Self::MEGABYTE {
|
||||
write!(fmt, "{:.2} KiB", bytes / Self::KILOBYTE)
|
||||
} else if bytes < Self::GIGABYTE {
|
||||
write!(fmt, "{:.2} MiB", bytes / Self::MEGABYTE)
|
||||
} else if bytes < Self::PETABYTE {
|
||||
write!(fmt, "{:.2} GiB", bytes / Self::GIGABYTE)
|
||||
} else {
|
||||
write!(fmt, "{:.2} PiB", bytes / Self::PETABYTE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use shellexpand::ShellExpandTrait;
|
||||
pub mod shellexpand {
|
||||
|
||||
use smallvec::SmallVec;
|
||||
use std::ffi::OsStr;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
#[cfg(not(any(target_os = "netbsd", target_os = "macos")))]
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
|
@ -340,7 +304,7 @@ pub mod shellexpand {
|
|||
.components()
|
||||
.last()
|
||||
.map(|c| c.as_os_str())
|
||||
.unwrap_or_else(|| OsStr::from_bytes(b""));
|
||||
.unwrap_or(OsStr::from_bytes(b""));
|
||||
let prefix = if let Some(p) = self.parent() {
|
||||
p
|
||||
} else {
|
||||
|
@ -354,33 +318,37 @@ pub mod shellexpand {
|
|||
}
|
||||
|
||||
if let Ok(iter) = std::fs::read_dir(&prefix) {
|
||||
for entry in iter.flatten() {
|
||||
if entry.path().as_os_str().as_bytes() != b"."
|
||||
&& entry.path().as_os_str().as_bytes() != b".."
|
||||
&& entry
|
||||
.path()
|
||||
.as_os_str()
|
||||
.as_bytes()
|
||||
.starts_with(_match.as_bytes())
|
||||
{
|
||||
if entry.path().is_dir()
|
||||
&& !entry.path().as_os_str().as_bytes().ends_with(b"/")
|
||||
for entry in iter {
|
||||
if let Ok(entry) = entry {
|
||||
if entry.path().as_os_str().as_bytes() != b"."
|
||||
&& entry.path().as_os_str().as_bytes() != b".."
|
||||
&& entry
|
||||
.path()
|
||||
.as_os_str()
|
||||
.as_bytes()
|
||||
.starts_with(_match.as_bytes())
|
||||
{
|
||||
let mut s = unsafe {
|
||||
String::from_utf8_unchecked(
|
||||
entry.path().as_os_str().as_bytes()[_match.as_bytes().len()..]
|
||||
.to_vec(),
|
||||
)
|
||||
};
|
||||
s.push('/');
|
||||
entries.push(s);
|
||||
} else {
|
||||
entries.push(unsafe {
|
||||
String::from_utf8_unchecked(
|
||||
entry.path().as_os_str().as_bytes()[_match.as_bytes().len()..]
|
||||
.to_vec(),
|
||||
)
|
||||
});
|
||||
if entry.path().is_dir()
|
||||
&& !entry.path().as_os_str().as_bytes().ends_with(b"/")
|
||||
{
|
||||
let mut s = unsafe {
|
||||
String::from_utf8_unchecked(
|
||||
entry.path().as_os_str().as_bytes()
|
||||
[_match.as_bytes().len()..]
|
||||
.to_vec(),
|
||||
)
|
||||
};
|
||||
s.push('/');
|
||||
entries.push(s);
|
||||
} else {
|
||||
entries.push(unsafe {
|
||||
String::from_utf8_unchecked(
|
||||
entry.path().as_os_str().as_bytes()
|
||||
[_match.as_bytes().len()..]
|
||||
.to_vec(),
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,8 +85,7 @@ pub fn log<S: AsRef<str>>(val: S, level: LoggingLevel) {
|
|||
if level <= b.level {
|
||||
b.dest
|
||||
.write_all(
|
||||
crate::datetime::timestamp_to_string(crate::datetime::now(), None, false)
|
||||
.as_bytes(),
|
||||
crate::datetime::timestamp_to_string(crate::datetime::now(), None).as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
b.dest.write_all(b" [").unwrap();
|
||||
|
|
|
@ -93,22 +93,6 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub fn map_res<'a, P, F, E, A, B>(parser: P, map_fn: F) -> impl Parser<'a, B>
|
||||
where
|
||||
P: Parser<'a, A>,
|
||||
F: Fn(A) -> std::result::Result<B, E>,
|
||||
{
|
||||
move |input| {
|
||||
parser.parse(input).and_then(|(next_input, result)| {
|
||||
if let Ok(res) = map_fn(result) {
|
||||
Ok((next_input, res))
|
||||
} else {
|
||||
Err(next_input)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_literal<'a>(expected: &'static str) -> impl Parser<'a, ()> {
|
||||
move |input: &'a str| match input.get(0..expected.len()) {
|
||||
Some(next) if next == expected => Ok((&input[expected.len()..], ())),
|
||||
|
@ -194,24 +178,6 @@ pub fn quoted_string<'a>() -> impl Parser<'a, String> {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn quoted_slice<'a>() -> impl Parser<'a, &'a str> {
|
||||
move |input: &'a str| {
|
||||
if input.is_empty() || !input.starts_with('"') {
|
||||
return Err(input);
|
||||
}
|
||||
|
||||
let mut i = 1;
|
||||
while i < input.len() {
|
||||
if input[i..].starts_with('\"') && !input[i - 1..].starts_with('\\') {
|
||||
return Ok((&input[i + 1..], &input[1..i]));
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
Err(input)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BoxedParser<'a, Output> {
|
||||
parser: Box<dyn Parser<'a, Output> + 'a>,
|
||||
}
|
||||
|
@ -251,8 +217,6 @@ where
|
|||
right(space0(), left(parser, space0()))
|
||||
}
|
||||
|
||||
pub use whitespace_wrap as ws_eat;
|
||||
|
||||
pub fn pair<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, (R1, R2)>
|
||||
where
|
||||
P1: Parser<'a, R1>,
|
||||
|
@ -326,69 +290,6 @@ pub fn space0<'a>() -> impl Parser<'a, Vec<char>> {
|
|||
zero_or_more(whitespace_char())
|
||||
}
|
||||
|
||||
pub fn is_a<'a>(slice: &'static [u8]) -> impl Parser<'a, &'a str> {
|
||||
move |input: &'a str| {
|
||||
let mut i = 0;
|
||||
for byte in input.as_bytes().iter() {
|
||||
if !slice.contains(byte) {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if i == 0 {
|
||||
return Err("");
|
||||
}
|
||||
let (b, a) = input.split_at(i);
|
||||
Ok((a, b))
|
||||
}
|
||||
}
|
||||
|
||||
/// Try alternative parsers in order until one succeeds.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use melib::parsec::{Parser, quoted_slice, match_literal, alt, delimited, prefix};
|
||||
///
|
||||
/// let parser = |input| {
|
||||
/// alt([
|
||||
/// delimited(
|
||||
/// match_literal("{"),
|
||||
/// quoted_slice(),
|
||||
/// match_literal("}"),
|
||||
/// ),
|
||||
/// delimited(
|
||||
/// match_literal("["),
|
||||
/// quoted_slice(),
|
||||
/// match_literal("]"),
|
||||
/// ),
|
||||
/// ]).parse(input)
|
||||
/// };
|
||||
///
|
||||
/// let input1: &str = "{\"quoted\"}";
|
||||
/// let input2: &str = "[\"quoted\"]";
|
||||
/// assert_eq!(
|
||||
/// Ok(("", "quoted")),
|
||||
/// parser.parse(input1)
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Ok(("", "quoted")),
|
||||
/// parser.parse(input2)
|
||||
/// );
|
||||
/// ```
|
||||
pub fn alt<'a, P, A, const N: usize>(parsers: [P; N]) -> impl Parser<'a, A>
|
||||
where
|
||||
P: Parser<'a, A>,
|
||||
{
|
||||
move |input| {
|
||||
for parser in parsers.iter() {
|
||||
if let Ok(res) = parser.parse(input) {
|
||||
return Ok(res);
|
||||
}
|
||||
}
|
||||
Err(input)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn and_then<'a, P, F, A, B, NextP>(parser: P, f: F) -> impl Parser<'a, B>
|
||||
where
|
||||
P: Parser<'a, A>,
|
||||
|
@ -444,198 +345,3 @@ where
|
|||
Ok((&input[offset..], input))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn separated_list0<'a, P, A, S, Sep>(
|
||||
parser: P,
|
||||
separator: S,
|
||||
terminated: bool,
|
||||
) -> impl Parser<'a, Vec<A>>
|
||||
where
|
||||
P: Parser<'a, A>,
|
||||
S: Parser<'a, Sep>,
|
||||
{
|
||||
move |mut input| {
|
||||
let mut result = Vec::new();
|
||||
let mut prev_sep_result = Ok(());
|
||||
let mut last_item_input = input;
|
||||
|
||||
while let Ok((next_input, next_item)) = parser.parse(input) {
|
||||
prev_sep_result?;
|
||||
input = next_input;
|
||||
last_item_input = next_input;
|
||||
result.push(next_item);
|
||||
match separator.parse(input) {
|
||||
Ok((next_input, _)) => {
|
||||
input = next_input;
|
||||
}
|
||||
Err(err) => {
|
||||
prev_sep_result = Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !terminated {
|
||||
input = last_item_input;
|
||||
}
|
||||
|
||||
Ok((input, result))
|
||||
}
|
||||
}
|
||||
|
||||
/// Take `count` bytes
|
||||
pub fn take<'a>(count: usize) -> impl Parser<'a, &'a str> {
|
||||
move |i: &'a str| {
|
||||
if i.len() < count || !i.is_char_boundary(count) {
|
||||
Err("")
|
||||
} else {
|
||||
let (b, a) = i.split_at(count);
|
||||
Ok((a, b))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Take a literal
|
||||
///
|
||||
///```rust
|
||||
/// # use std::str::FromStr;
|
||||
/// # use melib::parsec::{Parser, delimited, match_literal, map_res, is_a, take_literal};
|
||||
/// let lit: &str = "{31}\r\nThere is no script by that name\r\n";
|
||||
/// assert_eq!(
|
||||
/// take_literal(delimited(
|
||||
/// match_literal("{"),
|
||||
/// map_res(is_a(b"0123456789"), |s| usize::from_str(s)),
|
||||
/// match_literal("}\r\n"),
|
||||
/// ))
|
||||
/// .parse(lit),
|
||||
/// Ok((
|
||||
/// "\r\n",
|
||||
/// "There is no script by that name",
|
||||
/// ))
|
||||
/// );
|
||||
///```
|
||||
pub fn take_literal<'a, P>(parser: P) -> impl Parser<'a, &'a str>
|
||||
where
|
||||
P: Parser<'a, usize>,
|
||||
{
|
||||
move |input: &'a str| {
|
||||
let (rest, length) = parser.parse(input)?;
|
||||
take(length).parse(rest)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn test_parsec() {
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum JsonValue {
|
||||
JsonString(String),
|
||||
JsonNumber(f64),
|
||||
JsonBool(bool),
|
||||
JsonNull,
|
||||
JsonObject(HashMap<String, JsonValue>),
|
||||
JsonArray(Vec<JsonValue>),
|
||||
}
|
||||
|
||||
fn parse_value<'a>() -> impl Parser<'a, JsonValue> {
|
||||
move |input| {
|
||||
either(
|
||||
either(
|
||||
either(
|
||||
either(
|
||||
either(
|
||||
map(parse_bool(), |b| JsonValue::JsonBool(b)),
|
||||
map(parse_null(), |()| JsonValue::JsonNull),
|
||||
),
|
||||
map(parse_array(), |vec| JsonValue::JsonArray(vec)),
|
||||
),
|
||||
map(parse_object(), |obj| JsonValue::JsonObject(obj)),
|
||||
),
|
||||
map(parse_number(), |n| JsonValue::JsonNumber(n)),
|
||||
),
|
||||
map(quoted_string(), |s| JsonValue::JsonString(s)),
|
||||
)
|
||||
.parse(input)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_number<'a>() -> impl Parser<'a, f64> {
|
||||
move |input| {
|
||||
either(
|
||||
map(match_literal("TRUE"), |()| 1.0),
|
||||
map(match_literal("FALSe"), |()| 1.0),
|
||||
)
|
||||
.parse(input)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_bool<'a>() -> impl Parser<'a, bool> {
|
||||
move |input| {
|
||||
ws_eat(either(
|
||||
map(match_literal("true"), |()| true),
|
||||
map(match_literal("false"), |()| false),
|
||||
))
|
||||
.parse(input)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_null<'a>() -> impl Parser<'a, ()> {
|
||||
move |input| ws_eat(match_literal("null")).parse(input)
|
||||
}
|
||||
|
||||
fn parse_array<'a>() -> impl Parser<'a, Vec<JsonValue>> {
|
||||
move |input| {
|
||||
delimited(
|
||||
ws_eat(match_literal("[")),
|
||||
separated_list0(parse_value(), ws_eat(match_literal(",")), false),
|
||||
ws_eat(match_literal("]")),
|
||||
)
|
||||
.parse(input)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_object<'a>() -> impl Parser<'a, HashMap<String, JsonValue>> {
|
||||
move |input| {
|
||||
map(
|
||||
delimited(
|
||||
ws_eat(match_literal("{")),
|
||||
separated_list0(
|
||||
pair(
|
||||
suffix(quoted_string(), ws_eat(match_literal(":"))),
|
||||
parse_value(),
|
||||
),
|
||||
ws_eat(match_literal(",")),
|
||||
false,
|
||||
),
|
||||
ws_eat(match_literal("}")),
|
||||
),
|
||||
|vec: Vec<(String, JsonValue)>| vec.into_iter().collect(),
|
||||
)
|
||||
.parse(input)
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
Ok(("", JsonValue::JsonString("a".to_string()))),
|
||||
parse_value().parse(r#""a""#)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(("", JsonValue::JsonBool(true))),
|
||||
parse_value().parse(r#"true"#)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(("", JsonValue::JsonObject(HashMap::default()))),
|
||||
parse_value().parse(r#"{}"#)
|
||||
);
|
||||
println!("{:?}", parse_value().parse(r#"{"a":true}"#));
|
||||
println!("{:?}", parse_value().parse(r#"{"a":true,"b":false}"#));
|
||||
println!("{:?}", parse_value().parse(r#"{ "a" : true,"b": false }"#));
|
||||
println!("{:?}", parse_value().parse(r#"{ "a" : true,"b": false,}"#));
|
||||
println!("{:?}", parse_value().parse(r#"{"a":false,"b":false,}"#));
|
||||
// Line:0 Col:18 Error parsing object
|
||||
// { "a":1, "b" : 2, }
|
||||
// ^Unexpected ','
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,9 +25,6 @@
|
|||
/*!
|
||||
* SMTP client support
|
||||
*
|
||||
* This module implements a client for the SMTP protocol as specified by [RFC 5321 Simple Mail
|
||||
* Transfer Protocol](https://www.rfc-editor.org/rfc/rfc5321).
|
||||
*
|
||||
* The connection and methods are `async` and uses the `smol` runtime.
|
||||
*# Example
|
||||
*
|
||||
|
@ -53,11 +50,7 @@
|
|||
* require_auth: true,
|
||||
* },
|
||||
*};
|
||||
*
|
||||
*std::thread::Builder::new().spawn(move || {
|
||||
* let ex = smol::Executor::new();
|
||||
* futures::executor::block_on(ex.run(futures::future::pending::<()>()));
|
||||
*}).unwrap();
|
||||
*std::thread::spawn(|| smol::run(futures::future::pending::<()>()));
|
||||
*
|
||||
*let mut conn = futures::executor::block_on(SmtpConnection::new_connection(conf)).unwrap();
|
||||
*futures::executor::block_on(conn.mail_transaction(r#"To: l10@mail.gr
|
||||
|
@ -80,7 +73,7 @@ use crate::error::{MeliError, Result, ResultIntoMeliError};
|
|||
use futures::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use native_tls::TlsConnector;
|
||||
use smallvec::SmallVec;
|
||||
use smol::unblock;
|
||||
use smol::blocking;
|
||||
use smol::Async as AsyncWrapper;
|
||||
use std::borrow::Cow;
|
||||
use std::convert::TryFrom;
|
||||
|
@ -140,29 +133,15 @@ pub enum SmtpAuth {
|
|||
password: Password,
|
||||
#[serde(default = "true_val")]
|
||||
require_auth: bool,
|
||||
#[serde(skip_serializing, skip_deserializing, default)]
|
||||
auth_type: SmtpAuthType,
|
||||
},
|
||||
#[serde(alias = "xoauth2")]
|
||||
XOAuth2 {
|
||||
token_command: String,
|
||||
#[serde(default = "true_val")]
|
||||
require_auth: bool,
|
||||
},
|
||||
// md5, sasl, etc
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct SmtpAuthType {
|
||||
plain: bool,
|
||||
login: bool,
|
||||
}
|
||||
|
||||
const fn true_val() -> bool {
|
||||
fn true_val() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
const fn false_val() -> bool {
|
||||
fn false_val() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
|
@ -171,7 +150,7 @@ impl SmtpAuth {
|
|||
use SmtpAuth::*;
|
||||
match self {
|
||||
None => false,
|
||||
Auto { require_auth, .. } | XOAuth2 { require_auth, .. } => *require_auth,
|
||||
Auto { require_auth, .. } => *require_auth,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -198,9 +177,6 @@ pub struct SmtpExtensionSupport {
|
|||
pipelining: bool,
|
||||
#[serde(default = "crate::conf::true_val")]
|
||||
chunking: bool,
|
||||
/// [RFC 6152: SMTP Service Extension for 8-bit MIME Transport](https://www.rfc-editor.org/rfc/rfc6152)
|
||||
#[serde(default = "crate::conf::true_val")]
|
||||
_8bitmime: bool,
|
||||
//Essentially, the PRDR extension to SMTP allows (but does not require) an SMTP server to
|
||||
//issue multiple responses after a message has been transferred, by mutual consent of the
|
||||
//client and server. SMTP clients that support the PRDR extension then use the expanded
|
||||
|
@ -208,7 +184,7 @@ pub struct SmtpExtensionSupport {
|
|||
//envelope exchange.
|
||||
#[serde(default = "crate::conf::true_val")]
|
||||
prdr: bool,
|
||||
#[serde(default = "crate::conf::true_val")]
|
||||
#[serde(default = "crate::conf::false_val")]
|
||||
binarymime: bool,
|
||||
//Resources:
|
||||
//- http://www.postfix.org/SMTPUTF8_README.html
|
||||
|
@ -230,8 +206,7 @@ impl Default for SmtpExtensionSupport {
|
|||
pipelining: true,
|
||||
chunking: true,
|
||||
prdr: true,
|
||||
_8bitmime: true,
|
||||
binarymime: true,
|
||||
binarymime: false,
|
||||
smtputf8: true,
|
||||
auth: true,
|
||||
dsn_notify: Some("FAILURE".into()),
|
||||
|
@ -268,13 +243,16 @@ impl SmtpConnection {
|
|||
if danger_accept_invalid_certs {
|
||||
connector.danger_accept_invalid_certs(true);
|
||||
}
|
||||
let connector = connector.build()?;
|
||||
let connector = connector
|
||||
.build()
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
|
||||
let addr = lookup_ipv4(path, server_conf.port)?;
|
||||
let mut socket = AsyncWrapper::new(Connection::Tcp(TcpStream::connect_timeout(
|
||||
&addr,
|
||||
std::time::Duration::new(4, 0),
|
||||
)?))?;
|
||||
let mut socket = AsyncWrapper::new(Connection::Tcp(
|
||||
TcpStream::connect_timeout(&addr, std::time::Duration::new(4, 0))
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?,
|
||||
))
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
let pre_ehlo_extensions_reply = read_lines(
|
||||
&mut socket,
|
||||
&mut res,
|
||||
|
@ -297,7 +275,10 @@ impl SmtpConnection {
|
|||
return Err(MeliError::new("Please specify what SMTP security transport to use explicitly instead of `auto`."));
|
||||
}
|
||||
}
|
||||
socket.write_all(b"EHLO meli.delivery\r\n").await?;
|
||||
socket
|
||||
.write_all(b"EHLO meli.delivery\r\n")
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
if let SmtpSecurity::StartTLS { .. } = server_conf.security {
|
||||
let pre_tls_extensions_reply = read_lines(
|
||||
&mut socket,
|
||||
|
@ -308,7 +289,10 @@ impl SmtpConnection {
|
|||
.await?;
|
||||
drop(pre_tls_extensions_reply);
|
||||
//debug!(pre_tls_extensions_reply);
|
||||
socket.write_all(b"STARTTLS\r\n").await?;
|
||||
socket
|
||||
.write_all(b"STARTTLS\r\n")
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
let _post_starttls_extensions_reply = read_lines(
|
||||
&mut socket,
|
||||
&mut res,
|
||||
|
@ -320,11 +304,13 @@ impl SmtpConnection {
|
|||
}
|
||||
|
||||
let mut ret = {
|
||||
let socket = socket.into_inner()?;
|
||||
let socket = socket
|
||||
.into_inner()
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
let _path = path.clone();
|
||||
|
||||
socket.set_nonblocking(false)?;
|
||||
let conn = unblock(move || connector.connect(&_path, socket)).await?;
|
||||
let conn_result = blocking!(connector.connect(&_path, socket));
|
||||
/*
|
||||
if let Err(native_tls::HandshakeError::WouldBlock(midhandshake_stream)) =
|
||||
conn_result
|
||||
|
@ -346,17 +332,23 @@ impl SmtpConnection {
|
|||
}
|
||||
}
|
||||
*/
|
||||
AsyncWrapper::new(Connection::Tls(conn))?
|
||||
AsyncWrapper::new(Connection::Tls(
|
||||
conn_result.chain_err_kind(crate::error::ErrorKind::Network)?,
|
||||
))
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?
|
||||
};
|
||||
ret.write_all(b"EHLO meli.delivery\r\n").await?;
|
||||
ret.write_all(b"EHLO meli.delivery\r\n")
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
ret
|
||||
}
|
||||
SmtpSecurity::None => {
|
||||
let addr = lookup_ipv4(path, server_conf.port)?;
|
||||
let mut ret = AsyncWrapper::new(Connection::Tcp(TcpStream::connect_timeout(
|
||||
&addr,
|
||||
std::time::Duration::new(4, 0),
|
||||
)?))?;
|
||||
let mut ret = AsyncWrapper::new(Connection::Tcp(
|
||||
TcpStream::connect_timeout(&addr, std::time::Duration::new(4, 0))
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?,
|
||||
))
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
res.clear();
|
||||
let reply = read_lines(
|
||||
&mut ret,
|
||||
|
@ -374,7 +366,9 @@ impl SmtpConnection {
|
|||
Reply::new(&res, code)
|
||||
)));
|
||||
}
|
||||
ret.write_all(b"EHLO meli.delivery\r\n").await?;
|
||||
ret.write_all(b"EHLO meli.delivery\r\n")
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
ret
|
||||
}
|
||||
};
|
||||
|
@ -398,55 +392,38 @@ impl SmtpConnection {
|
|||
return Err(MeliError::new(format!(
|
||||
"SMTP Server doesn't advertise Authentication support. Server response was: {:?}",
|
||||
pre_auth_extensions_reply
|
||||
)).set_kind(crate::error::ErrorKind::Authentication));
|
||||
)));
|
||||
}
|
||||
no_auth_needed =
|
||||
ret.server_conf.auth == SmtpAuth::None || !ret.server_conf.auth.require_auth();
|
||||
if no_auth_needed {
|
||||
ret.set_extension_support(pre_auth_extensions_reply);
|
||||
} else if let SmtpAuth::Auto {
|
||||
ref mut auth_type, ..
|
||||
} = ret.server_conf.auth
|
||||
{
|
||||
if let Some(l) = pre_auth_extensions_reply
|
||||
.lines
|
||||
.iter()
|
||||
.find(|l| l.starts_with("AUTH"))
|
||||
{
|
||||
let l = l["AUTH ".len()..].trim();
|
||||
for _type in l.split_whitespace() {
|
||||
if _type == "PLAIN" {
|
||||
auth_type.plain = true;
|
||||
} else if _type == "LOGIN" {
|
||||
auth_type.login = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !no_auth_needed {
|
||||
match &ret.server_conf.auth {
|
||||
SmtpAuth::None => {}
|
||||
SmtpAuth::Auto {
|
||||
username,
|
||||
password,
|
||||
auth_type,
|
||||
..
|
||||
username, password, ..
|
||||
} => {
|
||||
let password = match password {
|
||||
Password::Raw(p) => p.as_bytes().to_vec(),
|
||||
// # RFC 4616 The PLAIN SASL Mechanism
|
||||
// # https://www.ietf.org/rfc/rfc4616.txt
|
||||
// message = [authzid] UTF8NUL authcid UTF8NUL passwd
|
||||
// authcid = 1*SAFE ; MUST accept up to 255 octets
|
||||
// authzid = 1*SAFE ; MUST accept up to 255 octets
|
||||
// passwd = 1*SAFE ; MUST accept up to 255 octets
|
||||
// UTF8NUL = %x00 ; UTF-8 encoded NUL character
|
||||
let username_password = match password {
|
||||
Password::Raw(p) => base64::encode(format!("\0{}\0{}", username, p)),
|
||||
Password::CommandEval(command) => {
|
||||
let _command = command.clone();
|
||||
|
||||
let mut output = unblock(move || {
|
||||
Command::new("sh")
|
||||
.args(&["-c", &_command])
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.output()
|
||||
})
|
||||
.await?;
|
||||
let mut output = blocking!(Command::new("sh")
|
||||
.args(&["-c", &_command])
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.output())?;
|
||||
if !output.status.success() {
|
||||
return Err(MeliError::new(format!(
|
||||
"SMTP password evaluation command `{}` returned {}: {}",
|
||||
|
@ -455,77 +432,22 @@ impl SmtpConnection {
|
|||
String::from_utf8_lossy(&output.stderr)
|
||||
)));
|
||||
}
|
||||
if output.stdout.ends_with(b"\n") {
|
||||
output.stdout.pop();
|
||||
}
|
||||
output.stdout
|
||||
}
|
||||
};
|
||||
if auth_type.login {
|
||||
let username = username.to_string();
|
||||
ret.send_command(&[b"AUTH LOGIN"]).await?;
|
||||
ret.read_lines(&mut res, Some((ReplyCode::_334, &[])))
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Authentication)?;
|
||||
let buf = base64::encode(&username);
|
||||
ret.send_command(&[buf.as_bytes()]).await?;
|
||||
ret.read_lines(&mut res, Some((ReplyCode::_334, &[])))
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Authentication)?;
|
||||
let buf = base64::encode(&password);
|
||||
ret.send_command(&[buf.as_bytes()]).await?;
|
||||
} else {
|
||||
// # RFC 4616 The PLAIN SASL Mechanism
|
||||
// # https://www.ietf.org/rfc/rfc4616.txt
|
||||
// message = [authzid] UTF8NUL authcid UTF8NUL passwd
|
||||
// authcid = 1*SAFE ; MUST accept up to 255 octets
|
||||
// authzid = 1*SAFE ; MUST accept up to 255 octets
|
||||
// passwd = 1*SAFE ; MUST accept up to 255 octets
|
||||
// UTF8NUL = %x00 ; UTF-8 encoded NUL character
|
||||
let username_password = {
|
||||
let mut buf = Vec::with_capacity(2 + username.len() + password.len());
|
||||
let mut buf =
|
||||
Vec::with_capacity(2 + username.len() + output.stdout.len());
|
||||
buf.push(b'\0');
|
||||
buf.extend(username.as_bytes().to_vec());
|
||||
buf.push(b'\0');
|
||||
buf.extend(password);
|
||||
if output.stdout.ends_with(b"\n") {
|
||||
output.stdout.pop();
|
||||
}
|
||||
buf.extend(output.stdout);
|
||||
base64::encode(buf)
|
||||
};
|
||||
ret.send_command(&[b"AUTH PLAIN ", username_password.as_bytes()])
|
||||
.await?;
|
||||
}
|
||||
ret.read_lines(&mut res, Some((ReplyCode::_235, &[])))
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Authentication)?;
|
||||
ret.send_command(&[b"EHLO meli.delivery"]).await?;
|
||||
}
|
||||
SmtpAuth::XOAuth2 { token_command, .. } => {
|
||||
let password_token = {
|
||||
let _token_command = token_command.clone();
|
||||
let mut output = unblock(move || {
|
||||
Command::new("sh")
|
||||
.args(&["-c", &_token_command])
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.output()
|
||||
})
|
||||
.await?;
|
||||
if !output.status.success() {
|
||||
return Err(MeliError::new(format!(
|
||||
"SMTP XOAUTH2 token evaluation command `{}` returned {}: {}",
|
||||
&token_command,
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)));
|
||||
}
|
||||
if output.stdout.ends_with(b"\n") {
|
||||
output.stdout.pop();
|
||||
}
|
||||
output.stdout
|
||||
};
|
||||
// https://developers.google.com/gmail/imap/xoauth2-protocol#smtp_protocol_exchange
|
||||
ret.send_command(&[b"AUTH XOAUTH2 ", &password_token])
|
||||
.await?;
|
||||
let mut auth_command: SmallVec<[&[u8]; 16]> = SmallVec::new();
|
||||
auth_command.push(b"AUTH PLAIN ");
|
||||
auth_command.push(username_password.as_bytes());
|
||||
ret.send_command(&auth_command).await?;
|
||||
ret.read_lines(&mut res, Some((ReplyCode::_235, &[])))
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Authentication)?;
|
||||
|
@ -548,7 +470,6 @@ impl SmtpConnection {
|
|||
self.server_conf.extensions.pipelining &= reply.lines.contains(&"PIPELINING");
|
||||
self.server_conf.extensions.chunking &= reply.lines.contains(&"CHUNKING");
|
||||
self.server_conf.extensions.prdr &= reply.lines.contains(&"PRDR");
|
||||
self.server_conf.extensions._8bitmime &= reply.lines.contains(&"8BITMIME");
|
||||
self.server_conf.extensions.binarymime &= reply.lines.contains(&"BINARYMIME");
|
||||
self.server_conf.extensions.smtputf8 &= reply.lines.contains(&"SMTPUTF8");
|
||||
if !reply.lines.contains(&"DSN") {
|
||||
|
@ -582,10 +503,15 @@ impl SmtpConnection {
|
|||
// .trim()
|
||||
//);
|
||||
for c in command {
|
||||
self.stream.write_all(c).await?;
|
||||
self.stream
|
||||
.write_all(c)
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
}
|
||||
self.stream.write_all(b"\r\n").await?;
|
||||
Ok(())
|
||||
self.stream
|
||||
.write_all(b"\r\n")
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)
|
||||
}
|
||||
|
||||
/// Sends mail
|
||||
|
@ -599,7 +525,7 @@ impl SmtpConnection {
|
|||
let envelope = Envelope::from_bytes(mail.as_bytes(), None)
|
||||
.chain_err_summary(|| "SMTP submission was aborted")?;
|
||||
let tos = tos.unwrap_or_else(|| envelope.to());
|
||||
if tos.is_empty() && envelope.cc().is_empty() && envelope.bcc().is_empty() {
|
||||
if tos.is_empty() {
|
||||
return Err(MeliError::new("SMTP submission was aborted because there was no e-mail address found in the To: header field. Consider adding recipients."));
|
||||
}
|
||||
let mut current_command: SmallVec<[&[u8]; 16]> = SmallVec::new();
|
||||
|
@ -620,11 +546,6 @@ impl SmtpConnection {
|
|||
if self.server_conf.extensions.prdr {
|
||||
current_command.push(b" PRDR");
|
||||
}
|
||||
if self.server_conf.extensions.binarymime {
|
||||
current_command.push(b" BODY=BINARYMIME");
|
||||
} else if self.server_conf.extensions._8bitmime {
|
||||
current_command.push(b" BODY=8BITMIME");
|
||||
}
|
||||
self.send_command(¤t_command).await?;
|
||||
current_command.clear();
|
||||
if !self.server_conf.extensions.pipelining {
|
||||
|
@ -639,16 +560,12 @@ impl SmtpConnection {
|
|||
//return a reply indicating whether the failure is permanent (i.e., will occur again if
|
||||
//the client tries to send the same address again) or temporary (i.e., the address might
|
||||
//be accepted if the client tries again later).
|
||||
for addr in tos
|
||||
.iter()
|
||||
.chain(envelope.cc().iter())
|
||||
.chain(envelope.bcc().iter())
|
||||
{
|
||||
for addr in tos {
|
||||
current_command.clear();
|
||||
current_command.push(b"RCPT TO:<");
|
||||
current_command.push(addr.address_spec_raw().trim());
|
||||
if let Some(dsn_notify) = dsn_notify.as_ref() {
|
||||
current_command.push(b"> NOTIFY=");
|
||||
current_command.push(b" NOTIFY=");
|
||||
current_command.push(dsn_notify.as_bytes());
|
||||
} else {
|
||||
current_command.push(b">");
|
||||
|
@ -669,63 +586,71 @@ impl SmtpConnection {
|
|||
//permitted on either side of the colon following FROM in the MAIL command or TO in the
|
||||
//RCPT command. The syntax is exactly as given above.
|
||||
|
||||
if self.server_conf.extensions.binarymime {
|
||||
let mail_length = format!("{}", mail.as_bytes().len());
|
||||
self.send_command(&[b"BDAT", mail_length.as_bytes(), b"LAST"])
|
||||
.await?;
|
||||
self.stream.write_all(mail.as_bytes()).await?;
|
||||
} else {
|
||||
//The third step in the procedure is the DATA command
|
||||
//(or some alternative specified in a service extension).
|
||||
//DATA <CRLF>
|
||||
self.send_command(&[b"DATA"]).await?;
|
||||
//Client SMTP implementations that employ pipelining MUST check ALL statuses associated
|
||||
//with each command in a group. For example, if none of the RCPT TO recipient addresses
|
||||
//were accepted the client must then check the response to the DATA command -- the client
|
||||
//cannot assume that the DATA command will be rejected just because none of the RCPT TO
|
||||
//commands worked. If the DATA command was properly rejected the client SMTP can just
|
||||
//issue RSET, but if the DATA command was accepted the client SMTP should send a single
|
||||
//dot.
|
||||
let mut _all_error = self.server_conf.extensions.pipelining;
|
||||
let mut _any_error = false;
|
||||
let mut ignore_mailfrom = true;
|
||||
for expected_reply_code in pipelining_queue {
|
||||
let reply = self.read_lines(&mut res, expected_reply_code).await?;
|
||||
if !ignore_mailfrom {
|
||||
_all_error &= reply.code.is_err();
|
||||
_any_error |= reply.code.is_err();
|
||||
}
|
||||
ignore_mailfrom = false;
|
||||
pipelining_results.push(reply.into());
|
||||
//The third step in the procedure is the DATA command
|
||||
//(or some alternative specified in a service extension).
|
||||
//DATA <CRLF>
|
||||
self.send_command(&[b"DATA"]).await?;
|
||||
//Client SMTP implementations that employ pipelining MUST check ALL statuses associated
|
||||
//with each command in a group. For example, if none of the RCPT TO recipient addresses
|
||||
//were accepted the client must then check the response to the DATA command -- the client
|
||||
//cannot assume that the DATA command will be rejected just because none of the RCPT TO
|
||||
//commands worked. If the DATA command was properly rejected the client SMTP can just
|
||||
//issue RSET, but if the DATA command was accepted the client SMTP should send a single
|
||||
//dot.
|
||||
let mut _all_error = self.server_conf.extensions.pipelining;
|
||||
let mut _any_error = false;
|
||||
let mut ignore_mailfrom = true;
|
||||
for expected_reply_code in pipelining_queue {
|
||||
let reply = self.read_lines(&mut res, expected_reply_code).await?;
|
||||
if !ignore_mailfrom {
|
||||
_all_error &= reply.code.is_err();
|
||||
_any_error |= reply.code.is_err();
|
||||
}
|
||||
|
||||
//If accepted, the SMTP server returns a 354 Intermediate reply and considers all
|
||||
//succeeding lines up to but not including the end of mail data indicator to be the
|
||||
//message text. When the end of text is successfully received and stored, the
|
||||
//SMTP-receiver sends a "250 OK" reply.
|
||||
self.read_lines(&mut res, Some((ReplyCode::_354, &[])))
|
||||
.await?;
|
||||
|
||||
//Before sending a line of mail text, the SMTP client checks the first character of the
|
||||
//line.If it is a period, one additional period is inserted at the beginning of the line.
|
||||
for line in mail.lines() {
|
||||
if line.starts_with('.') {
|
||||
self.stream.write_all(b".").await?;
|
||||
}
|
||||
self.stream.write_all(line.as_bytes()).await?;
|
||||
self.stream.write_all(b"\r\n").await?;
|
||||
}
|
||||
|
||||
if !mail.ends_with('\n') {
|
||||
self.stream.write_all(b".\r\n").await?;
|
||||
}
|
||||
|
||||
//The mail data are terminated by a line containing only a period, that is, the character
|
||||
//sequence "<CRLF>.<CRLF>", where the first <CRLF> is actually the terminator of the
|
||||
//previous line (see Section 4.5.2). This is the end of mail data indication.
|
||||
self.stream.write_all(b".\r\n").await?;
|
||||
ignore_mailfrom = false;
|
||||
pipelining_results.push(reply.into());
|
||||
}
|
||||
|
||||
//If accepted, the SMTP server returns a 354 Intermediate reply and considers all
|
||||
//succeeding lines up to but not including the end of mail data indicator to be the
|
||||
//message text. When the end of text is successfully received and stored, the
|
||||
//SMTP-receiver sends a "250 OK" reply.
|
||||
self.read_lines(&mut res, Some((ReplyCode::_354, &[])))
|
||||
.await?;
|
||||
|
||||
//Before sending a line of mail text, the SMTP client checks the first character of the
|
||||
//line.If it is a period, one additional period is inserted at the beginning of the line.
|
||||
for line in mail.lines() {
|
||||
if line.starts_with('.') {
|
||||
self.stream
|
||||
.write_all(b".")
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
}
|
||||
self.stream
|
||||
.write_all(line.as_bytes())
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
self.stream
|
||||
.write_all(b"\r\n")
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
}
|
||||
|
||||
if !mail.ends_with('\n') {
|
||||
self.stream
|
||||
.write_all(b".\r\n")
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
}
|
||||
|
||||
//The mail data are terminated by a line containing only a period, that is, the character
|
||||
//sequence "<CRLF>.<CRLF>", where the first <CRLF> is actually the terminator of the
|
||||
//previous line (see Section 4.5.2). This is the end of mail data indication.
|
||||
self.stream
|
||||
.write_all(b".\r\n")
|
||||
.await
|
||||
.chain_err_kind(crate::error::ErrorKind::Network)?;
|
||||
|
||||
//The end of mail data indicator also confirms the mail transaction and tells the SMTP
|
||||
//server to now process the stored recipients and mail data. If accepted, the SMTP
|
||||
//server returns a "250 OK" reply.
|
||||
|
@ -779,8 +704,6 @@ pub enum ReplyCode {
|
|||
_251,
|
||||
///Cannot VRFY user, but will accept message and attempt delivery (See Section 3.5.3)
|
||||
_252,
|
||||
///rfc4954 AUTH continuation request
|
||||
_334,
|
||||
///PRDR specific, eg "content analysis has started|
|
||||
_353,
|
||||
///Start mail input; end with <CRLF>.<CRLF>
|
||||
|
@ -835,7 +758,6 @@ impl ReplyCode {
|
|||
_235 => "Authentication successful",
|
||||
_251 => "User not local; will forward",
|
||||
_252 => "Cannot VRFY user, but will accept message and attempt delivery",
|
||||
_334 => "Intermediate response to the AUTH command",
|
||||
_353 => "PRDR specific notice",
|
||||
_354 => "Start mail input; end with <CRLF>.<CRLF>",
|
||||
_421 => "Service not available, closing transmission channel",
|
||||
|
@ -861,26 +783,11 @@ impl ReplyCode {
|
|||
|
||||
fn is_err(&self) -> bool {
|
||||
use ReplyCode::*;
|
||||
matches!(
|
||||
self,
|
||||
_421 | _450
|
||||
| _451
|
||||
| _452
|
||||
| _455
|
||||
| _500
|
||||
| _501
|
||||
| _502
|
||||
| _503
|
||||
| _504
|
||||
| _535
|
||||
| _550
|
||||
| _551
|
||||
| _552
|
||||
| _553
|
||||
| _554
|
||||
| _555
|
||||
| _530
|
||||
)
|
||||
match self {
|
||||
_421 | _450 | _451 | _452 | _455 | _500 | _501 | _502 | _503 | _504 | _535 | _550
|
||||
| _551 | _552 | _553 | _554 | _555 | _530 => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -901,7 +808,6 @@ impl TryFrom<&'_ str> for ReplyCode {
|
|||
"250" => Ok(_250),
|
||||
"251" => Ok(_251),
|
||||
"252" => Ok(_252),
|
||||
"334" => Ok(_334),
|
||||
"354" => Ok(_354),
|
||||
"421" => Ok(_421),
|
||||
"450" => Ok(_450),
|
||||
|
@ -999,14 +905,11 @@ async fn read_lines<'r>(
|
|||
Ok(b) => {
|
||||
ret.push_str(unsafe { std::str::from_utf8_unchecked(&buf[0..b]) });
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(MeliError::from(err));
|
||||
Err(e) => {
|
||||
return Err(MeliError::from(e).set_kind(crate::error::ErrorKind::Network));
|
||||
}
|
||||
}
|
||||
}
|
||||
if ret.len() < 3 {
|
||||
return Err(MeliError::new(format!("Invalid SMTP reply: {}", ret)));
|
||||
}
|
||||
let code = ReplyCode::try_from(&ret[..3])?;
|
||||
let reply = Reply::new(ret, code);
|
||||
//debug!(&reply);
|
||||
|
@ -1024,219 +927,3 @@ async fn read_lines<'r>(
|
|||
}
|
||||
Ok(reply)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use mailin_embedded::{Handler, Response, Server, SslConfig};
|
||||
use std::net::IpAddr; //, Ipv4Addr, Ipv6Addr};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
const ADDRESS: &str = "127.0.0.1:8825";
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
Helo,
|
||||
Mail {
|
||||
from: String,
|
||||
},
|
||||
Rcpt {
|
||||
from: String,
|
||||
to: Vec<String>,
|
||||
},
|
||||
DataStart {
|
||||
from: String,
|
||||
to: Vec<String>,
|
||||
},
|
||||
Data {
|
||||
#[allow(dead_code)]
|
||||
from: String,
|
||||
to: Vec<String>,
|
||||
buf: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct MyHandler {
|
||||
mails: Arc<Mutex<Vec<((IpAddr, String), Message)>>>,
|
||||
stored: Arc<Mutex<Vec<(String, crate::Envelope)>>>,
|
||||
}
|
||||
use mailin_embedded::response::{INTERNAL_ERROR, OK};
|
||||
|
||||
impl Handler for MyHandler {
|
||||
fn helo(&mut self, ip: IpAddr, domain: &str) -> Response {
|
||||
eprintln!("helo ip {:?} domain {:?}", ip, domain);
|
||||
self.mails
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(((ip, domain.to_string()), Message::Helo));
|
||||
OK
|
||||
}
|
||||
|
||||
fn mail(&mut self, ip: IpAddr, domain: &str, from: &str) -> Response {
|
||||
eprintln!("mail() ip {:?} domain {:?} from {:?}", ip, domain, from);
|
||||
if let Some((_, message)) = self
|
||||
.mails
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter_mut()
|
||||
.find(|((i, d), _)| (i, d.as_str()) == (&ip, domain))
|
||||
{
|
||||
std::dbg!(&message);
|
||||
if let Message::Helo = message {
|
||||
*message = Message::Mail {
|
||||
from: from.to_string(),
|
||||
};
|
||||
return OK;
|
||||
}
|
||||
}
|
||||
INTERNAL_ERROR
|
||||
}
|
||||
|
||||
fn rcpt(&mut self, _to: &str) -> Response {
|
||||
eprintln!("rcpt() to {:?}", _to);
|
||||
if let Some((_, message)) = self.mails.lock().unwrap().last_mut() {
|
||||
std::dbg!(&message);
|
||||
if let Message::Mail { from } = message {
|
||||
*message = Message::Rcpt {
|
||||
from: from.clone(),
|
||||
to: vec![_to.to_string()],
|
||||
};
|
||||
return OK;
|
||||
} else if let Message::Rcpt { to, .. } = message {
|
||||
to.push(_to.to_string());
|
||||
return OK;
|
||||
}
|
||||
}
|
||||
INTERNAL_ERROR
|
||||
}
|
||||
|
||||
fn data_start(
|
||||
&mut self,
|
||||
_domain: &str,
|
||||
_from: &str,
|
||||
_is8bit: bool,
|
||||
_to: &[String],
|
||||
) -> Response {
|
||||
eprintln!(
|
||||
"data_start() domain {:?} from {:?} is8bit {:?} to {:?}",
|
||||
_domain, _from, _is8bit, _to
|
||||
);
|
||||
if let Some(((_, d), ref mut message)) = self.mails.lock().unwrap().last_mut() {
|
||||
if d != _domain {
|
||||
return INTERNAL_ERROR;
|
||||
}
|
||||
std::dbg!(&message);
|
||||
if let Message::Rcpt { from, to } = message {
|
||||
*message = Message::DataStart {
|
||||
from: from.to_string(),
|
||||
to: to.to_vec(),
|
||||
};
|
||||
return OK;
|
||||
}
|
||||
}
|
||||
INTERNAL_ERROR
|
||||
}
|
||||
|
||||
fn data(&mut self, _buf: &[u8]) -> std::result::Result<(), std::io::Error> {
|
||||
if let Some(((_, _), ref mut message)) = self.mails.lock().unwrap().last_mut() {
|
||||
if let Message::DataStart { from, to } = message {
|
||||
*message = Message::Data {
|
||||
from: from.to_string(),
|
||||
to: to.clone(),
|
||||
buf: _buf.to_vec(),
|
||||
};
|
||||
return Ok(());
|
||||
} else if let Message::Data { buf, .. } = message {
|
||||
buf.extend(_buf.into_iter().copied());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn data_end(&mut self) -> Response {
|
||||
eprintln!("datae_nd() ");
|
||||
if let Some(((_, _), message)) = self.mails.lock().unwrap().pop() {
|
||||
if let Message::Data { from: _, to, buf } = message {
|
||||
for to in to {
|
||||
match crate::Envelope::from_bytes(&buf, None) {
|
||||
Ok(env) => {
|
||||
std::dbg!(&env);
|
||||
std::dbg!(env.other_headers());
|
||||
self.stored.lock().unwrap().push((to.clone(), env));
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("envelope parse error {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
return OK;
|
||||
}
|
||||
}
|
||||
INTERNAL_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
fn get_smtp_conf() -> SmtpServerConf {
|
||||
SmtpServerConf {
|
||||
hostname: "127.0.0.1".into(),
|
||||
port: 8825,
|
||||
envelope_from: "foo-chat@example.com".into(),
|
||||
auth: SmtpAuth::None,
|
||||
security: SmtpSecurity::None,
|
||||
extensions: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_smtp() {
|
||||
stderrlog::new()
|
||||
.quiet(false)
|
||||
.verbosity(0)
|
||||
.show_module_names(true)
|
||||
.timestamp(stderrlog::Timestamp::Millisecond)
|
||||
.init()
|
||||
.unwrap();
|
||||
|
||||
let handler = MyHandler {
|
||||
mails: Arc::new(Mutex::new(vec![])),
|
||||
stored: Arc::new(Mutex::new(vec![])),
|
||||
};
|
||||
let handler2 = handler.clone();
|
||||
let _smtp_handle = thread::spawn(move || {
|
||||
let mut server = Server::new(handler2);
|
||||
|
||||
server
|
||||
.with_name("example.com")
|
||||
.with_ssl(SslConfig::None)
|
||||
.unwrap()
|
||||
.with_addr(ADDRESS)
|
||||
.unwrap();
|
||||
eprintln!("Running smtp server at {}", ADDRESS);
|
||||
server.serve().expect("Could not run server");
|
||||
});
|
||||
|
||||
let smtp_server_conf = get_smtp_conf();
|
||||
let input_str = include_str!("../test_sample_longmessage.eml");
|
||||
match crate::Envelope::from_bytes(input_str.as_bytes(), None) {
|
||||
Ok(_envelope) => {}
|
||||
Err(err) => {
|
||||
panic!("Could not parse message: {}", err);
|
||||
}
|
||||
}
|
||||
let mut connection =
|
||||
futures::executor::block_on(SmtpConnection::new_connection(smtp_server_conf)).unwrap();
|
||||
futures::executor::block_on(connection.mail_transaction(
|
||||
input_str,
|
||||
/*tos*/
|
||||
Some(&[
|
||||
Address::try_from("foo-chat@example.com").unwrap(),
|
||||
Address::try_from("webmaster@example.com").unwrap(),
|
||||
]),
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(handler.stored.lock().unwrap().len(), 2);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,9 +34,9 @@ pub struct DatabaseDescription {
|
|||
pub fn db_path(name: &str) -> Result<PathBuf> {
|
||||
let data_dir =
|
||||
xdg::BaseDirectories::with_prefix("meli").map_err(|e| MeliError::new(e.to_string()))?;
|
||||
data_dir
|
||||
Ok(data_dir
|
||||
.place_data_file(name)
|
||||
.map_err(|err| MeliError::new(err.to_string()))
|
||||
.map_err(|e| MeliError::new(e.to_string()))?)
|
||||
}
|
||||
|
||||
pub fn open_db(db_path: PathBuf) -> Result<Connection> {
|
||||
|
@ -50,65 +50,50 @@ pub fn open_or_create_db(
|
|||
description: &DatabaseDescription,
|
||||
identifier: Option<&str>,
|
||||
) -> Result<Connection> {
|
||||
let mut second_try: bool = false;
|
||||
loop {
|
||||
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| MeliError::new(e.to_string()))?;
|
||||
if set_mode {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let file = std::fs::File::open(&db_path)?;
|
||||
let metadata = file.metadata()?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
permissions.set_mode(0o600); // Read/write for owner only.
|
||||
file.set_permissions(permissions)?;
|
||||
}
|
||||
let version: i32 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
|
||||
if version != 0_i32 && version as u32 != description.version {
|
||||
log(
|
||||
format!(
|
||||
"Database version mismatch, is {} but expected {}",
|
||||
version, description.version
|
||||
),
|
||||
crate::INFO,
|
||||
);
|
||||
if second_try {
|
||||
return Err(MeliError::new(format!(
|
||||
"Database version mismatch, is {} but expected {}. Could not recreate database.",
|
||||
version, description.version
|
||||
)));
|
||||
}
|
||||
reset_db(description, identifier)?;
|
||||
second_try = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if version == 0 {
|
||||
conn.pragma_update(None, "user_version", &description.version)?;
|
||||
}
|
||||
if let Some(s) = description.init_script {
|
||||
conn.execute_batch(s)
|
||||
.map_err(|e| MeliError::new(e.to_string()))?;
|
||||
}
|
||||
|
||||
return Ok(conn);
|
||||
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| MeliError::new(e.to_string()))?;
|
||||
if set_mode {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let file = std::fs::File::open(&db_path)?;
|
||||
let metadata = file.metadata()?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
permissions.set_mode(0o600); // Read/write for owner only.
|
||||
file.set_permissions(permissions)?;
|
||||
}
|
||||
let version: i32 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
|
||||
if version != 0_i32 && version as u32 != description.version {
|
||||
return Err(MeliError::new(format!(
|
||||
"Database version mismatch, is {} but expected {}",
|
||||
version, description.version
|
||||
)));
|
||||
}
|
||||
|
||||
if version == 0 {
|
||||
conn.pragma_update(None, "user_version", &description.version)?;
|
||||
}
|
||||
if let Some(s) = description.init_script {
|
||||
conn.execute_batch(s)
|
||||
.map_err(|e| MeliError::new(e.to_string()))?;
|
||||
}
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
/// Return database to a clean slate.
|
||||
|
@ -135,27 +120,17 @@ pub fn reset_db(description: &DatabaseDescription, identifier: Option<&str>) ->
|
|||
|
||||
impl ToSql for Envelope {
|
||||
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
|
||||
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())))
|
||||
})?;
|
||||
let v: Vec<u8> = bincode::serialize(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)?;
|
||||
|
||||
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)))
|
||||
Ok(bincode::deserialize(&b)
|
||||
.map_err(|e| FromSqlError::Other(Box::new(MeliError::new(e.to_string()))))?)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,11 +35,11 @@ extern crate unicode_segmentation;
|
|||
use self::unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
pub trait TextProcessing: UnicodeSegmentation + CodePointsIter {
|
||||
fn split_graphemes(&self) -> Vec<&str> {
|
||||
fn split_graphemes<'a>(&'a self) -> Vec<&'a str> {
|
||||
UnicodeSegmentation::graphemes(self, true).collect::<Vec<&str>>()
|
||||
}
|
||||
|
||||
fn graphemes_indices(&self) -> Vec<(usize, &str)> {
|
||||
fn graphemes_indices<'a>(&'a self) -> Vec<(usize, &'a str)> {
|
||||
UnicodeSegmentation::grapheme_indices(self, true).collect::<Vec<(usize, &str)>>()
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@ use super::types::Reflow;
|
|||
use core::cmp::Ordering;
|
||||
use core::iter::Peekable;
|
||||
use core::str::FromStr;
|
||||
use std::collections::VecDeque;
|
||||
use LineBreakClass::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Copy, Clone)]
|
||||
|
@ -128,7 +127,7 @@ trait EvenAfterSpaces {
|
|||
impl EvenAfterSpaces for str {
|
||||
fn even_after_spaces(&self) -> &Self {
|
||||
let mut ret = self;
|
||||
while !ret.is_empty() && get_class!(ret) != SP {
|
||||
while !ret.is_empty() && get_class!(&ret) != SP {
|
||||
ret = &ret[get_base_character!(ret).unwrap().len_utf8()..];
|
||||
}
|
||||
ret
|
||||
|
@ -158,7 +157,7 @@ impl<'a> Iterator for LineBreakCandidateIter<'a> {
|
|||
}
|
||||
$last_break = $pos;
|
||||
};
|
||||
}
|
||||
};
|
||||
// After end of text, there are no breaks.
|
||||
if self.pos > self.text.len() {
|
||||
return None;
|
||||
|
@ -173,7 +172,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,
|
||||
|
@ -972,8 +971,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;
|
||||
|
@ -997,8 +995,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 {
|
||||
|
@ -1104,23 +1102,12 @@ pub fn split_lines_reflow(text: &str, reflow: Reflow, width: Option<usize>) -> V
|
|||
split(&mut ret, line, width);
|
||||
continue;
|
||||
}
|
||||
let segment_tree = {
|
||||
use std::iter::FromIterator;
|
||||
let mut t: smallvec::SmallVec<[usize; 1024]> =
|
||||
smallvec::SmallVec::from_iter(std::iter::repeat(0).take(line.len()));
|
||||
for (idx, _g) in UnicodeSegmentation::grapheme_indices(line, true) {
|
||||
t[idx] = 1;
|
||||
}
|
||||
Box::new(segment_tree::SegmentTree::new(t))
|
||||
};
|
||||
|
||||
let mut prev = 0;
|
||||
let mut prev_line_offset = 0;
|
||||
while prev < breaks.len() {
|
||||
let new_off = match breaks[prev..].binary_search_by(|(offset, _)| {
|
||||
segment_tree
|
||||
.get_sum(prev_line_offset, offset.saturating_sub(1))
|
||||
.cmp(&width)
|
||||
line[prev_line_offset..*offset].grapheme_len().cmp(&width)
|
||||
}) {
|
||||
Ok(v) => v,
|
||||
Err(v) => v,
|
||||
|
@ -1180,8 +1167,8 @@ fn reflow_helper(
|
|||
let paragraph = paragraph
|
||||
.trim_start_matches("es)
|
||||
.replace(&format!("\n{}", "es), "")
|
||||
.replace('\n', "")
|
||||
.replace('\r', "");
|
||||
.replace("\n", "")
|
||||
.replace("\r", "");
|
||||
if in_paragraph {
|
||||
if let Some(width) = width {
|
||||
ret.extend(
|
||||
|
@ -1196,7 +1183,7 @@ fn reflow_helper(
|
|||
ret.push(format!("{}{}", "es, ¶graph));
|
||||
}
|
||||
} else {
|
||||
let paragraph = paragraph.replace('\n', "").replace('\r', "");
|
||||
let paragraph = paragraph.replace("\n", "").replace("\r", "");
|
||||
|
||||
if in_paragraph {
|
||||
if let Some(width) = width {
|
||||
|
@ -1246,552 +1233,3 @@ easy to take MORE than nothing.'"#;
|
|||
println!("{}", l);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
array: SmallVec<[usize; 1024]>,
|
||||
tree: SmallVec<[usize; 1024]>,
|
||||
}
|
||||
|
||||
impl SegmentTree {
|
||||
pub(super) fn new(val: SmallVec<[usize; 1024]>) -> SegmentTree {
|
||||
if val.is_empty() {
|
||||
return SegmentTree {
|
||||
array: val.clone(),
|
||||
tree: val,
|
||||
};
|
||||
}
|
||||
|
||||
let height = (f64::from(u32::try_from(val.len()).unwrap_or(0)))
|
||||
.log2()
|
||||
.ceil() as u32;
|
||||
let max_size = 2 * (2_usize.pow(height));
|
||||
|
||||
let mut segment_tree: SmallVec<[usize; 1024]> =
|
||||
SmallVec::from_iter(core::iter::repeat(0).take(max_size));
|
||||
for i in 0..val.len() {
|
||||
segment_tree[val.len() + i] = val[i];
|
||||
}
|
||||
|
||||
for i in (1..val.len()).rev() {
|
||||
segment_tree[i] = segment_tree[2 * i] + segment_tree[2 * i + 1];
|
||||
}
|
||||
|
||||
SegmentTree {
|
||||
array: val,
|
||||
tree: segment_tree,
|
||||
}
|
||||
}
|
||||
|
||||
/// (left, right) is inclusive
|
||||
pub(super) fn get_sum(&self, mut left: usize, mut right: usize) -> usize {
|
||||
if self.array.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let len = self.array.len();
|
||||
if left > right {
|
||||
return 0;
|
||||
}
|
||||
if right >= len {
|
||||
right = len.saturating_sub(1);
|
||||
}
|
||||
|
||||
left += len;
|
||||
right += len + 1;
|
||||
|
||||
let mut sum = 0;
|
||||
|
||||
while left < right {
|
||||
if (left & 1) > 0 {
|
||||
sum += self.tree[left];
|
||||
left += 1;
|
||||
}
|
||||
|
||||
if (right & 1) > 0 {
|
||||
right -= 1;
|
||||
sum += self.tree[right];
|
||||
}
|
||||
|
||||
left /= 2;
|
||||
right /= 2;
|
||||
}
|
||||
sum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
reflow: Reflow,
|
||||
paragraph: VecDeque<String>,
|
||||
paragraph_start_index: usize,
|
||||
width: Option<usize>,
|
||||
state: ReflowState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum ReflowState {
|
||||
No {
|
||||
cur_index: usize,
|
||||
},
|
||||
AllWidth {
|
||||
width: usize,
|
||||
state: LineBreakTextState,
|
||||
},
|
||||
All {
|
||||
cur_index: usize,
|
||||
},
|
||||
FormatFlowed {
|
||||
cur_index: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl ReflowState {
|
||||
fn new(reflow: Reflow, width: Option<usize>, cur_index: usize) -> ReflowState {
|
||||
match reflow {
|
||||
Reflow::All if width.is_some() => ReflowState::AllWidth {
|
||||
width: width.unwrap(),
|
||||
state: LineBreakTextState::AtLine { cur_index },
|
||||
},
|
||||
Reflow::All => ReflowState::All { cur_index },
|
||||
Reflow::FormatFlowed => ReflowState::FormatFlowed { cur_index },
|
||||
Reflow::No => ReflowState::No { cur_index },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum LineBreakTextState {
|
||||
AtLine {
|
||||
cur_index: usize,
|
||||
},
|
||||
WithinLine {
|
||||
line_index: usize,
|
||||
line_length: usize,
|
||||
within_line_index: usize,
|
||||
breaks: Vec<(usize, LineBreakCandidate)>,
|
||||
prev_break: usize,
|
||||
segment_tree: Box<segment_tree::SegmentTree>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for LineBreakText {
|
||||
fn default() -> Self {
|
||||
Self::new(String::new(), Reflow::default(), None)
|
||||
}
|
||||
}
|
||||
|
||||
impl LineBreakText {
|
||||
pub fn new(text: String, reflow: Reflow, width: Option<usize>) -> Self {
|
||||
LineBreakText {
|
||||
text,
|
||||
state: ReflowState::new(reflow, width, 0),
|
||||
paragraph: VecDeque::new(),
|
||||
paragraph_start_index: 0,
|
||||
reflow,
|
||||
width,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(&self) -> Option<usize> {
|
||||
self.width
|
||||
}
|
||||
|
||||
pub fn set_reflow(&mut self, new_val: Reflow) -> &mut Self {
|
||||
self.reflow = new_val;
|
||||
self.paragraph.clear();
|
||||
self.state = ReflowState::new(self.reflow, self.width, self.paragraph_start_index);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_width(&mut self, new_val: Option<usize>) -> &mut Self {
|
||||
self.width = new_val;
|
||||
self.paragraph.clear();
|
||||
self.state = ReflowState::new(self.reflow, self.width, self.paragraph_start_index);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, new_val: String) -> &mut Self {
|
||||
self.text = new_val;
|
||||
self.reset()
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) -> &mut Self {
|
||||
self.paragraph.clear();
|
||||
self.state = ReflowState::new(self.reflow, self.width, 0);
|
||||
self.paragraph_start_index = 0;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_finished(&self) -> bool {
|
||||
match self.state {
|
||||
ReflowState::No { cur_index }
|
||||
| ReflowState::All { cur_index }
|
||||
| ReflowState::FormatFlowed { cur_index }
|
||||
| ReflowState::AllWidth {
|
||||
width: _,
|
||||
state: LineBreakTextState::AtLine { cur_index },
|
||||
} => cur_index >= self.text.len(),
|
||||
ReflowState::AllWidth {
|
||||
width: _,
|
||||
state: LineBreakTextState::WithinLine { .. },
|
||||
} => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for LineBreakText {
|
||||
type Item = String;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if !self.paragraph.is_empty() {
|
||||
return self.paragraph.pop_front();
|
||||
}
|
||||
if self.is_finished() {
|
||||
return None;
|
||||
}
|
||||
match self.state {
|
||||
ReflowState::FormatFlowed { ref mut cur_index } => {
|
||||
/* rfc3676 - The Text/Plain Format and DelSp Parameters
|
||||
* https://tools.ietf.org/html/rfc3676 */
|
||||
|
||||
/*
|
||||
* - Split lines with indices using str::match_indices()
|
||||
* - Iterate and reflow flow regions, and pass fixed regions through
|
||||
*/
|
||||
self.paragraph_start_index = *cur_index;
|
||||
let line_indices_iter = self.text[*cur_index..].match_indices('\n').map(|(i, _)| i);
|
||||
let start_offset = *cur_index;
|
||||
let mut prev_index = *cur_index;
|
||||
let mut in_paragraph = false;
|
||||
let mut paragraph_start = *cur_index;
|
||||
|
||||
let mut prev_quote_depth = 0;
|
||||
let mut paragraph = VecDeque::new();
|
||||
for i in line_indices_iter {
|
||||
let i = i + start_offset + 1;
|
||||
let line = &self.text[prev_index..i];
|
||||
let mut trimmed = line.trim_start().lines().next().unwrap_or("");
|
||||
let mut quote_depth = 0;
|
||||
let p_str: usize = trimmed
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.position(|&b| {
|
||||
if b != b'>' {
|
||||
/* position() is short-circuiting */
|
||||
true
|
||||
} else {
|
||||
quote_depth += 1;
|
||||
false
|
||||
}
|
||||
})
|
||||
.unwrap_or(0);
|
||||
trimmed = &trimmed[p_str..];
|
||||
if trimmed.starts_with(' ') {
|
||||
/* Remove space stuffing before checking for ending space character.
|
||||
* [rfc3676#section-4.4] */
|
||||
trimmed = &trimmed[1..];
|
||||
}
|
||||
|
||||
if trimmed.ends_with(' ') {
|
||||
if !in_paragraph {
|
||||
in_paragraph = true;
|
||||
paragraph_start = prev_index;
|
||||
} 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. */
|
||||
let paragraph_s = &self.text[paragraph_start..prev_index];
|
||||
reflow_helper2(
|
||||
&mut paragraph,
|
||||
paragraph_s,
|
||||
prev_quote_depth,
|
||||
in_paragraph,
|
||||
self.width,
|
||||
);
|
||||
|
||||
paragraph_start = prev_index;
|
||||
}
|
||||
} else {
|
||||
if prev_quote_depth == quote_depth || !in_paragraph {
|
||||
let paragraph_s = &self.text[paragraph_start..i];
|
||||
reflow_helper2(
|
||||
&mut paragraph,
|
||||
paragraph_s,
|
||||
quote_depth,
|
||||
in_paragraph,
|
||||
self.width,
|
||||
);
|
||||
} else {
|
||||
/*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,
|
||||
paragraph_s,
|
||||
prev_quote_depth,
|
||||
in_paragraph,
|
||||
self.width,
|
||||
);
|
||||
let paragraph_s = &self.text[prev_index..i];
|
||||
reflow_helper2(
|
||||
&mut paragraph,
|
||||
paragraph_s,
|
||||
quote_depth,
|
||||
false,
|
||||
self.width,
|
||||
);
|
||||
}
|
||||
*cur_index = i;
|
||||
std::mem::swap(&mut self.paragraph, &mut paragraph);
|
||||
paragraph_start = i;
|
||||
in_paragraph = false;
|
||||
break;
|
||||
}
|
||||
*cur_index = i;
|
||||
prev_quote_depth = quote_depth;
|
||||
prev_index = i;
|
||||
}
|
||||
if in_paragraph {
|
||||
let paragraph_s = &self.text[paragraph_start..self.text.len()];
|
||||
*cur_index = self.text.len();
|
||||
reflow_helper2(
|
||||
&mut paragraph,
|
||||
paragraph_s,
|
||||
prev_quote_depth,
|
||||
in_paragraph,
|
||||
self.width,
|
||||
);
|
||||
self.paragraph = paragraph;
|
||||
}
|
||||
self.paragraph.pop_front()
|
||||
}
|
||||
ReflowState::AllWidth {
|
||||
width,
|
||||
ref mut state,
|
||||
} => {
|
||||
let width = width.saturating_sub(2);
|
||||
|
||||
loop {
|
||||
let line: &str;
|
||||
let cur_index: &mut usize;
|
||||
let within_line_index: &mut usize;
|
||||
let prev_break: &mut usize;
|
||||
let segment_tree: &segment_tree::SegmentTree;
|
||||
let breaks: &Vec<(usize, LineBreakCandidate)>;
|
||||
match state {
|
||||
LineBreakTextState::AtLine {
|
||||
cur_index: ref mut _cur_index,
|
||||
} => {
|
||||
line = if let Some(line) = self
|
||||
.text
|
||||
.get(*_cur_index..)
|
||||
.and_then(|slice| slice.split('\n').next())
|
||||
{
|
||||
line
|
||||
} else {
|
||||
*_cur_index = self.text.len();
|
||||
return None;
|
||||
};
|
||||
let _cur_index = *_cur_index;
|
||||
*state = LineBreakTextState::WithinLine {
|
||||
line_index: _cur_index,
|
||||
line_length: line.len(),
|
||||
within_line_index: 0,
|
||||
breaks: LineBreakCandidateIter::new(line).collect::<Vec<(
|
||||
usize,
|
||||
LineBreakCandidate,
|
||||
)>>(
|
||||
),
|
||||
prev_break: 0,
|
||||
segment_tree: {
|
||||
use std::iter::FromIterator;
|
||||
let mut t: smallvec::SmallVec<[usize; 1024]> =
|
||||
smallvec::SmallVec::from_iter(
|
||||
std::iter::repeat(0).take(line.len()),
|
||||
);
|
||||
for (idx, _g) in
|
||||
UnicodeSegmentation::grapheme_indices(line, true)
|
||||
{
|
||||
t[idx] = 1;
|
||||
}
|
||||
Box::new(segment_tree::SegmentTree::new(t))
|
||||
},
|
||||
};
|
||||
if let LineBreakTextState::WithinLine {
|
||||
ref mut line_index,
|
||||
line_length: _,
|
||||
within_line_index: ref mut _within_line_index,
|
||||
breaks: ref _breaks,
|
||||
prev_break: ref mut _prev_break,
|
||||
segment_tree: ref _segment_tree,
|
||||
} = state
|
||||
{
|
||||
cur_index = line_index;
|
||||
within_line_index = _within_line_index;
|
||||
breaks = _breaks;
|
||||
prev_break = _prev_break;
|
||||
|
||||
segment_tree = _segment_tree;
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
LineBreakTextState::WithinLine {
|
||||
ref mut line_index,
|
||||
ref line_length,
|
||||
within_line_index: ref mut _within_line_index,
|
||||
breaks: ref _breaks,
|
||||
prev_break: ref mut _prev_break,
|
||||
segment_tree: ref _segment_tree,
|
||||
} => {
|
||||
line = &self.text[*line_index..(*line_index + *line_length)];
|
||||
cur_index = line_index;
|
||||
within_line_index = _within_line_index;
|
||||
breaks = _breaks;
|
||||
prev_break = _prev_break;
|
||||
segment_tree = _segment_tree;
|
||||
}
|
||||
}
|
||||
|
||||
if segment_tree.get_sum(0, line.len()) <= width {
|
||||
*state = LineBreakTextState::AtLine {
|
||||
cur_index: *cur_index + line.len() + 1,
|
||||
};
|
||||
return Some(
|
||||
line.trim_end_matches(|c| c == '\r' || c == '\n')
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if breaks.len() < 2 {
|
||||
let mut line = line;
|
||||
while !line.is_empty() {
|
||||
let mut chop_index = std::cmp::min(line.len().saturating_sub(1), width);
|
||||
while chop_index > 0 && !line.is_char_boundary(chop_index) {
|
||||
chop_index -= 1;
|
||||
}
|
||||
if chop_index == 0 {
|
||||
self.paragraph.push_back(format!("⤷{}", line));
|
||||
*cur_index += line.len();
|
||||
break;
|
||||
} else {
|
||||
self.paragraph
|
||||
.push_back(format!("⤷{}", &line[..chop_index]));
|
||||
*cur_index += chop_index;
|
||||
}
|
||||
line = &line[chop_index..];
|
||||
}
|
||||
*state = LineBreakTextState::AtLine {
|
||||
cur_index: *cur_index,
|
||||
};
|
||||
if !self.paragraph.is_empty() {
|
||||
return self.paragraph.pop_front();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
while *prev_break < breaks.len() {
|
||||
let new_off = match breaks[*prev_break..].binary_search_by(|(offset, _)| {
|
||||
segment_tree
|
||||
.get_sum(*within_line_index, offset.saturating_sub(1))
|
||||
.cmp(&width)
|
||||
}) {
|
||||
Ok(v) => v,
|
||||
Err(v) => v,
|
||||
} + *prev_break;
|
||||
let end_offset = if new_off >= breaks.len() {
|
||||
line.len()
|
||||
} else {
|
||||
breaks[new_off].0
|
||||
};
|
||||
if !line[*within_line_index..end_offset].is_empty() {
|
||||
if *within_line_index == 0 {
|
||||
let ret = line[*within_line_index..end_offset]
|
||||
.trim_end_matches(|c| c == '\r' || c == '\n');
|
||||
*within_line_index = end_offset;
|
||||
return Some(ret.to_string());
|
||||
} else {
|
||||
let ret = format!(
|
||||
"⤷{}",
|
||||
&line[*within_line_index..end_offset]
|
||||
.trim_end_matches(|c| c == '\r' || c == '\n')
|
||||
);
|
||||
*within_line_index = end_offset;
|
||||
return Some(ret);
|
||||
}
|
||||
}
|
||||
if *within_line_index == end_offset && *prev_break == new_off {
|
||||
break;
|
||||
}
|
||||
*within_line_index = end_offset + 1;
|
||||
*prev_break = new_off;
|
||||
}
|
||||
*state = LineBreakTextState::AtLine {
|
||||
cur_index: *cur_index + line.len() + 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
ReflowState::No { ref mut cur_index } | ReflowState::All { ref mut cur_index } => {
|
||||
if let Some(line) = self.text[*cur_index..].split('\n').next() {
|
||||
let ret = line.to_string();
|
||||
*cur_index += line.len() + 2;
|
||||
return Some(ret);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn reflow_helper2(
|
||||
ret: &mut VecDeque<String>,
|
||||
paragraph: &str,
|
||||
quote_depth: usize,
|
||||
in_paragraph: bool,
|
||||
width: Option<usize>,
|
||||
) {
|
||||
if quote_depth > 0 {
|
||||
let quotes: String = ">".repeat(quote_depth);
|
||||
let paragraph = paragraph
|
||||
.trim_start_matches("es)
|
||||
.replace(&format!("\n{}", "es), "")
|
||||
.replace('\n', "")
|
||||
.replace('\r', "");
|
||||
if in_paragraph {
|
||||
if let Some(width) = width {
|
||||
ret.extend(
|
||||
linear(¶graph, width.saturating_sub(quote_depth))
|
||||
.into_iter()
|
||||
.map(|l| format!("{}{}", "es, l)),
|
||||
);
|
||||
} else {
|
||||
ret.push_back(format!("{}{}", "es, ¶graph));
|
||||
}
|
||||
} else {
|
||||
ret.push_back(format!("{}{}", "es, ¶graph));
|
||||
}
|
||||
} else {
|
||||
let paragraph = paragraph.replace('\n', "").replace('\r', "");
|
||||
|
||||
if in_paragraph {
|
||||
if let Some(width) = width {
|
||||
let ex = linear(¶graph, width);
|
||||
ret.extend(ex.into_iter());
|
||||
} else {
|
||||
ret.push_back(paragraph);
|
||||
}
|
||||
} else {
|
||||
ret.push_back(paragraph);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,8 +33,6 @@ pub use wcwidth::*;
|
|||
pub trait Truncate {
|
||||
fn truncate_at_boundary(&mut self, new_len: usize);
|
||||
fn trim_at_boundary(&self, new_len: usize) -> &str;
|
||||
fn trim_left_at_boundary(&self, new_len: usize) -> &str;
|
||||
fn truncate_left_at_boundary(&mut self, new_len: usize);
|
||||
}
|
||||
|
||||
impl Truncate for &str {
|
||||
|
@ -69,33 +67,6 @@ impl Truncate for &str {
|
|||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn trim_left_at_boundary(&self, skip_len: usize) -> &str {
|
||||
if skip_len >= self.len() {
|
||||
return "";
|
||||
}
|
||||
|
||||
extern crate unicode_segmentation;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(*self, true).nth(skip_len) {
|
||||
&self[first..]
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn truncate_left_at_boundary(&mut self, skip_len: usize) {
|
||||
if skip_len >= self.len() {
|
||||
*self = "";
|
||||
return;
|
||||
}
|
||||
|
||||
extern crate unicode_segmentation;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
if let Some((first, _)) = UnicodeSegmentation::grapheme_indices(*self, true).nth(skip_len) {
|
||||
*self = &self[first..];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Truncate for String {
|
||||
|
@ -130,37 +101,6 @@ impl Truncate for String {
|
|||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
fn trim_left_at_boundary(&self, skip_len: usize) -> &str {
|
||||
if skip_len >= self.len() {
|
||||
return "";
|
||||
}
|
||||
|
||||
extern crate unicode_segmentation;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
if let Some((first, _)) =
|
||||
UnicodeSegmentation::grapheme_indices(self.as_str(), true).nth(skip_len)
|
||||
{
|
||||
&self[first..]
|
||||
} else {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
fn truncate_left_at_boundary(&mut self, skip_len: usize) {
|
||||
if skip_len >= self.len() {
|
||||
self.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
extern crate unicode_segmentation;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
if let Some((first, _)) =
|
||||
UnicodeSegmentation::grapheme_indices(self.as_str(), true).nth(skip_len)
|
||||
{
|
||||
*self = self[first..].to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait GlobMatch {
|
||||
|
@ -170,11 +110,17 @@ pub trait GlobMatch {
|
|||
|
||||
impl GlobMatch for str {
|
||||
fn matches_glob(&self, _pattern: &str) -> bool {
|
||||
let pattern: Vec<&str> = _pattern
|
||||
.strip_suffix('/')
|
||||
.unwrap_or(_pattern)
|
||||
.split_graphemes();
|
||||
let s: Vec<&str> = self.strip_suffix('/').unwrap_or(self).split_graphemes();
|
||||
macro_rules! strip_slash {
|
||||
($v:expr) => {
|
||||
if $v.ends_with("/") {
|
||||
&$v[..$v.len() - 1]
|
||||
} else {
|
||||
$v
|
||||
}
|
||||
};
|
||||
}
|
||||
let pattern: Vec<&str> = strip_slash!(_pattern).split_graphemes();
|
||||
let s: Vec<&str> = strip_slash!(self).split_graphemes();
|
||||
|
||||
// Taken from https://research.swtch.com/glob
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -19,7 +19,6 @@
|
|||
* along with meli. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum LineBreakClass {
|
||||
BK,
|
||||
|
|
|
@ -44,7 +44,7 @@ type WChar = u32;
|
|||
type Interval = (WChar, WChar);
|
||||
|
||||
pub struct CodePointsIterator<'a> {
|
||||
rest: std::str::Chars<'a>,
|
||||
rest: &'a [u8],
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -61,7 +61,36 @@ impl<'a> Iterator for CodePointsIterator<'a> {
|
|||
type Item = WChar;
|
||||
|
||||
fn next(&mut self) -> Option<WChar> {
|
||||
self.rest.next().map(|c| c as WChar)
|
||||
if self.rest.is_empty() {
|
||||
return None;
|
||||
}
|
||||
/* Input is UTF-8 valid strings, guaranteed by Rust's std */
|
||||
if self.rest[0] & 0b1000_0000 == 0x0 {
|
||||
let ret: WChar = WChar::from(self.rest[0]);
|
||||
self.rest = &self.rest[1..];
|
||||
return Some(ret);
|
||||
}
|
||||
if self.rest[0] & 0b1110_0000 == 0b1100_0000 {
|
||||
let ret: WChar = (WChar::from(self.rest[0]) & 0b0001_1111).rotate_left(6)
|
||||
+ (WChar::from(self.rest[1]) & 0b0111_1111);
|
||||
self.rest = &self.rest[2..];
|
||||
return Some(ret);
|
||||
}
|
||||
|
||||
if self.rest[0] & 0b1111_0000 == 0b1110_0000 {
|
||||
let ret: WChar = (WChar::from(self.rest[0]) & 0b0000_0111).rotate_left(12)
|
||||
+ (WChar::from(self.rest[1]) & 0b0011_1111).rotate_left(6)
|
||||
+ (WChar::from(self.rest[2]) & 0b0011_1111);
|
||||
self.rest = &self.rest[3..];
|
||||
return Some(ret);
|
||||
}
|
||||
|
||||
let ret: WChar = (WChar::from(self.rest[0]) & 0b0000_0111).rotate_left(18)
|
||||
+ (WChar::from(self.rest[1]) & 0b0011_1111).rotate_left(12)
|
||||
+ (WChar::from(self.rest[2]) & 0b0011_1111).rotate_left(6)
|
||||
+ (WChar::from(self.rest[3]) & 0b0011_1111);
|
||||
self.rest = &self.rest[4..];
|
||||
Some(ret)
|
||||
}
|
||||
}
|
||||
pub trait CodePointsIter {
|
||||
|
@ -70,12 +99,16 @@ pub trait CodePointsIter {
|
|||
|
||||
impl CodePointsIter for str {
|
||||
fn code_points(&self) -> CodePointsIterator {
|
||||
CodePointsIterator { rest: self.chars() }
|
||||
CodePointsIterator {
|
||||
rest: self.as_bytes(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl CodePointsIter for &str {
|
||||
fn code_points(&self) -> CodePointsIterator {
|
||||
CodePointsIterator { rest: self.chars() }
|
||||
CodePointsIterator {
|
||||
rest: self.as_bytes(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,54 +136,174 @@ fn bisearch(ucs: WChar, table: &'static [Interval]) -> bool {
|
|||
false
|
||||
}
|
||||
|
||||
pub fn wcwidth(ucs: WChar) -> Option<usize> {
|
||||
if bisearch(ucs, super::tables::ASCII) {
|
||||
Some(1)
|
||||
} else if bisearch(ucs, super::tables::PRIVATE) {
|
||||
None
|
||||
} else if bisearch(ucs, super::tables::NONPRINT) {
|
||||
None
|
||||
} else if bisearch(ucs, super::tables::COMBINING) {
|
||||
None
|
||||
} else if bisearch(ucs, super::tables::DOUBLEWIDE) {
|
||||
Some(2)
|
||||
} else if bisearch(ucs, super::tables::AMBIGUOUS) {
|
||||
Some(1)
|
||||
} else if bisearch(ucs, super::tables::UNASSIGNED) {
|
||||
Some(2)
|
||||
} else if bisearch(ucs, super::tables::WIDENEDIN9) {
|
||||
Some(2)
|
||||
} else {
|
||||
Some(1)
|
||||
}
|
||||
}
|
||||
/* The following functions define the column width of an ISO 10646
|
||||
* character as follows:
|
||||
*
|
||||
* - The null character (U+0000) has a column width of 0.
|
||||
*
|
||||
* - Other C0/C1 control characters and DEL will lead to a return
|
||||
* value of -1.
|
||||
*
|
||||
* - Non-spacing and enclosing combining characters (general
|
||||
* category code Mn or Me in the Unicode database) have a
|
||||
* column width of 0.
|
||||
*
|
||||
* - Other format characters (general category code Cf in the Unicode
|
||||
* database) and ZERO WIDTH SPACE (U+200B) have a column width of 0.
|
||||
*
|
||||
* - Hangul Jamo medial vowels and final consonants (U+1160-U+11FF)
|
||||
* have a column width of 0.
|
||||
*
|
||||
* - Spacing characters in the East Asian Wide (W) or East Asian
|
||||
* FullWidth (F) category as defined in Unicode Technical
|
||||
* Report #11 have a column width of 2.
|
||||
*
|
||||
* - All remaining characters (including all printable
|
||||
* ISO 8859-1 and WGL4 characters, Unicode control characters,
|
||||
* etc.) have a column width of 1.
|
||||
*
|
||||
* This implementation assumes that wchar_t characters are encoded
|
||||
* in ISO 10646.
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn test_wcwidth() {
|
||||
assert_eq!(
|
||||
&"abc\0".code_points().collect::<Vec<_>>(),
|
||||
&[0x61, 0x62, 0x63, 0x0]
|
||||
);
|
||||
assert_eq!(&"●".code_points().collect::<Vec<_>>(), &[0x25cf]);
|
||||
assert_eq!(&"📎".code_points().collect::<Vec<_>>(), &[0x1f4ce]);
|
||||
assert_eq!(
|
||||
&"𐼹𐼺𐼻𐼼𐼽".code_points().collect::<Vec<_>>(),
|
||||
&[0x10F39, 0x10F3A, 0x10F3B, 0x10F3C, 0x10F3D]
|
||||
); // Sogdian alphabet
|
||||
assert_eq!(
|
||||
&"𐼹a𐼽b".code_points().collect::<Vec<_>>(),
|
||||
&[0x10F39, 0x61, 0x10F3D, 0x62]
|
||||
); // Sogdian alphabet
|
||||
assert_eq!(
|
||||
&"📎\u{FE0E}".code_points().collect::<Vec<_>>(),
|
||||
&[0x1f4ce, 0xfe0e]
|
||||
);
|
||||
use crate::text_processing::grapheme_clusters::TextProcessing;
|
||||
assert_eq!("●".grapheme_width(), 1);
|
||||
assert_eq!("●📎".grapheme_width(), 3);
|
||||
assert_eq!("●\u{FE0E}📎\u{FE0E}".grapheme_width(), 3);
|
||||
assert_eq!("🎃".grapheme_width(), 2);
|
||||
assert_eq!("👻".grapheme_width(), 2);
|
||||
pub fn wcwidth(ucs: WChar) -> Option<usize> {
|
||||
/* sorted list of non-overlapping intervals of non-spacing characters */
|
||||
const COMBINING: &[Interval] = &[
|
||||
(0x0300, 0x034E),
|
||||
(0x0360, 0x0362),
|
||||
(0x0483, 0x0486),
|
||||
(0x0488, 0x0489),
|
||||
(0x0591, 0x05A1),
|
||||
(0x05A3, 0x05B9),
|
||||
(0x05BB, 0x05BD),
|
||||
(0x05BF, 0x05BF),
|
||||
(0x05C1, 0x05C2),
|
||||
(0x05C4, 0x05C4),
|
||||
(0x064B, 0x0655),
|
||||
(0x0670, 0x0670),
|
||||
(0x06D6, 0x06E4),
|
||||
(0x06E7, 0x06E8),
|
||||
(0x06EA, 0x06ED),
|
||||
(0x070F, 0x070F),
|
||||
(0x0711, 0x0711),
|
||||
(0x0730, 0x074A),
|
||||
(0x07A6, 0x07B0),
|
||||
(0x0901, 0x0902),
|
||||
(0x093C, 0x093C),
|
||||
(0x0941, 0x0948),
|
||||
(0x094D, 0x094D),
|
||||
(0x0951, 0x0954),
|
||||
(0x0962, 0x0963),
|
||||
(0x0981, 0x0981),
|
||||
(0x09BC, 0x09BC),
|
||||
(0x09C1, 0x09C4),
|
||||
(0x09CD, 0x09CD),
|
||||
(0x09E2, 0x09E3),
|
||||
(0x0A02, 0x0A02),
|
||||
(0x0A3C, 0x0A3C),
|
||||
(0x0A41, 0x0A42),
|
||||
(0x0A47, 0x0A48),
|
||||
(0x0A4B, 0x0A4D),
|
||||
(0x0A70, 0x0A71),
|
||||
(0x0A81, 0x0A82),
|
||||
(0x0ABC, 0x0ABC),
|
||||
(0x0AC1, 0x0AC5),
|
||||
(0x0AC7, 0x0AC8),
|
||||
(0x0ACD, 0x0ACD),
|
||||
(0x0B01, 0x0B01),
|
||||
(0x0B3C, 0x0B3C),
|
||||
(0x0B3F, 0x0B3F),
|
||||
(0x0B41, 0x0B43),
|
||||
(0x0B4D, 0x0B4D),
|
||||
(0x0B56, 0x0B56),
|
||||
(0x0B82, 0x0B82),
|
||||
(0x0BC0, 0x0BC0),
|
||||
(0x0BCD, 0x0BCD),
|
||||
(0x0C3E, 0x0C40),
|
||||
(0x0C46, 0x0C48),
|
||||
(0x0C4A, 0x0C4D),
|
||||
(0x0C55, 0x0C56),
|
||||
(0x0CBF, 0x0CBF),
|
||||
(0x0CC6, 0x0CC6),
|
||||
(0x0CCC, 0x0CCD),
|
||||
(0x0D41, 0x0D43),
|
||||
(0x0D4D, 0x0D4D),
|
||||
(0x0DCA, 0x0DCA),
|
||||
(0x0DD2, 0x0DD4),
|
||||
(0x0DD6, 0x0DD6),
|
||||
(0x0E31, 0x0E31),
|
||||
(0x0E34, 0x0E3A),
|
||||
(0x0E47, 0x0E4E),
|
||||
(0x0EB1, 0x0EB1),
|
||||
(0x0EB4, 0x0EB9),
|
||||
(0x0EBB, 0x0EBC),
|
||||
(0x0EC8, 0x0ECD),
|
||||
(0x0F18, 0x0F19),
|
||||
(0x0F35, 0x0F35),
|
||||
(0x0F37, 0x0F37),
|
||||
(0x0F39, 0x0F39),
|
||||
(0x0F71, 0x0F7E),
|
||||
(0x0F80, 0x0F84),
|
||||
(0x0F86, 0x0F87),
|
||||
(0x0F90, 0x0F97),
|
||||
(0x0F99, 0x0FBC),
|
||||
(0x0FC6, 0x0FC6),
|
||||
(0x102D, 0x1030),
|
||||
(0x1032, 0x1032),
|
||||
(0x1036, 0x1037),
|
||||
(0x1039, 0x1039),
|
||||
(0x1058, 0x1059),
|
||||
(0x1160, 0x11FF),
|
||||
(0x17B7, 0x17BD),
|
||||
(0x17C6, 0x17C6),
|
||||
(0x17C9, 0x17D3),
|
||||
(0x180B, 0x180E),
|
||||
(0x18A9, 0x18A9),
|
||||
(0x200B, 0x200F),
|
||||
(0x202A, 0x202E),
|
||||
(0x206A, 0x206F),
|
||||
(0x20D0, 0x20E3),
|
||||
(0x302A, 0x302F),
|
||||
(0x3099, 0x309A),
|
||||
(0xFB1E, 0xFB1E),
|
||||
(0xFE20, 0xFE23),
|
||||
(0xFEFF, 0xFEFF),
|
||||
(0xFFF9, 0xFFFB),
|
||||
];
|
||||
|
||||
/* test for 8-bit control characters */
|
||||
if ucs == 0 {
|
||||
return Some(0);
|
||||
}
|
||||
if ucs < 32 || (ucs >= 0x7f && ucs < 0xa0) {
|
||||
return None;
|
||||
}
|
||||
|
||||
/* binary search in table of emojis */
|
||||
if bisearch(ucs, EMOJI_RANGES) {
|
||||
return Some(2);
|
||||
}
|
||||
/* binary search in table of non-spacing characters */
|
||||
if bisearch(ucs, COMBINING) {
|
||||
return Some(1);
|
||||
}
|
||||
|
||||
/* if we arrive here, ucs is not a combining or C0/C1 control character */
|
||||
|
||||
Some(
|
||||
1 + big_if_true!(
|
||||
ucs >= 0x1100
|
||||
&& (ucs <= 0x115f || /* Hangul Jamo init. consonants */
|
||||
(ucs >= 0x2e80 && ucs <= 0xa4cf && (ucs & !0x0011) != 0x300a &&
|
||||
ucs != 0x303f) || /* CJK ... Yi */
|
||||
(ucs >= 0xac00 && ucs <= 0xd7a3) || /* Hangul Syllables */
|
||||
(ucs >= 0xf900 && ucs <= 0xfaff) || /* CJK Compatibility Ideographs */
|
||||
(ucs >= 0xfe30 && ucs <= 0xfe6f) || /* CJK Compatibility Forms */
|
||||
(ucs >= 0xff00 && ucs <= 0xff5f) || /* Fullwidth Forms */
|
||||
(ucs >= 0xffe0 && ucs <= 0xffe6) ||
|
||||
(ucs >= 0x20000 && ucs <= 0x2ffff))
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn wcswidth(mut pwcs: WChar, mut n: usize) -> Option<usize> {
|
||||
|
@ -169,3 +322,360 @@ pub fn wcswidth(mut pwcs: WChar, mut n: usize) -> Option<usize> {
|
|||
|
||||
Some(width)
|
||||
}
|
||||
|
||||
const EMOJI_RANGES: &[Interval] = &[
|
||||
(0x231A, 0x231B), // ; Basic_Emoji ; watch # 1.1 [2] (⌚..⌛)
|
||||
(0x23E9, 0x23EC), // ; Basic_Emoji ; fast-forward button # 6.0 [4] (⏩..⏬)
|
||||
(0x23F0, 0x23F0), // ; Basic_Emoji ; alarm clock # 6.0 [1] (⏰)
|
||||
(0x23F3, 0x23F3), // ; Basic_Emoji ; hourglass not done # 6.0 [1] (⏳)
|
||||
(0x25FD, 0x25FE), // ; Basic_Emoji ; white medium-small square # 3.2 [2] (◽..◾)
|
||||
(0x2614, 0x2615), // ; Basic_Emoji ; umbrella with rain drops # 4.0 [2] (☔..☕)
|
||||
(0x2648, 0x2653), // ; Basic_Emoji ; Aries # 1.1 [12] (♈..♓)
|
||||
(0x267F, 0x267F), // ; Basic_Emoji ; wheelchair symbol # 4.1 [1] (♿)
|
||||
(0x2693, 0x2693), // ; Basic_Emoji ; anchor # 4.1 [1] (⚓)
|
||||
(0x26A1, 0x26A1), // ; Basic_Emoji ; high voltage # 4.0 [1] (⚡)
|
||||
(0x26AA, 0x26AB), // ; Basic_Emoji ; white circle # 4.1 [2] (⚪..⚫)
|
||||
(0x26BD, 0x26BE), // ; Basic_Emoji ; soccer ball # 5.2 [2] (⚽..⚾)
|
||||
(0x26C4, 0x26C5), // ; Basic_Emoji ; snowman without snow # 5.2 [2] (⛄..⛅)
|
||||
(0x26CE, 0x26CE), // ; Basic_Emoji ; Ophiuchus # 6.0 [1] (⛎)
|
||||
(0x26D4, 0x26D4), // ; Basic_Emoji ; no entry # 5.2 [1] (⛔)
|
||||
(0x26EA, 0x26EA), // ; Basic_Emoji ; church # 5.2 [1] (⛪)
|
||||
(0x26F2, 0x26F3), // ; Basic_Emoji ; fountain # 5.2 [2] (⛲..⛳)
|
||||
(0x26F5, 0x26F5), // ; Basic_Emoji ; sailboat # 5.2 [1] (⛵)
|
||||
(0x26FA, 0x26FA), // ; Basic_Emoji ; tent # 5.2 [1] (⛺)
|
||||
(0x26FD, 0x26FD), // ; Basic_Emoji ; fuel pump # 5.2 [1] (⛽)
|
||||
(0x2705, 0x2705), // ; Basic_Emoji ; check mark button # 6.0 [1] (✅)
|
||||
(0x270A, 0x270B), // ; Basic_Emoji ; raised fist # 6.0 [2] (✊..✋)
|
||||
(0x2728, 0x2728), // ; Basic_Emoji ; sparkles # 6.0 [1] (✨)
|
||||
(0x274C, 0x274C), // ; Basic_Emoji ; cross mark # 6.0 [1] (❌)
|
||||
(0x274E, 0x274E), // ; Basic_Emoji ; cross mark button # 6.0 [1] (❎)
|
||||
(0x2753, 0x2755), // ; Basic_Emoji ; question mark # 6.0 [3] (❓..❕)
|
||||
(0x2757, 0x2757), // ; Basic_Emoji ; exclamation mark # 5.2 [1] (❗)
|
||||
(0x2795, 0x2797), // ; Basic_Emoji ; plus sign # 6.0 [3] (➕..➗)
|
||||
(0x27B0, 0x27B0), // ; Basic_Emoji ; curly loop # 6.0 [1] (➰)
|
||||
(0x27BF, 0x27BF), // ; Basic_Emoji ; double curly loop # 6.0 [1] (➿)
|
||||
(0x2B1B, 0x2B1C), // ; Basic_Emoji ; black large square # 5.1 [2] (⬛..⬜)
|
||||
(0x2B50, 0x2B50), // ; Basic_Emoji ; star # 5.1 [1] (⭐)
|
||||
(0x2B55, 0x2B55), // ; Basic_Emoji ; hollow red circle # 5.2 [1] (⭕)
|
||||
(0x1F004, 0x1F004), // ; Basic_Emoji ; mahjong red dragon # 5.1 [1] (🀄)
|
||||
(0x1F0CF, 0x1F0CF), // ; Basic_Emoji ; joker # 6.0 [1] (🃏)
|
||||
(0x1F18E, 0x1F18E), // ; Basic_Emoji ; AB button (blood type) # 6.0 [1] (🆎)
|
||||
(0x1F191, 0x1F19A), // ; Basic_Emoji ; CL button # 6.0 [10] (🆑..🆚)
|
||||
(0x1F201, 0x1F201), // ; Basic_Emoji ; Japanese “here” button # 6.0 [1] (🈁)
|
||||
(0x1F21A, 0x1F21A), // ; Basic_Emoji ; Japanese “free of charge” button # 5.2 [1] (🈚)
|
||||
(0x1F22F, 0x1F22F), // ; Basic_Emoji ; Japanese “reserved” button # 5.2 [1] (🈯)
|
||||
(0x1F232, 0x1F236), // ; Basic_Emoji ; Japanese “prohibited” button # 6.0 [5] (🈲..🈶)
|
||||
(0x1F238, 0x1F23A), // ; Basic_Emoji ; Japanese “application” button # 6.0 [3] (🈸..🈺)
|
||||
(0x1F250, 0x1F251), // ; Basic_Emoji ; Japanese “bargain” button # 6.0 [2] (🉐..🉑)
|
||||
(0x1F300, 0x1F320), // ; Basic_Emoji ; cyclone # 6.0 [33] (🌀..🌠)
|
||||
(0x1F32D, 0x1F32F), // ; Basic_Emoji ; hot dog # 8.0 [3] (🌭..🌯)
|
||||
(0x1F330, 0x1F335), // ; Basic_Emoji ; chestnut # 6.0 [6] (🌰..🌵)
|
||||
(0x1F337, 0x1F37C), // ; Basic_Emoji ; tulip # 6.0 [70] (🌷..🍼)
|
||||
(0x1F37E, 0x1F37F), // ; Basic_Emoji ; bottle with popping cork # 8.0 [2] (🍾..🍿)
|
||||
(0x1F380, 0x1F393), // ; Basic_Emoji ; ribbon # 6.0 [20] (🎀..🎓)
|
||||
(0x1F3A0, 0x1F3C4), // ; Basic_Emoji ; carousel horse # 6.0 [37] (🎠..🏄)
|
||||
(0x1F3C5, 0x1F3C5), // ; Basic_Emoji ; sports medal # 7.0 [1] (🏅)
|
||||
(0x1F3C6, 0x1F3CA), // ; Basic_Emoji ; trophy # 6.0 [5] (🏆..🏊)
|
||||
(0x1F3CF, 0x1F3D3), // ; Basic_Emoji ; cricket game # 8.0 [5] (🏏..🏓)
|
||||
(0x1F3E0, 0x1F3F0), // ; Basic_Emoji ; house # 6.0 [17] (🏠..🏰)
|
||||
(0x1F3F4, 0x1F3F4), // ; Basic_Emoji ; black flag # 7.0 [1] (🏴)
|
||||
(0x1F3F8, 0x1F3FF), // ; Basic_Emoji ; badminton # 8.0 [8] (🏸..🏿)
|
||||
(0x1F400, 0x1F43E), // ; Basic_Emoji ; rat # 6.0 [63] (🐀..🐾)
|
||||
(0x1F440, 0x1F440), // ; Basic_Emoji ; eyes # 6.0 [1] (👀)
|
||||
(0x1F442, 0x1F4F7), // ; Basic_Emoji ; ear # 6.0[182] (👂..📷)
|
||||
(0x1F4F8, 0x1F4F8), // ; Basic_Emoji ; camera with flash # 7.0 [1] (📸)
|
||||
(0x1F4F9, 0x1F4FC), // ; Basic_Emoji ; video camera # 6.0 [4] (📹..📼)
|
||||
(0x1F4FF, 0x1F4FF), // ; Basic_Emoji ; prayer beads # 8.0 [1] (📿)
|
||||
(0x1F500, 0x1F53D), // ; Basic_Emoji ; shuffle tracks button # 6.0 [62] (🔀..🔽)
|
||||
(0x1F54B, 0x1F54E), // ; Basic_Emoji ; kaaba # 8.0 [4] (🕋..🕎)
|
||||
(0x1F550, 0x1F567), // ; Basic_Emoji ; one o’clock # 6.0 [24] (🕐..🕧)
|
||||
(0x1F57A, 0x1F57A), // ; Basic_Emoji ; man dancing # 9.0 [1] (🕺)
|
||||
(0x1F595, 0x1F596), // ; Basic_Emoji ; middle finger # 7.0 [2] (🖕..🖖)
|
||||
(0x1F5A4, 0x1F5A4), // ; Basic_Emoji ; black heart # 9.0 [1] (🖤)
|
||||
(0x1F5FB, 0x1F5FF), // ; Basic_Emoji ; mount fuji # 6.0 [5] (🗻..🗿)
|
||||
(0x1F600, 0x1F600), // ; Basic_Emoji ; grinning face # 6.1 [1] (😀)
|
||||
(0x1F601, 0x1F610), // ; Basic_Emoji ; beaming face with smiling eyes # 6.0 [16] (😁..😐)
|
||||
(0x1F611, 0x1F611), // ; Basic_Emoji ; expressionless face # 6.1 [1] (😑)
|
||||
(0x1F612, 0x1F614), // ; Basic_Emoji ; unamused face # 6.0 [3] (😒..😔)
|
||||
(0x1F615, 0x1F615), // ; Basic_Emoji ; confused face # 6.1 [1] (😕)
|
||||
(0x1F616, 0x1F616), // ; Basic_Emoji ; confounded face # 6.0 [1] (😖)
|
||||
(0x1F617, 0x1F617), // ; Basic_Emoji ; kissing face # 6.1 [1] (😗)
|
||||
(0x1F618, 0x1F618), // ; Basic_Emoji ; face blowing a kiss # 6.0 [1] (😘)
|
||||
(0x1F619, 0x1F619), // ; Basic_Emoji ; kissing face with smiling eyes # 6.1 [1] (😙)
|
||||
(0x1F61A, 0x1F61A), // ; Basic_Emoji ; kissing face with closed eyes # 6.0 [1] (😚)
|
||||
(0x1F61B, 0x1F61B), // ; Basic_Emoji ; face with tongue # 6.1 [1] (😛)
|
||||
(0x1F61C, 0x1F61E), // ; Basic_Emoji ; winking face with tongue # 6.0 [3] (😜..😞)
|
||||
(0x1F61F, 0x1F61F), // ; Basic_Emoji ; worried face # 6.1 [1] (😟)
|
||||
(0x1F620, 0x1F625), // ; Basic_Emoji ; angry face # 6.0 [6] (😠..😥)
|
||||
(0x1F626, 0x1F627), // ; Basic_Emoji ; frowning face with open mouth # 6.1 [2] (😦..😧)
|
||||
(0x1F628, 0x1F62B), // ; Basic_Emoji ; fearful face # 6.0 [4] (😨..😫)
|
||||
(0x1F62C, 0x1F62C), // ; Basic_Emoji ; grimacing face # 6.1 [1] (😬)
|
||||
(0x1F62D, 0x1F62D), // ; Basic_Emoji ; loudly crying face # 6.0 [1] (😭)
|
||||
(0x1F62E, 0x1F62F), // ; Basic_Emoji ; face with open mouth # 6.1 [2] (😮..😯)
|
||||
(0x1F630, 0x1F633), // ; Basic_Emoji ; anxious face with sweat # 6.0 [4] (😰..😳)
|
||||
(0x1F634, 0x1F634), // ; Basic_Emoji ; sleeping face # 6.1 [1] (😴)
|
||||
(0x1F635, 0x1F640), // ; Basic_Emoji ; dizzy face # 6.0 [12] (😵..🙀)
|
||||
(0x1F641, 0x1F642), // ; Basic_Emoji ; slightly frowning face # 7.0 [2] (🙁..🙂)
|
||||
(0x1F643, 0x1F644), // ; Basic_Emoji ; upside-down face # 8.0 [2] (🙃..🙄)
|
||||
(0x1F645, 0x1F64F), // ; Basic_Emoji ; person gesturing NO # 6.0 [11] (🙅..🙏)
|
||||
(0x1F680, 0x1F6C5), // ; Basic_Emoji ; rocket # 6.0 [70] (🚀..🛅)
|
||||
(0x1F6CC, 0x1F6CC), // ; Basic_Emoji ; person in bed # 7.0 [1] (🛌)
|
||||
(0x1F6D0, 0x1F6D0), // ; Basic_Emoji ; place of worship # 8.0 [1] (🛐)
|
||||
(0x1F6D1, 0x1F6D2), // ; Basic_Emoji ; stop sign # 9.0 [2] (🛑..🛒)
|
||||
(0x1F6D5, 0x1F6D5), // ; Basic_Emoji ; hindu temple # 12.0 [1] (🛕)
|
||||
(0x1F6EB, 0x1F6EC), // ; Basic_Emoji ; airplane departure # 7.0 [2] (🛫..🛬)
|
||||
(0x1F6F4, 0x1F6F6), // ; Basic_Emoji ; kick scooter # 9.0 [3] (🛴..🛶)
|
||||
(0x1F6F7, 0x1F6F8), // ; Basic_Emoji ; sled # 10.0 [2] (🛷..🛸)
|
||||
(0x1F6F9, 0x1F6F9), // ; Basic_Emoji ; skateboard # 11.0 [1] (🛹)
|
||||
(0x1F6FA, 0x1F6FA), // ; Basic_Emoji ; auto rickshaw # 12.0 [1] (🛺)
|
||||
(0x1F7E0, 0x1F7EB), // ; Basic_Emoji ; orange circle # 12.0 [12] (🟠..🟫)
|
||||
(0x1F90D, 0x1F90F), // ; Basic_Emoji ; white heart # 12.0 [3] (🤍..🤏)
|
||||
(0x1F910, 0x1F918), // ; Basic_Emoji ; zipper-mouth face # 8.0 [9] (🤐..🤘)
|
||||
(0x1F919, 0x1F91E), // ; Basic_Emoji ; call me hand # 9.0 [6] (🤙..🤞)
|
||||
(0x1F91F, 0x1F91F), // ; Basic_Emoji ; love-you gesture # 10.0 [1] (🤟)
|
||||
(0x1F920, 0x1F927), // ; Basic_Emoji ; cowboy hat face # 9.0 [8] (🤠..🤧)
|
||||
(0x1F928, 0x1F92F), // ; Basic_Emoji ; face with raised eyebrow # 10.0 [8] (🤨..🤯)
|
||||
(0x1F930, 0x1F930), // ; Basic_Emoji ; pregnant woman # 9.0 [1] (🤰)
|
||||
(0x1F931, 0x1F932), // ; Basic_Emoji ; breast-feeding # 10.0 [2] (🤱..🤲)
|
||||
(0x1F933, 0x1F93A), // ; Basic_Emoji ; selfie # 9.0 [8] (🤳..🤺)
|
||||
(0x1F93C, 0x1F93E), // ; Basic_Emoji ; people wrestling # 9.0 [3] (🤼..🤾)
|
||||
(0x1F93F, 0x1F93F), // ; Basic_Emoji ; diving mask # 12.0 [1] (🤿)
|
||||
(0x1F940, 0x1F945), // ; Basic_Emoji ; wilted flower # 9.0 [6] (🥀..🥅)
|
||||
(0x1F947, 0x1F94B), // ; Basic_Emoji ; 1st place medal # 9.0 [5] (🥇..🥋)
|
||||
(0x1F94C, 0x1F94C), // ; Basic_Emoji ; curling stone # 10.0 [1] (🥌)
|
||||
(0x1F94D, 0x1F94F), // ; Basic_Emoji ; lacrosse # 11.0 [3] (🥍..🥏)
|
||||
(0x1F950, 0x1F95E), // ; Basic_Emoji ; croissant # 9.0 [15] (🥐..🥞)
|
||||
(0x1F95F, 0x1F96B), // ; Basic_Emoji ; dumpling # 10.0 [13] (🥟..🥫)
|
||||
(0x1F96C, 0x1F970), // ; Basic_Emoji ; leafy green # 11.0 [5] (🥬..🥰)
|
||||
(0x1F971, 0x1F971), // ; Basic_Emoji ; yawning face # 12.0 [1] (🥱)
|
||||
(0x1F973, 0x1F976), // ; Basic_Emoji ; partying face # 11.0 [4] (🥳..🥶)
|
||||
(0x1F97A, 0x1F97A), // ; Basic_Emoji ; pleading face # 11.0 [1] (🥺)
|
||||
(0x1F97B, 0x1F97B), // ; Basic_Emoji ; sari # 12.0 [1] (🥻)
|
||||
(0x1F97C, 0x1F97F), // ; Basic_Emoji ; lab coat # 11.0 [4] (🥼..🥿)
|
||||
(0x1F980, 0x1F984), // ; Basic_Emoji ; crab # 8.0 [5] (🦀..🦄)
|
||||
(0x1F985, 0x1F991), // ; Basic_Emoji ; eagle # 9.0 [13] (🦅..🦑)
|
||||
(0x1F992, 0x1F997), // ; Basic_Emoji ; giraffe # 10.0 [6] (🦒..🦗)
|
||||
(0x1F998, 0x1F9A2), // ; Basic_Emoji ; kangaroo # 11.0 [11] (🦘..🦢)
|
||||
(0x1F9A5, 0x1F9AA), // ; Basic_Emoji ; sloth # 12.0 [6] (🦥..🦪)
|
||||
(0x1F9AE, 0x1F9AF), // ; Basic_Emoji ; guide dog # 12.0 [2] (🦮..🦯)
|
||||
(0x1F9B0, 0x1F9B9), // ; Basic_Emoji ; red hair # 11.0 [10] (🦰..🦹)
|
||||
(0x1F9BA, 0x1F9BF), // ; Basic_Emoji ; safety vest # 12.0 [6] (🦺..🦿)
|
||||
(0x1F9C0, 0x1F9C0), // ; Basic_Emoji ; cheese wedge # 8.0 [1] (🧀)
|
||||
(0x1F9C1, 0x1F9C2), // ; Basic_Emoji ; cupcake # 11.0 [2] (🧁..🧂)
|
||||
(0x1F9C3, 0x1F9CA), // ; Basic_Emoji ; beverage box # 12.0 [8] (🧃..🧊)
|
||||
(0x1F9CD, 0x1F9CF), // ; Basic_Emoji ; person standing # 12.0 [3] (🧍..🧏)
|
||||
(0x1F9D0, 0x1F9E6), // ; Basic_Emoji ; face with monocle # 10.0 [23] (🧐..🧦)
|
||||
(0x1F9E7, 0x1F9FF), // ; Basic_Emoji ; red envelope # 11.0 [25] (🧧..🧿)
|
||||
(0x1FA70, 0x1FA73), // ; Basic_Emoji ; ballet shoes # 12.0 [4] (🩰..🩳)
|
||||
(0x1FA78, 0x1FA7A), // ; Basic_Emoji ; drop of blood # 12.0 [3] (🩸..🩺)
|
||||
(0x1FA80, 0x1FA82), // ; Basic_Emoji ; yo-yo # 12.0 [3] (🪀..🪂)
|
||||
(0x1FA90, 0x1FA95), // ; Basic_Emoji ; ringed planet # 12.0 [6] (🪐..🪕)
|
||||
];
|
||||
/*
|
||||
00A9 FE0F ; Basic_Emoji ; copyright # 3.2 [1] (©️)
|
||||
00AE FE0F ; Basic_Emoji ; registered # 3.2 [1] (®️)
|
||||
203C FE0F ; Basic_Emoji ; double exclamation mark # 3.2 [1] (‼️)
|
||||
2049 FE0F ; Basic_Emoji ; exclamation question mark # 3.2 [1] (⁉️)
|
||||
2122 FE0F ; Basic_Emoji ; trade mark # 3.2 [1] (™️)
|
||||
2139 FE0F ; Basic_Emoji ; information # 3.2 [1] (ℹ️)
|
||||
2194 FE0F ; Basic_Emoji ; left-right arrow # 3.2 [1] (↔️)
|
||||
2195 FE0F ; Basic_Emoji ; up-down arrow # 3.2 [1] (↕️)
|
||||
2196 FE0F ; Basic_Emoji ; up-left arrow # 3.2 [1] (↖️)
|
||||
2197 FE0F ; Basic_Emoji ; up-right arrow # 3.2 [1] (↗️)
|
||||
2198 FE0F ; Basic_Emoji ; down-right arrow # 3.2 [1] (↘️)
|
||||
2199 FE0F ; Basic_Emoji ; down-left arrow # 3.2 [1] (↙️)
|
||||
21A9 FE0F ; Basic_Emoji ; right arrow curving left # 3.2 [1] (↩️)
|
||||
21AA FE0F ; Basic_Emoji ; left arrow curving right # 3.2 [1] (↪️)
|
||||
2328 FE0F ; Basic_Emoji ; keyboard # 3.2 [1] (⌨️)
|
||||
23CF FE0F ; Basic_Emoji ; eject button # 4.0 [1] (⏏️)
|
||||
23ED FE0F ; Basic_Emoji ; next track button # 6.0 [1] (⏭️)
|
||||
23EE FE0F ; Basic_Emoji ; last track button # 6.0 [1] (⏮️)
|
||||
23EF FE0F ; Basic_Emoji ; play or pause button # 6.0 [1] (⏯️)
|
||||
23F1 FE0F ; Basic_Emoji ; stopwatch # 6.0 [1] (⏱️)
|
||||
23F2 FE0F ; Basic_Emoji ; timer clock # 6.0 [1] (⏲️)
|
||||
23F8 FE0F ; Basic_Emoji ; pause button # 7.0 [1] (⏸️)
|
||||
23F9 FE0F ; Basic_Emoji ; stop button # 7.0 [1] (⏹️)
|
||||
23FA FE0F ; Basic_Emoji ; record button # 7.0 [1] (⏺️)
|
||||
24C2 FE0F ; Basic_Emoji ; circled M # 3.2 [1] (Ⓜ️)
|
||||
25AA FE0F ; Basic_Emoji ; black small square # 3.2 [1] (▪️)
|
||||
25AB FE0F ; Basic_Emoji ; white small square # 3.2 [1] (▫️)
|
||||
25B6 FE0F ; Basic_Emoji ; play button # 3.2 [1] (▶️)
|
||||
25C0 FE0F ; Basic_Emoji ; reverse button # 3.2 [1] (◀️)
|
||||
25FB FE0F ; Basic_Emoji ; white medium square # 3.2 [1] (◻️)
|
||||
25FC FE0F ; Basic_Emoji ; black medium square # 3.2 [1] (◼️)
|
||||
2600 FE0F ; Basic_Emoji ; sun # 3.2 [1] (☀️)
|
||||
2601 FE0F ; Basic_Emoji ; cloud # 3.2 [1] (☁️)
|
||||
2602 FE0F ; Basic_Emoji ; umbrella # 3.2 [1] (☂️)
|
||||
2603 FE0F ; Basic_Emoji ; snowman # 3.2 [1] (☃️)
|
||||
2604 FE0F ; Basic_Emoji ; comet # 3.2 [1] (☄️)
|
||||
260E FE0F ; Basic_Emoji ; telephone # 3.2 [1] (☎️)
|
||||
2611 FE0F ; Basic_Emoji ; check box with check # 3.2 [1] (☑️)
|
||||
2618 FE0F ; Basic_Emoji ; shamrock # 4.1 [1] (☘️)
|
||||
261D FE0F ; Basic_Emoji ; index pointing up # 3.2 [1] (☝️)
|
||||
2620 FE0F ; Basic_Emoji ; skull and crossbones # 3.2 [1] (☠️)
|
||||
2622 FE0F ; Basic_Emoji ; radioactive # 3.2 [1] (☢️)
|
||||
2623 FE0F ; Basic_Emoji ; biohazard # 3.2 [1] (☣️)
|
||||
2626 FE0F ; Basic_Emoji ; orthodox cross # 3.2 [1] (☦️)
|
||||
262A FE0F ; Basic_Emoji ; star and crescent # 3.2 [1] (☪️)
|
||||
262E FE0F ; Basic_Emoji ; peace symbol # 3.2 [1] (☮️)
|
||||
262F FE0F ; Basic_Emoji ; yin yang # 3.2 [1] (☯️)
|
||||
2638 FE0F ; Basic_Emoji ; wheel of dharma # 3.2 [1] (☸️)
|
||||
2639 FE0F ; Basic_Emoji ; frowning face # 3.2 [1] (☹️)
|
||||
263A FE0F ; Basic_Emoji ; smiling face # 3.2 [1] (☺️)
|
||||
2640 FE0F ; Basic_Emoji ; female sign # 3.2 [1] (♀️)
|
||||
2642 FE0F ; Basic_Emoji ; male sign # 3.2 [1] (♂️)
|
||||
265F FE0F ; Basic_Emoji ; chess pawn # 3.2 [1] (♟️)
|
||||
2660 FE0F ; Basic_Emoji ; spade suit # 3.2 [1] (♠️)
|
||||
2663 FE0F ; Basic_Emoji ; club suit # 3.2 [1] (♣️)
|
||||
2665 FE0F ; Basic_Emoji ; heart suit # 3.2 [1] (♥️)
|
||||
2666 FE0F ; Basic_Emoji ; diamond suit # 3.2 [1] (♦️)
|
||||
2668 FE0F ; Basic_Emoji ; hot springs # 3.2 [1] (♨️)
|
||||
267B FE0F ; Basic_Emoji ; recycling symbol # 3.2 [1] (♻️)
|
||||
267E FE0F ; Basic_Emoji ; infinity # 4.1 [1] (♾️)
|
||||
2692 FE0F ; Basic_Emoji ; hammer and pick # 4.1 [1] (⚒️)
|
||||
2694 FE0F ; Basic_Emoji ; crossed swords # 4.1 [1] (⚔️)
|
||||
2695 FE0F ; Basic_Emoji ; medical symbol # 4.1 [1] (⚕️)
|
||||
2696 FE0F ; Basic_Emoji ; balance scale # 4.1 [1] (⚖️)
|
||||
2697 FE0F ; Basic_Emoji ; alembic # 4.1 [1] (⚗️)
|
||||
2699 FE0F ; Basic_Emoji ; gear # 4.1 [1] (⚙️)
|
||||
269B FE0F ; Basic_Emoji ; atom symbol # 4.1 [1] (⚛️)
|
||||
269C FE0F ; Basic_Emoji ; fleur-de-lis # 4.1 [1] (⚜️)
|
||||
26A0 FE0F ; Basic_Emoji ; warning # 4.0 [1] (⚠️)
|
||||
26B0 FE0F ; Basic_Emoji ; coffin # 4.1 [1] (⚰️)
|
||||
26B1 FE0F ; Basic_Emoji ; funeral urn # 4.1 [1] (⚱️)
|
||||
26C8 FE0F ; Basic_Emoji ; cloud with lightning and rain # 5.2 [1] (⛈️)
|
||||
26CF FE0F ; Basic_Emoji ; pick # 5.2 [1] (⛏️)
|
||||
26D1 FE0F ; Basic_Emoji ; rescue worker’s helmet # 5.2 [1] (⛑️)
|
||||
26D3 FE0F ; Basic_Emoji ; chains # 5.2 [1] (⛓️)
|
||||
26E9 FE0F ; Basic_Emoji ; shinto shrine # 5.2 [1] (⛩️)
|
||||
26F0 FE0F ; Basic_Emoji ; mountain # 5.2 [1] (⛰️)
|
||||
26F1 FE0F ; Basic_Emoji ; umbrella on ground # 5.2 [1] (⛱️)
|
||||
26F4 FE0F ; Basic_Emoji ; ferry # 5.2 [1] (⛴️)
|
||||
26F7 FE0F ; Basic_Emoji ; skier # 5.2 [1] (⛷️)
|
||||
26F8 FE0F ; Basic_Emoji ; ice skate # 5.2 [1] (⛸️)
|
||||
26F9 FE0F ; Basic_Emoji ; person bouncing ball # 5.2 [1] (⛹️)
|
||||
2702 FE0F ; Basic_Emoji ; scissors # 3.2 [1] (✂️)
|
||||
2708 FE0F ; Basic_Emoji ; airplane # 3.2 [1] (✈️)
|
||||
2709 FE0F ; Basic_Emoji ; envelope # 3.2 [1] (✉️)
|
||||
270C FE0F ; Basic_Emoji ; victory hand # 3.2 [1] (✌️)
|
||||
270D FE0F ; Basic_Emoji ; writing hand # 3.2 [1] (✍️)
|
||||
270F FE0F ; Basic_Emoji ; pencil # 3.2 [1] (✏️)
|
||||
2712 FE0F ; Basic_Emoji ; black nib # 3.2 [1] (✒️)
|
||||
2714 FE0F ; Basic_Emoji ; check mark # 3.2 [1] (✔️)
|
||||
2716 FE0F ; Basic_Emoji ; multiplication sign # 3.2 [1] (✖️)
|
||||
271D FE0F ; Basic_Emoji ; latin cross # 3.2 [1] (✝️)
|
||||
2721 FE0F ; Basic_Emoji ; star of David # 3.2 [1] (✡️)
|
||||
2733 FE0F ; Basic_Emoji ; eight-spoked asterisk # 3.2 [1] (✳️)
|
||||
2734 FE0F ; Basic_Emoji ; eight-pointed star # 3.2 [1] (✴️)
|
||||
2744 FE0F ; Basic_Emoji ; snowflake # 3.2 [1] (❄️)
|
||||
2747 FE0F ; Basic_Emoji ; sparkle # 3.2 [1] (❇️)
|
||||
2763 FE0F ; Basic_Emoji ; heart exclamation # 3.2 [1] (❣️)
|
||||
2764 FE0F ; Basic_Emoji ; red heart # 3.2 [1] (❤️)
|
||||
27A1 FE0F ; Basic_Emoji ; right arrow # 3.2 [1] (➡️)
|
||||
2934 FE0F ; Basic_Emoji ; right arrow curving up # 3.2 [1] (⤴️)
|
||||
2935 FE0F ; Basic_Emoji ; right arrow curving down # 3.2 [1] (⤵️)
|
||||
2B05 FE0F ; Basic_Emoji ; left arrow # 4.0 [1] (⬅️)
|
||||
2B06 FE0F ; Basic_Emoji ; up arrow # 4.0 [1] (⬆️)
|
||||
2B07 FE0F ; Basic_Emoji ; down arrow # 4.0 [1] (⬇️)
|
||||
3030 FE0F ; Basic_Emoji ; wavy dash # 3.2 [1] (〰️)
|
||||
303D FE0F ; Basic_Emoji ; part alternation mark # 3.2 [1] (〽️)
|
||||
3297 FE0F ; Basic_Emoji ; Japanese “congratulations” button # 3.2 [1] (㊗️)
|
||||
3299 FE0F ; Basic_Emoji ; Japanese “secret” button # 3.2 [1] (㊙️)
|
||||
1F170 FE0F ; Basic_Emoji ; A button (blood type) # 6.0 [1] (🅰️)
|
||||
1F171 FE0F ; Basic_Emoji ; B button (blood type) # 6.0 [1] (🅱️)
|
||||
1F17E FE0F ; Basic_Emoji ; O button (blood type) # 6.0 [1] (🅾️)
|
||||
1F17F FE0F ; Basic_Emoji ; P button # 5.2 [1] (🅿️)
|
||||
1F202 FE0F ; Basic_Emoji ; Japanese “service charge” button # 6.0 [1] (🈂️)
|
||||
1F237 FE0F ; Basic_Emoji ; Japanese “monthly amount” button # 6.0 [1] (🈷️)
|
||||
1F321 FE0F ; Basic_Emoji ; thermometer # 7.0 [1] (🌡️)
|
||||
1F324 FE0F ; Basic_Emoji ; sun behind small cloud # 7.0 [1] (🌤️)
|
||||
1F325 FE0F ; Basic_Emoji ; sun behind large cloud # 7.0 [1] (🌥️)
|
||||
1F326 FE0F ; Basic_Emoji ; sun behind rain cloud # 7.0 [1] (🌦️)
|
||||
1F327 FE0F ; Basic_Emoji ; cloud with rain # 7.0 [1] (🌧️)
|
||||
1F328 FE0F ; Basic_Emoji ; cloud with snow # 7.0 [1] (🌨️)
|
||||
1F329 FE0F ; Basic_Emoji ; cloud with lightning # 7.0 [1] (🌩️)
|
||||
1F32A FE0F ; Basic_Emoji ; tornado # 7.0 [1] (🌪️)
|
||||
1F32B FE0F ; Basic_Emoji ; fog # 7.0 [1] (🌫️)
|
||||
1F32C FE0F ; Basic_Emoji ; wind face # 7.0 [1] (🌬️)
|
||||
1F336 FE0F ; Basic_Emoji ; hot pepper # 7.0 [1] (🌶️)
|
||||
1F37D FE0F ; Basic_Emoji ; fork and knife with plate # 7.0 [1] (🍽️)
|
||||
1F396 FE0F ; Basic_Emoji ; military medal # 7.0 [1] (🎖️)
|
||||
1F397 FE0F ; Basic_Emoji ; reminder ribbon # 7.0 [1] (🎗️)
|
||||
1F399 FE0F ; Basic_Emoji ; studio microphone # 7.0 [1] (🎙️)
|
||||
1F39A FE0F ; Basic_Emoji ; level slider # 7.0 [1] (🎚️)
|
||||
1F39B FE0F ; Basic_Emoji ; control knobs # 7.0 [1] (🎛️)
|
||||
1F39E FE0F ; Basic_Emoji ; film frames # 7.0 [1] (🎞️)
|
||||
1F39F FE0F ; Basic_Emoji ; admission tickets # 7.0 [1] (🎟️)
|
||||
1F3CB FE0F ; Basic_Emoji ; person lifting weights # 7.0 [1] (🏋️)
|
||||
1F3CC FE0F ; Basic_Emoji ; person golfing # 7.0 [1] (🏌️)
|
||||
1F3CD FE0F ; Basic_Emoji ; motorcycle # 7.0 [1] (🏍️)
|
||||
1F3CE FE0F ; Basic_Emoji ; racing car # 7.0 [1] (🏎️)
|
||||
1F3D4 FE0F ; Basic_Emoji ; snow-capped mountain # 7.0 [1] (🏔️)
|
||||
1F3D5 FE0F ; Basic_Emoji ; camping # 7.0 [1] (🏕️)
|
||||
1F3D6 FE0F ; Basic_Emoji ; beach with umbrella # 7.0 [1] (🏖️)
|
||||
1F3D7 FE0F ; Basic_Emoji ; building construction # 7.0 [1] (🏗️)
|
||||
1F3D8 FE0F ; Basic_Emoji ; houses # 7.0 [1] (🏘️)
|
||||
1F3D9 FE0F ; Basic_Emoji ; cityscape # 7.0 [1] (🏙️)
|
||||
1F3DA FE0F ; Basic_Emoji ; derelict house # 7.0 [1] (🏚️)
|
||||
1F3DB FE0F ; Basic_Emoji ; classical building # 7.0 [1] (🏛️)
|
||||
1F3DC FE0F ; Basic_Emoji ; desert # 7.0 [1] (🏜️)
|
||||
1F3DD FE0F ; Basic_Emoji ; desert island # 7.0 [1] (🏝️)
|
||||
1F3DE FE0F ; Basic_Emoji ; national park # 7.0 [1] (🏞️)
|
||||
1F3DF FE0F ; Basic_Emoji ; stadium # 7.0 [1] (🏟️)
|
||||
1F3F3 FE0F ; Basic_Emoji ; white flag # 7.0 [1] (🏳️)
|
||||
1F3F5 FE0F ; Basic_Emoji ; rosette # 7.0 [1] (🏵️)
|
||||
1F3F7 FE0F ; Basic_Emoji ; label # 7.0 [1] (🏷️)
|
||||
1F43F FE0F ; Basic_Emoji ; chipmunk # 7.0 [1] (🐿️)
|
||||
1F441 FE0F ; Basic_Emoji ; eye # 7.0 [1] (👁️)
|
||||
1F4FD FE0F ; Basic_Emoji ; film projector # 7.0 [1] (📽️)
|
||||
1F549 FE0F ; Basic_Emoji ; om # 7.0 [1] (🕉️)
|
||||
1F54A FE0F ; Basic_Emoji ; dove # 7.0 [1] (🕊️)
|
||||
1F56F FE0F ; Basic_Emoji ; candle # 7.0 [1] (🕯️)
|
||||
1F570 FE0F ; Basic_Emoji ; mantelpiece clock # 7.0 [1] (🕰️)
|
||||
1F573 FE0F ; Basic_Emoji ; hole # 7.0 [1] (🕳️)
|
||||
1F574 FE0F ; Basic_Emoji ; man in suit levitating # 7.0 [1] (🕴️)
|
||||
1F575 FE0F ; Basic_Emoji ; detective # 7.0 [1] (🕵️)
|
||||
1F576 FE0F ; Basic_Emoji ; sunglasses # 7.0 [1] (🕶️)
|
||||
1F577 FE0F ; Basic_Emoji ; spider # 7.0 [1] (🕷️)
|
||||
1F578 FE0F ; Basic_Emoji ; spider web # 7.0 [1] (🕸️)
|
||||
1F579 FE0F ; Basic_Emoji ; joystick # 7.0 [1] (🕹️)
|
||||
1F587 FE0F ; Basic_Emoji ; linked paperclips # 7.0 [1] (🖇️)
|
||||
1F58A FE0F ; Basic_Emoji ; pen # 7.0 [1] (🖊️)
|
||||
1F58B FE0F ; Basic_Emoji ; fountain pen # 7.0 [1] (🖋️)
|
||||
1F58C FE0F ; Basic_Emoji ; paintbrush # 7.0 [1] (🖌️)
|
||||
1F58D FE0F ; Basic_Emoji ; crayon # 7.0 [1] (🖍️)
|
||||
1F590 FE0F ; Basic_Emoji ; hand with fingers splayed # 7.0 [1] (🖐️)
|
||||
1F5A5 FE0F ; Basic_Emoji ; desktop computer # 7.0 [1] (🖥️)
|
||||
1F5A8 FE0F ; Basic_Emoji ; printer # 7.0 [1] (🖨️)
|
||||
1F5B1 FE0F ; Basic_Emoji ; computer mouse # 7.0 [1] (🖱️)
|
||||
1F5B2 FE0F ; Basic_Emoji ; trackball # 7.0 [1] (🖲️)
|
||||
1F5BC FE0F ; Basic_Emoji ; framed picture # 7.0 [1] (🖼️)
|
||||
1F5C2 FE0F ; Basic_Emoji ; card index dividers # 7.0 [1] (🗂️)
|
||||
1F5C3 FE0F ; Basic_Emoji ; card file box # 7.0 [1] (🗃️)
|
||||
1F5C4 FE0F ; Basic_Emoji ; file cabinet # 7.0 [1] (🗄️)
|
||||
1F5D1 FE0F ; Basic_Emoji ; wastebasket # 7.0 [1] (🗑️)
|
||||
1F5D2 FE0F ; Basic_Emoji ; spiral notepad # 7.0 [1] (🗒️)
|
||||
1F5D3 FE0F ; Basic_Emoji ; spiral calendar # 7.0 [1] (🗓️)
|
||||
1F5DC FE0F ; Basic_Emoji ; clamp # 7.0 [1] (🗜️)
|
||||
1F5DD FE0F ; Basic_Emoji ; old key # 7.0 [1] (🗝️)
|
||||
1F5DE FE0F ; Basic_Emoji ; rolled-up newspaper # 7.0 [1] (🗞️)
|
||||
1F5E1 FE0F ; Basic_Emoji ; dagger # 7.0 [1] (🗡️)
|
||||
1F5E3 FE0F ; Basic_Emoji ; speaking head # 7.0 [1] (🗣️)
|
||||
1F5E8 FE0F ; Basic_Emoji ; left speech bubble # 7.0 [1] (🗨️)
|
||||
1F5EF FE0F ; Basic_Emoji ; right anger bubble # 7.0 [1] (🗯️)
|
||||
1F5F3 FE0F ; Basic_Emoji ; ballot box with ballot # 7.0 [1] (🗳️)
|
||||
1F5FA FE0F ; Basic_Emoji ; world map # 7.0 [1] (🗺️)
|
||||
1F6CB FE0F ; Basic_Emoji ; couch and lamp # 7.0 [1] (🛋️)
|
||||
1F6CD FE0F ; Basic_Emoji ; shopping bags # 7.0 [1] (🛍️)
|
||||
1F6CE FE0F ; Basic_Emoji ; bellhop bell # 7.0 [1] (🛎️)
|
||||
1F6CF FE0F ; Basic_Emoji ; bed # 7.0 [1] (🛏️)
|
||||
1F6E0 FE0F ; Basic_Emoji ; hammer and wrench # 7.0 [1] (🛠️)
|
||||
1F6E1 FE0F ; Basic_Emoji ; shield # 7.0 [1] (🛡️)
|
||||
1F6E2 FE0F ; Basic_Emoji ; oil drum # 7.0 [1] (🛢️)
|
||||
1F6E3 FE0F ; Basic_Emoji ; motorway # 7.0 [1] (🛣️)
|
||||
1F6E4 FE0F ; Basic_Emoji ; railway track # 7.0 [1] (🛤️)
|
||||
1F6E5 FE0F ; Basic_Emoji ; motor boat # 7.0 [1] (🛥️)
|
||||
1F6E9 FE0F ; Basic_Emoji ; small airplane # 7.0 [1] (🛩️)
|
||||
1F6F0 FE0F ; Basic_Emoji ; satellite # 7.0 [1] (🛰️)
|
||||
1F6F3 FE0F ; Basic_Emoji ; passenger ship # 7.0 [1] (🛳️)
|
||||
*/
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
*/
|
||||
|
||||
use crate::datetime::UnixTimestamp;
|
||||
use crate::email::address::StrBuild;
|
||||
use crate::email::parser::BytesExt;
|
||||
use crate::email::*;
|
||||
|
||||
|
@ -44,6 +43,7 @@ pub use iterators::*;
|
|||
use crate::text_processing::grapheme_clusters::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt;
|
||||
|
@ -130,7 +130,7 @@ macro_rules! make {
|
|||
e.parent = Some($p);
|
||||
});
|
||||
let old_group = std::mem::replace($threads.groups.entry(old_group_hash).or_default(), ThreadGroup::Node {
|
||||
parent: Arc::new(RwLock::new(parent_group_hash)),
|
||||
parent: RefCell::new(parent_group_hash),
|
||||
});
|
||||
$threads.thread_nodes.entry($c).and_modify(|e| {
|
||||
e.group = parent_group_hash;
|
||||
|
@ -172,127 +172,18 @@ macro_rules! make {
|
|||
}};
|
||||
}
|
||||
|
||||
/// Strip common prefixes from subjects
|
||||
///
|
||||
///
|
||||
/// ```rust
|
||||
/// use melib::thread::SubjectPrefix;
|
||||
///
|
||||
/// let mut subject = "Re: RE: Res: Re: Res: Subject";
|
||||
/// assert_eq!(subject.strip_prefixes_from_list(<&str>::USUAL_PREFIXES, None), &"Subject");
|
||||
/// let mut subject = "Re: RE: Res: Re: Res: Subject";
|
||||
/// assert_eq!(subject.strip_prefixes_from_list(<&str>::USUAL_PREFIXES, Some(1)), &"RE: Res: Re: Res: Subject");
|
||||
/// ```
|
||||
pub trait SubjectPrefix {
|
||||
const USUAL_PREFIXES: &'static [&'static str] = &[
|
||||
"Re:",
|
||||
// Canada (Réponse)
|
||||
// Spanish (Respuesta)
|
||||
"RE:",
|
||||
"Fwd:",
|
||||
"Fw:",
|
||||
/* taken from
|
||||
* https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations#Abbreviations_in_other_languages
|
||||
* */
|
||||
"回复:",
|
||||
"回覆:",
|
||||
// Dutch (Antwoord)
|
||||
"Antw:",
|
||||
// Dutch (Doorsturen)
|
||||
"Doorst:",
|
||||
// Finnish (Välitetty)
|
||||
"VL:",
|
||||
// French (Référence)
|
||||
"REF:",
|
||||
// French (Transfert)
|
||||
// Canada (Transfert)
|
||||
"TR:",
|
||||
// German (Antwort)
|
||||
"AW:",
|
||||
// German (Weitergeleitet)
|
||||
"WG:",
|
||||
// Greek (Απάντηση)
|
||||
"ΑΠ:",
|
||||
"Απ:",
|
||||
"απ:",
|
||||
// Greek (Προωθημένο)
|
||||
"ΠΡΘ:",
|
||||
"Πρθ:",
|
||||
"πρθ:",
|
||||
// Greek (Σχετικό)
|
||||
"ΣΧΕΤ:",
|
||||
"Σχετ:",
|
||||
"σχετ:",
|
||||
// Greek (Προωθημένο)
|
||||
"ΠΡΘ:",
|
||||
"Πρθ:",
|
||||
"πρθ:",
|
||||
// Hungarian (Válasz)
|
||||
"Vá:",
|
||||
// Hungarian
|
||||
"Továbbítás:",
|
||||
// Italian (Riferimento)
|
||||
"R:",
|
||||
// Italian (Inoltro)
|
||||
"I:",
|
||||
// Italian (Riferimento)
|
||||
"RIF:",
|
||||
// Icelandic (Svara)
|
||||
// Swedish (Svar)
|
||||
// Norwegian (Svar)
|
||||
// Danish (Svar)
|
||||
"SV:",
|
||||
"Sv:",
|
||||
// Icelandic (Framsenda)
|
||||
"FS:",
|
||||
"Fs:",
|
||||
// Indonesian (Balas)
|
||||
"BLS:",
|
||||
// Indonesian (Terusan)
|
||||
"TRS:",
|
||||
// Norwegian (Videresendt)
|
||||
// Danish (Videresendt)
|
||||
// Finnish (Vastaus)
|
||||
"VS:",
|
||||
"Vs:",
|
||||
// Swedish (Vidarebefordrat)
|
||||
"VB:",
|
||||
"Vb:",
|
||||
// Spanish (Reenviado)
|
||||
"RV:",
|
||||
"Rv:",
|
||||
// Portuguese (Resposta)
|
||||
"RES:",
|
||||
"Res:",
|
||||
// Portuguese (Encaminhado)
|
||||
"ENC:",
|
||||
// Polish (Odpowiedź)
|
||||
"Odp:",
|
||||
// Polish (Podaj dalej)
|
||||
"PD:",
|
||||
// Turkish (Yanıt)
|
||||
"YNT:",
|
||||
// Turkish (İlet)
|
||||
"İLT:",
|
||||
// Welsh (Ateb)
|
||||
"ATB:",
|
||||
// Welsh (Ymlaen)
|
||||
"YML:",
|
||||
];
|
||||
/* Strip common prefixes from subjects */
|
||||
trait SubjectPrefix {
|
||||
fn is_a_reply(&self) -> bool;
|
||||
fn strip_prefixes(&mut self) -> &mut Self;
|
||||
fn strip_prefixes_from_list(&mut self, list: &[&str], times: Option<u8>) -> &mut Self;
|
||||
}
|
||||
|
||||
impl SubjectPrefix for &[u8] {
|
||||
fn is_a_reply(&self) -> bool {
|
||||
let self_ = self.trim();
|
||||
self_.starts_with(b"RE: ")
|
||||
|| self_.starts_with(b"Re: ")
|
||||
|| self_.starts_with(b"RES: ")
|
||||
|| self_.starts_with(b"Res: ")
|
||||
|| self_.starts_with(b"FW: ")
|
||||
|| self_.starts_with(b"Fw: ")
|
||||
self.starts_with(b"RE: ")
|
||||
|| self.starts_with(b"Re: ")
|
||||
|| self.starts_with(b"FW: ")
|
||||
|| self.starts_with(b"Fw: ")
|
||||
}
|
||||
|
||||
fn strip_prefixes(&mut self) -> &mut Self {
|
||||
|
@ -336,112 +227,6 @@ impl SubjectPrefix for &[u8] {
|
|||
*self = result;
|
||||
self
|
||||
}
|
||||
|
||||
fn strip_prefixes_from_list(&mut self, list: &[&str], mut times: Option<u8>) -> &mut Self {
|
||||
let result = {
|
||||
let mut slice = self.trim();
|
||||
'outer: loop {
|
||||
let len = slice.len();
|
||||
for prefix in list.iter() {
|
||||
if slice
|
||||
.get(0..prefix.as_bytes().len())
|
||||
.map(|p| p.eq_ignore_ascii_case(prefix.as_bytes()))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
slice = &slice[prefix.len()..];
|
||||
slice = slice.trim();
|
||||
times = times.map(|u| u.saturating_sub(1));
|
||||
if times == Some(0) {
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
if slice.len() == len || times == Some(0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
slice
|
||||
};
|
||||
*self = result;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl SubjectPrefix for &str {
|
||||
fn is_a_reply(&self) -> bool {
|
||||
self.as_bytes().is_a_reply()
|
||||
}
|
||||
|
||||
fn strip_prefixes(&mut self) -> &mut Self {
|
||||
let result = {
|
||||
let mut slice = self.trim();
|
||||
loop {
|
||||
if slice.starts_with("RE: ")
|
||||
|| slice.starts_with("Re: ")
|
||||
|| slice.starts_with("FW: ")
|
||||
|| slice.starts_with("Fw: ")
|
||||
{
|
||||
slice = &slice[3..];
|
||||
continue;
|
||||
}
|
||||
if slice.starts_with("FWD: ")
|
||||
|| slice.starts_with("Fwd: ")
|
||||
|| slice.starts_with("fwd: ")
|
||||
{
|
||||
slice = &slice[4..];
|
||||
continue;
|
||||
}
|
||||
if slice.starts_with(' ') || slice.starts_with('\t') || slice.starts_with('\r') {
|
||||
//FIXME just trim whitespace
|
||||
slice = &slice[1..];
|
||||
continue;
|
||||
}
|
||||
if slice.starts_with('[')
|
||||
&& !(slice.starts_with("[PATCH") || slice.starts_with("[RFC"))
|
||||
{
|
||||
if let Some(pos) = slice.find(']') {
|
||||
slice = &slice[pos..];
|
||||
continue;
|
||||
}
|
||||
slice = &slice[1..];
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
slice
|
||||
};
|
||||
*self = result;
|
||||
self
|
||||
}
|
||||
|
||||
fn strip_prefixes_from_list(&mut self, list: &[&str], mut times: Option<u8>) -> &mut Self {
|
||||
let result = {
|
||||
let mut slice = self.trim();
|
||||
'outer: loop {
|
||||
let len = slice.len();
|
||||
for prefix in list.iter() {
|
||||
if slice
|
||||
.get(0..prefix.as_bytes().len())
|
||||
.map(|p| p.eq_ignore_ascii_case(prefix))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
slice = &slice[prefix.len()..];
|
||||
slice = slice.trim();
|
||||
times = times.map(|u| u.saturating_sub(1));
|
||||
if times == Some(0) {
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
if slice.len() == len || times == Some(0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
slice
|
||||
};
|
||||
*self = result;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/* Sorting states. */
|
||||
|
@ -506,7 +291,7 @@ pub struct Thread {
|
|||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub enum ThreadGroup {
|
||||
Root(Thread),
|
||||
Node { parent: Arc<RwLock<ThreadHash>> },
|
||||
Node { parent: RefCell<ThreadHash> },
|
||||
}
|
||||
|
||||
impl Default for ThreadGroup {
|
||||
|
@ -552,7 +337,6 @@ impl Thread {
|
|||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ThreadNode {
|
||||
pub message: Option<EnvelopeHash>,
|
||||
pub other_mailbox: bool,
|
||||
pub parent: Option<ThreadNodeHash>,
|
||||
pub children: Vec<ThreadNodeHash>,
|
||||
pub date: UnixTimestamp,
|
||||
|
@ -566,7 +350,6 @@ impl Default for ThreadNode {
|
|||
ThreadNode {
|
||||
message: None,
|
||||
parent: None,
|
||||
other_mailbox: false,
|
||||
children: Vec::new(),
|
||||
date: UnixTimestamp::default(),
|
||||
show_subject: true,
|
||||
|
@ -625,16 +408,16 @@ impl ThreadNode {
|
|||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct Threads {
|
||||
pub thread_nodes: HashMap<ThreadNodeHash, ThreadNode>,
|
||||
root_set: Arc<RwLock<Vec<ThreadNodeHash>>>,
|
||||
tree_index: Arc<RwLock<Vec<ThreadNodeHash>>>,
|
||||
root_set: RefCell<Vec<ThreadNodeHash>>,
|
||||
tree_index: RefCell<Vec<ThreadNodeHash>>,
|
||||
pub groups: HashMap<ThreadHash, ThreadGroup>,
|
||||
|
||||
message_ids: HashMap<Vec<u8>, ThreadNodeHash>,
|
||||
pub message_ids_set: HashSet<Vec<u8>>,
|
||||
pub missing_message_ids: HashSet<Vec<u8>>,
|
||||
pub hash_set: HashSet<EnvelopeHash>,
|
||||
sort: Arc<RwLock<(SortField, SortOrder)>>,
|
||||
subsort: Arc<RwLock<(SortField, SortOrder)>>,
|
||||
sort: RefCell<(SortField, SortOrder)>,
|
||||
subsort: RefCell<(SortField, SortOrder)>,
|
||||
}
|
||||
|
||||
impl PartialEq for ThreadNode {
|
||||
|
@ -668,13 +451,13 @@ impl Threads {
|
|||
pub fn find_group(&self, h: ThreadHash) -> ThreadHash {
|
||||
let p = match self.groups[&h] {
|
||||
ThreadGroup::Root(_) => return h,
|
||||
ThreadGroup::Node { ref parent } => *parent.read().unwrap(),
|
||||
ThreadGroup::Node { ref parent } => *parent.borrow(),
|
||||
};
|
||||
|
||||
let parent_group = self.find_group(p);
|
||||
match self.groups[&h] {
|
||||
ThreadGroup::Node { ref parent } => {
|
||||
*parent.write().unwrap() = parent_group;
|
||||
*parent.borrow_mut() = parent_group;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
@ -705,8 +488,8 @@ impl Threads {
|
|||
message_ids_set,
|
||||
missing_message_ids,
|
||||
hash_set,
|
||||
sort: Arc::new(RwLock::new((SortField::Date, SortOrder::Desc))),
|
||||
subsort: Arc::new(RwLock::new((SortField::Subject, SortOrder::Desc))),
|
||||
sort: RefCell::new((SortField::Date, SortOrder::Desc)),
|
||||
subsort: RefCell::new((SortField::Subject, SortOrder::Desc)),
|
||||
|
||||
..Default::default()
|
||||
}
|
||||
|
@ -787,7 +570,7 @@ impl Threads {
|
|||
};
|
||||
|
||||
if self.thread_nodes[&t_id].parent.is_none() {
|
||||
let mut tree_index = self.tree_index.write().unwrap();
|
||||
let mut tree_index = self.tree_index.borrow_mut();
|
||||
if let Some(i) = tree_index.iter().position(|t| *t == t_id) {
|
||||
tree_index.remove(i);
|
||||
}
|
||||
|
@ -844,7 +627,6 @@ impl Threads {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..self.thread_nodes[&id].children.len() {
|
||||
let child_hash = self.thread_nodes[&id].children[i];
|
||||
if let Some(child_env_hash) = self.thread_nodes[&child_hash].message() {
|
||||
|
@ -870,35 +652,17 @@ impl Threads {
|
|||
env_hash: EnvelopeHash,
|
||||
other_mailbox: bool,
|
||||
) -> bool {
|
||||
{
|
||||
let envelopes_lck = envelopes.read().unwrap();
|
||||
let message_id = envelopes_lck[&env_hash].message_id().raw();
|
||||
if self.message_ids.contains_key(message_id)
|
||||
&& !self.missing_message_ids.contains(message_id)
|
||||
{
|
||||
let thread_hash = self.message_ids[message_id];
|
||||
let node = self.thread_nodes.entry(thread_hash).or_default();
|
||||
drop(envelopes_lck);
|
||||
envelopes
|
||||
.write()
|
||||
.unwrap()
|
||||
.get_mut(&env_hash)
|
||||
.unwrap()
|
||||
.set_thread(thread_hash);
|
||||
|
||||
/* If thread node currently has a message from a foreign mailbox and env_hash is
|
||||
* from current mailbox we want to update it, otherwise return */
|
||||
if !node.other_mailbox || other_mailbox {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
let envelopes_lck = envelopes.read().unwrap();
|
||||
let reply_to_id: Option<ThreadNodeHash> = envelopes_lck[&env_hash]
|
||||
.in_reply_to()
|
||||
.map(StrBuild::raw)
|
||||
.map(crate::email::StrBuild::raw)
|
||||
.and_then(|r| self.message_ids.get(r).cloned());
|
||||
let message_id = envelopes_lck[&env_hash].message_id().raw();
|
||||
if self.message_ids_set.contains(message_id)
|
||||
&& !self.missing_message_ids.contains(message_id)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if other_mailbox
|
||||
&& reply_to_id.is_none()
|
||||
|
@ -922,14 +686,13 @@ impl Threads {
|
|||
None
|
||||
},
|
||||
)
|
||||
.unwrap_or_else(|| ThreadNodeHash::from(message_id));
|
||||
.unwrap_or_else(ThreadNodeHash::new);
|
||||
{
|
||||
let mut node = self.thread_nodes.entry(new_id).or_default();
|
||||
node.message = Some(env_hash);
|
||||
if node.parent.is_none() {
|
||||
node.parent = reply_to_id;
|
||||
}
|
||||
node.other_mailbox = other_mailbox;
|
||||
node.date = envelopes_lck[&env_hash].date();
|
||||
node.unseen = !envelopes_lck[&env_hash].is_seen();
|
||||
}
|
||||
|
@ -976,8 +739,11 @@ impl Threads {
|
|||
self.hash_set.insert(env_hash);
|
||||
if let Some(reply_to_id) = reply_to_id {
|
||||
make!((reply_to_id) parent of (new_id), self);
|
||||
} else if let Some(r) = envelopes_lck[&env_hash].in_reply_to().map(StrBuild::raw) {
|
||||
let reply_to_id = ThreadNodeHash::from(r);
|
||||
} else if let Some(r) = envelopes_lck[&env_hash]
|
||||
.in_reply_to()
|
||||
.map(crate::email::StrBuild::raw)
|
||||
{
|
||||
let reply_to_id = ThreadNodeHash::new();
|
||||
self.thread_nodes.insert(
|
||||
reply_to_id,
|
||||
ThreadNode {
|
||||
|
@ -1021,7 +787,7 @@ impl Threads {
|
|||
make!((id) parent of (current_descendant_id), self);
|
||||
current_descendant_id = id;
|
||||
} else {
|
||||
let id = ThreadNodeHash::from(reference.raw());
|
||||
let id = ThreadNodeHash::new();
|
||||
self.thread_nodes.insert(
|
||||
id,
|
||||
ThreadNode {
|
||||
|
@ -1059,7 +825,7 @@ impl Threads {
|
|||
|
||||
/*
|
||||
save_graph(
|
||||
&self.tree_index.read().unwrap(),
|
||||
&self.tree_index.borrow(),
|
||||
&self.thread_nodes,
|
||||
&self
|
||||
.message_ids
|
||||
|
@ -1085,7 +851,7 @@ impl Threads {
|
|||
ref thread_nodes,
|
||||
..
|
||||
} = self;
|
||||
let tree = &mut tree_index.write().unwrap();
|
||||
let tree = &mut tree_index.borrow_mut();
|
||||
for t in tree.iter_mut() {
|
||||
thread_nodes[t].children.sort_by(|a, b| match subsort {
|
||||
(SortField::Date, SortOrder::Desc) => {
|
||||
|
@ -1232,18 +998,18 @@ impl Threads {
|
|||
let envelopes = envelopes.read().unwrap();
|
||||
vec.sort_by(|a, b| match sort {
|
||||
(SortField::Date, SortOrder::Desc) => {
|
||||
let a = self.thread_ref(self.thread_nodes[a].group).date();
|
||||
let b = self.thread_ref(self.thread_nodes[b].group).date();
|
||||
let a = self.thread_ref(self.thread_nodes[&a].group).date();
|
||||
let b = self.thread_ref(self.thread_nodes[&b].group).date();
|
||||
b.cmp(&a)
|
||||
}
|
||||
(SortField::Date, SortOrder::Asc) => {
|
||||
let a = self.thread_ref(self.thread_nodes[a].group).date();
|
||||
let b = self.thread_ref(self.thread_nodes[b].group).date();
|
||||
let a = self.thread_ref(self.thread_nodes[&a].group).date();
|
||||
let b = self.thread_ref(self.thread_nodes[&b].group).date();
|
||||
a.cmp(&b)
|
||||
}
|
||||
(SortField::Subject, SortOrder::Desc) => {
|
||||
let a = &self.thread_nodes[a].message();
|
||||
let b = &self.thread_nodes[b].message();
|
||||
let a = &self.thread_nodes[&a].message();
|
||||
let b = &self.thread_nodes[&b].message();
|
||||
|
||||
match (a, b) {
|
||||
(Some(_), Some(_)) => {}
|
||||
|
@ -1271,8 +1037,8 @@ impl Threads {
|
|||
}
|
||||
}
|
||||
(SortField::Subject, SortOrder::Asc) => {
|
||||
let a = &self.thread_nodes[a].message();
|
||||
let b = &self.thread_nodes[b].message();
|
||||
let a = &self.thread_nodes[&a].message();
|
||||
let b = &self.thread_nodes[&b].message();
|
||||
|
||||
match (a, b) {
|
||||
(Some(_), Some(_)) => {}
|
||||
|
@ -1304,22 +1070,22 @@ impl Threads {
|
|||
});
|
||||
}
|
||||
fn inner_sort_by(&self, sort: (SortField, SortOrder), envelopes: &Envelopes) {
|
||||
let tree = &mut self.tree_index.write().unwrap();
|
||||
let tree = &mut self.tree_index.borrow_mut();
|
||||
let envelopes = envelopes.read().unwrap();
|
||||
tree.sort_by(|a, b| match sort {
|
||||
(SortField::Date, SortOrder::Desc) => {
|
||||
let a = self.thread_ref(self.thread_nodes[a].group).date();
|
||||
let b = self.thread_ref(self.thread_nodes[b].group).date();
|
||||
let a = self.thread_ref(self.thread_nodes[&a].group).date();
|
||||
let b = self.thread_ref(self.thread_nodes[&b].group).date();
|
||||
b.cmp(&a)
|
||||
}
|
||||
(SortField::Date, SortOrder::Asc) => {
|
||||
let a = self.thread_ref(self.thread_nodes[a].group).date();
|
||||
let b = self.thread_ref(self.thread_nodes[b].group).date();
|
||||
let a = self.thread_ref(self.thread_nodes[&a].group).date();
|
||||
let b = self.thread_ref(self.thread_nodes[&b].group).date();
|
||||
a.cmp(&b)
|
||||
}
|
||||
(SortField::Subject, SortOrder::Desc) => {
|
||||
let a = &self.thread_nodes[a].message();
|
||||
let b = &self.thread_nodes[b].message();
|
||||
let a = &self.thread_nodes[&a].message();
|
||||
let b = &self.thread_nodes[&b].message();
|
||||
|
||||
match (a, b) {
|
||||
(Some(_), Some(_)) => {}
|
||||
|
@ -1347,8 +1113,8 @@ impl Threads {
|
|||
}
|
||||
}
|
||||
(SortField::Subject, SortOrder::Asc) => {
|
||||
let a = &self.thread_nodes[a].message();
|
||||
let b = &self.thread_nodes[b].message();
|
||||
let a = &self.thread_nodes[&a].message();
|
||||
let b = &self.thread_nodes[&b].message();
|
||||
|
||||
match (a, b) {
|
||||
(Some(_), Some(_)) => {}
|
||||
|
@ -1386,13 +1152,13 @@ impl Threads {
|
|||
subsort: (SortField, SortOrder),
|
||||
envelopes: &Envelopes,
|
||||
) {
|
||||
if *self.sort.read().unwrap() != sort {
|
||||
if *self.sort.borrow() != sort {
|
||||
self.inner_sort_by(sort, envelopes);
|
||||
*self.sort.write().unwrap() = sort;
|
||||
*self.sort.borrow_mut() = sort;
|
||||
}
|
||||
if *self.subsort.read().unwrap() != subsort {
|
||||
if *self.subsort.borrow() != subsort {
|
||||
self.inner_subsort_by(subsort, envelopes);
|
||||
*self.subsort.write().unwrap() = subsort;
|
||||
*self.subsort.borrow_mut() = subsort;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1410,11 +1176,11 @@ impl Threads {
|
|||
}
|
||||
|
||||
pub fn root_len(&self) -> usize {
|
||||
self.tree_index.read().unwrap().len()
|
||||
self.tree_index.borrow().len()
|
||||
}
|
||||
|
||||
pub fn root_set(&self, idx: usize) -> ThreadNodeHash {
|
||||
self.tree_index.read().unwrap()[idx]
|
||||
self.tree_index.borrow()[idx]
|
||||
}
|
||||
|
||||
pub fn roots(&self) -> SmallVec<[ThreadHash; 1024]> {
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
Return-Path: <japoeunp@hotmail.com>
|
||||
Delivered-To: jonnny@miami-dice.co.uk
|
||||
Received: from violet.xenserver.co.uk
|
||||
by violet.xenserver.co.uk with LMTP
|
||||
id qBHcI7LKml9FxzIAYrQLqw
|
||||
(envelope-from <japoeunp@hotmail.com>)
|
||||
for <jonnny@miami-dice.co.uk>; Thu, 29 Oct 2020 13:59:14 +0000
|
||||
Return-path: <japoeunp@hotmail.com>
|
||||
Envelope-to: jonnny@miami-dice.co.uk
|
||||
Delivery-date: Thu, 29 Oct 2020 13:59:14 +0000
|
||||
Received: from mail-oln040092254105.outbound.protection.outlook.com ([40.92.254.105]:29481 helo=APC01-PU1-obe.outbound.protection.outlook.com)
|
||||
by violet.xenserver.co.uk with esmtps (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
|
||||
(Exim 4.93)
|
||||
(envelope-from <japoeunp@hotmail.com>)
|
||||
id 1kY8SJ-00DxYw-WD
|
||||
for jonnny@miami-dice.co.uk; Thu, 29 Oct 2020 13:59:14 +0000
|
||||
ARC-Seal: i=1; a=rsa-sha256; s=arcselector9901; d=microsoft.com; cv=none;
|
||||
b=KKU/kthPXLl8CnAmBXXsD1QQWr4evL4ymaLwgHgRi5eSnOe2d2sQxrhcZ1VvLSvW2DQEQoNAm6NUtTC5uRUnBDS0n+g1E5/t1z8oFbzdioCIT6rL77ta3MVcaQ/o+gRa6dIwiNfu8z5GxAujOOu57gCfnCw3/gLeOHH01KtP4ezEB/DvAU9bC8eyso1T7nv+HT0riTjZOywGwDHnVb1aIPPIUiOQrrEi+cfLQRiCer01d94U8Wp+FUECrVYbr4uZGl8mbTwU4oZL1rJ25ubYG54e1ktaPJRa2YEitgJEF5sS8Z503c3RjzzBvvHkc/Kl6ypXcovP9xxeoSrS7YIPKA==
|
||||
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com;
|
||||
s=arcselector9901;
|
||||
h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck;
|
||||
bh=NUSuxgSF4fNUuTts93/OAIsK9q9w8XhbybHWH/oRmXo=;
|
||||
b=VU2clBW8reAfnfCef0DeEDlBzcCU2u288YCjTvB0ekvBkJGSdI657WyS8KR7JSy0KcPWRfGbN9GJaETaasoa7bLdfuB6K9foup+vSqlA1witS5JQXQM/vJCKx67DbT8/8emLrKi7yDD2qjtRsb6HfvbwAGGvmPyUeyfTvRv6js+4YUbe5eN6CCdJEploBXDrWjFXHpSCwVCL1oF6rgrJf0+Td+ufX0QEHbOz2uJWj4yz0A8hK2yV+2JDVW7GiBwZMrO4yLNXYck/0HQRyYFe8I86xUBJWp/0IITCTe96x5L/H3lqmGkh4uRt8IsXT/2jBEm5CmXLxJZAMR8RONG9BQ==
|
||||
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
|
||||
dkim=none; arc=none
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=hotmail.com;
|
||||
s=selector1;
|
||||
h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck;
|
||||
bh=NUSuxgSF4fNUuTts93/OAIsK9q9w8XhbybHWH/oRmXo=;
|
||||
b=JRkih9HxwazdzH6MSzSetJMcRwvDr+e97VnoDCQYJf9qQqgtQvzMZR0Z+d2Gu74Ip3ebcvx5oYlOpV15yVZAqUmUeirpF2rdkmMWQiaDQMq9SLiF09eMDkDfEdGLD4V+C36QIISRamgyagIsC72/UB6OyxpXoAjP0SFxbyItvWVgB9EVVsSJLOKXWgRWiYSZxMLye3OQUqdWoiQ9Tw/o8uywLTvcojOizZaS2SrYWajYScBmMiCh58dUarKzrfXmR/WisfBepCf1ia7BKttjalhuJBcMyKfM923X5IbZ+Yw+gVpLtzwGUyPt2cobOAxKna11whmpWdtoBeXRR/hKOg==
|
||||
Received: from PU1APC01FT013.eop-APC01.prod.protection.outlook.com
|
||||
(2a01:111:e400:7ebe::45) by
|
||||
PU1APC01HT068.eop-APC01.prod.protection.outlook.com (2a01:111:e400:7ebe::323)
|
||||
with Microsoft SMTP Server (version=TLS1_2,
|
||||
cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.3520.15; Thu, 29 Oct
|
||||
2020 13:58:16 +0000
|
||||
Received: from PS1PR0601MB3675.apcprd06.prod.outlook.com
|
||||
(2a01:111:e400:7ebe::44) by PU1APC01FT013.mail.protection.outlook.com
|
||||
(2a01:111:e400:7ebe::78) with Microsoft SMTP Server (version=TLS1_2,
|
||||
cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.3520.15 via Frontend
|
||||
Transport; Thu, 29 Oct 2020 13:58:16 +0000
|
||||
Received: from PS1PR0601MB3675.apcprd06.prod.outlook.com
|
||||
([fe80::65ed:e320:1c31:1695]) by PS1PR0601MB3675.apcprd06.prod.outlook.com
|
||||
([fe80::65ed:e320:1c31:1695%7]) with mapi id 15.20.3499.027; Thu, 29 Oct 2020
|
||||
13:58:16 +0000
|
||||
From: Jamaica Poe <japoeunp@hotmail.com>
|
||||
To: <foo-chat@example.com>
|
||||
Subject: thankful that I had the chance to written report, that I could learn
|
||||
and let alone the chance $4454.32
|
||||
Thread-Topic: thankful that I had the chance to written report, that I could
|
||||
learn and let alone the chance $4454.32
|
||||
Thread-Index: AQHWrfuHFQ6EC5DxDEG0hktDfP8BQg==
|
||||
Date: Thu, 29 Oct 2020 13:58:16 +0000
|
||||
Message-ID:
|
||||
<PS1PR0601MB36750BD00EA89E1482FA98A2D5140@PS1PR0601MB3675.apcprd06.prod.outlook.com>
|
||||
Accept-Language: en-US
|
||||
Content-Language: en-US
|
||||
Content-Type: text/html
|
||||
Content-Transfer-Encoding: base64
|
||||
MIME-Version: 1.0
|
||||
|
||||
PCFET0NUWVBFPjxodG1sPjxoZWFkPjx0aXRsZT5mb288L3RpdGxlPjwvaGVhZD48Ym9k
|
||||
eT48dGFibGUgY2xhc3M9ImZvbyI+PHRoZWFkPjx0cj48dGQ+Zm9vPC90ZD48L3RoZWFk
|
||||
Pjx0Ym9keT48dHI+PHRkPmZvbzE8L3RkPjwvdHI+PC90Ym9keT48L3RhYmxlPjwvYm9k
|
||||
eT48L2h0bWw+
|
|
@ -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,46 +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 }
|
||||
##
|
||||
## Setting up a Gmail account
|
||||
#[accounts."gmail"]
|
||||
#root_mailbox = '[Gmail]'
|
||||
#format = "imap"
|
||||
#server_hostname='imap.gmail.com'
|
||||
#server_password="password"
|
||||
#server_username="username@gmail.com"
|
||||
#server_port="993"
|
||||
#listing.index_style = "Conversations"
|
||||
#identity = "username@gmail.com"
|
||||
#[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"
|
||||
### match every mailbox:
|
||||
#subscribed_mailboxes = ["*" ]
|
||||
#composing.send_mail = 'msmtp --read-recipients --read-envelope-from'
|
||||
### 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'
|
||||
#
|
||||
# # 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 }
|
||||
##
|
||||
#[pager]
|
||||
#filter = "COLUMNS=72 /usr/local/bin/pygmentize -l email"
|
||||
#pager_context = 0 # default, optional
|
||||
|
@ -103,6 +75,10 @@
|
|||
#[shortcuts.composing]
|
||||
#edit_mail = 'e'
|
||||
#
|
||||
##Thread view defaults:
|
||||
#[shortcuts.compact-listing]
|
||||
#exit_thread = 'i'
|
||||
#
|
||||
#[shortcuts.contact-list]
|
||||
#create_contact = 'c'
|
||||
#edit_contact = 'e'
|
||||
|
@ -117,7 +93,6 @@
|
|||
#next_account = 'h'
|
||||
#new_mail = 'm'
|
||||
#set_seen = 'n'
|
||||
#exit_entry = 'i'
|
||||
#
|
||||
##Pager defaults
|
||||
#
|
||||
|
@ -130,13 +105,14 @@
|
|||
#[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.
|
||||
#
|
||||
#
|
||||
#[pgp]
|
||||
#auto_sign = false # always sign sent messages
|
||||
#auto_verify_signatures = true # always verify signatures when reading signed e-mails
|
||||
#gpg_binary = "/usr/bin/gpg2" #optional
|
||||
#
|
||||
#[terminal]
|
||||
#theme = "dark" # or "light"
|
|
@ -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" }
|
||||
|
@ -49,7 +51,7 @@ color_aliases = { "neon_green" = "#6ef9d4", "darkgrey" = "#4a4a4a", "neon_purp
|
|||
"pager.highlight_search" = { fg = "White", bg = "Teal", attrs = "Bold" }
|
||||
"pager.highlight_search_current" = { fg = "White", bg = "NavyBlue", attrs = "Bold" }
|
||||
"status.bar" = { fg = "White", bg = "Black", attrs = "theme_default" }
|
||||
"status.notification" = { fg = "Plum1", bg = "theme_default", attrs = "theme_default" }
|
||||
"status.notification" = { fg = "Plum1", bg = "DarkRed", attrs = "theme_default" }
|
||||
"tab.bar" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"tab.focused" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
|
||||
"tab.unfocused" = { fg = "$darkgrey", bg = "Black", 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" }
|
|
@ -27,7 +27,60 @@
|
|||
//! split is done to theoretically be able to create different frontends with the same innards.
|
||||
//!
|
||||
|
||||
use meli::*;
|
||||
use std::alloc::System;
|
||||
use std::collections::VecDeque;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate linkify;
|
||||
extern crate uuid;
|
||||
|
||||
extern crate bitflags;
|
||||
extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate smallvec;
|
||||
extern crate termion;
|
||||
|
||||
#[global_allocator]
|
||||
static GLOBAL: System = System;
|
||||
|
||||
#[macro_use]
|
||||
extern crate melib;
|
||||
use melib::*;
|
||||
|
||||
mod unix;
|
||||
use unix::*;
|
||||
|
||||
#[macro_use]
|
||||
pub mod types;
|
||||
use crate::types::*;
|
||||
|
||||
#[macro_use]
|
||||
pub mod terminal;
|
||||
use crate::terminal::*;
|
||||
|
||||
#[macro_use]
|
||||
pub mod command;
|
||||
use crate::command::*;
|
||||
|
||||
pub mod state;
|
||||
use crate::state::*;
|
||||
|
||||
pub mod components;
|
||||
use crate::components::*;
|
||||
|
||||
#[macro_use]
|
||||
pub mod conf;
|
||||
use crate::conf::*;
|
||||
|
||||
#[cfg(feature = "sqlite3")]
|
||||
pub mod sqlite3;
|
||||
|
||||
pub mod jobs;
|
||||
pub mod mailcap;
|
||||
pub mod plugins;
|
||||
|
||||
use std::os::raw::c_int;
|
||||
|
||||
fn notify(
|
||||
|
@ -35,22 +88,24 @@ fn notify(
|
|||
sender: crossbeam::channel::Sender<ThreadEvent>,
|
||||
) -> std::result::Result<crossbeam::channel::Receiver<c_int>, std::io::Error> {
|
||||
use std::time::Duration;
|
||||
let (alarm_pipe_r, alarm_pipe_w) =
|
||||
nix::unistd::pipe().map_err(|err| std::io::Error::from_raw_os_error(err as i32))?;
|
||||
let (alarm_pipe_r, alarm_pipe_w) = nix::unistd::pipe().map_err(|err| {
|
||||
std::io::Error::from_raw_os_error(err.as_errno().map(|n| n as i32).unwrap_or(0))
|
||||
})?;
|
||||
let alarm_handler = move |info: &nix::libc::siginfo_t| {
|
||||
let value = unsafe { info.si_value().sival_ptr as u8 };
|
||||
let _ = nix::unistd::write(alarm_pipe_w, &[value]);
|
||||
};
|
||||
unsafe {
|
||||
signal_hook_registry::register_sigaction(signal_hook::consts::SIGALRM, alarm_handler)?;
|
||||
signal_hook_registry::register_sigaction(signal_hook::SIGALRM, alarm_handler)?;
|
||||
}
|
||||
let (s, r) = crossbeam::channel::bounded(100);
|
||||
let mut signals = signal_hook::iterator::Signals::new(signals)?;
|
||||
let signals = signal_hook::iterator::Signals::new(signals)?;
|
||||
let _ = nix::fcntl::fcntl(
|
||||
alarm_pipe_r,
|
||||
nix::fcntl::FcntlArg::F_SETFL(nix::fcntl::OFlag::O_NONBLOCK),
|
||||
);
|
||||
std::thread::spawn(move || {
|
||||
let mut buf = [0; 1];
|
||||
let mut ctr = 0;
|
||||
loop {
|
||||
ctr %= 3;
|
||||
|
@ -63,6 +118,16 @@ fn notify(
|
|||
for signal in signals.pending() {
|
||||
let _ = s.send_timeout(signal, Duration::from_millis(500)).ok();
|
||||
}
|
||||
while nix::unistd::read(alarm_pipe_r, buf.as_mut())
|
||||
.map(|s| s > 0)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let value = buf[0];
|
||||
let _ = sender.send_timeout(
|
||||
ThreadEvent::UIEvent(UIEvent::Timer(value)),
|
||||
Duration::from_millis(2000),
|
||||
);
|
||||
}
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
ctr += 1;
|
||||
|
@ -71,13 +136,11 @@ fn notify(
|
|||
Ok(r)
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli-docs")]
|
||||
fn parse_manpage(src: &str) -> Result<ManPages> {
|
||||
match src {
|
||||
"" | "meli" | "meli.1" | "main" => Ok(ManPages::Main),
|
||||
"meli.7" | "guide" => Ok(ManPages::Guide),
|
||||
"meli.conf" | "meli.conf.5" | "conf" | "config" | "configuration" => Ok(ManPages::Conf),
|
||||
"meli-themes" | "meli-themes.5" | "themes" | "theming" | "theme" => Ok(ManPages::Themes),
|
||||
"" | "meli" | "main" => Ok(ManPages::Main),
|
||||
"meli.conf" | "conf" | "config" | "configuration" => Ok(ManPages::Conf),
|
||||
"meli-themes" | "themes" | "theming" | "theme" => Ok(ManPages::Themes),
|
||||
_ => Err(MeliError::new(format!(
|
||||
"Invalid documentation page: {}",
|
||||
src
|
||||
|
@ -85,8 +148,9 @@ fn parse_manpage(src: &str) -> Result<ManPages> {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli-docs")]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
use structopt::StructOpt;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Choose manpage
|
||||
enum ManPages {
|
||||
/// meli(1)
|
||||
|
@ -95,8 +159,6 @@ enum ManPages {
|
|||
Conf = 1,
|
||||
/// meli-themes(5)
|
||||
Themes = 2,
|
||||
/// meli(7)
|
||||
Guide = 3,
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
|
@ -133,10 +195,6 @@ enum SubCommand {
|
|||
/// print documentation page and exit (Piping to a pager is recommended.).
|
||||
Man(ManOpt),
|
||||
|
||||
#[structopt(display_order = 4)]
|
||||
/// print compile time feature flags of this binary
|
||||
CompiledWith,
|
||||
|
||||
/// View mail from input file.
|
||||
View {
|
||||
#[structopt(value_name = "INPUT", parse(from_os_str))]
|
||||
|
@ -146,13 +204,8 @@ enum SubCommand {
|
|||
|
||||
#[derive(Debug, StructOpt)]
|
||||
struct ManOpt {
|
||||
#[structopt(default_value = "meli", possible_values=&["meli", "conf", "themes", "meli.7", "guide"], value_name="PAGE", parse(try_from_str = parse_manpage))]
|
||||
#[cfg(feature = "cli-docs")]
|
||||
#[structopt(default_value = "meli", possible_values=&["meli", "conf", "themes"], value_name="PAGE", parse(try_from_str = parse_manpage))]
|
||||
page: ManPages,
|
||||
/// If true, output text in stdout instead of spawning $PAGER.
|
||||
#[structopt(long = "no-raw", alias = "no-raw", value_name = "bool")]
|
||||
#[cfg(feature = "cli-docs")]
|
||||
no_raw: Option<Option<bool>>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
@ -178,7 +231,7 @@ fn run_app(opt: Opt) -> Result<()> {
|
|||
} else {
|
||||
crate::conf::get_config_file()?
|
||||
};
|
||||
conf::FileSettings::validate(config_path, true, false)?; // TODO: test for tty/interaction
|
||||
conf::FileSettings::validate(config_path)?;
|
||||
return Ok(());
|
||||
}
|
||||
Some(SubCommand::CreateConfig { path }) => {
|
||||
|
@ -198,79 +251,22 @@ fn run_app(opt: Opt) -> Result<()> {
|
|||
}
|
||||
#[cfg(feature = "cli-docs")]
|
||||
Some(SubCommand::Man(manopt)) => {
|
||||
let ManOpt { page, no_raw } = manopt;
|
||||
const MANPAGES: [&[u8]; 4] = [
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/meli.txt.gz")),
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/meli.conf.txt.gz")),
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/meli-themes.txt.gz")),
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/meli.7.txt.gz")),
|
||||
let _page = manopt.page;
|
||||
const MANPAGES: [&'static str; 3] = [
|
||||
include_str!(concat!(env!("OUT_DIR"), "/meli.txt")),
|
||||
include_str!(concat!(env!("OUT_DIR"), "/meli.conf.txt")),
|
||||
include_str!(concat!(env!("OUT_DIR"), "/meli-themes.txt")),
|
||||
];
|
||||
use flate2::bufread::GzDecoder;
|
||||
use std::io::prelude::*;
|
||||
let mut gz = GzDecoder::new(MANPAGES[page as usize]);
|
||||
let mut v = String::with_capacity(
|
||||
str::parse::<usize>(unsafe {
|
||||
std::str::from_utf8_unchecked(gz.header().unwrap().comment().unwrap())
|
||||
})
|
||||
.unwrap_or_else(|_| {
|
||||
panic!("{:?} was not compressed with size comment header", page)
|
||||
}),
|
||||
);
|
||||
gz.read_to_string(&mut v)?;
|
||||
|
||||
if let Some(no_raw) = no_raw {
|
||||
match no_raw {
|
||||
Some(true) => {}
|
||||
None if (unsafe { libc::isatty(libc::STDOUT_FILENO) == 1 }) => {}
|
||||
Some(false) | None => {
|
||||
println!("{}", &v);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
} else if unsafe { libc::isatty(libc::STDOUT_FILENO) != 1 } {
|
||||
println!("{}", &v);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
use std::process::{Command, Stdio};
|
||||
let mut handle = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(std::env::var("PAGER").unwrap_or_else(|_| "more".to_string()))
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()?;
|
||||
handle.stdin.take().unwrap().write_all(v.as_bytes())?;
|
||||
handle.wait()?;
|
||||
|
||||
println!("{}", MANPAGES[_page as usize]);
|
||||
return Ok(());
|
||||
}
|
||||
#[cfg(not(feature = "cli-docs"))]
|
||||
Some(SubCommand::Man(_manopt)) => {
|
||||
return Err(MeliError::new("error: this version of meli was not build with embedded documentation (cargo feature `cli-docs`). You might have it installed as manpages (eg `man meli`), otherwise check https://meli.delivery"));
|
||||
}
|
||||
Some(SubCommand::CompiledWith) => {
|
||||
#[cfg(feature = "notmuch")]
|
||||
println!("notmuch");
|
||||
#[cfg(feature = "jmap")]
|
||||
println!("jmap");
|
||||
#[cfg(feature = "sqlite3")]
|
||||
println!("sqlite3");
|
||||
#[cfg(feature = "smtp")]
|
||||
println!("smtp");
|
||||
#[cfg(feature = "regexp")]
|
||||
println!("regexp");
|
||||
#[cfg(feature = "dbus-notifications")]
|
||||
println!("dbus-notifications");
|
||||
#[cfg(feature = "cli-docs")]
|
||||
println!("cli-docs");
|
||||
#[cfg(feature = "gpgme")]
|
||||
println!("gpgme");
|
||||
return Ok(());
|
||||
return Err(MeliError::new("error: this version of meli was not build with embedded documentation. You might have it installed as manpages (eg `man meli`), otherwise check https://meli.delivery"));
|
||||
}
|
||||
Some(SubCommand::PrintLoadedThemes) => {
|
||||
let s = conf::FileSettings::new()?;
|
||||
print!("{}", s.terminal.themes);
|
||||
print!("{}", s.terminal.themes.to_string());
|
||||
return Ok(());
|
||||
}
|
||||
Some(SubCommand::PrintDefaultTheme) => {
|
||||
|
@ -299,9 +295,9 @@ fn run_app(opt: Opt) -> Result<()> {
|
|||
/* Catch SIGWINCH to handle terminal resizing */
|
||||
let signals = &[
|
||||
/* Catch SIGWINCH to handle terminal resizing */
|
||||
signal_hook::consts::SIGWINCH,
|
||||
signal_hook::SIGWINCH,
|
||||
/* Catch SIGCHLD to handle embed applications status change */
|
||||
signal_hook::consts::SIGCHLD,
|
||||
signal_hook::SIGCHLD,
|
||||
];
|
||||
|
||||
let signal_recvr = notify(signals, sender.clone())?;
|
||||
|
@ -328,18 +324,20 @@ fn run_app(opt: Opt) -> Result<()> {
|
|||
vec![
|
||||
Box::new(listing::Listing::new(&mut state.context)),
|
||||
Box::new(ContactList::new(&state.context)),
|
||||
Box::new(StatusPanel::new(crate::conf::value(
|
||||
&state.context,
|
||||
"theme_default",
|
||||
))),
|
||||
],
|
||||
&state.context,
|
||||
));
|
||||
|
||||
let status_bar = Box::new(StatusBar::new(&state.context, window));
|
||||
let status_bar = Box::new(StatusBar::new(window));
|
||||
state.register_component(status_bar);
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "dbus-notifications"))]
|
||||
#[cfg(feature = "dbus-notifications")]
|
||||
{
|
||||
let dbus_notifications = Box::new(components::notifications::DbusNotifications::new(
|
||||
&state.context,
|
||||
));
|
||||
let dbus_notifications = Box::new(components::notifications::DbusNotifications::new());
|
||||
state.register_component(dbus_notifications);
|
||||
}
|
||||
state.register_component(Box::new(
|
||||
|
@ -353,7 +351,6 @@ fn run_app(opt: Opt) -> Result<()> {
|
|||
.general
|
||||
.enter_command_mode
|
||||
.clone();
|
||||
let quit_key: Key = state.context.settings.shortcuts.general.quit.clone();
|
||||
|
||||
/* Keep track of the input mode. See UIMode for details */
|
||||
'main: loop {
|
||||
|
@ -400,7 +397,7 @@ fn run_app(opt: Opt) -> Result<()> {
|
|||
match state.mode {
|
||||
UIMode::Normal => {
|
||||
match k {
|
||||
_ if k == quit_key => {
|
||||
Key::Char('q') | Key::Char('Q') => {
|
||||
if state.can_quit_cleanly() {
|
||||
drop(state);
|
||||
break 'main;
|
||||
|
@ -421,7 +418,8 @@ fn run_app(opt: Opt) -> Result<()> {
|
|||
},
|
||||
UIMode::Insert => {
|
||||
match k {
|
||||
Key::Esc => {
|
||||
Key::Char('\n') | Key::Esc => {
|
||||
state.mode = UIMode::Normal;
|
||||
state.rcv_event(UIEvent::ChangeMode(UIMode::Normal));
|
||||
state.redraw();
|
||||
},
|
||||
|
@ -484,14 +482,14 @@ fn run_app(opt: Opt) -> Result<()> {
|
|||
},
|
||||
recv(signal_recvr) -> sig => {
|
||||
match sig.unwrap() {
|
||||
signal_hook::consts::SIGWINCH => {
|
||||
signal_hook::SIGWINCH => {
|
||||
if state.mode != UIMode::Fork {
|
||||
state.update_size();
|
||||
state.render();
|
||||
state.redraw();
|
||||
}
|
||||
},
|
||||
signal_hook::consts::SIGCHLD => {
|
||||
signal_hook::SIGCHLD => {
|
||||
state.rcv_event(UIEvent::EmbedInput((Key::Null, vec![0])));
|
||||
state.redraw();
|
||||
|
315
src/command.rs
315
src/command.rs
|
@ -28,8 +28,7 @@ use melib::nom::{
|
|||
bytes::complete::{is_a, is_not, tag, take_until},
|
||||
character::complete::{digit1, not_line_ending},
|
||||
combinator::{map, map_res},
|
||||
error::Error as NomError,
|
||||
multi::separated_list1,
|
||||
multi::separated_list,
|
||||
sequence::{pair, preceded, separated_pair},
|
||||
IResult,
|
||||
};
|
||||
|
@ -58,7 +57,7 @@ macro_rules! to_stream {
|
|||
};
|
||||
($($tokens:expr),*) => {
|
||||
TokenStream {
|
||||
tokens: &[$($tokens),*],
|
||||
tokens: &[$($token),*],
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -73,10 +72,7 @@ macro_rules! define_commands {
|
|||
|
||||
pub fn quoted_argument(input: &[u8]) -> IResult<&[u8], &str> {
|
||||
if input.is_empty() {
|
||||
return Err(nom::Err::Error(NomError {
|
||||
input,
|
||||
code: nom::error::ErrorKind::Tag,
|
||||
}));
|
||||
return Err(nom::Err::Error((input, nom::error::ErrorKind::Tag)));
|
||||
}
|
||||
|
||||
if input[0] == b'"' {
|
||||
|
@ -89,10 +85,7 @@ pub fn quoted_argument(input: &[u8]) -> IResult<&[u8], &str> {
|
|||
}
|
||||
i += 1;
|
||||
}
|
||||
Err(nom::Err::Error(NomError {
|
||||
input,
|
||||
code: nom::error::ErrorKind::Tag,
|
||||
}))
|
||||
Err(nom::Err::Error((input, nom::error::ErrorKind::Tag)))
|
||||
} else {
|
||||
map_res(is_not(" "), std::str::from_utf8)(input)
|
||||
}
|
||||
|
@ -115,44 +108,36 @@ impl TokenStream {
|
|||
ptr += 1;
|
||||
}
|
||||
*s = &s[ptr..];
|
||||
//println!("\t before s.is_empty() {:?} {:?}", t, s);
|
||||
if s.is_empty() || *s == " " {
|
||||
//println!("{:?} {:?}", t, s);
|
||||
if s.is_empty() {
|
||||
match t.inner() {
|
||||
Literal(lit) => {
|
||||
sugg.insert(format!("{}{}", if s.is_empty() { " " } else { "" }, lit));
|
||||
sugg.insert(format!(" {}", lit));
|
||||
}
|
||||
Alternatives(v) => {
|
||||
for t in v.iter() {
|
||||
//println!("adding empty suggestions for {:?}", t);
|
||||
let mut _s = *s;
|
||||
let mut m = t.matches(&mut _s, sugg);
|
||||
tokens.append(&mut m);
|
||||
t.matches(&mut _s, sugg);
|
||||
}
|
||||
}
|
||||
Seq(_s) => {}
|
||||
RestOfStringValue => {
|
||||
sugg.insert(String::new());
|
||||
}
|
||||
t @ AttachmentIndexValue
|
||||
| t @ MailboxIndexValue
|
||||
| t @ IndexValue
|
||||
| t @ Filepath
|
||||
| t @ AccountName
|
||||
| t @ MailboxPath
|
||||
| t @ QuotedStringValue
|
||||
| t @ AlphanumericStringValue => {
|
||||
let _t = t;
|
||||
//sugg.insert(format!("{}{:?}", if s.is_empty() { " " } else { "" }, t));
|
||||
}
|
||||
RestOfStringValue => {}
|
||||
AttachmentIndexValue
|
||||
| MailboxIndexValue
|
||||
| IndexValue
|
||||
| Filepath
|
||||
| AccountName
|
||||
| MailboxPath
|
||||
| QuotedStringValue
|
||||
| AlphanumericStringValue => {}
|
||||
}
|
||||
tokens.push((*s, *t.inner()));
|
||||
return tokens;
|
||||
}
|
||||
match t.inner() {
|
||||
Literal(lit) => {
|
||||
if lit.starts_with(*s) && lit.len() != s.len() {
|
||||
sugg.insert(lit[s.len()..].to_string());
|
||||
tokens.push((s, *t.inner()));
|
||||
return tokens;
|
||||
} else if s.starts_with(lit) {
|
||||
tokens.push((&s[..lit.len()], *t.inner()));
|
||||
|
@ -167,16 +152,13 @@ impl TokenStream {
|
|||
let mut _s = *s;
|
||||
let mut m = t.matches(&mut _s, sugg);
|
||||
if !m.is_empty() {
|
||||
tokens.append(&mut m);
|
||||
tokens.extend(m.drain(..));
|
||||
//println!("_s is empty {}", _s.is_empty());
|
||||
cont = !_s.is_empty();
|
||||
*s = _s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if tokens.is_empty() {
|
||||
return tokens;
|
||||
}
|
||||
if !cont {
|
||||
*s = "";
|
||||
}
|
||||
|
@ -250,10 +232,7 @@ fn eof(input: &[u8]) -> IResult<&[u8], ()> {
|
|||
if input.is_empty() {
|
||||
Ok((input, ()))
|
||||
} else {
|
||||
Err(nom::Err::Error(NomError {
|
||||
input,
|
||||
code: nom::error::ErrorKind::Tag,
|
||||
}))
|
||||
Err(nom::Err::Error((input, nom::error::ErrorKind::Tag)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -262,7 +241,7 @@ define_commands!([
|
|||
desc: "set [seen/unseen], toggles message's Seen flag.",
|
||||
tokens: &[One(Literal("set")), One(Alternatives(&[to_stream!(One(Literal("seen"))), to_stream!(One(Literal("unseen")))]))],
|
||||
parser: (
|
||||
fn seen_flag(input: &'_ [u8]) -> IResult<&'_ [u8], Action> {
|
||||
fn seen_flag<'a>(input: &'a [u8]) -> IResult<&'a [u8], Action> {
|
||||
let (input, _) = tag("set")(input.trim())?;
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, ret) = alt((map(tag("seen"), |_| Listing(SetSeen)), map(tag("unseen"), |_| Listing(SetUnseen))))(input)?;
|
||||
|
@ -275,7 +254,7 @@ define_commands!([
|
|||
desc: "delete message",
|
||||
tokens: &[One(Literal("delete"))],
|
||||
parser: (
|
||||
fn delete_message(input: &'_ [u8]) -> IResult<&'_ [u8], Action> {
|
||||
fn delete_message<'a>(input: &'a [u8]) -> IResult<&'a [u8], Action> {
|
||||
let (input, ret) = map(preceded(tag("delete"), eof), |_| Listing(Delete))(input)?;
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, ret))
|
||||
|
@ -324,21 +303,6 @@ define_commands!([
|
|||
}
|
||||
)
|
||||
},
|
||||
{ tags: ["import "],
|
||||
desc: "import FILESYSTEM_PATH MAILBOX_PATH",
|
||||
tokens: &[One(Literal("import")), One(Filepath), One(MailboxPath)],
|
||||
parser:(
|
||||
fn import(input: &[u8]) -> IResult<&[u8], Action> {
|
||||
let (input, _) = tag("import")(input.trim())?;
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, file) = quoted_argument(input)?;
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, mailbox_path) = quoted_argument(input)?;
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Listing(Import(file.to_string().into(), mailbox_path.to_string()))))
|
||||
}
|
||||
)
|
||||
},
|
||||
{ tags: ["close"],
|
||||
desc: "close non-sticky tabs",
|
||||
tokens: &[One(Literal("close"))],
|
||||
|
@ -401,16 +365,14 @@ define_commands!([
|
|||
}
|
||||
)
|
||||
},
|
||||
{ tags: ["toggle thread_snooze"],
|
||||
{ tags: ["toggle_thread_snooze"],
|
||||
desc: "turn off new notifications for this thread",
|
||||
tokens: &[One(Literal("toggle")), One(Literal("thread_snooze"))],
|
||||
tokens: &[One(Literal("toggle_thread_snooze"))],
|
||||
parser: (
|
||||
fn toggle_thread_snooze(input: &[u8]) -> IResult<&[u8], Action> {
|
||||
let (input, _) = tag("toggle")(input)?;
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, _) = tag("thread_snooze")(input)?;
|
||||
let (input, _) = tag("toggle_thread_snooze")(input.trim())?;
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Listing(ToggleThreadSnooze)))
|
||||
Ok((input, ToggleThreadSnooze))
|
||||
}
|
||||
)
|
||||
},
|
||||
|
@ -440,19 +402,6 @@ define_commands!([
|
|||
}
|
||||
)
|
||||
},
|
||||
{ tags: ["export-mbox "],
|
||||
desc: "export-mbox PATH",
|
||||
tokens: &[One(Literal("export-mbox")), One(Filepath)],
|
||||
parser:(
|
||||
fn export_mbox(input: &[u8]) -> IResult<&[u8], Action> {
|
||||
let (input, _) = tag("export-mbox")(input.trim())?;
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, path) = quoted_argument(input.trim())?;
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Listing(ExportMbox(Some(melib::backends::mbox::MboxFormat::MboxCl2), path.to_string().into()))))
|
||||
}
|
||||
)
|
||||
},
|
||||
{ tags: ["list-archive", "list-post", "list-unsubscribe", "list-"],
|
||||
desc: "list-[unsubscribe/post/archive]",
|
||||
tokens: &[One(Alternatives(&[to_stream!(One(Literal("list-archive"))), to_stream!(One(Literal("list-post"))), to_stream!(One(Literal("list-unsubscribe")))]))],
|
||||
|
@ -512,7 +461,7 @@ define_commands!([
|
|||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, bin) = quoted_argument(input)?;
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, args) = separated_list1(is_a(" "), quoted_argument)(input)?;
|
||||
let (input, args) = separated_list(is_a(" "), quoted_argument)(input)?;
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, {
|
||||
View(Pipe(bin.to_string(), args.into_iter().map(String::from).collect::<Vec<String>>()))
|
||||
|
@ -531,25 +480,9 @@ define_commands!([
|
|||
}
|
||||
)
|
||||
},
|
||||
/* Filter pager contents through binary */
|
||||
{ tags: ["filter "],
|
||||
desc: "filter EXECUTABLE ARGS",
|
||||
tokens: &[One(Literal("filter")), One(Filepath), ZeroOrMore(QuotedStringValue)],
|
||||
parser:(
|
||||
fn filter(input: &'_ [u8]) -> IResult<&'_ [u8], Action> {
|
||||
let (input, _) = tag("filter")(input.trim())?;
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, cmd) = map_res(not_line_ending, std::str::from_utf8)(input)?;
|
||||
Ok((input, {
|
||||
View(Filter(cmd.to_string()))
|
||||
}))
|
||||
}
|
||||
)
|
||||
},
|
||||
{ tags: ["add-attachment ", "add-attachment-file-picker "],
|
||||
{ tags: ["add-attachment "],
|
||||
desc: "add-attachment PATH",
|
||||
tokens: &[One(
|
||||
Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_stream!(One(Literal("add-attachment-file-picker")))]))],
|
||||
tokens: &[One(Literal("add-attachment")), One(Filepath)],
|
||||
parser:(
|
||||
fn add_attachment<'a>(input: &'a [u8]) -> IResult<&'a [u8], Action> {
|
||||
alt((
|
||||
|
@ -567,18 +500,6 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str
|
|||
let (input, path) = quoted_argument(input)?;
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Compose(AddAttachment(path.to_string()))))
|
||||
}, |input: &'a [u8]| -> IResult<&'a [u8], Action> {
|
||||
let (input, _) = tag("add-attachment-file-picker")(input.trim())?;
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Compose(AddAttachmentFilePicker(None))))
|
||||
}, |input: &'a [u8]| -> IResult<&'a [u8], Action> {
|
||||
let (input, _) = tag("add-attachment-file-picker")(input.trim())?;
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, _) = tag("<")(input.trim())?;
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, shell) = map_res(not_line_ending, std::str::from_utf8)(input)?;
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Compose(AddAttachmentFilePicker(Some(shell.to_string())))))
|
||||
}
|
||||
))(input)
|
||||
}
|
||||
|
@ -621,19 +542,6 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str
|
|||
}
|
||||
)
|
||||
},
|
||||
{ tags: ["toggle encrypt"],
|
||||
desc: "toggle encryption for this draft",
|
||||
tokens: &[One(Literal("toggle")), One(Literal("encrypt"))],
|
||||
parser:(
|
||||
fn toggle_encrypt(input: &[u8]) -> IResult<&[u8], Action> {
|
||||
let (input, _) = tag("toggle")(input)?;
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, _) = tag("encrypt")(input)?;
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, Compose(ToggleEncrypt)))
|
||||
}
|
||||
)
|
||||
},
|
||||
{ tags: ["create-mailbox "],
|
||||
desc: "create-mailbox ACCOUNT MAILBOX_PATH",
|
||||
tokens: &[One(Literal("create-mailbox")), One(AccountName), One(MailboxPath)],
|
||||
|
@ -750,30 +658,6 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str
|
|||
}
|
||||
)
|
||||
},
|
||||
{ tags: ["export-mail "],
|
||||
desc: "export-mail PATH",
|
||||
tokens: &[One(Literal("export-mail")), One(Filepath)],
|
||||
parser:(
|
||||
fn export_mail(input: &[u8]) -> IResult<&[u8], Action> {
|
||||
let (input, _) = tag("export-mail")(input.trim())?;
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, path) = quoted_argument(input.trim())?;
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, View(ExportMail(path.to_string()))))
|
||||
}
|
||||
)
|
||||
},
|
||||
{ tags: ["add-addresses-to-contacts "],
|
||||
desc: "add-addresses-to-contacts",
|
||||
tokens: &[One(Literal("add-addresses-to-contacts"))],
|
||||
parser:(
|
||||
fn add_addresses_to_contacts(input: &[u8]) -> IResult<&[u8], Action> {
|
||||
let (input, _) = tag("add-addresses-to-contacts")(input.trim())?;
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, View(AddAddressesToContacts)))
|
||||
}
|
||||
)
|
||||
},
|
||||
{ tags: ["tag", "tag add", "tag remove"],
|
||||
desc: "tag [add/remove], edits message's tags.",
|
||||
tokens: &[One(Literal("tag")), One(Alternatives(&[to_stream!(One(Literal("add"))), to_stream!(One(Literal("remove")))]))],
|
||||
|
@ -826,41 +710,6 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str
|
|||
Ok((input, PrintSetting(setting.to_string())))
|
||||
}
|
||||
)
|
||||
},
|
||||
{ tags: ["toggle mouse"],
|
||||
desc: "toggle mouse support",
|
||||
tokens: &[One(Literal("toggle")), One(Literal("mouse"))],
|
||||
parser:(
|
||||
fn toggle_mouse(input: &[u8]) -> IResult<&[u8], Action> {
|
||||
let (input, _) = tag("toggle")(input)?;
|
||||
let (input, _) = is_a(" ")(input)?;
|
||||
let (input, _) = tag("mouse")(input)?;
|
||||
let (input, _) = eof(input)?;
|
||||
Ok((input, ToggleMouse))
|
||||
}
|
||||
)
|
||||
},
|
||||
{ tags: ["quit"],
|
||||
desc: "quit meli",
|
||||
tokens: &[One(Literal("quit"))],
|
||||
parser:(
|
||||
fn quit(input: &[u8]) -> IResult<&[u8], Action> {
|
||||
let (input, _) = tag("quit")(input.trim())?;
|
||||
let (input, _) = eof(input.trim())?;
|
||||
Ok((input, Quit))
|
||||
}
|
||||
)
|
||||
},
|
||||
{ tags: ["reload-config"],
|
||||
desc: "reload configuration file",
|
||||
tokens: &[One(Literal("reload-config"))],
|
||||
parser:(
|
||||
fn reload_config(input: &[u8]) -> IResult<&[u8], Action> {
|
||||
let (input, _) = tag("reload-config")(input.trim())?;
|
||||
let (input, _) = eof(input.trim())?;
|
||||
Ok((input, ReloadConfiguration))
|
||||
}
|
||||
)
|
||||
}
|
||||
]);
|
||||
|
||||
|
@ -907,24 +756,16 @@ fn listing_action(input: &[u8]) -> IResult<&[u8], Action> {
|
|||
seen_flag,
|
||||
delete_message,
|
||||
copymove,
|
||||
import,
|
||||
search,
|
||||
select,
|
||||
toggle_thread_snooze,
|
||||
open_in_new_tab,
|
||||
export_mbox,
|
||||
_tag,
|
||||
))(input)
|
||||
}
|
||||
|
||||
fn compose_action(input: &[u8]) -> IResult<&[u8], Action> {
|
||||
alt((
|
||||
add_attachment,
|
||||
remove_attachment,
|
||||
toggle_sign,
|
||||
toggle_encrypt,
|
||||
save_draft,
|
||||
))(input)
|
||||
alt((add_attachment, remove_attachment, toggle_sign, save_draft))(input)
|
||||
}
|
||||
|
||||
fn account_action(input: &[u8]) -> IResult<&[u8], Action> {
|
||||
|
@ -932,13 +773,7 @@ fn account_action(input: &[u8]) -> IResult<&[u8], Action> {
|
|||
}
|
||||
|
||||
fn view(input: &[u8]) -> IResult<&[u8], Action> {
|
||||
alt((
|
||||
filter,
|
||||
pipe,
|
||||
save_attachment,
|
||||
export_mail,
|
||||
add_addresses_to_contacts,
|
||||
))(input)
|
||||
alt((pipe, save_attachment))(input)
|
||||
}
|
||||
|
||||
pub fn parse_command(input: &[u8]) -> Result<Action, MeliError> {
|
||||
|
@ -960,102 +795,40 @@ pub fn parse_command(input: &[u8]) -> Result<Action, MeliError> {
|
|||
rename_mailbox,
|
||||
account_action,
|
||||
print_setting,
|
||||
toggle_mouse,
|
||||
reload_config,
|
||||
quit,
|
||||
))(input)
|
||||
.map(|(_, v)| v)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
let mut input = "sort".to_string();
|
||||
macro_rules! match_input {
|
||||
($input:expr) => {{
|
||||
let mut sugg = Default::default();
|
||||
let mut vec = vec![];
|
||||
//print!("{}", $input);
|
||||
for (_tags, _desc, tokens) in COMMAND_COMPLETION.iter() {
|
||||
//println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
|
||||
let m = tokens.matches(&mut $input.as_str(), &mut sugg);
|
||||
if !m.is_empty() {
|
||||
vec.push(tokens);
|
||||
//print!("{:?} ", desc);
|
||||
//println!(" result = {:#?}\n\n", m);
|
||||
}
|
||||
}
|
||||
//println!("suggestions = {:#?}", sugg);
|
||||
sugg.into_iter()
|
||||
.map(|s| format!("{}{}", $input.as_str(), s.as_str()))
|
||||
.collect::<HashSet<String>>()
|
||||
}};
|
||||
}
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["sort date".to_string(), "sort subject".to_string()]).collect(),
|
||||
);
|
||||
input = "so".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["sort".to_string()]).collect(),
|
||||
);
|
||||
input = "so ".to_string();
|
||||
assert_eq!(&match_input!(input), &HashSet::default(),);
|
||||
input = "to".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter(["toggle".to_string()]).collect(),
|
||||
);
|
||||
input = "toggle ".to_string();
|
||||
assert_eq!(
|
||||
&match_input!(input),
|
||||
&IntoIterator::into_iter([
|
||||
"toggle mouse".to_string(),
|
||||
"toggle sign".to_string(),
|
||||
"toggle encrypt".to_string(),
|
||||
"toggle thread_snooze".to_string()
|
||||
])
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_parser_interactive() {
|
||||
fn test_parser() {
|
||||
use std::io;
|
||||
let mut input = String::new();
|
||||
loop {
|
||||
input.clear();
|
||||
print!("> ");
|
||||
match io::stdin().read_line(&mut input) {
|
||||
Ok(_n) => {
|
||||
println!("Input is {:?}", input.as_str().trim());
|
||||
let mut sugg = Default::default();
|
||||
let mut vec = vec![];
|
||||
//print!("{}", input);
|
||||
for (_tags, _desc, tokens) in COMMAND_COMPLETION.iter() {
|
||||
//println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
|
||||
for (_tags, desc, tokens) in COMMAND_COMPLETION.iter() {
|
||||
let m = tokens.matches(&mut input.as_str().trim(), &mut sugg);
|
||||
if !m.is_empty() {
|
||||
vec.push(tokens);
|
||||
//print!("{:?} ", desc);
|
||||
//println!(" result = {:#?}\n\n", m);
|
||||
print!("{:?} ", desc);
|
||||
println!(" result = {:#?}\n\n", m);
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"suggestions = {:#?}",
|
||||
sugg.into_iter()
|
||||
.zip(vec.into_iter())
|
||||
.map(|(s, v)| format!(
|
||||
"{}{} {:?}",
|
||||
.map(|s| format!(
|
||||
"{}{}",
|
||||
input.as_str().trim(),
|
||||
if input.trim().is_empty() {
|
||||
s.trim()
|
||||
} else {
|
||||
s.as_str()
|
||||
},
|
||||
v
|
||||
}
|
||||
))
|
||||
.collect::<Vec<String>>()
|
||||
);
|
||||
|
@ -1080,10 +853,20 @@ pub fn command_completion_suggestions(input: &str) -> Vec<String> {
|
|||
}
|
||||
if let Some((s, Filepath)) = _m.last() {
|
||||
let p = std::path::Path::new(s);
|
||||
sugg.extend(p.complete(true).into_iter());
|
||||
sugg.extend(p.complete(true).into_iter().map(|m| m.into()));
|
||||
}
|
||||
}
|
||||
sugg.into_iter()
|
||||
.map(|s| format!("{}{}", input, s.as_str()))
|
||||
.map(|s| {
|
||||
format!(
|
||||
"{}{}",
|
||||
input.trim(),
|
||||
if input.trim().is_empty() {
|
||||
s.trim()
|
||||
} else {
|
||||
s.as_str()
|
||||
}
|
||||
)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
}
|
||||
|
|
|
@ -25,8 +25,9 @@
|
|||
|
||||
use crate::components::Component;
|
||||
pub use melib::thread::{SortField, SortOrder};
|
||||
use melib::uuid::Uuid;
|
||||
use std::path::PathBuf;
|
||||
|
||||
extern crate uuid;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TagAction {
|
||||
|
@ -48,12 +49,9 @@ pub enum ListingAction {
|
|||
CopyToOtherAccount(AccountName, MailboxPath),
|
||||
MoveTo(MailboxPath),
|
||||
MoveToOtherAccount(AccountName, MailboxPath),
|
||||
Import(PathBuf, MailboxPath),
|
||||
ExportMbox(Option<melib::backends::mbox::MboxFormat>, PathBuf),
|
||||
Delete,
|
||||
OpenInNewTab,
|
||||
Tag(TagAction),
|
||||
ToggleThreadSnooze,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -73,21 +71,16 @@ pub enum MailingListAction {
|
|||
#[derive(Debug)]
|
||||
pub enum ViewAction {
|
||||
Pipe(String, Vec<String>),
|
||||
Filter(String),
|
||||
SaveAttachment(usize, String),
|
||||
ExportMail(String),
|
||||
AddAddressesToContacts,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ComposeAction {
|
||||
AddAttachment(String),
|
||||
AddAttachmentFilePicker(Option<String>),
|
||||
AddAttachmentPipe(String),
|
||||
RemoveAttachment(usize),
|
||||
SaveDraft,
|
||||
ToggleSign,
|
||||
ToggleEncrypt,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -114,6 +107,7 @@ pub enum Action {
|
|||
Sort(SortField, SortOrder),
|
||||
SubSort(SortField, SortOrder),
|
||||
Tab(TabAction),
|
||||
ToggleThreadSnooze,
|
||||
MailingListAction(MailingListAction),
|
||||
View(ViewAction),
|
||||
SetEnv(String, String),
|
||||
|
@ -122,20 +116,17 @@ pub enum Action {
|
|||
Mailbox(AccountName, MailboxOperation),
|
||||
AccountAction(AccountName, AccountAction),
|
||||
PrintSetting(String),
|
||||
ReloadConfiguration,
|
||||
ToggleMouse,
|
||||
Quit,
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub fn needs_confirmation(&self) -> bool {
|
||||
match self {
|
||||
Action::Listing(ListingAction::Delete) => true,
|
||||
Action::Listing(_) => false,
|
||||
Action::ViewMailbox(_) => false,
|
||||
Action::Sort(_, _) => false,
|
||||
Action::SubSort(_, _) => false,
|
||||
Action::Tab(_) => false,
|
||||
Action::ToggleThreadSnooze => false,
|
||||
Action::MailingListAction(_) => true,
|
||||
Action::View(_) => false,
|
||||
Action::SetEnv(_, _) => false,
|
||||
|
@ -144,9 +135,6 @@ impl Action {
|
|||
Action::Mailbox(_, _) => true,
|
||||
Action::AccountAction(_, _) => false,
|
||||
Action::PrintSetting(_) => false,
|
||||
Action::ToggleMouse => false,
|
||||
Action::Quit => true,
|
||||
Action::ReloadConfiguration => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,34 +54,6 @@ pub type ComponentId = Uuid;
|
|||
pub type ShortcutMap = IndexMap<&'static str, Key>;
|
||||
pub type ShortcutMaps = IndexMap<&'static str, ShortcutMap>;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum PageMovement {
|
||||
Up(usize),
|
||||
Right(usize),
|
||||
Left(usize),
|
||||
Down(usize),
|
||||
PageUp(usize),
|
||||
PageDown(usize),
|
||||
Home,
|
||||
End,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ScrollContext {
|
||||
shown_lines: usize,
|
||||
total_lines: usize,
|
||||
has_more_lines: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ScrollUpdate {
|
||||
End(ComponentId),
|
||||
Update {
|
||||
id: ComponentId,
|
||||
context: ScrollContext,
|
||||
},
|
||||
}
|
||||
|
||||
/// Types implementing this Trait can draw on the terminal and receive events.
|
||||
/// If a type wants to skip drawing if it has not changed anything, it can hold some flag in its
|
||||
/// fields (eg self.dirty = false) and act upon that in their `draw` implementation.
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue