release v0.5.2 (#282)

* doc: fix blur in list msg screenshots (#181)

* fix a typo in mbox arg (#245)

`targetted` to `targeted` 👌🏻

* make inbox, sent and drafts folder customizable (#246)

* mbox: make inbox, sent and drafts folder customizable

* msg: update send handler parameters order

* vim: fix extracting message ids from list (#247)

The current method doesn't work because the list uses a fancy line
character (`│`) as the separator, not a regular pipe character (`|`).
Matching for the first number in the line instead solves the problem and
will continue to work regardless of what separator is used.

* add new line after printing strings (#251)

* init cargo workspace (#252)

* init cargo workspaces

* nix: fix assets path

* doc: update rtp vim plugin

* vim: add error message if loading vim plugin from vim/

* init sub crates (#253)

* init sub crates

* doc: update readme

* doc: improve main readme

* doc: add links, add missing crate task

* doc: update emojis

* update cargo lock

* implement contact completion with completefunc (#250)

This allows users to define a command for contact completion with
`g:himalaya_complete_contact_cmd` and trigger it with `<C-x><C-u>` when
writing an email.

* fix clippy lints (#255)

* revert cargo workspace feature

* fix nix run (#274)

* replace cargo2nix by naersk

* add rust-analyzer and rustfmt to nix build inputs

* remove wiki from git submodules, update changelog

* fix missing range when fetch fails, add more logs (#276)

* add missing fix in changelog

* remove blank lines and spaces from plain parts (#280)

* fix watch command (#271)

* remove also tabs from text parts (#280)

* pin native-tls minor version (#278)

* improve msg sanitization (#280)

* fix mbox vim plugin telescope preview (#249)

* bump version v0.5.2

* update changelog

Co-authored-by: Austin Traver <austintraver@gmail.com>
Co-authored-by: Jason Cox <dev@jasoncarloscox.com>
Co-authored-by: Gökmen Görgen <gkmngrgn@gmail.com>
Co-authored-by: Ethiraric <ethiraric@gmail.com>
This commit is contained in:
Clément DOUIN 2022-02-02 02:21:35 +01:00 committed by GitHub
parent f9775ae8af
commit 8cdeba62a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 494 additions and 487 deletions

11
.gitignore vendored
View file

@ -1,4 +1,13 @@
# Cargo build directory
/target /target
/vim/doc/tags
# Nix build directory
/result /result
/result-lib /result-lib
# Direnv
/.envrc
/.direnv
# Vim plugin doc tags file
/vim/doc/tags

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "wiki"]
path = wiki
url = git@github.com:soywod/himalaya.wiki.git

View file

@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.5.2] - 2022-02-02
### Fixed
- Blur in list msg screenshot [#181]
- Make inbox, sent and drafts folders customizable [#172]
- Vim plugin get focused msg id [#268]
- Nix run issue [#272]
- Range not displayed when fetch fails [#276]
- Blank lines and spaces in `text/plain` parts [#280]
- Watch command [#271]
- Mailbox telescope.nvim preview [#249]
### Removed
- The wiki git submodule [#273]
## [0.5.1] - 2021-10-24 ## [0.5.1] - 2021-10-24
### Added ### Added
@ -18,7 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Error when receiving notification from `notify` command [#228] - Error when receiving notification from `notify` command [#228]
### Change ### Changed
- Remove error when empty subject [#229] - Remove error when empty subject [#229]
- Vim plugin does not render anymore the msg by itself, it uses the one available from the CLI [#220] - Vim plugin does not render anymore the msg by itself, it uses the one available from the CLI [#220]
@ -249,7 +266,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Password from command [#22] - Password from command [#22]
- Set up README [#20] - Set up README [#20]
[unreleased]: https://github.com/soywod/himalaya/compare/v0.5.1...HEAD [unreleased]: https://github.com/soywod/himalaya/compare/v0.5.2...HEAD
[0.5.2]: https://github.com/soywod/himalaya/compare/v0.5.1...v0.5.2
[0.5.1]: https://github.com/soywod/himalaya/compare/v0.5.0...v0.5.1 [0.5.1]: https://github.com/soywod/himalaya/compare/v0.5.0...v0.5.1
[0.5.0]: https://github.com/soywod/himalaya/compare/v0.4.0...v0.5.0 [0.5.0]: https://github.com/soywod/himalaya/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/soywod/himalaya/compare/v0.3.2...v0.4.0 [0.4.0]: https://github.com/soywod/himalaya/compare/v0.3.2...v0.4.0
@ -343,6 +361,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#160]: https://github.com/soywod/himalaya/issues/160 [#160]: https://github.com/soywod/himalaya/issues/160
[#162]: https://github.com/soywod/himalaya/issues/162 [#162]: https://github.com/soywod/himalaya/issues/162
[#176]: https://github.com/soywod/himalaya/issues/176 [#176]: https://github.com/soywod/himalaya/issues/176
[#172]: https://github.com/soywod/himalaya/issues/172
[#181]: https://github.com/soywod/himalaya/issues/181
[#185]: https://github.com/soywod/himalaya/issues/185 [#185]: https://github.com/soywod/himalaya/issues/185
[#186]: https://github.com/soywod/himalaya/issues/186 [#186]: https://github.com/soywod/himalaya/issues/186
[#190]: https://github.com/soywod/himalaya/issues/190 [#190]: https://github.com/soywod/himalaya/issues/190
@ -354,3 +374,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#220]: https://github.com/soywod/himalaya/issues/220 [#220]: https://github.com/soywod/himalaya/issues/220
[#228]: https://github.com/soywod/himalaya/issues/228 [#228]: https://github.com/soywod/himalaya/issues/228
[#229]: https://github.com/soywod/himalaya/issues/229 [#229]: https://github.com/soywod/himalaya/issues/229
[#249]: https://github.com/soywod/himalaya/issues/249
[#268]: https://github.com/soywod/himalaya/issues/268
[#272]: https://github.com/soywod/himalaya/issues/272
[#273]: https://github.com/soywod/himalaya/issues/273
[#276]: https://github.com/soywod/himalaya/issues/276
[#271]: https://github.com/soywod/himalaya/issues/271
[#280]: https://github.com/soywod/himalaya/issues/280

59
Cargo.lock generated
View file

@ -169,9 +169,9 @@ dependencies = [
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.1" version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3"
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@ -179,9 +179,9 @@ dependencies = [
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.2" version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]] [[package]]
name = "dirs-next" name = "dirs-next"
@ -206,9 +206,9 @@ dependencies = [
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.28" version = "0.8.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
] ]
@ -361,7 +361,7 @@ dependencies = [
[[package]] [[package]]
name = "himalaya" name = "himalaya"
version = "0.5.1" version = "0.5.2"
dependencies = [ dependencies = [
"ammonia", "ammonia",
"anyhow", "anyhow",
@ -484,9 +484,9 @@ dependencies = [
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.11" version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
] ]
@ -526,9 +526,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.103" version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" checksum = "a60553f9a9e039a333b4e9b20573b9e9b9c0bb3a11e201ccc48ef4283456d673"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
@ -728,9 +728,9 @@ checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.36" version = "0.10.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a" checksum = "2bc6b9e4403633698352880b22cbe2f0e45dd0177f6fabe4585536e56a3e4f75"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cfg-if 1.0.0", "cfg-if 1.0.0",
@ -748,9 +748,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.67" version = "0.9.68"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69df2d8dfc6ce3aaf44b40dec6f487d5a886516cf6879c49e98e0710f310a058" checksum = "1c571f25d3f66dd427e417cebf73dbe2361d6125cf6e3a70d143fdf97c9f5150"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"cc", "cc",
@ -876,15 +876,15 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.20" version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb" checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.10" version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
[[package]] [[package]]
name = "precomputed-hash" name = "precomputed-hash"
@ -894,9 +894,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.29" version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
dependencies = [ dependencies = [
"unicode-xid", "unicode-xid",
] ]
@ -1188,9 +1188,9 @@ checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.4" version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
@ -1200,12 +1200,13 @@ checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
[[package]] [[package]]
name = "string_cache" name = "string_cache"
version = "0.8.1" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a" checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"new_debug_unreachable", "new_debug_unreachable",
"parking_lot 0.11.2",
"phf_shared", "phf_shared",
"precomputed-hash", "precomputed-hash",
"serde", "serde",
@ -1231,9 +1232,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.80" version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1470,9 +1471,9 @@ checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
[[package]] [[package]]
name = "xml5ever" name = "xml5ever"
version = "0.16.1" version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b1b52e6e8614d4a58b8e70cf51ec0cc21b256ad8206708bcff8139b5bbd6a59" checksum = "9234163818fd8e2418fcde330655e757900d4236acd8cc70fef345ef91f6d865"
dependencies = [ dependencies = [
"log", "log",
"mac", "mac",

View file

@ -1,7 +1,7 @@
[package] [package]
name = "himalaya" name = "himalaya"
description = "CLI email client" description = "Command-line interface for email management"
version = "0.5.1" version = "0.5.2"
authors = ["soywod <clement.douin@posteo.net>"] authors = ["soywod <clement.douin@posteo.net>"]
edition = "2018" edition = "2018"
@ -17,10 +17,10 @@ imap = "3.0.0-alpha.4"
imap-proto = "0.14.3" imap-proto = "0.14.3"
# This commit includes the de/serialization of the ContentType # This commit includes the de/serialization of the ContentType
# lettre = { version = "0.10.0-rc.1", features = ["serde"] } # lettre = { version = "0.10.0-rc.1", features = ["serde"] }
lettre = {git = "https://github.com/TornaxO7/lettre/", branch = "master", features = ["serde"] } lettre = { git = "https://github.com/TornaxO7/lettre/", branch = "master", features = ["serde"] }
log = "0.4.14" log = "0.4.14"
mailparse = "0.13.6" mailparse = "0.13.6"
native-tls = "0.2" native-tls = "0.2.8"
regex = "1.5.4" regex = "1.5.4"
rfc2047-decoder = "0.1.2" rfc2047-decoder = "0.1.2"
serde = { version = "1.0.118", features = ["derive"] } serde = { version = "1.0.118", features = ["derive"] }

View file

@ -4,7 +4,7 @@
[![gh-actions](https://github.com/soywod/himalaya/workflows/nix-build/badge.svg)](https://github.com/soywod/himalaya/actions?query=workflow%3Anix-build) [![gh-actions](https://github.com/soywod/himalaya/workflows/nix-build/badge.svg)](https://github.com/soywod/himalaya/actions?query=workflow%3Anix-build)
[![Homebrew](https://img.shields.io/badge/dynamic/json.svg?url=https://formulae.brew.sh/api/formula/himalaya.json&query=$.versions.stable&label=homebrew)](https://formulae.brew.sh/formula/himalaya) [![Homebrew](https://img.shields.io/badge/dynamic/json.svg?url=https://formulae.brew.sh/api/formula/himalaya.json&query=$.versions.stable&label=homebrew)](https://formulae.brew.sh/formula/himalaya)
CLI email client Command-line interface for email management
*The project is under active development. Do not use in production before the *The project is under active development. Do not use in production before the
`v1.0.0` (see the [roadmap](https://github.com/soywod/himalaya/milestone/5)).* `v1.0.0` (see the [roadmap](https://github.com/soywod/himalaya/milestone/5)).*
@ -26,15 +26,12 @@ Possibilities are endless!
## Installation ## Installation
```sh ```sh
# As root:
curl -sSL https://raw.githubusercontent.com/soywod/himalaya/master/install.sh | sudo sh
# As a regular user:
curl -sSL https://raw.githubusercontent.com/soywod/himalaya/master/install.sh | PREFIX=~/.local sh curl -sSL https://raw.githubusercontent.com/soywod/himalaya/master/install.sh | PREFIX=~/.local sh
``` ```
*See the [wiki](https://github.com/soywod/himalaya/wiki) for other installation *See the
methods.* [wiki](https://github.com/soywod/himalaya/wiki/Installation:from-binary) for
other installation methods.*
## Configuration ## Configuration
@ -44,7 +41,7 @@ methods.*
name = "Your full name" name = "Your full name"
downloads-dir = "/abs/path/to/downloads" downloads-dir = "/abs/path/to/downloads"
signature = """ signature = """
-- Cordialement,
Regards, Regards,
""" """
@ -80,7 +77,8 @@ all the options.*
- JSON output - JSON output
- … - …
*See the [wiki](https://github.com/soywod/himalaya/wiki) for all the features.* *See the [wiki](https://github.com/soywod/himalaya/wiki/Usage:msg:list) for all
the features.*
## Sponsoring ## Sponsoring

View file

@ -3,9 +3,7 @@ Type=Application
Name=himalaya Name=himalaya
DesktopName=Himalaya DesktopName=Himalaya
GenericName=Mail Reader GenericName=Mail Reader
Comment=CLI email client Comment=Command-line interface for email management
Comment[lo]=CLI Rust
Comment[th]=CLI Rust
Terminal=true Terminal=true
Exec=himalaya %U Exec=himalaya %U
Categories=Application;Network Categories=Application;Network

View file

@ -1,22 +1,5 @@
{ {
"nodes": { "nodes": {
"crate2nix": {
"flake": false,
"locked": {
"lastModified": 1608814925,
"narHash": "sha256-GdFBG2LmpbY4C1OJBFfWLMKXzGyFq4mJBK+SVMNNE+8=",
"owner": "balsoft",
"repo": "crate2nix",
"rev": "68be3d90f31bf0bfd525da77e0ae6e89f48abd24",
"type": "github"
},
"original": {
"owner": "balsoft",
"ref": "tools-nix-version-comparison",
"repo": "crate2nix",
"type": "github"
}
},
"flake-compat": { "flake-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
@ -35,11 +18,11 @@
}, },
"flake-utils": { "flake-utils": {
"locked": { "locked": {
"lastModified": 1614513358, "lastModified": 1637014545,
"narHash": "sha256-LakhOx3S1dRjnh0b5Dg3mbZyH0ToC9I8Y2wKSkBaTzU=", "narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "5466c5bbece17adaab2d82fae80b46e807611bf3", "rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -48,42 +31,73 @@
"type": "github" "type": "github"
} }
}, },
"naersk": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1639947939,
"narHash": "sha256-pGsM8haJadVP80GFq4xhnSpNitYNQpaXk4cnA796Cso=",
"owner": "nix-community",
"repo": "naersk",
"rev": "2fc8ce9d3c025d59fee349c1f80be9785049d653",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1627857416, "lastModified": 1640418986,
"narHash": "sha256-AV0MsFVzbWI2MZbJ2j0kc8ooFLGSCZHuM9ipaWR9ds4=", "narHash": "sha256-a8GGtxn2iL3WAkY5H+4E0s3Q7XJt6bTOvos9qqxT5OQ=",
"owner": "nixos", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "aaf9676fbb7fb4570216ca1e189a3dc769d62c45", "rev": "5c37ad87222cfc1ec36d6cd1364514a9efc2f7f2",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nixos", "id": "nixpkgs",
"ref": "nixos-unstable", "type": "indirect"
"repo": "nixpkgs",
"type": "github"
} }
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1617325113, "lastModified": 1640418986,
"narHash": "sha256-GksR0nvGxfZ79T91UUtWjjccxazv6Yh/MvEJ82v1Xmw=", "narHash": "sha256-a8GGtxn2iL3WAkY5H+4E0s3Q7XJt6bTOvos9qqxT5OQ=",
"owner": "nixos", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "54c1e44240d8a527a8f4892608c4bce5440c3ecb", "rev": "5c37ad87222cfc1ec36d6cd1364514a9efc2f7f2",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1637453606,
"narHash": "sha256-Gy6cwUswft9xqsjWxFYEnx/63/qzaFUwatcbV5GF/GQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8afc4e543663ca0a6a4f496262cd05233737e732",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
}, },
"root": { "root": {
"inputs": { "inputs": {
"crate2nix": "crate2nix",
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
"nixpkgs": "nixpkgs", "naersk": "naersk",
"nixpkgs": "nixpkgs_2",
"rust-overlay": "rust-overlay", "rust-overlay": "rust-overlay",
"utils": "utils" "utils": "utils"
} }
@ -91,14 +105,14 @@
"rust-overlay": { "rust-overlay": {
"inputs": { "inputs": {
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_3"
}, },
"locked": { "locked": {
"lastModified": 1627957145, "lastModified": 1642838864,
"narHash": "sha256-cY5lS2S/RMsC1xFtkcmhLXlVP7ahZoxFeKedkXDvIzY=", "narHash": "sha256-pHnhm3HWwtvtOK7NdNHwERih3PgNlacrfeDwachIG8E=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "ab6f3086de97980e4fdcb0560921852a407e0b79", "rev": "9fb49daf1bbe1d91e6c837706c481f9ebb3d8097",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -1,27 +1,21 @@
{ {
description = "CLI email client"; description = "Command-line interface for email management";
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
utils.url = "github:numtide/flake-utils"; utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay"; rust-overlay.url = "github:oxalica/rust-overlay";
crate2nix = { naersk.url = "github:nix-community/naersk";
url = "github:balsoft/crate2nix/tools-nix-version-comparison";
flake = false;
};
flake-compat = { flake-compat = {
url = "github:edolstra/flake-compat"; url = "github:edolstra/flake-compat";
flake = false; flake = false;
}; };
}; };
outputs = { self, nixpkgs, utils, rust-overlay, crate2nix, ... }: outputs = { self, nixpkgs, utils, rust-overlay, naersk, ... }:
utils.lib.eachDefaultSystem utils.lib.eachDefaultSystem
(system: (system:
let let
name = "himalaya"; name = "himalaya";
# Imports
pkgs = import nixpkgs { pkgs = import nixpkgs {
inherit system; inherit system;
overlays = [ overlays = [
@ -35,37 +29,23 @@
}) })
]; ];
}; };
inherit (import "${crate2nix}/tools.nix" { inherit pkgs; }) naersk-lib = naersk.lib.${system};
generatedCargoNix; in
rec {
# Create the cargo2nix project # nix build
project = pkgs.callPackage (generatedCargoNix { defaultPackage = packages.${name};
inherit name; packages = {
src = ./.; ${name} = naersk-lib.buildPackage {
}) { pname = name;
# Individual crate overrides go here root = ./.;
# Example: https://github.com/balsoft/simple-osd-daemons/blob/6f85144934c0c1382c7a4d3a2bbb80106776e270/flake.nix#L28-L50 nativeBuildInputs = with pkgs; [ openssl.dev pkgconfig ];
defaultCrateOverrides = pkgs.defaultCrateOverrides // { overrideMain = _: {
# The himalaya crate itself is overriden here. Typically we
# configure non-Rust dependencies (see below) here.
${name} = oldAttrs: {
inherit buildInputs nativeBuildInputs;
postInstall = '' postInstall = ''
mkdir -p $out/share/applications/ mkdir -p $out/share/applications/
cp assets/himalaya.desktop $out/share/applications/ cp assets/himalaya.desktop $out/share/applications/
''; '';
}; };
}; };
};
# Configuration for the non-Rust dependencies
buildInputs = with pkgs; [ openssl.dev ];
nativeBuildInputs = with pkgs; [ rustc cargo pkgconfig ];
in
rec {
packages = {
${name} = project.rootCrate.build;
"${name}-vim" = pkgs.vimUtils.buildVimPluginFrom2Nix { "${name}-vim" = pkgs.vimUtils.buildVimPluginFrom2Nix {
inherit (packages.${name}) version; inherit (packages.${name}) version;
name = "${name}-vim"; name = "${name}-vim";
@ -80,21 +60,27 @@
}; };
}; };
# `nix build` # nix run
defaultPackage = packages.${name}; defaultApp = apps.${name};
# `nix run`
apps.${name} = utils.lib.mkApp { apps.${name} = utils.lib.mkApp {
inherit name; inherit name;
drv = packages.${name}; drv = packages.${name};
}; };
defaultApp = apps.${name};
# `nix develop` # nix develop
devShell = pkgs.mkShell { devShell = pkgs.mkShell {
inputsFrom = builtins.attrValues self.packages.${system};
buildInputs = with pkgs; [ cargo cargo-watch trunk ];
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
inputsFrom = builtins.attrValues self.packages.${system};
buildInputs = with pkgs; [
cargo
cargo-watch
trunk
ripgrep
rust-analyzer
rustfmt
rnix-lsp
nixpkgs-fmt
];
}; };
} }
); );

View file

@ -8,6 +8,10 @@ use crate::{
output::run_cmd, output::run_cmd,
}; };
pub const DEFAULT_INBOX_FOLDER: &str = "INBOX";
pub const DEFAULT_SENT_FOLDER: &str = "Sent";
pub const DEFAULT_DRAFT_FOLDER: &str = "Drafts";
/// Represent a user account. /// Represent a user account.
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Account { pub struct Account {
@ -16,6 +20,12 @@ pub struct Account {
pub downloads_dir: PathBuf, pub downloads_dir: PathBuf,
pub sig: Option<String>, pub sig: Option<String>,
pub default_page_size: usize, pub default_page_size: usize,
/// Defines the inbox folder name for this account
pub inbox_folder: String,
/// Defines the sent folder name for this account
pub sent_folder: String,
/// Defines the draft folder name for this account
pub draft_folder: String,
pub watch_cmds: Vec<String>, pub watch_cmds: Vec<String>,
pub default: bool, pub default: bool,
pub email: String, pub email: String,
@ -41,7 +51,7 @@ impl Account {
let has_special_chars = "()<>[]:;@.,".contains(|special_char| name.contains(special_char)); let has_special_chars = "()<>[]:;@.,".contains(|special_char| name.contains(special_char));
if name.is_empty() { if name.is_empty() {
format!("{}", self.email) self.email.clone()
} else if has_special_chars { } else if has_special_chars {
// so the name has special characters => Wrap it with '"' // so the name has special characters => Wrap it with '"'
format!("\"{}\" <{}>", name, self.email) format!("\"{}\" <{}>", name, self.email)
@ -102,7 +112,7 @@ impl<'a> TryFrom<(&'a Config, Option<&str>)> for Account {
.and_then(|dir| shellexpand::full(dir).ok()) .and_then(|dir| shellexpand::full(dir).ok())
.map(|dir| PathBuf::from(dir.to_string())) .map(|dir| PathBuf::from(dir.to_string()))
}) })
.unwrap_or_else(|| env::temp_dir()); .unwrap_or_else(env::temp_dir);
let default_page_size = account let default_page_size = account
.default_page_size .default_page_size
@ -134,6 +144,24 @@ impl<'a> TryFrom<(&'a Config, Option<&str>)> for Account {
downloads_dir, downloads_dir,
sig, sig,
default_page_size, default_page_size,
inbox_folder: account
.inbox_folder
.as_deref()
.or_else(|| config.inbox_folder.as_deref())
.unwrap_or(DEFAULT_INBOX_FOLDER)
.to_string(),
sent_folder: account
.sent_folder
.as_deref()
.or_else(|| config.sent_folder.as_deref())
.unwrap_or(DEFAULT_SENT_FOLDER)
.to_string(),
draft_folder: account
.draft_folder
.as_deref()
.or_else(|| config.draft_folder.as_deref())
.unwrap_or(DEFAULT_DRAFT_FOLDER)
.to_string(),
watch_cmds: account watch_cmds: account
.watch_cmds .watch_cmds
.as_ref() .as_ref()
@ -142,12 +170,14 @@ impl<'a> TryFrom<(&'a Config, Option<&str>)> for Account {
.to_owned(), .to_owned(),
default: account.default.unwrap_or(false), default: account.default.unwrap_or(false),
email: account.email.to_owned(), email: account.email.to_owned(),
imap_host: account.imap_host.to_owned(), imap_host: account.imap_host.to_owned(),
imap_port: account.imap_port, imap_port: account.imap_port,
imap_starttls: account.imap_starttls.unwrap_or_default(), imap_starttls: account.imap_starttls.unwrap_or_default(),
imap_insecure: account.imap_insecure.unwrap_or_default(), imap_insecure: account.imap_insecure.unwrap_or_default(),
imap_login: account.imap_login.to_owned(), imap_login: account.imap_login.to_owned(),
imap_passwd_cmd: account.imap_passwd_cmd.to_owned(), imap_passwd_cmd: account.imap_passwd_cmd.to_owned(),
smtp_host: account.smtp_host.to_owned(), smtp_host: account.smtp_host.to_owned(),
smtp_port: account.smtp_port, smtp_port: account.smtp_port,
smtp_starttls: account.smtp_starttls.unwrap_or_default(), smtp_starttls: account.smtp_starttls.unwrap_or_default(),

View file

@ -1,7 +1,7 @@
use anyhow::{Context, Error, Result}; use anyhow::{Context, Error, Result};
use log::{debug, trace}; use log::{debug, trace};
use serde::Deserialize; use serde::Deserialize;
use std::{collections::HashMap, convert::TryFrom, env, fs, path::PathBuf, thread}; use std::{collections::HashMap, convert::TryFrom, env, fs, path::PathBuf};
use toml; use toml;
use crate::output::run_cmd; use crate::output::run_cmd;
@ -13,18 +13,27 @@ pub const DEFAULT_SIG_DELIM: &str = "-- \n";
#[derive(Debug, Default, Clone, Deserialize)] #[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct Config { pub struct Config {
/// Define the full display name of the user. /// Defines the full display name of the user.
pub name: String, pub name: String,
/// Define the downloads directory (eg. for attachments). /// Defines the downloads directory (eg. for attachments).
pub downloads_dir: Option<PathBuf>, pub downloads_dir: Option<PathBuf>,
/// Override the default signature delimiter "`--\n `". /// Overrides the default signature delimiter "`--\n `".
pub signature_delimiter: Option<String>, pub signature_delimiter: Option<String>,
/// Define the signature. /// Defines the signature.
pub signature: Option<String>, pub signature: Option<String>,
/// Define the default page size for listings. /// Defines the default page size for listings.
pub default_page_size: Option<usize>, pub default_page_size: Option<usize>,
/// Defines the inbox folder name.
pub inbox_folder: Option<String>,
/// Defines the sent folder name.
pub sent_folder: Option<String>,
/// Defines the draft folder name.
pub draft_folder: Option<String>,
/// Defines the notify command.
pub notify_cmd: Option<String>, pub notify_cmd: Option<String>,
/// Defines the watch commands.
pub watch_cmds: Option<Vec<String>>, pub watch_cmds: Option<Vec<String>>,
#[serde(flatten)] #[serde(flatten)]
pub accounts: ConfigAccountsMap, pub accounts: ConfigAccountsMap,
} }
@ -41,15 +50,23 @@ pub struct ConfigAccountEntry {
pub signature_delimiter: Option<String>, pub signature_delimiter: Option<String>,
pub signature: Option<String>, pub signature: Option<String>,
pub default_page_size: Option<usize>, pub default_page_size: Option<usize>,
/// Defines a specific inbox folder name for this account.
pub inbox_folder: Option<String>,
/// Defines a specific sent folder name for this account.
pub sent_folder: Option<String>,
/// Defines a specific draft folder name for this account.
pub draft_folder: Option<String>,
pub watch_cmds: Option<Vec<String>>, pub watch_cmds: Option<Vec<String>>,
pub default: Option<bool>, pub default: Option<bool>,
pub email: String, pub email: String,
pub imap_host: String, pub imap_host: String,
pub imap_port: u16, pub imap_port: u16,
pub imap_starttls: Option<bool>, pub imap_starttls: Option<bool>,
pub imap_insecure: Option<bool>, pub imap_insecure: Option<bool>,
pub imap_login: String, pub imap_login: String,
pub imap_passwd_cmd: String, pub imap_passwd_cmd: String,
pub smtp_host: String, pub smtp_host: String,
pub smtp_port: u16, pub smtp_port: u16,
pub smtp_starttls: Option<bool>, pub smtp_starttls: Option<bool>,
@ -118,28 +135,8 @@ impl Config {
.map(|cmd| format!(r#"{} {:?} {:?}"#, cmd, subject, sender)) .map(|cmd| format!(r#"{} {:?} {:?}"#, cmd, subject, sender))
.unwrap_or(default_cmd); .unwrap_or(default_cmd);
debug!("run command: {}", cmd);
run_cmd(&cmd).context("cannot run notify cmd")?; run_cmd(&cmd).context("cannot run notify cmd")?;
Ok(())
}
pub fn _exec_watch_cmds(&self, account: &ConfigAccountEntry) -> Result<()> {
let cmds = account
.watch_cmds
.as_ref()
.or_else(|| self.watch_cmds.as_ref())
.map(|cmds| cmds.to_owned())
.unwrap_or_default();
thread::spawn(move || {
debug!("batch execution of {} cmd(s)", cmds.len());
cmds.iter().for_each(|cmd| {
debug!("running command {:?}…", cmd);
let res = run_cmd(cmd);
debug!("{:?}", res);
})
});
Ok(()) Ok(())
} }
} }

View file

@ -4,7 +4,10 @@
use anyhow::Result; use anyhow::Result;
use crate::{config::Config, domain::imap::ImapServiceInterface}; use crate::{
config::{Account, Config},
domain::imap::ImapServiceInterface,
};
/// Notify handler. /// Notify handler.
pub fn notify<'a, ImapService: ImapServiceInterface<'a>>( pub fn notify<'a, ImapService: ImapServiceInterface<'a>>(
@ -12,13 +15,14 @@ pub fn notify<'a, ImapService: ImapServiceInterface<'a>>(
config: &Config, config: &Config,
imap: &mut ImapService, imap: &mut ImapService,
) -> Result<()> { ) -> Result<()> {
imap.notify(&config, keepalive) imap.notify(config, keepalive)
} }
/// Watch handler. /// Watch handler.
pub fn watch<'a, ImapService: ImapServiceInterface<'a>>( pub fn watch<'a, ImapService: ImapServiceInterface<'a>>(
keepalive: u64, keepalive: u64,
account: &Account,
imap: &mut ImapService, imap: &mut ImapService,
) -> Result<()> { ) -> Result<()> {
imap.watch(keepalive) imap.watch(account, keepalive)
} }

View file

@ -8,20 +8,21 @@ use native_tls::{TlsConnector, TlsStream};
use std::{ use std::{
collections::HashSet, collections::HashSet,
convert::{TryFrom, TryInto}, convert::{TryFrom, TryInto},
iter::FromIterator,
net::TcpStream, net::TcpStream,
thread,
}; };
use crate::{ use crate::{
config::{Account, Config}, config::{Account, Config},
domain::{Envelope, Envelopes, Flags, Mbox, Mboxes, Msg, RawEnvelopes, RawMboxes}, domain::{Envelope, Envelopes, Flags, Mbox, Mboxes, Msg, RawEnvelopes, RawMboxes},
output::run_cmd,
}; };
type ImapSession = imap::Session<TlsStream<TcpStream>>; type ImapSession = imap::Session<TlsStream<TcpStream>>;
pub trait ImapServiceInterface<'a> { pub trait ImapServiceInterface<'a> {
fn notify(&mut self, config: &Config, keepalive: u64) -> Result<()>; fn notify(&mut self, config: &Config, keepalive: u64) -> Result<()>;
fn watch(&mut self, keepalive: u64) -> Result<()>; fn watch(&mut self, account: &Account, keepalive: u64) -> Result<()>;
fn fetch_mboxes(&'a mut self) -> Result<Mboxes>; fn fetch_mboxes(&'a mut self) -> Result<Mboxes>;
fn fetch_envelopes(&mut self, page_size: &usize, page: &usize) -> Result<Envelopes>; fn fetch_envelopes(&mut self, page_size: &usize, page: &usize) -> Result<Envelopes>;
fn fetch_envelopes_with( fn fetch_envelopes_with(
@ -58,7 +59,7 @@ pub struct ImapService<'a> {
impl<'a> ImapService<'a> { impl<'a> ImapService<'a> {
fn sess(&mut self) -> Result<&mut ImapSession> { fn sess(&mut self) -> Result<&mut ImapSession> {
if let None = self.sess { if self.sess.is_none() {
debug!("create TLS builder"); debug!("create TLS builder");
debug!("insecure: {}", self.account.imap_insecure); debug!("insecure: {}", self.account.imap_insecure);
let builder = TlsConnector::builder() let builder = TlsConnector::builder()
@ -122,12 +123,17 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
} }
fn fetch_envelopes(&mut self, page_size: &usize, page: &usize) -> Result<Envelopes> { fn fetch_envelopes(&mut self, page_size: &usize, page: &usize) -> Result<Envelopes> {
debug!("fetch envelopes");
debug!("page size: {:?}", page_size);
debug!("page: {:?}", page);
let mbox = self.mbox.to_owned(); let mbox = self.mbox.to_owned();
let last_seq = self let last_seq = self
.sess()? .sess()?
.select(&mbox.name) .select(&mbox.name)
.context(format!(r#"cannot select mailbox "{}""#, self.mbox.name))? .context(format!(r#"cannot select mailbox "{}""#, self.mbox.name))?
.exists as i64; .exists as i64;
debug!("last sequence number: {:?}", last_seq);
if last_seq == 0 { if last_seq == 0 {
return Ok(Envelopes::default()); return Ok(Envelopes::default());
@ -142,13 +148,14 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
} else { } else {
String::from("1:*") String::from("1:*")
}; };
debug!("range: {:?}", range);
let fetches = self let fetches = self
.sess()? .sess()?
.fetch(range, "(ENVELOPE FLAGS INTERNALDATE)") .fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)")
.context(r#"cannot fetch messages within range "{}""#)?; .context(format!(r#"cannot fetch messages within range "{}""#, range))?;
self._raw_msgs_cache = Some(fetches); self._raw_msgs_cache = Some(fetches);
Ok(Envelopes::try_from(self._raw_msgs_cache.as_ref().unwrap())?) Envelopes::try_from(self._raw_msgs_cache.as_ref().unwrap())
} }
fn fetch_envelopes_with( fn fetch_envelopes_with(
@ -186,7 +193,7 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
.fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)") .fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)")
.context(r#"cannot fetch messages within range "{}""#)?; .context(r#"cannot fetch messages within range "{}""#)?;
self._raw_msgs_cache = Some(fetches); self._raw_msgs_cache = Some(fetches);
Ok(Envelopes::try_from(self._raw_msgs_cache.as_ref().unwrap())?) Envelopes::try_from(self._raw_msgs_cache.as_ref().unwrap())
} }
/// Find a message by sequence number. /// Find a message by sequence number.
@ -201,9 +208,9 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
.context(r#"cannot fetch messages "{}""#)?; .context(r#"cannot fetch messages "{}""#)?;
let fetch = fetches let fetch = fetches
.first() .first()
.ok_or(anyhow!(r#"cannot find message "{}"#, seq))?; .ok_or_else(|| anyhow!(r#"cannot find message "{}"#, seq))?;
Ok(Msg::try_from(fetch)?) Msg::try_from(fetch)
} }
fn find_raw_msg(&mut self, seq: &str) -> Result<Vec<u8>> { fn find_raw_msg(&mut self, seq: &str) -> Result<Vec<u8>> {
@ -217,14 +224,14 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
.context(r#"cannot fetch raw messages "{}""#)?; .context(r#"cannot fetch raw messages "{}""#)?;
let fetch = fetches let fetch = fetches
.first() .first()
.ok_or(anyhow!(r#"cannot find raw message "{}"#, seq))?; .ok_or_else(|| anyhow!(r#"cannot find raw message "{}"#, seq))?;
Ok(fetch.body().map(Vec::from).unwrap_or_default()) Ok(fetch.body().map(Vec::from).unwrap_or_default())
} }
fn append_raw_msg_with_flags(&mut self, mbox: &Mbox, msg: &[u8], flags: Flags) -> Result<()> { fn append_raw_msg_with_flags(&mut self, mbox: &Mbox, msg: &[u8], flags: Flags) -> Result<()> {
self.sess()? self.sess()?
.append(&mbox.name, &msg) .append(&mbox.name, msg)
.flags(flags.0) .flags(flags.0)
.finish() .finish()
.context(format!(r#"cannot append message to "{}""#, mbox.name))?; .context(format!(r#"cannot append message to "{}""#, mbox.name))?;
@ -242,16 +249,21 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
} }
fn notify(&mut self, config: &Config, keepalive: u64) -> Result<()> { fn notify(&mut self, config: &Config, keepalive: u64) -> Result<()> {
debug!("notify");
let mbox = self.mbox.to_owned(); let mbox = self.mbox.to_owned();
debug!("examine mailbox: {}", mbox.name); debug!("examine mailbox {:?}", mbox);
self.sess()? self.sess()?
.examine(&mbox.name) .examine(&mbox.name)
.context(format!("cannot examine mailbox `{}`", &self.mbox.name))?; .context(format!("cannot examine mailbox {}", self.mbox.name))?;
debug!("init messages hashset"); debug!("init messages hashset");
let mut msgs_set: HashSet<u32> = let mut msgs_set: HashSet<u32> = self
HashSet::from_iter(self.search_new_msgs()?.iter().cloned()); .search_new_msgs()?
.iter()
.cloned()
.collect::<HashSet<_>>();
trace!("messages hashset: {:?}", msgs_set); trace!("messages hashset: {:?}", msgs_set);
loop { loop {
@ -271,7 +283,7 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
let uids: Vec<u32> = self let uids: Vec<u32> = self
.search_new_msgs()? .search_new_msgs()?
.into_iter() .into_iter()
.filter(|uid| msgs_set.get(&uid).is_none()) .filter(|uid| -> bool { msgs_set.get(uid).is_none() })
.collect(); .collect();
debug!("found {} new messages not in hashset", uids.len()); debug!("found {} new messages not in hashset", uids.len());
trace!("messages hashet: {:?}", msgs_set); trace!("messages hashet: {:?}", msgs_set);
@ -309,7 +321,7 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
} }
} }
fn watch(&mut self, keepalive: u64) -> Result<()> { fn watch(&mut self, account: &Account, keepalive: u64) -> Result<()> {
debug!("examine mailbox: {}", &self.mbox.name); debug!("examine mailbox: {}", &self.mbox.name);
let mbox = self.mbox.to_owned(); let mbox = self.mbox.to_owned();
@ -330,8 +342,17 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> {
}) })
}) })
.context("cannot start the idle mode")?; .context("cannot start the idle mode")?;
// FIXME
// ctx.config.exec_watch_cmds(&ctx.account)?; let cmds = account.watch_cmds.clone();
thread::spawn(move || {
debug!("batch execution of {} cmd(s)", cmds.len());
cmds.iter().for_each(|cmd| {
debug!("running command {:?}…", cmd);
let res = run_cmd(cmd);
debug!("{:?}", res);
})
});
debug!("end loop"); debug!("end loop");
} }
} }

View file

@ -47,13 +47,12 @@ pub fn source_arg<'a>() -> clap::Arg<'a, 'a> {
.long("mailbox") .long("mailbox")
.help("Specifies the source mailbox") .help("Specifies the source mailbox")
.value_name("SOURCE") .value_name("SOURCE")
.default_value("INBOX")
} }
/// Defines the target mailbox argument. /// Defines the target mailbox argument.
pub fn target_arg<'a>() -> clap::Arg<'a, 'a> { pub fn target_arg<'a>() -> clap::Arg<'a, 'a> {
clap::Arg::with_name("mbox-target") clap::Arg::with_name("mbox-target")
.help("Specifies the targetted mailbox") .help("Specifies the targeted mailbox")
.value_name("TARGET") .value_name("TARGET")
.required(true) .required(true)
} }
@ -104,7 +103,7 @@ mod tests {
} }
let app = get_matches_from![]; let app = get_matches_from![];
assert_eq!(Some("INBOX"), app.value_of("mbox-source")); assert_eq!(None, app.value_of("mbox-source"));
let app = get_matches_from!["-m", "SOURCE"]; let app = get_matches_from!["-m", "SOURCE"];
assert_eq!(Some("SOURCE"), app.value_of("mbox-source")); assert_eq!(Some("SOURCE"), app.value_of("mbox-source"));

View file

@ -28,7 +28,7 @@ mod tests {
use termcolor::ColorSpec; use termcolor::ColorSpec;
use crate::{ use crate::{
config::Config, config::{Account, Config},
domain::{AttrRemote, Attrs, Envelopes, Flags, Mbox, Mboxes, Msg}, domain::{AttrRemote, Attrs, Envelopes, Flags, Mbox, Mboxes, Msg},
output::{Print, PrintTable, WriteColor}, output::{Print, PrintTable, WriteColor},
}; };
@ -117,7 +117,7 @@ mod tests {
fn notify(&mut self, _: &Config, _: u64) -> Result<()> { fn notify(&mut self, _: &Config, _: u64) -> Result<()> {
unimplemented!() unimplemented!()
} }
fn watch(&mut self, _: u64) -> Result<()> { fn watch(&mut self, _: &Account, _: u64) -> Result<()> {
unimplemented!() unimplemented!()
} }
fn fetch_envelopes(&mut self, _: &usize, _: &usize) -> Result<Envelopes> { fn fetch_envelopes(&mut self, _: &usize, _: &usize) -> Result<Envelopes> {

View file

@ -32,7 +32,7 @@ impl<'a> Deref for Mboxes<'a> {
impl<'a> PrintTable for Mboxes<'a> { impl<'a> PrintTable for Mboxes<'a> {
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writter)?; writeln!(writter)?;
Table::print(writter, &self, opts)?; Table::print(writter, self, opts)?;
writeln!(writter)?; writeln!(writter)?;
Ok(()) Ok(())
} }

View file

@ -39,7 +39,7 @@ impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> {
fn try_from(fetch: &'a RawEnvelope) -> Result<Envelope> { fn try_from(fetch: &'a RawEnvelope) -> Result<Envelope> {
let envelope = fetch let envelope = fetch
.envelope() .envelope()
.ok_or(anyhow!("cannot get envelope of message {}", fetch.message))?; .ok_or_else(|| anyhow!("cannot get envelope of message {}", fetch.message))?;
// Get the sequence number // Get the sequence number
let id = fetch.message; let id = fetch.message;
@ -57,7 +57,7 @@ impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> {
fetch.message fetch.message
)) ))
}) })
.unwrap_or(Ok(String::default()))? .unwrap_or_else(|| Ok(String::default()))?
.into(); .into();
// Get the sender // Get the sender
@ -66,7 +66,7 @@ impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> {
.as_ref() .as_ref()
.and_then(|addrs| addrs.get(0)) .and_then(|addrs| addrs.get(0))
.or_else(|| envelope.from.as_ref().and_then(|addrs| addrs.get(0))) .or_else(|| envelope.from.as_ref().and_then(|addrs| addrs.get(0)))
.ok_or(anyhow!("cannot get sender of message {}", fetch.message))?; .ok_or_else(|| anyhow!("cannot get sender of message {}", fetch.message))?;
let sender = if let Some(ref name) = sender.name { let sender = if let Some(ref name) = sender.name {
rfc2047_decoder::decode(&name.to_vec()).context(format!( rfc2047_decoder::decode(&name.to_vec()).context(format!(
"cannot decode sender's name of message {}", "cannot decode sender's name of message {}",
@ -76,10 +76,7 @@ impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> {
let mbox = sender let mbox = sender
.mailbox .mailbox
.as_ref() .as_ref()
.ok_or(anyhow!( .ok_or_else(|| anyhow!("cannot get sender's mailbox of message {}", fetch.message))
"cannot get sender's mailbox of message {}",
fetch.message
))
.and_then(|mbox| { .and_then(|mbox| {
rfc2047_decoder::decode(&mbox.to_vec()).context(format!( rfc2047_decoder::decode(&mbox.to_vec()).context(format!(
"cannot decode sender's mailbox of message {}", "cannot decode sender's mailbox of message {}",
@ -89,10 +86,7 @@ impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> {
let host = sender let host = sender
.host .host
.as_ref() .as_ref()
.ok_or(anyhow!( .ok_or_else(|| anyhow!("cannot get sender's host of message {}", fetch.message))
"cannot get sender's host of message {}",
fetch.message
))
.and_then(|host| { .and_then(|host| {
rfc2047_decoder::decode(&host.to_vec()).context(format!( rfc2047_decoder::decode(&host.to_vec()).context(format!(
"cannot decode sender's host of message {}", "cannot decode sender's host of message {}",
@ -133,11 +127,7 @@ impl<'a> Table for Envelope<'a> {
let unseen = !self.flags.contains(&Flag::Seen); let unseen = !self.flags.contains(&Flag::Seen);
let subject = &self.subject; let subject = &self.subject;
let sender = &self.sender; let sender = &self.sender;
let date = self let date = self.date.as_deref().unwrap_or_default();
.date
.as_ref()
.map(|date| date.as_str())
.unwrap_or_default();
Row::new() Row::new()
.cell(Cell::new(id).bold_if(unseen).red()) .cell(Cell::new(id).bold_if(unseen).red())
.cell(Cell::new(flags).bold_if(unseen).white()) .cell(Cell::new(flags).bold_if(unseen).white())

View file

@ -39,7 +39,7 @@ impl<'a> TryFrom<&'a RawEnvelopes> for Envelopes<'a> {
impl<'a> PrintTable for Envelopes<'a> { impl<'a> PrintTable for Envelopes<'a> {
fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writter)?; writeln!(writter)?;
Table::print(writter, &self, opts)?; Table::print(writter, self, opts)?;
writeln!(writter)?; writeln!(writter)?;
Ok(()) Ok(())
} }

View file

@ -126,16 +126,14 @@ impl<'a> From<Vec<&'a str>> for Flags {
let mut map: HashSet<Flag<'static>> = HashSet::new(); let mut map: HashSet<Flag<'static>> = HashSet::new();
for f in flags { for f in flags {
match f { match f.to_lowercase().as_str() {
"Answered" | _ if f.eq_ignore_ascii_case("answered") => map.insert(Flag::Answered), "answered" => map.insert(Flag::Answered),
"Deleted" | _ if f.eq_ignore_ascii_case("deleted") => map.insert(Flag::Deleted), "deleted" => map.insert(Flag::Deleted),
"Draft" | _ if f.eq_ignore_ascii_case("draft") => map.insert(Flag::Draft), "draft" => map.insert(Flag::Draft),
"Flagged" | _ if f.eq_ignore_ascii_case("flagged") => map.insert(Flag::Flagged), "flagged" => map.insert(Flag::Flagged),
"MayCreate" | _ if f.eq_ignore_ascii_case("maycreate") => { "maycreate" => map.insert(Flag::MayCreate),
map.insert(Flag::MayCreate) "recent" => map.insert(Flag::Recent),
} "seen" => map.insert(Flag::Seen),
"Recent" | _ if f.eq_ignore_ascii_case("recent") => map.insert(Flag::Recent),
"Seen" | _ if f.eq_ignore_ascii_case("seen") => map.insert(Flag::Seen),
custom => map.insert(Flag::Custom(Cow::Owned(custom.into()))), custom => map.insert(Flag::Custom(Cow::Owned(custom.into()))),
}; };
} }

View file

@ -200,11 +200,11 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
} }
if let Some(m) = m.subcommand_matches("template") { if let Some(m) = m.subcommand_matches("template") {
return Ok(Some(Command::Tpl(tpl_arg::matches(&m)?))); return Ok(Some(Command::Tpl(tpl_arg::matches(m)?)));
} }
if let Some(m) = m.subcommand_matches("flag") { if let Some(m) = m.subcommand_matches("flag") {
return Ok(Some(Command::Flag(flag_arg::matches(&m)?))); return Ok(Some(Command::Flag(flag_arg::matches(m)?)));
} }
debug!("default list command matched"); debug!("default list command matched");

View file

@ -100,21 +100,43 @@ impl Msg {
.tags(HashSet::default()) .tags(HashSet::default())
.clean(&html) .clean(&html)
.to_string(); .to_string();
// Replace `&nbsp;` by regular space
let sanitized_html = Regex::new(r"&nbsp;")
.unwrap()
.replace_all(&sanitized_html, " ")
.to_string();
// Merge new line chars // Merge new line chars
let sanitized_html = Regex::new(r"(\r?\n ?){2,}") let sanitized_html = Regex::new(r"(\r?\n\s*){2,}")
.unwrap() .unwrap()
.replace_all(&sanitized_html, "\n\n") .replace_all(&sanitized_html, "\n\n")
.to_string(); .to_string();
// Replace tabulations and &npsp; by spaces
let sanitized_html = Regex::new(r"(\t|&nbsp;)")
.unwrap()
.replace_all(&sanitized_html, " ")
.to_string();
// Merge spaces
let sanitized_html = Regex::new(r" {2,}")
.unwrap()
.replace_all(&sanitized_html, " ")
.to_string();
// Decode HTML entities // Decode HTML entities
let sanitized_html = html_escape::decode_html_entities(&sanitized_html).to_string(); let sanitized_html = html_escape::decode_html_entities(&sanitized_html).to_string();
sanitized_html sanitized_html
} else { } else {
plain // Merge new line chars
let sanitized_plain = Regex::new(r"(\r?\n\s*){2,}")
.unwrap()
.replace_all(&plain, "\n\n")
.to_string();
// Replace tabulations by spaces
let sanitized_plain = Regex::new(r"\t")
.unwrap()
.replace_all(&sanitized_plain, " ")
.to_string();
// Merge spaces
let sanitized_plain = Regex::new(r" {2,}")
.unwrap()
.replace_all(&sanitized_plain, " ")
.to_string();
sanitized_plain
} }
} }
@ -194,14 +216,18 @@ impl Msg {
.date .date
.as_ref() .as_ref()
.map(|date| date.format("%d %b %Y, at %H:%M").to_string()) .map(|date| date.format("%d %b %Y, at %H:%M").to_string())
.unwrap_or("unknown date".into()); .unwrap_or_else(|| "unknown date".into());
let sender = self let sender = self
.reply_to .reply_to
.as_ref() .as_ref()
.or(self.from.as_ref()) .or_else(|| self.from.as_ref())
.and_then(|addrs| addrs.first()) .and_then(|addrs| addrs.first())
.map(|addr| addr.name.to_owned().unwrap_or(addr.email.to_string())) .map(|addr| {
.unwrap_or("unknown sender".into()); addr.name
.to_owned()
.unwrap_or_else(|| addr.email.to_string())
})
.unwrap_or_else(|| "unknown sender".into());
let mut content = format!("\n\nOn {}, {} wrote:\n", date, sender); let mut content = format!("\n\nOn {}, {} wrote:\n", date, sender);
let mut glue = ""; let mut glue = "";
@ -210,8 +236,8 @@ impl Msg {
break; break;
} }
content.push_str(glue); content.push_str(glue);
content.push_str(">"); content.push('>');
content.push_str(if line.starts_with(">") { "" } else { " " }); content.push_str(if line.starts_with('>') { "" } else { " " });
content.push_str(line); content.push_str(line);
glue = "\n"; glue = "\n";
} }
@ -239,7 +265,7 @@ impl Msg {
self.in_reply_to = None; self.in_reply_to = None;
// From // From
self.from = Some(vec![account_addr.to_owned()]); self.from = Some(vec![account_addr]);
// To // To
self.to = Some(vec![]); self.to = Some(vec![]);
@ -270,7 +296,7 @@ impl Msg {
content.push_str(&addr.to_string()); content.push_str(&addr.to_string());
glue = ", "; glue = ", ";
} }
content.push_str("\n"); content.push('\n');
} }
if let Some(addrs) = prev_to.as_ref() { if let Some(addrs) = prev_to.as_ref() {
content.push_str("To: "); content.push_str("To: ");
@ -280,9 +306,9 @@ impl Msg {
content.push_str(&addr.to_string()); content.push_str(&addr.to_string());
glue = ", "; glue = ", ";
} }
content.push_str("\n"); content.push('\n');
} }
content.push_str("\n"); content.push('\n');
content.push_str(&self.fold_text_parts("plain")); content.push_str(&self.fold_text_parts("plain"));
self.parts self.parts
.replace_text_plain_parts_with(TextPlainPart { content }); .replace_text_plain_parts_with(TextPlainPart { content });
@ -337,7 +363,7 @@ impl Msg {
loop { loop {
match choice::post_edit() { match choice::post_edit() {
Ok(PostEditChoice::Send) => { Ok(PostEditChoice::Send) => {
let mbox = Mbox::new("Sent"); let mbox = Mbox::new(&account.sent_folder);
let sent_msg = smtp.send_msg(&self)?; let sent_msg = smtp.send_msg(&self)?;
let flags = Flags::try_from(vec![Flag::Seen])?; let flags = Flags::try_from(vec![Flag::Seen])?;
imap.append_raw_msg_with_flags(&mbox, &sent_msg.formatted(), flags)?; imap.append_raw_msg_with_flags(&mbox, &sent_msg.formatted(), flags)?;
@ -354,12 +380,15 @@ impl Msg {
break; break;
} }
Ok(PostEditChoice::RemoteDraft) => { Ok(PostEditChoice::RemoteDraft) => {
let mbox = Mbox::new("Drafts"); let mbox = Mbox::new(&account.draft_folder);
let flags = Flags::try_from(vec![Flag::Seen, Flag::Draft])?; let flags = Flags::try_from(vec![Flag::Seen, Flag::Draft])?;
let tpl = self.to_tpl(TplOverride::default(), account); let tpl = self.to_tpl(TplOverride::default(), account);
imap.append_raw_msg_with_flags(&mbox, tpl.as_bytes(), flags)?; imap.append_raw_msg_with_flags(&mbox, tpl.as_bytes(), flags)?;
msg_utils::remove_local_draft()?; msg_utils::remove_local_draft()?;
printer.print("Message successfully saved to Drafts")?; printer.print(format!(
"Message successfully saved to {}",
account.draft_folder
))?;
break; break;
} }
Ok(PostEditChoice::Discard) => { Ok(PostEditChoice::Discard) => {
@ -383,7 +412,7 @@ impl Msg {
let path = PathBuf::from(path.to_string()); let path = PathBuf::from(path.to_string());
let filename: String = path let filename: String = path
.file_name() .file_name()
.ok_or(anyhow!("cannot get file name of attachment {:?}", path))? .ok_or_else(|| anyhow!("cannot get file name of attachment {:?}", path))?
.to_string_lossy() .to_string_lossy()
.into(); .into();
let content = fs::read(&path).context(format!("cannot read attachment {:?}", path))?; let content = fs::read(&path).context(format!("cannot read attachment {:?}", path))?;
@ -424,17 +453,11 @@ impl Msg {
match part { match part {
Part::Binary(_) => self.parts.push(part), Part::Binary(_) => self.parts.push(part),
Part::TextPlain(_) => { Part::TextPlain(_) => {
self.parts.retain(|p| match p { self.parts.retain(|p| !matches!(p, Part::TextPlain(_)));
Part::TextPlain(_) => false,
_ => true,
});
self.parts.push(part); self.parts.push(part);
} }
Part::TextHtml(_) => { Part::TextHtml(_) => {
self.parts.retain(|p| match p { self.parts.retain(|p| !matches!(p, Part::TextHtml(_)));
Part::TextHtml(_) => false,
_ => true,
});
self.parts.push(part); self.parts.push(part);
} }
} }
@ -504,7 +527,7 @@ impl Msg {
)); ));
// Headers <=> body separator // Headers <=> body separator
tpl.push_str("\n"); tpl.push('\n');
// Body // Body
if let Some(body) = opts.body { if let Some(body) = opts.body {
@ -522,7 +545,7 @@ impl Msg {
tpl.push_str(sig); tpl.push_str(sig);
} }
tpl.push_str("\n"); tpl.push('\n');
trace!("template: {:#?}", tpl); trace!("template: {:#?}", tpl);
tpl tpl
@ -539,49 +562,45 @@ impl Msg {
let val = String::from_utf8(header.get_value_raw().to_vec()) let val = String::from_utf8(header.get_value_raw().to_vec())
.map(|val| val.trim().to_string())?; .map(|val| val.trim().to_string())?;
match key.as_str() { match key.to_lowercase().as_str() {
"Message-Id" | _ if key.eq_ignore_ascii_case("message-id") => { "message-id" => msg.message_id = Some(val.to_owned()),
msg.message_id = Some(val.to_owned()) "from" => {
}
"From" | _ if key.eq_ignore_ascii_case("from") => {
msg.from = Some( msg.from = Some(
val.split(',') val.split(',')
.filter_map(|addr| addr.parse().ok()) .filter_map(|addr| addr.parse().ok())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
); );
} }
"To" | _ if key.eq_ignore_ascii_case("to") => { "to" => {
msg.to = Some( msg.to = Some(
val.split(',') val.split(',')
.filter_map(|addr| addr.parse().ok()) .filter_map(|addr| addr.parse().ok())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
); );
} }
"Reply-To" | _ if key.eq_ignore_ascii_case("reply-to") => { "reply-to" => {
msg.reply_to = Some( msg.reply_to = Some(
val.split(',') val.split(',')
.filter_map(|addr| addr.parse().ok()) .filter_map(|addr| addr.parse().ok())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
); );
} }
"In-Reply-To" | _ if key.eq_ignore_ascii_case("in-reply-to") => { "in-reply-to" => msg.in_reply_to = Some(val.to_owned()),
msg.in_reply_to = Some(val.to_owned()) "cc" => {
}
"Cc" | _ if key.eq_ignore_ascii_case("cc") => {
msg.cc = Some( msg.cc = Some(
val.split(',') val.split(',')
.filter_map(|addr| addr.parse().ok()) .filter_map(|addr| addr.parse().ok())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
); );
} }
"Bcc" | _ if key.eq_ignore_ascii_case("bcc") => { "bcc" => {
msg.bcc = Some( msg.bcc = Some(
val.split(',') val.split(',')
.filter_map(|addr| addr.parse().ok()) .filter_map(|addr| addr.parse().ok())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
); );
} }
"Subject" | _ if key.eq_ignore_ascii_case("subject") => { "subject" => {
msg.subject = val; msg.subject = val;
} }
_ => (), _ => (),
@ -693,7 +712,7 @@ impl<'a> TryFrom<&'a imap::types::Fetch> for Msg {
fn try_from(fetch: &'a imap::types::Fetch) -> Result<Msg> { fn try_from(fetch: &'a imap::types::Fetch) -> Result<Msg> {
let envelope = fetch let envelope = fetch
.envelope() .envelope()
.ok_or(anyhow!("cannot get envelope of message {}", fetch.message))?; .ok_or_else(|| anyhow!("cannot get envelope of message {}", fetch.message))?;
// Get the sequence number // Get the sequence number
let id = fetch.message; let id = fetch.message;
@ -711,13 +730,13 @@ impl<'a> TryFrom<&'a imap::types::Fetch> for Msg {
fetch.message fetch.message
)) ))
}) })
.unwrap_or(Ok(String::default()))?; .unwrap_or_else(|| Ok(String::default()))?;
// Get the sender(s) address(es) // Get the sender(s) address(es)
let from = match envelope let from = match envelope
.sender .sender
.as_ref() .as_deref()
.or_else(|| envelope.from.as_ref()) .or_else(|| envelope.from.as_deref())
.map(parse_addrs) .map(parse_addrs)
{ {
Some(addrs) => Some(addrs?), Some(addrs) => Some(addrs?),
@ -770,7 +789,7 @@ impl<'a> TryFrom<&'a imap::types::Fetch> for Msg {
&mailparse::parse_mail( &mailparse::parse_mail(
fetch fetch
.body() .body()
.ok_or(anyhow!("cannot get body of message {}", id))?, .ok_or_else(|| anyhow!("cannot get body of message {}", id))?,
) )
.context(format!("cannot parse body of message {}", id))?, .context(format!("cannot parse body of message {}", id))?,
); );
@ -779,13 +798,13 @@ impl<'a> TryFrom<&'a imap::types::Fetch> for Msg {
id, id,
flags, flags,
subject, subject,
message_id,
from, from,
reply_to, reply_to,
in_reply_to,
to, to,
cc, cc,
bcc, bcc,
in_reply_to,
message_id,
date, date,
parts, parts,
}) })
@ -799,20 +818,20 @@ pub fn parse_addr(addr: &imap_proto::Address) -> Result<Addr> {
.map(|name| { .map(|name| {
rfc2047_decoder::decode(&name.to_vec()) rfc2047_decoder::decode(&name.to_vec())
.context("cannot decode address name") .context("cannot decode address name")
.map(|name| Some(name)) .map(Some)
}) })
.unwrap_or(Ok(None))?; .unwrap_or(Ok(None))?;
let mbox = addr let mbox = addr
.mailbox .mailbox
.as_ref() .as_ref()
.ok_or(anyhow!("cannot get address mailbox")) .ok_or_else(|| anyhow!("cannot get address mailbox"))
.and_then(|mbox| { .and_then(|mbox| {
rfc2047_decoder::decode(&mbox.to_vec()).context("cannot decode address mailbox") rfc2047_decoder::decode(&mbox.to_vec()).context("cannot decode address mailbox")
})?; })?;
let host = addr let host = addr
.host .host
.as_ref() .as_ref()
.ok_or(anyhow!("cannot get address host")) .ok_or_else(|| anyhow!("cannot get address host"))
.and_then(|host| { .and_then(|host| {
rfc2047_decoder::decode(&host.to_vec()).context("cannot decode address host") rfc2047_decoder::decode(&host.to_vec()).context("cannot decode address host")
})?; })?;
@ -820,7 +839,7 @@ pub fn parse_addr(addr: &imap_proto::Address) -> Result<Addr> {
Ok(Addr::new(name, lettre::Address::new(mbox, host)?)) Ok(Addr::new(name, lettre::Address::new(mbox, host)?))
} }
pub fn parse_addrs(addrs: &Vec<imap_proto::Address>) -> Result<Vec<Addr>> { pub fn parse_addrs(addrs: &[imap_proto::Address]) -> Result<Vec<Addr>> {
let mut parsed_addrs = vec![]; let mut parsed_addrs = vec![];
for addr in addrs { for addr in addrs {
parsed_addrs parsed_addrs
@ -830,7 +849,7 @@ pub fn parse_addrs(addrs: &Vec<imap_proto::Address>) -> Result<Vec<Addr>> {
} }
pub fn parse_some_addrs(addrs: &Option<Vec<imap_proto::Address>>) -> Result<Option<Vec<Addr>>> { pub fn parse_some_addrs(addrs: &Option<Vec<imap_proto::Address>>) -> Result<Option<Vec<Addr>>> {
Ok(match addrs.as_ref().map(parse_addrs) { Ok(match addrs.as_deref().map(parse_addrs) {
Some(addrs) => Some(addrs?), Some(addrs) => Some(addrs?),
None => None, None => None,
}) })

View file

@ -21,6 +21,7 @@ use crate::{
mbox::Mbox, mbox::Mbox,
msg::{Flags, Msg, Part, TextPlainPart}, msg::{Flags, Msg, Part, TextPlainPart},
smtp::SmtpServiceInterface, smtp::SmtpServiceInterface,
Parts,
}, },
output::{PrintTableOpts, PrinterService}, output::{PrintTableOpts, PrinterService},
}; };
@ -32,7 +33,7 @@ pub fn attachments<'a, Printer: PrinterService, ImapService: ImapServiceInterfac
printer: &mut Printer, printer: &mut Printer,
imap: &mut ImapService, imap: &mut ImapService,
) -> Result<()> { ) -> Result<()> {
let attachments = imap.find_msg(&seq)?.attachments(); let attachments = imap.find_msg(seq)?.attachments();
let attachments_len = attachments.len(); let attachments_len = attachments.len();
debug!( debug!(
r#"{} attachment(s) found for message "{}""#, r#"{} attachment(s) found for message "{}""#,
@ -60,7 +61,7 @@ pub fn copy<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
imap: &mut ImapService, imap: &mut ImapService,
) -> Result<()> { ) -> Result<()> {
let mbox = Mbox::new(mbox); let mbox = Mbox::new(mbox);
let msg = imap.find_raw_msg(&seq)?; let msg = imap.find_raw_msg(seq)?;
let flags = Flags::try_from(vec![Flag::Seen])?; let flags = Flags::try_from(vec![Flag::Seen])?;
imap.append_raw_msg_with_flags(&mbox, &msg, flags)?; imap.append_raw_msg_with_flags(&mbox, &msg, flags)?;
printer.print(format!( printer.print(format!(
@ -135,7 +136,7 @@ pub fn mailto<
) -> Result<()> { ) -> Result<()> {
let to: Vec<lettre::message::Mailbox> = url let to: Vec<lettre::message::Mailbox> = url
.path() .path()
.split(";") .split(';')
.filter_map(|s| s.parse().ok()) .filter_map(|s| s.parse().ok())
.collect(); .collect();
let mut cc = Vec::new(); let mut cc = Vec::new();
@ -161,16 +162,18 @@ pub fn mailto<
} }
} }
let mut msg = Msg::default(); let msg = Msg {
from: Some(vec![account.address().parse()?]),
to: if to.is_empty() { None } else { Some(to) },
cc: if cc.is_empty() { None } else { Some(cc) },
bcc: if bcc.is_empty() { None } else { Some(bcc) },
subject: subject.into(),
parts: Parts(vec![Part::TextPlain(TextPlainPart {
content: body.into(),
})]),
..Msg::default()
};
msg.from = Some(vec![account.address().parse()?]);
msg.to = if to.is_empty() { None } else { Some(to) };
msg.cc = if cc.is_empty() { None } else { Some(cc) };
msg.bcc = if bcc.is_empty() { None } else { Some(bcc) };
msg.subject = subject.into();
msg.parts.push(Part::TextPlain(TextPlainPart {
content: body.into(),
}));
msg.edit_with_editor(account, printer, imap, smtp) msg.edit_with_editor(account, printer, imap, smtp)
} }
@ -185,7 +188,7 @@ pub fn move_<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>
) -> Result<()> { ) -> Result<()> {
// Copy the message to targetted mailbox // Copy the message to targetted mailbox
let mbox = Mbox::new(mbox); let mbox = Mbox::new(mbox);
let msg = imap.find_raw_msg(&seq)?; let msg = imap.find_raw_msg(seq)?;
let flags = Flags::try_from(vec![Flag::Seen])?; let flags = Flags::try_from(vec![Flag::Seen])?;
imap.append_raw_msg_with_flags(&mbox, &msg, flags)?; imap.append_raw_msg_with_flags(&mbox, &msg, flags)?;
@ -210,9 +213,9 @@ pub fn read<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
) -> Result<()> { ) -> Result<()> {
let msg = if raw { let msg = if raw {
// Emails don't always have valid utf8. Using "lossy" to display what we can. // Emails don't always have valid utf8. Using "lossy" to display what we can.
String::from_utf8_lossy(&imap.find_raw_msg(&seq)?).into_owned() String::from_utf8_lossy(&imap.find_raw_msg(seq)?).into_owned()
} else { } else {
imap.find_msg(&seq)?.fold_text_parts(text_mime) imap.find_msg(seq)?.fold_text_parts(text_mime)
}; };
printer.print(msg) printer.print(msg)
@ -254,8 +257,7 @@ pub fn save<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>(
io::stdin() io::stdin()
.lock() .lock()
.lines() .lines()
.filter_map(|ln| ln.ok()) .filter_map(Result::ok)
.map(|ln| ln.to_string())
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\r\n") .join("\r\n")
}; };
@ -290,6 +292,7 @@ pub fn send<
SmtpService: SmtpServiceInterface, SmtpService: SmtpServiceInterface,
>( >(
raw_msg: &str, raw_msg: &str,
account: &Account,
printer: &mut Printer, printer: &mut Printer,
imap: &mut ImapService, imap: &mut ImapService,
smtp: &mut SmtpService, smtp: &mut SmtpService,
@ -300,19 +303,18 @@ pub fn send<
io::stdin() io::stdin()
.lock() .lock()
.lines() .lines()
.filter_map(|ln| ln.ok()) .filter_map(Result::ok)
.map(|ln| ln.to_string())
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\r\n") .join("\r\n")
}; };
let msg = Msg::from_tpl(&raw_msg.to_string())?; let msg = Msg::from_tpl(&raw_msg)?;
let envelope: lettre::address::Envelope = msg.try_into()?; let envelope: lettre::address::Envelope = msg.try_into()?;
smtp.send_raw_msg(&envelope, raw_msg.as_bytes())?; smtp.send_raw_msg(&envelope, raw_msg.as_bytes())?;
debug!("message sent!"); debug!("message sent!");
// Save message to sent folder // Save message to sent folder
let mbox = Mbox::new("Sent"); let mbox = Mbox::new(&account.sent_folder);
let flags = Flags::try_from(vec![Flag::Seen])?; let flags = Flags::try_from(vec![Flag::Seen])?;
imap.append_raw_msg_with_flags(&mbox, raw_msg.as_bytes(), flags) imap.append_raw_msg_with_flags(&mbox, raw_msg.as_bytes(), flags)
} }

View file

@ -39,24 +39,12 @@ pub struct Parts(pub Vec<Part>);
impl Parts { impl Parts {
pub fn replace_text_plain_parts_with(&mut self, part: TextPlainPart) { pub fn replace_text_plain_parts_with(&mut self, part: TextPlainPart) {
self.retain(|part| { self.retain(|part| !matches!(part, Part::TextPlain(_)));
if let Part::TextPlain(_) = part {
false
} else {
true
}
});
self.push(Part::TextPlain(part)); self.push(Part::TextPlain(part));
} }
pub fn replace_text_html_parts_with(&mut self, part: TextHtmlPart) { pub fn replace_text_html_parts_with(&mut self, part: TextHtmlPart) {
self.retain(|part| { self.retain(|part| !matches!(part, Part::TextHtml(_)));
if let Part::TextHtml(_) = part {
false
} else {
true
}
});
self.push(Part::TextHtml(part)); self.push(Part::TextHtml(part));
} }
} }
@ -92,7 +80,7 @@ fn build_parts_map_rec(part: &mailparse::ParsedMail, parts: &mut Vec<Part>) {
.params .params
.get("filename") .get("filename")
.map(String::from) .map(String::from)
.unwrap_or(String::from("noname")); .unwrap_or_else(|| String::from("noname"));
let content = part.get_body_raw().unwrap_or_default(); let content = part.get_body_raw().unwrap_or_default();
let mime = tree_magic::from_u8(&content); let mime = tree_magic::from_u8(&content);
parts.push(Part::Binary(BinaryPart { parts.push(Part::Binary(BinaryPart {
@ -103,16 +91,14 @@ fn build_parts_map_rec(part: &mailparse::ParsedMail, parts: &mut Vec<Part>) {
} }
// TODO: manage other use cases // TODO: manage other use cases
_ => { _ => {
part.get_headers() if let Some(ctype) = part.get_headers().get_first_value("content-type") {
.get_first_value("content-type") let content = part.get_body().unwrap_or_default();
.map(|ctype| { if ctype.starts_with("text/plain") {
let content = part.get_body().unwrap_or_default(); parts.push(Part::TextPlain(TextPlainPart { content }))
if ctype.starts_with("text/plain") { } else if ctype.starts_with("text/html") {
parts.push(Part::TextPlain(TextPlainPart { content })) parts.push(Part::TextHtml(TextHtmlPart { content }))
} else if ctype.starts_with("text/html") { }
parts.push(Part::TextHtml(TextHtmlPart { content })) };
}
});
} }
}; };
} else { } else {

View file

@ -1,6 +1,4 @@
use anyhow::Result; use anyhow::Result;
use clap;
use env_logger;
use output::StdoutPrinter; use output::StdoutPrinter;
use std::{convert::TryFrom, env}; use std::{convert::TryFrom, env};
use url::Url; use url::Url;
@ -36,6 +34,7 @@ fn create_app<'a>() -> clap::App<'a, 'a> {
.subcommands(msg_arg::subcmds()) .subcommands(msg_arg::subcmds())
} }
#[allow(clippy::single_match)]
fn main() -> Result<()> { fn main() -> Result<()> {
// Init env logger // Init env logger
env_logger::init_from_env( env_logger::init_from_env(
@ -45,9 +44,9 @@ fn main() -> Result<()> {
// Check mailto command BEFORE app initialization. // Check mailto command BEFORE app initialization.
let raw_args: Vec<String> = env::args().collect(); let raw_args: Vec<String> = env::args().collect();
if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") {
let mbox = Mbox::new("INBOX");
let config = Config::try_from(None)?; let config = Config::try_from(None)?;
let account = Account::try_from((&config, None))?; let account = Account::try_from((&config, None))?;
let mbox = Mbox::new(&account.inbox_folder);
let mut printer = StdoutPrinter::from(OutputFmt::Plain); let mut printer = StdoutPrinter::from(OutputFmt::Plain);
let url = Url::parse(&raw_args[1])?; let url = Url::parse(&raw_args[1])?;
let mut imap = ImapService::from((&account, &mbox)); let mut imap = ImapService::from((&account, &mbox));
@ -68,9 +67,9 @@ fn main() -> Result<()> {
} }
// Init entities and services. // Init entities and services.
let mbox = Mbox::new(m.value_of("mbox-source").unwrap());
let config = Config::try_from(m.value_of("config"))?; let config = Config::try_from(m.value_of("config"))?;
let account = Account::try_from((&config, m.value_of("account")))?; let account = Account::try_from((&config, m.value_of("account")))?;
let mbox = Mbox::new(m.value_of("mbox-source").unwrap_or(&account.inbox_folder));
let mut printer = StdoutPrinter::try_from(m.value_of("output"))?; let mut printer = StdoutPrinter::try_from(m.value_of("output"))?;
let mut imap = ImapService::from((&account, &mbox)); let mut imap = ImapService::from((&account, &mbox));
let mut smtp = SmtpService::from(&account); let mut smtp = SmtpService::from(&account);
@ -81,7 +80,7 @@ fn main() -> Result<()> {
return imap_handler::notify(keepalive, &config, &mut imap); return imap_handler::notify(keepalive, &config, &mut imap);
} }
Some(imap_arg::Command::Watch(keepalive)) => { Some(imap_arg::Command::Watch(keepalive)) => {
return imap_handler::watch(keepalive, &mut imap); return imap_handler::watch(keepalive, &account, &mut imap);
} }
_ => (), _ => (),
} }
@ -150,7 +149,7 @@ fn main() -> Result<()> {
); );
} }
Some(msg_arg::Command::Send(raw_msg)) => { Some(msg_arg::Command::Send(raw_msg)) => {
return msg_handler::send(raw_msg, &mut printer, &mut imap, &mut smtp); return msg_handler::send(raw_msg, &account, &mut printer, &mut imap, &mut smtp);
} }
Some(msg_arg::Command::Write(atts)) => { Some(msg_arg::Command::Write(atts)) => {
return msg_handler::write(atts, &account, &mut printer, &mut imap, &mut smtp); return msg_handler::write(atts, &account, &mut printer, &mut imap, &mut smtp);

View file

@ -36,9 +36,9 @@ impl TryFrom<Option<&str>> for OutputFmt {
impl Display for OutputFmt { impl Display for OutputFmt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let fmt = match self { let fmt = match *self {
&OutputFmt::Json => "JSON", OutputFmt::Json => "JSON",
&OutputFmt::Plain => "Plain", OutputFmt::Plain => "Plain",
}; };
write!(f, "{}", fmt) write!(f, "{}", fmt)
} }

View file

@ -9,7 +9,7 @@ pub trait Print {
impl Print for &str { impl Print for &str {
fn print(&self, writter: &mut dyn WriteColor) -> Result<()> { fn print(&self, writter: &mut dyn WriteColor) -> Result<()> {
write!(writter, "{}", self).with_context(|| { writeln!(writter, "{}", self).with_context(|| {
error!(r#"cannot write string to writter: "{}""#, self); error!(r#"cannot write string to writter: "{}""#, self);
"cannot write string to writter" "cannot write string to writter"
}) })

View file

@ -227,13 +227,11 @@ where
trace!("number of spaces added to shrinked value: {}", spaces_count); trace!("number of spaces added to shrinked value: {}", spaces_count);
value.push_str(&" ".repeat(spaces_count)); value.push_str(&" ".repeat(spaces_count));
cell.value = value; cell.value = value;
cell.print(writter)?;
} else { } else {
trace!("cell is not overflowing"); trace!("cell is not overflowing");
let spaces_count = cell_width - cell.unicode_width() + 1; let spaces_count = cell_width - cell.unicode_width() + 1;
trace!("number of spaces added to value: {}", spaces_count); trace!("number of spaces added to value: {}", spaces_count);
cell.value.push_str(&" ".repeat(spaces_count)); cell.value.push_str(&" ".repeat(spaces_count));
cell.print(writter)?;
} }
} else { } else {
trace!("table is not overflowing or cell is not shrinkable"); trace!("table is not overflowing or cell is not shrinkable");
@ -242,8 +240,8 @@ where
let spaces_count = cell_widths[i] - cell.unicode_width() + 1; let spaces_count = cell_widths[i] - cell.unicode_width() + 1;
trace!("number of spaces added to value: {}", spaces_count); trace!("number of spaces added to value: {}", spaces_count);
cell.value.push_str(&" ".repeat(spaces_count)); cell.value.push_str(&" ".repeat(spaces_count));
cell.print(writter)?;
} }
cell.print(writter)?;
glue = Cell::new("").ansi_256(8); glue = Cell::new("").ansi_256(8);
} }
writeln!(writter)?; writeln!(writter)?;

View file

@ -1,150 +0,0 @@
// FIXME: fix tests
// use std::convert::TryFrom;
// use himalaya::{
// domain::account::entity::Account, flag::model::Flags, imap::model::ImapConnector,
// mbox::model::Mboxes, msg::model::Msgs,
// };
// use imap::types::Flag;
// use lettre::message::SinglePart;
// use lettre::Message;
// fn get_account(addr: &str) -> Account {
// Account {
// name: None,
// downloads_dir: None,
// signature_delimiter: None,
// signature: None,
// default_page_size: None,
// default: Some(true),
// email: addr.into(),
// watch_cmds: None,
// imap_host: String::from("localhost"),
// imap_port: 3993,
// imap_starttls: Some(false),
// imap_insecure: Some(true),
// imap_login: addr.into(),
// imap_passwd_cmd: String::from("echo 'password'"),
// smtp_host: String::from("localhost"),
// smtp_port: 3465,
// smtp_starttls: Some(false),
// smtp_insecure: Some(true),
// smtp_login: addr.into(),
// smtp_passwd_cmd: String::from("echo 'password'"),
// }
// }
// #[test]
// fn mbox() {
// let account = get_account("inbox@localhost");
// let mut imap_conn = ImapConnector::new(&account).unwrap();
// let names = imap_conn.list_mboxes().unwrap();
// let mboxes: Vec<String> = Mboxes::from(&names)
// .0
// .into_iter()
// .map(|mbox| mbox.name)
// .collect();
// assert_eq!(mboxes, vec![String::from("INBOX")]);
// imap_conn.logout();
// }
// #[test]
// fn msg() {
// // Preparations
// // Get the test-account and clean up the server.
// let account = get_account("inbox@localhost");
// // Login
// let mut imap_conn = ImapConnector::new(&account).unwrap();
// // remove all previous mails first
// let fetches = imap_conn.list_msgs("INBOX", &10, &0).unwrap();
// let msgs = if let Some(ref fetches) = fetches {
// Msgs::try_from(fetches).unwrap()
// } else {
// Msgs::new()
// };
// // mark all mails as deleted
// for msg in msgs.0.iter() {
// imap_conn
// .add_flags(
// "INBOX",
// &msg.get_uid().unwrap().to_string(),
// Flags::from(vec![Flag::Deleted]),
// )
// .unwrap();
// }
// imap_conn.expunge("INBOX").unwrap();
// // make sure, that they are *really* deleted
// assert!(imap_conn.list_msgs("INBOX", &10, &0).unwrap().is_none());
// // == Testing ==
// // Add messages
// let message_a = Message::builder()
// .from("sender-a@localhost".parse().unwrap())
// .to("inbox@localhost".parse().unwrap())
// .subject("Subject A")
// .singlepart(SinglePart::builder().body("Body A".as_bytes().to_vec()))
// .unwrap();
// let message_b = Message::builder()
// .from("Sender B <sender-b@localhost>".parse().unwrap())
// .to("inbox@localhost".parse().unwrap())
// .subject("Subject B")
// .singlepart(SinglePart::builder().body("Body B".as_bytes().to_vec()))
// .unwrap();
// smtp::send(&account, &message_a).unwrap();
// smtp::send(&account, &message_b).unwrap();
// // -- Get the messages --
// // TODO: check non-existance of \Seen flag
// let msgs = imap_conn.list_msgs("INBOX", &10, &0).unwrap();
// let msgs = if let Some(ref fetches) = msgs {
// Msgs::try_from(fetches).unwrap()
// } else {
// Msgs::new()
// };
// // make sure that there are both mails which we sended
// assert_eq!(msgs.0.len(), 2);
// let msg_a = msgs
// .0
// .iter()
// .find(|msg| msg.headers.subject.clone().unwrap() == "Subject A")
// .unwrap();
// let msg_b = msgs
// .0
// .iter()
// .find(|msg| msg.headers.subject.clone().unwrap() == "Subject B")
// .unwrap();
// // -- Checkup --
// // look, if we received the correct credentials of the msgs.
// assert_eq!(
// msg_a.headers.subject.clone().unwrap_or_default(),
// "Subject A"
// );
// assert_eq!(&msg_a.headers.from[0], "sender-a@localhost");
// assert_eq!(
// msg_b.headers.subject.clone().unwrap_or_default(),
// "Subject B"
// );
// assert_eq!(&msg_b.headers.from[0], "Sender B <sender-b@localhost>");
// // TODO: search messages
// // TODO: read message (+ \Seen flag)
// // TODO: list message attachments
// // TODO: add/set/remove flags
// // Logout
// imap_conn.logout();
// }

View file

@ -49,6 +49,24 @@ let g:himalaya_telescope_preview_enabled = 0
Should enable telescope preview when picking a mailbox with the telescope Should enable telescope preview when picking a mailbox with the telescope
provider. provider.
### Contact completion
```vim
let g:himalaya_complete_contact_cmd = '<your completion command>'
```
Define the command to use for contact completion. When this is set,
`completefunc` will be set when composing messages so that contacts can be
completed with `<C-x><C-u>`.
The command must print each possible result on its own line. Each line must
contain tab-separated fields; the first must be the email address, and the
second, if present, must be the name. `%s` in the command will be replaced
with the search query.
For example, to complete contacts with khard, you could use
`khard email --remove-first-line --parsable '%s'` as the completion command.
## Usage ## Usage
### List messages view ### List messages view

View file

@ -305,6 +305,37 @@ function! himalaya#msg#attachments()
endtry endtry
endfunction endfunction
function! himalaya#msg#complete_contact(findstart, base)
try
if a:findstart
if !exists("g:himalaya_complete_contact_cmd")
echoerr "You must set 'g:himalaya_complete_contact_cmd' to complete contacts"
return -3
endif
" search for everything up to the last colon or comma
let line_to_cursor = getline(".")[:col(".") - 1]
let start = match(line_to_cursor, '[^:,]*$')
" don't include leading spaces
while start <= len(line_to_cursor) && line_to_cursor[start] == " "
let start += 1
endwhile
return start
else
let output = system(substitute(g:himalaya_complete_contact_cmd, "%s", a:base, ""))
let lines = split(output, "\n")
return map(lines, "s:line_to_complete_item(v:val)")
endif
catch
if !empty(v:exception)
redraw | call himalaya#shared#log#err(v:exception)
endif
endtry
endfunction
" Utils " Utils
" https://newbedev.com/get-usable-window-width-in-vim-script " https://newbedev.com/get-usable-window-width-in-vim-script
@ -326,9 +357,13 @@ function! s:bufwidth()
return width - numwidth - foldwidth - signwidth return width - numwidth - foldwidth - signwidth
endfunction endfunction
function! s:get_msg_id(line)
return matchstr(a:line, '[0-9]*')
endfunction
function! s:get_focused_msg_id() function! s:get_focused_msg_id()
try try
return s:trim(split(getline("."), "|")[0]) return s:get_msg_id(getline("."))
catch catch
throw "message not found" throw "message not found"
endtry endtry
@ -336,7 +371,7 @@ endfunction
function! s:get_focused_msg_ids(from, to) function! s:get_focused_msg_ids(from, to)
try try
return join(map(range(a:from, a:to), "s:trim(split(getline(v:val), '|')[0])"), ",") return join(map(range(a:from, a:to), "s:get_msg_id(getline(v:val))"), ",")
catch catch
throw "messages not found" throw "messages not found"
endtry endtry
@ -349,3 +384,14 @@ function! s:close_open_buffers(name)
execute ":bwipeout " . buffer_to_close execute ":bwipeout " . buffer_to_close
endfor endfor
endfunction endfunction
function! s:line_to_complete_item(line)
let fields = split(a:line, "\t")
let email = fields[0]
let name = ""
if len(fields) > 1
let name = '"' . fields[1] . '" '
endif
return name . "<" . email . ">"
endfunction

View file

@ -1,4 +1,4 @@
*himalaya.txt* - CLI email client *himalaya.txt* - Command-line interface for email management
_/ _/ _/_/_/ _/ _/ _/_/ _/ _/_/ _/ _/ _/_/ _/ _/ _/_/_/ _/ _/ _/_/ _/ _/_/ _/ _/ _/_/
_/ _/ _/ _/_/ _/_/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/_/ _/_/ _/ _/ _/ _/ _/ _/ _/ _/ _/
@ -46,6 +46,23 @@ TELESCOPE PREVIEW
Should enable telescope preview when picking a mailbox with the telescope Should enable telescope preview when picking a mailbox with the telescope
provider. provider.
------------------------------------------------------------------------------
CONTACT COMPLETION
>
let g:himalaya_complete_contact_cmd = '<your completion command>'
<
Define the command to use for contact completion. When this is set,
'completefunc' will be set when composing messages so that contacts can be
completed with |i_CTRL-X_CTRL-U|.
The command must print each possible result on its own line. Each line must
contain tab-separated fields; the first must be the email address, and the
second, if present, must be the name. `%s` in the command will be replaced
with the search query.
For example, to complete contacts with khard, you could use
`khard email --remove-first-line --parsable '%s'` as the completion command.
============================================================================== ==============================================================================
USAGE *himalaya-usage* USAGE *himalaya-usage*

View file

@ -3,6 +3,10 @@ setlocal foldexpr=himalaya#shared#thread#fold(v:lnum)
setlocal foldmethod=expr setlocal foldmethod=expr
setlocal startofline setlocal startofline
if exists("g:himalaya_complete_contact_cmd")
setlocal completefunc=himalaya#msg#complete_contact
endif
augroup himalaya_write augroup himalaya_write
autocmd! * <buffer> autocmd! * <buffer>
autocmd BufWriteCmd <buffer> call himalaya#msg#draft_save() autocmd BufWriteCmd <buffer> call himalaya#msg#draft_save()

View file

@ -10,7 +10,7 @@ local previewers = require('telescope.previewers')
local function preview_command(entry, bufnr) local function preview_command(entry, bufnr)
vim.api.nvim_buf_call(bufnr, function() vim.api.nvim_buf_call(bufnr, function()
local page = 0 -- page 0 for preview local page = 0 -- page 0 for preview
local account = pcall(vim.fn['himalaya#account#curr']) local account = vim.fn['himalaya#account#curr']()
local success, output = pcall(vim.fn['himalaya#msg#list_with'], account, entry.value, page, true) local success, output = pcall(vim.fn['himalaya#msg#list_with'], account, entry.value, page, true)
if not (success) then if not (success) then
vim.cmd('redraw') vim.cmd('redraw')

1
wiki

@ -1 +0,0 @@
Subproject commit 9fbd490bd4f42524cb0099e9914144375ea5514a