forked from meli/meli
1
Fork 0

Compare commits

..

5 Commits
master ... jmap

Author SHA1 Message Date
Manos Pitsidianakis a548f7509f
JMAP WIP #5 2019-12-05 00:04:03 +02:00
Manos Pitsidianakis bfa5bab15d
JMAP WIP #4 2019-12-04 19:45:30 +02:00
Manos Pitsidianakis 138c14f730
JMAP WIP #3 2019-12-04 01:04:38 +02:00
Manos Pitsidianakis 994e64d8a6
JMAP WIP #2 2019-12-03 21:29:26 +02:00
Manos Pitsidianakis 2a573af016
JMAP WIP 2019-12-03 13:30:42 +02:00
288 changed files with 42060 additions and 105715 deletions

12
.gitignore vendored
View File

@ -6,15 +6,3 @@ target/
**/*.rs.bk
.gdb_history
*.log
__pycache__/
*.py[cod]
debian/.debhelper/
debian/debhelper-build-stamp
debian/files
debian/meli.substvars
debian/meli/
# CLion IDE
.idea

View File

@ -1,223 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Added listing configuration setting `thread_subject_pack` (see meli.conf.5)
- Added shortcuts for focusing to sidebar menu and back to the e-mail view (`focus_left` and `focus_right`)
- `f76f4ea3` A new manual page, `meli.7` which contains a general tutorial for using meli.
- `cbe593cf` add configurable header preample suffix and prefix for editing
- `a484b397` Added instructions and information to error shown when libnotmuch could not be found.
- `a484b397` Added configuration setting `library_file_path` to notmuch backend if user wants to specify the library's location manually.
- `aa99b0d7` Implement configurable subject prefix stripping when replying
- `a73885ac` added RGB support to embedded terminal emulator.
- `f4e0970d` added ability to kill embed process with Ctrl-C, or Ctrl-Z and pressing 'q'.
- `9205f3b8` added a per account mail sort order parameter.
- `d921b3c3` implemented sorting with user sort order parameter if defined.
- `dc5afa13` use osascript/applescript for notifications on macos
- `d0de0485` add {in,de}crease_sidebar shortcuts
- `340d6451` add config setting for sidebar ratio
- `36e29cb6` Add configurable mailbox sort order
- `7606317f` melib/notmuch: add support for virtual mailbox hierarchy
Add optional `parent` property to notmuch mailbox configuration.
- `d9c07def` Add command to select charset encoding for email
Open dialog to select charset with `d`.
- `d679a744` melib/jmap: Implement Bearer token authentication
Fastmail now uses an API token in a http header for authentication.
This can be used either as a server_password or provided by a
`server_password_command` like oauth2.
- `47e6d5d9` add edit-config CLI subcommand that opens config files on `EDITOR`
- `8c671935` Add compose (pre-submission) hooks for validation/linting
compose-hooks run before submitting an e-mail.
They perform draft validation and/or transformations.
If a hook encounters an error or warning, it will show up as a notification.
The currently available hooks are:
- `past-date-warn`
Warn if Date header value is far in the past or future.
- `important-header-warn`
Warn if important headers (From, Date, To, Cc, Bcc) are missing or invalid.
- `missing-attachment-warn`
Warn if Subject, draft body mention attachments but they are missing.
- `empty-draft-warn`
Warn if draft has no subject and no body.
They can be disabled with `[composing.disabled_compose_hooks]` setting.
### Changed
- `f76f4ea3` Shortcut `open_thread` and `exit_thread` renamed to `open_entry` and `exit_entry`.
- `7650805c` Binary size reduced significantly.
### Fixed
- `a42a6ca8` show notifications in terminal if there is no other alternative.
## [alpha-0.7.2] - 2021-10-15
### Added
- Add forward mail option
- Add url_launcher config setting
- Add add_addresses_to_contacts command
- Add show_date_in_my_timezone pager config flag
- docs: add pager filter documentation
- mail/view: respect per-folder/account pager filter override
- pager: add filter command, esc to clear filter
- Show compile time features in with command argument
### Fixed
- melib/email/address: quote display_name if it contains ","
- melib/smtp: fix Cc and Bcc ignored when sending mail
- melib/email/address: quote display_name if it contains "."
## [alpha-0.7.1] - 2021-09-08
### Added
- Change all Down/Up shortcuts to j/k
- add 'GB18030' charset
- melib/nntp: implement refresh
- melib/nntp: update total/new counters on new articles
- melib/nntp: implement NNTP posting
- configs: throw error on extra unusued conf flags in some imap/nntp
- configs: throw error on missing `composing` section with explanation
### Fixed
- Fix compilation for netbsd-9.2
- conf: fixed some boolean flag values requiring to be string e.g. "true"
## [alpha-0.7.0] - 2021-09-03
### Added
Notable changes:
- add import command to import email from files into accounts
- add add-attachment-file-picker command and `file_picker_command` setting to
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
- added experimental NNTP backend
- added server extension support and use in account status tab
### Fixed
- imap: fixed IDLE connection getting stuck when using DEFLATE
## [alpha-0.6.0] - 2020-07-29
### Added
- Add `select` command to select threads that match search query
- Add support for mass copying/deleting/flagging/moving of messages
- IMAP: add support for COMPRESS=DEFLATE and others
Extension use can be configured with individual flags such as `use_deflate`
- Rename EXECUTE mode to COMMAND
- add async IMAP backend
- add in-app SMTP support
- ui: Show decoded source by default when viewing an Envelope's source
- ui: Add search in pagers
- Add managesieve REPL binary for managesieve script management
- imap: `add server_password_command`
- configuration: Add per-folder and per-account configuration overrides.
e.g. `accounts."imap.domain.tld".mailboxes."INBOX".index_style = "plain"`
The selection is done for a specific field as follows:
```text
if per-folder override is defined, return per-folder override
else if per-account override is defined, return per-account override
else return global setting field value.
```
- themes: Add Italics, Blink, Dim and Hidden text attributes
- ui: recognize readline shortcuts in Execute mode
- ui: hopefully smarter auto-completion in Execute mode
- demo NNTP python plugin
- ui: add `auto_choose_multipart_alternative`: Choose `text/html` alternative if `text/plain` is empty in `multipart/alternative` attachments.
- ui: custom date format strings
- ui: manual refresh for mailbox view
- ui: create mailbox command
- fs autocomplete
- ui: add support for [`NO_COLOR`](https://no-color.org/)
- enhanced, portable Makefile
- added Debian packaging
- added `default_header_values`: default header values used when creating a new draft
- ui: switch between sidebar and mailbox view with {left,right} keys for more intuitive navigation
- ui: add optional filter query for each mailbox view to view only the matching subset of messages (for example, you can hide all seen envelopes with `filter = "not flags:seen"`
### Changed
- Replace any use of 'folder' with 'mailbox' in user configuration
- Load libnotmuch dynamically
- Launch all user shell commands with `sh -c "..."`
### Fixed
- notmuch: add support for multiple accounts on same notmuch db
## [alpha-0.5.1] - 2020-02-09
### Added
- Added in-terminal floating notifications with history
- Added mailbox creation/deletion commands in IMAP accounts
- Added cli-docs compile time feature: Optionally build manpages to text with mandoc and print them from the command line.
- Added new theme keys
[unreleased]: #
[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

3033
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,97 +1,32 @@
[package]
name = "meli"
version = "0.7.2"
version = "0.3.2"
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
edition = "2018"
rust-version = "1.65.0"
license = "GPL-3.0-or-later"
readme = "README.md"
description = "terminal mail client"
homepage = "https://meli.delivery"
repository = "https://git.meli.delivery/meli/meli.git"
keywords = ["mail", "mua", "maildir", "terminal", "imap"]
categories = ["command-line-utilities", "email"]
default-run = "meli"
[[bin]]
name = "meli"
path = "src/main.rs"
[lib]
name = "meli"
path = "src/lib.rs"
[[bin]]
name = "managesieve-client"
path = "src/managesieve.rs"
required-features = ["melib/imap_backend"]
path = "src/bin.rs"
[dependencies]
async-task = "^4.2.0"
bitflags = "1.0"
crossbeam = { version = "^0.8" }
flate2 = { version = "1", optional = true }
futures = "0.3.5"
indexmap = { version = "^1.6", features = ["serde-1", ] }
libc = { version = "0.2.125", default-features = false, features = ["extra_traits",] }
linkify = { version = "^0.8", default-features = false }
melib = { path = "melib", version = "0.7.2" }
nix = { version = "^0.24", default-features = false }
notify = { version = "4.0.1", default-features = false } # >:c
num_cpus = "1.12.0"
pcre2 = { version = "0.2.3", optional = true }
serde = "1.0.71"
serde_derive = "1.0.71"
serde_json = "1.0"
signal-hook = { version = "^0.3", default-features = false }
signal-hook-registry = { version = "1.2.0", default-features = false }
smallvec = { version = "^1.5.0", features = ["serde", ] }
structopt = { version = "0.3.14", default-features = false }
svg_crate = { version = "^0.13", optional = true, package = "svg" }
termion = { version = "1.5.1", default-features = false }
toml = { version = "0.5.6", default-features = false, features = ["preserve_order", ] }
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 }
[build-dependencies]
flate2 = { version = "1", optional = true }
proc-macro2 = "1.0.37"
quote = "^1.0"
regex = "1"
syn = { version = "1", features = [] }
[dev-dependencies]
flate2 = { version = "1" }
regex = "1"
tempfile = "3.3"
crossbeam = "0.7.2"
signal-hook = "0.1.10"
nix = "*"
melib = { path = "melib", version = "*" }
ui = { path = "ui", version = "*" }
[profile.release]
lto = "fat"
codegen-units = 1
opt-level = "s"
lto = true
debug = false
strip = true
[workspace]
members = ["melib", "tools", ]
members = ["melib", "ui", "debug_printer", "testing", "text_processing"]
[features]
default = ["sqlite3", "notmuch", "regexp", "smtp", "dbus-notifications", "gpgme", "cli-docs"]
notmuch = ["melib/notmuch_backend", ]
jmap = ["melib/jmap_backend",]
sqlite3 = ["melib/sqlite3"]
smtp = ["melib/smtp"]
regexp = ["pcre2"]
dbus-notifications = ["notify-rust",]
cli-docs = ["flate2"]
svgscreenshot = ["svg_crate"]
gpgme = ["melib/gpgme"]
default = []
notmuch = ["melib/notmuch_backend", "ui/notmuch"]
# Print tracing logs as meli runs in stderr
# enable for debug tracing logs: build with --features=debug-tracing
debug-tracing = ["melib/debug-tracing", ]
debug-tracing = ["melib/debug-tracing", "ui/debug-tracing"]

195
Makefile
View File

@ -1,187 +1,30 @@
# meli - Makefile
#
# 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/>.
.POSIX:
.SUFFIXES:
CARGO_TARGET_DIR ?= target
MIN_RUSTC ?= 1.65.0
CARGO_BIN ?= cargo
CARGO_ARGS ?=
CARGO_SORT_BIN = cargo-sort
PRINTF = /usr/bin/printf
meli:
cargo build $(FEATURES)--release
# Options
PREFIX ?= /usr/local
EXPANDED_PREFIX := `cd ${PREFIX} && pwd -P`
BINDIR ?= ${EXPANDED_PREFIX}/bin
MANDIR ?= ${EXPANDED_PREFIX}/share/man
# Installation parameters
DOCS_SUBDIR ?= docs/
MANPAGES ?= meli.1 meli.conf.5 meli-themes.5
FEATURES ?= --features "${MELI_FEATURES}"
MANPATHS != ACCUM="";for m in `manpath 2> /dev/null | tr ':' ' '`; do if [ -d "$${m}" ]; then REAL_PATH=`cd $${m} && pwd` ACCUM="$${ACCUM}:$${REAL_PATH}";fi;done;echo -n $${ACCUM} | sed 's/^://'
VERSION != sed -n "s/^version\s*=\s*\"\(.*\)\"/\1/p" Cargo.toml
GIT_COMMIT != git show-ref -s --abbrev HEAD
DATE != date -I
# Output parameters
BOLD ?= `[ -z $${TERM} ] && echo "" || tput bold`
UNDERLINE ?= `[ -z $${TERM} ] && echo "" || tput smul`
ANSI_RESET ?= `[ -z $${TERM} ] && echo "" || tput sgr0`
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
meli: check-deps
@${CARGO_BIN} build ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --release --bin meli
.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:"
@echo " - ${BOLD}meli${ANSI_RESET} (builds meli with optimizations in \$$CARGO_TARGET_DIR)"
@echo " - ${BOLD}install${ANSI_RESET} (installs binary in \$$BINDIR and documentation to \$$MANDIR)"
@echo " - ${BOLD}uninstall${ANSI_RESET}"
@echo "Secondary subcommands:"
@echo " - ${BOLD}clean${ANSI_RESET} (cleans build artifacts)"
@echo " - ${BOLD}check-deps${ANSI_RESET} (checks dependencies)"
@echo " - ${BOLD}install-bin${ANSI_RESET} (installs binary to \$$BINDIR)"
@echo " - ${BOLD}install-doc${ANSI_RESET} (installs manpages to \$$MANDIR)"
@echo " - ${BOLD}help${ANSI_RESET} (prints this information)"
@echo " - ${BOLD}dist${ANSI_RESET} (creates release tarball named meli-"${VERSION}".tar.gz in this directory)"
@echo " - ${BOLD}deb-dist${ANSI_RESET} (builds debian package in the parent directory)"
@echo " - ${BOLD}distclean${ANSI_RESET} (cleans distribution build artifacts)"
@echo " - ${BOLD}build-rustdoc${ANSI_RESET} (builds rustdoc documentation for all packages in \$$CARGO_TARGET_DIR)"
@echo "\nENVIRONMENT variables of interest:"
@echo "* PREFIX = ${UNDERLINE}${EXPANDED_PREFIX}${ANSI_RESET}"
@echo -n "* MELI_FEATURES = ${UNDERLINE}"
@[ -z $${MELI_FEATURES+x} ] && echo -n "unset" || echo -n ${MELI_FEATURES}
@echo ${ANSI_RESET}
@echo "* BINDIR = ${UNDERLINE}${BINDIR}${ANSI_RESET}"
@echo "* MANDIR = ${UNDERLINE}${MANDIR}${ANSI_RESET}"
@echo -n "* MANPATH = ${UNDERLINE}"
@[ $${MANPATH+x} ] && echo -n $${MANPATH} || echo -n "unset"
@echo ${ANSI_RESET}
@echo "* (cleaned) output of manpath(1) = ${UNDERLINE}${MANPATHS}${ANSI_RESET}"
@echo -n "* NO_MAN ${UNDERLINE}"
@[ $${NO_MAN+x} ] && echo -n "set" || echo -n "unset"
@echo ${ANSI_RESET}
@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} check ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --tests --examples --benches --bins
.PHONY: fmt
fmt:
@$(CARGO_BIN) +nightly fmt --all || $(CARGO_BIN) fmt --all
@OUT=$$($(CARGO_SORT_BIN) -w 2>&1) || $(PRINTF) "WARN: %s cargo-sort failed or binary not found in PATH.\n" "$$OUT"
.PHONY: lint
lint:
@$(CARGO_BIN) clippy --no-deps --all-features --all --tests --examples --benches --bins
.PHONY: test
test:
@${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all --tests --examples --benches --bins
.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`" \
"\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)
PREFIX=/usr/local
ifdef MELI_FEATURES
FEATURES ?= --features="$(MELI_FEATURES)"
else
FEATURES ?=
endif
.PHONY: clean
clean:
-rm -rf ./${CARGO_TARGET_DIR}/
clean: rm -ri ./target/
.PHONY: distclean
distclean: clean
@rm -f meli-${VERSION}.tar.gz
.PHONY: uninstall
uninstall:
rm -f $(DESTDIR)${BINDIR}/meli
-rm $(DESTDIR)${MANDIR}/man1/meli.1.gz
-rm $(DESTDIR)${MANDIR}/man5/meli.conf.5.gz
-rm $(DESTDIR)${MANDIR}/man5/meli-themes.5.gz
.PHONY: install-doc
install-doc:
@(if [ -z $${NO_MAN+x} ]; then \
mkdir -p $(DESTDIR)${MANDIR}/man1 ; \
mkdir -p $(DESTDIR)${MANDIR}/man5 ; \
echo " - ${BOLD}Installing manpages to ${ANSI_RESET}${DESTDIR}${MANDIR}:" ; \
for MANPAGE in ${MANPAGES}; do \
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 ; \
(case ":${MANPATHS}:" in \
*:${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)
.PHONY: install-bin
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 "";; \
*) 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
uninstall: rm -f $(DESTDIR)$(PREFIX)/bin/meli
rm $(DESTDIR)$(PREFIX)/share/man/man1/meli.1.gz
rm $(DESTDIR)$(PREFIX)/share/man/man5/meli.conf.5.gz
.PHONY: install
.NOTPARALLEL: yes
install: meli install-bin install-doc
@(if [ -z $${NO_MAN+x} ]; then \
echo "\n You're ready to go. You might want to read the \"STARTING WITH meli\" section in the manpage (\`man meli\`)" ;\
fi)
@echo " - Report bugs in the mailing list or git issue tracker ${UNDERLINE}https://git.meli.delivery${ANSI_RESET}"
@echo " - If you have a specific feature or workflow you want to use, you can post in the mailing list or git issue tracker."
.PHONY: dist
dist:
@git archive --format=tar.gz --prefix=meli-${VERSION}/ HEAD >meli-${VERSION}.tar.gz
@echo meli-${VERSION}.tar.gz
.PHONY: deb-dist
deb-dist:
@dpkg-buildpackage -b -rfakeroot -us -uc
@echo ${BOLD}${GREEN}Generated${ANSI_RESET} ../meli_${VERSION}-1_amd64.deb
.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
install: meli
mkdir -p $(DESTDIR)$(PREFIX)/bin
mkdir -p $(DESTDIR)$(PREFIX)/share/man/man1
mkdir -p $(DESTDIR)$(PREFIX)/share/man/man5
cp -f target/release/meli $(DESTDIR)$(PREFIX)/bin
gzip < meli.1 > $(DESTDIR)$(PREFIX)/share/man/man1/meli.1.gz
gzip < meli.conf.5 > $(DESTDIR)$(PREFIX)/share/man/man5/meli.conf.5.gz

104
README 100644
View File

@ -0,0 +1,104 @@
__
__/ \__
/ \__/ \__ .
\__/ \__/ \ , _ , _ ___ │ '
/ \__ \__/ │' `│ `┒ .' ` │ │
\__/ \__/ \ │ │ │ |────' │ │
\__/ \__/ │ / `.___, /\__ /
\__/
,-.
\_/
terminal mail user agent {|||)<
/ \
`-'
DOCUMENTATION
=============
After installing meli, see meli(1) and meli.conf(5) for documentation.
BUILDING
========
meli requires rust 1.34 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
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
See meli(1) and meli.conf(5) for documentation.
You can build and run meli with one command:
# 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.
BUILDING IN DEBIAN
==================
Building with Debian's packaged cargo might require the installation of these
two packages: librust-openssl-sys-dev and librust-libdbus-sys-dev
BUILDING WITH NOTMUCH
=====================
To use the optional notmuch backend feature, you must have libnotmuch installed in your system. In Debian-like systems, install the "libnotmuch" package.
To build with notmuch support, prepend the environment variable "MELI_FEATURES='notmuch'" to your make invocation:
# MELI_FEATURES="notmuch" make
or if building directly with cargo, use the flag '--features="notmuch"'.
DEVELOPMENT
===========
Development builds can be built and/or run with
# cargo build
# 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`)
Code style follows the default rustfmt profile.
CONFIG
======
meli by default looks for a configuration file in this location:
# $XDG_CONFIG_HOME/meli/config
You can run meli with arbitrary configuration files by setting the MELI_CONFIG
environment variable to their locations, ie:
# MELI_CONFIG=./test_config cargo run
TESTING
=======
How to run specific tests:
# cargo test -p {melib, ui, meli} (-- --nocapture) (--test test_name)
PROFILING
=========
# perf record -g target/debug/bin
# perf script | stackcollapse-perf | rust-unmangle | flamegraph > perf.svg

137
README.md
View File

@ -1,137 +0,0 @@
# 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
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.
`meli` requires rust 1.65 and rust's package manager, Cargo. Information on how
to get it on your system can be found here: <https://doc.rust-lang.org/cargo/getting-started/installation.html>
With Cargo available, the project can be built with `make` and the resulting binary will then be found under `target/release/meli`. Run `make install` to install the binary and man pages. This requires root, so I suggest you override the default paths and install it in your `$HOME`: `make PREFIX=$HOME/.local install`.
You can build and run `meli` with one command: `cargo run --release`.
### Build 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)
- `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)
### Build Debian package (*deb*)
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
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
To build with JMAP support, prepend the environment variable `MELI_FEATURES='jmap'` to your make invocation:
```sh
MELI_FEATURES="jmap" make
```
or if building directly with cargo, use the flag `--features="jmap"'.
### HTML Rendering
HTML rendering is achieved using [w3m](https://github.com/tats/w3m) by default.
You can use the `pager.html_filter` setting to override this (for more details you can consult [`meli.conf(5)`](./docs/meli.conf.5)).
# Development
Development builds can be built and/or run with
```
cargo build
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`)
Code style follows the default rustfmt profile.
## Testing
How to run specific tests:
```sh
cargo test -p {melib, meli} (-- --nocapture) (--test test_name)
```
## Profiling
```sh
perf record -g target/debug/bin
perf script | stackcollapse-perf | rust-unmangle | flamegraph > perf.svg
```
## Running fuzz targets
Note: `cargo-fuzz` requires the nightly toolchain.
```sh
cargo +nightly fuzz run envelope_parse -- -dict=fuzz/envelope_tokens.dict
```

View File

@ -1,76 +0,0 @@
/*
* meli - build.rs
*
* 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/>.
*/
extern crate proc_macro;
extern crate quote;
extern crate syn;
mod config_macros;
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=src/conf/.rebuild.overrides.rs");
config_macros::override_derive(&[
("src/conf/pager.rs", "PagerSettings"),
("src/conf/listing.rs", "ListingSettings"),
("src/conf/notifications.rs", "NotificationsSettings"),
("src/conf/shortcuts.rs", "Shortcuts"),
("src/conf/composing.rs", "ComposingSettings"),
("src/conf/tags.rs", "TagsSettings"),
("src/conf/pgp.rs", "PGPSettings"),
]);
#[cfg(feature = "cli-docs")]
{
use flate2::{Compression, GzBuilder};
const MANDOC_OPTS: &[&str] = &["-T", "utf8", "-I", "os=Generated by mandoc(1)"];
use std::{env, fs::File, io::prelude::*, path::Path, process::Command};
let out_dir = env::var("OUT_DIR").unwrap();
let mut out_dir_path = Path::new(&out_dir).to_path_buf();
let mut cl = |filepath: &str, output: &str| {
out_dir_path.push(output);
let output = Command::new("mandoc")
.args(MANDOC_OPTS)
.arg(filepath)
.output()
.or_else(|_| Command::new("man").arg("-l").arg(filepath).output())
.expect(
"could not execute `mandoc` or `man`. If the binaries are not available in \
the PATH, disable `cli-docs` feature to be able to continue compilation.",
);
let file = File::create(&out_dir_path).unwrap_or_else(|err| {
panic!("Could not create file {}: {}", out_dir_path.display(), err)
});
let mut gz = GzBuilder::new()
.comment(output.stdout.len().to_string().into_bytes())
.write(file, Compression::default());
gz.write_all(&output.stdout).unwrap();
gz.finish().unwrap();
out_dir_path.pop();
};
cl("docs/meli.1", "meli.txt.gz");
cl("docs/meli.conf.5", "meli.conf.txt.gz");
cl("docs/meli-themes.5", "meli-themes.txt.gz");
cl("docs/meli.7", "meli.7.txt.gz");
}
}

View File

@ -1,245 +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 std::{
fs::File,
io::prelude::*,
process::{Command, Stdio},
};
use quote::{format_ident, quote};
use regex::Regex;
// Write ConfigStructOverride to overrides.rs
pub fn override_derive(filenames: &[(&str, &str)]) {
let mut output_file =
File::create("src/conf/overrides.rs").expect("Unable to open output file");
let mut output_string = r##"// @generated
/*
* meli - conf/overrides.rs
*
* 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/>.
*/
#![allow(clippy::derivable_impls)]
//! This module is automatically generated by config_macros.rs.
use super::*;
use melib::HeaderName;
"##
.to_string();
let cfg_attr_default_attr_regex = Regex::new(r"\s*default\s*[,]").unwrap();
let cfg_attr_default_val_attr_regex = Regex::new(r#"\s*default\s*=\s*"[^"]*"\s*,\s*"#).unwrap();
let cfg_attr_feature_regex = Regex::new(r"[(](?:not[(]\s*)?feature").unwrap();
'file_loop: for (filename, ident) in filenames {
println!("cargo:rerun-if-changed={}", filename);
let mut file = File::open(filename)
.unwrap_or_else(|err| panic!("Unable to open file `{}` {}", filename, err));
let mut src = String::new();
file.read_to_string(&mut src).expect("Unable to read file");
let syntax = syn::parse_file(&src).expect("Unable to parse file");
if syntax.items.iter().any(|item| {
if let syn::Item::Struct(s) = item {
if s.ident.to_string().ends_with("Override") {
println!("ident {} exists, skipping {}", ident, filename);
return true;
}
}
false
}) {
continue 'file_loop;
}
for item in syntax.items.iter() {
if let syn::Item::Struct(s) = item {
if s.ident != ident {
continue;
}
if s.ident.to_string().ends_with("Override") {
unreachable!();
}
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;
let ty = &f.ty;
let attrs = f
.attrs
.iter()
.filter_map(|f| {
let mut new_attr = f.clone();
if let proc_macro2::TokenTree::Group(g) =
f.tokens.clone().into_iter().next().unwrap()
{
let mut attr_inner_value = f.tokens.to_string();
if cfg_attr_feature_regex.is_match(&attr_inner_value) {
attr_inner_value = cfg_attr_default_val_attr_regex
.replace_all(&attr_inner_value, "")
.to_string();
if attr_inner_value.contains("default") {
attr_inner_value = cfg_attr_default_attr_regex
.replace_all(&attr_inner_value, "")
.to_string();
}
let new_toks: proc_macro2::TokenStream =
attr_inner_value.parse().unwrap();
new_attr.tokens = quote! { #new_toks };
}
if !attr_inner_value.starts_with("( default")
&& !attr_inner_value.starts_with("( default =")
&& !attr_inner_value.starts_with("(default")
&& !attr_inner_value.starts_with("(default =")
{
return Some(new_attr);
}
if attr_inner_value.starts_with("( default =")
|| attr_inner_value.starts_with("(default =")
{
let rest = g.stream().into_iter().skip(4);
new_attr.tokens = quote! { ( #(#rest)*) };
match new_attr.tokens.to_string().as_str() {
"( )" | "()" => {
return None;
}
_ => {}
}
} else if attr_inner_value.starts_with("( default")
|| attr_inner_value.starts_with("(default")
{
let rest = g.stream().into_iter().skip(2);
new_attr.tokens = quote! { ( #(#rest)*) };
match new_attr.tokens.to_string().as_str() {
"( )" | "()" => {
return None;
}
_ => {}
}
}
}
Some(new_attr)
})
.collect::<Vec<_>>();
let t = quote! {
#(#attrs)*
#[serde(default)]
pub #ident : Option<#ty>
};
if !field_idents.contains(&ident) {
field_idents.push(ident);
}
field_tokentrees.push(t);
}
//let fields = &s.fields;
let literal_struct = quote! {
#(#attrs_tokens)*
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct #override_ident {
#(#field_tokentrees),*
}
#(#attrs_tokens)*
impl Default for #override_ident {
fn default() -> Self {
#override_ident {
#(#field_idents: None),*
}
}
}
};
output_string.push_str(&literal_struct.to_string());
output_string.push_str("\n\n");
}
}
}
let rustfmt_closure = move |output_file: &mut File, output_string: &str| {
let mut rustfmt = Command::new("rustfmt")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|err| format!("failed to execute rustfmt {}", err))?;
{
// limited borrow of stdin
let stdin = rustfmt
.stdin
.as_mut()
.ok_or("failed to get rustfmt stdin")?;
stdin
.write_all(output_string.as_bytes())
.map_err(|err| format!("failed to write to rustfmt stdin {}", err))?;
}
let output = rustfmt
.wait_with_output()
.map_err(|err| format!("failed to wait on rustfmt child {}", err))?;
if !output.stderr.is_empty() {
return Err(format!(
"rustfmt invocation replied with: `{}`",
String::from_utf8_lossy(&output.stderr)
));
}
output_file
.write_all(&output.stdout)
.expect("failed to write to src/conf/overrides.rs");
Ok(())
};
if let Err(err) = rustfmt_closure(&mut output_file, &output_string) {
println!("Tried rustfmt on overrides module, got error: {}", err);
output_file.write_all(output_string.as_bytes()).unwrap();
}
}

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)

86
debian/changelog vendored
View File

@ -1,86 +0,0 @@
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
* added server extension support and use in account status tab
* imap: fixed IDLE connection getting stuck when using DEFLATE
-- Manos Pitsidianakis <epilys@nessuent.xyz> Sun, 02 Aug 2020 01:09:05 +0200
meli (0.6.0-1) buster; urgency=low
* Update to 0.6.0
-- Manos Pitsidianakis <epilys@nessuent.xyz> Wed, 29 Jul 2020 22:24:08 +0200
meli (0.5.1-1) buster; urgency=low
* Update to 0.5.1
-- Manos Pitsidianakis <epilys@nessuent.xyz> Wed, 29 Jan 2020 22:24:08 +0200
meli (0.5.0-1) buster; urgency=low
* Update to 0.5.0
-- Manos Pitsidianakis <epilys@nessuent.xyz> Wed, 29 Jan 2020 22:24:08 +0200
meli (0.4.1-1) buster; urgency=low
* Initial release.
-- Manos Pitsidianakis <epilys@nessuent.xyz> Wed, 29 Jan 2020 22:24:08 +0200

1
debian/compat vendored
View File

@ -1 +0,0 @@
11

14
debian/control vendored
View File

@ -1,14 +0,0 @@
Source: meli
Section: mail
Priority: optional
Maintainer: Manos Pitsidianakis <epilys@nessuent.xyz>
Build-Depends: debhelper (>=11~), mandoc (>=1.14.4-1), quilt, libsqlite3-dev
Standards-Version: 4.1.4
Homepage: https://meli.delivery
Package: meli
Architecture: any
Multi-Arch: foreign
Depends: ${misc:Depends}, ${shlibs:Depends}
Recommends: libnotmuch, xdg-utils (>=1.1.3-1)
Description: terminal mail client

685
debian/copyright vendored
View File

@ -1,685 +0,0 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: meli
Source: <https://git.meli.delivery/meli/meli>
#
# Please double check copyright with the licensecheck(1) command.
Files: *
Copyright: 2017-2020 Manos Pitsidianakis
License: GPL-3.0+
#----------------------------------------------------------------------------
# License file: COPYING
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
.
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
.
Preamble
.
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
.
The precise terms and conditions for copying, distribution and
modification follow.
.
TERMS AND CONDITIONS
.
0. Definitions.
.
"This License" refers to version 3 of the GNU General Public License.
.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
.
A "covered work" means either the unmodified Program or a work based
on the Program.
.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
.
1. Source Code.
.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
.
The Corresponding Source for a work in source code form is that
same work.
.
2. Basic Permissions.
.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
.
4. Conveying Verbatim Copies.
.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
.
5. Conveying Modified Source Versions.
.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
.
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
.
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
.
6. Conveying Non-Source Forms.
.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
.
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
.
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
.
7. Additional Terms.
.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
.
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
.
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
.
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
.
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
.
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
.
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
.
8. Termination.
.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
.
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
.
9. Acceptance Not Required for Having Copies.
.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
.
10. Automatic Licensing of Downstream Recipients.
.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
.
11. Patents.
.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
.
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
.
12. No Surrender of Others' Freedom.
.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
.
13. Use with the GNU Affero General Public License.
.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
.
14. Revised Versions of this License.
.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
.
15. Disclaimer of Warranty.
.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
.
16. Limitation of Liability.
.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
.
17. Interpretation of Sections 15 and 16.
.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
.
END OF TERMS AND CONDITIONS
.
How to Apply These Terms to Your New Programs
.
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
.
This program 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.
.
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
.
Also add information on how to contact you by electronic and paper mail.
.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
.
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

3
debian/meli.docs vendored
View File

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

View File

@ -1,16 +0,0 @@
Description: Fix PREFIX env var in Makefile for use in Debian
Author: Manos Pitsidianakis <epilys@nessuent.xyz>
Last-Update: 2023-03-06
Index: meli/Makefile
===================================================================
--- meli.orig/Makefile
+++ meli/Makefile
@@ -20,7 +20,7 @@
.SUFFIXES:
# Options
-PREFIX ?= /usr/local
+PREFIX ?= /usr
EXPANDED_PREFIX := `cd ${PREFIX} && pwd -P`
BINDIR ?= ${EXPANDED_PREFIX}/bin
MANDIR ?= ${EXPANDED_PREFIX}/share/man

View File

@ -1 +0,0 @@
fix-prefix-for-debian.patch

16
debian/rules vendored
View File

@ -1,16 +0,0 @@
#!/usr/bin/make -f
# You must remove unused comment lines for the released package.
#export DH_VERBOSE = 1
#export DEB_BUILD_MAINT_OPTIONS = hardening=+all
#export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic
#export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
export MELI_FEATURES = cli-docs sqlite3
%:
dh $@ --with quilt
#override_dh_auto_install:
# dh_auto_install -- prefix=/usr
#override_dh_install:
# dh_install --list-missing -X.pyc -X.pyo

View File

@ -1 +0,0 @@
3.0 (quilt)

View File

@ -1,2 +0,0 @@
#abort-on-upstream-changes
unapply-patches

View File

@ -0,0 +1,17 @@
[package]
name = "debug_printer"
version = "0.0.1" #:version
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
workspace = ".."
edition = "2018"
[lib]
name = "debugprinter"
crate-type = ["dylib"]
path = "src/lib.rs"
[dependencies]
libc = {version = "0.2.55", features = ["extra_traits",] }
melib = { path = "../melib", version = "*" }
ui = { path = "../ui", version = "*" }

View File

@ -0,0 +1,44 @@
extern crate libc;
extern crate melib;
use melib::Envelope;
use std::ffi::CString;
use std::os::raw::c_char;
#[no_mangle]
pub extern "C" fn print_envelope(ptr: *const Envelope) -> *const c_char {
unsafe {
assert!(!ptr.is_null(), "Null pointer in print_envelope");
//println!("got addr {}", p as u64);
//unsafe { CString::new("blah".to_string()).unwrap().as_ptr() }
let s = CString::new(format!("{:?}", *ptr)).unwrap();
drop(ptr);
let p = s.as_ptr();
std::mem::forget(s);
p
}
}
#[no_mangle]
pub extern "C" fn get_empty_envelope() -> *mut Envelope {
let mut ret = Envelope::default();
let ptr = std::ptr::NonNull::new(&mut ret as *mut Envelope)
.expect("Envelope::default() has a NULL pointer?");
let ptr = ptr.as_ptr();
std::mem::forget(ret);
ptr
}
#[no_mangle]
pub extern "C" fn destroy_cstring(ptr: *mut c_char) {
unsafe {
let slice = CString::from_raw(ptr);
drop(slice);
}
}
#[no_mangle]
pub extern "C" fn envelope_size() -> libc::size_t {
std::mem::size_of::<Envelope>()
}

View File

@ -1,122 +0,0 @@
# Using other apps with `meli`
## Sending mail with a command line tool
`composing.send_mail` can use either settings for an SMTP server or a shell
command to which it pipes new mail to.
### `msmtp` and `send_mail`
[`msmtp`][msmtp] is a command line SMTP client that can be configured to work
with many SMTP servers. It supports queuing and other small useful features.
See [the documentation](https://marlam.de/msmtp/msmtp.html).
```toml
[composing]
send_mail = 'msmtp --logfile=/home/user/.mail/msmtp.log --read-recipients
--read-envelope-from'
```
[msmtp]: https://marlam.de/msmtp/
## Editor
Any editor you specify in `composing.editor_cmd` will be invoked with the
e-mail draft file path appended as an argument to it. For example, if your
setting is `editor_cmd = 'nano'`, `meli` will execute `nano /tmp/meli/...`.
### Configuration
#### `vim` / `neovim` command
The following command setting in your `meli` configuration file makes editing
start at the first empty line, that is, after the e-mail headers. This allows
you to start writing the e-mail body right away after opening the editor from
`meli`.
```toml
[composing]
editor_cmd = '~/.local/bin/vim +/^$'
```
In `vim`, the `+` argument positions the cursor at the first file argument. `/`
specifies a pattern position instead of a line number. `^` specifies the start
of a line, and `$` the end of the line. The pattern altogether matches an empty
line, which will be after the e-mail headers.
### Composing with `format=flowed`
`format=flowed` is a proposed IETF standard[^formatflowed] that lets you
preserve the structure of paragraphs by disambiguating a *hard* and a *soft*
line break. A line break that is preceded by a space character is *soft* and
does not terminate the paragraph, while a line break without a space is a
*hard* one and creates a new paragraph. This allows text to be re-flowed in
e-mail clients at different display widths and font sizes without messing up
the author's formatting.
#### `vim` / `neovim` and `format=flowed`
Create a `mail.vim` file type plugin in:
- `$HOME/.vim/after/ftplugin/mail.vim` for vim
- `$HOME/.config/nvim/after/ftplugin/mail.vim` for neovim
```vim
setlocal nomodeline
setlocal textwidth=72
setlocal formatoptions=aqtw2r
setlocal nojoinspaces
setlocal nosmartindent
setlocal comments+=nb:>
match ErrorMsg '\s\+$'
```
Also, don't forget that you can easily quote stuff with `MailQuote`.
From `:help ft-mail-plugin`:
> Local mappings:
> `<LocalLeader>q` or `\\MailQuote`
> Quotes the text selected in Visual mode, or from the cursor position
> to the end of the file in Normal mode.
> This means "> " is inserted in each line.
See the accompanying [`mail.vim`](./mail.vim) for comments for each setting.
## `xbiff`
[`xbiff(1)`][xbiff] manual page says:[^xbiffmanpage]
> The `xbiff` program displays a little image of a mailbox. When there is no
> mail, the flag on the mailbox is down. When mail arrives, the flag goes up
> and the mailbox beeps.
This tool is very outdated, but some users might still have use for it.
Therefore `meli` provides support (also, it's easy to support this feature).
Specify a file path in `notifications.xbiff_file_path` and `meli` will write to
it when new mail arrives. This file can the be used as input to `xbiff`.
```toml
[notifications]
xbiff_file_path = "/tmp/xbiff"
```
[xbiff]: https://en.wikipedia.org/wiki/Xbiff
[^xbiffmanpage]: https://www.x.org/releases/X11R7.0/doc/html/xbiff.1.html
## Viewing HTML e-mail
By default `meli` tries to render HTML e-mail with `w3m`. You can override this
by setting the `pager.html_filter` setting. The default setting corresponds to:
```toml
[pager]
html_filter = "w3m -I utf-8 -T text/html"
```
The HTML of the e-mail is piped into `html_filter`'s standard input.
## Externally refreshing e-mail accounts
If your account's syncing is handled by an external tool, you can use the
refresh shortcuts within `meli` to call this tool with
`accounts.refresh_command`.

View File

@ -1,87 +0,0 @@
" Place this plugin in
"
" `$HOME/.vim/after/ftplugin/mail.vim` for vim
" `$HOME/.config/nvim/after/ftplugin/mail.vim` for neovim
" Don't use modelines in e-mail messages
setlocal nomodeline
setlocal textwidth=72
" *fo-a*
" a Automatic formatting of paragraphs.
" Every time text is inserted or deleted the paragraph will be reformatted.
" *fo-w*
" w Trailing white space indicates a paragraph continues in the next line.
" A line that ends in a non-white character ends a paragraph.
" *fo-q*
" q Allow formatting of comments with "gq".
" *fo-t*
" t Auto-wrap text using textwidth
" *fo-r*
" r Automatically insert the current comment leader after hitting <Enter> in
" Insert mode.
" *fo-c*
" c Auto-wrap comments using textwidth, inserting the current comment leader
" automatically.
" *fo-2*
" 2 When formatting text, use the indent of the second line of a paragraph for
" the rest of the paragraph, instead of the indent of the first line.
" This supports paragraphs in which the first line has a different indent than
" the rest.
" Note that 'autoindent' must be set too.
" Example:
" first line of a paragraph
" second line of the same paragraph
" third line.
" This also works inside comments, ignoring the comment leader.
setlocal formatoptions=aqtw2r
" Disable adding two spaces after '.', '?' and '!' with a join command.
setlocal nojoinspaces
" Disable smartident (meant for source code)
setlocal nosmartindent
" *'comments'* *'com'* *E524* *E525*
" A comma-separated list of strings that can start a comment line.
" See |format-comments|.
" See |option-backslash| about using backslashes to insert a space.
"
"
" The 'comments' option is a comma-separated list of parts.
" Each part defines a type of comment string.
" A part consists of: {flags}:{string}
"
" {string} is the literal text that must appear.
"
" {flags}:
" n Nested comment.
" Nesting with mixed parts is allowed.
" If 'comments' is "n:),n:>" a line starting with "> ) >" is a comment.
"
" b Blank (<Space>, <Tab> or <EOL>) required after {string}.
setlocal comments+=nb:>
" Highlight trailing whitespace as errors.
match ErrorMsg '\s\+$'
" MAIL *mail.vim* *ft-mail.vim*
" By default mail.vim synchronises syntax to 100 lines before the first
" displayed line.
" If you have a slow machine, and generally deal with emails with short
" headers, you can change this to a smaller value:
let mail_minlines = 30
" *no_mail_maps* *g:no_mail_maps*
" Disable defining mappings for a specific filetype by setting a variable,
" which contains the name of the filetype.
" For the "mail" filetype this would be:
let no_mail_maps = 1
" Local mappings:
" <LocalLeader>q or \\MailQuote
" Quotes the text selected in Visual mode, or from the cursor position
" to the end of the file in Normal mode.
" This means "> " is inserted in each line.

View File

@ -1,625 +0,0 @@
.\" meli - meli-themes.5
.\"
.\" 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/>.
.\"
.Dd November 11, 2022
.Dt MELI-THEMES 5
.Os
.Sh NAME
.Nm meli-themes
.Nd themes for the
.Xr meli 1
terminal e-mail client
.Sh SYNOPSIS
.Nm meli
comes with two themes,
.Ic dark
(default) and
.Ic light .
.sp
Custom themes are defined as lists of key-values in the configuration files:
.Bl -bullet -compact
.It
.Pa $XDG_CONFIG_HOME/meli/config.toml
.It
.Pa $XDG_CONFIG_HOME/meli/themes/*.toml
.El
.sp
The application theme is defined in the configuration as follows:
.Bd -literal
[terminal]
theme = "dark"
.Ed
.Sh DESCRIPTION
Themes for
.Nm meli
are described in the configuration language TOML, as they are key-value tables defined in the TERMINAL section of the configuration file.
Each key defines the semantic location of the theme attribute within the application.
For example,
.Ic mail.listing.compact.*
keys are settings for the
.Ic compact
mail listing style.
A setting contains three fields: fg for foreground color, bg for background color, and attrs for text attribute.
.sp
.Dl \&"widget.key.label\&" = { fg = \&"Default\&", bg = \&"Default\&", attrs = \&"Default\&" }
.sp
Each field contains a value, which may be either a color/attribute, a link (key name) or a valid alias.
An alias is a string starting with the \&"\&$\&" character and must be declared in advance in the
.Ic color_aliases
or
.Ic attr_aliases
fields of a theme.
An alias' value can be any valid value, including links and other aliases, as long as they are valid.
In the case of a link the setting's real value depends on the value of the referred key.
This allows for defaults within a group of associated values.
Cyclic references in a theme results in an error:
.sp
.Dl spooky theme contains a cycle: fg: mail.listing.compact.even -> mail.listing.compact.highlighted -> mail.listing.compact.odd -> mail.listing.compact.even
.Pp
Two themes are included by default, `light` and `dark`.
.Sh EXAMPLES
Specific settings from already defined themes can be overwritten:
.Bd -literal
[terminal]
theme = "dark"
.sp
[terminal.themes.dark]
"mail.sidebar_highlighted_account" = { bg = "#ff4529" }
"mail.listing.attachment_flag" = { fg = "#ff4529" }
"mail.view.headers" = { fg = "30" }
"mail.view.body" = {fg = "HotPink3", bg = "LightSalmon1"}
# Linked value keys can be whatever key:
"mail.listing.compact.even_unseen" = { bg = "mail.sidebar_highlighted_account" }
# Linked color value keys can optionally refer to another field:
"mail.listing.compact.odd_unseen" = { bg = "mail.sidebar_highlighted_account.fg" }
.sp
# define new theme. Undefined settings will inherit from the default "dark" theme.
[terminal.themes."hunter2"]
color_aliases= { "Jebediah" = "#b4da55" }
"mail.listing.tag_default" = { fg = "$Jebediah" }
"mail.view.headers" = { fg = "White", bg = "Black" }
.Ed
.Sh CUSTOM THEMES
Custom themes can be included in your configuration files or be saved independently in your
.Pa $XDG_CONFIG_HOME/meli/themes/
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
.sp
.Pa new_theme.toml
will now include all keys and values of the "dark" theme.
.sp
.Dl meli print-loaded-themes
.sp
will print all loaded themes with the links resolved.
.Sh VALID ATTRIBUTE VALUES
Case-sensitive.
.Bl -bullet -compact
.It
"Default"
.It
"Bold"
.It
"Dim"
.It
"Italics"
.It
"Underline"
.It
"Blink"
.It
"Reverse"
.It
"Hidden"
.It
Any combo of the above separated by a bitwise XOR "\&|" eg "Dim | Italics"
.El
.Sh VALID COLOR VALUES
Color values are of type String with the following valid contents:
.Bl -bullet -compact
.It
"Default" is the terminal default. (Case-sensitive)
.It
Hex triplet e.g. #FFFFFF for RGB colors.
Three character shorthand is also valid, e.g. #09c → #0099cc (Case-insensitive)
.It
0-255 byte for 256 colors.
.It
.Xr xterm 1
name but with some modifications (for a full table see COLOR NAMES addendum) (Case-sensitive)
.El
.Sh NO COLOR
To completely disable ANSI colors, there are two options:
.Bl -bullet -compact
.It
Set the
.Ic use_color
option (section
.Ic terminal Ns
) to false, which is true by default.
.It
The
.Ev NO_COLOR
environmental variable, when present (regardless of its value), prevents the addition of ANSI color.
When the configuration value
.Ic use_color
is explicitly set to true by the user,
.Ev NO_COLOR
is ignored.
.El
.sp
In this mode, cursor locations (i.e., currently selected entries/items) will use the "reverse video" ANSI attribute to invert the terminal's default foreground/background colors.
.Sh VALID KEYS
.Bl -bullet -compact
.It
theme_default
.It
error_message
.It
highlight
.It
status.bar
.It
status.command_bar
.It
status.history
.It
status.history.hints
.It
status.notification
.It
tab.focused
.It
tab.unfocused
.It
tab.bar
.It
widgets.list.header
.It
widgets.form.label
.It
widgets.form.field
.It
widgets.form.highlighted
.It
widgets.options.highlighted
.It
mail.sidebar
.It
mail.sidebar_divider
.It
mail.sidebar_unread_count
.It
mail.sidebar_index
.It
mail.sidebar_highlighted
.It
mail.sidebar_highlighted_unread_count
.It
mail.sidebar_highlighted_index
.It
mail.sidebar_highlighted_account
.It
mail.sidebar_highlighted_account_unread_count
.It
mail.sidebar_highlighted_account_index
.It
mail.listing.compact.even
.It
mail.listing.compact.odd
.It
mail.listing.compact.even_unseen
.It
mail.listing.compact.odd_unseen
.It
mail.listing.compact.even_selected
.It
mail.listing.compact.odd_selected
.It
mail.listing.compact.even_highlighted
.It
mail.listing.compact.odd_highlighted
.It
mail.listing.plain.even
.It
mail.listing.plain.odd
.It
mail.listing.plain.even_unseen
.It
mail.listing.plain.odd_unseen
.It
mail.listing.plain.even_selected
.It
mail.listing.plain.odd_selected
.It
mail.listing.plain.even_highlighted
.It
mail.listing.plain.odd_highlighted
.It
mail.listing.conversations
.It
mail.listing.conversations.subject
.It
mail.listing.conversations.from
.It
mail.listing.conversations.date
.It
mail.listing.conversations.unseen
.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
.It
mail.view.thread.indentation.b
.It
mail.view.thread.indentation.c
.It
mail.view.thread.indentation.d
.It
mail.view.thread.indentation.e
.It
mail.view.thread.indentation.f
.It
mail.listing.attachment_flag
.It
mail.listing.thread_snooze_flag
.It
mail.listing.tag_default
.It
pager.highlight_search
.It
pager.highlight_search_current
.El
.Sh COLOR NAMES
.TS
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
Aqua:14:_:Black:0
Aquamarine1:122:_:Maroon:1
Aquamarine2:86:_:Green:2
Aquamarine3:79:_:Olive:3
Black:0:_:Navy:4
Blue:12:_:Purple1:5
Blue1:21:_:Teal:6
Blue2:19:_:Silver:7
Blue3:20:_:Grey:8
BlueViolet:57:_:Red:9
CadetBlue:72:_:Lime:10
CadetBlue1:73:_:Yellow:11
Chartreuse1:118:_:Blue:12
Chartreuse2:112:_:Fuchsia:13
Chartreuse3:82:_:Aqua:14
Chartreuse4:70:_:White:15
Chartreuse5:76:_:Grey0:16
Chartreuse6:64:_:NavyBlue:17
CornflowerBlue:69:_:DarkBlue:18
Cornsilk1:230:_:Blue2:19
Cyan1:51:_:Blue3:20
Cyan2:50:_:Blue1:21
Cyan3:43:_:DarkGreen:22
DarkBlue:18:_:DeepSkyBlue5:23
DarkCyan:36:_:DeepSkyBlue6:24
DarkGoldenrod:136:_:DeepSkyBlue7:25
DarkGreen:22:_:DodgerBlue3:26
DarkKhaki:143:_:DodgerBlue2:27
DarkMagenta:90:_:Green4:28
DarkMagenta1:91:_:SpringGreen6:29
.TE
.TS
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
DarkOliveGreen1:192:_:Turquoise4:30
DarkOliveGreen2:155:_:DeepSkyBlue3:31
DarkOliveGreen3:191:_:DeepSkyBlue4:32
DarkOliveGreen4:107:_:DodgerBlue1:33
DarkOliveGreen5:113:_:Green2:34
DarkOliveGreen6:149:_:SpringGreen4:35
DarkOrange:208:_:DarkCyan:36
DarkOrange2:130:_:LightSeaGreen:37
DarkOrange3:166:_:DeepSkyBlue2:38
DarkRed:52:_:DeepSkyBlue1:39
DarkRed2:88:_:Green3:40
DarkSeaGreen:108:_:SpringGreen5:41
DarkSeaGreen1:158:_:SpringGreen2:42
DarkSeaGreen2:193:_:Cyan3:43
DarkSeaGreen3:151:_:DarkTurquoise:44
DarkSeaGreen4:157:_:Turquoise2:45
DarkSeaGreen5:115:_:Green1:46
DarkSeaGreen6:150:_:SpringGreen3:47
DarkSeaGreen7:65:_:SpringGreen1:48
DarkSeaGreen8:71:_:MediumSpringGreen:49
DarkSlateGray1:123:_:Cyan2:50
DarkSlateGray2:87:_:Cyan1:51
DarkSlateGray3:116:_:DarkRed:52
DarkTurquoise:44:_:DeepPink8:53
DarkViolet:128:_:Purple4:54
DarkViolet1:92:_:Purple5:55
DeepPink1:199:_:Purple3:56
DeepPink2:197:_:BlueViolet:57
DeepPink3:198:_:Orange3:58
DeepPink4:125:_:Grey37:59
.TE
.TS
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
DeepPink6:162:_:MediumPurple6:60
DeepPink7:89:_:SlateBlue2:61
DeepPink8:53:_:SlateBlue3:62
DeepPink9:161:_:RoyalBlue1:63
DeepSkyBlue1:39:_:Chartreuse6:64
DeepSkyBlue2:38:_:DarkSeaGreen7:65
DeepSkyBlue3:31:_:PaleTurquoise4:66
DeepSkyBlue4:32:_:SteelBlue:67
DeepSkyBlue5:23:_:SteelBlue3:68
DeepSkyBlue6:24:_:CornflowerBlue:69
DeepSkyBlue7:25:_:Chartreuse4:70
DodgerBlue1:33:_:DarkSeaGreen8:71
DodgerBlue2:27:_:CadetBlue:72
DodgerBlue3:26:_:CadetBlue1:73
Fuchsia:13:_:SkyBlue3:74
Gold1:220:_:SteelBlue1:75
Gold2:142:_:Chartreuse5:76
Gold3:178:_:PaleGreen4:77
Green:2:_:SeaGreen4:78
Green1:46:_:Aquamarine3:79
Green2:34:_:MediumTurquoise:80
Green3:40:_:SteelBlue2:81
Green4:28:_:Chartreuse3:82
GreenYellow:154:_:SeaGreen3:83
Grey:8:_:SeaGreen1:84
Grey0:16:_:SeaGreen2:85
Grey100:231:_:Aquamarine2:86
Grey11:234:_:DarkSlateGray2:87
Grey15:235:_:DarkRed2:88
Grey19:236:_:DeepPink7:89
.TE
.TS
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
Grey23:237:_:DarkMagenta:90
Grey27:238:_:DarkMagenta1:91
Grey3:232:_:DarkViolet1:92
Grey30:239:_:Purple2:93
Grey35:240:_:Orange4:94
Grey37:59:_:LightPink3:95
Grey39:241:_:Plum4:96
Grey42:242:_:MediumPurple4:97
Grey46:243:_:MediumPurple5:98
Grey50:244:_:SlateBlue1:99
Grey53:102:_:Yellow4:100
Grey54:245:_:Wheat4:101
Grey58:246:_:Grey53:102
Grey62:247:_:LightSlateGrey:103
Grey63:139:_:MediumPurple:104
Grey66:248:_:LightSlateBlue:105
Grey69:145:_:Yellow5:106
Grey7:233:_:DarkOliveGreen4:107
Grey70:249:_:DarkSeaGreen:108
Grey74:250:_:LightSkyBlue2:109
Grey78:251:_:LightSkyBlue3:110
Grey82:252:_:SkyBlue2:111
Grey84:188:_:Chartreuse2:112
Grey85:253:_:DarkOliveGreen5:113
Grey89:254:_:PaleGreen3:114
Grey93:255:_:DarkSeaGreen5:115
Honeydew2:194:_:DarkSlateGray3:116
HotPink:205:_:SkyBlue1:117
HotPink1:206:_:Chartreuse1:118
HotPink2:169:_:LightGreen:119
.TE
.TS
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
HotPink3:132:_:LightGreen1:120
HotPink4:168:_:PaleGreen1:121
IndianRed:131:_:Aquamarine1:122
IndianRed1:167:_:DarkSlateGray1:123
IndianRed2:204:_:Red2:124
IndianRed3:203:_:DeepPink4:125
Khaki1:228:_:MediumVioletRed:126
Khaki3:185:_:Magenta4:127
LightCoral:210:_:DarkViolet:128
LightCyan2:195:_:Purple:129
LightCyan3:152:_:DarkOrange2:130
LightGoldenrod1:227:_:IndianRed:131
LightGoldenrod2:222:_:HotPink3:132
LightGoldenrod3:179:_:MediumOrchid3:133
LightGoldenrod4:221:_:MediumOrchid:134
LightGoldenrod5:186:_:MediumPurple2:135
LightGreen:119:_:DarkGoldenrod:136
LightGreen1:120:_:LightSalmon2:137
LightPink1:217:_:RosyBrown:138
LightPink2:174:_:Grey63:139
LightPink3:95:_:MediumPurple3:140
LightSalmon1:216:_:MediumPurple1:141
LightSalmon2:137:_:Gold2:142
LightSalmon3:173:_:DarkKhaki:143
LightSeaGreen:37:_:NavajoWhite3:144
LightSkyBlue1:153:_:Grey69:145
LightSkyBlue2:109:_:LightSteelBlue3:146
LightSkyBlue3:110:_:LightSteelBlue:147
LightSlateBlue:105:_:Yellow6:148
LightSlateGrey:103:_:DarkOliveGreen6:149
.TE
.TS
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
LightSteelBlue:147:_:DarkSeaGreen6:150
LightSteelBlue1:189:_:DarkSeaGreen3:151
LightSteelBlue3:146:_:LightCyan3:152
LightYellow3:187:_:LightSkyBlue1:153
Lime:10:_:GreenYellow:154
Magenta1:201:_:DarkOliveGreen2:155
Magenta2:165:_:PaleGreen2:156
Magenta3:200:_:DarkSeaGreen4:157
Magenta4:127:_:DarkSeaGreen1:158
Magenta5:163:_:PaleTurquoise1:159
Magenta6:164:_:Red3:160
Maroon:1:_:DeepPink9:161
MediumOrchid:134:_:DeepPink6:162
MediumOrchid1:171:_:Magenta5:163
MediumOrchid2:207:_:Magenta6:164
MediumOrchid3:133:_:Magenta2:165
MediumPurple:104:_:DarkOrange3:166
MediumPurple1:141:_:IndianRed1:167
MediumPurple2:135:_:HotPink4:168
MediumPurple3:140:_:HotPink2:169
MediumPurple4:97:_:Orchid:170
MediumPurple5:98:_:MediumOrchid1:171
MediumPurple6:60:_:Orange2:172
MediumSpringGreen:49:_:LightSalmon3:173
MediumTurquoise:80:_:LightPink2:174
MediumVioletRed:126:_:Pink3:175
MistyRose1:224:_:Plum3:176
MistyRose3:181:_:Violet:177
NavajoWhite1:223:_:Gold3:178
NavajoWhite3:144:_:LightGoldenrod3:179
.TE
.TS
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
Navy:4:_:Tan:180
NavyBlue:17:_:MistyRose3:181
Olive:3:_:Thistle3:182
Orange1:214:_:Plum2:183
Orange2:172:_:Yellow3:184
Orange3:58:_:Khaki3:185
Orange4:94:_:LightGoldenrod5:186
OrangeRed1:202:_:LightYellow3:187
Orchid:170:_:Grey84:188
Orchid1:213:_:LightSteelBlue1:189
Orchid2:212:_:Yellow2:190
PaleGreen1:121:_:DarkOliveGreen3:191
PaleGreen2:156:_:DarkOliveGreen1:192
PaleGreen3:114:_:DarkSeaGreen2:193
PaleGreen4:77:_:Honeydew2:194
PaleTurquoise1:159:_:LightCyan2:195
PaleTurquoise4:66:_:Red1:196
PaleVioletRed1:211:_:DeepPink2:197
Pink1:218:_:DeepPink3:198
Pink3:175:_:DeepPink1:199
Plum1:219:_:Magenta3:200
Plum2:183:_:Magenta1:201
Plum3:176:_:OrangeRed1:202
Plum4:96:_:IndianRed3:203
Purple:129:_:IndianRed2:204
Purple1:5:_:HotPink:205
Purple2:93:_:HotPink1:206
Purple3:56:_:MediumOrchid2:207
Purple4:54:_:DarkOrange:208
Purple5:55:_:Salmon1:209
.TE
.TS
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
Red:9:_:LightCoral:210
Red1:196:_:PaleVioletRed1:211
Red2:124:_:Orchid2:212
Red3:160:_:Orchid1:213
RosyBrown:138:_:Orange1:214
RoyalBlue1:63:_:SandyBrown:215
Salmon1:209:_:LightSalmon1:216
SandyBrown:215:_:LightPink1:217
SeaGreen1:84:_:Pink1:218
SeaGreen2:85:_:Plum1:219
SeaGreen3:83:_:Gold1:220
SeaGreen4:78:_:LightGoldenrod4:221
Silver:7:_:LightGoldenrod2:222
SkyBlue1:117:_:NavajoWhite1:223
SkyBlue2:111:_:MistyRose1:224
SkyBlue3:74:_:Thistle1:225
SlateBlue1:99:_:Yellow1:226
SlateBlue2:61:_:LightGoldenrod1:227
SlateBlue3:62:_:Khaki1:228
SpringGreen1:48:_:Wheat1:229
SpringGreen2:42:_:Cornsilk1:230
SpringGreen3:47:_:Grey100:231
SpringGreen4:35:_:Grey3:232
SpringGreen5:41:_:Grey7:233
SpringGreen6:29:_:Grey11:234
SteelBlue:67:_:Grey15:235
SteelBlue1:75:_:Grey19:236
SteelBlue2:81:_:Grey23:237
SteelBlue3:68:_:Grey27:238
Tan:180:_:Grey30:239
.TE
.TS
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
Teal:6:_:Grey35:240
Thistle1:225:_:Grey39:241
Thistle3:182:_:Grey42:242
Turquoise2:45:_:Grey46:243
Turquoise4:30:_:Grey50:244
Violet:177:_:Grey54:245
Wheat1:229:_:Grey58:246
Wheat4:101:_:Grey62:247
White:15:_:Grey66:248
Yellow:11:_:Grey70:249
Yellow1:226:_:Grey74:250
Yellow2:190:_:Grey78:251
Yellow3:184:_:Grey82:252
Yellow4:100:_:Grey85:253
Yellow5:106:_:Grey89:254
Yellow6:148:_:Grey93:255
.TE
.Sh SEE ALSO
.Xr meli 1 ,
.Xr meli.conf 5
.Sh CONFORMING TO
TOML Standard v.0.5.0 https://toml.io/en/v0.5.0
.sp
https://no-color.org/
.Sh AUTHORS
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.)
.Pp
.Aq https://meli.delivery

View File

@ -1,687 +0,0 @@
.\" meli - meli.1
.\"
.\" Copyright 2017-2019 Manos Pitsidianakis
.\"
.\" This file is part of meli.
.\"
.\" meli is free software: you can redistribute it and/or modify
.\" it under the terms of the GNU General Public License as published by
.\" the Free Software Foundation, either version 3 of the License, or
.\" (at your option) any later version.
.\"
.\" meli is distributed in the hope that it will be useful,
.\" but WITHOUT ANY WARRANTY; without even the implied warranty of
.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
.\" GNU General Public License for more details.
.\"
.\" You should have received a copy of the GNU General Public License
.\" along with meli. If not, see <http://www.gnu.org/licenses/>.
.\"
.de Shortcut
.Sm
.Aq \\$1
\
.Po
.Em shortcuts.\\$2\&. Ns
.Em \\$3
.Pc
.Sm
..
.de ShortcutPeriod
.Aq \\$1
.Po
.Em shortcuts.\\$2\&. Ns
.Em \\$3
.Pc Ns
..
.de Command
.Bd -ragged
.Cm \\$*
.Ed
.sp
..
.Dd November 11, 2022
.Dt MELI 1
.Os
.Sh NAME
.Nm meli
.Nd terminal e-mail client
.Em μέλι
is the Greek word for honey
.Sh SYNOPSIS
.Nm
.Op Fl -help | h
.Op Fl -version | v
.Op Fl -config Ar path
.Bl -tag -width flag -offset indent
.It Fl -help | h
Show help message and exit.
.It Fl -version | v
Show version and exit.
.It Fl -config Ar path
Start meli with given configuration file.
.It Cm create-config Op Ar path
Create configuration file in
.Pa path
if given, or at
.Pa $XDG_CONFIG_HOME/meli/config.toml
.It Cm edit-config
Edit configuration files with
.Ev EDITOR
or
.Ev VISUAL Ns
\&.
.It Cm test-config Op Ar path
Test a configuration file for syntax issues or missing options.
.It Cm man Op Ar page
Print documentation page and exit (Piping to a pager is recommended).
.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
.Sh DESCRIPTION
.Nm
is a terminal mail client aiming for extensive and user-frendly configurability.
.Bd -literal
^^ .-=-=-=-. ^^
^^ (`-=-=-=-=-`) ^^
(`-=-=-=-=-=-=-`) ^^ ^^
^^ (`-=-=-=-=-=-=-=-`) ^^
( `-=-=-=-(@)-=-=-` ) ^^
(`-=-=-=-=-=-=-=-=-`) ^^
(`-=-=-=-=-=-=-=-=-`) ^^
(`-=-=-=-=-=-=-=-=-`)
^^ (`-=-=-=-=-=-=-=-=-`) ^^
^^ (`-=-=-=-=-=-=-=-`) ^^
(`-=-=-=-=-=-=-`) ^^
^^ (`-=-=-=-=-`)
`-=-=-=-=-` ^^
.Ed
.Sh STARTING WITH meli
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
The main visual navigation tool, the left-side sidebar may be toggled with
.ShortcutPeriod ` listing toggle_menu_visibility
\&.
.Pp
Each mailbox may be viewed in 4 modes:
Plain views each mail individually, Threaded shows their thread relationship visually, Conversations collapses each thread of emails into a single entry, Compact shows one row per thread.
.Pp
If you're using a light color palette in your terminal, you should set
.Em theme = "light"
in the
.Em terminal
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
\&.
.Nm
will attempt to open text inside its pager, and other content via
.Cm xdg-open Ns
\&.
Press
.Shortcut m envelope_view open_mailcap
instead to use the mailcap entry for the MIME type of the attachment, if any.
See
.Sx FILES
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.
.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 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.
.Em IMAP
uses the SEARCH command,
.Em notmuch
uses libnotmuch and
.Em Maildir/mbox
performs a slow linear search.
It is advised to use a search backend on
.Em Maildir/mbox
accounts.
.Nm Ns
, if built with sqlite3, includes the ability to perform full text search on the following fields:
.Em From ,
.Em To ,
.Em Cc ,
.Em Bcc ,
.Em In-Reply-To ,
.Em References ,
.Em Subject
and
.Em Date .
The message body (in plain text human readable form) and the flags can also be queried.
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
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
.D1 subject:helloooo or subject:\&"call for help\&" or \&"You remind me today of a small, Mexican chihuahua.\&"
.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
.Pp
Boolean operators are
.Em or Ns
,
.Em and
and
.Em not
.Po
alias:
.Em \&!
.Pc
String keywords with spaces must be quoted.
Quotes should always be escaped.
.sp
.Sy Important Notice about IMAP/JMAP
.sp
To prevent downloading all your messages from your IMAP/JMAP server, don't set
.Em search_backend
to
.Em sqlite3 Ns
\&.
.Nm
will relay your queries to the IMAP server.
Expect a delay between query and response.
Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticable delay.
.Ss QUERY ABNF SYNTAX
.Bl -bullet
.It
.Li query = \&"(\&" query \&")\&" | from | to | cc | bcc | alladdresses | subject | flags | has_attachments | query \&"or\&" query | query \&"and\&" query | not query
.It
.Li not = \&"not\&" | \&"!\&"
.It
.Li quoted = ALPHA / SP *(ALPHA / DIGIT / SP)
.It
.Li term = ALPHA *(ALPHA / DIGIT) | DQUOTE quoted DQUOTE
.It
.Li tagname = term
.It
.Li flagval = \&"passed\&" | \&"replied\&" | \&"seen\&" | \&"read\&" | \&"junk\&" | \&"trash\&" | \&"trashed\&" | \&"draft\&" | \&"flagged\&" | tagname
.It
.Li flagterm = flagval | flagval \&",\&" flagterm
.It
.Li from = \&"from:\&" term
.It
.Li to = \&"to:\&" term
.It
.Li cc = \&"cc:\&" term
.It
.Li bcc = \&"bcc:\&" term
.It
.Li alladdresses = \&"alladdresses:\&" term
.It
.Li subject = \&"subject:\&" term
.It
.Li flags = \&"flags:\&" flag | \&"tags:\&" flag | \&"is:\&" flag
.El
.Sh TAGS
.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
and
.Command tag remove TAG
(see
.Xr meli.conf 5 TAGS Ns
, settings
.Ic colors
and
.Ic ignore_tags
for how to set tag colors and tag visibility)
.Sh COMPOSING
.Ss Opening the message Composer tab
To create a new mail message, press
.Shortcut m listing new_mail
while viewing a mailbox.
To reply to a mail, press
.ShortcutPeriod R envelope_view reply
\&.
Both these actions open the mail composer view in a new tab.
.Ss Editing text
.Bl -bullet -compact
.It
Edit the header fields by selecting with the arrow keys and pressing
.Shortcut Enter general focus_in_text_field
to enter
.Em INSERT
mode and
.Cm Esc
key to exit.
.It
At any time you may press
.Shortcut e composing edit Ns
to launch your editor (see
.Xr meli.conf 5 COMPOSING Ns
, setting
.Ic editor_command
for how to select which editor to launch).
.It
Your editor can be used in
.Nm Ns
\&'s embed terminal emulator by setting
.Ic embed
to
.Em true
in your composing settings
.Po
You can return to
.Nm
at any time by pressing
.Aq Ctrl-Z
.Pc
.It
When launched, your editor captures all input until it exits or stops.
.It
To stop your editor and return to
.Nm
press
.Aq Ctrl-z
and to resume editing press the
.Ic edit
command again.
.El
.Ss Attachments
Attachments may be handled with the
.Cm add-attachment Ns
,
.Cm remove-attachment
commands (see below).
.Ss Sending
Finally, pressing
.Shortcut s composing send_mail
will send your message according to your settings
.Po
see
.Xr meli.conf 5 COMPOSING Ns
, setting name
.Ic send_mail
.Pc Ns
\&.
With no Draft or Sent mailbox,
.Nm
tries first saving mail in your INBOX and then at any other mailbox.
On complete failure to save your draft or sent message it will be saved in your
.Em tmp
directory instead and you will be notified of its location.
.Ss Drafts
To save your draft without sending it, issue
.Em COMMAND
.Cm close
and select 'save as draft'.
.sp
To open a draft for further editing, select your draft in the mail listing and press
.Ic edit Ns
\&.
.Sh CONTACTS
.Nm
supports three kinds of contact backends:
.sp
.Bl -enum -compact -offset indent
.It
an internal format that gets saved under
.Pa $XDG_DATA_HOME/meli/account_name/addressbook Ns
\&.
.It
vCard files (v3, v4) through the
.Ic vcard_folder
option in the account section.
The path defined as
.Ic vcard_folder
can hold multiple vCards per file.
They are loaded read only.
.It
a
.Xr mutt 1
compatible alias file in the option
.Ic mutt_alias_file
.El
.sp
See
.Xr meli.conf 5 ACCOUNTS
for the complete account configuration values.
.Sh MODES
.Bl -tag -compact -width 8n
.It NORMAL
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
key.
.It EMBED
is the mode of the embed terminal emulator
.It INSERT
captures all input as text input, and is exited with
.Cm Esc
key.
.El
.Ss COMMAND Mode
.Ss Mail listing commands
.Bl -tag -width 36n
.It Cm set Ar plain | threaded | compact | conversations
set the way mailboxes are displayed
.El
.TS
allbox tab(:);
lb l.
conversations:shows one entry per thread
compact:shows one row per thread
threaded:shows threads as a tree structure
plain:shows one row per mail, regardless of threading
.TE
.Bl -tag -width 36n
.It Cm sort Ar subject | date \ Ar asc | desc
sort mail listing
.It Cm subsort Ar subject | date \ Ar asc | desc
sorts only the first level of replies.
.It Cm go Ar n
where
.Ar n
is a mailbox prefixed with the
.Ar n
number in the side menu for the current account
.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.
.It Cm create-mailbox Ar ACCOUNT Ar MAILBOX_PATH
create mailbox with given path.
Be careful with backends and separator sensitivity (eg IMAP)
.It Cm subscribe-mailbox Ar ACCOUNT Ar MAILBOX_PATH
subscribe to mailbox with given path
.It Cm unsubscribe-mailbox Ar ACCOUNT Ar MAILBOX_PATH
unsubscribe to mailbox with given path
.It Cm rename-mailbox Ar ACCOUNT Ar MAILBOX_PATH_SRC Ar MAILBOX_PATH_DEST
rename mailbox
.It Cm delete-mailbox Ar ACCOUNT Ar MAILBOX_PATH
deletes mailbox in the mail backend.
This action is unreversible.
.El
.Ss Mail view commands
.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
unsubscribe automatically from list of viewed envelope
.It Cm list-archive
open list archive with
.Cm xdg-open
.El
.Ss composing mail commands
.Bl -tag -width 36n
.It Cm mailto Ar MAILTO_ADDRESS
Opens a composer tab with initial values parsed from the
.Li mailto:
address.
.It Cm add-attachment Ar PATH
in composer, add
.Ar PATH
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
.Ss generic commands
.Bl -tag -width 36n
.It Cm open-in-tab
opens envelope view in new tab
.It Cm close
closes closeable tabs
.It Cm setenv Ar KEY=VALUE
set environment variable
.Ar KEY
to
.Ar VALUE
.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
.Xr meli.conf 5 SHORTCUTS
for shortcuts and their default values.
.Sh EXIT STATUS
.Nm
exits with 0 on a successful run.
Other exit statuses are:
.Bl -tag -width 5n
.It 1
catchall for general errors
.It 101
process panic
.El
.Sh ENVIRONMENT
.Bl -tag -width "$XDG_CONFIG_HOME/meli/plugins/*" -offset indent
.It Ev EDITOR
Specifies the editor to use
.It Ev MELI_CONFIG
Override the configuration file
.It Ev NO_COLOR
When present (regardless of its value), prevents the addition of ANSI color.
The configuration value
.Ic use_color
overrides this.
.El
.Sh FILES
.Nm
uses the following parts of the XDG standard:
.Bl -tag -width "$XDG_CONFIG_HOME/meli/plugins/*" -offset indent
.It Ev XDG_CONFIG_HOME
defaults to
.Pa ~/.config/
.It Ev XDG_CACHE_HOME
defaults to
.Pa ~/.cache/
.El
.Pp
and appropriates the following locations:
.Bl -tag -width "$XDG_CONFIG_HOME/meli/plugins/*" -offset indent
.It Pa $XDG_CONFIG_HOME/meli/
User configuration directory
.It Pa $XDG_CONFIG_HOME/meli/config.toml
User configuration file, see
.Xr meli.conf 5
for its syntax and values.
.It Pa $XDG_CONFIG_HOME/meli/hooks/*
Reserved for event hooks.
.It Pa $XDG_CONFIG_HOME/meli/plugins/*
Reserved for plugin files.
.It Pa $XDG_CACHE_HOME/meli/*
Internal cached data used by meli.
.It Pa $XDG_DATA_HOME/meli/*
Internal data used by meli.
.It Pa $XDG_DATA_HOME/meli/meli.log
Operation log.
.It Pa /tmp/meli/*
Temporary files generated by
.Nm Ns
\&.
.El
.Pp
Mailcap entries are searched for in the following files, in this order:
.Pp
.Bl -enum -compact -offset indent
.It
.Pa $XDG_CONFIG_HOME/meli/mailcap
.It
.Pa $XDG_CONFIG_HOME/.mailcap
.It
.Pa $HOME/.mailcap
.It
.Pa /etc/mailcap
.It
.Pa /usr/etc/mailcap
.It
.Pa /usr/local/etc/mailcap
.El
.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
.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

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

File diff suppressed because it is too large Load Diff

View File

@ -1,142 +0,0 @@
## Look into meli.conf(5) for all valid configuration options, their
## descriptions and default values
##
## The syntax for including other configuration files is enclosed in `:
##`include("account_one")`
##`include("./account_two")`
##`include("/home/absolute/path/to/shortcuts/config.toml")`
##
##
## Setting up a Maildir account
#[accounts.account-name]
#root_mailbox = "/path/to/root/mailbox"
#format = "Maildir"
#listing.index_style = "Conversations" # or [plain, threaded, compact]
#identity="email@example.com"
#display_name = "Name"
#subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
#
## Set mailbox-specific settings
# [accounts.account-name.mailboxes]
# "INBOX" = { rename="Inbox" }
# "drafts" = { rename="Drafts" }
# "foobar-devel" = { ignore = true } # don't show notifications for this mailbox
#
## Setting up an mbox account
#[accounts.mbox]
#root_mailbox = "/var/mail/username"
#format = "mbox"
#listing.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_password="pha2hiLohs2eeeish2phaii1We3ood4chakaiv0hien2ahie3m"
#server_username="username@example.com"
##server_port="993" # imaps
#server_port="143" # STARTTLS
#use_starttls=true #optional
#listing.index_style = "Conversations"
#identity = "username@example.com"
#display_name = "Name Name"
### match every mailbox:
#subscribed_mailboxes = ["*" ]
### match specific mailboxes:
##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"
#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'
#
#[pager]
#filter = "COLUMNS=72 /usr/local/bin/pygmentize -l email"
#pager_context = 0 # default, optional
#sticky_headers = true # default, optional
#
#[notifications]
#script = "notify-send"
#xbiff_file_path = "path" # for use with xbiff(1)
#play_sound = true # default, optional
#sound_file = "path" # optional
#
###shortcuts
#[shortcuts.composing]
#edit = 'e'
#
#[shortcuts.contact-list]
#create_contact = 'c'
#edit_contact = 'e'
#
##Mail listing defaults
#[shortcuts.listing]
#prev_page = "PageUp"
#next_page = "PageDown"
#prev_mailbox = 'K'
#next_mailbox = 'J'
#prev_account = 'l'
#next_account = 'h'
#new_mail = 'm'
#set_seen = 'n'
#exit_entry = 'i'
#
##Pager defaults
#
#[shortcuts.pager]
#scroll_up = 'k'
#scroll_down = 'j'
#page_up = "PageUp"
#page_down = "PageDown"
#
#[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" } }
#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
#
#[terminal]
#theme = "dark" # or "light"

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,60 +0,0 @@
[terminal.themes.orca]
color_aliases = { "neon_green" = "#6ef9d4", "darkgrey" = "#4a4a4a", "neon_purple" = "#df2f94" }
"theme_default" = { fg = "White", bg = "Black", attrs = "Default" }
"mail.listing.attachment_flag" = { fg = "$neon_green", bg = "theme_default", attrs = "theme_default" }
"mail.listing.compact.even" = { fg = "$darkgrey", bg = "theme_default", attrs = "theme_default" }
"mail.listing.compact.even_highlighted" = { fg = "theme_default", bg = "Grey58", attrs = "theme_default" }
"mail.listing.compact.even_selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
"mail.listing.compact.even_unseen" = { fg = "Black", bg = "Grey78", attrs = "theme_default" }
"mail.listing.compact.odd" = { fg = "$darkgrey", bg = "theme_default", attrs = "theme_default" }
"mail.listing.compact.odd_highlighted" = { fg = "theme_default", bg = "Grey58", attrs = "theme_default" }
"mail.listing.compact.odd_selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
"mail.listing.compact.odd_unseen" = { fg = "Black", bg = "Grey78", attrs = "theme_default" }
"mail.listing.conversations" = { fg = "$darkgrey", bg = "theme_default", attrs = "Default" }
"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.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.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" }
"mail.listing.plain.even_unseen" = { fg = "Black", bg = "Grey78", attrs = "theme_default" }
"mail.listing.plain.odd" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"mail.listing.plain.odd_highlighted" = { fg = "theme_default", bg = "Grey58", attrs = "theme_default" }
"mail.listing.plain.odd_selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
"mail.listing.plain.odd_unseen" = { fg = "Black", bg = "Grey78", attrs = "theme_default" }
"mail.listing.tag_default" = { fg = "Black", bg = "$neon_green", attrs = "theme_default" }
"mail.listing.thread_snooze_flag" = { fg = "Red", bg = "theme_default", attrs = "theme_default" }
"mail.sidebar" = { fg = "$darkgrey", bg = "theme_default", attrs = "theme_default" }
"mail.sidebar_highlighted" = { fg = "Grey7", bg = "White", attrs = "theme_default" }
"mail.sidebar_highlighted_account" = { fg = "$darkgrey", bg = "theme_default", attrs = "theme_default" }
"mail.sidebar_highlighted_account_name" = { fg = "White", bg = "theme_default", attrs = "theme_default" }
"mail.sidebar_account_name" = { fg = "$darkgrey", bg = "theme_default", 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 = "Grey46", bg = "theme_default", attrs = "theme_default" }
"mail.sidebar_unread_count" = { fg = "Grey46", bg = "theme_default", attrs = "theme_default" }
"mail.view.body" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"mail.view.headers" = { fg = "DodgerBlue1", bg = "theme_default", attrs = "theme_default" }
"mail.view.thread.indentation.a" = { fg = "theme_default", bg = "#EC4436", attrs = "theme_default" }
"mail.view.thread.indentation.b" = { fg = "theme_default", bg = "#D301F9", attrs = "theme_default" }
"mail.view.thread.indentation.c" = { fg = "theme_default", bg = "#314EFB", attrs = "theme_default" }
"mail.view.thread.indentation.d" = { fg = "theme_default", bg = "#068ACD", attrs = "theme_default" }
"mail.view.thread.indentation.e" = { fg = "theme_default", bg = "#019589", 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 = "White", bg = "Black", 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 = "theme_default" }
"tab.unfocused" = { fg = "$darkgrey", bg = "Black", attrs = "theme_default" }
"widgets.form.field" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"widgets.form.highlighted" = { fg = "theme_default", bg = "Grey58", 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 = "Grey", attrs = "theme_default" }

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

View File

@ -1,42 +0,0 @@
[terminal.themes.spooky]
"theme_default" = { fg = "#333", bg = "#fe9b13", attrs = "Default" }
"mail.listing.attachment_flag" = { fg = "LightSlateGrey", bg = "theme_default", attrs = "theme_default" }
"mail.listing.compact.even" = { fg = "theme_default", bg = "#bf200e", attrs = "theme_default" }
"mail.listing.compact.odd" = { fg = "theme_default", bg = "#fa4113", attrs = "theme_default" }
"mail.listing.compact.even_highlighted" = { fg = "theme_default", bg = "Yellow6", attrs = "theme_default" }
"mail.listing.compact.odd_highlighted" = { fg = "theme_default", bg = "Yellow6", attrs = "theme_default" }
"mail.listing.compact.even_selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
"mail.listing.compact.odd_selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
"mail.listing.compact.even_unseen" = { fg = "Black", bg = "Orange3", attrs = "theme_default" }
"mail.listing.compact.odd_unseen" = { fg = "Black", bg = "Orange3", attrs = "theme_default" }
"mail.listing.conversations.date" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"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.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.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" }
"mail.listing.plain.odd_unseen" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"mail.listing.plain.even_highlighted" = { fg = "theme_default", bg = "Yellow6", attrs = "theme_default" }
"mail.listing.plain.odd_highlighted" = { fg = "theme_default", bg = "Yellow6", attrs = "theme_default" }
"mail.listing.plain.even_selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
"mail.listing.plain.odd_selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
"mail.listing.thread_snooze_flag" = { fg = "Red", bg = "theme_default", attrs = "theme_default" }
"mail.sidebar" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"mail.sidebar_highlighted_account" = { fg = "White", bg = "Orange3", 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" = { fg = "Grey7", bg = "White", 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 = "Grey46", bg = "theme_default", attrs = "theme_default" }
"mail.sidebar_unread_count" = { fg = "Grey46", bg = "theme_default", attrs = "theme_default" }
"mail.view.body" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"mail.view.headers" = { fg = "DodgerBlue1", bg = "theme_default", attrs = "theme_default" }
"status.bar" = { fg = "White", bg = "#A21500", attrs = "theme_default" }
"tab.bar" = { fg = "theme_default", bg = "#332300", attrs = "theme_default" }
"tab.unfocused" = { fg = "theme_default", bg = "#A26F00", attrs = "theme_default" }
"tab.focused" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }

View File

@ -1,46 +0,0 @@
[terminal.themes.watermelon]
color_aliases = { "JewelGreen" = "#157241", "PinkLace" = "#FFD5FD", "TorchRed" = "#F50431", "ChelseaCucumber" = "#6CA94A", "ScreaminGreen" = "#8FFF52", "SunsetOrange" = "#f74b41", "Melon" = "#fdbcb4", "BlueStone" = "#005F5F", "HotPink" = "#FF74D7" }
"theme_default" = { fg = "$TorchRed", bg = "$PinkLace", attrs = "Default" }
"widgets.list.header" = { fg = "$PinkLace", bg = "$TorchRed", attrs = "Bold" }
"mail.listing.attachment_flag" = { fg = "LightSlateGrey", bg = "theme_default", attrs = "theme_default" }
"mail.listing.tag_default" = { bg = "$Melon", attrs = "Bold" }
"mail.listing.compact.even" = { fg = "White", bg = "$ChelseaCucumber", attrs = "Bold" }
"mail.listing.compact.odd" = { fg = "$PinkLace", bg = "$JewelGreen", attrs = "theme_default" }
"mail.listing.compact.even_unseen" = { fg = "$JewelGreen", bg = "$ScreaminGreen", attrs = "theme_default" }
"mail.listing.compact.odd_unseen" = { fg = "$JewelGreen", bg = "$ScreaminGreen", attrs = "theme_default" }
"mail.listing.compact.even_highlighted" = { fg = "$JewelGreen", bg = "$SunsetOrange", attrs = "theme_default" }
"mail.listing.compact.odd_highlighted" = { fg = "$JewelGreen", bg = "$SunsetOrange", attrs = "theme_default" }
"mail.listing.compact.even_selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
"mail.listing.compact.odd_selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
"mail.listing.conversations.date" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"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.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.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" }
"mail.listing.plain.odd_unseen" = { fg = "theme_default", bg = "mail.listing.compact.odd_unseen", attrs = "theme_default" }
"mail.listing.plain.even_highlighted" = { fg = "$JewelGreen", bg = "$SunsetOrange", attrs = "theme_default" }
"mail.listing.plain.odd_highlighted" = { fg = "$JewelGreen", bg = "$SunsetOrange", attrs = "theme_default" }
"mail.listing.plain.even_selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
"mail.listing.plain.odd_selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
"mail.listing.thread_snooze_flag" = { fg = "Red", bg = "theme_default", attrs = "theme_default" }
"mail.sidebar" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"mail.sidebar_highlighted_account" = { fg = "White", bg = "$TorchRed", 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" = { fg = "Grey7", bg = "White", 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 = "Grey46", bg = "theme_default", attrs = "theme_default" }
"mail.sidebar_unread_count" = { fg = "Grey46", bg = "theme_default", attrs = "theme_default" }
"mail.view.body" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"mail.view.headers" = { fg = "DodgerBlue1", bg = "theme_default", attrs = "theme_default" }
"status.bar" = { fg = "$PinkLace", bg = "$TorchRed", attrs = "theme_default" }
"status.notification" = { fg = "theme_default", bg = "theme_default", attrs = "Default" }
"tab.bar" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"tab.unfocused" = { fg = "$PinkLace", bg = "$HotPink", attrs = "theme_default" }
"tab.focused" = { fg = "theme_default", bg = "theme_default", 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

4
fuzz/.gitignore vendored
View File

@ -1,4 +0,0 @@
target
corpus
artifacts

1377
fuzz/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +0,0 @@
[package]
name = "melib-fuzz"
version = "0.0.0"
authors = ["Automatically generated"]
publish = false
edition = "2018"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.3"
[dependencies.melib]
path = "../melib"
features = ["unicode_algorithms"]
# Prevent this from interfering with workspaces
[workspace]
members = ["."]
[[bin]]
name = "envelope_parse"
path = "fuzz_targets/envelope_parse.rs"

View File

@ -1,25 +0,0 @@
","
";"
"<"
">"
"@"
":"
# tab character
"\x09"
# new line character
"\x0A"
" "
"Subject: "
"Subject"
"To"
"To: "
"Date"
"Date: "
"Message-Id"
"Message-Id: "
"From"
"From: "
"Cc"
"Cc: "
"Bcc"
"Bcc: "

View File

@ -1,11 +0,0 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
extern crate melib;
use melib::Envelope;
fuzz_target!(|data: &[u8]| {
// fuzzed code goes here
let _envelope = Envelope::from_bytes(data, None);
});

460
meli.1 100644
View File

@ -0,0 +1,460 @@
.\" meli - meli.1
.\"
.\" Copyright 2017-2019 Manos Pitsidianakis
.\"
.\" This file is part of meli.
.\"
.\" meli is free software: you can redistribute it and/or modify
.\" it under the terms of the GNU General Public License as published by
.\" the Free Software Foundation, either version 3 of the License, or
.\" (at your option) any later version.
.\"
.\" meli is distributed in the hope that it will be useful,
.\" but WITHOUT ANY WARRANTY; without even the implied warranty of
.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
.\" GNU General Public License for more details.
.\"
.\" You should have received a copy of the GNU General Public License
.\" along with meli. If not, see <http://www.gnu.org/licenses/>.
.\"
.Dd July 29, 2019
.Dt MELI 1
.Os
.Sh NAME
.Nm meli
.Nd Meli Mail User Agent. meli is the Greek word for honey
.Sh SYNOPSIS
.Nm
.Op Fl -help | h
.Op Fl -version | v
.Op Fl -create-config Op Ar path
.Op Fl -test-config Op Ar path
.Op Fl -config Ar path
.Sh DESCRIPTION
Experimental terminal mail client
.Bl -tag -width flag -offset indent
.It Fl -help, h
Show help message and exit.
.It Fl -version, v
Show version and exit.
.It Fl -create-config Op Ar path
Create configuration file in
.Pa path
if given, or at
.Pa $XDG_CONFIG_HOME/meli/config
.It Fl -test-config Op Ar path
Test a configuration file for syntax issues or missing options.
.It Fl -config Ar path
Start meli with given configuration file.
.El
.Sh STARTING WITH meli
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 along with a sample configuration. The sample configuration
.Pa $XDG_CONFIG_HOME/meli/config
includes comments with 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
.Cm \&?
to show 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 is the left-side sidebar. The menu's visibility may be toggled with
.Cm `
(shortcuts.listing:
.Ic toggle_menu_visibility Ns
).
.Pp
The view into each folder has 4 modes: plain, threaded, conversations and compact. Plain views each mail indvidually, threaded shows their thread relationship visually, and conversations includes one entry per thread of emails (compact is one row per thread).
.Pp
If you're using a light color palette in your terminal, you may set
.Em theme = "light"
in the
.Em terminal
section of your configuration.
.Bd -literal
^^ .-=-=-=-. ^^
^^ (`-=-=-=-=-`) ^^
(`-=-=-=-=-=-=-`) ^^ ^^
^^ (`-=-=-=-=-=-=-=-`) ^^
( `-=-=-=-(@)-=-=-` ) ^^
(`-=-=-=-=-=-=-=-=-`) ^^
(`-=-=-=-=-=-=-=-=-`) ^^
(`-=-=-=-=-=-=-=-=-`)
^^ (`-=-=-=-=-=-=-=-=-`) ^^
^^ (`-=-=-=-=-=-=-=-`) ^^
(`-=-=-=-=-=-=-`) ^^
^^ (`-=-=-=-=-`)
`-=-=-=-=-` ^^
.Ed
.Sh VIEWING MAIL
Open attachments by typing their index in the attachments list and then
.Cm a Ns
\&.
.Ns
.Nm
will attempt to open text inside its pager and other content via
.Cm xdg-open Ns
\&. Press
.Cm m
instead to use the mailcap entry for the MIME type of the attachment, if any. See
.Sx FILES
for the location of the mailcap files and
.Xr mailcap 5
for their syntax.
.Sh SEARCH
Each e-mail storage backend has its default search method.
.Em IMAP
uses the SEARCH command,
.Em notmuch
uses libnotmuch and
.Em Maildir/mbox
have to do a slow linear search. Thus it is advised to use a cache on
.Em Maildir/mbox
accounts.
.Nm Ns
, if built with sqlite3, includes the ability to perform full text search on the following fields: From, To, Cc, Bcc, In-Reply-To, References, Subject and Date. The message body (in plain text human readable form) and the flags can also be queried. To enable sqlite3 indexing for an account set
.Em cache_type
to
.Em sqlite3
in the configuration file and to create the sqlite3 index issue command
.Cm index Ar ACCOUNT_NAME Ns \&.
To search in the message body type your keywords without any special formatting.
To search in specific fields, prepend your search keyword with "field:" like so:
.Pp
.D1 subject:helloooo or subject:\&"call for help\&" or \&"You remind me today of a small, Mexican chihuahua.\&"
.Pp
.D1 not ((from:unrealistic and (to:complex or not "query")) or flags:seen,draft)
.Pp
.D1 alladdresses:mailing@list.tld and cc:me@domain.tld
.Pp
Boolean operators are
.Em or Ns
,
.Em and
and
.Em not
.Po
alias:
.Em \&!
.Pc
String keywords with spaces must be quoted. Quotes should always be escaped.
.sp
.Sy Important Notice about IMAP
.sp
To prevent downloading all your messages from your IMAP server, don't set
.Em cache_type
to
.Em sqlite3 Ns
\&.
.Nm
will relay your queries to the IMAP server. Expect a delay between query and response. Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticable delay.
.Sh COMPOSING
To send mail, press
.Cm m
while viewing the appropriate account to open a new composing tab. To reply to a mail, press
.Cm R Ns
\&. You may edit some of the header fields from within the view, by selecting with the arrow keys and pressing
.Cm enter
to enter
.Ar INSERT
mode. At any time you may press
.Cm e
to launch your editor (see
.Xr meli.conf 5 COMPOSING Ns
, setting
.Ic editor_cmd
for how to select which editor to launch). Attachments may be handled with the
.Em add-attachment Ns
,
.Em remove-attachment
commands (see below). Finally, pressing
.Cm s
will send your message by piping it into a binary of your choosing (see
.Xr meli.conf 5 COMPOSING Ns
, setting
.Ic mailer_cmd Ns
). To save your draft without sending it, issue command
.Cm close
and select 'save as draft'.
.Pp
With no Draft or Sent folder,
.Nm
tries first saving mail in your INBOX and then at any other folder. On complete failure to save your draft or sent message it will be saved in your
.Em tmp
directory instead and you will be notified of its location.
.Pp
To open a draft for editing later, select your draft in the mail listing and press
.Cm e Ns
\&.
Your editor can be used in
.Nm Ns
\&'s embed terminal emulator by setting
.Ic embed
to
.Em true
in your composing settings. When launched, your editor captures all input until it exits or stops. To stop your editor and return to
.Nm
issue Ctrl-z and to resume editing press the
.Ic edit_mail
command again (default
.Em e Ns
).
.Sh CONTACTS
.Nm
supports two kinds of contact backends:
.Bl -enum -compact -offset indent
.It
an internal format that gets saved under
.Pa $XDG_DATA_HOME/meli/account_name/addressbook Ns
\&.
.It
vCard files (v3, v4) through the
.Ic vcard_folder
option in the account section. The path defined as
.Ic vcard_folder
can hold multiple vCards per file. They are loaded read only.
.El
See
.Xr meli.conf 5 ACCOUNTS
for the complete account configuration values.
.Sh EXECUTE mode
Commands are issued in EXECUTE mode, by default started with Space and exited with Escape key.
.Pp
the following commands are valid in the mail listing context:
.Bl -tag -width 36n
.It Cm set Ar plain | threaded | compact | conversations
set the way mailboxes are displayed
.El
.TS
allbox tab(:);
lb l.
conversations:shows one entry per thread
compact:shows one row per thread
threaded:shows threads as a tree structure
plain:shows one row per mail, regardless of threading
.TE
.Bl -tag -width 36n
.It Cm sort Ar subject | date \ Ar asc | desc
sort mail listing
.It Cm subsort Ar subject | date \ Ar asc | desc
sorts only the first level of replies.
.It Cm go Ar n
where
.Ar n
is a mailbox prefixed with the
.Ar n
number in the side menu for the current account
.It Cm toggle_thread_snooze
don't issue notifications for thread under cursor in thread listing
.It Cm filter Ar STRING
filter mailbox with
.Ar STRING
key. Escape exits filter results
.It Cm set read, set unread
.It Cm create-folder Ar ACCOUNT Ar FOLDER_PATH
create folder with given path. be careful with backends and separator sensitivity (eg IMAP)
.It Cm subscribe-folder Ar ACCOUNT Ar FOLDER_PATH
subscribe to folder with given path
.It Cm unsubscribe-folder Ar ACCOUNT Ar FOLDER_PATH
unsubscribe to folder with given path
.It Cm rename-folder Ar ACCOUNT Ar FOLDER_PATH_SRC Ar FOLDER_PATH_DEST
rename folder
.It Cm delete-folder Ar ACCOUNT Ar FOLDER_PATH
delete folder
.El
.Pp
envelope view commands:
.Bl -tag -width 36n
.It Cm pipe Ar EXECUTABLE Ar ARGS
pipe pager contents to binary
.It Cm list-post
post in list of viewed envelope
.It Cm list-unsubscribe
unsubscribe automatically from list of viewed envelope
.It Cm list-archive
open list archive with
.Cm xdg-open
.El
.Pp
composing mail commands:
.Bl -tag -width 36n
.It Cm add-attachment Ar PATH
in composer, add
.Ar PATH
as an attachment
.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.
.El
.Pp
generic commands:
.Bl -tag -width 36n
.It Cm open-in-tab
opens envelope view in new tab
.It Cm close
closes closeable tabs
.It Cm setenv Ar KEY=VALUE
set environment variable
.Ar KEY
to
.Ar VALUE
.It Cm printenv Ar KEY
print environment variable
.Ar KEY
.El
.Sh SHORTCUTS
Non-complete list of shortcuts and their default values.
.Bl -tag -width 36n
.It Ic open_thread
\&'\\n'
.It Ic exit_thread
\&'i'
.It Ic create_contact
\&'c'
.It Ic edit_contact
\&'e'
.It Ic prev_page
PageUp,
.It Ic next_page
PageDown
.It Ic prev_folder
\&'K'
.It Ic next_folder
\&'J'
.It Ic prev_account
\&'l'
.It Ic next_account
\&'h'
.It Ic new_mail
\&'m'
.It Ic scroll_up
\&'k'
.It Ic scroll_down
\&'j'
.It Ic page_up
PageUp
.It Ic page_down
PageDown
.It Ic toggle-menu-visibility
\&'`'
.It Ic select
\&'v'
.It Ic `
toggles hiding of sidebar in mail listings
.It Ic \&?
opens up a shortcut window that shows available actions in the current component you are using (eg mail listing, contact list, mail composing)
.It Ic m
starts a new mail composer
.It Ic R
replies to the viewed mail.
.It Ic u
displays numbers next to urls in the body text of an email and
.Ar n Ns Ic g
opens the
.Ar n Ns
th
url with xdg-open
.It Ar n Ns Ic a
opens the
.Ar n Ns
th
attachment.
.It Ar n Ns Ic m
opens the
.Ar n Ns
th
attachment according to its mailcap entry.
.It Ic v
(un)selects mail entries in mail listings
.El
.Sh EXIT STATUS
.Nm
exits with 0 on a successful run. Other exit statuses are:
.Bl -tag -width 2n
.It 1
catchall for general errors
.El
.Sh ENVIRONMENT
.Bl -tag -width "$XDG_CONFIG_HOME/meli/plugins/*" -offset indent
.It Ev EDITOR
Specifies the editor to use
.It Ev MELI_CONFIG
Override the configuration file
.El
.Sh FILES
.Nm
uses the following parts of the XDG standard:
.Bl -tag -width "$XDG_CONFIG_HOME/meli/plugins/*" -offset indent
.It Ev XDG_CONFIG_HOME
defaults to
.Pa ~/.config/
.It Ev XDG_CACHE_HOME
defaults to
.Pa ~/.cache/
.El
.Pp
and appropriates the following locations:
.Bl -tag -width "$XDG_CONFIG_HOME/meli/plugins/*" -offset indent
.It Pa $XDG_CONFIG_HOME/meli/
User configuration directory.
.It Pa $XDG_CONFIG_HOME/meli/config
User configuration file. See
.Xr meli.conf 5
for its syntax and values.
.It Pa $XDG_CONFIG_HOME/meli/hooks/*
Reserved for event hooks.
.It Pa $XDG_CONFIG_HOME/meli/plugins/*
Reserved for plugin files.
.It Pa $XDG_CACHE_HOME/meli/*
Internal cached data used by meli.
.It Pa $XDG_DATA_HOME/meli/*
Internal data used by meli.
.It Pa $XDG_DATA_HOME/meli/meli.log
Operation log.
.It Pa /tmp/meli/*
Temporary files generated by
.Nm Ns
\&.
.El
.Pp
Mailcap entries are searched for in the following files, in this order:
.Pp
.Bl -enum -compact -offset indent
.It
.Pa $XDG_CONFIG_HOME/meli/mailcap
.It
.Pa $XDG_CONFIG_HOME/.mailcap
.It
.Pa $HOME/.mailcap
.It
.Pa /etc/mailcap
.It
.Pa /usr/etc/mailcap
.It
.Pa /usr/local/etc/mailcap
.El
.Sh SEE ALSO
.Xr meli.conf 5 ,
.Xr xdg-open 1 ,
.Xr mailcap 5
.Sh CONFORMING TO
XDG Standard
.Aq https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html Ns
, maildir
.Aq https://cr.yp.to/proto/maildir.html Ns
, IMAPv4rev1 RFC3501.
.Sh AUTHORS
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.)
.Pp
.Aq https://meli.delivery

590
meli.conf.5 100644
View File

@ -0,0 +1,590 @@
.\" meli - meli.1
.\"
.\" Copyright 2017-2019 Manos Pitsidianakis
.\"
.\" This file is part of meli.
.\"
.\" meli is free software: you can redistribute it and/or modify
.\" it under the terms of the GNU General Public License as published by
.\" the Free Software Foundation, either version 3 of the License, or
.\" (at your option) any later version.
.\"
.\" meli is distributed in the hope that it will be useful,
.\" but WITHOUT ANY WARRANTY; without even the implied warranty of
.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
.\" GNU General Public License for more details.
.\"
.\" You should have received a copy of the GNU General Public License
.\" along with meli. If not, see <http://www.gnu.org/licenses/>.
.\"
.Dd September 16, 2019
.Dt MELI.CONF 5
.Os
.Sh NAME
.Nm meli.conf
.Nd configuration file for the Meli Mail User Agent
.Sh SYNOPSIS
.Pa $XDG_CONFIG_HOME/meli/config
.Sh DESCRIPTION
Configuration for meli is written in TOML. Few things to consider before writing TOML (quoting the spec):
.Pp
.Bl -bullet -compact
.It
TOML is case sensitive.
.It
A TOML file must be a valid UTF-8 encoded Unicode document.
.It
Whitespace means tab (0x09) or space (0x20).
.It
Newline means LF (0x0A) or CRLF (0x0D 0x0A).
.El
.Pp
Refer to TOML documentation for valid TOML syntax.
Thought not part of TOML syntax,
.Nm
can have nested configuration files by using the following include directive, which though starting with
.Em \&#
is not a comment:
.Bd -literal
#include "/path/to/file"
.Ed
The accepted regular expression is
.Li ^\es*include\es*\&\\&\e"(\e\e.|[^\e"])+\e"\es*$
.Sh SECTIONS
The top level sections of the config are accounts, shortcuts, notifications, pager, composing, pgp, terminal.
.Pp
.Sy example configuration
.Bd -literal
# Setting up a Maildir account
[accounts.account-name]
root_folder = "/path/to/root/folder"
format = "Maildir"
index_style = "Compact"
identity="email@address.tld"
subscribed_folders = ["folder", "folder/Sent"] # or [ "*", ] for all folders
display_name = "Name"
# Set folder-specific settings
[accounts.account-name.folders]
"INBOX" = { rename="Inbox" } #inline table
"drafts" = { rename="Drafts" } #inline table
[accounts.account-name.folders."foobar-devel"] # or a regular table
ignore = true # don't show notifications for this folder
# Setting up an mbox account
[accounts.mbox]
root_folder = "/var/mail/username"
format = "mbox"
index_style = "Compact"
identity="username@hostname.local"
[pager]
filter = "/usr/bin/pygmentize"
html_filter = "w3m -I utf-8 -T text/html"
[notifications]
script = "notify-send"
[composing]
# required for sending e-mail
mailer_cmd = 'msmtp --read-recipients --read-envelope-from'
editor_cmd = 'vim +/^$'
[shortcuts]
[shortcuts.composing]
edit_mail = 'e'
[shortcuts.listing]
new_mail = 'm'
set_seen = 'n'
[terminal]
theme = "light"
.Ed
.Pp
available options are listed below.
.Sy default values are shown in parentheses.
.Sh ACCOUNTS
.Bl -tag -width 36n
.It Ic root_folder Ar String
the backend-specific path of the root_folder, usually INBOX.
.It Ic format Ar String Op maildir mbox imap notmuch
the format of the mail backend.
.It Ic subscribed_folders Ar [String,]
an array of folder paths to display in the UI. Paths are relative to the root folder (eg "INBOX/Sent", not "Sent").
The glob wildcard
.Em \&*
can be used to match every folder name and path.
.It Ic identity Ar String
your e-mail address that is inserted in the From: headers of outgoing mail
.It Ic index_style Ar String
set the way mailboxes are displayed
.El
.TS
allbox tab(:);
lb l.
conversations:shows one entry per thread
compact:shows one row per thread
threaded:shows threads as a tree structure
plain:shows one row per mail, regardless of threading
.TE
.Bl -tag -width 36n
.It Ic display_name Ar String
(optional) a name which can be combined with your address:
"Name <email@address.tld>"
.It Ic read_only Ar boolean
attempt to not make any changes to this account.
.Pq Em false
.It Ic cache_type Ar String
(optional) choose which cache backend to use. Available options are 'none' and 'sqlite3'
.Pq Em "sqlite3"
.It Ic vcard_folder Ar String
(optional) Folder that contains .vcf files. They are parsed and imported read-only.
.It Ic folders Ar folder_config
(optional) configuration for each folder. Its format is described below in
.Sx FOLDERS Ns
\&.
.El
.Sh notmuch only
.Ic root_folder
points to the directory which contains the
.Pa .notmuch/
subdirectory. notmuch folders are virtual, since they are defined by user-given notmuch queries. Thus you have to explicitly state the folders you want in the
.Ic folders
field and set the
.Ar query
property to each of them. Example:
.Bd -literal
[accounts.notmuch]
format = "notmuch"
\&...
[accounts.notmuch.folders]
"INBOX" = { query="tag:inbox", subscribe = true }
"Drafts" = { query="tag:draft", subscribe = true }
"Sent" = { query="from:username@server.tld from:username2@server.tld", subscribe = true }
.Ed
.Sh IMAP only
IMAP specific options are:
.Bl -tag -width 36n
.It Ic server_hostname Ar String
example:
.Qq mail.example.tld
.It Ic server_username Ar String
.It Ic server_password Ar String
.It Ic server_port Ar number
(optional)
.\" default value
.Pq Em 143
.It Ic use_starttls Ar boolean
(optional) if port is 993 and use_starttls is unspecified, it becomes false by default.
.\" default value
.Pq Em true
.It Ic danger_accept_invalid_certs Ar boolean
(optional) do not validate TLS certificates.
.\" default value
.Pq Em false
.El
.Sh FOLDERS
.Bl -tag -width 36n
.It Ic rename Ar String
(optional) show a different name for this folder in the UI
.It Ic autoload Ar boolean
(optional) load this folder on startup (not functional yet)
.It Ic subscribe Ar boolean
(optional) watch this folder for updates
.\" default value
.Pq Em true
.It Ic ignore Ar boolean
(optional) silently insert updates for this folder, if any
.\" default value
.Pq Em false
.It Ic usage Ar boolean
(optional) special usage of this folder. valid values are:
.Bl -bullet -compact
.It
.Ar Normal
.It
.Ar Inbox
.It
.Ar Archive
.It
.Ar Drafts
.It
.Ar Flagged
.It
.Ar Junk
.It
.Ar Sent
.It
.Ar Trash
.El
otherwise usage is inferred from the folder title.
.It Ic conf_override Ar boolean
(optional) override global settings for this folder. available sections to override are
.Em pager, notifications, shortcuts, composing
and the account options
.Em identity and index_style Ns
\&. example:
.Bd -literal
[accounts."imap.domain.tld".folders."INBOX"]
index_style = "plain"
[accounts."imap.domain.tld".folders."INBOX".pager]
filter = ""
.Ed
.El
.Sh COMPOSING
.Bl -tag -width 36n
.It Ic mailer_cmd Ar String
command to pipe new mail to, exit code must be 0 for success.
.It Ic editor_cmd Ar String
command to launch editor. Can have arguments. Draft filename is given as the last argument. If it's missing, the environment variable $EDITOR is looked up.
.It Ic embed Ar boolean
(optional) embed editor within meli
.\" default value
.Pq Em false
.It Ic format_flowed Ar boolean
(optional) set format=flowed [RFC3676] in text/plain attachments.
.\" default value
.Pq Em true
.El
.Sh SHORTCUTS
Shortcuts can take the following values:
.Qq Em Backspace
.Qq Em Left
.Qq Em Right
.Qq Em Up
.Qq Em Down
.Qq Em Home
.Qq Em End
.Qq Em PageUp
.Qq Em PageDown
.Qq Em Delete
.Qq Em Insert
.Qq Em Enter
.Qq Em Tab
.Qq Em Esc
.Qq Em F1..F12
.Qq Em M-char
.Qq Em C-char
and
.Qq Em char Ns
, where char is a single character string.
The headings before each list indicate the map key of the shortcut list. For example for the first list titled
.Em general
the configuration is typed as follows:
.Bd -literal
[shortcuts.general]
next_tab = 'T'
.Ed
and for
.Em compact-listing Ns
:
.Bd -literal
[shortcuts.compact-listing]
open_thread = "Enter"
exit_thread = 'i'
.Bd
.Sy Em general
.Bl -tag -width 36n
.It Ic next_tab
Go to next tab.
.\" default value
.Pq Em T
.It Ic go_to_tab
Go to the
.Em n Ns
th tab
.Pq Em cannot be redefined
.El
.Sy Em listing
.Bl -tag -width 36n
.It Ic prev_page
Go to previous page.
.\" default value
.Pq Em PageUp
.It Ic next_page
Go to next page.
.\" default value
.Pq Em PageDown
.It Ic prev_folder
Go to previous folder.
.\" default value
.Pq Em K
.It Ic next_folder
Go to next folder.
.\" default value
.Pq Em J
.It Ic prev_account
Go to previous account.
.\" default value
.Pq Em l
.It Ic next_account
Go to next account.
.\" default value
.Pq Em h
.It Ic new_mail
Start new mail draft in new tab
.\" default value
.Pq Em m
.It Ic search
Search within list of e-mails.
.\" default value
.Pq Em /
.It Ic toggle_menu_visibility
Toggle visibility of side menu in mail list.
.\" default value
.Pq Em `
.El
.Sy Em compact-listing
.Bl -tag -width 36n
.It Ic exit_thread
Exit thread view
.\" default value
.Pq Em i
.It Ic open_thread
Open thread.
.\" default value
.Pq Em Enter
.It Ic select_entry
Select thread entry.
.\" default value
.Pq Em v
.El
.Sy Em pager
.Bl -tag -width 36n
.It Ic scroll_up
Scroll up pager.
.\" default value
.Pq Em k
.It Ic scroll_down
Scroll down pager.
.\" default value
.Pq Em j
.It Ic page_up
Go to previous pager page
.\" default value
.Pq Em PageUp
.It Ic page_down
Go to next pager pag
.\" default value
.Pq Em PageDown
.El
.Sy Em contact-list
.Bl -tag -width 36n
.It Ic create_contact
Create new contact.
.\" default value
.Pq Em c
.It Ic edit_contact
Edit contact under cursor
.\" default value
.Pq Em e
.It Ic mail_contact
Mail contact under cursor
.\" default value
.Pq Em m
.It Ic toggle_menu_visibility
Toggle visibility of side menu in mail list.
.\" default value
.Pq Em `
.El
.Sy Em composing
.Bl -tag -width 36n
.It Ic send_mail
Deliver draft to mailer
.\" default value
.Pq Em s
.It Ic edit_mail
Edit mail.
.\" default value
.Pq Em e
.El
.Sy Em envelope-view
To "select" an attachment, type its index (you will see the typed result in the command buffer on your bottom right of the status line) and then issue the corresponding command.
.Bl -tag -width 36n
.It Ic add_addresses_to_contacts Ns
Select addresses from envelope to add to contacts.
.\" default value
.Pq Em c
.It Ic view_raw_source
View raw envelope source in a pager.
.\" default value
.Pq Em M-r
.It Ic reply
Reply to envelope.
.\" default value
.Pq Em R
.It Ic edit
Open envelope in composer.
.\" default value
.Pq Em e
.It Ic return_to_normal_view
Return to envelope if viewing raw source or attachment.
.\" default value
.Pq Em r
.It Ic open_attachment
Opens selected attachment with
.Cm xdg-open
.\" default value
.Pq Em a
.It Ic open_mailcap
Opens selected attachment according to its mailcap entry. See
.Xr meli.1 FILES
for the mailcap file locations.
.\" default value
.Pq Em m
.It Ic go_to_url
Go to url of given index
.\" default value
.Pq Em g
.It Ic toggle_url_mode
Toggles url open mode. When active, it prepends an index next to each url that you can select by typing and open by issuing
.Ic go_to_url
.\" default value
.Pq Em u
.It Ic toggle_expand_headers
Expand extra headers (References and others)
.\" default value
.Pq Em h
.El
.Sy Em thread-view
.Bl -tag -width 36n
.It Ic reverse_thread_order
Reverse thread order.
.\" default value
.Pq Em r
.It Ic toggle_mailview
Toggle mail view visibility.
.\" default value
.Pq Em p
.It Ic toggle_threadview
Toggle thread view visibility.
.\" default value
.Pq Em t
.It Ic collapse_subtree
Collapse thread branches.
.\" default value
.Pq Em h
.It Ic prev_page
Go to previous page.
.\" default value
.Pq Em PageUp
.It Ic next_page
Go to next page.
.\" default value
.Pq Em PageDown
.El
.Bl -tag -width 36n
.Sh NOTIFICATIONS
.Bl -tag -width 36n
.It Ic enable Ar boolean
enable freedesktop-spec notifications. this is usually what you want
.\" default value
.Pq Em true
.It Ic script Ar String
(optional) script to pass notifications to, with title as 1st arg and body as 2nd
.\" default value
.Pq Em none
.It Ic xbiff_file_path Ar String
(optional) file that gets its size updated when new mail arrives
.Pq Em none
.\" default value
.It Ic play_sound Ar boolean
(optional) play theme sound in notifications if possible
.Pq Em false
.\" default value
.It Ic sound_file Ar String
(optional) play sound file in notifications if possible
.\" default value
.Pq Em none
.El
.Sh PAGER
.Bl -tag -width 36n
.It Ic pager_context Ar num
(optional) number of context lines when going to next page. (Unimplemented)
.\" default value
.Pq Em 0
.It Ic headers_sticky Ar boolean
(optional) always show headers when scrolling.
.\" default value
.Pq Em true
.It Ic html_filter Ar String
(optional) pipe html attachments through this filter before display
.\" default value
.Pq Em none
.It Ic filter Ar String
(optional) a command to pipe mail output through for viewing in pager.
.\" default value
.Pq Em none
.It Ic format_flowed Ar bool
(optional) respect format=flowed
.\" default value
.Pq Em true
.It Ic split_long_lines Ar bool
(optional) Split long lines that would overflow on the x axis.
.\" default value
.Pq Em true
.It Ic minimum_width Ar num
(optional) Minimum text width in columns.
.\" default value
.Pq Em 80
.El
.Sh PGP
.Bl -tag -width 36n
.It Ic auto_verify_signatures Ar boolean
auto verify signed e-mail according to RFC3156
.\" default value
.Pq Em true
.It Ic auto_sign Ar boolean
(optional) always sign sent messages
.\" default value
.Pq Em false
.It Ic key Ar String
(optional) key to be used when signing/encrypting (not functional yet)
.\" default value
.Pq Em none
.It Ic gpg_binary Ar String
(optional) gpg binary name or file location to use
.\" default value
.Pq Em "gpg2"
.El
.Sh TERMINAL
.Bl -tag -width 36n
.It Ic theme Ar String
(optional) select between these themes: light / dark
.\" default value
.Pq Em dark
.It Ic ascii_drawing Ar boolean
(optional) if true, box drawing will be done with ascii characters.
.\" default value
.Pq Em false
.It Ic window_title Ar String
(optional) set window title in xterm compatible terminals (empty string means no window title is set)
.\" default value
.Pq Em "meli"
.El
.Sh SEE ALSO
.Xr meli 1
.Sh CONFORMING TO
TOML Standard v.0.5.0 https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.5.0.md
.Sh AUTHORS
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.)
.Pp
.Aq https://meli.delivery

View File

@ -1,89 +1,42 @@
[package]
name = "melib"
version = "0.7.2"
authors = ["Manos Pitsidianakis <epilys@nessuent.xyz>"]
version = "0.3.2"
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
workspace = ".."
edition = "2018"
build = "build.rs"
rust-version = "1.65.0"
homepage = "https://meli.delivery"
repository = "https://git.meli.delivery/meli/meli.git"
description = "mail library"
keywords = ["mail", "mua", "maildir", "imap", "jmap"]
categories = ["email", "parser-implementations"]
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 }
bitflags = "1.0"
data-encoding = { version = "2.1.1" }
encoding = { version = "0.2.33", default-features = false }
encoding_rs = { version = "^0.8" }
flate2 = { version = "1.0.16", optional = true }
futures = "0.3.5"
indexmap = { version = "^1.5", default-features = false, features = ["serde-1", ] }
isahc = { version = "^1.7.2", optional = true, default-features = false, features = ["http2", "json", "text-decoding"] }
libc = { version = "0.2.125", features = ["extra_traits",] }
libloading = "^0.7"
log = { version = "0.4", features = ["std"] }
native-tls = { version = "0.2.3", default-features = false, optional = true }
nix = "^0.24"
nom = { version = "7" }
notify = { version = "4.0.15", optional = true }
regex = { version = "1" }
rusqlite = { version = "^0.28", default-features = false, optional = true }
chrono = { version = "0.4", features = ["serde"] }
crossbeam = "0.7.2"
data-encoding = "2.1.1"
encoding = "0.2.33"
fnv = "1.0.3"
memmap = { version = "0.5.2", optional = true }
nom = "3.2.0"
notify = { version = "4.0.1", optional = true }
notify-rust = { version = "^3", optional = true }
termion = "1.5.1"
xdg = "2.1.0"
native-tls = { version ="0.2", optional=true }
serde = { version = "1.0.71", features = ["rc", ] }
serde_derive = "1.0.71"
serde_json = { version = "1.0", features = ["raw_value",] }
smallvec = { version = "^1.5.0", features = ["serde", ] }
smol = "1.0.0"
unicode-segmentation = { version = "1.2.1", default-features = false, optional = true }
uuid = { version = "^1", features = ["serde", "v4", "v5"] }
xdg = "2.1.0"
xdg-utils = "^0.4.0"
[dependencies.imap-codec]
version = "0.10.0"
features = [
"ext_condstore_qresync",
"ext_enable",
"ext_idle",
"ext_literal",
"ext_move",
"ext_sasl_ir",
"ext_unselect"
]
optional = true
[dev-dependencies]
mailin-embedded = { version = "0.7", features = ["rtls"] }
stderrlog = "^0.5"
bincode = "1.2.0"
uuid = { version = "0.7.4", features = ["serde", "v4"] }
text_processing = { path = "../text_processing", version = "*", optional= true }
libc = {version = "0.2.59", features = ["extra_traits",]}
reqwest = { version ="0.10.0-alpha.2", optional=true, features = ["json", "blocking" ]}
serde_json = { version = "1.0", optional = true, features = ["raw_value",] }
[features]
default = ["unicode_algorithms", "imap_backend", "maildir_backend", "mbox_backend", "vcard", "sqlite3", "smtp", "deflate_compression"]
default = ["unicode_algorithms", "imap_backend", "maildir_backend", "mbox_backend", "jmap_backend", "vcard"]
debug-tracing = []
deflate_compression = ["flate2", "imap-codec/ext_compress"]
gpgme = []
http = ["isahc"]
http-static = ["isahc", "isahc/static-curl"]
imap_backend = ["imap-codec", "tls"]
jmap_backend = ["http"]
maildir_backend = ["notify"]
mbox_backend = ["notify"]
unicode_algorithms = ["text_processing"]
imap_backend = ["native-tls"]
maildir_backend = ["notify", "notify-rust", "memmap"]
mbox_backend = ["notify", "notify-rust", "memmap"]
notmuch_backend = []
smtp = ["tls", "base64"]
sqlite3 = ["rusqlite", ]
tls = ["native-tls"]
unicode_algorithms = ["unicode-segmentation"]
jmap_backend = ["reqwest", "serde_json" ]
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

@ -1,430 +1,6 @@
/*
* meli - melib 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/>.
*/
#![allow(clippy::needless_range_loop)]
#[cfg(feature = "unicode_algorithms")]
include!("src/text_processing/types.rs");
fn main() -> Result<(), std::io::Error> {
#[cfg(feature = "unicode_algorithms")]
fn main() {
#[cfg(feature = "notmuch_backend")]
{
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,
io::{prelude::*, BufReader},
path::Path,
process::{Command, Stdio},
};
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);
if mod_path.exists() {
eprintln!(
"{} already exists, delete it if you want to replace it.",
mod_path.display()
);
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 buf_reader = BufReader::new(child.stdout.take().unwrap());
let mut line_break_table: Vec<(u32, u32, LineBreakClass)> = Vec::with_capacity(3800);
for line in buf_reader.lines() {
let line = line.unwrap();
if line.starts_with('#') || line.starts_with(' ') || line.is_empty() {
continue;
}
let tokens: &str = line.split_whitespace().next().unwrap();
let semicolon_idx: usize = tokens.chars().position(|c| c == ';').unwrap();
/* LineBreak.txt list is ascii encoded so we can assume each char takes one
* byte: */
let chars_str: &str = &tokens[..semicolon_idx];
let mut codepoint_iter = chars_str.split("..");
let first_codepoint: u32 =
u32::from_str_radix(codepoint_iter.next().unwrap(), 16).unwrap();
let sec_codepoint: u32 = codepoint_iter
.next()
.map(|v| u32::from_str_radix(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.split_whitespace().next().unwrap();
if v.starts_with('E') {
v = &v[1..];
}
if v.as_bytes()
.first()
.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();
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();
}
println!("cargo:rustc-link-lib=notmuch");
}
Ok(())
}

View File

@ -22,16 +22,11 @@
#[cfg(feature = "vcard")]
pub mod vcard;
pub mod mutt;
use std::{collections::HashMap, ops::Deref};
use chrono::{DateTime, Local};
use fnv::FnvHashMap;
use uuid::Uuid;
use crate::utils::{
datetime::{self, UnixTimestamp},
parsec::Parser,
};
use std::ops::Deref;
#[derive(Hash, Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
#[serde(from = "String")]
@ -41,50 +36,35 @@ pub enum CardId {
Hash(u64),
}
impl std::fmt::Display for CardId {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
impl Into<String> for CardId {
fn into(self) -> String {
match self {
Self::Uuid(u) => u.as_hyphenated().fmt(fmt),
Self::Hash(u) => u.fmt(fmt),
CardId::Uuid(u) => u.to_string(),
CardId::Hash(u) => u.to_string(),
}
}
}
impl From<CardId> for String {
fn from(val: CardId) -> Self {
val.to_string()
}
}
impl From<String> for CardId {
fn from(s: String) -> Self {
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
str::FromStr,
};
if let Ok(u) = Uuid::parse_str(s.as_str()) {
Self::Uuid(u)
} else if let Ok(num) = u64::from_str(s.trim()) {
Self::Hash(num)
fn from(s: String) -> CardId {
if let Ok(u) = uuid::Uuid::parse_str(s.as_str()) {
CardId::Uuid(u)
} else {
let mut hasher = DefaultHasher::default();
s.hash(&mut hasher);
Self::Hash(hasher.finish())
use std::str::FromStr;
CardId::Hash(u64::from_str(&s).unwrap())
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct AddressBook {
display_name: String,
created: UnixTimestamp,
last_edited: UnixTimestamp,
pub cards: HashMap<CardId, Card>,
created: DateTime<Local>,
last_edited: DateTime<Local>,
pub cards: FnvHashMap<CardId, Card>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct Card {
id: CardId,
title: String,
@ -93,69 +73,43 @@ pub struct Card {
name_prefix: String,
name_suffix: String,
//address
birthday: Option<UnixTimestamp>,
birthday: Option<DateTime<Local>>,
email: String,
url: String,
key: String,
color: u8,
last_edited: UnixTimestamp,
extra_properties: HashMap<String, String>,
last_edited: DateTime<Local>,
extra_properties: FnvHashMap<String, String>,
/// If true, we can't make any changes because we do not manage this
/// resource.
/// If true, we can't make any changes because we do not manage this resource.
external_resource: bool,
}
impl AddressBook {
pub fn new(display_name: String) -> Self {
Self {
pub fn new(display_name: String) -> AddressBook {
AddressBook {
display_name,
created: datetime::now(),
last_edited: datetime::now(),
cards: HashMap::default(),
created: Local::now(),
last_edited: Local::now(),
cards: FnvHashMap::default(),
}
}
pub fn with_account(s: &crate::conf::AccountSettings) -> Self {
let mut ret = Self::new(s.name.clone());
if let Some(mutt_alias_file) = s.extra.get("mutt_alias_file").map(String::as_str) {
match std::fs::read_to_string(std::path::Path::new(mutt_alias_file))
.map_err(|err| err.to_string())
.and_then(|contents| {
contents
.lines()
.map(|line| mutt::parse_mutt_contact().parse(line).map(|(_, c)| c))
.collect::<Result<Vec<Card>, &str>>()
.map_err(|err| err.to_string())
}) {
Ok(cards) => {
for c in cards {
ret.add_card(c);
}
}
Err(err) => {
log::warn!(
"Could not load mutt alias file {:?}: {}",
mutt_alias_file,
err
);
}
}
}
pub fn with_account(s: &crate::conf::AccountSettings) -> AddressBook {
let mut ret = AddressBook::new(s.name.clone());
#[cfg(feature = "vcard")]
if let Some(vcard_path) = s.vcard_folder() {
match vcard::load_cards(std::path::Path::new(vcard_path)) {
Ok(cards) => {
{
if let Some(vcard_path) = s.vcard_folder() {
if let Ok(cards) = vcard::load_cards(&std::path::Path::new(vcard_path)) {
for c in cards {
ret.add_card(c);
}
}
Err(err) => {
log::warn!("Could not load vcards from {:?}: {}", vcard_path, err);
}
}
}
ret
}
@ -178,16 +132,16 @@ impl AddressBook {
}
impl Deref for AddressBook {
type Target = HashMap<CardId, Card>;
type Target = FnvHashMap<CardId, Card>;
fn deref(&self) -> &HashMap<CardId, Card> {
fn deref(&self) -> &FnvHashMap<CardId, Card> {
&self.cards
}
}
impl Card {
pub fn new() -> Self {
Self {
pub fn new() -> Card {
Card {
id: CardId::Uuid(Uuid::new_v4()),
title: String::new(),
name: String::new(),
@ -200,9 +154,9 @@ impl Card {
url: String::new(),
key: String::new(),
last_edited: datetime::now(),
last_edited: Local::now(),
external_resource: false,
extra_properties: HashMap::default(),
extra_properties: FnvHashMap::default(),
color: 0,
}
}
@ -236,70 +190,51 @@ impl Card {
self.key.as_str()
}
pub fn last_edited(&self) -> String {
datetime::timestamp_to_string(self.last_edited, None, false)
self.last_edited.to_rfc2822()
}
pub fn set_id(&mut self, new_val: CardId) -> &mut Self {
pub fn set_id(&mut self, new_val: CardId) {
self.id = new_val;
self
}
pub fn set_title(&mut self, new: String) -> &mut Self {
pub fn set_title(&mut self, new: String) {
self.title = new;
self
}
pub fn set_name(&mut self, new: String) -> &mut Self {
pub fn set_name(&mut self, new: String) {
self.name = new;
self
}
pub fn set_additionalname(&mut self, new: String) -> &mut Self {
pub fn set_additionalname(&mut self, new: String) {
self.additionalname = new;
self
}
pub fn set_name_prefix(&mut self, new: String) -> &mut Self {
pub fn set_name_prefix(&mut self, new: String) {
self.name_prefix = new;
self
}
pub fn set_name_suffix(&mut self, new: String) -> &mut Self {
pub fn set_name_suffix(&mut self, new: String) {
self.name_suffix = new;
self
}
pub fn set_email(&mut self, new: String) -> &mut Self {
pub fn set_email(&mut self, new: String) {
self.email = new;
self
}
pub fn set_url(&mut self, new: String) -> &mut Self {
pub fn set_url(&mut self, new: String) {
self.url = new;
self
}
pub fn set_key(&mut self, new: String) -> &mut Self {
pub fn set_key(&mut self, new: String) {
self.key = new;
self
}
pub fn set_extra_property(&mut self, key: &str, value: String) -> &mut Self {
pub fn set_extra_property(&mut self, key: &str, value: String) {
self.extra_properties.insert(key.to_string(), value);
self
}
pub fn extra_property(&self, key: &str) -> Option<&str> {
self.extra_properties.get(key).map(String::as_str)
}
pub fn extra_properties(&self) -> &HashMap<String, String> {
pub fn extra_properties(&self) -> &FnvHashMap<String, String> {
&self.extra_properties
}
pub fn set_external_resource(&mut self, new_val: bool) -> &mut Self {
pub fn set_external_resource(&mut self, new_val: bool) {
self.external_resource = new_val;
self
}
pub fn external_resource(&self) -> bool {
@ -307,9 +242,9 @@ impl Card {
}
}
impl From<HashMap<String, String>> for Card {
fn from(mut map: HashMap<String, String>) -> Self {
let mut card = Self::new();
impl From<FnvHashMap<String, String>> for Card {
fn from(mut map: FnvHashMap<String, String>) -> Card {
let mut card = Card::new();
if let Some(val) = map.remove("TITLE") {
card.title = val;
}

View File

@ -1,106 +0,0 @@
/*
* meli - addressbook module
*
* Copyright 2019 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
//! # Mutt contact formats
use std::collections::VecDeque;
use super::*;
use crate::utils::parsec::{is_not, map_res, match_literal_anycase, prefix, Parser};
//alias <nickname> [ <long name> ] <address>
// From mutt doc:
//
// ```text
// Since the name can consist of several whitespace-separated words, the
// last word is considered the address, and it can be optionally enclosed
// between angle brackets.
// For example: alias mumon My dear pupil Mumon foobar@example.com
// will be parsed in this way:
//
// alias mumon My dear pupil Mumon foobar@example.com
// ^ ^ ^
// nickname long name email address
// The nickname (or alias) will be used to select a corresponding long name
// and email address when specifying the To field of an outgoing message,
// e.g. when using the function in the browser or index context.
// The long name is optional, so you can specify an alias command in this
// way:
//
// alias mumon foobar@example.com
// ^ ^
// nickname email address
// ```
pub fn parse_mutt_contact<'a>() -> impl Parser<'a, Card> {
move |input| {
map_res(
prefix(match_literal_anycase("alias "), is_not(b"\r\n")),
|l| {
let mut tokens = l.split_whitespace().collect::<VecDeque<&str>>();
let mut ret = Card::new();
let title = tokens.pop_front().ok_or(l)?.to_string();
let mut email = tokens.pop_back().ok_or(l)?.to_string();
if email.starts_with('<') && email.ends_with('>') {
email.pop();
email.remove(0);
}
let mut name = tokens.into_iter().fold(String::new(), |mut acc, el| {
acc.push_str(el);
acc.push(' ');
acc
});
name.pop();
if name.trim().is_empty() {
name = title.clone();
}
ret.set_title(title).set_email(email).set_name(name);
Ok::<Card, &'a str>(ret)
},
)
.parse(input)
}
}
#[test]
fn test_mutt_contacts() {
let a = "alias mumon My dear pupil Mumon foobar@example.com";
let b = "alias mumon foobar@example.com";
let c = "alias <nickname> <long name> <address>";
let (other, a_card) = parse_mutt_contact().parse(a).unwrap();
assert!(other.is_empty());
assert_eq!(a_card.name(), "My dear pupil Mumon");
assert_eq!(a_card.title(), "mumon");
assert_eq!(a_card.email(), "foobar@example.com");
let (other, b_card) = parse_mutt_contact().parse(b).unwrap();
assert!(other.is_empty());
assert_eq!(b_card.name(), "mumon");
assert_eq!(b_card.title(), "mumon");
assert_eq!(b_card.email(), "foobar@example.com");
let (other, c_card) = parse_mutt_contact().parse(c).unwrap();
assert!(other.is_empty());
assert_eq!(c_card.name(), "<long name>");
assert_eq!(c_card.title(), "<nickname>");
assert_eq!(c_card.email(), "address");
}

View File

@ -19,21 +19,13 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
//! # vCard format
//!
//! This module implements the standards:
//!
//! - Version 3 (read-only) [RFC 2426: vCard MIME Directory Profile](https://datatracker.ietf.org/doc/2426)
//! - Version 4 [RFC 6350: vCard Format Specification](https://datatracker.ietf.org/doc/rfc6350/)
//! - Parameter escaping [RFC 6868 Parameter Value Encoding in iCalendar and vCard](https://datatracker.ietf.org/doc/rfc6868/)
use std::{collections::HashMap, convert::TryInto};
/// Convert VCard strings to meli Cards (contacts).
use super::*;
use crate::{
error::{Error, Result},
utils::parsec::{match_literal_anycase, one_or_more, peek, prefix, take_until, Parser},
};
use crate::chrono::TimeZone;
use crate::error::{MeliError, Result};
use crate::parsec::{match_literal_anycase, one_or_more, peek, prefix, take_until, Parser};
use fnv::FnvHashMap;
use std::convert::TryInto;
/* Supported vcard versions */
pub trait VCardVersion: core::fmt::Debug {}
@ -42,35 +34,31 @@ 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: &'static str = "BEGIN:VCARD\r\n"; //VERSION:4.0\r\n";
static FOOTER: &'static str = "END:VCARD\r\n";
#[derive(Debug)]
pub struct VCard<T: VCardVersion>(
HashMap<String, ContentLine>,
fnv::FnvHashMap<String, ContentLine>,
std::marker::PhantomData<*const T>,
);
impl<V: VCardVersion> VCard<V> {
pub fn new_v4() -> VCard<impl VCardVersion> {
VCard(
HashMap::default(),
FnvHashMap::default(),
std::marker::PhantomData::<*const VCardVersion4>,
)
}
@ -84,22 +72,14 @@ pub struct ContentLine {
}
impl CardDeserializer {
pub fn try_from_str(mut input: &str) -> Result<VCard<impl VCardVersion>> {
input = if (!input.starts_with(HEADER_CRLF) || !input.ends_with(FOOTER_CRLF))
&& (!input.starts_with(HEADER_LF) || !input.ends_with(FOOTER_LF))
{
return Err(Error::new(format!(
"Error while parsing vcard: input does not start or end with correct header and \
footer. input is:\n{:?}",
input
)));
} else if input.starts_with(HEADER_CRLF) {
&input[HEADER_CRLF.len()..input.len() - FOOTER_CRLF.len()]
pub fn from_str(mut input: &str) -> Result<VCard<impl VCardVersion>> {
input = if !input.starts_with(HEADER) || !input.ends_with(FOOTER) {
return Err(MeliError::new(format!("Error while parsing vcard: input does not start or end with correct header and footer. input is:\n{:?}", input)));
} else {
&input[HEADER_LF.len()..input.len() - FOOTER_LF.len()]
&input[HEADER.len()..input.len() - FOOTER.len()]
};
let mut ret = HashMap::default();
let mut ret = FnvHashMap::default();
enum Stage {
Group,
@ -153,13 +133,13 @@ impl CardDeserializer {
}
}
if !has_colon {
return Err(Error::new(format!(
return Err(MeliError::new(format!(
"Error while parsing vcard: error at line {}, no colon. {:?}",
l, el
)));
}
if name.is_empty() {
return Err(Error::new(format!(
return Err(MeliError::new(format!(
"Error while parsing vcard: error at line {}, no name for content line. {:?}",
l, el
)));
@ -172,7 +152,7 @@ impl CardDeserializer {
}
impl<V: VCardVersion> TryInto<Card> for VCard<V> {
type Error = crate::error::Error;
type Error = crate::error::MeliError;
fn try_into(mut self) -> crate::error::Result<Card> {
let mut card = Card::new();
@ -193,7 +173,7 @@ impl<V: VCardVersion> TryInto<Card> for VCard<V> {
if let Some(val) = self.0.remove("FN") {
card.set_name(val.value);
} else {
return Err(Error::new("FN entry missing in VCard."));
return Err(MeliError::new("FN entry missing in VCard."));
}
if let Some(val) = self.0.remove("NICKNAME") {
card.set_additionalname(val.value);
@ -222,9 +202,7 @@ impl<V: VCardVersion> TryInto<Card> for VCard<V> {
T102200Z
T102200-0800
*/
card.birthday =
crate::utils::datetime::timestamp_from_string(val.value.as_str(), "%Y%m%d\0")
.unwrap_or_default();
card.birthday = chrono::Local.datetime_from_str(&val.value, "%Y%m%d").ok();
}
if let Some(val) = self.0.remove("EMAIL") {
card.set_email(val.value);
@ -270,7 +248,7 @@ fn test_load_cards() {
for s in parse_card().parse(contents.as_str()).unwrap().1 {
println!("");
println!("{}", s);
println!("{:?}", CardDeserializer::try_from_str(s));
println!("{:?}", CardDeserializer::from_str(s));
println!("");
}
*/
@ -291,22 +269,17 @@ pub fn load_cards(p: &std::path::Path) -> Result<Vec<Card>> {
use std::io::Read;
contents.clear();
std::fs::File::open(&f)?.read_to_string(&mut contents)?;
match parse_card().parse(contents.as_str()) {
Ok((_, c)) => {
for s in c {
ret.push(
CardDeserializer::try_from_str(s)
.and_then(TryInto::try_into)
.map(|mut card| {
Card::set_external_resource(&mut card, true);
is_any_valid = true;
card
}),
);
}
}
Err(err) => {
log::warn!("Could not parse vcard from {}: {}", f.display(), err);
if let Ok((_, c)) = parse_card().parse(contents.as_str()) {
for s in c {
ret.push(
CardDeserializer::from_str(s)
.and_then(TryInto::try_into)
.and_then(|mut card| {
Card::set_external_resource(&mut card, true);
is_any_valid = true;
Ok(card)
}),
);
}
}
}
@ -317,22 +290,16 @@ pub fn load_cards(p: &std::path::Path) -> Result<Vec<Card>> {
debug!(&c);
}
}
if is_any_valid {
if !is_any_valid {
ret.into_iter().collect::<Result<Vec<Card>>>()
} else {
ret.retain(Result::is_ok);
ret.into_iter().collect::<Result<Vec<Card>>>()
}
ret.into_iter().collect::<Result<Vec<Card>>>()
}
#[test]
fn test_card() {
let j = "BEGIN:VCARD\r\nVERSION:4.0\r\nN:Gump;Forrest;;Mr.;\r\nFN:Forrest Gump\r\nORG:Bubba Gump Shrimp Co.\r\nTITLE:Shrimp Man\r\nPHOTO;MEDIATYPE=image/gif:http://www.example.com/dir_photos/my_photo.gif\r\nTEL;TYPE=work,voice;VALUE=uri:tel:+1-111-555-1212\r\nTEL;TYPE=home,voice;VALUE=uri:tel:+1-404-555-1212\r\nADR;TYPE=WORK;PREF=1;LABEL=\"100 Waters Edge\\nBaytown\\, LA 30314\\nUnited States of America\":;;100 Waters Edge;Baytown;LA;30314;United States of America\r\nADR;TYPE=HOME;LABEL=\"42 Plantation St.\\nBaytown\\, LA 30314\\nUnited States of America\":;;42 Plantation St.;Baytown;LA;30314;United States of America\r\nEMAIL:forrestgump@example.com\r\nREV:20080424T195243Z\r\nx-qq:21588891\r\nEND:VCARD\r\n";
println!(
"results = {:#?}",
CardDeserializer::try_from_str(j).unwrap()
);
let j = "BEGIN:VCARD\nVERSION:4.0\nN:Gump;Forrest;;Mr.;\nFN:Forrest Gump\nORG:Bubba Gump Shrimp Co.\nTITLE:Shrimp Man\nPHOTO;MEDIATYPE=image/gif:http://www.example.com/dir_photos/my_photo.gif\nTEL;TYPE=work,voice;VALUE=uri:tel:+1-111-555-1212\nTEL;TYPE=home,voice;VALUE=uri:tel:+1-404-555-1212\nADR;TYPE=WORK;PREF=1;LABEL=\"100 Waters Edge\\nBaytown\\, LA 30314\\nUnited States of America\":;;100 Waters Edge;Baytown;LA;30314;United States of America\nADR;TYPE=HOME;LABEL=\"42 Plantation St.\\nBaytown\\, LA 30314\\nUnited States of America\":;;42 Plantation St.;Baytown;LA;30314;United States of America\nEMAIL:forrestgump@example.com\nREV:20080424T195243Z\nx-qq:21588891\nEND:VCARD\n";
println!(
"results = {:#?}",
CardDeserializer::try_from_str(j).unwrap()
);
println!("results = {:#?}", CardDeserializer::from_str(j).unwrap());
}

View File

@ -0,0 +1,261 @@
/*
* meli - async module
*
* Copyright 2017 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/>.
*/
/*!
* Primitive Async/Wait implementation.
*
* To create an Async promise, create an AsyncBuilder. Ask for its channel receiver/sender with
* `tx` and `rx` methods to pass them in your worker's closure. Build an `Async<T>` with your
* `JoinHandle<T>`. The thread must communicate with the `Async<T>` object via `AsyncStatus`
* messages.
*
* When `Async<T>` receives `AsyncStatus::Finished` it joins the thread and takes its value which
* can be extracted with `extract`.
*/
use crossbeam::{
bounded,
channel::{Receiver, Sender},
select,
};
use std::fmt;
use std::sync::Arc;
#[derive(Clone, Debug)]
pub struct WorkContext {
pub new_work: Sender<Work>,
pub set_name: Sender<(std::thread::ThreadId, String)>,
pub set_status: Sender<(std::thread::ThreadId, String)>,
pub finished: Sender<std::thread::ThreadId>,
}
#[derive(Clone)]
pub struct Work {
priority: u64,
pub is_static: bool,
pub closure: Arc<Box<dyn Fn(WorkContext) -> () + Send + Sync>>,
name: String,
status: String,
}
impl Ord for Work {
fn cmp(&self, other: &Work) -> std::cmp::Ordering {
self.priority.cmp(&other.priority)
}
}
impl PartialOrd for Work {
fn partial_cmp(&self, other: &Work) -> Option<std::cmp::Ordering> {
Some(self.priority.cmp(&other.priority))
}
}
impl PartialEq for Work {
fn eq(&self, other: &Work) -> bool {
self.priority == other.priority
}
}
impl Eq for Work {}
impl Work {
pub fn compute(&self, work_context: WorkContext) {
(self.closure)(work_context);
}
}
impl fmt::Debug for Work {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Work object")
}
}
/// Messages to pass between `Async<T>` owner and its worker thread.
#[derive(Clone)]
pub enum AsyncStatus<T> {
NoUpdate,
Payload(T),
Finished,
///The number may hold whatever meaning the user chooses.
ProgressReport(usize),
}
impl<T> fmt::Debug for AsyncStatus<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AsyncStatus::NoUpdate => write!(f, "AsyncStatus<T>::NoUpdate"),
AsyncStatus::Payload(_) => write!(f, "AsyncStatus<T>::Payload(_)"),
AsyncStatus::Finished => write!(f, "AsyncStatus<T>::Finished"),
AsyncStatus::ProgressReport(u) => write!(f, "AsyncStatus<T>::ProgressReport({})", u),
}
}
}
/// A builder object for `Async<T>`
#[derive(Debug, Clone)]
pub struct AsyncBuilder<T: Send + Sync> {
tx: Sender<AsyncStatus<T>>,
rx: Receiver<AsyncStatus<T>>,
priority: u64,
is_static: bool,
}
#[derive(Clone, Debug)]
pub struct Async<T: Send + Sync> {
work: Work,
active: bool,
tx: Sender<AsyncStatus<T>>,
rx: Receiver<AsyncStatus<T>>,
}
impl<T: Send + Sync> Default for AsyncBuilder<T> {
fn default() -> Self {
AsyncBuilder::<T>::new()
}
}
impl<T> AsyncBuilder<T>
where
T: Send + Sync,
{
pub fn new() -> Self {
let (sender, receiver) = bounded(8 * ::std::mem::size_of::<AsyncStatus<T>>());
AsyncBuilder {
tx: sender,
rx: receiver,
priority: 0,
is_static: false,
}
}
/// Returns the sender object of the promise's channel.
pub fn tx(&mut self) -> Sender<AsyncStatus<T>> {
self.tx.clone()
}
/// Returns the receiver object of the promise's channel.
pub fn rx(&mut self) -> Receiver<AsyncStatus<T>> {
self.rx.clone()
}
pub fn set_priority(&mut self, new_val: u64) -> &mut Self {
self.priority = new_val;
self
}
pub fn set_is_static(&mut self, new_val: bool) -> &mut Self {
self.is_static = new_val;
self
}
/// Returns an `Async<T>` object that contains a `Thread` join handle that returns a `T`
pub fn build(self, work: Box<dyn Fn(WorkContext) -> () + Send + Sync>) -> Async<T> {
Async {
work: Work {
priority: self.priority,
is_static: self.is_static,
closure: Arc::new(work),
name: String::new(),
status: String::new(),
},
tx: self.tx,
rx: self.rx,
active: false,
}
}
}
impl<T> Async<T>
where
T: Send + Sync,
{
pub fn work(&mut self) -> Option<Work> {
if !self.active {
self.active = true;
Some(self.work.clone())
} else {
None
}
}
/// Returns the sender object of the promise's channel.
pub fn tx(&mut self) -> Sender<AsyncStatus<T>> {
self.tx.clone()
}
/// Returns the receiver object of the promise's channel.
pub fn rx(&mut self) -> Receiver<AsyncStatus<T>> {
self.rx.clone()
}
/// Polls worker thread and returns result.
pub fn poll_block(&mut self) -> Result<AsyncStatus<T>, ()> {
if !self.active {
return Ok(AsyncStatus::Finished);
}
let rx = &self.rx;
select! {
recv(rx) -> r => {
match r {
Ok(p @ AsyncStatus::Payload(_)) => {
return Ok(p);
},
Ok(f @ AsyncStatus::Finished) => {
self.active = false;
return Ok(f);
},
Ok(a) => {
return Ok(a);
}
Err(_) => {
return Err(());
},
}
},
};
}
/// Polls worker thread and returns result.
pub fn poll(&mut self) -> Result<AsyncStatus<T>, ()> {
if !self.active {
return Ok(AsyncStatus::Finished);
}
let rx = &self.rx;
select! {
default => {
return Ok(AsyncStatus::NoUpdate);
},
recv(rx) -> r => {
match r {
Ok(p @ AsyncStatus::Payload(_)) => {
return Ok(p);
},
Ok(f @ AsyncStatus::Finished) => {
self.active = false;
return Ok(f);
},
Ok(a) => {
return Ok(a);
}
Err(_) => {
return Err(());
},
}
},
};
}
}

View File

@ -18,148 +18,77 @@
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
pub mod utf7;
use smallvec::SmallVec;
#[cfg(feature = "imap_backend")]
pub mod imap;
#[cfg(feature = "imap_backend")]
pub mod nntp;
#[cfg(feature = "maildir_backend")]
pub mod maildir;
#[cfg(feature = "mbox_backend")]
pub mod mbox;
#[cfg(feature = "notmuch_backend")]
pub mod notmuch;
#[cfg(feature = "notmuch_backend")]
pub use self::notmuch::NotmuchDb;
#[cfg(feature = "jmap_backend")]
pub mod jmap;
#[cfg(feature = "maildir_backend")]
pub mod maildir;
#[cfg(feature = "mbox_backend")]
pub mod mbox;
use std::{
any::Any,
borrow::Cow,
collections::{BTreeSet, HashMap},
fmt,
fmt::Debug,
future::Future,
ops::Deref,
pin::Pin,
sync::{Arc, RwLock},
};
use futures::stream::Stream;
#[cfg(feature = "jmap_backend")]
pub use self::jmap::JmapType;
#[cfg(feature = "imap_backend")]
pub use self::imap::ImapType;
use crate::async_workers::*;
use crate::conf::AccountSettings;
use crate::error::{MeliError, Result};
#[cfg(feature = "maildir_backend")]
use self::maildir::MaildirType;
#[cfg(feature = "mbox_backend")]
use self::mbox::MboxType;
#[cfg(feature = "imap_backend")]
pub use self::nntp::NntpType;
use super::email::{Envelope, EnvelopeHash, Flag};
use crate::{
conf::AccountSettings,
error::{Error, ErrorKind, Result},
LogLevel,
};
use std::any::Any;
use std::collections::BTreeMap;
use std::fmt;
use std::fmt::Debug;
use std::ops::Deref;
use std::sync::{Arc, RwLock};
#[macro_export]
macro_rules! get_path_hash {
($path:expr) => {{
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
let mut hasher = DefaultHasher::new();
$path.hash(&mut hasher);
hasher.finish()
}};
}
use fnv::FnvHashMap;
use std;
pub type BackendCreator = Box<
dyn Fn(
&AccountSettings,
Box<dyn Fn(&str) -> bool + Send + Sync>,
BackendEventConsumer,
) -> Result<Box<dyn MailBackend>>,
>;
pub type BackendValidateConfigFn = Box<dyn Fn(&mut AccountSettings) -> Result<()>>;
/// A hashmap containing all available mail backends.
/// An abstraction over any available backends.
pub struct Backends {
map: HashMap<std::string::String, Backend>,
map: FnvHashMap<std::string::String, Backend>,
}
pub struct Backend {
pub create_fn: Box<dyn Fn() -> BackendCreator>,
pub validate_conf_fn: BackendValidateConfigFn,
pub validate_conf_fn: Box<dyn Fn(&AccountSettings) -> Result<()>>,
}
impl Default for Backends {
fn default() -> Self {
Self::new()
Backends::new()
}
}
#[cfg(feature = "notmuch_backend")]
pub const NOTMUCH_ERROR_MSG: &str = "libnotmuch5 was not found in your system. Make sure it is \
installed and in the library paths. For a custom file path, \
use `library_file_path` setting in your notmuch account.\n";
#[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 = Self {
map: HashMap::with_capacity_and_hasher(1, Default::default()),
let mut b = Backends {
map: FnvHashMap::with_capacity_and_hasher(1, Default::default()),
};
#[cfg(feature = "maildir_backend")]
{
b.register(
"maildir".to_string(),
Backend {
create_fn: Box::new(|| Box::new(|f, i, ev| MaildirType::new(f, i, ev))),
create_fn: Box::new(|| Box::new(|f, i| MaildirType::new(f, i))),
validate_conf_fn: Box::new(MaildirType::validate_config),
},
);
@ -169,7 +98,7 @@ impl Backends {
b.register(
"mbox".to_string(),
Backend {
create_fn: Box::new(|| Box::new(|f, i, ev| MboxType::new(f, i, ev))),
create_fn: Box::new(|| Box::new(|f, i| MboxType::new(f, i))),
validate_conf_fn: Box::new(MboxType::validate_config),
},
);
@ -179,15 +108,8 @@ impl Backends {
b.register(
"imap".to_string(),
Backend {
create_fn: Box::new(|| Box::new(|f, i, ev| imap::ImapType::new(f, i, ev))),
validate_conf_fn: Box::new(imap::ImapType::validate_config),
},
);
b.register(
"nntp".to_string(),
Backend {
create_fn: Box::new(|| Box::new(|f, i, ev| nntp::NntpType::new(f, i, ev))),
validate_conf_fn: Box::new(nntp::NntpType::validate_config),
create_fn: Box::new(|| Box::new(|f, i| ImapType::new(f, i))),
validate_conf_fn: Box::new(ImapType::validate_config),
},
);
}
@ -196,7 +118,7 @@ impl Backends {
b.register(
"notmuch".to_string(),
Backend {
create_fn: Box::new(|| Box::new(|f, i, ev| NotmuchDb::new(f, i, ev))),
create_fn: Box::new(|| Box::new(|f, i| NotmuchDb::new(f, i))),
validate_conf_fn: Box::new(NotmuchDb::validate_config),
},
);
@ -206,8 +128,8 @@ impl Backends {
b.register(
"jmap".to_string(),
Backend {
create_fn: Box::new(|| Box::new(|f, i, ev| jmap::JmapType::new(f, i, ev))),
validate_conf_fn: Box::new(jmap::JmapType::validate_config),
create_fn: Box::new(|| Box::new(|f, i| JmapType::new(f, i))),
validate_conf_fn: Box::new(JmapType::validate_config),
},
);
}
@ -216,13 +138,6 @@ impl Backends {
pub fn get(&self, key: &str) -> BackendCreator {
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);
}
(self.map[key].create_fn)()
@ -235,236 +150,130 @@ impl Backends {
self.map.insert(key, backend);
}
pub fn validate_config(&self, key: &str, s: &mut AccountSettings) -> Result<()> {
pub fn validate_config(&self, key: &str, s: &AccountSettings) -> Result<()> {
(self
.map
.get(key)
.ok_or_else(|| {
Error::new(format!(
"{}{} is not a valid mail backend. {}",
if key == "notmuch" {
NOTMUCH_ERROR_MSG
} else {
""
},
key,
if cfg!(feature = "notmuch_backend") && key == "notmuch" {
NOTMUCH_ERROR_DETAILS
} else {
""
},
))
})?
.ok_or_else(|| MeliError::new(format!("{} is not a valid mail backend", key)))?
.validate_conf_fn)(s)
}
}
#[derive(Debug, Clone)]
pub enum BackendEvent {
Notice {
description: String,
content: Option<String>,
level: LogLevel,
},
Refresh(RefreshEvent),
AccountStateChange {
message: Cow<'static, str>,
},
//Job(Box<Future<Output = Result<()>> + Send + 'static>)
}
impl From<Error> for BackendEvent {
fn from(val: Error) -> Self {
Self::Notice {
description: val.summary.to_string(),
content: Some(val.to_string()),
level: LogLevel::ERROR,
}
}
}
#[derive(Debug, Clone)]
#[derive(Debug)]
pub enum RefreshEventKind {
Update(EnvelopeHash, Box<Envelope>),
/// Rename(old_hash, new_hash)
Rename(EnvelopeHash, EnvelopeHash),
Create(Box<Envelope>),
Remove(EnvelopeHash),
NewFlags(EnvelopeHash, (Flag, Vec<String>)),
Rescan,
Failure(Error),
MailboxCreate(Mailbox),
MailboxDelete(MailboxHash),
MailboxRename {
old_mailbox_hash: MailboxHash,
new_mailbox: Mailbox,
},
MailboxSubscribe(MailboxHash),
MailboxUnsubscribe(MailboxHash),
Failure(MeliError),
}
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct RefreshEvent {
pub mailbox_hash: MailboxHash,
pub account_hash: AccountHash,
pub kind: RefreshEventKind,
hash: FolderHash,
kind: RefreshEventKind,
}
#[derive(Clone)]
pub struct BackendEventConsumer(Arc<dyn Fn(AccountHash, BackendEvent) + Send + Sync>);
impl BackendEventConsumer {
pub fn new(b: Arc<dyn Fn(AccountHash, BackendEvent) + Send + Sync>) -> Self {
Self(b)
impl RefreshEvent {
pub fn hash(&self) -> FolderHash {
self.hash
}
pub fn kind(self) -> RefreshEventKind {
/* consumes self! */
self.kind
}
}
impl fmt::Debug for BackendEventConsumer {
/// A `RefreshEventConsumer` is a boxed closure that must be used to consume a `RefreshEvent` and
/// send it to a UI provided channel. We need this level of abstraction to provide an interface for
/// all users of mailbox refresh events.
pub struct RefreshEventConsumer(Box<dyn Fn(RefreshEvent) -> () + Send + Sync>);
impl RefreshEventConsumer {
pub fn new(b: Box<dyn Fn(RefreshEvent) -> () + Send + Sync>) -> Self {
RefreshEventConsumer(b)
}
pub fn send(&self, r: RefreshEvent) {
self.0(r);
}
}
pub struct NotifyFn(Box<dyn Fn(FolderHash) -> () + Send + Sync>);
impl fmt::Debug for NotifyFn {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "BackendEventConsumer")
write!(f, "NotifyFn Box")
}
}
impl Deref for BackendEventConsumer {
type Target = dyn Fn(AccountHash, BackendEvent) + Send + Sync;
fn deref(&self) -> &Self::Target {
&(*self.0)
impl From<Box<dyn Fn(FolderHash) -> () + Send + Sync>> for NotifyFn {
fn from(kind: Box<dyn Fn(FolderHash) -> () + Send + Sync>) -> Self {
NotifyFn(kind)
}
}
#[derive(Debug, Clone)]
pub struct MailBackendCapabilities {
pub is_async: bool,
pub is_remote: bool,
pub extensions: Option<Vec<(String, MailBackendExtensionStatus)>>,
pub supports_search: bool,
pub supports_tags: bool,
pub supports_submission: bool,
impl NotifyFn {
pub fn new(b: Box<dyn Fn(FolderHash) -> () + Send + Sync>) -> Self {
NotifyFn(b)
}
pub fn notify(&self, f: FolderHash) {
self.0(f);
}
}
#[derive(Debug, Copy, Clone)]
pub enum MailBackendExtensionStatus {
Unsupported { comment: Option<&'static str> },
Supported { comment: Option<&'static str> },
Enabled { comment: Option<&'static str> },
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub enum FolderOperation {
Create,
Delete,
Subscribe,
Unsubscribe,
Rename(NewFolderName),
SetPermissions(FolderPermissions),
}
pub type ResultFuture<T> = Result<Pin<Box<dyn Future<Output = Result<T>> + Send + 'static>>>;
type NewFolderName = String;
pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
fn capabilities(&self) -> MailBackendCapabilities;
fn is_online(&self) -> ResultFuture<()> {
Ok(Box::pin(async { Ok(()) }))
fn is_online(&self) -> bool;
fn get(&mut self, folder: &Folder) -> Async<Result<Vec<Envelope>>>;
fn watch(
&self,
sender: RefreshEventConsumer,
work_context: WorkContext,
) -> Result<std::thread::ThreadId>;
fn folders(&self) -> Result<FnvHashMap<FolderHash, Folder>>;
fn operation(&self, hash: EnvelopeHash) -> Box<dyn BackendOp>;
fn save(&self, bytes: &[u8], folder: &str, flags: Option<Flag>) -> Result<()>;
fn folder_operation(&mut self, _path: &str, _op: FolderOperation) -> Result<()> {
Ok(())
}
#[allow(clippy::type_complexity)]
fn fetch(
&mut self,
mailbox_hash: MailboxHash,
) -> Result<Pin<Box<dyn Stream<Item = Result<Vec<Envelope>>> + Send + 'static>>>;
fn refresh(&mut self, mailbox_hash: MailboxHash) -> ResultFuture<()>;
fn watch(&self) -> ResultFuture<()>;
fn mailboxes(&self) -> ResultFuture<HashMap<MailboxHash, Mailbox>>;
fn operation(&self, hash: EnvelopeHash) -> Result<Box<dyn BackendOp>>;
fn save(
&self,
bytes: Vec<u8>,
mailbox_hash: MailboxHash,
flags: Option<Flag>,
) -> ResultFuture<()>;
fn copy_messages(
&mut self,
env_hashes: EnvelopeHashBatch,
source_mailbox_hash: MailboxHash,
destination_mailbox_hash: MailboxHash,
move_: bool,
) -> ResultFuture<()>;
fn set_flags(
&mut self,
env_hashes: EnvelopeHashBatch,
mailbox_hash: MailboxHash,
flags: SmallVec<[(std::result::Result<Flag, String>, bool); 8]>,
) -> ResultFuture<()>;
fn delete_messages(
&mut self,
env_hashes: EnvelopeHashBatch,
mailbox_hash: MailboxHash,
) -> ResultFuture<()>;
fn collection(&self) -> crate::Collection;
fn tags(&self) -> Option<Arc<RwLock<BTreeMap<u64, String>>>> {
None
}
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn create_mailbox(
&mut self,
path: String,
) -> ResultFuture<(MailboxHash, HashMap<MailboxHash, Mailbox>)>;
fn delete_mailbox(
&mut self,
mailbox_hash: MailboxHash,
) -> ResultFuture<HashMap<MailboxHash, Mailbox>>;
fn set_mailbox_subscription(
&mut self,
mailbox_hash: MailboxHash,
val: bool,
) -> ResultFuture<()>;
fn rename_mailbox(
&mut self,
mailbox_hash: MailboxHash,
new_path: String,
) -> ResultFuture<Mailbox>;
fn set_mailbox_permissions(
&mut self,
mailbox_hash: MailboxHash,
val: MailboxPermissions,
) -> ResultFuture<()>;
fn search(
&self,
query: crate::search::Query,
mailbox_hash: Option<MailboxHash>,
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>>;
fn submit(
&self,
_bytes: Vec<u8>,
_mailbox_hash: Option<MailboxHash>,
_flags: Option<Flag>,
) -> ResultFuture<()> {
Err(Error::new("Submission not supported in this backend.")
.set_kind(ErrorKind::NotSupported))
}
}
/// A `BackendOp` manages common operations for the various mail backends. They
/// only live for the duration of the operation. They are generated by the
/// `operation` method of `Mailbackend` trait.
/// A `BackendOp` manages common operations for the various mail backends. They only live for the
/// duration of the operation. They are generated by the `operation` method of `Mailbackend` trait.
///
/// # Motivation
///
/// We need a way to do various operations on individual mails regardless of
/// what backend they come from (eg local or imap).
/// We need a way to do various operations on individual mails regardless of what backend they come
/// from (eg local or imap).
///
/// # Creation
/// ```ignore
/// ```no_run
/// /* Create operation from Backend */
///
/// let op = backend.operation(message.hash(), mailbox.hash());
/// let op = backend.operation(message.hash(), mailbox.folder.hash());
/// ```
///
/// # Example
/// ```ignore
/// use melib::backends::{BackendOp};
/// ```
/// use melib::mailbox::backends::{BackendOp};
/// use melib::Result;
/// use melib::{Envelope, Flag};
///
@ -472,49 +281,75 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
/// struct FooOp {}
///
/// impl BackendOp for FooOp {
/// fn description(&self) -> String {
/// "Foobar".to_string()
/// }
/// fn as_bytes(&mut self) -> Result<&[u8]> {
/// unimplemented!()
/// }
/// fn fetch_flags(&self) -> Result<Flag> {
/// fn fetch_headers(&mut self) -> Result<&[u8]> {
/// unimplemented!()
/// }
/// fn fetch_body(&mut self) -> Result<&[u8]> {
/// unimplemented!()
/// }
/// fn fetch_flags(&self) -> Flag {
/// unimplemented!()
/// }
/// }
///
/// let operation = Box::new(FooOp {});
/// assert_eq!("Foobar", &operation.description());
/// ```
pub trait BackendOp: ::std::fmt::Debug + ::std::marker::Send {
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>>;
fn fetch_flags(&self) -> ResultFuture<Flag>;
fn description(&self) -> String;
fn as_bytes(&mut self) -> Result<&[u8]>;
//fn delete(&self) -> ();
//fn copy(&self
fn fetch_headers(&mut self) -> Result<&[u8]>;
fn fetch_body(&mut self) -> Result<&[u8]>;
fn fetch_flags(&self) -> Flag;
fn set_flag(&mut self, envelope: &mut Envelope, flag: Flag, value: bool) -> Result<()>;
}
/// Wrapper for BackendOps that are to be set read-only.
///
/// Warning: Backend implementations may still cause side-effects (for example
/// IMAP can set the Seen flag when fetching an envelope)
/// Warning: Backend implementations may still cause side-effects (for example IMAP can set the
/// Seen flag when fetching an envelope)
#[derive(Debug)]
pub struct ReadOnlyOp {
op: Box<dyn BackendOp>,
}
impl ReadOnlyOp {
#[allow(clippy::new_ret_no_self)]
pub fn new(op: Box<dyn BackendOp>) -> Box<dyn BackendOp> {
Box::new(Self { op })
Box::new(ReadOnlyOp { op })
}
}
impl BackendOp for ReadOnlyOp {
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
fn description(&self) -> String {
format!("read-only: {}", self.op.description())
}
fn as_bytes(&mut self) -> Result<&[u8]> {
self.op.as_bytes()
}
fn fetch_flags(&self) -> ResultFuture<Flag> {
fn fetch_headers(&mut self) -> Result<&[u8]> {
self.op.fetch_headers()
}
fn fetch_body(&mut self) -> Result<&[u8]> {
self.op.fetch_body()
}
fn fetch_flags(&self) -> Flag {
self.op.fetch_flags()
}
fn set_flag(&mut self, _envelope: &mut Envelope, _flag: Flag, _value: bool) -> Result<()> {
Err(MeliError::new("read-only set."))
}
}
#[derive(Default, Debug, Copy, Hash, Eq, Clone, Serialize, Deserialize, PartialEq)]
pub enum SpecialUsageMailbox {
#[default]
#[derive(Debug, Copy, Hash, Eq, Clone, Serialize, Deserialize, PartialEq)]
pub enum SpecialUseMailbox {
Normal,
Inbox,
Archive,
@ -525,77 +360,79 @@ pub enum SpecialUsageMailbox {
Trash,
}
impl std::fmt::Display for SpecialUsageMailbox {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
use SpecialUsageMailbox::*;
write!(
f,
"{}",
match self {
Normal => "Normal",
Inbox => "Inbox",
Archive => "Archive",
Drafts => "Drafts",
Flagged => "Flagged",
Junk => "Junk",
Sent => "Sent",
Trash => "Trash",
}
)
}
}
impl SpecialUsageMailbox {
pub fn detect_usage(name: &str) -> Option<Self> {
if name.eq_ignore_ascii_case("inbox") {
Some(Self::Inbox)
} else if name.eq_ignore_ascii_case("archive") {
Some(Self::Archive)
} else if name.eq_ignore_ascii_case("drafts") {
Some(Self::Drafts)
} else if name.eq_ignore_ascii_case("junk") || name.eq_ignore_ascii_case("spam") {
Some(Self::Junk)
} else if name.eq_ignore_ascii_case("sent") {
Some(Self::Sent)
} else if name.eq_ignore_ascii_case("trash") {
Some(Self::Trash)
} else {
Some(Self::Normal)
}
}
}
pub trait BackendMailbox: Debug {
fn hash(&self) -> MailboxHash;
/// Final component of `path`.
pub trait BackendFolder: Debug {
fn hash(&self) -> FolderHash;
fn name(&self) -> &str;
/// Path of mailbox within the mailbox hierarchy, with `/` as separator.
/// Path of folder within the mailbox hierarchy, with `/` as separator.
fn path(&self) -> &str;
fn clone(&self) -> Mailbox;
fn children(&self) -> &[MailboxHash];
fn parent(&self) -> Option<MailboxHash>;
fn is_subscribed(&self) -> bool;
fn set_is_subscribed(&mut self, new_val: bool) -> Result<()>;
fn set_special_usage(&mut self, new_val: SpecialUsageMailbox) -> Result<()>;
fn special_usage(&self) -> SpecialUsageMailbox;
fn permissions(&self) -> MailboxPermissions;
fn count(&self) -> Result<(usize, usize)>;
fn change_name(&mut self, new_name: &str);
fn clone(&self) -> Folder;
fn children(&self) -> &[FolderHash];
fn parent(&self) -> Option<FolderHash>;
fn permissions(&self) -> FolderPermissions;
}
crate::declare_u64_hash!(AccountHash);
crate::declare_u64_hash!(MailboxHash);
crate::declare_u64_hash!(TagHash);
#[derive(Debug)]
struct DummyFolder {
v: Vec<FolderHash>,
}
pub type Mailbox = Box<dyn BackendMailbox + Send + Sync>;
impl BackendFolder for DummyFolder {
fn hash(&self) -> FolderHash {
0
}
impl Clone for Mailbox {
fn name(&self) -> &str {
""
}
fn path(&self) -> &str {
""
}
fn change_name(&mut self, _s: &str) {}
fn clone(&self) -> Folder {
folder_default()
}
fn children(&self) -> &[FolderHash] {
&self.v
}
fn parent(&self) -> Option<FolderHash> {
None
}
fn permissions(&self) -> FolderPermissions {
FolderPermissions::default()
}
}
pub fn folder_default() -> Folder {
Box::new(DummyFolder {
v: Vec::with_capacity(0),
})
}
pub type FolderHash = u64;
pub type Folder = Box<dyn BackendFolder + Send + Sync>;
impl Clone for Folder {
fn clone(&self) -> Self {
BackendMailbox::clone(self.deref())
BackendFolder::clone(self.deref())
}
}
impl Default for Folder {
fn default() -> Self {
folder_default()
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub struct MailboxPermissions {
pub struct FolderPermissions {
pub create_messages: bool,
pub remove_messages: bool,
pub set_flags: bool,
@ -606,172 +443,17 @@ pub struct MailboxPermissions {
pub change_permissions: bool,
}
impl Default for MailboxPermissions {
impl Default for FolderPermissions {
fn default() -> Self {
Self {
FolderPermissions {
create_messages: false,
remove_messages: false,
set_flags: false,
create_child: false,
rename_messages: false,
delete_messages: false,
delete_mailbox: true,
delete_mailbox: false,
change_permissions: false,
}
}
}
impl std::fmt::Display for MailboxPermissions {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(fmt, "{:#?}", self)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnvelopeHashBatch {
pub first: EnvelopeHash,
pub rest: SmallVec<[EnvelopeHash; 64]>,
}
impl From<EnvelopeHash> for EnvelopeHashBatch {
fn from(value: EnvelopeHash) -> Self {
Self {
first: value,
rest: SmallVec::new(),
}
}
}
impl std::convert::TryFrom<&[EnvelopeHash]> for EnvelopeHashBatch {
type Error = ();
fn try_from(value: &[EnvelopeHash]) -> std::result::Result<Self, Self::Error> {
if value.is_empty() {
return Err(());
}
Ok(Self {
first: value[0],
rest: value[1..].iter().cloned().collect(),
})
}
}
impl From<&EnvelopeHashBatch> for BTreeSet<EnvelopeHash> {
fn from(val: &EnvelopeHashBatch) -> Self {
val.iter().collect()
}
}
impl EnvelopeHashBatch {
pub fn iter(&self) -> impl std::iter::Iterator<Item = EnvelopeHash> + '_ {
std::iter::once(self.first).chain(self.rest.iter().cloned())
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn len(&self) -> usize {
1 + self.rest.len()
}
pub fn to_set(&self) -> BTreeSet<EnvelopeHash> {
self.into()
}
}
#[derive(Default, Clone)]
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);
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[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(EnvelopeHash(i)));
}
assert_eq!(new.len(), 10);
assert!(!new.insert_existing(EnvelopeHash(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
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,820 +0,0 @@
/*
* meli - imap melib
*
* 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::*;
mod sync;
use std::convert::TryFrom;
use crate::{
backends::MailboxHash,
email::{Envelope, EnvelopeHash},
error::*,
};
#[derive(Debug, PartialEq, Hash, Eq, Ord, PartialOrd, Copy, Clone)]
pub struct ModSequence(pub std::num::NonZeroU64);
impl TryFrom<i64> for ModSequence {
type Error = ();
fn try_from(val: i64) -> std::result::Result<Self, ()> {
std::num::NonZeroU64::new(val as u64)
.map(|u| Ok(Self(u)))
.unwrap_or(Err(()))
}
}
impl core::fmt::Display for ModSequence {
fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(fmt, "{}", &self.0)
}
}
#[derive(Debug)]
pub struct CachedEnvelope {
pub inner: Envelope,
pub uid: UID,
pub mailbox_hash: MailboxHash,
pub modsequence: Option<ModSequence>,
}
pub trait ImapCache: Send + core::fmt::Debug {
fn reset(&mut self) -> Result<()>;
fn mailbox_state(&mut self, mailbox_hash: MailboxHash) -> Result<Option<()>>;
fn find_envelope(
&mut self,
identifier: std::result::Result<UID, EnvelopeHash>,
mailbox_hash: MailboxHash,
) -> Result<Option<CachedEnvelope>>;
fn update(
&mut self,
mailbox_hash: MailboxHash,
refresh_events: &[(UID, RefreshEvent)],
) -> Result<()>;
fn update_mailbox(
&mut self,
mailbox_hash: MailboxHash,
select_response: &SelectResponse,
) -> Result<()>;
fn insert_envelopes(
&mut self,
mailbox_hash: MailboxHash,
fetches: &[FetchResponse<'_>],
) -> Result<()>;
fn envelopes(&mut self, mailbox_hash: MailboxHash) -> Result<Option<Vec<EnvelopeHash>>>;
fn clear(&mut self, mailbox_hash: MailboxHash, select_response: &SelectResponse) -> Result<()>;
fn rfc822(
&mut self,
identifier: std::result::Result<UID, EnvelopeHash>,
mailbox_hash: MailboxHash,
) -> Result<Option<Vec<u8>>>;
}
pub trait ImapCacheReset: Send + core::fmt::Debug {
fn reset_db(uid_store: &UIDStore) -> Result<()>
where
Self: Sized;
}
#[cfg(feature = "sqlite3")]
pub use sqlite3_m::*;
#[cfg(feature = "sqlite3")]
mod sqlite3_m {
use super::*;
use crate::utils::sqlite3::{
self,
rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput},
Connection, DatabaseDescription,
};
type Sqlite3UID = i32;
#[derive(Debug)]
pub struct Sqlite3Cache {
connection: Connection,
loaded_mailboxes: BTreeSet<MailboxHash>,
uid_store: Arc<UIDStore>,
}
const DB_DESCRIPTION: DatabaseDescription = DatabaseDescription {
name: "header_cache.db",
init_script: Some(
"PRAGMA foreign_keys = true;
PRAGMA encoding = 'UTF-8';
CREATE TABLE IF NOT EXISTS envelopes (
hash INTEGER NOT NULL,
mailbox_hash INTEGER NOT NULL,
uid INTEGER NOT NULL,
modsequence INTEGER,
rfc822 BLOB,
envelope BLOB NOT NULL,
PRIMARY KEY (mailbox_hash, uid),
FOREIGN KEY (mailbox_hash) REFERENCES mailbox(mailbox_hash) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS mailbox (
mailbox_hash INTEGER UNIQUE,
uidvalidity INTEGER,
flags BLOB NOT NULL,
highestmodseq INTEGER,
PRIMARY KEY (mailbox_hash)
);
CREATE INDEX IF NOT EXISTS envelope_uid_idx ON envelopes(mailbox_hash, uid);
CREATE INDEX IF NOT EXISTS envelope_idx ON envelopes(hash);
CREATE INDEX IF NOT EXISTS mailbox_idx ON mailbox(mailbox_hash);",
),
version: 3,
};
impl ToSql for ModSequence {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
Ok(ToSqlOutput::from(self.0.get() as i64))
}
}
impl FromSql for ModSequence {
fn column_result(value: rusqlite::types::ValueRef) -> FromSqlResult<Self> {
let i: i64 = FromSql::column_result(value)?;
if i == 0 {
return Err(FromSqlError::OutOfRange(0));
}
Ok(Self::try_from(i).unwrap())
}
}
impl Sqlite3Cache {
pub fn get(uid_store: Arc<UIDStore>) -> Result<Box<dyn ImapCache>> {
Ok(Box::new(Self {
connection: sqlite3::open_or_create_db(
&DB_DESCRIPTION,
Some(&uid_store.account_name),
)?,
loaded_mailboxes: BTreeSet::default(),
uid_store,
}))
}
fn max_uid(&self, mailbox_hash: MailboxHash) -> Result<UID> {
let mut stmt = self
.connection
.prepare("SELECT MAX(uid) FROM envelopes WHERE mailbox_hash = ?1;")?;
let mut ret: Vec<UID> = stmt
.query_map(sqlite3::params![mailbox_hash], |row| {
row.get(0).map(|i: Sqlite3UID| i as UID)
})?
.collect::<std::result::Result<_, _>>()?;
Ok(ret.pop().unwrap_or(0))
}
}
impl ImapCacheReset for Sqlite3Cache {
fn reset_db(uid_store: &UIDStore) -> Result<()> {
sqlite3::reset_db(&DB_DESCRIPTION, Some(&uid_store.account_name))
}
}
impl ImapCache for Sqlite3Cache {
fn reset(&mut self) -> Result<()> {
Self::reset_db(&self.uid_store)
}
fn mailbox_state(&mut self, mailbox_hash: MailboxHash) -> Result<Option<()>> {
if self.loaded_mailboxes.contains(&mailbox_hash) {
return Ok(Some(()));
}
debug!("loading mailbox state {} from cache", mailbox_hash);
let mut stmt = self.connection.prepare(
"SELECT uidvalidity, flags, highestmodseq FROM mailbox WHERE mailbox_hash = ?1;",
)?;
let mut ret = stmt.query_map(sqlite3::params![mailbox_hash], |row| {
Ok((
row.get(0).map(|u: Sqlite3UID| u as UID)?,
row.get(1)?,
row.get(2)?,
))
})?;
if let Some(v) = ret.next() {
let (uidvalidity, flags, highestmodseq): (
UIDVALIDITY,
Vec<u8>,
Option<ModSequence>,
) = v?;
debug!(
"mailbox state {} in cache uidvalidity {}",
mailbox_hash, uidvalidity
);
debug!(
"mailbox state {} in cache highestmodseq {:?}",
mailbox_hash, &highestmodseq
);
debug!(
"mailbox state {} inserting flags: {:?}",
mailbox_hash,
to_str!(&flags)
);
self.uid_store
.highestmodseqs
.lock()
.unwrap()
.entry(mailbox_hash)
.and_modify(|entry| *entry = highestmodseq.ok_or(()))
.or_insert_with(|| highestmodseq.ok_or(()));
self.uid_store
.uidvalidity
.lock()
.unwrap()
.entry(mailbox_hash)
.and_modify(|entry| *entry = uidvalidity)
.or_insert(uidvalidity);
let mut tag_lck = self.uid_store.collection.tag_index.write().unwrap();
for f in to_str!(&flags).split('\0') {
let hash = TagHash::from_bytes(f.as_bytes());
tag_lck.entry(hash).or_insert_with(|| f.to_string());
}
self.loaded_mailboxes.insert(mailbox_hash);
Ok(Some(()))
} else {
debug!("mailbox state {} not in cache", mailbox_hash);
Ok(None)
}
}
fn clear(
&mut self,
mailbox_hash: MailboxHash,
select_response: &SelectResponse,
) -> Result<()> {
debug!("clear mailbox_hash {} {:?}", mailbox_hash, select_response);
self.loaded_mailboxes.remove(&mailbox_hash);
self.connection
.execute(
"DELETE FROM mailbox WHERE mailbox_hash = ?1",
sqlite3::params![mailbox_hash],
)
.chain_err_summary(|| {
format!(
"Could not clear cache of mailbox {} account {}",
mailbox_hash, self.uid_store.account_name
)
})?;
if let Some(Ok(highestmodseq)) = select_response.highestmodseq {
self.connection
.execute(
"INSERT OR IGNORE INTO mailbox (uidvalidity, flags, highestmodseq, \
mailbox_hash) VALUES (?1, ?2, ?3, ?4)",
sqlite3::params![
select_response.uidvalidity as Sqlite3UID,
select_response
.flags
.1
.iter()
.map(|s| s.as_str())
.collect::<Vec<&str>>()
.join("\0")
.as_bytes(),
highestmodseq,
mailbox_hash
],
)
.chain_err_summary(|| {
format!(
"Could not insert uidvalidity {} in header_cache of account {}",
select_response.uidvalidity, self.uid_store.account_name
)
})?;
} else {
self.connection
.execute(
"INSERT OR IGNORE INTO mailbox (uidvalidity, flags, mailbox_hash) VALUES \
(?1, ?2, ?3)",
sqlite3::params![
select_response.uidvalidity as Sqlite3UID,
select_response
.flags
.1
.iter()
.map(|s| s.as_str())
.collect::<Vec<&str>>()
.join("\0")
.as_bytes(),
mailbox_hash
],
)
.chain_err_summary(|| {
format!(
"Could not insert mailbox {} in header_cache of account {}",
select_response.uidvalidity, self.uid_store.account_name
)
})?;
}
Ok(())
}
fn update_mailbox(
&mut self,
mailbox_hash: MailboxHash,
select_response: &SelectResponse,
) -> Result<()> {
if self.mailbox_state(mailbox_hash)?.is_none() {
return self.clear(mailbox_hash, select_response);
}
if let Some(Ok(highestmodseq)) = select_response.highestmodseq {
self.connection
.execute(
"UPDATE mailbox SET flags=?1, highestmodseq =?2 where mailbox_hash = ?3;",
sqlite3::params![
select_response
.flags
.1
.iter()
.map(|s| s.as_str())
.collect::<Vec<&str>>()
.join("\0")
.as_bytes(),
highestmodseq,
mailbox_hash
],
)
.chain_err_summary(|| {
format!(
"Could not update mailbox {} in header_cache of account {}",
mailbox_hash, self.uid_store.account_name
)
})?;
} else {
self.connection
.execute(
"UPDATE mailbox SET flags=?1 where mailbox_hash = ?2;",
sqlite3::params![
select_response
.flags
.1
.iter()
.map(|s| s.as_str())
.collect::<Vec<&str>>()
.join("\0")
.as_bytes(),
mailbox_hash
],
)
.chain_err_summary(|| {
format!(
"Could not update mailbox {} in header_cache of account {}",
mailbox_hash, self.uid_store.account_name
)
})?;
}
Ok(())
}
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() {
return Ok(None);
}
let ret: Vec<(UID, Envelope, Option<ModSequence>)> = match {
let mut stmt = self.connection.prepare(
"SELECT uid, envelope, modsequence FROM envelopes WHERE mailbox_hash = ?1;",
)?;
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
// for the temporary to live long enough
let x = stmt
.query_map(sqlite3::params![mailbox_hash], |row| {
Ok((
row.get(0).map(|i: Sqlite3UID| i as UID)?,
row.get(1)?,
row.get(2)?,
))
})?
.collect::<std::result::Result<_, _>>();
x
} {
Err(err) if matches!(&err, rusqlite::Error::FromSqlConversionFailure(_, _, _)) => {
drop(err);
self.reset()?;
return Ok(None);
}
Err(err) => return Err(err.into()),
Ok(v) => v,
};
let mut max_uid = 0;
let mut env_lck = self.uid_store.envelopes.lock().unwrap();
let mut hash_index_lck = self.uid_store.hash_index.lock().unwrap();
let mut uid_index_lck = self.uid_store.uid_index.lock().unwrap();
let mut env_hashes = Vec::with_capacity(ret.len());
for (uid, env, modseq) in ret {
env_hashes.push(env.hash());
max_uid = std::cmp::max(max_uid, uid);
hash_index_lck.insert(env.hash(), (uid, mailbox_hash));
uid_index_lck.insert((mailbox_hash, uid), env.hash());
env_lck.insert(
env.hash(),
CachedEnvelope {
inner: env,
uid,
mailbox_hash,
modsequence: modseq,
},
);
}
self.uid_store
.max_uids
.lock()
.unwrap()
.insert(mailbox_hash, max_uid);
Ok(Some(env_hashes))
}
fn insert_envelopes(
&mut self,
mailbox_hash: MailboxHash,
fetches: &[FetchResponse<'_>],
) -> Result<()> {
debug!(
"insert_envelopes mailbox_hash {} len {}",
mailbox_hash,
fetches.len()
);
let mut max_uid = self
.uid_store
.max_uids
.lock()
.unwrap()
.get(&mailbox_hash)
.cloned()
.unwrap_or_default();
if self.mailbox_state(mailbox_hash)?.is_none() {
return Err(Error::new("Mailbox is not in cache").set_kind(ErrorKind::Bug));
}
let Self {
ref mut connection,
ref uid_store,
loaded_mailboxes: _,
} = self;
let tx = connection.transaction()?;
for item in fetches {
if let FetchResponse {
uid: Some(uid),
message_sequence_number: _,
modseq,
flags: _,
body: _,
references: _,
envelope: Some(envelope),
raw_fetch_value: _,
} = item
{
max_uid = std::cmp::max(max_uid, *uid);
tx.execute(
"INSERT OR REPLACE INTO envelopes (hash, uid, mailbox_hash, modsequence, \
envelope) VALUES (?1, ?2, ?3, ?4, ?5)",
sqlite3::params![
envelope.hash(),
*uid as Sqlite3UID,
mailbox_hash,
modseq,
&envelope
],
)
.chain_err_summary(|| {
format!(
"Could not insert envelope {} {} in header_cache of account {}",
envelope.message_id(),
envelope.hash(),
uid_store.account_name
)
})?;
}
}
tx.commit()?;
self.uid_store
.max_uids
.lock()
.unwrap()
.insert(mailbox_hash, max_uid);
Ok(())
}
fn update(
&mut self,
mailbox_hash: MailboxHash,
refresh_events: &[(UID, RefreshEvent)],
) -> Result<()> {
if self.mailbox_state(mailbox_hash)?.is_none() {
return Err(Error::new("Mailbox is not in cache").set_kind(ErrorKind::Bug));
}
let Self {
ref mut connection,
ref uid_store,
loaded_mailboxes: _,
} = self;
let tx = connection.transaction()?;
let mut hash_index_lck = uid_store.hash_index.lock().unwrap();
for (uid, event) in refresh_events {
match &event.kind {
RefreshEventKind::Remove(env_hash) => {
hash_index_lck.remove(env_hash);
tx.execute(
"DELETE FROM envelopes WHERE mailbox_hash = ?1 AND uid = ?2;",
sqlite3::params![mailbox_hash, *uid as Sqlite3UID],
)
.chain_err_summary(|| {
format!(
"Could not remove envelope {} uid {} from mailbox {} account {}",
env_hash, *uid, mailbox_hash, uid_store.account_name
)
})?;
}
RefreshEventKind::NewFlags(env_hash, (flags, tags)) => {
let mut stmt = tx.prepare(
"SELECT envelope FROM envelopes WHERE mailbox_hash = ?1 AND uid = ?2;",
)?;
let mut ret: Vec<Envelope> = stmt
.query_map(sqlite3::params![mailbox_hash, *uid as Sqlite3UID], |row| {
row.get(0)
})?
.collect::<std::result::Result<_, _>>()?;
if let Some(mut env) = ret.pop() {
env.set_flags(*flags);
env.tags_mut().clear();
env.tags_mut()
.extend(tags.iter().map(|t| TagHash::from_bytes(t.as_bytes())));
tx.execute(
"UPDATE envelopes SET envelope = ?1 WHERE mailbox_hash = ?2 AND \
uid = ?3;",
sqlite3::params![&env, mailbox_hash, *uid as Sqlite3UID],
)
.chain_err_summary(|| {
format!(
"Could not update envelope {} uid {} from mailbox {} account \
{}",
env_hash, *uid, mailbox_hash, uid_store.account_name
)
})?;
uid_store
.envelopes
.lock()
.unwrap()
.entry(*env_hash)
.and_modify(|entry| {
entry.inner = env;
});
}
}
_ => {}
}
}
tx.commit()?;
let new_max_uid = self.max_uid(mailbox_hash).unwrap_or(0);
self.uid_store
.max_uids
.lock()
.unwrap()
.insert(mailbox_hash, new_max_uid);
Ok(())
}
fn find_envelope(
&mut self,
identifier: std::result::Result<UID, EnvelopeHash>,
mailbox_hash: MailboxHash,
) -> Result<Option<CachedEnvelope>> {
let mut ret: Vec<(UID, Envelope, Option<ModSequence>)> = match identifier {
Ok(uid) => {
let mut stmt = self.connection.prepare(
"SELECT uid, envelope, modsequence FROM envelopes WHERE mailbox_hash = ?1 \
AND uid = ?2;",
)?;
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
// for the temporary to live long enough
let x = stmt
.query_map(sqlite3::params![mailbox_hash, uid as Sqlite3UID], |row| {
Ok((
row.get(0).map(|u: Sqlite3UID| u as UID)?,
row.get(1)?,
row.get(2)?,
))
})?
.collect::<std::result::Result<_, _>>()?;
x
}
Err(env_hash) => {
let mut stmt = self.connection.prepare(
"SELECT uid, envelope, modsequence FROM envelopes WHERE mailbox_hash = ?1 \
AND hash = ?2;",
)?;
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
// for the temporary to live long enough
let x = stmt
.query_map(sqlite3::params![mailbox_hash, env_hash], |row| {
Ok((
row.get(0).map(|u: Sqlite3UID| u as UID)?,
row.get(1)?,
row.get(2)?,
))
})?
.collect::<std::result::Result<_, _>>()?;
x
}
};
if ret.len() != 1 {
return Ok(None);
}
let (uid, inner, modsequence) = ret.pop().unwrap();
Ok(Some(CachedEnvelope {
inner,
uid,
mailbox_hash,
modsequence,
}))
}
fn rfc822(
&mut self,
identifier: std::result::Result<UID, EnvelopeHash>,
mailbox_hash: MailboxHash,
) -> Result<Option<Vec<u8>>> {
let mut ret: Vec<Option<Vec<u8>>> = match identifier {
Ok(uid) => {
let mut stmt = self.connection.prepare(
"SELECT rfc822 FROM envelopes WHERE mailbox_hash = ?1 AND uid = ?2;",
)?;
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
// for the temporary to live long enough
let x = stmt
.query_map(sqlite3::params![mailbox_hash, uid as Sqlite3UID], |row| {
row.get(0)
})?
.collect::<std::result::Result<_, _>>()?;
x
}
Err(env_hash) => {
let mut stmt = self.connection.prepare(
"SELECT rfc822 FROM envelopes WHERE mailbox_hash = ?1 AND hash = ?2;",
)?;
#[allow(clippy::let_and_return)] // false positive, the let binding is needed
// for the temporary to live long enough
let x = stmt
.query_map(sqlite3::params![mailbox_hash, env_hash], |row| row.get(0))?
.collect::<std::result::Result<_, _>>()?;
x
}
};
if ret.len() != 1 {
return Ok(None);
}
Ok(ret.pop().unwrap())
}
}
}
pub(super) async fn fetch_cached_envs(state: &mut FetchState) -> Result<Option<Vec<Envelope>>> {
let FetchState {
stage: _,
ref mut connection,
mailbox_hash,
ref uid_store,
cache_handle: _,
} = state;
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),
Some(Ok(env_hashes)) => {
let env_lck = uid_store.envelopes.lock().unwrap();
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),
}
}
}
#[cfg(not(feature = "sqlite3"))]
pub use default_m::*;
#[cfg(not(feature = "sqlite3"))]
mod default_m {
use super::*;
#[derive(Debug)]
pub struct DefaultCache;
impl DefaultCache {
pub fn get(_uid_store: Arc<UIDStore>) -> Result<Box<dyn ImapCache>> {
Ok(Box::new(Self))
}
}
impl ImapCacheReset for DefaultCache {
fn reset_db(uid_store: &UIDStore) -> Result<()> {
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
}
}
impl ImapCache for DefaultCache {
fn reset(&mut self) -> Result<()> {
DefaultCache::reset_db(&self.uid_store)
}
fn mailbox_state(&mut self, _mailbox_hash: MailboxHash) -> Result<Option<()>> {
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
}
fn clear(
&mut self,
_mailbox_hash: MailboxHash,
_select_response: &SelectResponse,
) -> Result<()> {
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
}
fn envelopes(&mut self, _mailbox_hash: MailboxHash) -> Result<Option<Vec<EnvelopeHash>>> {
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
}
fn insert_envelopes(
&mut self,
_mailbox_hash: MailboxHash,
_fetches: &[FetchResponse<'_>],
) -> Result<()> {
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
}
fn update_mailbox(
&mut self,
_mailbox_hash: MailboxHash,
_select_response: &SelectResponse,
) -> Result<()> {
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
}
fn update(
&mut self,
_mailbox_hash: MailboxHash,
_refresh_events: &[(UID, RefreshEvent)],
) -> Result<()> {
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
}
fn find_envelope(
&mut self,
_identifier: std::result::Result<UID, EnvelopeHash>,
_mailbox_hash: MailboxHash,
) -> Result<Option<CachedEnvelope>> {
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
}
fn rfc822(
&mut self,
_identifier: std::result::Result<UID, EnvelopeHash>,
_mailbox_hash: MailboxHash,
) -> Result<Option<Vec<u8>>> {
Err(Error::new("melib is not built with any imap cache").set_kind(ErrorKind::Bug))
}
}
}

View File

@ -1,717 +0,0 @@
/*
* melib - IMAP
*
* 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 imap_codec::{
fetch::{MacroOrMessageDataItemNames, MessageDataItemName},
search::SearchKey,
sequence::SequenceSet,
status::StatusDataItemName,
};
use super::*;
impl ImapConnection {
pub async fn resync(&mut self, mailbox_hash: MailboxHash) -> Result<Option<Vec<Envelope>>> {
debug!("resync mailbox_hash {}", mailbox_hash);
debug!(&self.sync_policy);
if matches!(self.sync_policy, SyncPolicy::None) {
return Ok(None);
}
#[cfg(not(feature = "sqlite3"))]
let mut cache_handle = DefaultCache::get(self.uid_store.clone())?;
#[cfg(feature = "sqlite3")]
let mut cache_handle = Sqlite3Cache::get(self.uid_store.clone())?;
if cache_handle.mailbox_state(mailbox_hash)?.is_none() {
return Ok(None);
}
match self.sync_policy {
SyncPolicy::None => Ok(None),
SyncPolicy::Basic => self.resync_basic(cache_handle, mailbox_hash).await,
SyncPolicy::Condstore => self.resync_condstore(cache_handle, mailbox_hash).await,
SyncPolicy::CondstoreQresync => {
self.resync_condstoreqresync(cache_handle, mailbox_hash)
.await
}
}
}
pub async fn load_cache(
&mut self,
mailbox_hash: MailboxHash,
) -> Option<Result<Vec<EnvelopeHash>>> {
debug!("load_cache {}", mailbox_hash);
#[cfg(not(feature = "sqlite3"))]
let mut cache_handle = match DefaultCache::get(self.uid_store.clone()) {
Ok(v) => v,
Err(err) => return Some(Err(err)),
};
#[cfg(feature = "sqlite3")]
let mut cache_handle = match Sqlite3Cache::get(self.uid_store.clone()) {
Ok(v) => v,
Err(err) => return Some(Err(err)),
};
match cache_handle.mailbox_state(mailbox_hash) {
Err(err) => return Some(Err(err)),
Ok(Some(())) => {}
Ok(None) => {
return None;
}
};
match cache_handle.envelopes(mailbox_hash) {
Ok(Some(envs)) => Some(Ok(envs)),
Ok(None) => None,
Err(err) => Some(Err(err)),
}
}
//rfc4549_Synchronization_Operations_for_Disconnected_IMAP4_Clients
pub async fn resync_basic(
&mut self,
mut cache_handle: Box<dyn ImapCache>,
mailbox_hash: MailboxHash,
) -> Result<Option<Vec<Envelope>>> {
let mut payload = vec![];
debug!("resync_basic");
let mut response = Vec::with_capacity(8 * 1024);
let cached_uidvalidity = self
.uid_store
.uidvalidity
.lock()
.unwrap()
.get(&mailbox_hash)
.cloned();
let cached_max_uid = self
.uid_store
.max_uids
.lock()
.unwrap()
.get(&mailbox_hash)
.cloned();
// 3. tag2 UID FETCH 1:<lastseenuid> FLAGS
//if cached_uidvalidity.is_none() || cached_max_uid.is_none() {
// return Ok(None);
//}
let current_uidvalidity: UID = cached_uidvalidity.unwrap_or(1);
let max_uid: UID = cached_max_uid.unwrap_or(1);
let (mailbox_path, mailbox_exists, unseen) = {
let f = &self.uid_store.mailboxes.lock().await[&mailbox_hash];
(
f.imap_path().to_string(),
f.exists.clone(),
f.unseen.clone(),
)
};
let mut new_unseen = BTreeSet::default();
let select_response = self
.select_mailbox(mailbox_hash, &mut response, true)
.await?
.unwrap();
// 1. check UIDVALIDITY. If fail, discard cache and rebuild
if select_response.uidvalidity != current_uidvalidity {
cache_handle.clear(mailbox_hash, &select_response)?;
return Ok(None);
}
cache_handle.update_mailbox(mailbox_hash, &select_response)?;
// 2. tag1 UID FETCH <lastseenuid+1>:* <descriptors>
self.send_command(CommandBody::fetch(
max_uid + 1..,
common_attributes(),
true,
)?)
.await?;
self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
.await?;
debug!(
"fetch response is {} bytes and {} lines",
response.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,
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();
if let Some((flags, keywords)) = flags {
env.set_flags(*flags);
if !env.is_seen() {
new_unseen.insert(env.hash());
}
for f in keywords {
let hash = TagHash::from_bytes(f.as_bytes());
tag_lck.entry(hash).or_insert_with(|| f.to_string());
env.tags_mut().push(hash);
}
}
}
{
cache_handle
.insert_envelopes(mailbox_hash, &v)
.chain_err_summary(|| {
format!(
"Could not save envelopes in cache for mailbox {}",
mailbox_path
)
})?;
}
for FetchResponse {
uid,
message_sequence_number: _,
envelope,
..
} in v
{
let uid = uid.unwrap();
let env = envelope.unwrap();
/*
debug!(
"env hash {} {} UID = {} MSN = {}",
env.hash(),
env.subject(),
uid,
message_sequence_number
);
*/
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());
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);
// 3. tag2 UID FETCH 1:<lastseenuid> FLAGS
let sequence_set = if max_uid == 0 {
SequenceSet::from(..)
} else {
SequenceSet::try_from(..=max_uid)?
};
self.send_command(CommandBody::Fetch {
sequence_set,
macro_or_item_names: MacroOrMessageDataItemNames::MessageDataItemNames(vec![
MessageDataItemName::Flags,
]),
uid: true,
})
.await?;
self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
.await?;
// 1) update cached flags for old messages;
// 2) find out which old messages got expunged; and
// 3) build a mapping between message numbers and UIDs (for old messages).
let mut valid_envs = BTreeSet::default();
let mut env_lck = self.uid_store.envelopes.lock().unwrap();
let (_, v, _) = protocol_parser::fetch_responses(&response)?;
let mut refresh_events = vec![];
for FetchResponse { uid, flags, .. } in v {
let uid = uid.unwrap();
let env_hash = generate_envelope_hash(&mailbox_path, &uid);
valid_envs.insert(env_hash);
if !env_lck.contains_key(&env_hash) {
return Ok(None);
}
let (flags, tags) = flags.unwrap();
if env_lck[&env_hash].inner.flags() != flags
|| env_lck[&env_hash].inner.tags()
!= &tags
.iter()
.map(|t| TagHash::from_bytes(t.as_bytes()))
.collect::<SmallVec<[TagHash; 8]>>()
{
env_lck.entry(env_hash).and_modify(|entry| {
entry.inner.set_flags(flags);
entry.inner.tags_mut().clear();
entry
.inner
.tags_mut()
.extend(tags.iter().map(|t| TagHash::from_bytes(t.as_bytes())));
});
refresh_events.push((
uid,
RefreshEvent {
mailbox_hash,
account_hash: self.uid_store.account_hash,
kind: RefreshEventKind::NewFlags(env_hash, (flags, tags)),
},
));
}
}
for env_hash in env_lck
.iter()
.filter_map(|(h, cenv)| {
if cenv.mailbox_hash == mailbox_hash {
Some(*h)
} else {
None
}
})
.collect::<BTreeSet<EnvelopeHash>>()
.difference(&valid_envs)
{
refresh_events.push((
env_lck[env_hash].uid,
RefreshEvent {
mailbox_hash,
account_hash: self.uid_store.account_hash,
kind: RefreshEventKind::Remove(*env_hash),
},
));
env_lck.remove(env_hash);
}
drop(env_lck);
cache_handle.update(mailbox_hash, &refresh_events)?;
for (_uid, ev) in refresh_events {
self.add_refresh_event(ev);
}
Ok(Some(payload.into_iter().map(|(_, env)| env).collect()))
}
//rfc4549_Synchronization_Operations_for_Disconnected_IMAP4_Clients
//Section 6.1
pub async fn resync_condstore(
&mut self,
mut cache_handle: Box<dyn ImapCache>,
mailbox_hash: MailboxHash,
) -> Result<Option<Vec<Envelope>>> {
let mut payload = vec![];
debug!("resync_condstore");
let mut response = Vec::with_capacity(8 * 1024);
let cached_uidvalidity = self
.uid_store
.uidvalidity
.lock()
.unwrap()
.get(&mailbox_hash)
.cloned();
let cached_max_uid = self
.uid_store
.max_uids
.lock()
.unwrap()
.get(&mailbox_hash)
.cloned();
let cached_highestmodseq = self
.uid_store
.highestmodseqs
.lock()
.unwrap()
.get(&mailbox_hash)
.cloned();
if cached_uidvalidity.is_none()
|| cached_max_uid.is_none()
|| cached_highestmodseq.is_none()
{
// This means the mailbox is not cached.
return Ok(None);
}
let cached_uidvalidity: UID = cached_uidvalidity.unwrap();
let cached_max_uid: UID = cached_max_uid.unwrap();
let cached_highestmodseq: std::result::Result<ModSequence, ()> =
cached_highestmodseq.unwrap();
if cached_highestmodseq.is_err() {
// No MODSEQ is available for __this__ mailbox, fallback to basic sync
return self.resync_basic(cache_handle, mailbox_hash).await;
}
let cached_highestmodseq: ModSequence = cached_highestmodseq.unwrap();
let (mailbox_path, mailbox_exists, unseen) = {
let f = &self.uid_store.mailboxes.lock().await[&mailbox_hash];
(
f.imap_path().to_string(),
f.exists.clone(),
f.unseen.clone(),
)
};
let mut new_unseen = BTreeSet::default();
// 1. check UIDVALIDITY. If fail, discard cache and rebuild
let select_response = self
.select_mailbox(mailbox_hash, &mut response, true)
.await?
.unwrap();
if select_response.uidvalidity != cached_uidvalidity {
// 1a) Check the mailbox UIDVALIDITY (see section 4.1 for more
//details) with SELECT/EXAMINE/STATUS.
// If the UIDVALIDITY value returned by the server differs, the
// client MUST
// * empty the local cache of that mailbox;
// * "forget" the cached HIGHESTMODSEQ value for the mailbox;
// * remove any pending "actions" that refer to UIDs in that mailbox (note
// that this doesn't affect actions performed on client-generated fake UIDs;
// see Section 5); and
// * skip steps 1b and 2-II;
cache_handle.clear(mailbox_hash, &select_response)?;
return Ok(None);
}
if select_response.highestmodseq.is_none()
|| select_response.highestmodseq.as_ref().unwrap().is_err()
{
if select_response.highestmodseq.as_ref().unwrap().is_err() {
self.uid_store
.highestmodseqs
.lock()
.unwrap()
.insert(mailbox_hash, Err(()));
}
return self.resync_basic(cache_handle, mailbox_hash).await;
}
cache_handle.update_mailbox(mailbox_hash, &select_response)?;
let new_highestmodseq = select_response.highestmodseq.unwrap().unwrap();
let mut refresh_events = vec![];
// 1b) Check the mailbox HIGHESTMODSEQ.
// If the cached value is the same as the one returned by the server, skip
// fetching message flags on step 2-II, i.e., the client only has to
// find out which messages got expunged.
if cached_highestmodseq != new_highestmodseq {
/* Cache is synced, only figure out which messages got expunged */
// 2) Fetch the current "descriptors".
// I) Discover new messages.
// II) Discover changes to old messages and flags for new messages
// using
// "FETCH 1:* (FLAGS) (CHANGEDSINCE <cached-value>)" or
// "SEARCH MODSEQ <cached-value>".
// 2. tag1 UID FETCH <lastseenuid+1>:* <descriptors>
// TODO(#222): imap-codec does not support "CONDSTORE/QRESYNC" currently.
self.send_command_raw(
format!(
"UID FETCH {}:* (UID FLAGS ENVELOPE BODY.PEEK[HEADER.FIELDS (REFERENCES)] \
BODYSTRUCTURE) (CHANGEDSINCE {})",
cached_max_uid + 1,
cached_highestmodseq,
)
.as_bytes(),
)
.await?;
self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
.await?;
debug!(
"fetch response is {} bytes and {} lines",
response.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,
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();
if let Some((flags, keywords)) = flags {
env.set_flags(*flags);
if !env.is_seen() {
new_unseen.insert(env.hash());
}
for f in keywords {
let hash = TagHash::from_bytes(f.as_bytes());
tag_lck.entry(hash).or_insert_with(|| f.to_string());
env.tags_mut().push(hash);
}
}
}
{
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 {
let uid = uid.unwrap();
let env = envelope.unwrap();
/*
debug!(
"env hash {} {} UID = {} MSN = {}",
env.hash(),
env.subject(),
uid,
message_sequence_number
);
*/
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());
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);
// 3. tag2 UID FETCH 1:<lastseenuid> FLAGS
if cached_max_uid == 0 {
// TODO(#222): imap-codec does not support "CONDSTORE/QRESYNC" currently.
self.send_command_raw(
format!(
"UID FETCH 1:* FLAGS (CHANGEDSINCE {})",
cached_highestmodseq
)
.as_bytes(),
)
.await?;
} else {
// TODO(#222): imap-codec does not support "CONDSTORE/QRESYNC" currently.
self.send_command_raw(
format!(
"UID FETCH 1:{} FLAGS (CHANGEDSINCE {})",
cached_max_uid, cached_highestmodseq
)
.as_bytes(),
)
.await?;
}
self.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
.await?;
// 1) update cached flags for old messages;
let mut env_lck = self.uid_store.envelopes.lock().unwrap();
let (_, v, _) = protocol_parser::fetch_responses(&response)?;
for FetchResponse { uid, flags, .. } in v {
let uid = uid.unwrap();
let env_hash = generate_envelope_hash(&mailbox_path, &uid);
if !env_lck.contains_key(&env_hash) {
return Ok(None);
}
let (flags, tags) = flags.unwrap();
if env_lck[&env_hash].inner.flags() != flags
|| env_lck[&env_hash].inner.tags()
!= &tags
.iter()
.map(|t| TagHash::from_bytes(t.as_bytes()))
.collect::<SmallVec<[TagHash; 8]>>()
{
env_lck.entry(env_hash).and_modify(|entry| {
entry.inner.set_flags(flags);
entry.inner.tags_mut().clear();
entry
.inner
.tags_mut()
.extend(tags.iter().map(|t| TagHash::from_bytes(t.as_bytes())));
});
refresh_events.push((
uid,
RefreshEvent {
mailbox_hash,
account_hash: self.uid_store.account_hash,
kind: RefreshEventKind::NewFlags(env_hash, (flags, tags)),
},
));
}
}
self.uid_store
.highestmodseqs
.lock()
.unwrap()
.insert(mailbox_hash, Ok(new_highestmodseq));
}
let mut valid_envs = BTreeSet::default();
// This should be UID SEARCH 1:<maxuid> but it's difficult to compare to cached
// UIDs at the point of calling this function
self.send_command(CommandBody::search(None, SearchKey::All, true))
.await?;
self.read_response(&mut response, RequiredResponses::SEARCH)
.await?;
// 1) update cached flags for old messages;
let (_, v) = protocol_parser::search_results(response.as_slice())?;
for uid in v {
valid_envs.insert(generate_envelope_hash(&mailbox_path, &uid));
}
{
let mut env_lck = self.uid_store.envelopes.lock().unwrap();
let olds = env_lck
.iter()
.filter_map(|(h, cenv)| {
if cenv.mailbox_hash == mailbox_hash {
Some(*h)
} else {
None
}
})
.collect::<BTreeSet<EnvelopeHash>>();
for env_hash in olds.difference(&valid_envs) {
refresh_events.push((
env_lck[env_hash].uid,
RefreshEvent {
mailbox_hash,
account_hash: self.uid_store.account_hash,
kind: RefreshEventKind::Remove(*env_hash),
},
));
env_lck.remove(env_hash);
}
drop(env_lck);
}
cache_handle.update(mailbox_hash, &refresh_events)?;
for (_uid, ev) in refresh_events {
self.add_refresh_event(ev);
}
Ok(Some(payload.into_iter().map(|(_, env)| env).collect()))
}
//rfc7162_Quick Flag Changes Resynchronization (CONDSTORE)_and Quick Mailbox
// Resynchronization (QRESYNC)
pub async fn resync_condstoreqresync(
&mut self,
_cache_handle: Box<dyn ImapCache>,
_mailbox_hash: MailboxHash,
) -> Result<Option<Vec<Envelope>>> {
Ok(None)
}
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 f = &self.uid_store.mailboxes.lock().await[&mailbox_hash];
(
f.imap_path().to_string(),
f.exists.clone(),
f.permissions.clone(),
)
};
/* first SELECT the mailbox to get READ/WRITE permissions (because EXAMINE
* only returns READ-ONLY for both cases) */
let mut select_response = self
.select_mailbox(mailbox_hash, &mut response, true)
.await?
.unwrap();
debug!(
"mailbox: {} select_response: {:?}",
mailbox_path, select_response
);
{
{
let mut uidvalidities = self.uid_store.uidvalidity.lock().unwrap();
let v = uidvalidities
.entry(mailbox_hash)
.or_insert(select_response.uidvalidity);
*v = select_response.uidvalidity;
}
{
if let Some(highestmodseq) = select_response.highestmodseq {
let mut highestmodseqs = self.uid_store.highestmodseqs.lock().unwrap();
let v = highestmodseqs.entry(mailbox_hash).or_insert(highestmodseq);
*v = highestmodseq;
}
}
let mut permissions = permissions.lock().unwrap();
permissions.create_messages = !select_response.read_only;
permissions.remove_messages = !select_response.read_only;
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);
}
}
if select_response.exists == 0 {
return Ok(select_response);
}
/* reselecting the same mailbox with EXAMINE prevents expunging it */
self.examine_mailbox(mailbox_hash, &mut response, true)
.await?;
if select_response.uidnext == 0 {
/* UIDNEXT shouldn't be 0, since exists != 0 at this point */
self.send_command(CommandBody::status(
mailbox_path,
[StatusDataItemName::UidNext].as_slice(),
)?)
.await?;
self.read_response(&mut response, RequiredResponses::STATUS)
.await?;
let (_, status) = protocol_parser::status_response(response.as_slice())?;
if let Some(uidnext) = status.uidnext {
if uidnext == 0 {
return Err(Error::new(
"IMAP server error: zero UIDNEXT with nonzero exists.",
));
}
select_response.uidnext = uidnext;
} else {
return Err(Error::new("IMAP server did not reply with UIDNEXT"));
}
}
Ok(select_response)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,115 +0,0 @@
/*
* meli - imap module.
*
* Copyright 2023 Damian Poddebniak <poddebniak@mailbox.org>
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::{fmt, sync::Arc};
use imap_codec::{
command::{AppendError, CopyError, ListError},
core::LiteralError,
extensions::r#move::MoveError,
sequence::SequenceSetError,
};
use crate::error::{Error, ErrorKind};
impl From<LiteralError> for Error {
#[inline]
fn from(error: LiteralError) -> Self {
Self {
summary: error.to_string().into(),
details: None,
source: Some(Arc::new(error)),
kind: ErrorKind::Configuration,
}
}
}
impl From<SequenceSetError> for Error {
#[inline]
fn from(error: SequenceSetError) -> Self {
Self {
summary: error.to_string().into(),
details: None,
source: Some(Arc::new(error)),
kind: ErrorKind::Bug,
}
}
}
impl<S, L> From<AppendError<S, L>> for Error
where
AppendError<S, L>: fmt::Debug + fmt::Display + Sync + Send + 'static,
{
#[inline]
fn from(error: AppendError<S, L>) -> Self {
Self {
summary: error.to_string().into(),
details: None,
source: Some(Arc::new(error)),
kind: ErrorKind::Bug,
}
}
}
impl<S, L> From<CopyError<S, L>> for Error
where
CopyError<S, L>: fmt::Debug + fmt::Display + Sync + Send + 'static,
{
#[inline]
fn from(error: CopyError<S, L>) -> Self {
Self {
summary: error.to_string().into(),
details: None,
source: Some(Arc::new(error)),
kind: ErrorKind::Bug,
}
}
}
impl<S, M> From<MoveError<S, M>> for Error
where
MoveError<S, M>: fmt::Debug + fmt::Display + Sync + Send + 'static,
{
#[inline]
fn from(error: MoveError<S, M>) -> Self {
Self {
summary: error.to_string().into(),
details: None,
source: Some(Arc::new(error)),
kind: ErrorKind::Bug,
}
}
}
impl<L1, L2> From<ListError<L1, L2>> for Error
where
ListError<L1, L2>: fmt::Debug + fmt::Display + Sync + Send + 'static,
{
#[inline]
fn from(error: ListError<L1, L2>) -> Self {
Self {
summary: error.to_string().into(),
details: None,
source: Some(Arc::new(error)),
kind: ErrorKind::Bug,
}
}
}

View File

@ -0,0 +1,76 @@
/*
* meli - imap module.
*
* Copyright 2019 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use crate::backends::{BackendFolder, Folder, FolderHash, FolderPermissions};
use std::sync::{Arc, Mutex};
#[derive(Debug, Default, Clone)]
pub struct ImapFolder {
pub(super) hash: FolderHash,
pub(super) path: String,
pub(super) name: String,
pub(super) parent: Option<FolderHash>,
pub(super) children: Vec<FolderHash>,
pub permissions: Arc<Mutex<FolderPermissions>>,
pub exists: Arc<Mutex<usize>>,
}
impl BackendFolder for ImapFolder {
fn hash(&self) -> FolderHash {
self.hash
}
fn name(&self) -> &str {
&self.name
}
fn path(&self) -> &str {
&self.path
}
fn change_name(&mut self, s: &str) {
self.name = s.to_string();
}
fn children(&self) -> &[FolderHash] {
&self.children
}
fn clone(&self) -> Folder {
Box::new(ImapFolder {
hash: self.hash,
path: self.path.clone(),
name: self.name.clone(),
parent: self.parent,
children: self.children.clone(),
permissions: self.permissions.clone(),
exists: self.exists.clone(),
})
}
fn parent(&self) -> Option<FolderHash> {
self.parent
}
fn permissions(&self) -> FolderPermissions {
*self.permissions.lock().unwrap()
}
}

View File

@ -1,125 +0,0 @@
/*
* meli - imap module.
*
* Copyright 2019 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::sync::{Arc, Mutex, RwLock};
use super::protocol_parser::SelectResponse;
use crate::{
backends::{
BackendMailbox, LazyCountSet, Mailbox, MailboxHash, MailboxPermissions, SpecialUsageMailbox,
},
error::*,
};
#[derive(Debug, Default, Clone)]
pub struct ImapMailbox {
pub hash: MailboxHash,
pub imap_path: String,
pub path: String,
pub name: String,
pub parent: Option<MailboxHash>,
pub children: Vec<MailboxHash>,
pub separator: u8,
pub usage: Arc<RwLock<SpecialUsageMailbox>>,
pub select: Arc<RwLock<Option<SelectResponse>>>,
pub no_select: bool,
pub is_subscribed: bool,
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 {
fn hash(&self) -> MailboxHash {
self.hash
}
fn name(&self) -> &str {
&self.name
}
fn path(&self) -> &str {
&self.path
}
fn children(&self) -> &[MailboxHash] {
&self.children
}
fn clone(&self) -> Mailbox {
Box::new(std::clone::Clone::clone(self))
}
fn special_usage(&self) -> SpecialUsageMailbox {
*self.usage.read().unwrap()
}
fn parent(&self) -> Option<MailboxHash> {
self.parent
}
fn permissions(&self) -> MailboxPermissions {
*self.permissions.lock().unwrap()
}
fn is_subscribed(&self) -> bool {
self.is_subscribed
}
fn set_is_subscribed(&mut self, new_val: bool) -> Result<()> {
self.is_subscribed = new_val;
Ok(())
}
fn set_special_usage(&mut self, new_val: SpecialUsageMailbox) -> Result<()> {
*self.usage.write()? = new_val;
Ok(())
}
fn count(&self) -> Result<(usize, usize)> {
Ok((self.unseen.lock()?.len(), self.exists.lock()?.len()))
}
}

View File

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

View File

@ -19,160 +19,285 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::sync::Arc;
use imap_codec::fetch::MessageDataItemName;
use super::*;
use crate::{backends::*, email::*, error::Error};
use crate::backends::BackendOp;
use crate::email::*;
use crate::error::{MeliError, Result};
use std::cell::Cell;
use std::sync::{Arc, Mutex};
/// `BackendOp` implementor for Imap
#[derive(Debug, Clone)]
pub struct ImapOp {
uid: UID,
mailbox_hash: MailboxHash,
connection: Arc<FutureMutex<ImapConnection>>,
uid: usize,
bytes: Option<String>,
headers: Option<String>,
body: Option<String>,
folder_path: String,
flags: Cell<Option<Flag>>,
connection: Arc<Mutex<ImapConnection>>,
uid_store: Arc<UIDStore>,
}
impl ImapOp {
pub fn new(
uid: UID,
mailbox_hash: MailboxHash,
connection: Arc<FutureMutex<ImapConnection>>,
uid: usize,
folder_path: String,
connection: Arc<Mutex<ImapConnection>>,
uid_store: Arc<UIDStore>,
) -> Self {
Self {
ImapOp {
uid,
connection,
mailbox_hash,
bytes: None,
headers: None,
body: None,
folder_path,
flags: Cell::new(None),
uid_store,
}
}
}
impl BackendOp for ImapOp {
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
let connection = self.connection.clone();
let mailbox_hash = self.mailbox_hash;
let uid = self.uid;
let uid_store = self.uid_store.clone();
Ok(Box::pin(async move {
let exists_in_cache = {
let mut bytes_cache = uid_store.byte_cache.lock()?;
let cache = bytes_cache.entry(uid).or_default();
cache.bytes.is_some()
};
if !exists_in_cache {
let mut response = Vec::with_capacity(8 * 1024);
{
let mut conn = timeout(uid_store.timeout, connection.lock()).await?;
conn.connect().await?;
conn.examine_mailbox(mailbox_hash, &mut response, false)
.await?;
conn.send_command(CommandBody::fetch(
uid,
vec![MessageDataItemName::Flags, MessageDataItemName::Rfc822],
true,
)?)
.await?;
conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
.await?;
}
debug!(
"fetch response is {} bytes and {} lines",
response.len(),
String::from_utf8_lossy(&response).lines().count()
);
let mut results = protocol_parser::fetch_responses(&response)?.1;
if results.len() != 1 {
return Err(
Error::new(format!("Invalid/unexpected response: {:?}", response))
.set_summary(format!("message with UID {} was not found?", uid)),
);
}
let FetchResponse {
uid: _uid,
flags: _flags,
body,
..
} = results.pop().unwrap();
let _uid = _uid.unwrap();
assert_eq!(_uid, uid);
assert!(body.is_some());
let mut bytes_cache = uid_store.byte_cache.lock()?;
let cache = bytes_cache.entry(uid).or_default();
if let Some((_flags, _)) = _flags {
//flags.lock().await.set(Some(_flags));
cache.flags = Some(_flags);
}
cache.bytes = Some(body.unwrap().to_vec());
}
let mut bytes_cache = uid_store.byte_cache.lock()?;
let cache = bytes_cache.entry(uid).or_default();
let ret = cache.bytes.clone().unwrap();
Ok(ret)
}))
fn description(&self) -> String {
unimplemented!();
}
fn fetch_flags(&self) -> ResultFuture<Flag> {
let mut response = Vec::with_capacity(8 * 1024);
let connection = self.connection.clone();
let mailbox_hash = self.mailbox_hash;
let uid = self.uid;
let uid_store = self.uid_store.clone();
Ok(Box::pin(async move {
let exists_in_cache = {
let mut bytes_cache = uid_store.byte_cache.lock()?;
let cache = bytes_cache.entry(uid).or_default();
cache.flags.is_some()
};
if !exists_in_cache {
let mut conn = connection.lock().await;
conn.connect().await?;
conn.examine_mailbox(mailbox_hash, &mut response, false)
.await?;
conn.send_command(CommandBody::fetch(
uid,
vec![MessageDataItemName::Flags],
true,
)?)
.await?;
conn.read_response(&mut response, RequiredResponses::FETCH_REQUIRED)
.await?;
fn as_bytes(&mut self) -> Result<&[u8]> {
if self.bytes.is_none() {
let mut bytes_cache = self.uid_store.byte_cache.lock()?;
let cache = bytes_cache.entry(self.uid).or_default();
if cache.bytes.is_some() {
self.bytes = cache.bytes.clone();
} else {
let mut response = String::with_capacity(8 * 1024);
{
let mut conn = self.connection.lock().unwrap();
conn.send_command(format!("SELECT {}", self.folder_path).as_bytes())?;
conn.read_response(&mut response)?;
conn.send_command(format!("UID FETCH {} (FLAGS RFC822)", self.uid).as_bytes())?;
conn.read_response(&mut response)?;
}
debug!(
"fetch response is {} bytes and {} lines",
response.len(),
String::from_utf8_lossy(&response).lines().count()
response.lines().collect::<Vec<&str>>().len()
);
let v = protocol_parser::uid_fetch_flags_responses(&response)
.map(|(_, v)| v)
.map_err(Error::from)?;
if v.len() != 1 {
debug!("responses len is {}", v.len());
debug!(String::from_utf8_lossy(&response));
/* TODO: Trigger cache invalidation here. */
debug!("message with UID {} was not found", uid);
return Err(
Error::new(format!("Invalid/unexpected response: {:?}", response))
.set_summary(format!("message with UID {} was not found?", uid)),
);
match protocol_parser::uid_fetch_response(response.as_bytes())
.to_full_result()
.map_err(MeliError::from)
{
Ok(v) => {
if v.len() != 1 {
debug!("responses len is {}", v.len());
/* TODO: Trigger cache invalidation here. */
return Err(MeliError::new(format!(
"message with UID {} was not found",
self.uid
)));
}
let (uid, flags, b) = v[0];
assert_eq!(uid, self.uid);
if flags.is_some() {
self.flags.set(flags);
cache.flags = flags;
}
cache.bytes = Some(unsafe { std::str::from_utf8_unchecked(b).to_string() });
}
Err(e) => return Err(e),
}
let (_uid, (_flags, _)) = v[0];
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);
self.bytes = cache.bytes.clone();
}
}
Ok(self.bytes.as_ref().unwrap().as_bytes())
}
fn fetch_headers(&mut self) -> Result<&[u8]> {
if self.bytes.is_some() {
let result =
parser::headers_raw(self.bytes.as_ref().unwrap().as_bytes()).to_full_result()?;
return Ok(result);
}
if self.headers.is_none() {
let mut bytes_cache = self.uid_store.byte_cache.lock()?;
let cache = bytes_cache.entry(self.uid).or_default();
if cache.headers.is_some() {
self.headers = cache.headers.clone();
} else {
let mut response = String::with_capacity(8 * 1024);
let mut conn = self.connection.lock().unwrap();
conn.send_command(
format!("UID FETCH {} (FLAGS RFC822.HEADER)", self.uid).as_bytes(),
)?;
conn.read_response(&mut response)?;
debug!(
"fetch response is {} bytes and {} lines",
response.len(),
response.lines().collect::<Vec<&str>>().len()
);
match protocol_parser::uid_fetch_response(response.as_bytes())
.to_full_result()
.map_err(MeliError::from)
{
Ok(v) => {
if v.len() != 1 {
debug!("responses len is {}", v.len());
/* TODO: Trigger cache invalidation here. */
return Err(MeliError::new(format!(
"message with UID {} was not found",
self.uid
)));
}
let (uid, flags, b) = v[0];
assert_eq!(uid, self.uid);
if flags.is_some() {
self.flags.set(flags);
cache.flags = flags;
}
cache.headers =
Some(unsafe { std::str::from_utf8_unchecked(b).to_string() });
}
Err(e) => return Err(e),
}
self.headers = cache.headers.clone();
}
}
Ok(self.headers.as_ref().unwrap().as_bytes())
}
fn fetch_body(&mut self) -> Result<&[u8]> {
if self.bytes.is_some() {
let result =
parser::body_raw(self.bytes.as_ref().unwrap().as_bytes()).to_full_result()?;
return Ok(result);
}
if self.body.is_none() {
let mut bytes_cache = self.uid_store.byte_cache.lock()?;
let cache = bytes_cache.entry(self.uid).or_default();
if cache.body.is_some() {
self.body = cache.body.clone();
} else {
let mut response = String::with_capacity(8 * 1024);
let mut conn = self.connection.lock().unwrap();
conn.send_command(
format!("UID FETCH {} (FLAGS RFC822.TEXT)", self.uid).as_bytes(),
)?;
conn.read_response(&mut response)?;
debug!(
"fetch response is {} bytes and {} lines",
response.len(),
response.lines().collect::<Vec<&str>>().len()
);
match protocol_parser::uid_fetch_response(response.as_bytes())
.to_full_result()
.map_err(MeliError::from)
{
Ok(v) => {
if v.len() != 1 {
debug!("responses len is {}", v.len());
/* TODO: Trigger cache invalidation here. */
return Err(MeliError::new(format!(
"message with UID {} was not found",
self.uid
)));
}
let (uid, flags, b) = v[0];
assert_eq!(uid, self.uid);
if flags.is_some() {
self.flags.set(flags);
}
cache.body = Some(unsafe { std::str::from_utf8_unchecked(b).to_string() });
}
Err(e) => return Err(e),
}
self.body = cache.body.clone();
}
}
Ok(self.body.as_ref().unwrap().as_bytes())
}
fn fetch_flags(&self) -> Flag {
if self.flags.get().is_some() {
return self.flags.get().unwrap();
}
let mut bytes_cache = self.uid_store.byte_cache.lock().unwrap();
let cache = bytes_cache.entry(self.uid).or_default();
if cache.flags.is_some() {
self.flags.set(cache.flags);
} else {
let mut response = String::with_capacity(8 * 1024);
let mut conn = self.connection.lock().unwrap();
conn.send_command(format!("EXAMINE \"{}\"", &self.folder_path,).as_bytes())
.unwrap();
conn.read_response(&mut response).unwrap();
conn.send_command(format!("UID FETCH {} FLAGS", self.uid).as_bytes())
.unwrap();
conn.read_response(&mut response).unwrap();
debug!(
"fetch response is {} bytes and {} lines",
response.len(),
response.lines().collect::<Vec<&str>>().len()
);
match protocol_parser::uid_fetch_flags_response(response.as_bytes())
.to_full_result()
.map_err(MeliError::from)
{
let val = {
let mut bytes_cache = uid_store.byte_cache.lock()?;
let cache = bytes_cache.entry(uid).or_default();
cache.flags
};
Ok(val.unwrap())
Ok(v) => {
if v.len() != 1 {
debug!("responses len is {}", v.len());
/* TODO: Trigger cache invalidation here. */
panic!(format!("message with UID {} was not found", self.uid));
}
let (uid, flags) = v[0];
assert_eq!(uid, self.uid);
cache.flags = Some(flags);
self.flags.set(Some(flags));
}
Err(e) => Err(e).unwrap(),
}
}))
}
self.flags.get().unwrap()
}
fn set_flag(&mut self, _envelope: &mut Envelope, f: Flag, value: bool) -> Result<()> {
let mut flags = self.fetch_flags();
flags.set(f, value);
let mut response = String::with_capacity(8 * 1024);
let mut conn = self.connection.lock().unwrap();
conn.send_command(format!("SELECT \"{}\"", &self.folder_path,).as_bytes())?;
conn.read_response(&mut response)?;
debug!(&response);
conn.send_command(
format!(
"UID STORE {} FLAGS.SILENT ({})",
self.uid,
flags_to_imap_list!(flags)
)
.as_bytes(),
)?;
conn.read_response(&mut response)?;
debug!(&response);
match protocol_parser::uid_fetch_flags_response(response.as_bytes())
.to_full_result()
.map_err(MeliError::from)
{
Ok(v) => {
if v.len() == 1 {
debug!("responses len is {}", v.len());
let (uid, flags) = v[0];
assert_eq!(uid, self.uid);
self.flags.set(Some(flags));
}
}
Err(e) => Err(e).unwrap(),
}
let mut bytes_cache = self.uid_store.byte_cache.lock()?;
let cache = bytes_cache.entry(self.uid).or_default();
cache.flags = Some(flags);
Ok(())
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,311 +0,0 @@
/*
* meli - imap module.
*
* Copyright 2017 - 2023 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
//! Convert [`crate::search::Query`] into IMAP search criteria.
use std::collections::VecDeque;
use crate::{
search::*,
utils::datetime::{formats::IMAP_DATE, timestamp_to_string},
};
mod private {
pub trait Sealed {}
}
pub trait ToImapSearch: private::Sealed {
/// Convert [`crate::search::Query`] into IMAP search criteria.
fn to_imap_search(&self) -> String;
}
impl private::Sealed for Query {}
macro_rules! space_pad {
($s:ident) => {{
if !$s.is_empty() && !$s.ends_with('(') && !$s.ends_with(' ') {
$s.push(' ');
false
} else {
true
}
}};
}
impl ToImapSearch for Query {
fn to_imap_search(&self) -> String {
enum Step<'a> {
Q(&'a Query),
Lit(char),
}
use Step::*;
let mut stack = VecDeque::new();
stack.push_front(Q(self));
let mut s = String::new();
while let Some(q) = stack.pop_front() {
use Query::*;
match q {
Lit(lit) => {
s.push(lit);
}
Q(Subject(t)) => {
space_pad!(s);
s.push_str("SUBJECT \"");
s.extend(escape_double_quote(t).chars());
s.push('"');
}
Q(From(t)) => {
space_pad!(s);
s.push_str("FROM \"");
s.extend(escape_double_quote(t).chars());
s.push('"');
}
Q(To(t)) => {
space_pad!(s);
s.push_str("TO \"");
s.extend(escape_double_quote(t).chars());
s.push('"');
}
Q(Cc(t)) => {
space_pad!(s);
s.push_str("CC \"");
s.extend(escape_double_quote(t).chars());
s.push('"');
}
Q(Bcc(t)) => {
space_pad!(s);
s.push_str("BCC \"");
s.extend(escape_double_quote(t).chars());
s.push('"');
}
Q(AllText(t)) => {
space_pad!(s);
s.push_str("TEXT \"");
s.extend(escape_double_quote(t).chars());
s.push('"');
}
Q(Flags(v)) => {
space_pad!(s);
for f in v {
match f.as_str() {
"draft" => {
s.push_str("DRAFT ");
}
"deleted" => {
s.push_str("DELETED ");
}
"flagged" => {
s.push_str("FLAGGED ");
}
"recent" => {
s.push_str("RECENT ");
}
"seen" | "read" => {
s.push_str("SEEN ");
}
"unseen" | "unread" => {
s.push_str("UNSEEN ");
}
"answered" => {
s.push_str("ANSWERED ");
}
"unanswered" => {
s.push_str("UNANSWERED ");
}
keyword => {
s.push_str("KEYWORD ");
s.push_str(keyword);
s.push(' ');
}
}
}
}
Q(And(q1, q2)) => {
let is_empty = space_pad!(s);
if !is_empty {
stack.push_front(Lit(')'));
}
stack.push_front(Q(q2));
stack.push_front(Q(q1));
if !is_empty {
stack.push_front(Lit('('));
}
}
Q(Or(q1, q2)) => {
space_pad!(s);
s.push_str("OR");
stack.push_front(Q(q2));
stack.push_front(Q(q1));
}
Q(Not(q)) => {
space_pad!(s);
s.push_str("NOT (");
stack.push_front(Lit(')'));
stack.push_front(Q(q));
}
Q(Before(t)) => {
space_pad!(s);
s.push_str("BEFORE ");
s.push_str(&timestamp_to_string(*t, Some(IMAP_DATE), true));
}
Q(After(t)) => {
space_pad!(s);
s.push_str("SINCE ");
s.push_str(&timestamp_to_string(*t, Some(IMAP_DATE), true));
}
Q(Between(t1, t2)) => {
space_pad!(s);
s.push_str("(SINCE ");
s.push_str(&timestamp_to_string(*t1, Some(IMAP_DATE), true));
s.push_str(" BEFORE ");
s.push_str(&timestamp_to_string(*t2, Some(IMAP_DATE), true));
s.push(')');
}
Q(On(t)) => {
space_pad!(s);
s.push_str("ON ");
s.push_str(&timestamp_to_string(*t, Some(IMAP_DATE), true));
}
Q(InReplyTo(t)) => {
space_pad!(s);
s.push_str(r#"HEADER "In-Reply-To" ""#);
s.extend(escape_double_quote(t).chars());
s.push('"');
}
Q(References(t)) => {
space_pad!(s);
s.push_str(r#"HEADER "References" ""#);
s.extend(escape_double_quote(t).chars());
s.push('"');
}
Q(AllAddresses(t)) => {
let is_empty = space_pad!(s);
if !is_empty {
s.push('(');
}
s.push_str("OR FROM \"");
s.extend(escape_double_quote(t).chars());
s.push_str("\" (OR TO \"");
s.extend(escape_double_quote(t).chars());
s.push_str("\" (OR CC \"");
s.extend(escape_double_quote(t).chars());
s.push_str("\" BCC \"");
s.extend(escape_double_quote(t).chars());
s.push_str(r#""))"#);
if !is_empty {
s.push(')');
}
}
Q(Body(t)) => {
space_pad!(s);
s.push_str(r#"BODY ""#);
s.extend(escape_double_quote(t).chars());
s.push('"');
}
Q(HasAttachment) => {
log::warn!("HasAttachment in IMAP is unimplemented.");
}
Q(Answered) => {
space_pad!(s);
s.push_str(r#"ANSWERED ""#);
}
Q(AnsweredBy { by }) => {
space_pad!(s);
s.push_str(r#"HEADER "From" ""#);
s.extend(escape_double_quote(by).chars());
s.push('"');
}
Q(Larger { than }) => {
space_pad!(s);
s.push_str("LARGER ");
s.push_str(&than.to_string());
}
Q(Smaller { than }) => {
space_pad!(s);
s.push_str("SMALLER ");
s.push_str(&than.to_string());
}
}
}
while s.ends_with(' ') {
s.pop();
}
s
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::parsec::Parser;
#[test]
fn test_imap_query_search() {
let (_, q) = query().parse_complete("subject: test and i").unwrap();
assert_eq!(&q.to_imap_search(), r#"SUBJECT "test" TEXT "i""#);
let (_, q) = query().parse_complete("is:unseen").unwrap();
assert_eq!(&q.to_imap_search(), r#"UNSEEN"#);
let (_, q) = query().parse_complete("from:user@example.org").unwrap();
assert_eq!(&q.to_imap_search(), r#"FROM "user@example.org""#);
let (_, q) = query()
.parse_complete(
"from:user@example.org and subject:
\"foobar space\"",
)
.unwrap();
assert_eq!(
&q.to_imap_search(),
r#"FROM "user@example.org" SUBJECT "foobar space""#
);
assert_eq!(
&timestamp_to_string(1685739600, Some(IMAP_DATE), true),
"03-Jun-2023"
);
let (_, q) = query()
.parse_complete("before:2023-06-04 from:user@example.org")
.unwrap();
assert_eq!(
&q.to_imap_search(),
r#"BEFORE 04-Jun-2023 FROM "user@example.org""#
);
let (_, q) = query()
.parse_complete(r#"subject:"wah ah ah" or (from:Manos and from:Sia)"#)
.unwrap();
assert_eq!(
&q.to_imap_search(),
r#"OR SUBJECT "wah ah ah" (FROM "Manos" FROM "Sia")"#
);
let (_, q) = query()
.parse_complete(r#"subject:wo or (all-addresses:Manos)"#)
.unwrap();
assert_eq!(
&q.to_imap_search(),
r#"OR SUBJECT "wo" (OR FROM "Manos" (OR TO "Manos" (OR CC "Manos" BCC "Manos")))"#
);
}
}

View File

@ -0,0 +1,571 @@
use std::fmt;
use std::num::NonZeroUsize;
trait Join {
fn join(&self, sep: char) -> String;
}
impl<T> Join for [T]
where
T: fmt::Display,
{
fn join(&self, sep: char) -> String {
if self.is_empty() {
String::from("")
} else if self.len() == 1 {
format!("{}", self[0])
} else {
format!("{}{}{}", self[0], sep, self[1..].join(sep))
}
}
}
struct Search {
charset: Option<String>,
search_keys: Vec<SearchKey>,
}
impl fmt::Display for Search {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"SEARCH{} {}",
if let Some(ch) = self.charset.as_ref() {
format!(" CHARSET {}", ch)
} else {
format!("")
},
self.search_keys.join(' ')
)
}
}
enum SearchKey {
All,
Answered,
Bcc(String),
Before(String),
Body(String),
Cc(String),
Deleted,
Flagged,
From(String),
Keyword(FlagKeyword),
New,
Old,
On(String),
Recent,
Seen,
Since(String),
Subject(String),
Text(String),
To(String),
Unanswered,
Undeleted,
Unflagged,
Unkeyword(FlagKeyword),
Unseen,
Draft,
Header(String, String), //HeaderFldName
Larger(u64),
Not(Box<SearchKey>),
Or(Box<SearchKey>, Box<SearchKey>),
SentBefore(String), //Date
SentOn(String), //Date
SentSince(String), //Date
Smaller(u64),
Uid(SequenceSet),
Undraft,
SequenceSet(SequenceSet),
And(Vec<SearchKey>),
}
impl fmt::Display for SearchKey {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match self {
SearchKey::All => format!("ALL"),
SearchKey::Answered => format!("ANSWERED"),
SearchKey::Bcc(ref s) => format!("BCC {}", s),
SearchKey::Before(ref s) => format!("BEFORE {}", s),
SearchKey::Body(ref s) => format!("BODY {}", s),
SearchKey::Cc(ref s) => format!("CC {}", s),
SearchKey::Deleted => format!("DELETED"),
SearchKey::Flagged => format!("FLAGGED"),
SearchKey::From(ref s) => format!("FROM {}", s),
SearchKey::Keyword(ref s) => format!("KEYWORD {}", s),
SearchKey::New => format!("NEW"),
SearchKey::Old => format!("OLD"),
SearchKey::On(ref s) => format!("ON {}", s),
SearchKey::Recent => format!("RECENT"),
SearchKey::Seen => format!("SEEN"),
SearchKey::Since(ref s) => format!("SINCE {}", s),
SearchKey::Subject(ref s) => format!("SUBJECT {}", s),
SearchKey::Text(ref s) => format!("TEXT {}", s),
SearchKey::To(ref s) => format!("TO {}", s),
SearchKey::Unanswered => format!("UNANSWERED"),
SearchKey::Undeleted => format!("UNDELETED"),
SearchKey::Unflagged => format!("UNFLAGGED"),
SearchKey::Unkeyword(ref s) => format!("UNKEYWORD {}", s),
SearchKey::Unseen => format!("UNSEEN"),
SearchKey::Draft => format!("DRAFT"),
SearchKey::Header(ref name, ref value) => format!("HEADER {} {}", name, value),
SearchKey::Larger(ref s) => format!("LARGER {}", s),
SearchKey::Not(ref s) => format!("NOT {}", s),
SearchKey::Or(ref a, ref b) => format!("OR {} {}", a, b),
SearchKey::SentBefore(ref s) => format!("SENTBEFORE {}", s),
SearchKey::SentOn(ref s) => format!("SENTON {}", s),
SearchKey::SentSince(ref s) => format!("SENTSINCE {}", s),
SearchKey::Smaller(ref s) => format!("SMALLER {}", s),
SearchKey::Uid(ref s) => format!("UID {}", s),
SearchKey::Undraft => format!("UNDRAFT"),
SearchKey::SequenceSet(ref s) => format!("SEQUENCESET {}", s),
SearchKey::And(ref s) => format!("({})", s.join(' ')),
}
)
}
}
struct Delete {
mailbox: Mailbox,
}
impl fmt::Display for Delete {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "DELETE {}", self.mailbox)
}
}
struct Examine {
mailbox: Mailbox,
}
impl fmt::Display for Examine {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "EXAMINE {}", self.mailbox)
}
}
struct Select {
mailbox: Mailbox,
}
impl fmt::Display for Select {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "SELECT {}", self.mailbox)
}
}
struct List {
mailbox: Mailbox,
list: String,
}
impl fmt::Display for List {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"LIST {} \"{}\"",
if self.mailbox.is_empty() {
format!("\"\"")
} else {
format!("{}", self.mailbox)
},
self.list.as_str()
)
}
}
struct Lsub {
mailbox: Mailbox,
list: String,
}
impl fmt::Display for Lsub {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "LSUB {} \"{}\"", self.mailbox, self.list)
}
}
enum StatusAttribute {
Messages,
Recent,
UidNext,
UidValidity,
Unseen,
}
impl fmt::Display for StatusAttribute {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match self {
StatusAttribute::Messages => "MESSAGES",
StatusAttribute::Recent => "RECENT",
StatusAttribute::UidNext => "UIDNEXT",
StatusAttribute::UidValidity => "UIDVALIDITY",
StatusAttribute::Unseen => "UNSEEN",
}
)
}
}
struct Status {
mailbox: Mailbox,
status_attributes: Vec<StatusAttribute>,
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"STATUS {} ({})",
self.mailbox,
self.status_attributes.join(' ')
)
}
}
struct Store {
sequence_set: SequenceSet,
//store_att_flags:
}
impl fmt::Display for Store {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
unimplemented!()
//write!(f, "STORE {}", self.sequence_set)
}
}
struct Unsubscribe {
mailbox: Mailbox,
}
impl fmt::Display for Unsubscribe {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "UNSUBSCRIBE {}", self.mailbox)
}
}
struct Subscribe {
mailbox: Mailbox,
}
impl fmt::Display for Subscribe {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "SUBSCRIBE {}", self.mailbox)
}
}
struct Copy {
sequence_set: SequenceSet,
mailbox: Mailbox,
}
impl fmt::Display for Copy {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "COPY {} {}", self.sequence_set, self.mailbox)
}
}
struct Create {
mailbox: Mailbox,
}
impl fmt::Display for Create {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "CREATE {}", self.mailbox)
}
}
struct Rename {
from: Mailbox,
to: Mailbox,
}
impl fmt::Display for Rename {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "RENAME {} {}", self.from, self.to)
}
}
struct Append {
mailbox: Mailbox,
flag_list: Vec<Flag>,
date_time: Option<String>,
literal: String,
}
impl fmt::Display for Append {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"APPEND {}{}{} {}",
self.mailbox,
if self.flag_list.is_empty() {
String::from("")
} else {
format!(" {}", self.flag_list.join(' '))
},
if let Some(date_time) = self.date_time.as_ref() {
format!(" {}", date_time)
} else {
String::from("")
},
self.literal.as_str()
)
}
}
struct Fetch {
sequence_set: SequenceSet,
}
impl fmt::Display for Fetch {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "FETCH {}", self.sequence_set)
}
}
enum Flag {
Answered,
Flagged,
Deleted,
Seen,
Draft,
/*atom */
X(String),
}
impl fmt::Display for Flag {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"\\{}",
match self {
Flag::Answered => "Answered",
Flag::Flagged => "Flagged",
Flag::Deleted => "Deleted",
Flag::Seen => "Seen",
Flag::Draft => "Draft",
Flag::X(ref c) => c.as_str(),
}
)
}
}
enum Uid {
Copy(Copy),
Fetch(Fetch),
Search(Search),
Store(Store),
}
impl fmt::Display for Uid {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"UID {}",
match self {
Uid::Copy(ref c) => format!("{}", c),
Uid::Fetch(ref c) => format!("{}", c),
Uid::Search(ref c) => format!("{}", c),
Uid::Store(ref c) => format!("{}", c),
}
)
}
}
enum CommandSelect {
Check,
Close,
Expunge,
Copy(Copy),
Fetch(Fetch),
Store(Store),
Uid(Uid),
Search(Search),
}
impl fmt::Display for CommandSelect {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match self {
CommandSelect::Check => format!("CHECK"),
CommandSelect::Close => format!("CLOSE"),
CommandSelect::Expunge => format!("EXPUNGE"),
CommandSelect::Copy(ref c) => format!("{}", c),
CommandSelect::Fetch(ref c) => format!("{}", c),
CommandSelect::Store(ref c) => format!("{}", c),
CommandSelect::Uid(ref c) => format!("{}", c),
CommandSelect::Search(ref c) => format!("{}", c),
}
)
}
}
/// Valid in all states
enum CommandAny {
Capability,
Logout,
Noop,
XCommand(String),
}
impl fmt::Display for CommandAny {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match self {
CommandAny::Capability => format!("CAPABILITY"),
CommandAny::Logout => format!("LOGOUT"),
CommandAny::Noop => format!("NOOP"),
CommandAny::XCommand(ref x) => format!("{}", x),
}
)
}
}
enum CommandAuth {
Append(Append),
Create(Create),
Delete(Delete),
Examine(Examine),
List(List),
Lsub(Lsub),
Rename(Rename),
Select(Select),
Status(Status),
Subscribe(Subscribe),
Unsubscribe(Unsubscribe),
}
impl fmt::Display for CommandAuth {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match self {
CommandAuth::Append(ref c) => c.to_string(),
CommandAuth::Create(ref c) => c.to_string(),
CommandAuth::Delete(ref c) => c.to_string(),
CommandAuth::Examine(ref c) => c.to_string(),
CommandAuth::List(ref c) => c.to_string(),
CommandAuth::Lsub(ref c) => c.to_string(),
CommandAuth::Rename(ref c) => c.to_string(),
CommandAuth::Select(ref c) => c.to_string(),
CommandAuth::Status(ref c) => c.to_string(),
CommandAuth::Subscribe(ref c) => c.to_string(),
CommandAuth::Unsubscribe(ref c) => c.to_string(),
}
)
}
}
enum CommandNonAuth {
Login(String, String),
Authenticate(String, String),
StartTls,
}
impl fmt::Display for CommandNonAuth {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
CommandNonAuth::Login(ref userid, ref password) => {
write!(f, "LOGIN \"{}\" \"{}\"", userid, password)
}
CommandNonAuth::Authenticate(ref auth_type, ref base64) => {
write!(f, "AUTHENTICATE \"{}\" \"{}\"", auth_type, base64)
}
CommandNonAuth::StartTls => write!(f, "STARTTLS"),
}
}
}
enum Command {
Any(CommandAny),
Auth(CommandAuth),
NonAuth(CommandNonAuth),
Select(CommandSelect),
}
impl fmt::Display for Command {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Command::Any(c) => write!(f, "{}", c),
Command::Auth(c) => write!(f, "{}", c),
Command::NonAuth(c) => write!(f, "{}", c),
Command::Select(c) => write!(f, "{}", c),
}
}
}
pub(super) struct ImapCommand {
tag: usize,
command: Command,
}
impl fmt::Display for ImapCommand {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} {}\r\n", self.tag, self.command)
}
}
enum SeqNumber {
MsgNumber(NonZeroUsize),
UID(NonZeroUsize),
/** "*" represents the largest number in use. In
the case of message sequence numbers, it is the number of messages in a
non-empty mailbox. In the case of unique identifiers, it is the unique
identifier of the last message in the mailbox or, if the mailbox is empty, the
mailbox's current UIDNEXT value **/
Largest,
}
impl fmt::Display for SeqNumber {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SeqNumber::MsgNumber(n) => write!(f, "{}", n),
SeqNumber::UID(u) => write!(f, "{}", u),
SeqNumber::Largest => write!(f, "*"),
}
}
}
struct SeqRange(SeqNumber, SeqNumber);
impl fmt::Display for SeqRange {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}:{}", self.0, self.1)
}
}
struct SequenceSet {
numbers: Vec<SeqNumber>,
ranges: Vec<SeqRange>,
}
impl fmt::Display for SequenceSet {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}{}",
if self.numbers.is_empty() {
String::from("")
} else {
self.numbers.join(',')
},
if self.ranges.is_empty() {
String::from("")
} else {
self.ranges.join(',')
}
)
}
}
type Mailbox = String;
type FlagKeyword = String;

View File

@ -1,546 +0,0 @@
/*
* meli - imap
*
* 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 std::convert::{TryFrom, TryInto};
use imap_codec::{command::CommandBody, search::SearchKey, sequence::SequenceSet};
use super::{ImapConnection, MailboxSelection, UID};
use crate::{
backends::{
imap::protocol_parser::{
generate_envelope_hash, FetchResponse, ImapLineSplit, RequiredResponses,
UntaggedResponse,
},
BackendMailbox, RefreshEvent,
RefreshEventKind::{self, *},
TagHash,
},
email::common_attributes,
error::*,
};
impl ImapConnection {
pub async fn process_untagged(&mut self, line: &[u8]) -> Result<bool> {
macro_rules! try_fail {
($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());
self.add_refresh_event(RefreshEvent {
account_hash: self.uid_store.account_hash,
mailbox_hash: $mailbox_hash,
kind: RefreshEventKind::Failure(err.clone()),
});
Err(err)
} else { Ok(()) }?;)+
};
}
let mailbox_hash = match self.stream.as_ref()?.current_mailbox {
MailboxSelection::Select(h) | MailboxSelection::Examine(h) => h,
MailboxSelection::None => return Ok(false),
};
let mailbox =
std::clone::Clone::clone(&self.uid_store.mailboxes.lock().await[&mailbox_hash]);
#[cfg(not(feature = "sqlite3"))]
let mut cache_handle = super::cache::DefaultCache::get(self.uid_store.clone())?;
#[cfg(feature = "sqlite3")]
let mut cache_handle = super::cache::Sqlite3Cache::get(self.uid_store.clone())?;
let mut response = Vec::with_capacity(8 * 1024);
let untagged_response =
match super::protocol_parser::untagged_responses(line).map(|(_, v, _)| v) {
Ok(None) | Err(_) => {
return Ok(false);
}
Ok(Some(r)) => r,
};
match untagged_response {
UntaggedResponse::Bye { reason } => {
self.uid_store.is_online.lock().unwrap().1 = Err(reason.into());
}
UntaggedResponse::Expunge(n) => {
if self
.uid_store
.msn_index
.lock()
.unwrap()
.get(&mailbox_hash)
.map(|i| i.len() < TryInto::<usize>::try_into(n).unwrap())
.unwrap_or(true)
{
debug!(
"Received expunge {} but mailbox msn index is {:?}",
n,
self.uid_store.msn_index.lock().unwrap().get(&mailbox_hash)
);
self.send_command(CommandBody::search(
None,
SearchKey::SequenceSet(SequenceSet::from(..)),
true,
))
.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![];
let deleteds = 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)>>();
for (deleted_uid, deleted_hash) in deleteds {
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
.uid_store
.msn_index
.lock()
.unwrap()
.entry(mailbox_hash)
.or_default()
.remove(TryInto::<usize>::try_into(n).unwrap().saturating_sub(1));
debug!("expunge {}, UID = {}", n, deleted_uid);
let deleted_hash: crate::email::EnvelopeHash = match self
.uid_store
.uid_index
.lock()
.unwrap()
.remove(&(mailbox_hash, deleted_uid))
{
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()
.unwrap()
.remove(&deleted_hash);
let mut event: [(UID, RefreshEvent); 1] = [(
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, &event)?;
}
self.add_refresh_event(std::mem::replace(
&mut event[0].1,
RefreshEvent {
account_hash: self.uid_store.account_hash,
mailbox_hash,
kind: Rescan,
},
));
}
UntaggedResponse::Exists(n) => {
debug!("exists {}", n);
try_fail!(
mailbox_hash,
self.send_command(CommandBody::fetch(n, common_attributes(), false)?).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 = TagHash::from_bytes(f.as_bytes());
tag_lck.entry(hash).or_insert_with(|| f.to_string());
env.tags_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()
)
})
{
log::info!("{err}");
}
}
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)),
});
}
}
}
UntaggedResponse::Recent(_) => {
try_fail!(
mailbox_hash,
self.send_command(CommandBody::search(None, SearchKey::Recent, true)).await
self.read_response(&mut response, RequiredResponses::SEARCH).await
);
match super::protocol_parser::search_results_raw(&response)
.map(|(_, v)| v)
.map_err(Error::from)
{
Ok(&[]) => {
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_raw(command.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 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 = TagHash::from_bytes(f.as_bytes());
tag_lck.entry(hash).or_insert_with(|| f.to_string());
env.tags_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()
)
})
{
log::info!("{err}");
}
}
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))
{
self.uid_store
.msn_index
.lock()
.unwrap()
.entry(mailbox_hash)
.or_default()
.push(uid);
}
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!(
"UID SEARCH RECENT err: {}\nresp: {}",
e.to_string(),
to_str!(&response)
);
}
}
}
UntaggedResponse::Fetch(FetchResponse {
uid,
message_sequence_number: msg_seq,
modseq,
flags,
body: _,
references: _,
envelope: _,
raw_fetch_value: _,
}) => {
if let Some(flags) = flags {
let uid = if let Some(uid) = uid {
uid
} else {
try_fail!(
mailbox_hash,
self.send_command(CommandBody::search(
None,
SearchKey::SequenceSet(SequenceSet::try_from(msg_seq)?),
true
))
.await,
self.read_response(&mut response, RequiredResponses::SEARCH)
.await,
);
match super::protocol_parser::search_results(
response.split_rn().next().unwrap_or(b""),
)
.map(|(_, v)| v)
{
Ok(mut v) if v.len() == 1 => v.pop().unwrap(),
Ok(_) => {
return Ok(false);
}
Err(e) => {
debug!("SEARCH error failed: {}", e);
debug!(to_str!(&response));
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
} {
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
.modseq
.lock()
.unwrap()
.insert(env_hash, modseq);
}
let mut event: [(UID, RefreshEvent); 1] = [(
uid,
RefreshEvent {
account_hash: self.uid_store.account_hash,
mailbox_hash,
kind: NewFlags(env_hash, flags),
},
)];
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,
},
));
};
}
}
}
Ok(true)
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -19,402 +19,48 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::sync::MutexGuard;
use isahc::config::Configurable;
use super::*;
#[derive(Debug)]
pub struct JmapConnection {
pub session: Arc<Mutex<JmapSession>>,
pub request_no: Arc<Mutex<usize>>,
pub client: Arc<HttpClient>,
pub client: Arc<Mutex<Client>>,
pub online_status: Arc<Mutex<bool>>,
pub server_conf: JmapServerConf,
pub store: Arc<Store>,
pub account_id: Arc<Mutex<String>>,
}
impl JmapConnection {
pub fn new(server_conf: &JmapServerConf, store: Arc<Store>) -> Result<Self> {
let client = HttpClient::builder()
.timeout(std::time::Duration::from_secs(10))
.connection_cache_size(8)
.connection_cache_ttl(std::time::Duration::from_secs(30 * 60))
.default_header("Content-Type", "application/json")
.redirect_policy(RedirectPolicy::Limit(10));
let client = if server_conf.use_token {
client
.authentication(isahc::auth::Authentication::none())
.default_header(
"Authorization",
format!("Bearer {}", &server_conf.server_password),
)
} else {
client
.authentication(isahc::auth::Authentication::basic())
.credentials(isahc::auth::Credentials::new(
&server_conf.server_username,
&server_conf.server_password,
))
};
let client = client.build()?;
pub fn new(server_conf: &JmapServerConf, online_status: Arc<Mutex<bool>>) -> Result<Self> {
use reqwest::header;
let mut headers = header::HeaderMap::new();
headers.insert(
header::AUTHORIZATION,
header::HeaderValue::from_static("fc32dffe-14e7-11ea-a277-2477037a1804"),
);
headers.insert(
header::ACCEPT,
header::HeaderValue::from_static("application/json"),
);
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
);
let client = reqwest::blocking::ClientBuilder::new()
.danger_accept_invalid_certs(server_conf.danger_accept_invalid_certs)
.default_headers(headers)
.build()?;
let res_text = client.get(&server_conf.server_hostname).send()?.text()?;
debug!(&res_text);
let server_conf = server_conf.clone();
Ok(Self {
session: Arc::new(Mutex::new(Default::default())),
Ok(JmapConnection {
request_no: Arc::new(Mutex::new(0)),
client: Arc::new(client),
client: Arc::new(Mutex::new(client)),
online_status,
server_conf,
store,
account_id: Arc::new(Mutex::new(String::new())),
})
}
pub async fn connect(&mut self) -> Result<()> {
if self.store.online_status.lock().await.1.is_ok() {
return Ok(());
}
let mut jmap_session_resource_url = self.server_conf.server_url.to_string();
jmap_session_resource_url.push_str("/.well-known/jmap");
let mut req = self
.client
.get_async(&jmap_session_resource_url)
.await
.map_err(|err| {
//*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
Error::new(format!(
"Could not connect to JMAP server endpoint for {}. Is your server url setting \
correct? (i.e. \"jmap.mailserver.org\") (Note: only session resource \
discovery via /.well-known/jmap is supported. DNS SRV records are not \
suppported.)\nError connecting to server: {}",
&self.server_conf.server_url, &err
))
.set_source(Some(Arc::new(err)))
})?;
if !req.status().is_success() {
let kind: crate::error::NetworkErrorKind = req.status().into();
let res_text = req.text().await.unwrap_or_default();
let err = Error::new(format!(
"Could not connect to JMAP server endpoint for {}. Reply from server: {}",
&self.server_conf.server_url, res_text
))
.set_kind(kind.into());
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
let res_text = req.text().await?;
let session: JmapSession = match serde_json::from_str(&res_text) {
Err(err) => {
let err = Error::new(format!(
"Could not connect to JMAP server endpoint for {}. Is your server url setting \
correct? (i.e. \"jmap.mailserver.org\") (Note: only session resource \
discovery via /.well-known/jmap is supported. DNS SRV records are not \
suppported.)\nReply from server: {}",
&self.server_conf.server_url, &res_text
))
.set_source(Some(Arc::new(err)));
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
if !session
.capabilities
.contains_key("urn:ietf:params:jmap:core")
{
let err = Error::new(format!(
"Server {} did not return JMAP Core capability (urn:ietf:params:jmap:core). \
Returned capabilities were: {}",
&self.server_conf.server_url,
session
.capabilities
.keys()
.map(String::as_str)
.collect::<Vec<&str>>()
.join(", ")
));
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
if !session
.capabilities
.contains_key("urn:ietf:params:jmap:mail")
{
let err = Error::new(format!(
"Server {} does not support JMAP Mail capability (urn:ietf:params:jmap:mail). \
Returned capabilities were: {}",
&self.server_conf.server_url,
session
.capabilities
.keys()
.map(String::as_str)
.collect::<Vec<&str>>()
.join(", ")
));
*self.store.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;
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 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 = Error::new(format!(
"BUG: Could not deserialize {} server JSON response properly, please \
report this!\nReply from server: {}",
&self.server_conf.server_url, &res_text
))
.set_source(Some(Arc::new(err)))
.set_kind(ErrorKind::Bug);
*self.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
let 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(())
}
}

View File

@ -0,0 +1,73 @@
/*
* meli - jmap module.
*
* Copyright 2019 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use super::*;
use crate::backends::FolderPermissions;
#[derive(Debug, Clone)]
pub struct JmapFolder {
pub name: String,
pub path: String,
pub hash: FolderHash,
pub v: Vec<FolderHash>,
pub id: String,
pub is_subscribed: bool,
pub my_rights: JmapRights,
pub parent_id: Option<String>,
pub role: Option<String>,
pub sort_order: u64,
pub total_emails: u64,
pub total_threads: u64,
pub unread_emails: u64,
pub unread_threads: u64,
}
impl BackendFolder for JmapFolder {
fn hash(&self) -> FolderHash {
self.hash
}
fn name(&self) -> &str {
&self.name
}
fn path(&self) -> &str {
&self.path
}
fn change_name(&mut self, _s: &str) {}
fn clone(&self) -> Folder {
Box::new(std::clone::Clone::clone(self))
}
fn children(&self) -> &[FolderHash] {
&self.v
}
fn parent(&self) -> Option<FolderHash> {
None
}
fn permissions(&self) -> FolderPermissions {
FolderPermissions::default()
}
}

View File

@ -1,119 +0,0 @@
/*
* meli - jmap module.
*
* Copyright 2019 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::sync::{Arc, Mutex, RwLock};
use super::*;
use crate::backends::{LazyCountSet, MailboxPermissions, SpecialUsageMailbox};
#[derive(Debug, Clone)]
pub struct JmapMailbox {
pub name: String,
pub path: String,
pub hash: MailboxHash,
pub children: Vec<MailboxHash>,
pub id: Id<MailboxObject>,
pub is_subscribed: bool,
pub my_rights: JmapRights,
pub parent_id: Option<Id<MailboxObject>>,
pub parent_hash: Option<MailboxHash>,
pub role: Option<String>,
pub sort_order: u64,
pub total_emails: Arc<Mutex<LazyCountSet>>,
pub total_threads: u64,
pub unread_emails: Arc<Mutex<LazyCountSet>>,
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 {
fn hash(&self) -> MailboxHash {
self.hash
}
fn name(&self) -> &str {
&self.name
}
fn path(&self) -> &str {
&self.path
}
fn clone(&self) -> Mailbox {
Box::new(std::clone::Clone::clone(self))
}
fn children(&self) -> &[MailboxHash] {
&self.children
}
fn parent(&self) -> Option<MailboxHash> {
self.parent_hash
}
fn permissions(&self) -> MailboxPermissions {
MailboxPermissions::default()
}
fn special_usage(&self) -> SpecialUsageMailbox {
match self.role.as_deref() {
Some("inbox") => SpecialUsageMailbox::Inbox,
Some("archive") => SpecialUsageMailbox::Archive,
Some("junk") => SpecialUsageMailbox::Junk,
Some("trash") => SpecialUsageMailbox::Trash,
Some("drafts") => SpecialUsageMailbox::Drafts,
Some("sent") => SpecialUsageMailbox::Sent,
Some(other) => {
debug!(
"unknown JMAP mailbox role for mailbox {}: {}",
self.path(),
other
);
SpecialUsageMailbox::Normal
}
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
Ok(())
}
fn set_special_usage(&mut self, new_val: SpecialUsageMailbox) -> Result<()> {
*self.usage.write()? = new_val;
Ok(())
}
fn count(&self) -> Result<(usize, usize)> {
Ok((
self.unread_emails.lock()?.len(),
self.total_emails.lock()?.len(),
))
}
}

View File

@ -19,34 +19,14 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use core::marker::PhantomData;
use std::collections::HashMap;
use serde::de::{Deserialize, Deserializer};
use serde_json::{value::RawValue, Value};
use super::*;
use crate::{
backends::jmap::rfc8620::bool_false,
email::address::{Address, MailboxAddress},
utils::datetime,
};
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 {
EnvelopeHash::from_bytes(self.inner.as_bytes())
}
}
use crate::backends::jmap::protocol::*;
use crate::backends::jmap::rfc8620::bool_false;
use serde::de::{Deserialize, Deserializer};
use serde_json::Value;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::Hasher;
// 4.1.1.
// Metadata
@ -87,13 +67,14 @@ impl Id<EmailObject> {
// first character changed from "\" in IMAP to "$" in JMAP and have
// particular semantic meaning:
//
// * "$draft": The Email is a draft the user is composing.
// * "$draft": The Email is a draft the user is composing.
//
// * "$seen": The Email has been read.
// * "$seen": The Email has been read.
//
// * "$flagged": The Email has been flagged for urgent/special attention.
// * "$flagged": The Email has been flagged for urgent/special
// attention.
//
// * "$answered": The Email has been replied to.
// * "$answered": The Email has been replied to.
//
// The IMAP "\Recent" keyword is not exposed via JMAP. The IMAP
// "\Deleted" keyword is also not present: IMAP uses a delete+expunge
@ -118,19 +99,19 @@ impl Id<EmailObject> {
// keywords in common use. New keywords may be established here in
// the future. In particular, note:
//
// * "$forwarded": The Email has been forwarded.
// * "$forwarded": The Email has been forwarded.
//
// * "$phishing": The Email is highly likely to be phishing. Clients SHOULD
// warn users to take care when viewing this Email and disable links and
// attachments.
// * "$phishing": The Email is highly likely to be phishing.
// Clients SHOULD warn users to take care when viewing this Email
// and disable links and attachments.
//
// * "$junk": The Email is definitely spam. Clients SHOULD set this flag
// when users report spam to help train automated spam- detection
// systems.
// * "$junk": The Email is definitely spam. Clients SHOULD set this
// flag when users report spam to help train automated spam-
// detection systems.
//
// * "$notjunk": The Email is definitely not spam. Clients SHOULD set this
// flag when users indicate an Email is legitimate, to help train
// automated spam-detection systems.
// * "$notjunk": The Email is definitely not spam. Clients SHOULD
// set this flag when users indicate an Email is legitimate, to
// help train automated spam-detection systems.
//
// o size: "UnsignedInt" (immutable; server-set)
//
@ -148,81 +129,57 @@ impl Id<EmailObject> {
#[serde(rename_all = "camelCase")]
pub struct EmailObject {
#[serde(default)]
pub id: Id<EmailObject>,
id: Id,
#[serde(default)]
pub blob_id: Id<BlobObject>,
mailbox_ids: HashMap<Id, bool>,
#[serde(default)]
pub mailbox_ids: HashMap<Id<MailboxObject>, bool>,
size: u64,
#[serde(default)]
pub size: u64,
received_at: String,
#[serde(default)]
pub received_at: String,
#[serde(default, deserialize_with = "deserialize_none_default")]
pub message_id: Vec<String>,
to: Vec<EmailAddress>,
#[serde(default)]
pub to: Option<SmallVec<[EmailAddress; 1]>>,
bcc: Vec<EmailAddress>,
#[serde(default)]
pub bcc: Option<Vec<EmailAddress>>,
reply_to: Option<EmailAddress>,
#[serde(default)]
pub reply_to: Option<Vec<EmailAddress>>,
cc: Vec<EmailAddress>,
#[serde(default)]
pub cc: Option<SmallVec<[EmailAddress; 1]>>,
from: Vec<EmailAddress>,
#[serde(default)]
pub sender: Option<Vec<EmailAddress>>,
in_reply_to_email_id: Id,
#[serde(default)]
pub from: Option<SmallVec<[EmailAddress; 1]>>,
keywords: Value,
#[serde(default)]
pub in_reply_to: Option<Vec<String>>,
attached_emails: Option<Id>,
#[serde(default)]
pub references: Option<Vec<String>>,
attachments: Vec<Value>,
#[serde(default)]
pub keywords: HashMap<String, bool>,
blob_id: String,
#[serde(default)]
pub attached_emails: Option<Id<BlobObject>>,
#[serde(default)]
pub 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: String,
#[serde(default)]
pub subject: Option<String>,
subject: String,
#[serde(default)]
pub text_body: Vec<TextBody>,
text_body: Vec<TextBody>,
#[serde(default)]
pub thread_id: Id<ThreadObject>,
#[serde(flatten)]
pub extra: HashMap<String, Value>,
}
/// Deserializer that uses `Default::default()` in place of a present but `null`
/// value. Note that `serde(default)` doesn't apply if the key is present but
/// has a value of `null`.
fn deserialize_none_default<'de, D, T>(deserializer: D) -> std::result::Result<T, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de> + Default,
{
let v = Option::<T>::deserialize(deserializer)?;
Ok(v.unwrap_or_default())
}
impl EmailObject {
_impl!(get keywords, keywords: HashMap<String, bool>);
thread_id: Id,
}
#[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>(
@ -237,14 +194,14 @@ 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 From<EmailAddress> for crate::email::Address {
fn from(val: EmailAddress) -> Self {
let EmailAddress { email, mut name } = val;
impl Into<crate::email::Address> for EmailAddress {
fn into(self) -> crate::email::Address {
let Self { email, mut name } = self;
crate::make_address!((name.take().unwrap_or_default()), email)
}
}
@ -260,172 +217,155 @@ impl std::fmt::Display for EmailAddress {
}
impl std::convert::From<EmailObject> for crate::Envelope {
fn from(mut t: EmailObject) -> Self {
let mut env = Self::new(t.id.into_hash());
if let Ok(d) = crate::email::parser::dates::rfc5322_date(env.date_as_str().as_bytes()) {
fn from(mut t: EmailObject) -> crate::Envelope {
let mut env = crate::Envelope::new(0);
env.set_date(std::mem::replace(&mut t.sent_at, String::new()).as_bytes());
if let Some(d) = crate::email::parser::date(env.date_as_str().as_bytes()) {
env.set_datetime(d);
}
if let Some(ref mut sent_at) = t.sent_at {
let unix = datetime::rfc3339_to_timestamp(sent_at.as_bytes().to_vec()).unwrap_or(0);
env.set_datetime(unix);
env.set_date(std::mem::take(sent_at).as_bytes());
}
if let Some(v) = t.message_id.get(0) {
if let Some(v) = t.headers.get("Message-ID").or(t.headers.get("Message-Id")) {
env.set_message_id(v.as_bytes());
}
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);
}
if let Some(v) = t.headers.get("In-Reply-To") {
env.set_in_reply_to(v.as_bytes());
env.push_references(v.as_bytes());
}
if let Some(v) = t.headers.get("References") {
let parse_result = crate::email::parser::references(v.as_bytes());
if parse_result.is_done() {
for v in parse_result.to_full_result().unwrap() {
env.push_references(v);
}
}
env.set_references(v.as_bytes());
}
if let Some(v) = t.headers.get("Date") {
env.set_date(v.as_bytes());
if let Ok(d) = crate::email::parser::dates::rfc5322_date(v.as_bytes()) {
if let Some(d) = crate::email::parser::date(v.as_bytes()) {
env.set_datetime(d);
}
} else if let Ok(d) = crate::email::parser::dates::rfc5322_date(t.received_at.as_bytes()) {
env.set_datetime(d);
}
env.set_has_attachments(t.has_attachment);
if let Some(ref mut subject) = t.subject {
env.set_subject(std::mem::take(subject).into_bytes());
}
if let Some(ref mut from) = t.from {
env.set_from(
std::mem::take(from)
.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::take(to)
.into_iter()
.map(|addr| addr.into())
.collect::<SmallVec<[crate::email::Address; 1]>>(),
);
}
if let Some(ref mut cc) = t.cc {
env.set_cc(
std::mem::take(cc)
.into_iter()
.map(|addr| addr.into())
.collect::<SmallVec<[crate::email::Address; 1]>>(),
);
}
if let Some(ref mut bcc) = t.bcc {
env.set_bcc(
std::mem::take(bcc)
.into_iter()
.map(|addr| addr.into())
.collect::<Vec<crate::email::Address>>(),
);
}
if let (Some(ref mut r), message_id) = (&mut env.references, &env.message_id) {
r.refs.retain(|r| r != message_id);
env.set_subject(std::mem::replace(&mut t.subject, String::new()).into_bytes());
env.set_from(
std::mem::replace(&mut t.from, Vec::new())
.into_iter()
.map(|addr| addr.into())
.collect::<Vec<crate::email::Address>>(),
);
env.set_to(
std::mem::replace(&mut t.to, Vec::new())
.into_iter()
.map(|addr| addr.into())
.collect::<Vec<crate::email::Address>>(),
);
env.set_cc(
std::mem::replace(&mut t.cc, Vec::new())
.into_iter()
.map(|addr| addr.into())
.collect::<Vec<crate::email::Address>>(),
);
env.set_bcc(
std::mem::replace(&mut t.bcc, Vec::new())
.into_iter()
.map(|addr| addr.into())
.collect::<Vec<crate::email::Address>>(),
);
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);
}
}
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>,
#[serde(default)]
pub charset: String,
#[serde(default)]
pub cid: Option<String>,
#[serde(default)]
pub disposition: Option<String>,
#[serde(default)]
pub headers: Value,
#[serde(default)]
pub language: Option<Vec<String>>,
#[serde(default)]
pub location: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub part_id: Option<String>,
pub size: u64,
struct HtmlBody {
blob_id: Id,
cid: Option<String>,
disposition: String,
headers: Value,
language: Option<Vec<String>>,
location: Option<String>,
name: Option<String>,
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>,
#[serde(default)]
pub charset: String,
#[serde(default)]
pub cid: Option<String>,
#[serde(default)]
pub disposition: Option<String>,
#[serde(default)]
pub headers: Value,
#[serde(default)]
pub language: Option<Vec<String>>,
#[serde(default)]
pub location: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub part_id: Option<String>,
pub size: u64,
struct TextBody {
blob_id: Id,
cid: Option<String>,
disposition: String,
headers: Value,
language: Option<Vec<String>>,
location: Option<String>,
name: Option<String>,
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(Serialize, Debug)]
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct EmailQuery {
#[serde(flatten)]
pub query_call: Query<Filter<EmailFilterCondition, EmailObject>, EmailObject>,
//pub filter: EmailFilterCondition, /* "inMailboxes": [ mailbox.id ] },*/
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,
}
impl Method<EmailObject> for EmailQuery {
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct EmailQueryCall {
pub filter: EmailFilterCondition, /* "inMailboxes": [ folder.id ] },*/
pub collapse_threads: bool,
pub position: u64,
pub fetch_threads: bool,
pub fetch_messages: bool,
pub fetch_message_properties: Vec<MessageProperty>,
}
impl Method<EmailObject> for EmailQueryCall {
const NAME: &'static str = "Email/query";
}
impl EmailQuery {
pub const RESULT_FIELD_IDS: ResultField<Self, EmailObject> = ResultField::<Self, EmailObject> {
field: "/ids",
_ph: PhantomData,
};
pub fn new(query_call: Query<Filter<EmailFilterCondition, EmailObject>, EmailObject>) -> Self {
Self {
query_call,
collapse_threads: false,
}
}
_impl!(collapse_threads: bool);
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct EmailGet {
@ -436,12 +376,10 @@ pub struct EmailGet {
#[serde(default = "bool_false")]
pub fetch_text_body_values: bool,
#[serde(default = "bool_false")]
#[serde(rename = "fetchHTMLBodyValues")]
pub fetch_html_body_values: bool,
#[serde(default = "bool_false")]
pub fetch_all_body_values: bool,
#[serde(default)]
#[serde(skip_serializing_if = "u64_zero")]
pub max_body_value_bytes: u64,
}
@ -451,7 +389,7 @@ impl Method<EmailObject> for EmailGet {
impl EmailGet {
pub fn new(get_call: Get<EmailObject>) -> Self {
Self {
EmailGet {
get_call,
body_properties: Vec::new(),
fetch_text_body_values: false,
@ -472,9 +410,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")]
@ -520,8 +458,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>);
@ -544,23 +482,11 @@ impl EmailFilterCondition {
impl FilterTrait<EmailObject> for EmailFilterCondition {}
impl From<EmailFilterCondition> for FilterCondition<EmailFilterCondition, EmailObject> {
fn from(val: EmailFilterCondition) -> Self {
Self {
cond: val,
_ph: PhantomData,
}
}
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub enum MessageProperty {
ThreadId,
MailboxIds,
Keywords,
Size,
ReceivedAt,
MailboxId,
IsUnread,
IsFlagged,
IsAnswered,
@ -568,310 +494,7 @@ pub enum MessageProperty {
HasAttachment,
From,
To,
Cc,
Bcc,
ReplyTo,
Subject,
SentAt,
Date,
Preview,
Id,
BlobId,
MessageId,
InReplyTo,
Sender,
}
impl From<crate::search::Query> for Filter<EmailFilterCondition, EmailObject> {
fn from(val: crate::search::Query) -> Self {
let mut ret = Self::Condition(EmailFilterCondition::new().into());
fn rec(q: &crate::search::Query, f: &mut Filter<EmailFilterCondition, EmailObject>) {
use datetime::{formats::RFC3339_DATE, timestamp_to_string};
use crate::search::Query::*;
match q {
Subject(t) => {
*f = Filter::Condition(EmailFilterCondition::new().subject(t.clone()).into());
}
From(t) => {
*f = Filter::Condition(EmailFilterCondition::new().from(t.clone()).into());
}
To(t) => {
*f = Filter::Condition(EmailFilterCondition::new().to(t.clone()).into());
}
Cc(t) => {
*f = Filter::Condition(EmailFilterCondition::new().cc(t.clone()).into());
}
Bcc(t) => {
*f = Filter::Condition(EmailFilterCondition::new().bcc(t.clone()).into());
}
AllText(t) => {
*f = Filter::Condition(EmailFilterCondition::new().text(t.clone()).into());
}
Body(t) => {
*f = Filter::Condition(EmailFilterCondition::new().body(t.clone()).into());
}
Before(t) => {
*f = Filter::Condition(
EmailFilterCondition::new()
.before(timestamp_to_string(*t, Some(RFC3339_DATE), true))
.into(),
);
}
After(t) => {
*f = Filter::Condition(
EmailFilterCondition::new()
.after(timestamp_to_string(*t, Some(RFC3339_DATE), true))
.into(),
);
}
Between(a, b) => {
*f = Filter::Condition(
EmailFilterCondition::new()
.after(timestamp_to_string(*a, Some(RFC3339_DATE), true))
.into(),
);
*f &= Filter::Condition(
EmailFilterCondition::new()
.before(timestamp_to_string(*b, Some(RFC3339_DATE), true))
.into(),
);
}
On(t) => {
rec(&Between(*t, *t), f);
}
InReplyTo(ref s) => {
*f = Filter::Condition(
EmailFilterCondition::new()
.header(vec!["In-Reply-To".to_string().into(), s.to_string().into()])
.into(),
);
}
References(ref s) => {
*f = Filter::Condition(
EmailFilterCondition::new()
.header(vec!["References".to_string().into(), s.to_string().into()])
.into(),
);
}
AllAddresses(_) => {
//TODO
}
Flags(v) => {
fn flag_to_filter(f: &str) -> Filter<EmailFilterCondition, EmailObject> {
match f {
"draft" => Filter::Condition(
EmailFilterCondition::new()
.has_keyword("$draft".to_string())
.into(),
),
"flagged" => Filter::Condition(
EmailFilterCondition::new()
.has_keyword("$flagged".to_string())
.into(),
),
"seen" | "read" => Filter::Condition(
EmailFilterCondition::new()
.has_keyword("$seen".to_string())
.into(),
),
"unseen" | "unread" => Filter::Condition(
EmailFilterCondition::new()
.not_keyword("$seen".to_string())
.into(),
),
"answered" => Filter::Condition(
EmailFilterCondition::new()
.has_keyword("$answered".to_string())
.into(),
),
"unanswered" => Filter::Condition(
EmailFilterCondition::new()
.not_keyword("$answered".to_string())
.into(),
),
keyword => Filter::Condition(
EmailFilterCondition::new()
.not_keyword(keyword.to_string())
.into(),
),
}
}
let mut accum = if let Some(first) = v.first() {
flag_to_filter(first.as_str())
} else {
Filter::Condition(EmailFilterCondition::new().into())
};
for f in v.iter().skip(1) {
accum &= flag_to_filter(f.as_str());
}
*f = accum;
}
HasAttachment => {
*f = Filter::Condition(
EmailFilterCondition::new()
.has_attachment(Some(true))
.into(),
);
}
And(q1, q2) => {
let mut rhs = Filter::Condition(EmailFilterCondition::new().into());
let mut lhs = Filter::Condition(EmailFilterCondition::new().into());
rec(q1, &mut rhs);
rec(q2, &mut lhs);
rhs &= lhs;
*f = rhs;
}
Or(q1, q2) => {
let mut rhs = Filter::Condition(EmailFilterCondition::new().into());
let mut lhs = Filter::Condition(EmailFilterCondition::new().into());
rec(q1, &mut rhs);
rec(q2, &mut lhs);
rhs |= lhs;
*f = rhs;
}
Not(q) => {
let mut qhs = Filter::Condition(EmailFilterCondition::new().into());
rec(q, &mut qhs);
*f = !qhs;
}
Answered => {
// TODO
}
AnsweredBy { .. } => {
// TODO
}
Larger { .. } => {
// TODO
}
Smaller { .. } => {
// TODO
}
}
}
rec(&val, &mut ret);
ret
}
}
#[test]
fn test_jmap_query() {
use std::sync::{Arc, Mutex};
let q: crate::search::Query = crate::search::Query::try_from(
"subject:wah or (from:Manos and (subject:foo or subject:bar))",
)
.unwrap();
let f: Filter<EmailFilterCondition, EmailObject> = Filter::from(q);
assert_eq!(
r#"{"operator":"OR","conditions":[{"subject":"wah"},{"operator":"AND","conditions":[{"from":"Manos"},{"operator":"OR","conditions":[{"subject":"foo"},{"subject":"bar"}]}]}]}"#,
serde_json::to_string(&f).unwrap().as_str()
);
let filter = {
let mailbox_id = "mailbox_id".to_string();
let mut r = Filter::Condition(
EmailFilterCondition::new()
.in_mailbox(Some(mailbox_id.into()))
.into(),
);
r &= f;
r
};
let email_call: EmailQuery = EmailQuery::new(
Query::new()
.account_id("account_id".to_string().into())
.filter(Some(filter))
.position(0),
)
.collapse_threads(false);
let request_no = Arc::new(Mutex::new(0));
let mut req = Request::new(request_no.clone());
req.add_call(&email_call);
assert_eq!(
r#"{"using":["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],"methodCalls":[["Email/query",{"accountId":"account_id","calculateTotal":false,"collapseThreads":false,"filter":{"conditions":[{"inMailbox":"mailbox_id"},{"conditions":[{"subject":"wah"},{"conditions":[{"from":"Manos"},{"conditions":[{"subject":"foo"},{"subject":"bar"}],"operator":"OR"}],"operator":"AND"}],"operator":"OR"}],"operator":"AND"},"position":0,"sort":null},"m0"]]}"#,
serde_json::to_string(&req).unwrap().as_str()
);
assert_eq!(*request_no.lock().unwrap(), 1);
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct EmailSet {
#[serde(flatten)]
pub set_call: Set<EmailObject>,
}
impl Method<EmailObject> for EmailSet {
const NAME: &'static str = "Email/set";
}
impl EmailSet {
pub fn new(set_call: Set<EmailObject>) -> Self {
Self { 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 {
Self { 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 {
Self { 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::Error;
fn try_from(t: &RawValue) -> Result<Self> {
let res: (String, Self, String) = serde_json::from_str(t.get()).map_err(|err| {
crate::error::Error::new(format!(
"BUG: Could not deserialize server JSON response properly, please report \
this!\nReply from server: {}",
&t
))
.set_source(Some(Arc::new(err)))
.set_kind(ErrorKind::Bug)
})?;
assert_eq!(&res.0, "Email/queryChanges");
Ok(res.1)
}
}

View File

@ -1,221 +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 serde_json::value::RawValue;
use super::*;
/// #`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 Default for ImportCall {
fn default() -> Self {
Self::new()
}
}
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>);
}
impl Default for EmailImport {
fn default() -> Self {
Self::new()
}
}
#[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::Error;
fn try_from(t: &RawValue) -> Result<Self> {
let res: (String, Self, String) = serde_json::from_str(t.get()).map_err(|err| {
crate::error::Error::new(format!(
"BUG: Could not deserialize server JSON response properly, please report \
this!\nReply from server: {}",
&t
))
.set_source(Some(Arc::new(err)))
.set_kind(ErrorKind::Bug)
})?;
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,20 +21,14 @@
use super::*;
impl Id<MailboxObject> {
pub fn into_hash(&self) -> MailboxHash {
MailboxHash::from_bytes(self.inner.as_bytes())
}
}
#[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,
@ -68,7 +62,7 @@ pub struct MailboxGet {
}
impl MailboxGet {
pub fn new(get_call: Get<MailboxObject>) -> Self {
Self { get_call }
MailboxGet { get_call }
}
}

View File

@ -1,91 +0,0 @@
/*
* meli - jmap module.
*
* Copyright 2017 - 2019 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::sync::Arc;
use super::*;
/// `BackendOp` implementor for Imap
#[derive(Debug, Clone)]
pub struct JmapOp {
hash: EnvelopeHash,
connection: Arc<FutureMutex<JmapConnection>>,
store: Arc<Store>,
}
impl JmapOp {
pub fn new(
hash: EnvelopeHash,
connection: Arc<FutureMutex<JmapConnection>>,
store: Arc<Store>,
) -> Self {
Self {
hash,
connection,
store,
}
}
}
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();
return Ok(Box::pin(async move { Ok(ret.into_bytes()) }));
}
}
let store = self.store.clone();
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 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(),
&blob_id,
None,
))
.await?;
let res_text = res.text().await?;
store
.byte_cache
.lock()
.unwrap()
.entry(hash)
.or_default()
.bytes = Some(res_text.clone());
Ok(res_text.into_bytes())
}))
}
fn fetch_flags(&self) -> ResultFuture<Flag> {
Ok(Box::pin(async { Ok(Flag::default()) }))
}
}

View File

@ -19,13 +19,13 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::convert::{TryFrom, TryInto};
use serde::Serialize;
use super::folder::JmapFolder;
use super::*;
use serde::{de::DeserializeOwned, Serialize};
use serde_json::{json, Value};
use std::convert::TryFrom;
use super::{mailbox::JmapMailbox, *};
pub type Id = String;
pub type UtcDate = String;
use super::rfc8620::Object;
@ -47,7 +47,17 @@ pub trait Method<OBJ: Object>: Serialize {
const NAME: &'static str;
}
static USING: &[&str] = &["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"];
macro_rules! get_path_hash {
($path:expr) => {{
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
$path.hash(&mut hasher);
hasher.finish()
}};
}
static USING: &'static [&'static str] = &["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"];
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
@ -63,7 +73,7 @@ pub struct Request {
impl Request {
pub fn new(request_no: Arc<Mutex<usize>>) -> Self {
Self {
Request {
using: USING,
method_calls: Vec::new(),
request_no,
@ -78,56 +88,45 @@ impl Request {
}
}
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(),
serde_json::to_string(&json!({
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
"methodCalls": [["Mailbox/get", {
"accountId": conn.mail_account_id()
},
format!("#m{}",seq).as_str()]],
}))?,
)
.await?;
#[derive(Serialize, Debug)]
#[serde(untagged)]
pub enum MethodCall {
#[serde(rename_all = "camelCase")]
EmailQuery {
filter: Vec<String>, /* "inMailboxes": [ folder.id ] },*/
collapse_threads: bool,
position: u64,
fetch_threads: bool,
fetch_messages: bool,
fetch_message_properties: Vec<MessageProperty>,
},
MailboxGet {},
Empty {},
}
let res_text = res.text().await?;
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = Error::new(format!(
"BUG: Could not deserialize {} server JSON response properly, please report \
this!\nReply from server: {}",
&conn.server_conf.server_url, &res_text
))
.set_source(Some(Arc::new(err)))
.set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
*conn.store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
pub fn get_mailboxes(conn: &JmapConnection) -> Result<FnvHashMap<FolderHash, JmapFolder>> {
let seq = get_request_no!(conn.request_no);
let res = conn
.client
.lock()
.unwrap()
.post("https://jmap-proxy.local/jmap/fc32dffe-14e7-11ea-a277-2477037a1804/")
.json(&json!({
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
"methodCalls": [["Mailbox/get", {},
format!("#m{}",seq).as_str()]],
}))
.send();
let res_text = res?.text()?;
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
*conn.online_status.lock().unwrap() = true;
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 {
@ -143,109 +142,168 @@ 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 = get_path_hash!(&name);
(
hash,
JmapMailbox {
JmapFolder {
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,
total_emails: Arc::new(Mutex::new(total_emails)),
total_emails,
total_threads,
unread_emails: Arc::new(Mutex::new(unread_emails)),
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 {
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>>> {
let email_call: EmailQuery = EmailQuery::new(
Query::new()
.account_id(conn.mail_account_id())
.filter(Some(Filter::Condition(
EmailFilterCondition::new()
.in_mailbox(Some(mailbox.id.clone()))
.into(),
)))
.position(0),
)
.collapse_threads(false);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct JsonResponse<'a> {
#[serde(borrow)]
method_responses: Vec<MethodResponse<'a>>,
}
pub fn get_message_list(conn: &JmapConnection, folder: &JmapFolder) -> Result<Vec<String>> {
let seq = get_request_no!(conn.request_no);
let email_call: EmailQueryCall = EmailQueryCall {
filter: EmailFilterCondition::new().in_mailbox(Some(folder.id.clone())),
collapse_threads: false,
position: 0,
fetch_threads: true,
fetch_messages: true,
fetch_message_properties: vec![
MessageProperty::ThreadId,
MessageProperty::MailboxId,
MessageProperty::IsUnread,
MessageProperty::IsFlagged,
MessageProperty::IsAnswered,
MessageProperty::IsDraft,
MessageProperty::HasAttachment,
MessageProperty::From,
MessageProperty::To,
MessageProperty::Subject,
MessageProperty::Date,
MessageProperty::Preview,
],
};
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)?)
.await?;
/*
{
"using": [
"urn:ietf:params:jmap:core",
"urn:ietf:params:jmap:mail"
],
"methodCalls": [[
"Email/query",
{
"collapseThreads": false,
"fetchMessageProperties": [
"threadId",
"mailboxId",
"isUnread",
"isFlagged",
"isAnswered",
"isDraft",
"hasAttachment",
"from",
"to",
"subject",
"date",
"preview"
],
"fetchMessages": true,
"fetchThreads": true,
"filter": [
{
"inMailboxes": [
"fde49e47-14e7-11ea-a277-2477037a1804"
]
}
],
"position": 0
},
"f"
]]
}
*/
/*
r#"
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
"methodCalls": [["Email/query", { "filter": {
"inMailboxes": [ folder.id ]
},
"collapseThreads": false,
"position": 0,
"fetchThreads": true,
"fetchMessages": true,
"fetchMessageProperties": [
"threadId",
"mailboxId",
"isUnread",
"isFlagged",
"isAnswered",
"isDraft",
"hasAttachment",
"from",
"to",
"subject",
"date",
"preview"
],
}, format!("m{}", seq).as_str()]],
});"
);*/
let res_text = res.text().await?;
let mut v: MethodResponse = match serde_json::from_str(&res_text) {
Err(err) => {
let err = Error::new(format!(
"BUG: Could not deserialize {} server JSON response properly, please report \
this!\nReply from server: {}",
&conn.server_conf.server_url, &res_text
))
.set_source(Some(Arc::new(err)))
.set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
*conn.store.online_status.lock().await = (std::time::Instant::now(), Ok(()));
let res = conn
.client
.lock()
.unwrap()
.post("https://jmap-proxy.local/jmap/fc32dffe-14e7-11ea-a277-2477037a1804/")
.json(&req)
.send();
let res_text = res?.text()?;
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
*conn.online_status.lock().unwrap() = true;
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>> {
pub fn get_message(conn: &JmapConnection, ids: &[String]) -> Result<Vec<Envelope>> {
let seq = get_request_no!(conn.request_no);
let email_call: EmailGet = EmailGet::new(
Get::new()
.ids(Some(JmapArgument::value(ids.to_vec())))
.account_id(conn.mail_account_id().to_string()),
.ids(Some(JmapArgument::value(
ids.iter().cloned().collect::<Vec<String>>(),
)))
.account_id(conn.account_id.lock().unwrap().clone()),
);
let mut req = Request::new(conn.request_no.clone());
req.add_call(&email_call);
let mut res = conn
let res = conn
.client
.post_async(&conn.session.api_url, serde_json::to_string(&req)?)
.await?;
.lock()
.unwrap()
.post("https://jmap-proxy.local/jmap/fc32dffe-14e7-11ea-a277-2477037a1804/")
.json(&req)
.send();
let res_text = res.text().await?;
let res_text = res?.text()?;
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;
@ -254,25 +312,46 @@ pub async fn get_message(conn: &JmapConnection, ids: &[String]) -> Result<Vec<En
.map(std::convert::Into::into)
.collect::<Vec<Envelope>>())
}
/*
*
*json!({
"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
"methodCalls": [["Email/get", {
"ids": ids,
"properties": [ "threadId", "mailboxIds", "from", "subject",
"receivedAt",
"htmlBody", "bodyValues" ],
"bodyProperties": [ "partId", "blobId", "size", "type" ],
"fetchHTMLBodyValues": true,
"maxBodyValueBytes": 256
}, format!("m{}", seq).as_str()]],
}))
*/
pub async fn fetch(
conn: &JmapConnection,
store: &Store,
mailbox_hash: MailboxHash,
) -> Result<Vec<Envelope>> {
let mailbox_id = store.mailboxes.read().unwrap()[&mailbox_hash].id.clone();
let email_query_call: EmailQuery = EmailQuery::new(
Query::new()
.account_id(conn.mail_account_id())
.filter(Some(Filter::Condition(
EmailFilterCondition::new()
.in_mailbox(Some(mailbox_id))
.into(),
)))
.position(0),
)
.collapse_threads(false);
pub fn get(conn: &JmapConnection, folder: &JmapFolder) -> Result<Vec<Envelope>> {
let email_query_call: EmailQueryCall = EmailQueryCall {
filter: EmailFilterCondition::new().in_mailbox(Some(folder.id.clone())),
collapse_threads: false,
position: 0,
fetch_threads: true,
fetch_messages: true,
fetch_message_properties: vec![
MessageProperty::ThreadId,
MessageProperty::MailboxId,
MessageProperty::IsUnread,
MessageProperty::IsFlagged,
MessageProperty::IsAnswered,
MessageProperty::IsDraft,
MessageProperty::HasAttachment,
MessageProperty::From,
MessageProperty::To,
MessageProperty::Subject,
MessageProperty::Date,
MessageProperty::Preview,
],
};
let mut req = Request::new(conn.request_no.clone());
let prev_seq = req.add_call(&email_query_call);
@ -281,112 +360,29 @@ pub async fn fetch(
Get::new()
.ids(Some(JmapArgument::reference(
prev_seq,
EmailQuery::RESULT_FIELD_IDS,
&email_query_call,
"/ids",
)))
.account_id(conn.mail_account_id()),
.account_id(conn.account_id.lock().unwrap().clone()),
);
req.add_call(&email_call);
let api_url = conn.session.lock().unwrap().api_url.clone();
let mut res = conn
let 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 = Error::new(format!(
"BUG: Could not deserialize {} server JSON response properly, please report \
this!\nReply from server: {}",
&conn.server_conf.server_url, &res_text
))
.set_source(Some(Arc::new(err)))
.set_kind(ErrorKind::Bug);
*conn.store.online_status.lock().await = (Instant::now(), Err(err.clone()));
return Err(err);
}
Ok(s) => s,
};
let 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()
.lock()
.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),
)
})
.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?;
}
}
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)
}
.post("https://jmap-proxy.local/jmap/fc32dffe-14e7-11ea-a277-2477037a1804/")
.json(&req)
.send();
pub fn keywords_to_flags(keywords: Vec<String>) -> (Flag, Vec<String>) {
let mut f = Flag::default();
let mut tags = vec![];
for k in keywords {
match k.as_str() {
"$draft" => {
f |= Flag::DRAFT;
}
"$seen" => {
f |= Flag::SEEN;
}
"$flagged" => {
f |= Flag::FLAGGED;
}
"$answered" => {
f |= Flag::REPLIED;
}
"$junk" | "$notjunk" => { /* ignore */ }
_ => tags.push(k),
}
}
(f, tags)
let res_text = res?.text()?;
let mut v: MethodResponse = serde_json::from_str(&res_text).unwrap();
let e = GetResponse::<EmailObject>::try_from(v.method_responses.pop().unwrap())?;
let GetResponse::<EmailObject> { list, .. } = e;
Ok(list
.into_iter()
.map(std::convert::Into::into)
.collect::<Vec<Envelope>>())
}

File diff suppressed because it is too large Load Diff

View File

@ -19,10 +19,10 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use crate::backends::jmap::{
protocol::Method,
rfc8620::{Object, ResultField},
};
use crate::backends::jmap::protocol::Method;
use crate::backends::jmap::rfc8620::Object;
use serde::de::DeserializeOwned;
use serde::ser::{Serialize, SerializeStruct, Serializer};
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
@ -37,18 +37,18 @@ pub enum JmapArgument<T> {
impl<T> JmapArgument<T> {
pub fn value(v: T) -> Self {
Self::Value(v)
JmapArgument::Value(v)
}
pub fn reference<M, OBJ>(result_of: usize, path: ResultField<M, OBJ>) -> Self
pub fn reference<M, OBJ>(result_of: usize, method: &M, path: &str) -> Self
where
M: Method<OBJ>,
OBJ: Object,
{
Self::ResultReference {
JmapArgument::ResultReference {
result_of: format!("m{}", result_of),
name: M::NAME.to_string(),
path: path.field.to_string(),
path: path.to_string(),
}
}
}

View File

@ -32,7 +32,7 @@ pub struct Comparator<OBJ: Object> {
//#[serde(flatten)]
additional_properties: Vec<String>,
_ph: PhantomData<fn() -> OBJ>,
_ph: PhantomData<*const OBJ>,
}
impl<OBJ: Object> Comparator<OBJ> {
@ -52,8 +52,10 @@ impl<OBJ: Object> Comparator<OBJ> {
_impl!(additional_properties: Vec<String>);
}
impl<OBJ: Object> Default for Comparator<OBJ> {
fn default() -> Self {
Self::new()
}
#[derive(Serialize, Debug)]
#[serde(rename_all = "UPPERCASE")]
pub enum FilterOperator {
And,
Or,
Not,
}

View File

@ -21,7 +21,7 @@
use super::*;
pub trait FilterTrait<T>: Default {}
pub trait FilterTrait<T> {}
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
@ -33,107 +33,18 @@ pub enum Filter<F: FilterTrait<OBJ>, OBJ: Object> {
Condition(FilterCondition<F, OBJ>),
}
impl<F: FilterTrait<OBJ>, OBJ: Object> FilterTrait<OBJ> for Filter<F, OBJ> {}
impl<F: FilterTrait<OBJ>, OBJ: Object> FilterTrait<OBJ> for FilterCondition<F, OBJ> {}
#[derive(Serialize, Debug)]
pub struct FilterCondition<F: FilterTrait<OBJ>, OBJ: Object> {
#[serde(flatten)]
pub cond: F,
cond: F,
#[serde(skip)]
pub _ph: PhantomData<fn() -> OBJ>,
_ph: PhantomData<*const OBJ>,
}
#[derive(Serialize, Debug, Eq, PartialEq)]
#[derive(Serialize, Debug)]
#[serde(rename_all = "UPPERCASE")]
pub enum FilterOperator {
And,
Or,
Not,
}
impl<F: FilterTrait<OBJ>, OBJ: Object> FilterCondition<F, OBJ> {
pub fn new() -> Self {
Self {
cond: F::default(),
_ph: PhantomData,
}
}
}
impl<F: FilterTrait<OBJ>, OBJ: Object> Default for FilterCondition<F, OBJ> {
fn default() -> Self {
Self::new()
}
}
impl<F: FilterTrait<OBJ>, OBJ: Object> Default for Filter<F, OBJ> {
fn default() -> Self {
Self::Condition(FilterCondition::default())
}
}
use std::ops::{BitAndAssign, BitOrAssign, Not};
impl<F: FilterTrait<OBJ>, OBJ: Object> BitAndAssign for Filter<F, OBJ> {
fn bitand_assign(&mut self, rhs: Self) {
match self {
Self::Operator {
operator: FilterOperator::And,
ref mut conditions,
} => {
conditions.push(rhs);
}
Self::Condition(_) | Self::Operator { .. } => {
*self = Self::Operator {
operator: FilterOperator::And,
conditions: vec![
std::mem::replace(self, Self::Condition(FilterCondition::new())),
rhs,
],
};
}
}
}
}
impl<F: FilterTrait<OBJ>, OBJ: Object> BitOrAssign for Filter<F, OBJ> {
fn bitor_assign(&mut self, rhs: Self) {
match self {
Self::Operator {
operator: FilterOperator::Or,
ref mut conditions,
} => {
conditions.push(rhs);
}
Self::Condition(_) | Self::Operator { .. } => {
*self = Self::Operator {
operator: FilterOperator::Or,
conditions: vec![
std::mem::replace(self, Self::Condition(FilterCondition::new())),
rhs,
],
};
}
}
}
}
impl<F: FilterTrait<OBJ>, OBJ: Object> Not for Filter<F, OBJ> {
type Output = Self;
fn not(self) -> Self {
match self {
Self::Operator {
operator,
conditions,
} if operator == FilterOperator::Not => Self::Operator {
operator: FilterOperator::Or,
conditions,
},
Self::Condition(_) | Self::Operator { .. } => Self::Operator {
operator: FilterOperator::Not,
conditions: vec![self],
},
}
}
}

View File

@ -23,40 +23,32 @@
mod backend;
pub use self::backend::*;
mod stream;
use std::{
collections::hash_map::DefaultHasher,
fs,
hash::{Hash, Hasher},
io::{BufReader, Read},
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use crate::backends::*;
use crate::email::parser;
use crate::email::{Envelope, Flag};
use crate::error::{MeliError, Result};
use crate::shellexpand::ShellExpandTrait;
use futures::stream::Stream;
pub use stream::*;
use crate::{
backends::*,
email::Flag,
error::{Error, Result},
utils::shellexpand::ShellExpandTrait,
};
use memmap::{Mmap, Protection};
use std::collections::hash_map::DefaultHasher;
use std::fs;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
/// `BackendOp` implementor for Maildir
#[derive(Debug)]
pub struct MaildirOp {
hash_index: HashIndexes,
mailbox_hash: MailboxHash,
folder_hash: FolderHash,
hash: EnvelopeHash,
slice: Option<Vec<u8>>,
slice: Option<Mmap>,
}
impl Clone for MaildirOp {
fn clone(&self) -> Self {
Self {
MaildirOp {
hash_index: self.hash_index.clone(),
mailbox_hash: self.mailbox_hash,
folder_hash: self.folder_hash,
hash: self.hash,
slice: None,
}
@ -64,212 +56,60 @@ impl Clone for MaildirOp {
}
impl MaildirOp {
pub fn new(hash: EnvelopeHash, hash_index: HashIndexes, mailbox_hash: MailboxHash) -> Self {
Self {
pub fn new(hash: EnvelopeHash, hash_index: HashIndexes, folder_hash: FolderHash) -> Self {
MaildirOp {
hash_index,
mailbox_hash,
folder_hash,
hash,
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);
let map = &map[&self.folder_hash];
debug!("looking for {} in {} map", self.hash, self.folder_hash);
if !map.contains_key(&self.hash) {
debug!("doesn't contain it though len = {}\n{:#?}", map.len(), map);
for e in map.iter() {
debug!("{:#?}", e);
}
return Err(Error::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 BackendOp for MaildirOp {
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
impl<'a> BackendOp for MaildirOp {
fn description(&self) -> String {
format!("Path of file: {}", self.path().display())
}
fn as_bytes(&mut self) -> Result<&[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());
Ok(Box::pin(async move { ret }))
/* Unwrap is safe since we use ? above. */
Ok(unsafe { self.slice.as_ref().unwrap().as_slice() })
}
fn fetch_flags(&self) -> ResultFuture<Flag> {
let path = self.path()?;
let ret = Ok(path.flags());
Ok(Box::pin(async move { ret }))
fn fetch_headers(&mut self) -> Result<&[u8]> {
let raw = self.as_bytes()?;
let result = parser::headers_raw(raw).to_full_result()?;
Ok(result)
}
}
#[derive(Debug, Default, Clone)]
pub struct MaildirMailbox {
hash: MailboxHash,
name: String,
fs_path: PathBuf,
path: PathBuf,
parent: Option<MailboxHash>,
children: Vec<MailboxHash>,
pub usage: Arc<RwLock<SpecialUsageMailbox>>,
pub is_subscribed: bool,
permissions: MailboxPermissions,
pub total: Arc<Mutex<usize>>,
pub unseen: Arc<Mutex<usize>>,
}
impl MaildirMailbox {
pub fn new(
path: String,
file_name: String,
parent: Option<MailboxHash>,
children: Vec<MailboxHash>,
accept_invalid: bool,
settings: &AccountSettings,
) -> Result<Self> {
let pathbuf = PathBuf::from(&path);
let mut h = DefaultHasher::new();
pathbuf.hash(&mut h);
/* Check if mailbox path (Eg `INBOX/Lists/luddites`) is included in the
* subscribed mailboxes in user configuration */
let fname = pathbuf
.strip_prefix(
PathBuf::from(&settings.root_mailbox)
.expand()
.parent()
.unwrap_or_else(|| Path::new("/")),
)
.ok();
let read_only = if let Ok(metadata) = std::fs::metadata(&pathbuf) {
metadata.permissions().readonly()
} else {
true
};
let ret = Self {
hash: MailboxHash(h.finish()),
name: file_name,
path: fname.unwrap().to_path_buf(),
fs_path: pathbuf,
parent,
children,
usage: Arc::new(RwLock::new(SpecialUsageMailbox::Normal)),
is_subscribed: false,
permissions: MailboxPermissions {
create_messages: !read_only,
remove_messages: !read_only,
set_flags: !read_only,
create_child: !read_only,
rename_messages: !read_only,
delete_messages: !read_only,
delete_mailbox: !read_only,
change_permissions: false,
},
unseen: Arc::new(Mutex::new(0)),
total: Arc::new(Mutex::new(0)),
};
if !accept_invalid {
ret.is_valid()?;
}
Ok(ret)
fn fetch_body(&mut self) -> Result<&[u8]> {
let raw = self.as_bytes()?;
let result = parser::body_raw(raw).to_full_result()?;
Ok(result)
}
pub fn fs_path(&self) -> &Path {
self.fs_path.as_path()
}
fn is_valid(&self) -> Result<()> {
let path = self.fs_path();
let mut p = PathBuf::from(path);
for d in &["cur", "new", "tmp"] {
p.push(d);
if !p.is_dir() {
return Err(Error::new(format!(
"{} is not a valid maildir mailbox",
path.display()
)));
}
p.pop();
}
Ok(())
}
}
impl BackendMailbox for MaildirMailbox {
fn hash(&self) -> MailboxHash {
self.hash
}
fn name(&self) -> &str {
&self.name
}
fn path(&self) -> &str {
self.path.to_str().unwrap_or_else(|| self.name())
}
fn children(&self) -> &[MailboxHash] {
&self.children
}
fn clone(&self) -> Mailbox {
Box::new(std::clone::Clone::clone(self))
}
fn special_usage(&self) -> SpecialUsageMailbox {
*self.usage.read().unwrap()
}
fn parent(&self) -> Option<MailboxHash> {
self.parent
}
fn permissions(&self) -> MailboxPermissions {
self.permissions
}
fn is_subscribed(&self) -> bool {
self.is_subscribed
}
fn set_is_subscribed(&mut self, new_val: bool) -> Result<()> {
self.is_subscribed = new_val;
Ok(())
}
fn set_special_usage(&mut self, new_val: SpecialUsageMailbox) -> Result<()> {
*self.usage.write()? = new_val;
Ok(())
}
fn count(&self) -> Result<(usize, usize)> {
Ok((*self.unseen.lock()?, *self.total.lock()?))
}
}
pub trait MaildirPathTrait {
fn flags(&self) -> Flag;
}
impl MaildirPathTrait for Path {
fn flags(&self) -> Flag {
fn fetch_flags(&self) -> Flag {
let mut flag = Flag::default();
let path = self.to_string_lossy();
let path = self.path();
let path = path.to_str().unwrap(); // Assume UTF-8 validity
if !path.contains(":2,") {
return flag;
}
@ -284,15 +124,199 @@ impl MaildirPathTrait for Path {
'S' => flag |= Flag::SEEN,
'T' => flag |= Flag::TRASHED,
_ => {
debug!(
"DEBUG: in MaildirPathTrait::flags(), encountered unknown flag marker \
{:?}, path is {}",
f, path
);
debug!("DEBUG: in fetch_flags, path is {}", path);
}
}
}
flag
}
fn set_flag(&mut self, envelope: &mut Envelope, f: Flag, value: bool) -> Result<()> {
let path = self.path();
let path = path.to_str().unwrap(); // Assume UTF-8 validity
let idx: usize = path
.rfind(":2,")
.ok_or_else(|| MeliError::new(format!("Invalid email filename: {:?}", self)))?
+ 3;
let mut new_name: String = path[..idx].to_string();
let mut flags = self.fetch_flags();
flags.set(f, value);
if !(flags & Flag::DRAFT).is_empty() {
new_name.push('D');
}
if !(flags & Flag::FLAGGED).is_empty() {
new_name.push('F');
}
if !(flags & Flag::PASSED).is_empty() {
new_name.push('P');
}
if !(flags & Flag::REPLIED).is_empty() {
new_name.push('R');
}
if !(flags & Flag::SEEN).is_empty() {
new_name.push('S');
}
if !(flags & Flag::TRASHED).is_empty() {
new_name.push('T');
}
let old_hash = envelope.hash();
let new_name: PathBuf = new_name.into();
let hash_index = self.hash_index.clone();
let mut map = hash_index.lock().unwrap();
let map = map.entry(self.folder_hash).or_default();
map.entry(old_hash).or_default().modified = Some(PathMod::Path(new_name.clone()));
debug!("renaming {:?} to {:?}", path, new_name);
fs::rename(&path, &new_name)?;
debug!("success in rename");
Ok(())
}
}
#[derive(Debug, Default)]
pub struct MaildirFolder {
hash: FolderHash,
name: String,
fs_path: PathBuf,
path: PathBuf,
parent: Option<FolderHash>,
children: Vec<FolderHash>,
permissions: FolderPermissions,
}
impl MaildirFolder {
pub fn new(
path: String,
file_name: String,
parent: Option<FolderHash>,
children: Vec<FolderHash>,
settings: &AccountSettings,
) -> Result<Self> {
macro_rules! strip_slash {
($v:expr) => {
if $v.ends_with("/") {
&$v[..$v.len() - 1]
} else {
$v
}
};
}
let pathbuf = PathBuf::from(&path);
let mut h = DefaultHasher::new();
pathbuf.hash(&mut h);
/* Check if folder path (Eg `INBOX/Lists/luddites`) is included in the subscribed
* mailboxes in user configuration */
let fname = if let Ok(fname) = pathbuf.strip_prefix(
PathBuf::from(&settings.root_folder)
.expand()
.parent()
.unwrap_or_else(|| &Path::new("/")),
) {
if fname.components().count() != 0
&& !settings
.subscribed_folders
.iter()
.any(|x| x == strip_slash!(fname.to_str().unwrap()))
{
return Err(MeliError::new(format!(
"Folder with name `{}` is not included in configured subscribed mailboxes",
fname.display()
)));
}
Some(fname)
} else {
None
};
let read_only = if let Ok(metadata) = std::fs::metadata(&pathbuf) {
metadata.permissions().readonly()
} else {
true
};
let ret = MaildirFolder {
hash: h.finish(),
name: file_name,
path: fname.unwrap().to_path_buf(),
fs_path: pathbuf,
parent,
children,
permissions: FolderPermissions {
create_messages: !read_only,
remove_messages: !read_only,
set_flags: !read_only,
create_child: !read_only,
rename_messages: !read_only,
delete_messages: !read_only,
delete_mailbox: !read_only,
change_permissions: false,
},
};
ret.is_valid()?;
Ok(ret)
}
pub fn fs_path(&self) -> &Path {
self.fs_path.as_path()
}
fn is_valid(&self) -> Result<()> {
let path = self.fs_path();
let mut p = PathBuf::from(path);
for d in &["cur", "new", "tmp"] {
p.push(d);
if !p.is_dir() {
return Err(MeliError::new(format!(
"{} is not a valid maildir folder",
path.display()
)));
}
p.pop();
}
Ok(())
}
}
impl BackendFolder for MaildirFolder {
fn hash(&self) -> FolderHash {
self.hash
}
fn name(&self) -> &str {
&self.name
}
fn path(&self) -> &str {
self.path.to_str().unwrap_or(self.name())
}
fn change_name(&mut self, s: &str) {
self.name = s.to_string();
}
fn children(&self) -> &[FolderHash] {
&self.children
}
fn clone(&self) -> Folder {
Box::new(MaildirFolder {
hash: self.hash,
name: self.name.clone(),
fs_path: self.fs_path.clone(),
path: self.path.clone(),
children: self.children.clone(),
parent: self.parent,
permissions: self.permissions,
})
}
fn parent(&self) -> Option<FolderHash> {
self.parent
}
fn permissions(&self) -> FolderPermissions {
self.permissions
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,140 +0,0 @@
/*
* meli - maildir async
*
* 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 core::{future::Future, pin::Pin};
use std::{
io::{self, Read},
path::PathBuf,
sync::{Arc, Mutex},
};
use futures::{
stream::{FuturesUnordered, StreamExt},
task::{Context, Poll},
};
use super::*;
use crate::backends::maildir::backend::move_to_cur;
type Payload = Pin<Box<dyn Future<Output = Result<Vec<Envelope>>> + Send + 'static>>;
pub struct MaildirStream {
payloads: Pin<Box<FuturesUnordered<Payload>>>,
}
impl MaildirStream {
#[allow(clippy::type_complexity, clippy::new_ret_no_self)]
pub fn new(
mailbox_hash: MailboxHash,
unseen: Arc<Mutex<usize>>,
total: Arc<Mutex<usize>>,
mut 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();
}
path.pop();
path.push("cur");
let files: Vec<PathBuf> = path
.read_dir()?
.flatten()
.map(|e| e.path())
.collect::<Vec<_>>();
let payloads = Box::pin(if !files.is_empty() {
files
.chunks(chunk_size)
.map(|chunk| {
Box::pin(Self::chunk(
SmallVec::from(chunk),
mailbox_hash,
unseen.clone(),
total.clone(),
map.clone(),
mailbox_index.clone(),
)) as Pin<Box<dyn Future<Output = _> + Send + 'static>>
})
.collect::<_>()
} else {
FuturesUnordered::new()
});
Ok(Self { payloads }.boxed())
}
async fn chunk(
chunk: SmallVec<[std::path::PathBuf; 2048]>,
mailbox_hash: MailboxHash,
unseen: Arc<Mutex<usize>>,
total: Arc<Mutex<usize>>,
map: HashIndexes,
mailbox_index: Arc<Mutex<HashMap<EnvelopeHash, MailboxHash>>>,
) -> Result<Vec<Envelope>> {
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 {
let env_hash = get_file_hash(&file);
{
map.lock()
.unwrap()
.entry(mailbox_hash)
.or_default()
.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 !env.is_seen() {
unseen_total += 1;
}
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)
}
}
impl Stream for MaildirStream {
type Item = Result<Vec<Envelope>>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let payloads = self.payloads.as_mut();
payloads.poll_next(cx)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,262 +0,0 @@
/*
* meli - mailbox module.
*
* Copyright 2021 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use super::*;
use crate::utils::datetime;
impl MboxFormat {
#[allow(clippy::too_many_arguments)]
pub fn append(
&self,
writer: &mut dyn std::io::Write,
input: &[u8],
envelope_from: Option<&Address>,
delivery_date: Option<crate::UnixTimestamp>,
(flags, tags): (Flag, Vec<&str>),
metadata_format: MboxMetadata,
is_empty: bool,
crlf: bool,
) -> Result<()> {
if tags.iter().any(|t| t.contains(' ')) {
return Err(Error::new("mbox tags/keywords can't contain spaces"));
}
let line_ending: &'static [u8] = if crlf { &b"\r\n"[..] } else { &b"\n"[..] };
if !is_empty {
writer.write_all(line_ending)?;
writer.write_all(line_ending)?;
}
writer.write_all(&b"From "[..])?;
if let Some(from) = envelope_from {
writer.write_all(from.address_spec_raw())?;
} else {
writer.write_all(&b"MAILER-DAEMON"[..])?;
}
writer.write_all(&b" "[..])?;
writer.write_all(
datetime::timestamp_to_string(
delivery_date.unwrap_or_else(datetime::now),
Some(datetime::formats::ASCTIME_FMT),
true,
)
.trim()
.as_bytes(),
)?;
writer.write_all(line_ending)?;
let (mut headers, body) = parser::mail(input)?;
headers.retain(|(header_name, _)| {
!header_name.eq_ignore_ascii_case(b"Status")
&& !header_name.eq_ignore_ascii_case(b"X-Status")
&& !header_name.eq_ignore_ascii_case(b"X-Keywords")
&& !header_name.eq_ignore_ascii_case(b"Content-Length")
});
let write_header_val_fn = |writer: &mut dyn std::io::Write, bytes: &[u8]| {
let mut i = 0;
if crlf {
while i < bytes.len() {
if bytes[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\r', b'\n'])?;
i += 2;
continue;
} else if bytes[i] == b'\n' {
writer.write_all(&[b'\r', b'\n'])?;
} else {
writer.write_all(&[bytes[i]])?;
}
i += 1;
}
} else {
while i < bytes.len() {
if bytes[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\n'])?;
i += 2;
} else {
writer.write_all(&[bytes[i]])?;
i += 1;
}
}
}
Ok::<(), Error>(())
};
let write_metadata_fn = |writer: &mut dyn std::io::Write| match metadata_format {
MboxMetadata::CClient => {
for (h, v) in {
if flags.is_seen() {
Some((&b"Status"[..], "R".into()))
} else {
None
}
.into_iter()
.chain(
if !flags.is_flagged()
&& !flags.is_replied()
&& !flags.is_draft()
&& !flags.is_trashed()
{
None
} else {
Some((
&b"X-Status"[..],
format!(
"{flagged}{replied}{draft}{trashed}",
flagged = if flags.is_flagged() { "F" } else { "" },
replied = if flags.is_replied() { "A" } else { "" },
draft = if flags.is_draft() { "T" } else { "" },
trashed = if flags.is_trashed() { "D" } else { "" }
),
))
},
)
.chain(if tags.is_empty() {
None
} else {
Some((&b"X-Keywords"[..], tags.as_slice().join(" ")))
})
} {
writer.write_all(h)?;
writer.write_all(&b": "[..])?;
writer.write_all(v.as_bytes())?;
writer.write_all(line_ending)?;
}
Ok::<(), Error>(())
}
MboxMetadata::None => Ok(()),
};
let body_len = {
let mut len = body.len();
if crlf {
let stray_lfs = body.iter().filter(|b| **b == b'\n').count()
- body.windows(b"\r\n".len()).filter(|w| w == b"\r\n").count();
len += stray_lfs;
} else {
let crlfs = body.windows(b"\r\n".len()).filter(|w| w == b"\r\n").count();
len -= crlfs;
}
len
};
match self {
Self::MboxO | Self::MboxRd => Err(Error::new("Unimplemented.")),
Self::MboxCl => {
let len = (body_len
+ body
.windows(b"\nFrom ".len())
.filter(|w| w == b"\nFrom ")
.count()
+ if body.starts_with(b"From ") { 1 } else { 0 })
.to_string();
for (h, v) in headers
.into_iter()
.chain(Some((&b"Content-Length"[..], len.as_bytes())))
{
writer.write_all(h)?;
writer.write_all(&b": "[..])?;
write_header_val_fn(writer, v)?;
writer.write_all(line_ending)?;
}
write_metadata_fn(writer)?;
writer.write_all(line_ending)?;
if body.starts_with(b"From ") {
writer.write_all(&[b'>'])?;
}
let mut i = 0;
if crlf {
while i < body.len() {
if body[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\r', b'\n'])?;
if body[i..].starts_with(b"\r\nFrom ") {
writer.write_all(&[b'>'])?;
}
i += 2;
} else if body[i] == b'\n' {
writer.write_all(&[b'\r', b'\n'])?;
if body[i..].starts_with(b"\nFrom ") {
writer.write_all(&[b'>'])?;
}
i += 1;
} else {
writer.write_all(&[body[i]])?;
i += 1;
}
}
} else {
while i < body.len() {
if body[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\n'])?;
if body[i..].starts_with(b"\r\nFrom ") {
writer.write_all(&[b'>'])?;
}
i += 2;
} else {
writer.write_all(&[body[i]])?;
if body[i..].starts_with(b"\nFrom ") {
writer.write_all(&[b'>'])?;
}
i += 1;
}
}
}
Ok(())
}
Self::MboxCl2 => {
let len = body_len.to_string();
for (h, v) in headers
.into_iter()
.chain(Some((&b"Content-Length"[..], len.as_bytes())))
{
writer.write_all(h)?;
writer.write_all(&b": "[..])?;
write_header_val_fn(writer, v)?;
writer.write_all(line_ending)?;
}
write_metadata_fn(writer)?;
writer.write_all(line_ending)?;
let mut i = 0;
if crlf {
while i < body.len() {
if body[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\r', b'\n'])?;
i += 2;
continue;
} else if body[i] == b'\n' {
writer.write_all(&[b'\r', b'\n'])?;
} else {
writer.write_all(&[body[i]])?;
}
i += 1;
}
} else {
while i < body.len() {
if body[i..].starts_with(b"\r\n") {
writer.write_all(&[b'\n'])?;
i += 2;
} else {
writer.write_all(&[body[i]])?;
i += 1;
}
}
}
Ok(())
}
}
}
}

View File

@ -1,885 +0,0 @@
/*
* meli - nntp module.
*
* Copyright 2019 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
//! # 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 smallvec::SmallVec;
use crate::{get_conf_val, get_path_hash};
#[macro_use]
mod protocol_parser;
pub use protocol_parser::*;
mod mailbox;
pub use mailbox::*;
mod operations;
pub use operations::*;
mod connection;
use std::{
collections::{hash_map::DefaultHasher, BTreeSet, HashMap, HashSet},
hash::Hasher,
pin::Pin,
str::FromStr,
sync::{Arc, Mutex},
time::{Duration, Instant},
};
pub use connection::*;
use futures::{lock::Mutex as FutureMutex, stream::Stream};
use crate::{
backends::*,
conf::AccountSettings,
email::*,
error::{Error, Result, ResultIntoError},
utils::futures::timeout,
Collection,
};
pub type UID = usize;
macro_rules! get_conf_val {
($s:ident[$var:literal]) => {
$s.extra.get($var).ok_or_else(|| {
Error::new(format!(
"Configuration error ({}): NNTP connection requires the field `{}` set",
$s.name.as_str(),
$var
))
})
};
($s:ident[$var:literal], $default:expr) => {
$s.extra
.get($var)
.map(|v| {
<_>::from_str(v).map_err(|e| {
Error::new(format!(
"Configuration error ({}) NNTP: Invalid value for field `{}`: {}\n{}",
$s.name.as_str(),
$var,
v,
e
))
})
})
.unwrap_or_else(|| Ok($default))
};
}
pub static SUPPORTED_CAPABILITIES: &[&str] = &[
#[cfg(feature = "deflate_compression")]
"COMPRESS DEFLATE",
"VERSION 2",
"NEWNEWS",
"POST",
"OVER",
"OVER MSGID",
"READER",
"STARTTLS",
"HDR",
"AUTHINFO USER",
];
#[derive(Debug, Clone)]
pub struct NntpServerConf {
pub server_hostname: String,
pub server_username: String,
pub server_password: String,
pub server_port: u16,
pub use_starttls: bool,
pub use_tls: bool,
pub require_auth: bool,
pub danger_accept_invalid_certs: bool,
pub extension_use: NntpExtensionUse,
}
type Capabilities = HashSet<String>;
#[derive(Debug)]
pub struct UIDStore {
account_hash: AccountHash,
account_name: Arc<str>,
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,
}
impl UIDStore {
fn new(
account_hash: AccountHash,
account_name: Arc<str>,
event_consumer: BackendEventConsumer,
) -> Self {
Self {
account_hash,
account_name,
event_consumer,
capabilities: Default::default(),
message_id_index: Default::default(),
hash_index: Default::default(),
uid_index: Default::default(),
mailboxes: Arc::new(FutureMutex::new(Default::default())),
collection: Collection::new(),
is_online: Arc::new(Mutex::new((
Instant::now(),
Err(Error::new("Account is uninitialised.")),
))),
}
}
}
#[derive(Debug)]
pub struct NntpType {
_is_subscribed: Arc<IsSubscribedFn>,
connection: Arc<FutureMutex<NntpConnection>>,
server_conf: NntpServerConf,
uid_store: Arc<UIDStore>,
_can_create_flags: Arc<Mutex<bool>>,
}
impl MailBackend for NntpType {
fn capabilities(&self) -> MailBackendCapabilities {
let mut extensions = self
.uid_store
.capabilities
.lock()
.unwrap()
.iter()
.map(|c| {
(
c.to_string(),
MailBackendExtensionStatus::Unsupported { comment: None },
)
})
.collect::<Vec<(String, MailBackendExtensionStatus)>>();
let mut supports_submission = false;
let NntpExtensionUse {
#[cfg(feature = "deflate_compression")]
deflate,
} = self.server_conf.extension_use;
{
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")]
{
if deflate {
*status = MailBackendExtensionStatus::Enabled { comment: None };
} else {
*status = MailBackendExtensionStatus::Supported {
comment: Some("Disabled by user configuration"),
};
}
}
#[cfg(not(feature = "deflate_compression"))]
{
*status = MailBackendExtensionStatus::Unsupported {
comment: Some("melib not compiled with DEFLATE."),
};
}
}
_ => {
if SUPPORTED_CAPABILITIES.contains(&name.as_str()) {
*status = MailBackendExtensionStatus::Enabled { comment: None };
}
}
}
}
}
extensions.sort_by(|a, b| a.0.cmp(&b.0));
MailBackendCapabilities {
is_async: true,
is_remote: true,
supports_search: false,
extensions: Some(extensions),
supports_tags: false,
supports_submission,
}
}
fn fetch(
&mut self,
mailbox_hash: MailboxHash,
) -> Result<Pin<Box<dyn Stream<Item = Result<Vec<Envelope>>> + Send + 'static>>> {
let mut state = FetchState {
mailbox_hash,
uid_store: self.uid_store.clone(),
connection: self.connection.clone(),
high_low_total: None,
};
Ok(Box::pin(async_stream::try_stream! {
{
let f = &state.uid_store.mailboxes.lock().await[&state.mailbox_hash];
f.exists.lock().unwrap().clear();
f.unseen.lock().unwrap().clear();
};
loop {
if let Some(ret) = state.fetch_envs().await? {
yield ret;
continue;
}
break;
}
}))
}
fn refresh(&mut self, mailbox_hash: MailboxHash) -> ResultFuture<()> {
let uid_store = self.uid_store.clone();
let connection = self.connection.clone();
Ok(Box::pin(async move {
/* To get updates, either issue NEWNEWS if it's supported by the server, and
* fallback to OVER otherwise */
let mbox: NntpMailbox = uid_store
.mailboxes
.lock()
.await
.get(&mailbox_hash)
.map(std::clone::Clone::clone)
.ok_or_else(|| {
Error::new(format!(
"Mailbox with hash {} not found in NNTP connection, this could possibly \
be a bug or it was deleted.",
mailbox_hash
))
})?;
let latest_article: Option<crate::UnixTimestamp> = *mbox.latest_article.lock().unwrap();
let (over_msgid_support, newnews_support): (bool, bool) = {
let caps = uid_store.capabilities.lock().unwrap();
(
caps.iter().any(|c| c.eq_ignore_ascii_case("OVER MSGID")),
caps.iter().any(|c| c.eq_ignore_ascii_case("NEWNEWS")),
)
};
let mut res = String::with_capacity(8 * 1024);
let mut conn = timeout(Some(Duration::from_secs(60 * 16)), connection.lock()).await?;
if let Some(mut latest_article) = latest_article {
let timestamp = latest_article - 10 * 60;
let datetime_str = crate::utils::datetime::timestamp_to_string(
timestamp,
Some("%Y%m%d %H%M%S"),
true,
);
if newnews_support {
conn.send_command(
format!("NEWNEWS {} {}", &mbox.nntp_path, datetime_str).as_bytes(),
)
.await?;
conn.read_response(&mut res, true, &["230 "]).await?;
let message_ids = {
let message_id_lck = uid_store.message_id_index.lock().unwrap();
res.split_rn()
.skip(1)
.map(|s| s.trim())
.filter(|msg_id| !message_id_lck.contains_key(*msg_id))
.map(str::to_string)
.collect::<Vec<String>>()
};
if message_ids.is_empty() || !over_msgid_support {
return Ok(());
}
let mut env_hash_set: BTreeSet<EnvelopeHash> = Default::default();
for msg_id in message_ids {
conn.send_command(format!("OVER {}", msg_id).as_bytes())
.await?;
conn.read_response(&mut res, true, &["224 "]).await?;
let mut message_id_lck = uid_store.message_id_index.lock().unwrap();
let mut hash_index_lck = uid_store.hash_index.lock().unwrap();
let mut uid_index_lck = uid_store.uid_index.lock().unwrap();
for l in res.split_rn().skip(1) {
let (_, (num, env)) = protocol_parser::over_article(l)?;
env_hash_set.insert(env.hash());
message_id_lck.insert(env.message_id_display().to_string(), env.hash());
hash_index_lck.insert(env.hash(), (num, mailbox_hash));
uid_index_lck.insert((mailbox_hash, num), env.hash());
latest_article = std::cmp::max(latest_article, env.timestamp);
(uid_store.event_consumer)(
uid_store.account_hash,
crate::backends::BackendEvent::Refresh(RefreshEvent {
mailbox_hash,
account_hash: uid_store.account_hash,
kind: RefreshEventKind::Create(Box::new(env)),
}),
);
}
}
{
let f = &uid_store.mailboxes.lock().await[&mailbox_hash];
*f.latest_article.lock().unwrap() = Some(latest_article);
f.exists
.lock()
.unwrap()
.insert_existing_set(env_hash_set.clone());
f.unseen.lock().unwrap().insert_existing_set(env_hash_set);
}
return Ok(());
}
}
//conn.select_group(mailbox_hash, false, &mut res).await?;
Ok(())
}))
}
fn mailboxes(&self) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
let uid_store = self.uid_store.clone();
let connection = self.connection.clone();
Ok(Box::pin(async move {
Self::nntp_mailboxes(&connection).await?;
let mailboxes_lck = uid_store.mailboxes.lock().await;
let ret = mailboxes_lck
.iter()
.map(|(h, f)| (*h, Box::new(Clone::clone(f)) as Mailbox))
.collect();
Ok(ret)
}))
}
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 {
Ok(mut conn) => {
debug!("is_online");
match debug!(timeout(Some(Duration::from_secs(60 * 16)), conn.connect()).await)
{
Ok(Ok(())) => Ok(()),
Err(err) | Ok(Err(err)) => {
conn.stream = Err(err.clone());
debug!(conn.connect().await)
}
}
}
Err(err) => Err(err),
}
}))
}
fn watch(&self) -> ResultFuture<()> {
Err(
Error::new("Watching is currently uniplemented for nntp backend")
.set_kind(ErrorKind::NotImplemented),
)
}
fn operation(&self, env_hash: EnvelopeHash) -> Result<Box<dyn BackendOp>> {
let (uid, mailbox_hash) =
if let Some(v) = self.uid_store.hash_index.lock().unwrap().get(&env_hash) {
*v
} else {
return Err(Error::new(
"Message not found in local cache, it might have been deleted before you \
requested it.",
));
};
Ok(Box::new(NntpOp::new(
uid,
mailbox_hash,
self.connection.clone(),
self.uid_store.clone(),
)))
}
fn save(
&self,
_bytes: Vec<u8>,
_mailbox_hash: MailboxHash,
_flags: Option<Flag>,
) -> ResultFuture<()> {
Err(Error::new("NNTP doesn't support saving."))
}
fn copy_messages(
&mut self,
_env_hashes: EnvelopeHashBatch,
_source_mailbox_hash: MailboxHash,
_destination_mailbox_hash: MailboxHash,
_move_: bool,
) -> ResultFuture<()> {
Err(Error::new("NNTP doesn't support copying/moving."))
}
fn set_flags(
&mut self,
_env_hashes: EnvelopeHashBatch,
_mailbox_hash: MailboxHash,
_flags: SmallVec<[(std::result::Result<Flag, String>, bool); 8]>,
) -> ResultFuture<()> {
Err(Error::new("NNTP doesn't support flags."))
}
fn delete_messages(
&mut self,
_env_hashes: EnvelopeHashBatch,
_mailbox_hash: MailboxHash,
) -> ResultFuture<()> {
Err(Error::new("NNTP doesn't support deletion."))
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn collection(&self) -> Collection {
self.uid_store.collection.clone()
}
fn create_mailbox(
&mut self,
_path: String,
) -> ResultFuture<(MailboxHash, HashMap<MailboxHash, Mailbox>)> {
Err(Error::new(
"Creating mailbox is currently unimplemented for nntp backend.",
))
}
fn delete_mailbox(
&mut self,
_mailbox_hash: MailboxHash,
) -> ResultFuture<HashMap<MailboxHash, Mailbox>> {
Err(Error::new(
"Deleting a mailbox is currently unimplemented for nntp backend.",
))
}
fn set_mailbox_subscription(
&mut self,
_mailbox_hash: MailboxHash,
_new_val: bool,
) -> ResultFuture<()> {
Err(Error::new(
"Setting mailbox description is currently unimplemented for nntp backend.",
))
}
fn rename_mailbox(
&mut self,
_mailbox_hash: MailboxHash,
_new_path: String,
) -> ResultFuture<Mailbox> {
Err(Error::new(
"Renaming mailbox is currently unimplemented for nntp backend.",
))
}
fn set_mailbox_permissions(
&mut self,
_mailbox_hash: MailboxHash,
_val: crate::backends::MailboxPermissions,
) -> ResultFuture<()> {
Err(Error::new(
"Setting mailbox permissions is currently unimplemented for nntp backend.",
))
}
fn search(
&self,
_query: crate::search::Query,
_mailbox_hash: Option<MailboxHash>,
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>> {
Err(Error::new(
"Searching is currently unimplemented for nntp backend.",
))
}
fn submit(
&self,
bytes: Vec<u8>,
mailbox_hash: Option<MailboxHash>,
_flags: Option<Flag>,
) -> ResultFuture<()> {
let connection = self.connection.clone();
Ok(Box::pin(async move {
match timeout(Some(Duration::from_secs(60 * 16)), connection.lock()).await {
Ok(mut conn) => {
match &conn.stream {
Ok(stream) => {
if !stream.supports_submission {
return Err(Error::new("Server prohibits posting."));
}
}
Err(err) => return Err(err.clone()),
}
let mut res = String::with_capacity(8 * 1024);
if let Some(mailbox_hash) = mailbox_hash {
conn.select_group(mailbox_hash, false, &mut res).await?;
}
conn.send_command(b"POST").await?;
conn.read_response(&mut res, false, &["340 "]).await?;
conn.send_multiline_data_block(&bytes).await?;
conn.read_response(&mut res, false, &["240 "]).await?;
Ok(())
}
Err(err) => Err(err),
}
}))
}
}
impl NntpType {
#[allow(clippy::new_ret_no_self)]
pub fn new(
s: &AccountSettings,
is_subscribed: Box<dyn Fn(&str) -> bool + Send + Sync>,
event_consumer: BackendEventConsumer,
) -> Result<Box<dyn MailBackend>> {
let server_hostname = get_conf_val!(s["server_hostname"])?;
/*let server_username = get_conf_val!(s["server_username"], "")?;
let server_password = if !s.extra.contains_key("server_password_command") {
get_conf_val!(s["server_password"], "")?.to_string()
} else {
let invocation = get_conf_val!(s["server_password_command"])?;
let output = std::process::Command::new("sh")
.args(&["-c", invocation])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()?;
if !output.status.success() {
return Err(Error::new(format!(
"({}) server_password_command `{}` returned {}: {}",
s.name,
get_conf_val!(s["server_password_command"])?,
output.status,
String::from_utf8_lossy(&output.stderr)
)));
}
std::str::from_utf8(&output.stdout)?.trim_end().to_string()
};
*/
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 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 server_conf = NntpServerConf {
server_hostname: server_hostname.to_string(),
server_username: if require_auth {
get_conf_val!(s["server_username"])?.to_string()
} else {
get_conf_val!(s["server_username"], String::new())?
},
server_password: if require_auth {
get_conf_val!(s["server_password"])?.to_string()
} else {
get_conf_val!(s["server_password"], String::new())?
},
require_auth,
server_port,
use_tls,
use_starttls,
danger_accept_invalid_certs,
extension_use: NntpExtensionUse {
#[cfg(feature = "deflate_compression")]
deflate: get_conf_val!(s["use_deflate"], false)?,
},
};
let account_hash = AccountHash::from_bytes(s.name.as_bytes());
let account_name = s.name.to_string().into();
let mut mailboxes = HashMap::default();
for (k, _f) in s.mailboxes.iter() {
let mailbox_hash = MailboxHash(get_path_hash!(&k));
mailboxes.insert(
mailbox_hash,
NntpMailbox {
hash: mailbox_hash,
nntp_path: k.to_string(),
high_watermark: Arc::new(Mutex::new(0)),
low_watermark: Arc::new(Mutex::new(0)),
latest_article: Arc::new(Mutex::new(None)),
exists: Default::default(),
unseen: Default::default(),
},
);
}
if mailboxes.is_empty() {
return Err(Error::new(format!(
"{} has no newsgroups configured.",
account_name
)));
}
let uid_store: Arc<UIDStore> = Arc::new(UIDStore {
mailboxes: Arc::new(FutureMutex::new(mailboxes)),
..UIDStore::new(account_hash, account_name, event_consumer)
});
let connection = NntpConnection::new_connection(&server_conf, uid_store.clone());
Ok(Box::new(Self {
server_conf,
_is_subscribed: Arc::new(IsSubscribedFn(is_subscribed)),
_can_create_flags: Arc::new(Mutex::new(false)),
connection: Arc::new(FutureMutex::new(connection)),
uid_store,
}))
}
pub async fn nntp_mailboxes(connection: &Arc<FutureMutex<NntpConnection>>) -> Result<()> {
let mut res = String::with_capacity(8 * 1024);
let mut conn = connection.lock().await;
let command = {
let mailboxes_lck = conn.uid_store.mailboxes.lock().await;
mailboxes_lck
.values()
.fold("LIST ACTIVE ".to_string(), |mut acc, x| {
if acc.len() != "LIST ACTIVE ".len() {
acc.push(',');
}
acc.push_str(x.name());
acc
})
};
conn.send_command(command.as_bytes()).await?;
conn.read_response(&mut res, true, &["215 "])
.await
.chain_err_summary(|| {
format!(
"Could not get newsgroups {}: expected LIST ACTIVE response but got: {}",
&conn.uid_store.account_name, res
)
})?;
debug!(&res);
let mut mailboxes_lck = conn.uid_store.mailboxes.lock().await;
for l in res.split_rn().skip(1) {
let s = l.split_whitespace().collect::<SmallVec<[&str; 4]>>();
if s.len() != 3 {
continue;
}
let mailbox_hash = MailboxHash(get_path_hash!(&s[0]));
mailboxes_lck.entry(mailbox_hash).and_modify(|m| {
*m.high_watermark.lock().unwrap() = usize::from_str(s[1]).unwrap_or(0);
*m.low_watermark.lock().unwrap() = usize::from_str(s[2]).unwrap_or(0);
});
}
Ok(())
}
pub fn validate_config(s: &mut AccountSettings) -> Result<()> {
let mut keys: HashSet<&'static str> = Default::default();
macro_rules! get_conf_val {
($s:ident[$var:literal]) => {{
keys.insert($var);
$s.extra.remove($var).ok_or_else(|| {
Error::new(format!(
"Configuration error ({}): NNTP connection requires the field `{}` set",
$s.name.as_str(),
$var
))
})
}};
($s:ident[$var:literal], $default:expr) => {{
keys.insert($var);
$s.extra
.remove($var)
.map(|v| {
<_>::from_str(&v).map_err(|e| {
Error::new(format!(
"Configuration error ({}) NNTP: Invalid value for field `{}`: \
{}\n{}",
$s.name.as_str(),
$var,
v,
e
))
})
})
.unwrap_or_else(|| Ok($default))
}};
}
get_conf_val!(s["require_auth"], false)?;
get_conf_val!(s["server_hostname"])?;
get_conf_val!(s["server_username"], String::new())?;
if !s.extra.contains_key("server_password_command") {
get_conf_val!(s["server_password"], String::new())?;
} else if s.extra.contains_key("server_password") {
return Err(Error::new(format!(
"Configuration error ({}): both server_password and server_password_command are \
set, cannot choose",
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)?;
if !use_tls && use_starttls {
return Err(Error::new(format!(
"Configuration error ({}): incompatible use_tls and use_starttls values: use_tls \
= false, use_starttls = true",
s.name.as_str(),
)));
}
#[cfg(feature = "deflate_compression")]
get_conf_val!(s["use_deflate"], false)?;
#[cfg(not(feature = "deflate_compression"))]
if s.extra.contains_key("use_deflate") {
return Err(Error::new(format!(
"Configuration error ({}): setting `use_deflate` is set but this version of meli \
isn't compiled with DEFLATE support.",
s.name.as_str(),
)));
}
get_conf_val!(s["danger_accept_invalid_certs"], false)?;
let extra_keys = s
.extra
.keys()
.map(String::as_str)
.collect::<HashSet<&str>>();
let diff = extra_keys.difference(&keys).collect::<Vec<&&str>>();
if !diff.is_empty() {
return Err(Error::new(format!(
"Configuration error ({}) NNTP: the following flags are set but are not \
recognized: {:?}.",
s.name.as_str(),
diff
)));
}
Ok(())
}
pub fn capabilities(&self) -> Vec<String> {
self.uid_store
.capabilities
.lock()
.unwrap()
.iter()
.cloned()
.collect::<Vec<String>>()
}
}
struct FetchState {
mailbox_hash: MailboxHash,
connection: Arc<FutureMutex<NntpConnection>>,
uid_store: Arc<UIDStore>,
high_low_total: Option<(usize, usize, usize)>,
}
impl FetchState {
async fn fetch_envs(&mut self) -> Result<Option<Vec<Envelope>>> {
let Self {
mailbox_hash,
ref connection,
ref uid_store,
ref mut high_low_total,
} = self;
let mailbox_hash = *mailbox_hash;
let mut res = String::with_capacity(8 * 1024);
let mut conn = connection.lock().await;
if high_low_total.is_none() {
conn.select_group(mailbox_hash, true, &mut res).await?;
/*
* Parameters
group Name of newsgroup
number Estimated number of articles in the group
low Reported low water mark
high Reported high water mark
*/
let s = res.split_whitespace().collect::<SmallVec<[&str; 6]>>();
let path = conn.uid_store.mailboxes.lock().await[&mailbox_hash]
.name()
.to_string();
if s.len() != 5 {
return Err(Error::new(format!(
"{} Could not select newsgroup {}: expected GROUP response but got: {}",
&uid_store.account_name, path, res
)));
}
let total = usize::from_str(s[1]).unwrap_or(0);
let _low = usize::from_str(s[2]).unwrap_or(0);
let high = usize::from_str(s[3]).unwrap_or(0);
*high_low_total = Some((high, _low, total));
{
let f = &uid_store.mailboxes.lock().await[&mailbox_hash];
f.exists.lock().unwrap().set_not_yet_seen(total);
f.unseen.lock().unwrap().set_not_yet_seen(total);
};
}
let (high, low, _) = high_low_total.unwrap();
if high <= low {
return Ok(None);
}
const CHUNK_SIZE: usize = 50000;
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"))
.await
.chain_err_summary(|| {
format!(
"{} Could not select newsgroup: expected OVER response but got: {}",
&uid_store.account_name, res
)
})?;
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());
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()
.insert_existing_set(hash_set.clone());
f.unseen.lock().unwrap().insert_existing_set(hash_set);
};
Ok(Some(ret))
}
}

View File

@ -1,561 +0,0 @@
/*
* meli - nntp module.
*
* Copyright 2017 - 2019 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use crate::{
backends::{BackendMailbox, MailboxHash},
email::parser::BytesExt,
error::*,
log,
utils::connections::{lookup_ipv4, Connection},
};
extern crate native_tls;
use std::{collections::HashSet, future::Future, pin::Pin, sync::Arc, time::Instant};
use futures::io::{AsyncReadExt, AsyncWriteExt};
use native_tls::TlsConnector;
pub use smol::Async as AsyncWrapper;
use super::{Capabilities, NntpServerConf, UIDStore};
#[derive(Debug, Default, Clone, Copy)]
pub struct NntpExtensionUse {
#[cfg(feature = "deflate_compression")]
pub deflate: bool,
}
#[derive(Debug)]
pub struct NntpStream {
pub stream: AsyncWrapper<Connection>,
pub extension_use: NntpExtensionUse,
pub current_mailbox: MailboxSelection,
pub supports_submission: bool,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum MailboxSelection {
None,
Select(MailboxHash),
}
impl MailboxSelection {
pub fn take(&mut self) -> Self {
std::mem::replace(self, Self::None)
}
}
async fn try_await(cl: impl Future<Output = Result<()>> + Send) -> Result<()> {
cl.await
}
#[derive(Debug)]
pub struct NntpConnection {
pub stream: Result<NntpStream>,
pub server_conf: NntpServerConf,
pub uid_store: Arc<UIDStore>,
}
impl NntpStream {
pub async fn new_connection(server_conf: &NntpServerConf) -> Result<(Capabilities, Self)> {
use std::net::TcpStream;
let path = &server_conf.server_hostname;
let stream = {
let addr = lookup_ipv4(path, server_conf.server_port)?;
AsyncWrapper::new(Connection::Tcp(TcpStream::connect_timeout(
&addr,
std::time::Duration::new(16, 0),
)?))?
};
let mut res = String::with_capacity(8 * 1024);
let mut ret = Self {
stream,
extension_use: server_conf.extension_use,
current_mailbox: MailboxSelection::None,
supports_submission: false,
};
if server_conf.use_tls {
let mut connector = TlsConnector::builder();
if server_conf.danger_accept_invalid_certs {
connector.danger_accept_invalid_certs(true);
}
let connector = connector.build()?;
if server_conf.use_starttls {
ret.read_response(&mut res, false, &["200 ", "201 "])
.await?;
ret.supports_submission = res.starts_with("200");
ret.send_command(b"CAPABILITIES").await?;
ret.read_response(&mut res, true, command_to_replycodes("CAPABILITIES"))
.await?;
if !res.starts_with("101 ") {
return Err(Error::new(format!(
"Could not connect to {}: expected CAPABILITIES response but got:{}",
&server_conf.server_hostname, res
)));
}
let capabilities: Vec<&str> = res.lines().skip(1).collect();
if !capabilities
.iter()
.any(|cap| cap.eq_ignore_ascii_case("VERSION 2"))
{
return Err(Error::new(format!(
"Could not connect to {}: server is not NNTP VERSION 2 compliant",
&server_conf.server_hostname
)));
}
if !capabilities
.iter()
.any(|cap| cap.eq_ignore_ascii_case("STARTTLS"))
{
return Err(Error::new(format!(
"Could not connect to {}: server does not support STARTTLS",
&server_conf.server_hostname
)));
}
ret.stream.write_all(b"STARTTLS\r\n").await?;
ret.stream.flush().await?;
ret.read_response(&mut res, false, command_to_replycodes("STARTTLS"))
.await?;
if !res.starts_with("382 ") {
return Err(Error::new(format!(
"Could not connect to {}: could not begin TLS negotiation, got: {}",
&server_conf.server_hostname, res
)));
}
}
{
// FIXME: This is blocking
let socket = ret.stream.into_inner()?;
let mut conn_result = connector.connect(path, socket);
if let Err(native_tls::HandshakeError::WouldBlock(midhandshake_stream)) =
conn_result
{
let mut midhandshake_stream = Some(midhandshake_stream);
loop {
match midhandshake_stream.take().unwrap().handshake() {
Ok(r) => {
conn_result = Ok(r);
break;
}
Err(native_tls::HandshakeError::WouldBlock(stream)) => {
midhandshake_stream = Some(stream);
}
p => {
p.chain_err_kind(crate::error::ErrorKind::Network(
crate::error::NetworkErrorKind::InvalidTLSConnection,
))?;
}
}
}
}
ret.stream =
AsyncWrapper::new(Connection::Tls(conn_result?)).chain_err_summary(|| {
format!("Could not initiate TLS negotiation to {}.", path)
})?;
}
}
//ret.send_command(
// format!(
// "LOGIN \"{}\" \"{}\"",
// &server_conf.server_username, &server_conf.server_password
// )
// .as_bytes(),
//)
//.await?;
if let Err(err) = ret
.stream
.get_ref()
.set_keepalive(Some(std::time::Duration::new(60 * 9, 0)))
{
log::warn!("Could not set TCP keepalive in NNTP connection: {}", err);
}
ret.read_response(&mut res, false, &["200 ", "201 "])
.await?;
ret.supports_submission = res.starts_with("200");
ret.send_command(b"CAPABILITIES").await?;
ret.read_response(&mut res, true, command_to_replycodes("CAPABILITIES"))
.await?;
if !res.starts_with("101 ") {
return Err(Error::new(format!(
"Could not connect to {}: expected CAPABILITIES response but got:{}",
&server_conf.server_hostname, res
)));
}
let capabilities: HashSet<String> =
res.lines().skip(1).map(|l| l.trim().to_string()).collect();
if !capabilities
.iter()
.any(|cap| cap.eq_ignore_ascii_case("VERSION 2"))
{
return Err(Error::new(format!(
"Could not connect to {}: server is not NNTP compliant",
&server_conf.server_hostname
)));
}
if !capabilities
.iter()
.any(|cap| cap.eq_ignore_ascii_case("POST"))
{
ret.supports_submission = false;
}
if server_conf.require_auth {
if capabilities.iter().any(|c| c.starts_with("AUTHINFO USER")) {
ret.send_command(
format!("AUTHINFO USER {}", server_conf.server_username).as_bytes(),
)
.await?;
ret.read_response(&mut res, false, command_to_replycodes("AUTHINFO USER"))
.await
.chain_err_summary(|| format!("Authentication state error: {}", res))
.chain_err_kind(ErrorKind::Authentication)?;
if res.starts_with("381 ") {
ret.send_command(
format!("AUTHINFO PASS {}", server_conf.server_password).as_bytes(),
)
.await?;
ret.read_response(&mut res, false, command_to_replycodes("AUTHINFO PASS"))
.await
.chain_err_summary(|| format!("Authentication state error: {}", res))
.chain_err_kind(ErrorKind::Authentication)?;
}
} else {
return Err(Error::new(format!(
"Could not connect: no supported auth mechanisms in server capabilities: {:?}",
capabilities
))
.set_err_kind(ErrorKind::Authentication));
}
}
#[cfg(feature = "deflate_compression")]
if capabilities.contains("COMPRESS DEFLATE") && ret.extension_use.deflate {
ret.send_command(b"COMPRESS DEFLATE").await?;
ret.read_response(&mut res, false, command_to_replycodes("COMPRESS DEFLATE"))
.await
.chain_err_summary(|| {
format!(
"Could not use COMPRESS DEFLATE in account `{}`: server replied with `{}`",
server_conf.server_hostname, res
)
})?;
let Self {
stream,
extension_use,
current_mailbox,
supports_submission,
} = ret;
let stream = stream.into_inner()?;
return Ok((
capabilities,
Self {
stream: AsyncWrapper::new(stream.deflate())?,
extension_use,
current_mailbox,
supports_submission,
},
));
}
Ok((capabilities, ret))
}
pub async fn read_response(
&mut self,
ret: &mut String,
is_multiline: bool,
expected_reply_code: &[&str],
) -> Result<()> {
self.read_lines(ret, is_multiline, expected_reply_code)
.await?;
Ok(())
}
pub async fn read_lines(
&mut self,
ret: &mut String,
is_multiline: bool,
expected_reply_code: &[&str],
) -> Result<()> {
let mut buf: Vec<u8> = vec![0; Connection::IO_BUF_SIZE];
ret.clear();
let mut last_line_idx: usize = 0;
loop {
match self.stream.read(&mut buf).await {
Ok(0) => break,
Ok(b) => {
ret.push_str(unsafe { std::str::from_utf8_unchecked(&buf[0..b]) });
if ret.len() > 4 {
if ret.starts_with("205 ") {
return Err(Error::new(format!("Disconnected: {}", ret)));
} else if ret.starts_with("501 ") || ret.starts_with("500 ") {
return Err(Error::new(format!("Syntax error: {}", ret)));
} else if ret.starts_with("403 ") {
return Err(Error::new(format!("Internal error: {}", ret)));
} else if ret.starts_with("502 ")
|| ret.starts_with("480 ")
|| ret.starts_with("483 ")
|| ret.starts_with("401 ")
{
return Err(Error::new(format!("Connection state error: {}", ret))
.set_err_kind(ErrorKind::Authentication));
} else if !expected_reply_code.iter().any(|r| ret.starts_with(r)) {
return Err(Error::new(format!("Unexpected reply code: {}", ret)));
}
}
if let Some(mut pos) = ret[last_line_idx..].rfind("\r\n") {
if !is_multiline {
break;
} else if let Some(pos) = ret.find("\r\n.\r\n") {
ret.replace_range(pos + "\r\n".len()..pos + "\r\n.\r\n".len(), "");
break;
}
if let Some(prev_line) =
ret[last_line_idx..pos + last_line_idx].rfind("\r\n")
{
last_line_idx += prev_line + "\r\n".len();
pos -= prev_line + "\r\n".len();
}
last_line_idx += pos + "\r\n".len();
}
}
Err(err) => {
return Err(Error::from(err));
}
}
}
//debug!("returning nntp response:\n{:?}", &ret);
Ok(())
}
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?;
self.stream.write_all(b"\r\n").await?;
self.stream.flush().await?;
debug!("sent: {}", unsafe {
std::str::from_utf8_unchecked(command)
});
Ok(())
})
.await
{
debug!("stream send_command err {:?}", err);
Err(err)
} else {
Ok(())
}
}
pub async fn send_multiline_data_block(&mut self, data: &[u8]) -> 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".") {
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(b"\r\n").await?;
}
self.stream.write_all(b".\r\n").await?;
self.stream.flush().await?;
debug!("sent data block {} bytes", data.len());
Ok(())
})
.await
{
debug!("stream send_multiline_data_block err {:?}", err);
Err(err)
} else {
Ok(())
}
}
}
impl NntpConnection {
pub fn new_connection(server_conf: &NntpServerConf, uid_store: Arc<UIDStore>) -> Self {
Self {
stream: Err(Error::new("Offline".to_string())),
server_conf: server_conf.clone(),
uid_store,
}
}
pub fn connect<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async move {
if let (instant, ref mut status @ Ok(())) = *self.uid_store.is_online.lock().unwrap() {
if Instant::now().duration_since(instant) >= std::time::Duration::new(60 * 30, 0) {
*status = Err(Error::new("Connection timed out"));
self.stream = Err(Error::new("Connection timed out"));
}
}
if self.stream.is_ok() {
self.uid_store.is_online.lock().unwrap().0 = Instant::now();
return Ok(());
}
let new_stream = NntpStream::new_connection(&self.server_conf).await;
if let Err(err) = new_stream.as_ref() {
*self.uid_store.is_online.lock().unwrap() = (Instant::now(), Err(err.clone()));
} else {
*self.uid_store.is_online.lock().unwrap() = (Instant::now(), Ok(()));
}
let (capabilities, stream) = new_stream?;
self.stream = Ok(stream);
*self.uid_store.capabilities.lock().unwrap() = capabilities;
Ok(())
})
}
pub fn read_response<'a>(
&'a mut self,
ret: &'a mut String,
is_multiline: bool,
expected_reply_code: &'static [&str],
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async move {
ret.clear();
self.stream
.as_mut()?
.read_response(ret, is_multiline, expected_reply_code)
.await
})
}
pub async fn read_lines(
&mut self,
ret: &mut String,
is_multiline: bool,
expected_reply_code: &[&str],
) -> Result<()> {
self.stream
.as_mut()?
.read_lines(ret, is_multiline, expected_reply_code)
.await?;
Ok(())
}
pub async fn send_command(&mut self, command: &[u8]) -> Result<()> {
if let Err(err) =
try_await(async { self.stream.as_mut()?.send_command(command).await }).await
{
self.stream = Err(err.clone());
debug!(err.kind);
if err.kind.is_network() {
debug!(self.connect().await)?;
}
Err(err)
} else {
Ok(())
}
}
pub fn add_refresh_event(&mut self, ev: crate::backends::RefreshEvent) {
(self.uid_store.event_consumer)(
self.uid_store.account_hash,
crate::backends::BackendEvent::Refresh(ev),
);
}
pub async fn select_group(
&mut self,
mailbox_hash: MailboxHash,
force: bool,
res: &mut String,
) -> Result<()> {
if !force {
match self.stream.as_ref()?.current_mailbox {
MailboxSelection::Select(m) if m == mailbox_hash => return Ok(()),
_ => {}
}
}
let path = self.uid_store.mailboxes.lock().await[&mailbox_hash]
.name()
.to_string();
self.send_command(format!("GROUP {}", path).as_bytes())
.await?;
self.read_response(res, false, command_to_replycodes("GROUP"))
.await
.chain_err_summary(|| {
format!(
"{} Could not select newsgroup {}: expected GROUP response but got: {}",
&self.uid_store.account_name, path, res
)
})?;
self.stream.as_mut()?.current_mailbox = MailboxSelection::Select(mailbox_hash);
Ok(())
}
pub async fn send_multiline_data_block(&mut self, message: &[u8]) -> Result<()> {
self.stream
.as_mut()?
.send_multiline_data_block(message)
.await
}
}
pub fn command_to_replycodes(c: &str) -> &'static [&'static str] {
if c.starts_with("OVER") {
&["224 "]
} else if c.starts_with("LIST") {
&["215 "]
} else if c.starts_with("POST") {
&["340 "]
} else if c.starts_with("STARTTLS") {
&["382 "]
} else if c.starts_with("GROUP") {
&["211 "]
} else if c.starts_with("CAPABILITIES") {
&["101 "]
} else if c.starts_with("ARTICLE") {
&["220 "]
} else if c.starts_with("DATE") {
&["111 "]
} else if c.starts_with("NEWNEWS") {
&["230 "]
} else if c.starts_with("AUTHINFO USER") {
&["281 ", "381 "]
} else if c.starts_with("AUTHINFO PASS") {
&["281 "]
} else if c.starts_with("COMPRESS DEFLATE") {
&["206 "]
} else {
&[]
}
}

View File

@ -1,100 +0,0 @@
/*
* meli - nntp module.
*
* Copyright 2019 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::sync::{Arc, Mutex};
use crate::{
backends::{
BackendMailbox, LazyCountSet, Mailbox, MailboxHash, MailboxPermissions, SpecialUsageMailbox,
},
error::*,
UnixTimestamp,
};
#[derive(Debug, Default, Clone)]
pub struct NntpMailbox {
pub(super) hash: MailboxHash,
pub(super) nntp_path: String,
pub high_watermark: Arc<Mutex<usize>>,
pub low_watermark: Arc<Mutex<usize>>,
pub exists: Arc<Mutex<LazyCountSet>>,
pub unseen: Arc<Mutex<LazyCountSet>>,
pub latest_article: Arc<Mutex<Option<UnixTimestamp>>>,
}
impl NntpMailbox {
pub fn nntp_path(&self) -> &str {
&self.nntp_path
}
}
impl BackendMailbox for NntpMailbox {
fn hash(&self) -> MailboxHash {
self.hash
}
fn name(&self) -> &str {
&self.nntp_path
}
fn path(&self) -> &str {
&self.nntp_path
}
fn children(&self) -> &[MailboxHash] {
&[]
}
fn clone(&self) -> Mailbox {
Box::new(std::clone::Clone::clone(self))
}
fn special_usage(&self) -> SpecialUsageMailbox {
SpecialUsageMailbox::default()
}
fn parent(&self) -> Option<MailboxHash> {
None
}
fn permissions(&self) -> MailboxPermissions {
MailboxPermissions::default()
}
fn is_subscribed(&self) -> bool {
true
}
fn set_is_subscribed(&mut self, _new_val: bool) -> Result<()> {
Err(Error::new("Cannot set subscription in NNTP."))
}
fn set_special_usage(&mut self, _new_val: SpecialUsageMailbox) -> Result<()> {
Err(Error::new("Cannot set special usage in NNTP."))
}
fn count(&self) -> Result<(usize, usize)> {
Ok((self.unseen.lock()?.len(), self.exists.lock()?.len()))
}
}

View File

@ -1,91 +0,0 @@
/*
* meli - nntp module.
*
* Copyright 2017 - 2019 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::sync::Arc;
use super::*;
use crate::{backends::*, email::*, error::Error};
/// `BackendOp` implementor for Nntp
#[derive(Debug, Clone)]
pub struct NntpOp {
uid: usize,
mailbox_hash: MailboxHash,
connection: Arc<FutureMutex<NntpConnection>>,
uid_store: Arc<UIDStore>,
}
impl NntpOp {
pub fn new(
uid: usize,
mailbox_hash: MailboxHash,
connection: Arc<FutureMutex<NntpConnection>>,
uid_store: Arc<UIDStore>,
) -> Self {
Self {
uid,
connection,
mailbox_hash,
uid_store,
}
}
}
impl BackendOp for NntpOp {
fn as_bytes(&mut self) -> ResultFuture<Vec<u8>> {
let mailbox_hash = self.mailbox_hash;
let uid = self.uid;
let uid_store = self.uid_store.clone();
let connection = self.connection.clone();
Ok(Box::pin(async move {
let mut res = String::with_capacity(8 * 1024);
let mut conn = connection.lock().await;
let path = uid_store.mailboxes.lock().await[&mailbox_hash]
.name()
.to_string();
conn.send_command(format!("GROUP {}", path).as_bytes())
.await?;
conn.read_response(&mut res, false, &["211 "]).await?;
if !res.starts_with("211 ") {
return Err(Error::new(format!(
"{} Could not select newsgroup {}: expected GROUP response but got: {}",
&uid_store.account_name, path, res
)));
}
conn.send_command(format!("ARTICLE {}", uid).as_bytes())
.await?;
conn.read_response(&mut res, true, &["220 "]).await?;
if !res.starts_with("220 ") {
return Err(Error::new(format!(
"{} Could not select article {}: expected ARTICLE response but got: {}",
&uid_store.account_name, path, res
)));
}
let pos = res.find("\r\n").unwrap_or(0) + 2;
Ok(res.as_bytes()[pos..].to_vec())
}))
}
fn fetch_flags(&self) -> ResultFuture<Flag> {
Ok(Box::pin(async move { Ok(Flag::default()) }))
}
}

View File

@ -1,158 +0,0 @@
/*
* meli - melib 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 std::str::FromStr;
use nom::{
bytes::complete::{is_not, tag},
combinator::opt,
};
use super::*;
use crate::email::parser::IResult;
pub struct NntpLineIterator<'a> {
slice: &'a str,
}
impl<'a> std::iter::DoubleEndedIterator for NntpLineIterator<'a> {
fn next_back(&mut self) -> Option<Self::Item> {
if self.slice.is_empty() {
None
} else if let Some(pos) = self.slice.rfind("\r\n") {
if self.slice[..pos].is_empty() {
self.slice = &self.slice[..pos];
None
} else if let Some(prev_pos) = self.slice[..pos].rfind("\r\n") {
let ret = &self.slice[prev_pos + 2..pos + 2];
self.slice = &self.slice[..prev_pos + 2];
Some(ret)
} else {
let ret = self.slice;
self.slice = &self.slice[ret.len()..];
Some(ret)
}
} else {
let ret = self.slice;
self.slice = &self.slice[ret.len()..];
Some(ret)
}
}
}
impl<'a> Iterator for NntpLineIterator<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<&'a str> {
if self.slice.is_empty() {
None
} else if let Some(pos) = self.slice.find("\r\n") {
let ret = &self.slice[..pos + 2];
self.slice = &self.slice[pos + 2..];
Some(ret)
} else {
let ret = self.slice;
self.slice = &self.slice[ret.len()..];
Some(ret)
}
}
}
pub trait NntpLineSplit {
fn split_rn(&self) -> NntpLineIterator;
}
impl NntpLineSplit for str {
fn split_rn(&self) -> NntpLineIterator {
NntpLineIterator { slice: self }
}
}
pub fn over_article(input: &str) -> IResult<&str, (UID, Envelope)> {
/*
"0" or article number (see below)
Subject header content
From header content
Date header content
Message-ID header content
References header content
:bytes metadata item
:lines metadata item
*/
let (input, num) = is_not("\t")(input)?;
let (input, _) = tag("\t")(input)?;
let (input, subject) = opt(is_not("\t"))(input)?;
let (input, _) = tag("\t")(input)?;
let (input, from) = opt(is_not("\t"))(input)?;
let (input, _) = tag("\t")(input)?;
let (input, date) = opt(is_not("\t"))(input)?;
let (input, _) = tag("\t")(input)?;
let (input, message_id) = opt(is_not("\t"))(input)?;
let (input, _) = tag("\t")(input)?;
let (input, references) = opt(is_not("\t"))(input)?;
let (input, _) = tag("\t")(input)?;
let (input, _bytes) = opt(is_not("\t"))(input)?;
let (input, _) = tag("\t")(input)?;
let (input, _lines) = opt(is_not("\t\r\n"))(input)?;
let (input, _other_headers) = opt(is_not("\r\n"))(input)?;
let (input, _) = tag("\r\n")(input)?;
Ok((
input,
({
let env_hash = {
let mut hasher = DefaultHasher::new();
hasher.write(num.as_bytes());
hasher.write(message_id.unwrap_or_default().as_bytes());
EnvelopeHash(hasher.finish())
};
let mut env = Envelope::new(env_hash);
if let Some(date) = date {
env.set_date(date.as_bytes());
if let Ok(d) =
crate::email::parser::dates::rfc5322_date(env.date_as_str().as_bytes())
{
env.set_datetime(d);
}
}
if let Some(subject) = subject {
env.set_subject(subject.into());
}
if let Some(from) = from {
if let Ok((_, from)) =
crate::email::parser::address::rfc2822address_list(from.as_bytes())
{
env.set_from(from);
}
}
if let Some(references) = references {
env.set_references(references.as_bytes());
}
if let Some(message_id) = message_id {
env.set_message_id(message_id.as_bytes());
}
(usize::from_str(num).unwrap(), env)
}),
))
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,280 +0,0 @@
/*
* melib - notmuch backend
*
* 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 crate::thread::{ThreadHash, ThreadNode, ThreadNodeHash};
#[derive(Clone)]
pub struct Message<'m> {
pub lib: Arc<libloading::Library>,
pub message: *mut notmuch_message_t,
pub is_from_thread: bool,
pub _ph: std::marker::PhantomData<&'m notmuch_message_t>,
}
impl<'m> Message<'m> {
pub fn find_message(db: &'m DbConnection, msg_id: &CStr) -> Result<Message<'m>> {
let mut message: *mut notmuch_message_t = std::ptr::null_mut();
let lib = db.lib.clone();
unsafe {
call!(lib, notmuch_database_find_message)(
*db.inner.read().unwrap(),
msg_id.as_ptr(),
std::ptr::addr_of_mut!(message),
)
};
if message.is_null() {
return Err(Error::new(format!(
"Message with message id {:?} not found in notmuch database.",
msg_id
)));
}
Ok(Message {
lib,
message,
is_from_thread: false,
_ph: std::marker::PhantomData,
})
}
pub fn env_hash(&self) -> EnvelopeHash {
let msg_id = unsafe { call!(self.lib, notmuch_message_get_message_id)(self.message) };
let c_str = unsafe { CStr::from_ptr(msg_id) };
EnvelopeHash::from_bytes(c_str.to_bytes_with_nul())
}
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()
}
pub fn msg_id_cstr(&self) -> &CStr {
let msg_id = unsafe { call!(self.lib, notmuch_message_get_message_id)(self.message) };
unsafe { CStr::from_ptr(msg_id) }
}
pub fn date(&self) -> crate::UnixTimestamp {
(unsafe { call!(self.lib, notmuch_message_get_date)(self.message) }) as u64
}
pub fn into_envelope(
self,
index: &RwLock<HashMap<EnvelopeHash, CString>>,
tag_index: &RwLock<BTreeMap<TagHash, String>>,
) -> Envelope {
let env_hash = self.env_hash();
let mut env = Envelope::new(env_hash);
index
.write()
.unwrap()
.insert(env_hash, self.msg_id_cstr().into());
let mut tag_lock = tag_index.write().unwrap();
let (flags, tags) = TagIterator::new(&self).collect_flags_and_tags();
for tag in tags {
let num = TagHash::from_bytes(tag.as_bytes());
tag_lock.entry(num).or_insert(tag);
env.tags_mut().push(num);
}
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
}
pub fn replies_iter(&self) -> Option<MessageIterator> {
if self.is_from_thread {
let messages = unsafe { call!(self.lib, notmuch_message_get_replies)(self.message) };
if messages.is_null() {
None
} else {
Some(MessageIterator {
lib: self.lib.clone(),
messages,
_ph: std::marker::PhantomData,
is_from_thread: true,
})
}
} else {
None
}
}
pub fn into_thread_node(&self) -> (ThreadNodeHash, ThreadNode) {
(
ThreadNodeHash::from(self.msg_id()),
ThreadNode {
message: Some(self.env_hash()),
parent: None,
other_mailbox: false,
children: vec![],
date: self.date(),
show_subject: true,
group: ThreadHash::new(),
unseen: false,
},
)
}
pub fn add_tag(&self, tag: &CStr) -> Result<()> {
if let Err(err) = unsafe {
try_call!(
self.lib,
call!(self.lib, notmuch_message_add_tag)(self.message, tag.as_ptr())
)
} {
return Err(Error::new("Could not set tag.").set_source(Some(Arc::new(err))));
}
Ok(())
}
pub fn remove_tag(&self, tag: &CStr) -> Result<()> {
if let Err(err) = unsafe {
try_call!(
self.lib,
call!(self.lib, notmuch_message_remove_tag)(self.message, tag.as_ptr())
)
} {
return Err(Error::new("Could not set tag.").set_source(Some(Arc::new(err))));
}
Ok(())
}
pub fn tags(&'m self) -> TagIterator<'m> {
TagIterator::new(self)
}
pub fn tags_to_maildir_flags(&self) -> Result<()> {
if let Err(err) = unsafe {
try_call!(
self.lib,
call!(self.lib, notmuch_message_tags_to_maildir_flags)(self.message)
)
} {
return Err(Error::new("Could not set flags.").set_source(Some(Arc::new(err))));
}
Ok(())
}
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())
}
}
impl Drop for Message<'_> {
fn drop(&mut self) {
unsafe { call!(self.lib, notmuch_message_destroy)(self.message) };
}
}
pub struct MessageIterator<'query> {
pub lib: Arc<libloading::Library>,
pub messages: *mut notmuch_messages_t,
pub is_from_thread: bool,
pub _ph: std::marker::PhantomData<*const Query<'query>>,
}
impl<'q> Iterator for MessageIterator<'q> {
type Item = Message<'q>;
fn next(&mut self) -> Option<Self::Item> {
if self.messages.is_null() {
None
} else if unsafe { call!(self.lib, notmuch_messages_valid)(self.messages) } == 1 {
let message = unsafe { call!(self.lib, notmuch_messages_get)(self.messages) };
unsafe {
call!(self.lib, notmuch_messages_move_to_next)(self.messages);
}
Some(Message {
lib: self.lib.clone(),
message,
is_from_thread: self.is_from_thread,
_ph: std::marker::PhantomData,
})
} else {
self.messages = std::ptr::null_mut();
None
}
}
}

View File

@ -1,129 +0,0 @@
/*
* melib - notmuch backend
*
* 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::*;
pub struct TagIterator<'m> {
pub tags: *mut notmuch_tags_t,
pub message: &'m Message<'m>,
}
impl Drop for TagIterator<'_> {
fn drop(&mut self) {
unsafe { call!(self.message.lib, notmuch_tags_destroy)(self.tags) };
}
}
impl<'m> TagIterator<'m> {
pub fn new(message: &'m Message<'m>) -> TagIterator<'m> {
TagIterator {
tags: unsafe { call!(message.lib, notmuch_message_get_tags)(message.message) },
message,
}
}
pub fn collect_flags_and_tags(self) -> (Flag, Vec<String>) {
fn flags(path: &CStr) -> Flag {
let mut flag = Flag::default();
let mut ptr = path.to_bytes().len().saturating_sub(1);
let mut is_valid = true;
while !path.to_bytes()[..ptr + 1].ends_with(b":2,") {
match path.to_bytes()[ptr] {
b'D' => flag |= Flag::DRAFT,
b'F' => flag |= Flag::FLAGGED,
b'P' => flag |= Flag::PASSED,
b'R' => flag |= Flag::REPLIED,
b'S' => flag |= Flag::SEEN,
b'T' => flag |= Flag::TRASHED,
_ => {
is_valid = false;
break;
}
}
if ptr == 0 {
is_valid = false;
break;
}
ptr -= 1;
}
if !is_valid {
return Flag::default();
}
flag
}
let fs_path =
unsafe { call!(self.message.lib, notmuch_message_get_filename)(self.message.message) };
let c_str = unsafe { CStr::from_ptr(fs_path) };
let tags = self.collect::<Vec<&CStr>>();
let mut flag = Flag::default();
let mut vec = vec![];
for t in tags {
match t.to_bytes() {
b"draft" => {
flag.set(Flag::DRAFT, true);
}
b"flagged" => {
flag.set(Flag::FLAGGED, true);
}
b"passed" => {
flag.set(Flag::PASSED, true);
}
b"replied" => {
flag.set(Flag::REPLIED, true);
}
b"unread" => {
flag.set(Flag::SEEN, false);
}
b"trashed" => {
flag.set(Flag::TRASHED, true);
}
_other => {
vec.push(t.to_string_lossy().into_owned());
}
}
}
(flag | flags(c_str), vec)
}
}
impl<'m> Iterator for TagIterator<'m> {
type Item = &'m CStr;
fn next(&mut self) -> Option<Self::Item> {
if self.tags.is_null() {
None
} else if unsafe { call!(self.message.lib, notmuch_tags_valid)(self.tags) } == 1 {
let ret = Some(unsafe {
CStr::from_ptr(call!(self.message.lib, notmuch_tags_get)(self.tags))
});
unsafe {
call!(self.message.lib, notmuch_tags_move_to_next)(self.tags);
}
ret
} else {
self.tags = std::ptr::null_mut();
None
}
}
}

View File

@ -1,92 +0,0 @@
/*
* melib - notmuch backend
*
* 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 crate::thread::ThreadHash;
pub struct Thread<'query> {
pub lib: Arc<libloading::Library>,
pub ptr: *mut notmuch_thread_t,
pub _ph: std::marker::PhantomData<*const Query<'query>>,
}
impl<'q> Thread<'q> {
pub fn id(&self) -> ThreadHash {
let thread_id = unsafe { call!(self.lib, notmuch_thread_get_thread_id)(self.ptr) };
let c_str = unsafe { CStr::from_ptr(thread_id) };
ThreadHash::from(c_str.to_bytes())
}
pub fn date(&self) -> crate::UnixTimestamp {
(unsafe { call!(self.lib, notmuch_thread_get_newest_date)(self.ptr) }) as u64
}
pub fn len(&self) -> usize {
(unsafe { call!(self.lib, notmuch_thread_get_total_messages)(self.ptr) }) as usize
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn iter(&'q self) -> MessageIterator<'q> {
let ptr = unsafe { call!(self.lib, notmuch_thread_get_messages)(self.ptr) };
MessageIterator {
lib: self.lib.clone(),
messages: ptr,
is_from_thread: true,
_ph: std::marker::PhantomData,
}
}
}
impl Drop for Thread<'_> {
fn drop(&mut self) {
unsafe { call!(self.lib, notmuch_thread_destroy)(self.ptr) }
}
}
pub struct ThreadsIterator<'query> {
pub lib: Arc<libloading::Library>,
pub threads: *mut notmuch_threads_t,
pub _ph: std::marker::PhantomData<*const Query<'query>>,
}
impl<'q> Iterator for ThreadsIterator<'q> {
type Item = Thread<'q>;
fn next(&mut self) -> Option<Self::Item> {
if self.threads.is_null() {
None
} else if unsafe { call!(self.lib, notmuch_threads_valid)(self.threads) } == 1 {
let thread = unsafe { call!(self.lib, notmuch_threads_get)(self.threads) };
unsafe {
call!(self.lib, notmuch_threads_move_to_next)(self.threads);
}
Some(Thread {
lib: self.lib.clone(),
ptr: thread,
_ph: std::marker::PhantomData,
})
} else {
self.threads = std::ptr::null_mut();
None
}
}
}

View File

@ -1,187 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2021 Ilya Medvedev
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
/* Code from <https://github.com/iam-medvedev/rust-utf7-imap> */
//! A Rust library for encoding and decoding [UTF-7](https://datatracker.ietf.org/doc/html/rfc2152) string as defined by the [IMAP](https://datatracker.ietf.org/doc/html/rfc3501) standard in [RFC 3501 (#5.1.3)](https://datatracker.ietf.org/doc/html/rfc3501#section-5.1.3).
//!
//! Idea is based on Python [mutf7](https://github.com/cheshire-mouse/mutf7) library.
use encoding_rs::UTF_16BE;
use regex::{Captures, Regex};
/// Encode UTF-7 IMAP mailbox name
///
/// <https://datatracker.ietf.org/doc/html/rfc3501#section-5.1.3>
pub fn encode_utf7_imap(text: &str) -> String {
let mut result = "".to_string();
let text = text.replace('&', "&-");
let mut text = text.as_str();
while !text.is_empty() {
result = format!("{}{}", result, get_ascii(text));
text = remove_ascii(text);
if !text.is_empty() {
let tmp = get_nonascii(text);
result = format!("{}{}", result, encode_modified_utf7(tmp));
text = remove_nonascii(text);
}
}
result
}
fn is_ascii_custom(c: u8) -> bool {
(0x20..=0x7f).contains(&c)
}
fn get_ascii(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if !is_ascii_custom(item) {
return &s[0..i];
}
}
s
}
fn get_nonascii(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if is_ascii_custom(item) {
return &s[0..i];
}
}
s
}
fn remove_ascii(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if !is_ascii_custom(item) {
return &s[i..];
}
}
""
}
fn remove_nonascii(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if is_ascii_custom(item) {
return &s[i..];
}
}
""
}
fn encode_modified_utf7(text: &str) -> String {
let capacity = 2 * text.len();
let mut input = Vec::with_capacity(capacity);
let text_u16 = text.encode_utf16();
for value in text_u16 {
input.extend_from_slice(&value.to_be_bytes());
}
let text_u16 = base64::encode(input);
let text_u16 = text_u16.trim_end_matches('=');
let result = text_u16.replace('/', ",");
format!("&{}-", result)
}
/// Decode UTF-7 IMAP mailbox name
///
/// <https://datatracker.ietf.org/doc/html/rfc3501#section-5.1.3>
pub fn decode_utf7_imap(text: &str) -> String {
let pattern = Regex::new(r"&([^-]*)-").unwrap();
pattern.replace_all(text, expand).to_string()
}
fn expand(cap: &Captures) -> String {
if cap.get(1).unwrap().as_str() == "" {
"&".to_string()
} else {
decode_utf7_part(cap.get(0).unwrap().as_str())
}
}
fn decode_utf7_part(text: &str) -> String {
if text == "&-" {
return String::from("&");
}
let text_mb64 = &text[1..text.len() - 1];
let mut text_b64 = text_mb64.replace(',', "/");
while (text_b64.len() % 4) != 0 {
text_b64 += "=";
}
let text_u16 = base64::decode(text_b64).unwrap();
let (cow, _encoding_used, _had_errors) = UTF_16BE.decode(&text_u16);
let result = cow.as_ref();
String::from(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_test() {
assert_eq!(
encode_utf7_imap("Отправленные"),
"&BB4EQgQ,BEAEMAQyBDsENQQ9BD0ESwQ1-"
);
}
#[test]
fn encode_test_split() {
assert_eq!(
encode_utf7_imap("Šiukšliadėžė"),
"&AWA-iuk&AWE-liad&ARcBfgEX-"
)
}
#[test]
fn encode_consecutive_accents() {
assert_eq!(encode_utf7_imap("théâtre"), "th&AOkA4g-tre")
}
#[test]
fn decode_test() {
assert_eq!(
decode_utf7_imap("&BB4EQgQ,BEAEMAQyBDsENQQ9BD0ESwQ1-"),
"Отправленные"
);
}
#[test]
fn decode_test_split() {
// input string with utf7 encoded bits being separated by ascii
assert_eq!(
decode_utf7_imap("&AWA-iuk&AWE-liad&ARcBfgEX-"),
"Šiukšliadėžė"
)
}
#[test]
fn decode_consecutive_accents() {
assert_eq!(decode_utf7_imap("th&AOkA4g-tre"), "théâtre")
}
}

View File

@ -1,76 +1,92 @@
/*
* meli - melib 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 std::{
collections::{BTreeMap, HashMap, HashSet},
ops::{Deref, DerefMut},
sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard},
};
use smallvec::SmallVec;
use super::*;
use crate::backends::{MailboxHash, TagHash};
use crate::backends::FolderHash;
use std::collections::BTreeMap;
use std::ops::{Deref, DerefMut};
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
pub type EnvelopeRef<'g> = RwRef<'g, EnvelopeHash, Envelope>;
pub type EnvelopeRefMut<'g> = RwRefMut<'g, EnvelopeHash, Envelope>;
use fnv::FnvHashMap;
#[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>>>,
pub mailboxes: Arc<RwLock<HashMap<MailboxHash, HashSet<EnvelopeHash>>>>,
pub tag_index: Arc<RwLock<BTreeMap<TagHash, String>>>,
pub struct EnvelopeRef<'g> {
guard: RwLockReadGuard<'g, FnvHashMap<EnvelopeHash, Envelope>>,
env_hash: EnvelopeHash,
}
impl Default for Collection {
fn default() -> Self {
Self::new()
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, FnvHashMap<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, Deserialize, Default, Serialize)]
pub struct Collection {
pub envelopes: Arc<RwLock<FnvHashMap<EnvelopeHash, Envelope>>>,
message_ids: FnvHashMap<Vec<u8>, EnvelopeHash>,
date_index: BTreeMap<UnixTimestamp, EnvelopeHash>,
subject_index: Option<BTreeMap<String, EnvelopeHash>>,
pub threads: FnvHashMap<FolderHash, Threads>,
sent_folder: Option<FolderHash>,
}
impl Drop for Collection {
fn drop(&mut self) {
/*
let cache_dir: xdg::BaseDirectories =
xdg::BaseDirectories::with_profile("meli", "threads".to_string()).unwrap();
if let Ok(cached) = cache_dir.place_cache_file("threads") {
/* place result in cache directory */
let f = match fs::File::create(cached) {
Ok(f) => f,
Err(e) => {
panic!("{}", e);
}
};
let writer = io::BufWriter::new(f);
bincode::serialize_into(writer, &self.threads).unwrap();
}
*/
}
}
impl Collection {
pub fn new() -> Self {
let message_id_index = Arc::new(RwLock::new(HashMap::with_capacity_and_hasher(
16,
Default::default(),
)));
let threads = Arc::new(RwLock::new(HashMap::with_capacity_and_hasher(
16,
Default::default(),
)));
let mailboxes = Arc::new(RwLock::new(HashMap::with_capacity_and_hasher(
16,
Default::default(),
)));
pub fn new(envelopes: FnvHashMap<EnvelopeHash, Envelope>) -> Collection {
let date_index = BTreeMap::new();
let subject_index = None;
let message_ids = FnvHashMap::with_capacity_and_hasher(2048, Default::default());
Self {
envelopes: Arc::new(RwLock::new(Default::default())),
tag_index: Arc::new(RwLock::new(BTreeMap::default())),
message_id_index,
/* Scrap caching for now. When a cached threads file is loaded, we must remove/rehash the
* thread nodes that shouldn't exist anymore (e.g. because their file moved from /new to
* /cur, or it was deleted).
*/
let threads = FnvHashMap::with_capacity_and_hasher(16, Default::default());
Collection {
envelopes: Arc::new(RwLock::new(envelopes)),
date_index,
message_ids,
subject_index,
threads,
mailboxes,
sent_mailbox: Arc::new(RwLock::new(None)),
sent_folder: None,
}
}
@ -82,23 +98,15 @@ impl Collection {
self.envelopes.read().unwrap().is_empty()
}
pub fn remove(&self, envelope_hash: EnvelopeHash, mailbox_hash: MailboxHash) {
pub fn remove(&mut self, envelope_hash: EnvelopeHash, folder_hash: FolderHash) {
debug!("DEBUG: Removing {}", envelope_hash);
self.envelopes.write().unwrap().remove(&envelope_hash);
self.mailboxes
.write()
.unwrap()
.entry(mailbox_hash)
.and_modify(|m| {
m.remove(&envelope_hash);
});
let mut threads_lck = self.threads.write().unwrap();
threads_lck
.entry(mailbox_hash)
self.threads
.entry(folder_hash)
.or_default()
.remove(envelope_hash);
for (h, t) in threads_lck.iter_mut() {
if *h == mailbox_hash {
for (h, t) in self.threads.iter_mut() {
if *h == folder_hash {
continue;
}
t.remove(envelope_hash);
@ -106,81 +114,72 @@ impl Collection {
}
pub fn rename(
&self,
&mut self,
old_hash: EnvelopeHash,
new_hash: EnvelopeHash,
mailbox_hash: MailboxHash,
) -> bool {
if !self.envelopes.read().unwrap().contains_key(&old_hash) {
return false;
folder_hash: FolderHash,
) {
if !self.envelopes.write().unwrap().contains_key(&old_hash) {
return;
}
let mut envelope = self.envelopes.write().unwrap().remove(&old_hash).unwrap();
self.mailboxes
.write()
.unwrap()
.entry(mailbox_hash)
.and_modify(|m| {
m.remove(&old_hash);
m.insert(new_hash);
});
envelope.set_hash(new_hash);
self.envelopes.write().unwrap().insert(new_hash, envelope);
let mut threads_lck = self.threads.write().unwrap();
let mut env = self.envelopes.write().unwrap().remove(&old_hash).unwrap();
env.set_hash(new_hash);
self.message_ids
.insert(env.message_id().raw().to_vec(), new_hash);
self.envelopes.write().unwrap().insert(new_hash, env);
{
if threads_lck
.entry(mailbox_hash)
if self
.threads
.entry(folder_hash)
.or_default()
.update_envelope(&self.envelopes, old_hash, new_hash)
.is_ok()
{
return true;
return;
}
}
/* envelope is not in threads, so insert it */
threads_lck
.entry(mailbox_hash)
self.threads
.entry(folder_hash)
.or_default()
.insert(&self.envelopes, new_hash);
for (h, t) in threads_lck.iter_mut() {
if *h == mailbox_hash {
.insert(&mut self.envelopes, new_hash);
for (h, t) in self.threads.iter_mut() {
if *h == folder_hash {
continue;
}
_ = t.update_envelope(&self.envelopes, old_hash, new_hash);
t.update_envelope(&self.envelopes, old_hash, new_hash)
.ok()
.take();
}
true
}
/// Merge new mailbox to collection and update threads.
/// Returns a list of already existing mailboxs whose threads were updated
/// Merge new Mailbox to collection and update threads.
/// Returns a list of already existing folders whose threads were updated
pub fn merge(
&self,
mut new_envelopes: HashMap<EnvelopeHash, Envelope>,
mailbox_hash: MailboxHash,
sent_mailbox: Option<MailboxHash>,
) -> Option<SmallVec<[MailboxHash; 8]>> {
*self.sent_mailbox.write().unwrap() = sent_mailbox;
&mut self,
mut new_envelopes: FnvHashMap<EnvelopeHash, Envelope>,
folder_hash: FolderHash,
sent_folder: Option<FolderHash>,
) -> Option<StackVec<FolderHash>> {
self.sent_folder = sent_folder;
for (h, e) in new_envelopes.iter() {
self.message_ids.insert(e.message_id().raw().to_vec(), *h);
}
let Self {
ref threads,
ref envelopes,
ref mailboxes,
ref sent_mailbox,
let &mut Collection {
ref mut threads,
ref mut envelopes,
ref sent_folder,
..
} = self;
let mut threads_lck = threads.write().unwrap();
let mut mailboxes_lck = mailboxes.write().unwrap();
if let std::collections::hash_map::Entry::Vacant(e) = threads_lck.entry(mailbox_hash) {
e.insert(Threads::new(new_envelopes.len()));
mailboxes_lck.insert(mailbox_hash, new_envelopes.keys().cloned().collect());
if !threads.contains_key(&folder_hash) {
threads.insert(folder_hash, Threads::new(new_envelopes.len()));
for (h, e) in new_envelopes {
envelopes.write().unwrap().insert(h, e);
}
} else {
mailboxes_lck.entry(mailbox_hash).and_modify(|m| {
m.extend(new_envelopes.keys().cloned());
});
threads_lck.entry(mailbox_hash).and_modify(|t| {
threads.entry(folder_hash).and_modify(|t| {
let mut ordered_hash_set =
new_envelopes.keys().cloned().collect::<Vec<EnvelopeHash>>();
ordered_hash_set.sort_by(|a, b| {
@ -199,20 +198,15 @@ impl Collection {
});
}
let mut ret = SmallVec::new();
let keys = threads_lck.keys().cloned().collect::<Vec<MailboxHash>>();
let mut ret = StackVec::new();
let keys = threads.keys().cloned().collect::<Vec<FolderHash>>();
for t_fh in keys {
if t_fh == mailbox_hash {
if t_fh == folder_hash {
continue;
}
if sent_mailbox
.read()
.unwrap()
.map(|f| f == mailbox_hash)
.unwrap_or(false)
{
if sent_folder.map(|f| f == folder_hash).unwrap_or(false) {
let envelopes_lck = envelopes.read().unwrap();
let mut ordered_hash_set = threads_lck[&mailbox_hash]
let mut ordered_hash_set = threads[&folder_hash]
.hash_set
.iter()
.cloned()
@ -226,24 +220,16 @@ impl Collection {
drop(envelopes_lck);
let mut updated = false;
for h in ordered_hash_set {
updated |= threads_lck
.entry(t_fh)
.or_default()
.insert_reply(envelopes, h);
updated |= threads.entry(t_fh).or_default().insert_reply(envelopes, h);
}
if updated {
ret.push(t_fh);
}
continue;
}
if sent_mailbox
.read()
.unwrap()
.map(|f| f == t_fh)
.unwrap_or(false)
{
if sent_folder.map(|f| f == t_fh).unwrap_or(false) {
let envelopes_lck = envelopes.read().unwrap();
let mut ordered_hash_set = threads_lck[&t_fh]
let mut ordered_hash_set = threads[&t_fh]
.hash_set
.iter()
.cloned()
@ -257,13 +243,13 @@ impl Collection {
drop(envelopes_lck);
let mut updated = false;
for h in ordered_hash_set {
updated |= threads_lck
.entry(mailbox_hash)
updated |= threads
.entry(folder_hash)
.or_default()
.insert_reply(envelopes, h);
}
if updated {
ret.push(mailbox_hash);
ret.push(folder_hash);
}
}
}
@ -275,38 +261,27 @@ impl Collection {
}
pub fn update(
&self,
&mut self,
old_hash: EnvelopeHash,
mut envelope: Envelope,
mailbox_hash: MailboxHash,
folder_hash: FolderHash,
) {
let old_env = self.envelopes.write().unwrap().remove(&old_hash).unwrap();
envelope.set_thread(old_env.thread());
let new_hash = envelope.hash();
self.mailboxes
.write()
.unwrap()
.entry(mailbox_hash)
.and_modify(|m| {
m.remove(&old_hash);
m.insert(new_hash);
});
self.message_ids
.insert(envelope.message_id().raw().to_vec(), new_hash);
self.envelopes.write().unwrap().insert(new_hash, envelope);
let mut threads_lck = self.threads.write().unwrap();
if self
.sent_mailbox
.read()
.unwrap()
.map(|f| f == mailbox_hash)
.unwrap_or(false)
{
for (_, t) in threads_lck.iter_mut() {
_ = t.update_envelope(&self.envelopes, old_hash, new_hash);
if self.sent_folder.map(|f| f == folder_hash).unwrap_or(false) {
for (_, t) in self.threads.iter_mut() {
t.update_envelope(&self.envelopes, old_hash, new_hash)
.unwrap_or(());
}
}
{
if threads_lck
.entry(mailbox_hash)
if self
.threads
.entry(folder_hash)
.or_default()
.update_envelope(&self.envelopes, old_hash, new_hash)
.is_ok()
@ -315,175 +290,56 @@ impl Collection {
}
}
/* envelope is not in threads, so insert it */
threads_lck
.entry(mailbox_hash)
.or_default()
.insert(&self.envelopes, new_hash);
for (h, t) in threads_lck.iter_mut() {
if *h == mailbox_hash {
continue;
}
_ = t.update_envelope(&self.envelopes, old_hash, new_hash);
}
}
pub fn update_flags(&self, env_hash: EnvelopeHash, mailbox_hash: MailboxHash) {
let mut threads_lck = self.threads.write().unwrap();
if self
.sent_mailbox
.read()
.unwrap()
.map(|f| f == mailbox_hash)
.unwrap_or(false)
{
for (_, t) in threads_lck.iter_mut() {
_ = t.update_envelope(&self.envelopes, env_hash, env_hash);
}
}
{
if threads_lck
.entry(mailbox_hash)
.or_default()
.update_envelope(&self.envelopes, env_hash, env_hash)
.is_ok()
{
return;
}
}
/* envelope is not in threads, so insert it */
threads_lck
.entry(mailbox_hash)
.or_default()
.insert(&self.envelopes, env_hash);
for (h, t) in threads_lck.iter_mut() {
if *h == mailbox_hash {
continue;
}
_ = t.update_envelope(&self.envelopes, env_hash, env_hash);
}
}
pub fn insert(&self, envelope: Envelope, mailbox_hash: MailboxHash) -> bool {
let hash = envelope.hash();
self.mailboxes
.write()
.unwrap()
.entry(mailbox_hash)
.and_modify(|m| {
m.insert(hash);
});
self.envelopes.write().unwrap().insert(hash, envelope);
self.threads
.write()
.unwrap()
.entry(mailbox_hash)
.entry(folder_hash)
.or_default()
.insert(&self.envelopes, hash);
if self
.sent_mailbox
.read()
.unwrap()
.map(|f| f == mailbox_hash)
.unwrap_or(false)
.insert(&mut self.envelopes, new_hash);
for (h, t) in self.threads.iter_mut() {
if *h == folder_hash {
continue;
}
t.update_envelope(&self.envelopes, old_hash, new_hash)
.ok()
.take();
}
}
pub fn insert(&mut self, envelope: Envelope, folder_hash: FolderHash) {
let hash = envelope.hash();
self.message_ids
.insert(envelope.message_id().raw().to_vec(), hash);
self.envelopes.write().unwrap().insert(hash, envelope);
if !self
.threads
.entry(folder_hash)
.or_default()
.insert_reply(&mut self.envelopes, hash)
{
self.insert_reply(hash);
self.threads
.entry(folder_hash)
.or_default()
.insert(&mut self.envelopes, hash);
}
false
}
pub fn insert_reply(&self, env_hash: EnvelopeHash) {
pub fn insert_reply(&mut self, env_hash: EnvelopeHash) {
debug_assert!(self.envelopes.read().unwrap().contains_key(&env_hash));
let mut iter = self.threads.write().unwrap();
for (_, t) in iter.iter_mut() {
t.insert_reply(&self.envelopes, env_hash);
for (_, t) in self.threads.iter_mut() {
t.insert_reply(&mut self.envelopes, env_hash);
}
}
pub fn get_env(&'_ self, hash: EnvelopeHash) -> EnvelopeRef<'_> {
let guard: RwLockReadGuard<'_, _> = self.envelopes.read().unwrap();
EnvelopeRef { guard, hash }
pub fn get_env<'g>(&'g self, env_hash: EnvelopeHash) -> EnvelopeRef<'g> {
let guard: RwLockReadGuard<'g, _> = self.envelopes.read().unwrap();
EnvelopeRef { guard, env_hash }
}
pub fn get_env_mut(&'_ self, hash: EnvelopeHash) -> EnvelopeRefMut<'_> {
pub fn get_env_mut<'g>(&'g mut self, env_hash: EnvelopeHash) -> EnvelopeRefMut<'g> {
let guard = self.envelopes.write().unwrap();
EnvelopeRefMut { guard, hash }
}
pub fn get_threads(&'_ self, hash: MailboxHash) -> RwRef<'_, MailboxHash, Threads> {
let guard = self.threads.read().unwrap();
RwRef { guard, hash }
}
pub fn get_mailbox(
&'_ self,
hash: MailboxHash,
) -> RwRef<'_, MailboxHash, HashSet<EnvelopeHash>> {
let guard = self.mailboxes.read().unwrap();
RwRef { guard, hash }
EnvelopeRefMut { guard, env_hash }
}
pub fn contains_key(&self, env_hash: &EnvelopeHash) -> bool {
self.envelopes.read().unwrap().contains_key(env_hash)
}
pub fn new_mailbox(&self, mailbox_hash: MailboxHash) {
let mut mailboxes_lck = self.mailboxes.write().unwrap();
if let std::collections::hash_map::Entry::Vacant(e) = mailboxes_lck.entry(mailbox_hash) {
e.insert(Default::default());
self.threads
.write()
.unwrap()
.insert(mailbox_hash, Threads::default());
}
}
}
pub struct RwRef<'g, K: std::cmp::Eq + std::hash::Hash, V> {
guard: RwLockReadGuard<'g, HashMap<K, V>>,
hash: K,
}
impl<K: std::cmp::Eq + std::hash::Hash, V> Deref for RwRef<'_, K, V> {
type Target = V;
fn deref(&self) -> &V {
self.guard.get(&self.hash).expect("Hash was not found")
}
}
impl<K: std::cmp::Eq + std::hash::Hash, V> AsRef<V> for RwRef<'_, K, V> {
fn as_ref(&self) -> &V {
self.guard.get(&self.hash).expect("Hash was not found")
}
}
pub struct RwRefMut<'g, K: std::cmp::Eq + std::hash::Hash, V> {
guard: RwLockWriteGuard<'g, HashMap<K, V>>,
hash: K,
}
impl<K: std::cmp::Eq + std::hash::Hash, V> DerefMut for RwRefMut<'_, K, V> {
fn deref_mut(&mut self) -> &mut V {
self.guard.get_mut(&self.hash).expect("Hash was not found")
}
}
impl<K: std::cmp::Eq + std::hash::Hash, V> Deref for RwRefMut<'_, K, V> {
type Target = V;
fn deref(&self) -> &V {
self.guard.get(&self.hash).expect("Hash was not found")
}
}
impl<K: std::cmp::Eq + std::hash::Hash, V> AsRef<V> for RwRefMut<'_, K, V> {
fn as_ref(&self) -> &V {
self.guard.get(&self.hash).expect("Hash was not found")
}
}
impl<K: std::cmp::Eq + std::hash::Hash, V> AsMut<V> for RwRefMut<'_, K, V> {
fn as_mut(&mut self) -> &mut V {
self.guard.get_mut(&self.hash).expect("Hash was not found")
}
}

View File

@ -18,258 +18,165 @@
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
//! Basic mail account configuration to use with
//! [`backends`](./backends/index.html)
use std::collections::HashMap;
use crate::backends::SpecialUseMailbox;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::{
backends::SpecialUsageMailbox,
error::{Error, Result},
};
pub use crate::{SortField, SortOrder};
use std::collections::HashMap;
#[derive(Debug, Serialize, Default, Clone)]
pub struct AccountSettings {
pub name: String,
pub root_mailbox: String,
pub root_folder: String,
pub format: String,
pub identity: String,
pub extra_identities: Vec<String>,
pub read_only: bool,
pub display_name: Option<String>,
pub subscribed_folders: Vec<String>,
#[serde(default)]
pub order: (SortField, SortOrder),
pub subscribed_mailboxes: Vec<String>,
#[serde(default)]
pub mailboxes: HashMap<String, MailboxConf>,
#[serde(default)]
pub manual_refresh: bool,
pub folders: HashMap<String, FolderConf>,
#[serde(flatten)]
pub extra: HashMap<String, String>,
}
impl AccountSettings {
/// Create the account's display name from fields
/// [`AccountSettings::identity`] and [`AccountSettings::display_name`].
pub fn make_display_name(&self) -> String {
if let Some(d) = self.display_name.as_ref() {
format!("{} <{}>", d, self.identity)
} else {
self.identity.to_string()
}
pub fn format(&self) -> &str {
&self.format
}
pub fn name(&self) -> &str {
&self.name
}
pub fn set_name(&mut self, s: String) {
self.name = s;
}
pub fn root_folder(&self) -> &str {
&self.root_folder
}
pub fn identity(&self) -> &str {
&self.identity
}
pub fn read_only(&self) -> bool {
self.read_only
}
pub fn display_name(&self) -> Option<&String> {
self.display_name.as_ref()
}
pub fn order(&self) -> Option<(SortField, SortOrder)> {
Some(self.order)
pub fn subscribed_folders(&self) -> &Vec<String> {
&self.subscribed_folders
}
#[cfg(feature = "vcard")]
pub fn vcard_folder(&self) -> Option<&str> {
self.extra.get("vcard_folder").map(String::as_str)
}
/// Get the server password, either directly from the `server_password`
/// settings value, or by running the `server_password_command` and reading
/// the output.
pub fn server_password(&self) -> Result<String> {
if let Some(cmd) = self.extra.get("server_password_command") {
let output = std::process::Command::new("sh")
.args(["-c", cmd])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()?;
if output.status.success() {
Ok(std::str::from_utf8(&output.stdout)?.trim_end().to_string())
} else {
Err(Error::new(format!(
"({}) server_password_command `{}` returned {}: {}",
self.name,
cmd,
output.status,
String::from_utf8_lossy(&output.stderr)
)))
}
} else if let Some(pass) = self.extra.get("server_password") {
Ok(pass.to_owned())
} else {
Err(Error::new(
"Configuration error: connection requires either server_password or \
server_password_command",
))
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct MailboxConf {
#[serde(alias = "rename")]
pub alias: Option<String>,
#[serde(default = "false_val")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FolderConf {
pub rename: Option<String>,
#[serde(default = "true_val")]
pub autoload: bool,
#[serde(default)]
#[serde(deserialize_with = "toggleflag_de")]
pub subscribe: ToggleFlag,
#[serde(default)]
#[serde(deserialize_with = "toggleflag_de")]
pub ignore: ToggleFlag,
#[serde(default = "none")]
pub usage: Option<SpecialUsageMailbox>,
#[serde(default = "none")]
pub sort_order: Option<usize>,
#[serde(default = "none")]
pub encoding: Option<String>,
pub usage: Option<SpecialUseMailbox>,
#[serde(flatten)]
pub extra: HashMap<String, String>,
}
impl Default for MailboxConf {
impl Default for FolderConf {
fn default() -> Self {
Self {
alias: None,
autoload: false,
FolderConf {
rename: None,
autoload: true,
subscribe: ToggleFlag::Unset,
ignore: ToggleFlag::Unset,
usage: None,
sort_order: None,
encoding: None,
extra: HashMap::default(),
}
}
}
impl MailboxConf {
pub fn alias(&self) -> Option<&str> {
self.alias.as_deref()
impl FolderConf {
pub fn rename(&self) -> Option<&str> {
self.rename.as_ref().map(String::as_str)
}
}
pub const fn true_val() -> bool {
pub(in crate::conf) fn true_val() -> bool {
true
}
pub const fn false_val() -> bool {
false
}
pub const fn none<T>() -> Option<T> {
pub(in crate::conf) fn none<T>() -> Option<T> {
None
}
macro_rules! named_unit_variant {
($variant:ident) => {
pub mod $variant {
pub fn deserialize<'de, D>(deserializer: D) -> Result<(), D::Error>
where
D: serde::Deserializer<'de>,
{
struct V;
impl<'de> serde::de::Visitor<'de> for V {
type Value = ();
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str(concat!("\"", stringify!($variant), "\""))
}
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
if value == stringify!($variant) {
Ok(())
} else {
Err(E::invalid_value(serde::de::Unexpected::Str(value), &self))
}
}
}
deserializer.deserialize_str(V)
}
}
};
}
mod strings {
named_unit_variant!(ask);
}
#[derive(Copy, Default, Debug, Clone, PartialEq, Eq)]
#[derive(Copy, Debug, Clone, PartialEq)]
pub enum ToggleFlag {
#[default]
Unset,
InternalVal(bool),
False,
True,
Ask,
}
impl From<bool> for ToggleFlag {
fn from(val: bool) -> Self {
if val {
Self::True
ToggleFlag::True
} else {
Self::False
ToggleFlag::False
}
}
}
impl Default for ToggleFlag {
fn default() -> Self {
ToggleFlag::Unset
}
}
impl ToggleFlag {
pub fn is_unset(&self) -> bool {
Self::Unset == *self
ToggleFlag::Unset == *self
}
pub fn is_internal(&self) -> bool {
matches!(self, Self::InternalVal(_))
if let ToggleFlag::InternalVal(_) = *self {
true
} else {
false
}
}
pub fn is_ask(&self) -> bool {
matches!(self, Self::Ask)
}
pub fn is_false(&self) -> bool {
matches!(self, Self::False | Self::InternalVal(false))
ToggleFlag::False == *self || ToggleFlag::InternalVal(false) == *self
}
pub fn is_true(&self) -> bool {
matches!(self, Self::True | Self::InternalVal(true))
ToggleFlag::True == *self || ToggleFlag::InternalVal(true) == *self
}
}
pub fn toggleflag_de<'de, D>(deserializer: D) -> std::result::Result<ToggleFlag, D::Error>
where
D: Deserializer<'de>,
{
let s = <bool>::deserialize(deserializer);
Ok(match s {
Err(_) => ToggleFlag::Unset,
Ok(true) => ToggleFlag::True,
Ok(false) => ToggleFlag::False,
})
}
impl Serialize for ToggleFlag {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Unset | Self::InternalVal(_) => serializer.serialize_none(),
Self::False => serializer.serialize_bool(false),
Self::True => serializer.serialize_bool(true),
Self::Ask => serializer.serialize_str("ask"),
ToggleFlag::Unset | ToggleFlag::InternalVal(_) => serializer.serialize_none(),
ToggleFlag::False => serializer.serialize_bool(false),
ToggleFlag::True => serializer.serialize_bool(true),
}
}
}
impl<'de> Deserialize<'de> for ToggleFlag {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
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) => Self::True,
InnerToggleFlag::Bool(false) => Self::False,
InnerToggleFlag::Ask => Self::Ask,
},
)
}
}

Some files were not shown because too many files have changed in this diff Show More