Compare commits

..

1 Commits

Author SHA1 Message Date
Manos Pitsidianakis 413be3f334
Add read-only memfd backed temporary files 2020-09-18 11:29:09 +03:00
164 changed files with 15317 additions and 38040 deletions

View File

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

1669
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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
View File

@ -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.

View File

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

View File

@ -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 {

View File

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

59
debian/changelog vendored
View File

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

6
debian/meli.docs vendored
View File

@ -1,3 +1,3 @@
docs/meli.1
docs/meli.conf.5
docs/meli-themes.5
meli.1
meli.conf.5
meli-themes.5

View File

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

View File

@ -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"

View File

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

2236
fuzz/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -17,29 +17,6 @@
.\" You should have received a copy of the GNU General Public License
.\" along with meli. If not, see <http://www.gnu.org/licenses/>.
.\"
.de Shortcut
.Sm
.Aq \\$1
\
.Po
.Em shortcuts.\\$2\&. Ns
.Em \\$3
.Pc
.Sm
..
.de ShortcutPeriod
.Aq \\$1
.Po
.Em shortcuts.\\$2\&. Ns
.Em \\$3
.Pc Ns
..
.de Command
.Bd -ragged
.Cm \\$*
.Ed
.sp
..
.Dd July 29, 2019
.Dt MELI 1
.Os
@ -66,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

View File

@ -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 = []

View File

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

View File

@ -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(())
}

View File

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

View File

@ -19,14 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
//! # vCard format
//!
//! This module implements the standards:
//!
//! - Version 3 (read-only) [RFC 2426: vCard MIME Directory Profile](https://datatracker.ietf.org/doc/2426)
//! - Version 4 [RFC 6350: vCard Format Specification](https://datatracker.ietf.org/doc/rfc6350/)
//! - Parameter escaping [RFC 6868 Parameter Value Encoding in iCalendar and vCard](https://datatracker.ietf.org/doc/rfc6868/)
/// Convert VCard strings to meli Cards (contacts).
use super::*;
use crate::error::{MeliError, Result};
use crate::parsec::{match_literal_anycase, one_or_more, peek, prefix, take_until, Parser};
@ -40,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());
}

View File

@ -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
}
}

View File

@ -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![]);

View File

@ -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)),
}
}
}

View File

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

View File

@ -44,7 +44,7 @@ use super::{Capabilities, ImapServerConf, UIDStore};
#[derive(Debug, Clone, Copy)]
pub enum SyncPolicy {
None,
///rfc4549 `Synch Ops for Disconnected IMAP4 Clients` <https://tools.ietf.org/html/rfc4549>
///rfc4549 `Synch Ops for Disconnected IMAP4 Clients` https://tools.ietf.org/html/rfc4549
Basic,
///rfc7162 `IMAP Extensions: Quick Flag Changes Resynchronization (CONDSTORE) and Quick Mailbox Resynchronization (QRESYNC)`
Condstore,
@ -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());
}

View File

@ -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 {

View File

@ -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(())
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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(())
}

View File

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

View File

@ -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,
))
}
}

View File

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

View File

@ -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,
}

View File

@ -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,

View File

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

View File

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

View File

@ -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,
}

View File

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

View File

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

View File

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

View File

