From 8cdeba62a1d0763c7e819eae085814df48799183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Wed, 2 Feb 2022 02:21:35 +0100 Subject: [PATCH] release v0.5.2 (#282) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 `` 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 Co-authored-by: Jason Cox Co-authored-by: Gökmen Görgen Co-authored-by: Ethiraric --- .gitignore | 11 +- .gitmodules | 3 - CHANGELOG.md | 31 +++++- Cargo.lock | 59 +++++------ Cargo.toml | 8 +- README.md | 16 ++- assets/himalaya.desktop | 4 +- flake.lock | 90 ++++++++++------- flake.nix | 70 ++++++------- src/config/account_entity.rs | 34 ++++++- src/config/config_entity.rs | 51 +++++----- src/domain/imap/imap_handler.rs | 10 +- src/domain/imap/imap_service.rs | 59 +++++++---- src/domain/mbox/mbox_arg.rs | 5 +- src/domain/mbox/mbox_handler.rs | 4 +- src/domain/mbox/mboxes_entity.rs | 2 +- src/domain/msg/envelope_entity.rs | 22 ++-- src/domain/msg/envelopes_entity.rs | 2 +- src/domain/msg/flags_entity.rs | 18 ++-- src/domain/msg/msg_arg.rs | 4 +- src/domain/msg/msg_entity.rs | 131 +++++++++++++----------- src/domain/msg/msg_handler.rs | 44 ++++---- src/domain/msg/parts_entity.rs | 36 ++----- src/main.rs | 11 +- src/output/output_entity.rs | 6 +- src/output/print.rs | 2 +- src/ui/table.rs | 4 +- tests/imap_test.rs | 150 ---------------------------- vim/README.md | 18 ++++ vim/autoload/himalaya/msg.vim | 50 +++++++++- vim/doc/himalaya.txt | 19 +++- vim/ftplugin/himalaya-msg-write.vim | 4 + vim/lua/himalaya/mbox.lua | 2 +- wiki | 1 - 34 files changed, 494 insertions(+), 487 deletions(-) delete mode 100644 .gitmodules delete mode 100644 tests/imap_test.rs delete mode 160000 wiki diff --git a/.gitignore b/.gitignore index 563e3ca..5b53bdd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,13 @@ +# Cargo build directory /target -/vim/doc/tags + +# Nix build directory /result /result-lib + +# Direnv +/.envrc +/.direnv + +# Vim plugin doc tags file +/vim/doc/tags diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index c5ee52d..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "wiki"] - path = wiki - url = git@github.com:soywod/himalaya.wiki.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 34748ee..668e046 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [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 ### 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] -### Change +### Changed - 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] @@ -249,7 +266,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Password from command [#22] - 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.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 @@ -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 [#162]: https://github.com/soywod/himalaya/issues/162 [#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 [#186]: https://github.com/soywod/himalaya/issues/186 [#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 [#228]: https://github.com/soywod/himalaya/issues/228 [#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 diff --git a/Cargo.lock b/Cargo.lock index 87def5a..cd5eacb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,9 +169,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" dependencies = [ "core-foundation-sys", "libc", @@ -179,9 +179,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "dirs-next" @@ -206,9 +206,9 @@ dependencies = [ [[package]] name = "encoding_rs" -version = "0.8.28" +version = "0.8.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" +checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746" dependencies = [ "cfg-if 1.0.0", ] @@ -361,7 +361,7 @@ dependencies = [ [[package]] name = "himalaya" -version = "0.5.1" +version = "0.5.2" dependencies = [ "ammonia", "anyhow", @@ -484,9 +484,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if 1.0.0", ] @@ -526,9 +526,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.103" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" +checksum = "a60553f9a9e039a333b4e9b20573b9e9b9c0bb3a11e201ccc48ef4283456d673" [[package]] name = "lock_api" @@ -728,9 +728,9 @@ checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" [[package]] name = "openssl" -version = "0.10.36" +version = "0.10.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a" +checksum = "2bc6b9e4403633698352880b22cbe2f0e45dd0177f6fabe4585536e56a3e4f75" dependencies = [ "bitflags", "cfg-if 1.0.0", @@ -748,9 +748,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" [[package]] name = "openssl-sys" -version = "0.9.67" +version = "0.9.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69df2d8dfc6ce3aaf44b40dec6f487d5a886516cf6879c49e98e0710f310a058" +checksum = "1c571f25d3f66dd427e417cebf73dbe2361d6125cf6e3a70d143fdf97c9f5150" dependencies = [ "autocfg", "cc", @@ -876,15 +876,15 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb" +checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" [[package]] name = "ppv-lite86" -version = "0.2.10" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" [[package]] name = "precomputed-hash" @@ -894,9 +894,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "proc-macro2" -version = "1.0.29" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" +checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" dependencies = [ "unicode-xid", ] @@ -1188,9 +1188,9 @@ checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b" [[package]] name = "slab" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] name = "smallvec" @@ -1200,12 +1200,13 @@ checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" [[package]] name = "string_cache" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a" +checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6" dependencies = [ "lazy_static", "new_debug_unreachable", + "parking_lot 0.11.2", "phf_shared", "precomputed-hash", "serde", @@ -1231,9 +1232,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "syn" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" +checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" dependencies = [ "proc-macro2", "quote", @@ -1470,9 +1471,9 @@ checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" [[package]] name = "xml5ever" -version = "0.16.1" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b1b52e6e8614d4a58b8e70cf51ec0cc21b256ad8206708bcff8139b5bbd6a59" +checksum = "9234163818fd8e2418fcde330655e757900d4236acd8cc70fef345ef91f6d865" dependencies = [ "log", "mac", diff --git a/Cargo.toml b/Cargo.toml index b2c7ebd..0fbaa5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "himalaya" -description = "CLI email client" -version = "0.5.1" +description = "Command-line interface for email management" +version = "0.5.2" authors = ["soywod "] edition = "2018" @@ -17,10 +17,10 @@ imap = "3.0.0-alpha.4" imap-proto = "0.14.3" # This commit includes the de/serialization of the ContentType # 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" mailparse = "0.13.6" -native-tls = "0.2" +native-tls = "0.2.8" regex = "1.5.4" rfc2047-decoder = "0.1.2" serde = { version = "1.0.118", features = ["derive"] } diff --git a/README.md b/README.md index 08c8d2a..595e6ce 100644 --- a/README.md +++ b/README.md @@ -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) [![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 `v1.0.0` (see the [roadmap](https://github.com/soywod/himalaya/milestone/5)).* @@ -26,15 +26,12 @@ Possibilities are endless! ## Installation ```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 ``` -*See the [wiki](https://github.com/soywod/himalaya/wiki) for other installation -methods.* +*See the +[wiki](https://github.com/soywod/himalaya/wiki/Installation:from-binary) for +other installation methods.* ## Configuration @@ -44,7 +41,7 @@ methods.* name = "Your full name" downloads-dir = "/abs/path/to/downloads" signature = """ --- +Cordialement, Regards, """ @@ -80,7 +77,8 @@ all the options.* - 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 diff --git a/assets/himalaya.desktop b/assets/himalaya.desktop index 3474b73..c5ed6aa 100644 --- a/assets/himalaya.desktop +++ b/assets/himalaya.desktop @@ -3,9 +3,7 @@ Type=Application Name=himalaya DesktopName=Himalaya GenericName=Mail Reader -Comment=CLI email client -Comment[lo]=CLI ອີເມວໄຄລແອນທີ່ຂຽນດ້ວຍພາສາRust -Comment[th]=CLI อีเมล์ไคลแอนท์ที่เขียนด้วยภาษาRust +Comment=Command-line interface for email management Terminal=true Exec=himalaya %U Categories=Application;Network diff --git a/flake.lock b/flake.lock index cb69e09..49eedd1 100644 --- a/flake.lock +++ b/flake.lock @@ -1,22 +1,5 @@ { "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": false, "locked": { @@ -35,11 +18,11 @@ }, "flake-utils": { "locked": { - "lastModified": 1614513358, - "narHash": "sha256-LakhOx3S1dRjnh0b5Dg3mbZyH0ToC9I8Y2wKSkBaTzU=", + "lastModified": 1637014545, + "narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=", "owner": "numtide", "repo": "flake-utils", - "rev": "5466c5bbece17adaab2d82fae80b46e807611bf3", + "rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4", "type": "github" }, "original": { @@ -48,42 +31,73 @@ "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": { "locked": { - "lastModified": 1627857416, - "narHash": "sha256-AV0MsFVzbWI2MZbJ2j0kc8ooFLGSCZHuM9ipaWR9ds4=", - "owner": "nixos", + "lastModified": 1640418986, + "narHash": "sha256-a8GGtxn2iL3WAkY5H+4E0s3Q7XJt6bTOvos9qqxT5OQ=", + "owner": "NixOS", "repo": "nixpkgs", - "rev": "aaf9676fbb7fb4570216ca1e189a3dc769d62c45", + "rev": "5c37ad87222cfc1ec36d6cd1364514a9efc2f7f2", "type": "github" }, "original": { - "owner": "nixos", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" + "id": "nixpkgs", + "type": "indirect" } }, "nixpkgs_2": { "locked": { - "lastModified": 1617325113, - "narHash": "sha256-GksR0nvGxfZ79T91UUtWjjccxazv6Yh/MvEJ82v1Xmw=", - "owner": "nixos", + "lastModified": 1640418986, + "narHash": "sha256-a8GGtxn2iL3WAkY5H+4E0s3Q7XJt6bTOvos9qqxT5OQ=", + "owner": "NixOS", "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" }, "original": { "owner": "NixOS", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { - "crate2nix": "crate2nix", "flake-compat": "flake-compat", - "nixpkgs": "nixpkgs", + "naersk": "naersk", + "nixpkgs": "nixpkgs_2", "rust-overlay": "rust-overlay", "utils": "utils" } @@ -91,14 +105,14 @@ "rust-overlay": { "inputs": { "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs_2" + "nixpkgs": "nixpkgs_3" }, "locked": { - "lastModified": 1627957145, - "narHash": "sha256-cY5lS2S/RMsC1xFtkcmhLXlVP7ahZoxFeKedkXDvIzY=", + "lastModified": 1642838864, + "narHash": "sha256-pHnhm3HWwtvtOK7NdNHwERih3PgNlacrfeDwachIG8E=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "ab6f3086de97980e4fdcb0560921852a407e0b79", + "rev": "9fb49daf1bbe1d91e6c837706c481f9ebb3d8097", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 671f3a2..a63b92f 100644 --- a/flake.nix +++ b/flake.nix @@ -1,27 +1,21 @@ { - description = "CLI email client"; + description = "Command-line interface for email management"; inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; utils.url = "github:numtide/flake-utils"; rust-overlay.url = "github:oxalica/rust-overlay"; - crate2nix = { - url = "github:balsoft/crate2nix/tools-nix-version-comparison"; - flake = false; - }; + naersk.url = "github:nix-community/naersk"; flake-compat = { url = "github:edolstra/flake-compat"; flake = false; }; }; - outputs = { self, nixpkgs, utils, rust-overlay, crate2nix, ... }: + outputs = { self, nixpkgs, utils, rust-overlay, naersk, ... }: utils.lib.eachDefaultSystem (system: let name = "himalaya"; - - # Imports pkgs = import nixpkgs { inherit system; overlays = [ @@ -35,37 +29,23 @@ }) ]; }; - inherit (import "${crate2nix}/tools.nix" { inherit pkgs; }) - generatedCargoNix; - - # Create the cargo2nix project - project = pkgs.callPackage (generatedCargoNix { - inherit name; - src = ./.; - }) { - # Individual crate overrides go here - # Example: https://github.com/balsoft/simple-osd-daemons/blob/6f85144934c0c1382c7a4d3a2bbb80106776e270/flake.nix#L28-L50 - defaultCrateOverrides = pkgs.defaultCrateOverrides // { - # The himalaya crate itself is overriden here. Typically we - # configure non-Rust dependencies (see below) here. - ${name} = oldAttrs: { - inherit buildInputs nativeBuildInputs; + naersk-lib = naersk.lib.${system}; + in + rec { + # nix build + defaultPackage = packages.${name}; + packages = { + ${name} = naersk-lib.buildPackage { + pname = name; + root = ./.; + nativeBuildInputs = with pkgs; [ openssl.dev pkgconfig ]; + overrideMain = _: { postInstall = '' mkdir -p $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 { inherit (packages.${name}) version; name = "${name}-vim"; @@ -80,21 +60,27 @@ }; }; - # `nix build` - defaultPackage = packages.${name}; - - # `nix run` + # nix run + defaultApp = apps.${name}; apps.${name} = utils.lib.mkApp { inherit name; drv = packages.${name}; }; - defaultApp = apps.${name}; - # `nix develop` + # nix develop 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}"; + inputsFrom = builtins.attrValues self.packages.${system}; + buildInputs = with pkgs; [ + cargo + cargo-watch + trunk + ripgrep + rust-analyzer + rustfmt + rnix-lsp + nixpkgs-fmt + ]; }; } ); diff --git a/src/config/account_entity.rs b/src/config/account_entity.rs index b3c2d66..a952daf 100644 --- a/src/config/account_entity.rs +++ b/src/config/account_entity.rs @@ -8,6 +8,10 @@ use crate::{ 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. #[derive(Debug, Default)] pub struct Account { @@ -16,6 +20,12 @@ pub struct Account { pub downloads_dir: PathBuf, pub sig: Option, 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, pub default: bool, pub email: String, @@ -41,7 +51,7 @@ impl Account { let has_special_chars = "()<>[]:;@.,".contains(|special_char| name.contains(special_char)); if name.is_empty() { - format!("{}", self.email) + self.email.clone() } else if has_special_chars { // so the name has special characters => Wrap it with '"' format!("\"{}\" <{}>", name, self.email) @@ -102,7 +112,7 @@ impl<'a> TryFrom<(&'a Config, Option<&str>)> for Account { .and_then(|dir| shellexpand::full(dir).ok()) .map(|dir| PathBuf::from(dir.to_string())) }) - .unwrap_or_else(|| env::temp_dir()); + .unwrap_or_else(env::temp_dir); let default_page_size = account .default_page_size @@ -134,6 +144,24 @@ impl<'a> TryFrom<(&'a Config, Option<&str>)> for Account { downloads_dir, sig, 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 .as_ref() @@ -142,12 +170,14 @@ impl<'a> TryFrom<(&'a Config, Option<&str>)> for Account { .to_owned(), default: account.default.unwrap_or(false), email: account.email.to_owned(), + imap_host: account.imap_host.to_owned(), imap_port: account.imap_port, imap_starttls: account.imap_starttls.unwrap_or_default(), imap_insecure: account.imap_insecure.unwrap_or_default(), imap_login: account.imap_login.to_owned(), imap_passwd_cmd: account.imap_passwd_cmd.to_owned(), + smtp_host: account.smtp_host.to_owned(), smtp_port: account.smtp_port, smtp_starttls: account.smtp_starttls.unwrap_or_default(), diff --git a/src/config/config_entity.rs b/src/config/config_entity.rs index 68016e0..dd57821 100644 --- a/src/config/config_entity.rs +++ b/src/config/config_entity.rs @@ -1,7 +1,7 @@ use anyhow::{Context, Error, Result}; use log::{debug, trace}; 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 crate::output::run_cmd; @@ -13,18 +13,27 @@ pub const DEFAULT_SIG_DELIM: &str = "-- \n"; #[derive(Debug, Default, Clone, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct Config { - /// Define the full display name of the user. + /// Defines the full display name of the user. pub name: String, - /// Define the downloads directory (eg. for attachments). + /// Defines the downloads directory (eg. for attachments). pub downloads_dir: Option, - /// Override the default signature delimiter "`--\n `". + /// Overrides the default signature delimiter "`--\n `". pub signature_delimiter: Option, - /// Define the signature. + /// Defines the signature. pub signature: Option, - /// Define the default page size for listings. + /// Defines the default page size for listings. pub default_page_size: Option, + /// Defines the inbox folder name. + pub inbox_folder: Option, + /// Defines the sent folder name. + pub sent_folder: Option, + /// Defines the draft folder name. + pub draft_folder: Option, + /// Defines the notify command. pub notify_cmd: Option, + /// Defines the watch commands. pub watch_cmds: Option>, + #[serde(flatten)] pub accounts: ConfigAccountsMap, } @@ -41,15 +50,23 @@ pub struct ConfigAccountEntry { pub signature_delimiter: Option, pub signature: Option, pub default_page_size: Option, + /// Defines a specific inbox folder name for this account. + pub inbox_folder: Option, + /// Defines a specific sent folder name for this account. + pub sent_folder: Option, + /// Defines a specific draft folder name for this account. + pub draft_folder: Option, pub watch_cmds: Option>, pub default: Option, pub email: String, + pub imap_host: String, pub imap_port: u16, pub imap_starttls: Option, pub imap_insecure: Option, pub imap_login: String, pub imap_passwd_cmd: String, + pub smtp_host: String, pub smtp_port: u16, pub smtp_starttls: Option, @@ -118,28 +135,8 @@ impl Config { .map(|cmd| format!(r#"{} {:?} {:?}"#, cmd, subject, sender)) .unwrap_or(default_cmd); + debug!("run command: {}", 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(()) } } diff --git a/src/domain/imap/imap_handler.rs b/src/domain/imap/imap_handler.rs index 2a43684..276c952 100644 --- a/src/domain/imap/imap_handler.rs +++ b/src/domain/imap/imap_handler.rs @@ -4,7 +4,10 @@ use anyhow::Result; -use crate::{config::Config, domain::imap::ImapServiceInterface}; +use crate::{ + config::{Account, Config}, + domain::imap::ImapServiceInterface, +}; /// Notify handler. pub fn notify<'a, ImapService: ImapServiceInterface<'a>>( @@ -12,13 +15,14 @@ pub fn notify<'a, ImapService: ImapServiceInterface<'a>>( config: &Config, imap: &mut ImapService, ) -> Result<()> { - imap.notify(&config, keepalive) + imap.notify(config, keepalive) } /// Watch handler. pub fn watch<'a, ImapService: ImapServiceInterface<'a>>( keepalive: u64, + account: &Account, imap: &mut ImapService, ) -> Result<()> { - imap.watch(keepalive) + imap.watch(account, keepalive) } diff --git a/src/domain/imap/imap_service.rs b/src/domain/imap/imap_service.rs index d51bb21..0587b51 100644 --- a/src/domain/imap/imap_service.rs +++ b/src/domain/imap/imap_service.rs @@ -8,20 +8,21 @@ use native_tls::{TlsConnector, TlsStream}; use std::{ collections::HashSet, convert::{TryFrom, TryInto}, - iter::FromIterator, net::TcpStream, + thread, }; use crate::{ config::{Account, Config}, domain::{Envelope, Envelopes, Flags, Mbox, Mboxes, Msg, RawEnvelopes, RawMboxes}, + output::run_cmd, }; type ImapSession = imap::Session>; pub trait ImapServiceInterface<'a> { 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; fn fetch_envelopes(&mut self, page_size: &usize, page: &usize) -> Result; fn fetch_envelopes_with( @@ -58,7 +59,7 @@ pub struct ImapService<'a> { impl<'a> ImapService<'a> { fn sess(&mut self) -> Result<&mut ImapSession> { - if let None = self.sess { + if self.sess.is_none() { debug!("create TLS builder"); debug!("insecure: {}", self.account.imap_insecure); 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 { + debug!("fetch envelopes"); + debug!("page size: {:?}", page_size); + debug!("page: {:?}", page); + let mbox = self.mbox.to_owned(); let last_seq = self .sess()? .select(&mbox.name) .context(format!(r#"cannot select mailbox "{}""#, self.mbox.name))? .exists as i64; + debug!("last sequence number: {:?}", last_seq); if last_seq == 0 { return Ok(Envelopes::default()); @@ -142,13 +148,14 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> { } else { String::from("1:*") }; + debug!("range: {:?}", range); let fetches = self .sess()? - .fetch(range, "(ENVELOPE FLAGS INTERNALDATE)") - .context(r#"cannot fetch messages within range "{}""#)?; + .fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)") + .context(format!(r#"cannot fetch messages within range "{}""#, range))?; 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( @@ -186,7 +193,7 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> { .fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)") .context(r#"cannot fetch messages within range "{}""#)?; 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. @@ -201,9 +208,9 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> { .context(r#"cannot fetch messages "{}""#)?; let fetch = fetches .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> { @@ -217,14 +224,14 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> { .context(r#"cannot fetch raw messages "{}""#)?; let fetch = fetches .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()) } fn append_raw_msg_with_flags(&mut self, mbox: &Mbox, msg: &[u8], flags: Flags) -> Result<()> { self.sess()? - .append(&mbox.name, &msg) + .append(&mbox.name, msg) .flags(flags.0) .finish() .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<()> { + debug!("notify"); + let mbox = self.mbox.to_owned(); - debug!("examine mailbox: {}", mbox.name); + debug!("examine mailbox {:?}", mbox); self.sess()? .examine(&mbox.name) - .context(format!("cannot examine mailbox `{}`", &self.mbox.name))?; + .context(format!("cannot examine mailbox {}", self.mbox.name))?; debug!("init messages hashset"); - let mut msgs_set: HashSet = - HashSet::from_iter(self.search_new_msgs()?.iter().cloned()); + let mut msgs_set: HashSet = self + .search_new_msgs()? + .iter() + .cloned() + .collect::>(); trace!("messages hashset: {:?}", msgs_set); loop { @@ -271,7 +283,7 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> { let uids: Vec = self .search_new_msgs()? .into_iter() - .filter(|uid| msgs_set.get(&uid).is_none()) + .filter(|uid| -> bool { msgs_set.get(uid).is_none() }) .collect(); debug!("found {} new messages not in hashset", uids.len()); 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); let mbox = self.mbox.to_owned(); @@ -330,8 +342,17 @@ impl<'a> ImapServiceInterface<'a> for ImapService<'a> { }) }) .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"); } } diff --git a/src/domain/mbox/mbox_arg.rs b/src/domain/mbox/mbox_arg.rs index 5469934..382cd34 100644 --- a/src/domain/mbox/mbox_arg.rs +++ b/src/domain/mbox/mbox_arg.rs @@ -47,13 +47,12 @@ pub fn source_arg<'a>() -> clap::Arg<'a, 'a> { .long("mailbox") .help("Specifies the source mailbox") .value_name("SOURCE") - .default_value("INBOX") } /// Defines the target mailbox argument. pub fn target_arg<'a>() -> clap::Arg<'a, 'a> { clap::Arg::with_name("mbox-target") - .help("Specifies the targetted mailbox") + .help("Specifies the targeted mailbox") .value_name("TARGET") .required(true) } @@ -104,7 +103,7 @@ mod tests { } 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"]; assert_eq!(Some("SOURCE"), app.value_of("mbox-source")); diff --git a/src/domain/mbox/mbox_handler.rs b/src/domain/mbox/mbox_handler.rs index ceb6db9..314227b 100644 --- a/src/domain/mbox/mbox_handler.rs +++ b/src/domain/mbox/mbox_handler.rs @@ -28,7 +28,7 @@ mod tests { use termcolor::ColorSpec; use crate::{ - config::Config, + config::{Account, Config}, domain::{AttrRemote, Attrs, Envelopes, Flags, Mbox, Mboxes, Msg}, output::{Print, PrintTable, WriteColor}, }; @@ -117,7 +117,7 @@ mod tests { fn notify(&mut self, _: &Config, _: u64) -> Result<()> { unimplemented!() } - fn watch(&mut self, _: u64) -> Result<()> { + fn watch(&mut self, _: &Account, _: u64) -> Result<()> { unimplemented!() } fn fetch_envelopes(&mut self, _: &usize, _: &usize) -> Result { diff --git a/src/domain/mbox/mboxes_entity.rs b/src/domain/mbox/mboxes_entity.rs index 4c437f0..e5a0130 100644 --- a/src/domain/mbox/mboxes_entity.rs +++ b/src/domain/mbox/mboxes_entity.rs @@ -32,7 +32,7 @@ impl<'a> Deref for Mboxes<'a> { impl<'a> PrintTable for Mboxes<'a> { fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { writeln!(writter)?; - Table::print(writter, &self, opts)?; + Table::print(writter, self, opts)?; writeln!(writter)?; Ok(()) } diff --git a/src/domain/msg/envelope_entity.rs b/src/domain/msg/envelope_entity.rs index 013e760..c3384f5 100644 --- a/src/domain/msg/envelope_entity.rs +++ b/src/domain/msg/envelope_entity.rs @@ -39,7 +39,7 @@ impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> { fn try_from(fetch: &'a RawEnvelope) -> Result { let envelope = fetch .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 let id = fetch.message; @@ -57,7 +57,7 @@ impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> { fetch.message )) }) - .unwrap_or(Ok(String::default()))? + .unwrap_or_else(|| Ok(String::default()))? .into(); // Get the sender @@ -66,7 +66,7 @@ impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> { .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 { rfc2047_decoder::decode(&name.to_vec()).context(format!( "cannot decode sender's name of message {}", @@ -76,10 +76,7 @@ impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> { let mbox = sender .mailbox .as_ref() - .ok_or(anyhow!( - "cannot get sender's mailbox of message {}", - fetch.message - )) + .ok_or_else(|| anyhow!("cannot get sender's mailbox of message {}", fetch.message)) .and_then(|mbox| { rfc2047_decoder::decode(&mbox.to_vec()).context(format!( "cannot decode sender's mailbox of message {}", @@ -89,10 +86,7 @@ impl<'a> TryFrom<&'a RawEnvelope> for Envelope<'a> { let host = sender .host .as_ref() - .ok_or(anyhow!( - "cannot get sender's host of message {}", - fetch.message - )) + .ok_or_else(|| anyhow!("cannot get sender's host of message {}", fetch.message)) .and_then(|host| { rfc2047_decoder::decode(&host.to_vec()).context(format!( "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 subject = &self.subject; let sender = &self.sender; - let date = self - .date - .as_ref() - .map(|date| date.as_str()) - .unwrap_or_default(); + let date = self.date.as_deref().unwrap_or_default(); Row::new() .cell(Cell::new(id).bold_if(unseen).red()) .cell(Cell::new(flags).bold_if(unseen).white()) diff --git a/src/domain/msg/envelopes_entity.rs b/src/domain/msg/envelopes_entity.rs index 6402317..802153f 100644 --- a/src/domain/msg/envelopes_entity.rs +++ b/src/domain/msg/envelopes_entity.rs @@ -39,7 +39,7 @@ impl<'a> TryFrom<&'a RawEnvelopes> for Envelopes<'a> { impl<'a> PrintTable for Envelopes<'a> { fn print_table(&self, writter: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { writeln!(writter)?; - Table::print(writter, &self, opts)?; + Table::print(writter, self, opts)?; writeln!(writter)?; Ok(()) } diff --git a/src/domain/msg/flags_entity.rs b/src/domain/msg/flags_entity.rs index 2ed3872..bc8590b 100644 --- a/src/domain/msg/flags_entity.rs +++ b/src/domain/msg/flags_entity.rs @@ -126,16 +126,14 @@ impl<'a> From> for Flags { let mut map: HashSet> = HashSet::new(); for f in flags { - match f { - "Answered" | _ if f.eq_ignore_ascii_case("answered") => map.insert(Flag::Answered), - "Deleted" | _ if f.eq_ignore_ascii_case("deleted") => map.insert(Flag::Deleted), - "Draft" | _ if f.eq_ignore_ascii_case("draft") => map.insert(Flag::Draft), - "Flagged" | _ if f.eq_ignore_ascii_case("flagged") => map.insert(Flag::Flagged), - "MayCreate" | _ if f.eq_ignore_ascii_case("maycreate") => { - map.insert(Flag::MayCreate) - } - "Recent" | _ if f.eq_ignore_ascii_case("recent") => map.insert(Flag::Recent), - "Seen" | _ if f.eq_ignore_ascii_case("seen") => map.insert(Flag::Seen), + match f.to_lowercase().as_str() { + "answered" => map.insert(Flag::Answered), + "deleted" => map.insert(Flag::Deleted), + "draft" => map.insert(Flag::Draft), + "flagged" => map.insert(Flag::Flagged), + "maycreate" => map.insert(Flag::MayCreate), + "recent" => map.insert(Flag::Recent), + "seen" => map.insert(Flag::Seen), custom => map.insert(Flag::Custom(Cow::Owned(custom.into()))), }; } diff --git a/src/domain/msg/msg_arg.rs b/src/domain/msg/msg_arg.rs index 989a8f4..4f91eac 100644 --- a/src/domain/msg/msg_arg.rs +++ b/src/domain/msg/msg_arg.rs @@ -200,11 +200,11 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { } 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") { - return Ok(Some(Command::Flag(flag_arg::matches(&m)?))); + return Ok(Some(Command::Flag(flag_arg::matches(m)?))); } debug!("default list command matched"); diff --git a/src/domain/msg/msg_entity.rs b/src/domain/msg/msg_entity.rs index edc6072..6676b2c 100644 --- a/src/domain/msg/msg_entity.rs +++ b/src/domain/msg/msg_entity.rs @@ -100,21 +100,43 @@ impl Msg { .tags(HashSet::default()) .clean(&html) .to_string(); - // Replace ` ` by regular space - let sanitized_html = Regex::new(r" ") - .unwrap() - .replace_all(&sanitized_html, " ") - .to_string(); // Merge new line chars - let sanitized_html = Regex::new(r"(\r?\n ?){2,}") + let sanitized_html = Regex::new(r"(\r?\n\s*){2,}") .unwrap() .replace_all(&sanitized_html, "\n\n") .to_string(); + // Replace tabulations and &npsp; by spaces + let sanitized_html = Regex::new(r"(\t| )") + .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 let sanitized_html = html_escape::decode_html_entities(&sanitized_html).to_string(); + sanitized_html } 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 .as_ref() .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 .reply_to .as_ref() - .or(self.from.as_ref()) + .or_else(|| self.from.as_ref()) .and_then(|addrs| addrs.first()) - .map(|addr| addr.name.to_owned().unwrap_or(addr.email.to_string())) - .unwrap_or("unknown sender".into()); + .map(|addr| { + 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 glue = ""; @@ -210,8 +236,8 @@ impl Msg { break; } content.push_str(glue); - content.push_str(">"); - content.push_str(if line.starts_with(">") { "" } else { " " }); + content.push('>'); + content.push_str(if line.starts_with('>') { "" } else { " " }); content.push_str(line); glue = "\n"; } @@ -239,7 +265,7 @@ impl Msg { self.in_reply_to = None; // From - self.from = Some(vec![account_addr.to_owned()]); + self.from = Some(vec![account_addr]); // To self.to = Some(vec![]); @@ -270,7 +296,7 @@ impl Msg { content.push_str(&addr.to_string()); glue = ", "; } - content.push_str("\n"); + content.push('\n'); } if let Some(addrs) = prev_to.as_ref() { content.push_str("To: "); @@ -280,9 +306,9 @@ impl Msg { content.push_str(&addr.to_string()); glue = ", "; } - content.push_str("\n"); + content.push('\n'); } - content.push_str("\n"); + content.push('\n'); content.push_str(&self.fold_text_parts("plain")); self.parts .replace_text_plain_parts_with(TextPlainPart { content }); @@ -337,7 +363,7 @@ impl Msg { loop { match choice::post_edit() { Ok(PostEditChoice::Send) => { - let mbox = Mbox::new("Sent"); + let mbox = Mbox::new(&account.sent_folder); let sent_msg = smtp.send_msg(&self)?; let flags = Flags::try_from(vec![Flag::Seen])?; imap.append_raw_msg_with_flags(&mbox, &sent_msg.formatted(), flags)?; @@ -354,12 +380,15 @@ impl Msg { break; } 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 tpl = self.to_tpl(TplOverride::default(), account); imap.append_raw_msg_with_flags(&mbox, tpl.as_bytes(), flags)?; msg_utils::remove_local_draft()?; - printer.print("Message successfully saved to Drafts")?; + printer.print(format!( + "Message successfully saved to {}", + account.draft_folder + ))?; break; } Ok(PostEditChoice::Discard) => { @@ -383,7 +412,7 @@ impl Msg { let path = PathBuf::from(path.to_string()); let filename: String = path .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() .into(); let content = fs::read(&path).context(format!("cannot read attachment {:?}", path))?; @@ -424,17 +453,11 @@ impl Msg { match part { Part::Binary(_) => self.parts.push(part), Part::TextPlain(_) => { - self.parts.retain(|p| match p { - Part::TextPlain(_) => false, - _ => true, - }); + self.parts.retain(|p| !matches!(p, Part::TextPlain(_))); self.parts.push(part); } Part::TextHtml(_) => { - self.parts.retain(|p| match p { - Part::TextHtml(_) => false, - _ => true, - }); + self.parts.retain(|p| !matches!(p, Part::TextHtml(_))); self.parts.push(part); } } @@ -504,7 +527,7 @@ impl Msg { )); // Headers <=> body separator - tpl.push_str("\n"); + tpl.push('\n'); // Body if let Some(body) = opts.body { @@ -522,7 +545,7 @@ impl Msg { tpl.push_str(sig); } - tpl.push_str("\n"); + tpl.push('\n'); trace!("template: {:#?}", tpl); tpl @@ -539,49 +562,45 @@ impl Msg { let val = String::from_utf8(header.get_value_raw().to_vec()) .map(|val| val.trim().to_string())?; - match key.as_str() { - "Message-Id" | _ if key.eq_ignore_ascii_case("message-id") => { - msg.message_id = Some(val.to_owned()) - } - "From" | _ if key.eq_ignore_ascii_case("from") => { + match key.to_lowercase().as_str() { + "message-id" => msg.message_id = Some(val.to_owned()), + "from" => { msg.from = Some( val.split(',') .filter_map(|addr| addr.parse().ok()) .collect::>(), ); } - "To" | _ if key.eq_ignore_ascii_case("to") => { + "to" => { msg.to = Some( val.split(',') .filter_map(|addr| addr.parse().ok()) .collect::>(), ); } - "Reply-To" | _ if key.eq_ignore_ascii_case("reply-to") => { + "reply-to" => { msg.reply_to = Some( val.split(',') .filter_map(|addr| addr.parse().ok()) .collect::>(), ); } - "In-Reply-To" | _ if key.eq_ignore_ascii_case("in-reply-to") => { - msg.in_reply_to = Some(val.to_owned()) - } - "Cc" | _ if key.eq_ignore_ascii_case("cc") => { + "in-reply-to" => msg.in_reply_to = Some(val.to_owned()), + "cc" => { msg.cc = Some( val.split(',') .filter_map(|addr| addr.parse().ok()) .collect::>(), ); } - "Bcc" | _ if key.eq_ignore_ascii_case("bcc") => { + "bcc" => { msg.bcc = Some( val.split(',') .filter_map(|addr| addr.parse().ok()) .collect::>(), ); } - "Subject" | _ if key.eq_ignore_ascii_case("subject") => { + "subject" => { 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 { let envelope = fetch .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 let id = fetch.message; @@ -711,13 +730,13 @@ impl<'a> TryFrom<&'a imap::types::Fetch> for Msg { fetch.message )) }) - .unwrap_or(Ok(String::default()))?; + .unwrap_or_else(|| Ok(String::default()))?; // Get the sender(s) address(es) let from = match envelope .sender - .as_ref() - .or_else(|| envelope.from.as_ref()) + .as_deref() + .or_else(|| envelope.from.as_deref()) .map(parse_addrs) { Some(addrs) => Some(addrs?), @@ -770,7 +789,7 @@ impl<'a> TryFrom<&'a imap::types::Fetch> for Msg { &mailparse::parse_mail( fetch .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))?, ); @@ -779,13 +798,13 @@ impl<'a> TryFrom<&'a imap::types::Fetch> for Msg { id, flags, subject, - message_id, from, reply_to, - in_reply_to, to, cc, bcc, + in_reply_to, + message_id, date, parts, }) @@ -799,20 +818,20 @@ pub fn parse_addr(addr: &imap_proto::Address) -> Result { .map(|name| { rfc2047_decoder::decode(&name.to_vec()) .context("cannot decode address name") - .map(|name| Some(name)) + .map(Some) }) .unwrap_or(Ok(None))?; let mbox = addr .mailbox .as_ref() - .ok_or(anyhow!("cannot get address mailbox")) + .ok_or_else(|| anyhow!("cannot get address mailbox")) .and_then(|mbox| { rfc2047_decoder::decode(&mbox.to_vec()).context("cannot decode address mailbox") })?; let host = addr .host .as_ref() - .ok_or(anyhow!("cannot get address host")) + .ok_or_else(|| anyhow!("cannot get address host")) .and_then(|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 { Ok(Addr::new(name, lettre::Address::new(mbox, host)?)) } -pub fn parse_addrs(addrs: &Vec) -> Result> { +pub fn parse_addrs(addrs: &[imap_proto::Address]) -> Result> { let mut parsed_addrs = vec![]; for addr in addrs { parsed_addrs @@ -830,7 +849,7 @@ pub fn parse_addrs(addrs: &Vec) -> Result> { } pub fn parse_some_addrs(addrs: &Option>) -> Result>> { - Ok(match addrs.as_ref().map(parse_addrs) { + Ok(match addrs.as_deref().map(parse_addrs) { Some(addrs) => Some(addrs?), None => None, }) diff --git a/src/domain/msg/msg_handler.rs b/src/domain/msg/msg_handler.rs index 88b152c..c93a5e4 100644 --- a/src/domain/msg/msg_handler.rs +++ b/src/domain/msg/msg_handler.rs @@ -21,6 +21,7 @@ use crate::{ mbox::Mbox, msg::{Flags, Msg, Part, TextPlainPart}, smtp::SmtpServiceInterface, + Parts, }, output::{PrintTableOpts, PrinterService}, }; @@ -32,7 +33,7 @@ pub fn attachments<'a, Printer: PrinterService, ImapService: ImapServiceInterfac printer: &mut Printer, imap: &mut ImapService, ) -> Result<()> { - let attachments = imap.find_msg(&seq)?.attachments(); + let attachments = imap.find_msg(seq)?.attachments(); let attachments_len = attachments.len(); debug!( r#"{} attachment(s) found for message "{}""#, @@ -60,7 +61,7 @@ pub fn copy<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>( imap: &mut ImapService, ) -> Result<()> { 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])?; imap.append_raw_msg_with_flags(&mbox, &msg, flags)?; printer.print(format!( @@ -135,7 +136,7 @@ pub fn mailto< ) -> Result<()> { let to: Vec = url .path() - .split(";") + .split(';') .filter_map(|s| s.parse().ok()) .collect(); 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) } @@ -185,7 +188,7 @@ pub fn move_<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>> ) -> Result<()> { // Copy the message to targetted mailbox 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])?; imap.append_raw_msg_with_flags(&mbox, &msg, flags)?; @@ -210,9 +213,9 @@ pub fn read<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>( ) -> Result<()> { let msg = if raw { // 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 { - imap.find_msg(&seq)?.fold_text_parts(text_mime) + imap.find_msg(seq)?.fold_text_parts(text_mime) }; printer.print(msg) @@ -254,8 +257,7 @@ pub fn save<'a, Printer: PrinterService, ImapService: ImapServiceInterface<'a>>( io::stdin() .lock() .lines() - .filter_map(|ln| ln.ok()) - .map(|ln| ln.to_string()) + .filter_map(Result::ok) .collect::>() .join("\r\n") }; @@ -290,6 +292,7 @@ pub fn send< SmtpService: SmtpServiceInterface, >( raw_msg: &str, + account: &Account, printer: &mut Printer, imap: &mut ImapService, smtp: &mut SmtpService, @@ -300,19 +303,18 @@ pub fn send< io::stdin() .lock() .lines() - .filter_map(|ln| ln.ok()) - .map(|ln| ln.to_string()) + .filter_map(Result::ok) .collect::>() .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()?; smtp.send_raw_msg(&envelope, raw_msg.as_bytes())?; debug!("message sent!"); // 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])?; imap.append_raw_msg_with_flags(&mbox, raw_msg.as_bytes(), flags) } diff --git a/src/domain/msg/parts_entity.rs b/src/domain/msg/parts_entity.rs index cc8d6e1..51e80be 100644 --- a/src/domain/msg/parts_entity.rs +++ b/src/domain/msg/parts_entity.rs @@ -39,24 +39,12 @@ pub struct Parts(pub Vec); impl Parts { pub fn replace_text_plain_parts_with(&mut self, part: TextPlainPart) { - self.retain(|part| { - if let Part::TextPlain(_) = part { - false - } else { - true - } - }); + self.retain(|part| !matches!(part, Part::TextPlain(_))); self.push(Part::TextPlain(part)); } pub fn replace_text_html_parts_with(&mut self, part: TextHtmlPart) { - self.retain(|part| { - if let Part::TextHtml(_) = part { - false - } else { - true - } - }); + self.retain(|part| !matches!(part, Part::TextHtml(_))); self.push(Part::TextHtml(part)); } } @@ -92,7 +80,7 @@ fn build_parts_map_rec(part: &mailparse::ParsedMail, parts: &mut Vec) { .params .get("filename") .map(String::from) - .unwrap_or(String::from("noname")); + .unwrap_or_else(|| String::from("noname")); let content = part.get_body_raw().unwrap_or_default(); let mime = tree_magic::from_u8(&content); parts.push(Part::Binary(BinaryPart { @@ -103,16 +91,14 @@ fn build_parts_map_rec(part: &mailparse::ParsedMail, parts: &mut Vec) { } // TODO: manage other use cases _ => { - part.get_headers() - .get_first_value("content-type") - .map(|ctype| { - let content = part.get_body().unwrap_or_default(); - if ctype.starts_with("text/plain") { - parts.push(Part::TextPlain(TextPlainPart { content })) - } else if ctype.starts_with("text/html") { - parts.push(Part::TextHtml(TextHtmlPart { content })) - } - }); + if let Some(ctype) = part.get_headers().get_first_value("content-type") { + let content = part.get_body().unwrap_or_default(); + if ctype.starts_with("text/plain") { + parts.push(Part::TextPlain(TextPlainPart { content })) + } else if ctype.starts_with("text/html") { + parts.push(Part::TextHtml(TextHtmlPart { content })) + } + }; } }; } else { diff --git a/src/main.rs b/src/main.rs index 2217de1..78738a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,4 @@ use anyhow::Result; -use clap; -use env_logger; use output::StdoutPrinter; use std::{convert::TryFrom, env}; use url::Url; @@ -36,6 +34,7 @@ fn create_app<'a>() -> clap::App<'a, 'a> { .subcommands(msg_arg::subcmds()) } +#[allow(clippy::single_match)] fn main() -> Result<()> { // Init env logger env_logger::init_from_env( @@ -45,9 +44,9 @@ fn main() -> Result<()> { // Check mailto command BEFORE app initialization. let raw_args: Vec = env::args().collect(); if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { - let mbox = Mbox::new("INBOX"); let config = Config::try_from(None)?; let account = Account::try_from((&config, None))?; + let mbox = Mbox::new(&account.inbox_folder); let mut printer = StdoutPrinter::from(OutputFmt::Plain); let url = Url::parse(&raw_args[1])?; let mut imap = ImapService::from((&account, &mbox)); @@ -68,9 +67,9 @@ fn main() -> Result<()> { } // Init entities and services. - let mbox = Mbox::new(m.value_of("mbox-source").unwrap()); let config = Config::try_from(m.value_of("config"))?; 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 imap = ImapService::from((&account, &mbox)); let mut smtp = SmtpService::from(&account); @@ -81,7 +80,7 @@ fn main() -> Result<()> { return imap_handler::notify(keepalive, &config, &mut imap); } 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)) => { - 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)) => { return msg_handler::write(atts, &account, &mut printer, &mut imap, &mut smtp); diff --git a/src/output/output_entity.rs b/src/output/output_entity.rs index e92b488..0accfe0 100644 --- a/src/output/output_entity.rs +++ b/src/output/output_entity.rs @@ -36,9 +36,9 @@ impl TryFrom> for OutputFmt { impl Display for OutputFmt { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let fmt = match self { - &OutputFmt::Json => "JSON", - &OutputFmt::Plain => "Plain", + let fmt = match *self { + OutputFmt::Json => "JSON", + OutputFmt::Plain => "Plain", }; write!(f, "{}", fmt) } diff --git a/src/output/print.rs b/src/output/print.rs index 023a678..62f65f6 100644 --- a/src/output/print.rs +++ b/src/output/print.rs @@ -9,7 +9,7 @@ pub trait Print { impl Print for &str { 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); "cannot write string to writter" }) diff --git a/src/ui/table.rs b/src/ui/table.rs index b1739d7..3763b18 100644 --- a/src/ui/table.rs +++ b/src/ui/table.rs @@ -227,13 +227,11 @@ where trace!("number of spaces added to shrinked value: {}", spaces_count); value.push_str(&" ".repeat(spaces_count)); cell.value = value; - cell.print(writter)?; } else { trace!("cell is not overflowing"); let spaces_count = cell_width - cell.unicode_width() + 1; trace!("number of spaces added to value: {}", spaces_count); cell.value.push_str(&" ".repeat(spaces_count)); - cell.print(writter)?; } } else { 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; trace!("number of spaces added to value: {}", spaces_count); cell.value.push_str(&" ".repeat(spaces_count)); - cell.print(writter)?; } + cell.print(writter)?; glue = Cell::new("│").ansi_256(8); } writeln!(writter)?; diff --git a/tests/imap_test.rs b/tests/imap_test.rs deleted file mode 100644 index bbd901c..0000000 --- a/tests/imap_test.rs +++ /dev/null @@ -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 = 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 ".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 "); - -// // TODO: search messages -// // TODO: read message (+ \Seen flag) -// // TODO: list message attachments -// // TODO: add/set/remove flags - -// // Logout -// imap_conn.logout(); -// } diff --git a/vim/README.md b/vim/README.md index ec28778..8dd2abe 100644 --- a/vim/README.md +++ b/vim/README.md @@ -49,6 +49,24 @@ let g:himalaya_telescope_preview_enabled = 0 Should enable telescope preview when picking a mailbox with the telescope provider. +### Contact completion + +```vim +let g:himalaya_complete_contact_cmd = '' +``` + +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 ``. + +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 ### List messages view diff --git a/vim/autoload/himalaya/msg.vim b/vim/autoload/himalaya/msg.vim index 8905296..32ba5bc 100644 --- a/vim/autoload/himalaya/msg.vim +++ b/vim/autoload/himalaya/msg.vim @@ -305,6 +305,37 @@ function! himalaya#msg#attachments() endtry 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 " https://newbedev.com/get-usable-window-width-in-vim-script @@ -326,9 +357,13 @@ function! s:bufwidth() return width - numwidth - foldwidth - signwidth endfunction +function! s:get_msg_id(line) + return matchstr(a:line, '[0-9]*') +endfunction + function! s:get_focused_msg_id() try - return s:trim(split(getline("."), "|")[0]) + return s:get_msg_id(getline(".")) catch throw "message not found" endtry @@ -336,7 +371,7 @@ endfunction function! s:get_focused_msg_ids(from, to) 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 throw "messages not found" endtry @@ -349,3 +384,14 @@ function! s:close_open_buffers(name) execute ":bwipeout " . buffer_to_close endfor 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 diff --git a/vim/doc/himalaya.txt b/vim/doc/himalaya.txt index de4a021..128fc16 100644 --- a/vim/doc/himalaya.txt +++ b/vim/doc/himalaya.txt @@ -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 provider. +------------------------------------------------------------------------------ +CONTACT COMPLETION +> + let g:himalaya_complete_contact_cmd = '' +< +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* diff --git a/vim/ftplugin/himalaya-msg-write.vim b/vim/ftplugin/himalaya-msg-write.vim index f9b1bee..d9dc050 100644 --- a/vim/ftplugin/himalaya-msg-write.vim +++ b/vim/ftplugin/himalaya-msg-write.vim @@ -3,6 +3,10 @@ setlocal foldexpr=himalaya#shared#thread#fold(v:lnum) setlocal foldmethod=expr setlocal startofline +if exists("g:himalaya_complete_contact_cmd") + setlocal completefunc=himalaya#msg#complete_contact +endif + augroup himalaya_write autocmd! * autocmd BufWriteCmd call himalaya#msg#draft_save() diff --git a/vim/lua/himalaya/mbox.lua b/vim/lua/himalaya/mbox.lua index 1a13a2d..bee0e3d 100644 --- a/vim/lua/himalaya/mbox.lua +++ b/vim/lua/himalaya/mbox.lua @@ -10,7 +10,7 @@ local previewers = require('telescope.previewers') local function preview_command(entry, bufnr) vim.api.nvim_buf_call(bufnr, function() 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) if not (success) then vim.cmd('redraw') diff --git a/wiki b/wiki deleted file mode 160000 index 9fbd490..0000000 --- a/wiki +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9fbd490bd4f42524cb0099e9914144375ea5514a