@ -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`: Messages 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: Messages allocated UID
/// - Status: R (Seen) and O (non-Recent) flags
/// - X-Status: A (Answered), F (Flagged), T (Draft) and D (Deleted) flags
/// - X-Keywords: Messages 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!(

View File

@ -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(())
}
}
}
}

View File

@ -19,13 +19,6 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
//! # NNTP backend / client
//!
//! Implements an NNTP client as specified by [RFC 3977: Network News Transfer Protocol
//! (NNTP)](https://datatracker.ietf.org/doc/html/rfc3977). Also implements [RFC 6048: Network News
//! Transfer Protocol (NNTP) Additions to LIST
//! Command](https://datatracker.ietf.org/doc/html/rfc6048).
use crate::get_conf_val;
use crate::get_path_hash;
use smallvec::SmallVec;
@ -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))
}
}
}

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

@ -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())
}
}

View File

@ -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()
}
}

View File

@ -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,
})
}
}

View File

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

View File

@ -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,

View File

@ -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 {

View File

@ -112,7 +112,7 @@ impl Address {
}
} else {
MailboxAddress {
raw: address.to_string().into_bytes(),
raw: format!("{}", address).into_bytes(),
display_name: StrBuilder {
offset: 0,
length: 0,
@ -262,7 +262,7 @@ impl Address {
let email = self.get_email();
let (local_part, domain) =
match super::parser::address::addr_spec_raw(email.as_bytes())
.map_err(Into::<MeliError>::into)
.map_err(|err| Into::<MeliError>::into(err))
.and_then(|(_, (l, d))| {
Ok((String::from_utf8(l.into())?, String::from_utf8(d.into())?))
}) {
@ -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()

View File

@ -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()
}
}
}

View File

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

View File

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

View File

@ -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",

View File

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

View File

@ -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") {

View File

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

View File

@ -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]

View File

@ -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,
}

View File

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

View File

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

View File

@ -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(),
)
});
}
}
}
}

View File

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

View File

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

View File

@ -25,9 +25,6 @@
/*!
* SMTP client support
*
* This module implements a client for the SMTP protocol as specified by [RFC 5321 Simple Mail
* Transfer Protocol](https://www.rfc-editor.org/rfc/rfc5321).
*
* The connection and methods are `async` and uses the `smol` runtime.
*# Example
*
@ -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(&current_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);
}
}

View File

@ -34,9 +34,9 @@ pub struct DatabaseDescription {
pub fn db_path(name: &str) -> Result<PathBuf> {
let data_dir =
xdg::BaseDirectories::with_prefix("meli").map_err(|e| MeliError::new(e.to_string()))?;
data_dir
Ok(data_dir
.place_data_file(name)
.map_err(|err| MeliError::new(err.to_string()))
.map_err(|e| MeliError::new(e.to_string()))?)
}
pub fn open_db(db_path: PathBuf) -> Result<Connection> {
@ -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()))))?)
}
}

View File

@ -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)>>()
}

View File

@ -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(&quotes)
.replace(&format!("\n{}", &quotes), "")
.replace('\n', "")
.replace('\r', "");
.replace("\n", "")
.replace("\r", "");
if in_paragraph {
if let Some(width) = width {
ret.extend(
@ -1196,7 +1183,7 @@ fn reflow_helper(
ret.push(format!("{}{}", &quotes, &paragraph));
}
} else {
let paragraph = paragraph.replace('\n', "").replace('\r', "");
let paragraph = paragraph.replace("\n", "").replace("\r", "");
if in_paragraph {
if let Some(width) = width {
@ -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(&quotes)
.replace(&format!("\n{}", &quotes), "")
.replace('\n', "")
.replace('\r', "");
if in_paragraph {
if let Some(width) = width {
ret.extend(
linear(&paragraph, width.saturating_sub(quote_depth))
.into_iter()
.map(|l| format!("{}{}", &quotes, l)),
);
} else {
ret.push_back(format!("{}{}", &quotes, &paragraph));
}
} else {
ret.push_back(format!("{}{}", &quotes, &paragraph));
}
} else {
let paragraph = paragraph.replace('\n', "").replace('\r', "");
if in_paragraph {
if let Some(width) = width {
let ex = linear(&paragraph, width);
ret.extend(ex.into_iter());
} else {
ret.push_back(paragraph);
}
} else {
ret.push_back(paragraph);
}
}
}

View File

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

View File

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

View File

@ -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 oclock # 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 workers 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] (🛳)
*/

View File

@ -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]> {

View File

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

View File

@ -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"

View File

@ -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" }

View File

@ -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" }

View File

@ -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" }

View File

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

View File

@ -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>>()
}

View File

@ -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,
}
}
}

View File

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