diff --git a/.github/ISSUE_TEMPLATE/do-not-open-issues-on-github.md b/.github/ISSUE_TEMPLATE/do-not-open-issues-on-github.md new file mode 100644 index 0000000..02c883e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/do-not-open-issues-on-github.md @@ -0,0 +1,17 @@ +--- +name: Do not open issues on GitHub +about: Instead send an email at ~soywod/pimalaya@todo.sr.ht +title: '' +labels: invalid +assignees: '' + +--- + +Himalaya is slowly migrating away from GitHub. The new bug tracker is +now on [sourcehut](https://sr.ht/). You can submit an issue either by: + +* Sending an email at + [~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht) + (it is the simplest since you do not need to create any account) +* Submitting [this form](https://todo.sr.ht/~soywod/pimalaya) (you + need a free sourcehut account) diff --git a/.github/workflows/deployment.yaml b/.github/workflows/deployment.yml similarity index 74% rename from .github/workflows/deployment.yaml rename to .github/workflows/deployment.yml index e4243fa..1e3d1f4 100644 --- a/.github/workflows/deployment.yaml +++ b/.github/workflows/deployment.yml @@ -21,7 +21,7 @@ jobs: release_name: ${{ github.ref }} draft: false prerelease: false - deploy_github: + deploy_linux_macos_windows_github: runs-on: ${{ matrix.os }} needs: create_release strategy: @@ -47,7 +47,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: check - - name: Build release + - name: Builds release uses: actions-rs/cargo@v1 with: command: build @@ -67,6 +67,26 @@ jobs: asset_path: himalaya.tar.gz asset_name: himalaya-${{ matrix.os_name }}.tar.gz asset_content_type: application/gzip + deploy_musl_github: + runs-on: ubuntu-latest + needs: create_release + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Build release + run: | + docker run -v "${PWD}:/volume" --rm -t clux/muslrust:stable cargo build --release + - name: Compress executable + run: tar czf himalaya.tar.gz -C target/x86_64-unknown-linux-musl/release himalaya + - name: Upload release asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_path: himalaya.tar.gz + asset_name: himalaya-musl.tar.gz + asset_content_type: application/gzip deploy_crates: runs-on: ubuntu-latest needs: create_release diff --git a/.github/workflows/nix-build.yaml b/.github/workflows/nix-build.yaml deleted file mode 100644 index 6eb21dd..0000000 --- a/.github/workflows/nix-build.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: nix-build - -on: - pull_request: - push: - branches: - - master - -jobs: - nix-build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2.3.4 - - uses: cachix/install-nix-action@v13 - with: - install_url: https://nixos-nix-install-tests.cachix.org/serve/i6laym9jw3wg9mw6ncyrk6gjx4l34vvx/install - install_options: '--tarball-url-prefix https://nixos-nix-install-tests.cachix.org/serve' - extra_nix_config: | - experimental-features = nix-command flakes - - run: nix develop -c rustc --version - - run: nix run . -- --version - - run: nix-build diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 0000000..46069ef --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,38 @@ +name: nix + +on: + pull_request: + push: + branches: + - master + +jobs: + nix-build: + runs-on: ubuntu-latest + steps: + - name: Checkouts code + uses: actions/checkout@v3 + + - name: Caches Nix store + uses: actions/cache@v3 + id: nix-cache + with: + path: /tmp/nix-cache + key: nix-${{ hashFiles('**/flake.*') }} + + - name: Installs Nix + uses: cachix/install-nix-action@v18 + with: + extra_nix_config: | + experimental-features = nix-command flakes + + - name: Imports Nix store cache + if: ${{ steps.nix-cache.outputs.cache-hit == 'true' }} + run: nix-store --import < /tmp/nix-cache + + - name: Builds the project + run: nix build + + - name: Exports Nix store cache + if: ${{ steps.nix-cache.outputs.cache-hit != 'true' }} + run: nix-store --export $(find /nix/store -maxdepth 1 -name '*-*') > /tmp/nix-cache diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yml similarity index 95% rename from .github/workflows/tests.yaml rename to .github/workflows/tests.yml index 3d57679..f315921 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yml @@ -26,7 +26,7 @@ jobs: -p 3465:3465 \ -p 3993:3993 \ -p 3995:3995 \ - greenmail/standalone:1.6.2 + greenmail/standalone:1.6.11 - name: Install rust uses: actions-rs/toolchain@v1 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 00c3c61..f62f4bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.7.0] - 2023-02-08 + +### Added + +* Added offline support with the `account sync` command to synchronize + a backend to a local Maildir backend [#342]. +* Added the flag `--disable-cache` to not use the local Maildir + backend. +* Added the email composer (from its own + [repository](https://git.sr.ht/~soywod/mime-msg-builder)) [#341]. +* Added Musl builds to releases [#356]. +* Added `himalaya man` command to generate man page [#419]. + +### Changed + +* Made commands `read`, `attachments`, `flags`, `copy`, `move`, + `delete` accept multiple ids. +* Flipped arguments `ids` and `folder` for commands `copy` and `move` + in order the folder not to be considered as an id. + +### Fixed + +* Fixed missing folder aliases [#430]. + +### Removed + +* Removed the `-a|--attachment` argument from `write`, `reply` and + `forward` commands. Instead you can attach documents directly from + the template using the syntax `<#part + filename=/path/to/you/document.ext>`. +* Removed the `-e|--encrypt` flag from `write`, `reply` and `forward` + commands. Instead you can encrypt and sign parts directly from the + template using the syntax `<#part type=text/plain encrypt=command + sign=command>Hello!<#/part>`. +* Removed the `-l|--log-level` option, use instead the `RUST_LOG` + environment variable (see the + [wiki](https://github.com/soywod/himalaya/wiki/Tips:debug-and-logs)) + ## [0.6.1] - 2022-10-12 ### Added @@ -436,7 +474,9 @@ 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.6.1...HEAD +[Unreleased]: https://github.com/soywod/himalaya/compare/v0.7.0...HEAD +[0.7.0]: https://github.com/soywod/himalaya/compare/v0.6.2...v0.7.0 +[0.6.2]: https://github.com/soywod/himalaya/compare/v0.6.1...v0.6.2 [0.6.1]: https://github.com/soywod/himalaya/compare/v0.6.0...v0.6.1 [0.6.0]: https://github.com/soywod/himalaya/compare/v0.5.10...v0.6.0 [0.5.10]: https://github.com/soywod/himalaya/compare/v0.5.9...v0.5.10 @@ -592,6 +632,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#335]: https://github.com/soywod/himalaya/issues/335 [#338]: https://github.com/soywod/himalaya/issues/338 [#340]: https://github.com/soywod/himalaya/issues/340 +[#341]: https://github.com/soywod/himalaya/issues/341 +[#342]: https://github.com/soywod/himalaya/issues/342 [#344]: https://github.com/soywod/himalaya/issues/344 [#346]: https://github.com/soywod/himalaya/issues/346 [#352]: https://github.com/soywod/himalaya/issues/352 +[#356]: https://github.com/soywod/himalaya/issues/356 +[#419]: https://github.com/soywod/himalaya/issues/419 +[#430]: https://github.com/soywod/himalaya/issues/430 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc72118..edf467d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,41 +2,47 @@ Thank you for investing your time in contributing to Himalaya! -In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR. +## Development -## New contributor guide +The development environment is managed by +[Nix](https://nixos.org/download.html). Running `nix-shell` will spawn +a shell with everything you need to get started with the tool: +`cargo`, `cargo-watch`, `rust-bin`, `rust-analyzer`… -To get an overview of the project, read the [README](README.md). To get more information about the project, read the [wiki](https://github.com/soywod/himalaya/wiki). +```sh +# starts a nix shell (the first launch may take a while) +$ nix-shell -## Getting started +# builds the CLI +$ cargo build -### Issues +# runs the CLI +$ cargo run -- list +``` -#### Create a new issue +## Contributing -If you spot a problem with the docs, [search if an issue already exists](https://github.com/soywod/himalaya/issues). If a related issue doesn't exist, you can open a new issue using a relevant [issue form](https://github.com/soywod/himalaya/issues/new/choose). +If you find a **bug**, please send an email at +[~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht). -#### Solve an issue +If you have a **question**, please send an email at +[~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht). -Scan through our [existing issues](https://github.com/soywod/himalaya/issues) to find one that interests you. You can narrow down the search using `labels` as filters. If you find an issue to work on, you are welcome to open a PR with a fix. +If you want to **propose a feature** or **fix a bug**, please send a +patch at +[~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht) +using [git send-email](https://git-scm.com/docs/git-send-email) (see +[this guide](https://git-send-email.io/) on how to configure it). -### Make Changes +If you want to **subscribe** to the mailing list, please send an email +at +[~soywod/pimalaya+subscribe@lists.sr.ht](mailto:~soywod/pimalaya+subscribe@lists.sr.ht). -#### Make changes in the UI +If you want to **unsubscribe** to the mailing list, please send an +email at +[~soywod/pimalaya+unsubscribe@lists.sr.ht](mailto:~soywod/pimalaya+unsubscribe@lists.sr.ht). -Click **Make a contribution** at the bottom of any docs page to make small changes such as a typo, sentence fix, or a broken link. This takes you to the `.md` file where you can make your changes and [create a pull request](#pull-request) for a review. - -#### Make changes locally - -First, follow the instructions on [how to install Himalaya from sources](https://github.com/soywod/himalaya/wiki/Installation:sources). Then, create a working branch and start with your changes! - -### Commit your update - -Commit the changes once you are happy with them. Commit messages follow the [Angular Convention](https://gist.github.com/stephenparish/9941e89d80e2bc58a153), but contain only a subject. The subject can be prefixed with a custom context like `msg: `, `mbox: `, `imap: ` etc. - - > Use imperative, present tense: “change” not “changed” nor - > “changes”
Don't capitalize first letter
No dot (.) at the end - -### Pull Request - -When you're finished with the changes, create a pull request, also known as a PR. +If you want to **discuss** about the project, feel free to join the +[Matrix](https://matrix.org/) workspace +[#pimalaya](https://matrix.to/#/#pimalaya:matrix.org) or contact me +directly [@soywod](https://matrix.to/#/@soywod:matrix.org). diff --git a/Cargo.lock b/Cargo.lock index 5e42379..4e4e908 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + [[package]] name = "ahash" version = "0.3.8" @@ -12,19 +18,36 @@ dependencies = [ ] [[package]] -name = "aho-corasick" -version = "0.7.19" +name = "ahash" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr 2.5.0", ] [[package]] -name = "ammonia" -version = "3.2.1" +name = "aliasable" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b477377562f3086b7778d241786e9406b883ccfaa03557c0fe0924b9349f13a" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "ammonia" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e6d1c7838db705c9b756557ee27c384ce695a1c51a6fe528784cb1c6840170" dependencies = [ "html5ever", "maplit", @@ -42,20 +65,11 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anyhow" -version = "1.0.65" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" +checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" [[package]] name = "atty" @@ -63,7 +77,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] @@ -76,9 +90,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "bitflags" @@ -86,18 +100,6 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "bitvec" -version = "0.19.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "bufstream" version = "0.1.4" @@ -106,15 +108,15 @@ checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" [[package]] name = "bumpalo" -version = "3.11.0" +version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" [[package]] name = "cc" -version = "1.0.73" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" [[package]] name = "cfg-if" @@ -140,9 +142,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" dependencies = [ "iana-time-zone", "js-sys", @@ -159,21 +161,58 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d02796e4586c6c41aeb68eae9bfb4558a522c35f1430c14b40136c3706e09e4" dependencies = [ - "ahash", + "ahash 0.3.8", +] + +[[package]] +name = "chumsky" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4d619fba796986dd538d82660b76e0b9756c6e19b2e4d4559ba5a57f9f00810" +dependencies = [ + "hashbrown", + "stacker", ] [[package]] name = "clap" -version = "2.34.0" +version = "4.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76" dependencies = [ - "ansi_term", - "atty", "bitflags", - "strsim 0.8.0", - "textwrap", - "unicode-width", + "clap_lex", + "is-terminal", + "strsim 0.10.0", + "termcolor", +] + +[[package]] +name = "clap_complete" +version = "4.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7b3c9eae0de7bf8e3f904a5e40612b21fb2e2e566456d177809a48b892d24da" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_lex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "clap_mangen" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e503c3058af0a0854668ea01db55c622482a080092fede9dd2e00a00a9436504" +dependencies = [ + "clap", + "roff", ] [[package]] @@ -196,10 +235,24 @@ dependencies = [ ] [[package]] -name = "const-random" -version = "0.1.13" +name = "console" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f590d95d011aa80b063ffe3253422ed5aa462af4e9867d43ce8337562bac77c4" +checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "terminal_size", + "unicode-width", + "winapi", +] + +[[package]] +name = "const-random" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368a7a772ead6ce7e1de82bfb04c485f3db8ec744f72925af5735e29a22cc18e" dependencies = [ "const-random-macro", "proc-macro-hack", @@ -207,12 +260,12 @@ dependencies = [ [[package]] name = "const-random-macro" -version = "0.1.13" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "615f6e27d000a2bffbc7f2f6a8669179378fa27ee4d0a509e985dfc0a7defb40" +checksum = "9d7d6ab3c3a2282db210df5f02c4dab6e0a7057af0fb7ebd4070f30fe05c0ddb" dependencies = [ "getrandom", - "lazy_static", + "once_cell", "proc-macro-hack", "tiny-keccak", ] @@ -239,6 +292,49 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +dependencies = [ + "autocfg", + "cfg-if 1.0.0", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -247,9 +343,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "cxx" -version = "1.0.78" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f39818dcfc97d45b03953c1292efc4e80954e1583c4aa770bac1383e2310a4" +checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453" dependencies = [ "cc", "cxxbridge-flags", @@ -259,9 +355,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.78" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e580d70777c116df50c390d1211993f62d40302881e54d4b79727acb83d0199" +checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0" dependencies = [ "cc", "codespan-reporting", @@ -274,15 +370,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.78" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56a46460b88d1cec95112c8c363f0e2c39afdb237f60583b0b36343bf627ea9c" +checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71" [[package]] name = "cxxbridge-macro" -version = "1.0.78" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747b608fecf06b0d72d440f27acc99288207324b793be2c17991839f3d4995ea" +checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470" dependencies = [ "proc-macro2", "quote", @@ -295,8 +391,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.10.2", + "darling_macro 0.10.2", +] + +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core 0.13.4", + "darling_macro 0.13.4", ] [[package]] @@ -313,22 +419,58 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn", +] + [[package]] name = "darling_macro" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" dependencies = [ - "darling_core", + "darling_core 0.10.2", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core 0.13.4", "quote", "syn", ] [[package]] name = "data-encoding" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" +checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" + +[[package]] +name = "dialoguer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92e7e37ecef6857fdc0c0c5d42fd5b0938e46590c2183cc92dd310a6d078eb1" +dependencies = [ + "console", + "tempfile", + "zeroize", +] [[package]] name = "dirs" @@ -350,6 +492,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + [[package]] name = "email-encoding" version = "0.1.3" @@ -362,9 +510,18 @@ dependencies = [ [[package]] name = "email_address" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1b32a7a2580c4473f10f66b512c34bdd7d33c5e3473227ca833abdb5afe4809" +checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" +dependencies = [ + "serde", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" @@ -397,6 +554,39 @@ dependencies = [ "serde", ] +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "1.8.0" @@ -457,17 +647,21 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e08079fa3c89edec9160ceaa9e7172785468c26c053d12924cce0d5a55c241a" dependencies = [ - "darling", + "darling 0.10.2", "proc-macro2", "quote", "syn", ] [[package]] -name = "funty" -version = "1.1.0" +name = "fs2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] [[package]] name = "futf" @@ -481,27 +675,27 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" [[package]] name = "futures-io" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" +checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" [[package]] name = "futures-task" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" [[package]] name = "futures-util" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" dependencies = [ "futures-core", "futures-io", @@ -524,9 +718,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if 1.0.0", "libc", @@ -538,6 +732,18 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.6", +] + +[[package]] +name = "hashlink" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" +dependencies = [ + "hashbrown", +] [[package]] name = "hermit-abi" @@ -548,20 +754,34 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + [[package]] name = "himalaya" -version = "0.6.1" +version = "0.7.0" dependencies = [ "anyhow", "atty", - "chrono", "clap", + "clap_complete", + "clap_mangen", + "console", + "dialoguer", + "dirs", + "email_address", "env_logger", "erased-serde", "himalaya-lib", - "lettre", + "indicatif", "log", - "mailparse", + "once_cell", "serde", "serde_json", "shellexpand", @@ -576,11 +796,14 @@ dependencies = [ [[package]] name = "himalaya-lib" -version = "0.4.0" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96448bba297a565c27dc18404da17d465e79df2feffa632b249281c8c0b5b68" dependencies = [ "ammonia", "chrono", "convert_case", + "dirs", "html-escape", "imap", "imap-proto", @@ -589,14 +812,20 @@ dependencies = [ "maildir", "mailparse", "md5", + "mime-msg-builder", "native-tls", "notmuch", + "ouroboros", + "proc-lock", + "rayon", "regex", "rfc2047-decoder", + "rusqlite", "serde", "shellexpand", "thiserror", "tree_magic", + "urlencoding", "utf7-imap", "uuid", ] @@ -614,9 +843,9 @@ dependencies = [ [[package]] name = "html-escape" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e7479fa1ef38eb49fb6a42c426be515df2d063f06cb8efd3e50af073dbc26c" +checksum = "15315cfa9503e9aa85a477138eff76a1b203a430703548052c330b69d8d8c205" dependencies = [ "utf8-width", ] @@ -649,9 +878,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "iana-time-zone" -version = "0.1.51" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a6ef98976b22b3b7f2f3a806f858cb862044cfa66805aa3ad84cb3d3b785ed" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -663,9 +892,9 @@ dependencies = [ [[package]] name = "iana-time-zone-haiku" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde6edd6cef363e9359ed3c98ba64590ba9eecba2293eb5a723ab32aee8926aa" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" dependencies = [ "cxx", "cxx-build", @@ -700,9 +929,9 @@ dependencies = [ [[package]] name = "imap" -version = "3.0.0-alpha.4" +version = "3.0.0-alpha.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b26b6f9b2c28b6aa7fabf623e75a8b6bd382ee0951d0e052c1d32c887150fb67" +checksum = "b1c2ff52273d9cd791687b4510d8a0047277e985a348e411c94fe84e193e7a76" dependencies = [ "base64", "bufstream", @@ -710,29 +939,42 @@ dependencies = [ "imap-proto", "lazy_static", "native-tls", - "nom 6.1.2", + "nom 7.1.1", + "ouroboros", "regex", ] [[package]] name = "imap-proto" -version = "0.14.3" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ad9b46a79efb6078e578ae04e51463d7c3e8767864687f7e63095b3cbefafbb" +checksum = "f73b1b63179418b20aa81002d616c5f21b4ba257da9bca6989ea64dc573933e0" dependencies = [ - "nom 6.1.2", + "nom 7.1.1", ] [[package]] name = "indexmap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", "hashbrown", ] +[[package]] +name = "indicatif" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef509aa9bc73864d6756f0d34d35504af3cf0844373afe9b8669a5b8005a729" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", +] + [[package]] name = "instant" version = "0.1.12" @@ -742,6 +984,28 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "io-lifetimes" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" +dependencies = [ + "libc", + "windows-sys 0.42.0", +] + +[[package]] +name = "is-terminal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330" +dependencies = [ + "hermit-abi 0.2.6", + "io-lifetimes", + "rustix", + "windows-sys 0.42.0", +] + [[package]] name = "itoa" version = "1.0.4" @@ -765,9 +1029,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lettre" -version = "0.10.0-rc.7" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f7e87d9d44162eea7abd87b1a7540fcb10d5e58e8bb4f173178f3dc6e453944" +checksum = "2eabca5e0b4d0e98e7f2243fb5b7520b6af2b65d8f87bcc86f2c75185a6ff243" dependencies = [ "base64", "email-encoding", @@ -788,9 +1052,20 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.135" +version = "0.2.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" + +[[package]] +name = "libsqlite3-sys" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] [[package]] name = "link-cplusplus" @@ -801,6 +1076,12 @@ dependencies = [ "cc", ] +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + [[package]] name = "lock_api" version = "0.3.4" @@ -837,9 +1118,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "maildir" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8e36818ad465a26adc92409158e147e74644f217e0fa77adec6a20c64ee45b5" +checksum = "5d2d08d52a925272eda99f8fe9e91237b1cb958804ee0628cc398ebd1bbc426f" dependencies = [ "gethostname", "mailparse", @@ -847,9 +1128,9 @@ dependencies = [ [[package]] name = "mailparse" -version = "0.13.8" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cae768a50835557749599277fc59f7c728118724eb34185e8feb633ef266a32" +checksum = "6b56570f5f8c0047260d1c8b5b331f62eb9c660b9dd4071a8c46f8c7d3f280aa" dependencies = [ "charset", "data-encoding", @@ -909,12 +1190,38 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime-msg-builder" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3981dce6db3e7f9faa1124409a6b94436902ecb2670f374d361789d61eb34ac" +dependencies = [ + "ammonia", + "chumsky 0.9.0", + "html-escape", + "lettre", + "log", + "regex", + "shellexpand", + "thiserror", + "tree_magic", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -923,9 +1230,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "native-tls" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" dependencies = [ "lazy_static", "libc", @@ -954,18 +1261,6 @@ dependencies = [ "memchr 1.0.2", ] -[[package]] -name = "nom" -version = "6.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" -dependencies = [ - "bitvec", - "funty", - "memchr 2.5.0", - "version_check", -] - [[package]] name = "nom" version = "7.1.1" @@ -978,9 +1273,9 @@ dependencies = [ [[package]] name = "notmuch" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca0941fd9af5b8529e3d42494f56efafb909b76190a7a454cde9d6e397390cf9" +checksum = "e25d11a2449f4f91cb71b138b241db30765a3b2f595eba0dd6a282b0e961dd44" dependencies = [ "from_variants", "libc", @@ -1006,16 +1301,32 @@ dependencies = [ ] [[package]] -name = "once_cell" +name = "num_cpus" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "openssl" -version = "0.10.42" +version = "0.10.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13" +checksum = "020433887e44c27ff16365eaa2d380547a94544ad509aff6eb5b6e3e0b27b376" dependencies = [ "bitflags", "cfg-if 1.0.0", @@ -1045,9 +1356,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.76" +version = "0.9.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5230151e44c0f05157effb743e8d517472843121cf9243e8b81393edb5acd9ce" +checksum = "07d5c8cb6e57b3a3612064d7b18b117912b4ce70955c2504d4b741c9e244b132" dependencies = [ "autocfg", "cc", @@ -1056,6 +1367,35 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "os_str_bytes" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" + +[[package]] +name = "ouroboros" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbb50b356159620db6ac971c6d5c9ab788c9cc38a6f49619fca2a27acb062ca" +dependencies = [ + "aliasable", + "ouroboros_macro", +] + +[[package]] +name = "ouroboros_macro" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0d9d1a6191c4f391f87219d1ea42b23f09ee84d64763cd05ee6ea88d9f384d" +dependencies = [ + "Inflector", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "parking_lot" version = "0.10.2" @@ -1073,7 +1413,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api 0.4.9", - "parking_lot_core 0.9.3", + "parking_lot_core 0.9.5", ] [[package]] @@ -1092,15 +1432,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall 0.2.16", "smallvec", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -1171,15 +1511,21 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "portable-atomic" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b" [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "precomputed-hash" @@ -1187,6 +1533,62 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "proc-lock" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cb4a8a82dcf0994c4bc5c14577b3449f606e0ec67af461842418ea08b687e79" +dependencies = [ + "proc-lock-api", + "proc-lock-macro", +] + +[[package]] +name = "proc-lock-api" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f278b068504592f8ae5781cec618cd3e3a26f69f5bf24befc3114aab138ad4f8" +dependencies = [ + "fs2", +] + +[[package]] +name = "proc-lock-macro" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58d0f2d2df050b4bbaf9ae33124d339ee1d6b8f8ea0f2ee4108b1658a18d2af0" +dependencies = [ + "darling 0.13.4", + "proc-lock-api", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -1195,13 +1597,22 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +dependencies = [ + "cc", +] + [[package]] name = "quote" version = "1.0.21" @@ -1213,15 +1624,9 @@ dependencies = [ [[package]] name = "quoted_printable" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fee2dce59f7a43418e3382c766554c614e06a552d53a8f07ef499ea4b332c0f" - -[[package]] -name = "radium" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" +checksum = "20f14e071918cbeefc5edc986a7aa92c425dae244e003a35e1cdddb5ca39b5cb" [[package]] name = "rand" @@ -1253,6 +1658,28 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "redox_syscall" version = "0.1.57" @@ -1281,9 +1708,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" dependencies = [ "aho-corasick", "memchr 2.5.0", @@ -1292,9 +1719,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "remove_dir_all" @@ -1313,12 +1740,46 @@ checksum = "11347d014ae34e1d367aaf9191c37bf071b161e2e1c09c8559c7717e87030e11" dependencies = [ "base64", "charset", - "chumsky", + "chumsky 0.8.0", "memchr 2.5.0", "quoted_printable", "thiserror", ] +[[package]] +name = "roff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" + +[[package]] +name = "rusqlite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustix" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.42.0", +] + [[package]] name = "ryu" version = "1.0.11" @@ -1332,7 +1793,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" dependencies = [ "lazy_static", - "windows-sys", + "windows-sys 0.36.1", ] [[package]] @@ -1372,18 +1833,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.145" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" +checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.145" +version = "1.0.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" +checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c" dependencies = [ "proc-macro2", "quote", @@ -1392,9 +1853,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.86" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074" +checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ "itoa", "ryu", @@ -1441,6 +1902,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "stacker" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" +dependencies = [ + "cc", + "cfg-if 1.0.0", + "libc", + "psm", + "winapi", +] + [[package]] name = "string_cache" version = "0.8.4" @@ -1467,12 +1941,6 @@ dependencies = [ "quote", ] -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - [[package]] name = "strsim" version = "0.9.3" @@ -1480,22 +1948,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" [[package]] -name = "syn" -version = "1.0.102" +name = "strsim" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae548ec36cf198c0ef7710d3c230987c2d6d7bd98ad6edc0274462724c585ce" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "tempfile" version = "3.3.0" @@ -1540,15 +2008,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - [[package]] name = "thiserror" version = "1.0.37" @@ -1571,9 +2030,9 @@ dependencies = [ [[package]] name = "time" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" dependencies = [ "libc", "wasi 0.10.0+wasi-snapshot-preview1", @@ -1664,6 +2123,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" + [[package]] name = "utf-8" version = "0.7.6" @@ -1811,37 +2276,88 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", ] +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.0", + "windows_i686_gnu 0.42.0", + "windows_i686_msvc 0.42.0", + "windows_x86_64_gnu 0.42.0", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + [[package]] name = "windows_aarch64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + [[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + [[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + [[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + [[package]] name = "windows_x86_64_msvc" version = "0.36.1" @@ -1849,7 +2365,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" [[package]] -name = "wyz" -version = "0.2.0" +name = "windows_x86_64_msvc" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + +[[package]] +name = "zeroize" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" diff --git a/Cargo.toml b/Cargo.toml index ed6c52c..25b3b89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "himalaya" description = "Command-line interface for email management." -version = "0.6.1" +version = "0.7.0" authors = ["soywod "] edition = "2021" license = "MIT" @@ -22,25 +22,30 @@ notmuch-backend = ["himalaya-lib/notmuch-backend"] default = ["imap-backend", "maildir-backend"] [dev-dependencies] -tempfile = "3.3.0" +tempfile = "3.3" [dependencies] -anyhow = "1.0.44" -atty = "0.2.14" -chrono = "0.4.19" -clap = { version = "2.33.3", default-features = false, features = ["suggestions", "color"] } -env_logger = "0.8.3" -erased-serde = "0.3.18" -himalaya-lib = "=0.4.0" -lettre = { version = "=0.10.0-rc.7", features = ["serde"] } -log = "0.4.14" -mailparse = "0.13.6" -serde = { version = "1.0.118", features = ["derive"] } -serde_json = "1.0.61" -shellexpand = "2.1.0" +anyhow = "1.0" +atty = "0.2" +clap = "4.0" +clap_complete = "4.0" +clap_mangen = "0.2" +console = "0.15.2" +dirs = "4.0.0" +dialoguer = "0.10.2" +email_address = "0.2.4" +env_logger = "0.8" +erased-serde = "0.3" +himalaya-lib = "0.5" +indicatif = "0.17" +log = "0.4" +once_cell = "1.16.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +shellexpand = "2.1" termcolor = "1.1" -terminal_size = "0.1.15" -toml = "0.5.8" -unicode-width = "0.1.7" -url = "2.2.2" +terminal_size = "0.1" +toml = "0.5" +unicode-width = "0.1" +url = "2.2" uuid = { version = "0.8", features = ["v4"] } diff --git a/README.md b/README.md index f80550c..0305985 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,66 @@ -# 📫 Himalaya +# 📫 Himalaya [![GitHub release](https://img.shields.io/github/v/release/soywod/himalaya?color=success&style=flat-square)](https://github.com/soywod/himalaya/releases/latest) [![Matrix](https://img.shields.io/matrix/himalaya.email.client:matrix.org?color=success&label=chat&style=flat-square)](https://matrix.to/#/#himalaya.email.client:matrix.org) Command-line interface for email management based on the [himalaya-lib](https://git.sr.ht/~soywod/himalaya-lib). ![image](https://user-images.githubusercontent.com/10437171/138774902-7b9de5a3-93eb-44b0-8cfb-6d2e11e3b1aa.png) -*The project is under active development. Do not use in production -before the `v1.0.0`.* +*Warning: the project is under active development, do not use in +production before the `v1.0.0`.* + +## Features + +- Folder listing +- Email listing and searching +- Email composition based on `$EDITOR` +- Email manipulation (copy/move/delete) +- Multi-accounting +- Account listing +- IMAP, Maildir and Notmuch support +- IMAP IDLE mode for real-time notifications +- PGP end-to-end encryption +- Completions for various shells +- JSON output +- … + +*Note: see the [wiki](https://github.com/soywod/himalaya/wiki) for all +the features.* ## Installation -[![Packaging -status](https://repology.org/badge/vertical-allrepos/himalaya.svg)](https://repology.org/project/himalaya/versions) + + + + + +
+ +Packaging status + + -```sh -curl -sSL https://raw.githubusercontent.com/soywod/himalaya/master/install.sh | PREFIX=~/.local sh +```shell +# Arch Linux (official) +$ pacman -S himalaya + +# Arch Linux (from sources) +$ yay -S himalaya-git + +# Homebrew +$ brew install himalaya + +# Cargo +$ cargo install himalaya + +# Nix +$ nix-env -i himalaya ``` -*See the -[wiki](https://github.com/soywod/himalaya/wiki/Installation:binary) -for other installation methods.* +*Note: see the +[wiki](https://github.com/soywod/himalaya/wiki/Installation) for other +installation methods.* + +
## Configuration @@ -34,61 +75,96 @@ signature = "Regards," default = true email = "test@gmail.com" -backend = "imap" # imap, maildir or notmuch +backend = "imap" imap-host = "imap.gmail.com" imap-port = 993 imap-login = "test@gmail.com" -imap-passwd-cmd = "pass show gmail" +imap-passwd-cmd = "security find-internet-password -gs gmail -w" -sender = "smtp" # smtp or sendmail +sender = "smtp" smtp-host = "smtp.gmail.com" smtp-port = 465 smtp-login = "test@gmail.com" smtp-passwd-cmd = "security find-internet-password -gs gmail -w" + +[gmail.folder-aliases] +inbox = "INBOX" +sent = "[Gmail]/Sent" +drafts = "[Gmail]/Drafts" + +[local] +email = "test@localhost" +signature-delim = "~~\n" +signature = "Regards," + +backend = "maildir" +maildir-root-dir = "~/emails" + +sender = "sendmail" +sendmail-cmd = "msmtp --read-envelope-from --read-recipients" ``` -*See the -[wiki](https://github.com/soywod/himalaya/wiki/Configuration:config-file) -for all the options.* +*Note: see the +[wiki](https://github.com/soywod/himalaya/wiki/Configuration) for all +the options.* -## Features +## Contributing -- Folder listing -- Email listing and searching -- Email composition based on `$EDITOR` -- Email manipulation (copy/move/delete) -- Multi-accounting -- Account listing -- IMAP, Maildir and Notmuch support -- IMAP IDLE mode for real-time notifications -- PGP end-to-end encryption -- Vim and Emacs plugins -- Completions for various shells -- JSON output -- … +If you find a **bug**, please send an email at +[~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht). -*See the -[wiki](https://github.com/soywod/himalaya/wiki/Usage:email:list) for -all the features.* +If you have a **question**, please send an email at +[~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht). + +If you want to **propose a feature** or **fix a bug**, please send a +patch at +[~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht) +using [git send-email](https://git-scm.com/docs/git-send-email) (see +[this guide](https://git-send-email.io/) on how to configure it). + +If you want to **subscribe** to the mailing list, please send an email +at +[~soywod/pimalaya+subscribe@lists.sr.ht](mailto:~soywod/pimalaya+subscribe@lists.sr.ht). + +If you want to **unsubscribe** to the mailing list, please send an +email at +[~soywod/pimalaya+unsubscribe@lists.sr.ht](mailto:~soywod/pimalaya+unsubscribe@lists.sr.ht). + +If you want to **discuss** about the project, feel free to join the +[Matrix](https://matrix.org/) workspace +[#pimalaya](https://matrix.to/#/#pimalaya:matrix.org) or contact me +directly [@soywod](https://matrix.to/#/@soywod:matrix.org). ## Credits -- [himalaya-lib](https://git.sr.ht/~soywod/himalaya-lib) -- [IMAP RFC3501](https://tools.ietf.org/html/rfc3501) -- [Iris](https://github.com/soywod/iris.vim), the himalaya predecessor -- [isync](https://isync.sourceforge.io/), an email synchronizer for +[![nlnet](https://nlnet.nl/logo/banner-160x60.png)](https://nlnet.nl/project/Himalaya/index.html) + +Special thanks to the +[nlnet](https://nlnet.nl/project/Himalaya/index.html) foundation that +helped Himalaya to receive financial support from the [NGI +Assure](https://www.ngi.eu/ngi-projects/ngi-assure/) program of the +European Commission in September, 2022. + +* [himalaya-lib](https://git.sr.ht/~soywod/himalaya-lib) +* [IMAP RFC3501](https://tools.ietf.org/html/rfc3501) +* [Iris](https://github.com/soywod/iris.vim), the himalaya predecessor +* [isync](https://isync.sourceforge.io/), an email synchronizer for offline usage -- [NeoMutt](https://neomutt.org/), an email terminal user interface -- [Alpine](http://alpine.x10host.com/alpine/alpine-info/), an other +* [NeoMutt](https://neomutt.org/), an email terminal user interface +* [Alpine](http://alpine.x10host.com/alpine/alpine-info/), an other email terminal user interface -- [mutt-wizard](https://github.com/LukeSmithxyz/mutt-wizard), a tool +* [mutt-wizard](https://github.com/LukeSmithxyz/mutt-wizard), a tool over NeoMutt and isync -- [rust-imap](https://github.com/jonhoo/rust-imap), a rust IMAP lib +* [rust-imap](https://github.com/jonhoo/rust-imap), a Rust IMAP + library +* [lettre](https://github.com/lettre/lettre), a Rust mailer library +* [mailparse](https://github.com/staktrace/mailparse), a Rust MIME + email parser. ## Sponsoring -[![github](https://img.shields.io/badge/-GitHub%20Sponsors-fafbfc?logo=GitHub%20Sponsors&style=flat-square)](https://github.com/sponsors/soywod) -[![paypal](https://img.shields.io/badge/-PayPal-0079c1?logo=PayPal&logoColor=ffffff&style=flat-square)](https://www.paypal.com/paypalme/soywod) -[![ko-fi](https://img.shields.io/badge/-Ko--fi-ff5e5a?logo=Ko-fi&logoColor=ffffff&style=flat-square)](https://ko-fi.com/soywod) -[![buy-me-a-coffee](https://img.shields.io/badge/-Buy%20Me%20a%20Coffee-ffdd00?logo=Buy%20Me%20A%20Coffee&logoColor=000000&style=flat-square)](https://www.buymeacoffee.com/soywod) -[![liberapay](https://img.shields.io/badge/-Liberapay-f6c915?logo=Liberapay&logoColor=222222&style=flat-square)](https://liberapay.com/soywod) +[![GitHub](https://img.shields.io/badge/-GitHub%20Sponsors-fafbfc?logo=GitHub%20Sponsors&style=flat-square)](https://github.com/sponsors/soywod) +[![PayPal](https://img.shields.io/badge/-PayPal-0079c1?logo=PayPal&logoColor=ffffff&style=flat-square)](https://www.paypal.com/paypalme/soywod) +[![Ko-fi](https://img.shields.io/badge/-Ko--fi-ff5e5a?logo=Ko-fi&logoColor=ffffff&style=flat-square)](https://ko-fi.com/soywod) +[![Buy Me a Coffee](https://img.shields.io/badge/-Buy%20Me%20a%20Coffee-ffdd00?logo=Buy%20Me%20A%20Coffee&logoColor=000000&style=flat-square)](https://www.buymeacoffee.com/soywod) +[![Liberapay](https://img.shields.io/badge/-Liberapay-f6c915?logo=Liberapay&logoColor=222222&style=flat-square)](https://liberapay.com/soywod) diff --git a/flake.lock b/flake.lock index 9f27790..95c6151 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1650374568, - "narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=", + "lastModified": 1673956053, + "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", "owner": "edolstra", "repo": "flake-compat", - "rev": "b4a34015c698c7793d592d66adbab377907a2be8", + "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", "type": "github" }, "original": { @@ -18,11 +18,11 @@ }, "flake-utils": { "locked": { - "lastModified": 1656928814, - "narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=", + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", "owner": "numtide", "repo": "flake-utils", - "rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", "type": "github" }, "original": { @@ -36,11 +36,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1662220400, - "narHash": "sha256-9o2OGQqu4xyLZP9K6kNe1pTHnyPz0Wr3raGYnr9AIgY=", + "lastModified": 1671096816, + "narHash": "sha256-ezQCsNgmpUHdZANDCILm3RvtO1xH8uujk/+EqNvzIOg=", "owner": "nix-community", "repo": "naersk", - "rev": "6944160c19cb591eb85bbf9b2f2768a935623ed3", + "rev": "d998160d6a076cfe8f9741e56aeec7e267e3e114", "type": "github" }, "original": { @@ -51,11 +51,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1664356419, - "narHash": "sha256-PD0hM9YWp2lepAJk7edh8g1VtzJip5rals1fpoQUlY0=", + "lastModified": 1675698036, + "narHash": "sha256-BgsQkQewdlQi8gapJN4phpxkI/FCE/2sORBaFcYbp/A=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "46e8398474ac3b1b7bb198bf9097fc213bbf59b1", + "rev": "1046c7b92e908a1202c0f1ba3fc21d19e1cf1b62", "type": "github" }, "original": { @@ -79,11 +79,11 @@ }, "nixpkgs_3": { "locked": { - "lastModified": 1659102345, - "narHash": "sha256-Vbzlz254EMZvn28BhpN8JOi5EuKqnHZ3ujFYgFcSGvk=", + "lastModified": 1665296151, + "narHash": "sha256-uOB0oxqxN9K7XGF1hcnY+PQnlQJ+3bP2vCn/+Ru/bbc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "11b60e4f80d87794a2a4a8a256391b37c59a1ea7", + "rev": "14ccaaedd95a488dd7ae142757884d8e125b3363", "type": "github" }, "original": { @@ -108,11 +108,11 @@ "nixpkgs": "nixpkgs_3" }, "locked": { - "lastModified": 1664334084, - "narHash": "sha256-cqP0TzDs3GDRprS6IgVQcWjQ0ynmjQFjYWvp+LE/s6I=", + "lastModified": 1675823425, + "narHash": "sha256-o/uLXQdq3OrRAv4BZVVY0VmhMmQBLWw6Y4o+p6ZiaR4=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "70eab96a255ae9b4b82b38ea5ac5c8e5b57e0abd", + "rev": "02e1abbdcbc2d516193ff8a7add71f44cd976ba0", "type": "github" }, "original": { @@ -123,11 +123,11 @@ }, "utils": { "locked": { - "lastModified": 1659877975, - "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", "owner": "numtide", "repo": "flake-utils", - "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", "type": "github" }, "original": { diff --git a/src/cache/args.rs b/src/cache/args.rs new file mode 100644 index 0000000..9da7989 --- /dev/null +++ b/src/cache/args.rs @@ -0,0 +1,19 @@ +//! This module provides arguments related to the cache. + +use clap::{Arg, ArgAction, ArgMatches}; + +const ARG_DISABLE_CACHE: &str = "disable-cache"; + +/// Represents the disable cache flag argument. This argument allows +/// the user to disable any sort of cache. +pub fn arg() -> Arg { + Arg::new(ARG_DISABLE_CACHE) + .long("disable-cache") + .help("Disable any sort of cache") + .action(ArgAction::SetTrue) +} + +/// Represents the disable cache flag parser. +pub fn parse_disable_cache_flag(m: &ArgMatches) -> bool { + m.get_flag(ARG_DISABLE_CACHE) +} diff --git a/src/cache/mod.rs b/src/cache/mod.rs new file mode 100644 index 0000000..6e10f4a --- /dev/null +++ b/src/cache/mod.rs @@ -0,0 +1 @@ +pub mod args; diff --git a/src/compl/args.rs b/src/compl/args.rs index 37d39a1..137d31f 100644 --- a/src/compl/args.rs +++ b/src/compl/args.rs @@ -3,37 +3,37 @@ //! This module provides subcommands and a command matcher related to completion. use anyhow::Result; -use clap::{self, App, Arg, ArgMatches, Shell, SubCommand}; -use log::{debug, info}; +use clap::{value_parser, Arg, ArgMatches, Command}; +use clap_complete::Shell; +use log::debug; -type OptionShell<'a> = Option<&'a str>; +const ARG_SHELL: &str = "shell"; +const CMD_COMPLETION: &str = "completion"; + +type SomeShell = Shell; /// Completion commands. -pub enum Command<'a> { - /// Generate completion script for the given shell slice. - Generate(OptionShell<'a>), +pub enum Cmd { + /// Generate completion script for the given shell. + Generate(SomeShell), } /// Completion command matcher. -pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { - info!("entering completion command matcher"); - - if let Some(m) = m.subcommand_matches("completion") { - info!("completion command matched"); - let shell = m.value_of("shell"); +pub fn matches(m: &ArgMatches) -> Result> { + if let Some(m) = m.subcommand_matches(CMD_COMPLETION) { + let shell = m.get_one::(ARG_SHELL).cloned().unwrap(); debug!("shell: {:?}", shell); - return Ok(Some(Command::Generate(shell))); + return Ok(Some(Cmd::Generate(shell))); }; Ok(None) } /// Completion subcommands. -pub fn subcmds<'a>() -> Vec> { - vec![SubCommand::with_name("completion") - .aliases(&["completions", "compl", "compe", "comp"]) +pub fn subcmd() -> Command { + Command::new(CMD_COMPLETION) .about("Generates the completion script for the given shell") - .args(&[Arg::with_name("shell") - .possible_values(&Shell::variants()[..]) - .required(true)])] + .args(&[Arg::new(ARG_SHELL) + .value_parser(value_parser!(Shell)) + .required(true)]) } diff --git a/src/compl/handlers.rs b/src/compl/handlers.rs index d504f27..5b36791 100644 --- a/src/compl/handlers.rs +++ b/src/compl/handlers.rs @@ -2,20 +2,14 @@ //! //! This module gathers all completion commands. -use anyhow::{anyhow, Context, Result}; -use clap::{App, Shell}; -use log::{debug, info}; -use std::{io, str::FromStr}; +use anyhow::Result; +use clap::Command; +use clap_complete::Shell; +use std::io::stdout; /// Generates completion script from the given [`clap::App`] for the given shell slice. -pub fn generate<'a>(mut app: App<'a, 'a>, shell: Option<&'a str>) -> Result<()> { - info!("entering generate completion handler"); - - let shell = Shell::from_str(shell.unwrap_or_default()) - .map_err(|err| anyhow!(err)) - .context("cannot parse shell")?; - debug!("shell: {}", shell); - - app.gen_completions_to("himalaya", shell, &mut io::stdout()); +pub fn generate<'a>(mut cmd: Command, shell: Shell) -> Result<()> { + let name = cmd.get_name().to_string(); + clap_complete::generate(shell, &mut cmd, name, &mut stdout()); Ok(()) } diff --git a/src/config/args.rs b/src/config/args.rs index a096932..20ba201 100644 --- a/src/config/args.rs +++ b/src/config/args.rs @@ -6,15 +6,15 @@ const ARG_CONFIG: &str = "config"; /// Represents the config file path argument. This argument allows the /// user to customize the config file path. -pub fn arg<'a>() -> Arg<'a, 'a> { - Arg::with_name(ARG_CONFIG) +pub fn arg() -> Arg { + Arg::new(ARG_CONFIG) .long("config") - .short("c") + .short('c') .help("Forces a specific config file path") .value_name("PATH") } /// Represents the config file path argument parser. -pub fn parse_arg<'a>(matches: &'a ArgMatches<'a>) -> Option<&'a str> { - matches.value_of(ARG_CONFIG) +pub fn parse_arg(matches: &ArgMatches) -> Option<&str> { + matches.get_one::(ARG_CONFIG).map(String::as_str) } diff --git a/src/config/config.rs b/src/config/config.rs index 8df2e69..42ba72f 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -4,16 +4,20 @@ //! user configuration file. use anyhow::{anyhow, Context, Result}; +use dirs::{config_dir, home_dir}; use himalaya_lib::{AccountConfig, BackendConfig, EmailHooks, EmailTextPlainFormat}; use log::{debug, trace}; -use serde::Deserialize; -use std::{collections::HashMap, env, fs, path::PathBuf}; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, fs, path::PathBuf}; use toml; -use crate::{account::DeserializedAccountConfig, config::prelude::*}; +use crate::{ + account::DeserializedAccountConfig, + config::{prelude::*, wizard::wizard}, +}; /// Represents the user config file. -#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct DeserializedConfig { #[serde(alias = "name")] @@ -27,11 +31,14 @@ pub struct DeserializedConfig { pub email_listing_page_size: Option, pub email_reading_headers: Option>, - #[serde(default, with = "email_text_plain_format")] + #[serde(default, with = "EmailTextPlainFormatOptionDef", skip_serializing_if = "Option::is_none")] pub email_reading_format: Option, + pub email_reading_verify_cmd: Option, pub email_reading_decrypt_cmd: Option, + pub email_writing_headers: Option>, + pub email_writing_sign_cmd: Option, pub email_writing_encrypt_cmd: Option, - #[serde(default, with = "email_hooks")] + #[serde(default, with = "EmailHooksOptionDef", skip_serializing_if = "Option::is_none")] pub email_hooks: Option, #[serde(flatten)] @@ -41,74 +48,52 @@ pub struct DeserializedConfig { impl DeserializedConfig { /// Tries to create a config from an optional path. pub fn from_opt_path(path: Option<&str>) -> Result { - trace!(">> parse config from path"); debug!("path: {:?}", path); - let path = path.map(|s| s.into()).unwrap_or(Self::path()?); - let content = fs::read_to_string(path).context("cannot read config file")?; - let config: Self = toml::from_str(&content).context("cannot parse config file")?; + let config: Self = match path.map(|s| s.into()).or_else(Self::path) { + Some(path) => { + let content = fs::read_to_string(path).context("cannot read config file")?; + toml::from_str(&content).context("cannot parse config file")? + } + None => wizard()?, + }; if config.accounts.is_empty() { return Err(anyhow!("config file must contain at least one account")); } - trace!("config: {:?}", config); - trace!("<< parse config from path"); + trace!("config: {:#?}", config); Ok(config) } - /// Tries to get the XDG config file path from XDG_CONFIG_HOME - /// environment variable. - fn path_from_xdg() -> Result { - let path = env::var("XDG_CONFIG_HOME").context("cannot read env var XDG_CONFIG_HOME")?; - let path = PathBuf::from(path).join("himalaya").join("config.toml"); - Ok(path) - } - - /// Tries to get the XDG config file path from HOME environment - /// variable. - fn path_from_xdg_alt() -> Result { - let home_var = if cfg!(target_family = "windows") { - "USERPROFILE" - } else { - "HOME" - }; - let path = env::var(home_var).context(format!("cannot read env var {}", &home_var))?; - let path = PathBuf::from(path) - .join(".config") - .join("himalaya") - .join("config.toml"); - Ok(path) - } - - /// Tries to get the .himalayarc config file path from HOME - /// environment variable. - fn path_from_home() -> Result { - let home_var = if cfg!(target_family = "windows") { - "USERPROFILE" - } else { - "HOME" - }; - let path = env::var(home_var).context(format!("cannot read env var {}", &home_var))?; - let path = PathBuf::from(path).join(".himalayarc"); - Ok(path) - } - - /// Tries to get the config file path. - pub fn path() -> Result { - Self::path_from_xdg() - .or_else(|_| Self::path_from_xdg_alt()) - .or_else(|_| Self::path_from_home()) + /// Tries to return a config path from a few default settings. + /// + /// Tries paths in this order: + /// + /// - `"$XDG_CONFIG_DIR/himalaya/config.toml"` (or equivalent to `$XDG_CONFIG_DIR` in other + /// OSes.) + /// - `"$HOME/.config/himalaya/config.toml"` + /// - `"$HOME/.himalayarc"` + /// + /// Returns `Some(path)` if the path exists, otherwise `None`. + pub fn path() -> Option { + config_dir() + .map(|p| p.join("himalaya").join("config.toml")) + .filter(|p| p.exists()) + .or_else(|| home_dir().map(|p| p.join(".config").join("himalaya").join("config.toml"))) + .filter(|p| p.exists()) + .or_else(|| home_dir().map(|p| p.join(".himalayarc"))) + .filter(|p| p.exists()) } pub fn to_configs(&self, account_name: Option<&str>) -> Result<(AccountConfig, BackendConfig)> { - let (account_config, backend_config) = match account_name { + let (account_name, deserialized_account_config) = match account_name { Some("default") | Some("") | None => self .accounts .iter() - .find_map(|(_, account)| { + .find_map(|(name, account)| { if account.is_default() { - Some(account) + Some((name.clone(), account)) } else { None } @@ -117,9 +102,12 @@ impl DeserializedConfig { Some(name) => self .accounts .get(name) + .map(|account| (name.to_string(), account)) .ok_or_else(|| anyhow!(format!("cannot find account {}", name))), - }? - .to_configs(self); + }?; + + let (account_config, backend_config) = + deserialized_account_config.to_configs(account_name, self); Ok((account_config, backend_config)) } diff --git a/src/config/mod.rs b/src/config/mod.rs index 20ee5fb..b22aa9b 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,5 +1,6 @@ pub mod args; pub mod config; pub mod prelude; +mod wizard; pub use config::*; diff --git a/src/config/prelude.rs b/src/config/prelude.rs index 0aae942..6b37fe5 100644 --- a/src/config/prelude.rs +++ b/src/config/prelude.rs @@ -1,5 +1,5 @@ use himalaya_lib::{EmailHooks, EmailSender, EmailTextPlainFormat, SendmailConfig, SmtpConfig}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::path::PathBuf; #[cfg(feature = "imap-backend")] @@ -11,7 +11,7 @@ use himalaya_lib::MaildirConfig; #[cfg(feature = "notmuch-backend")] use himalaya_lib::NotmuchConfig; -#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(remote = "SmtpConfig")] struct SmtpConfigDef { #[serde(rename = "smtp-host")] @@ -31,7 +31,7 @@ struct SmtpConfigDef { } #[cfg(feature = "imap-backend")] -#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(remote = "ImapConfig")] pub struct ImapConfigDef { #[serde(rename = "imap-host")] @@ -57,7 +57,7 @@ pub struct ImapConfigDef { } #[cfg(feature = "maildir-backend")] -#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(remote = "MaildirConfig")] pub struct MaildirConfigDef { #[serde(rename = "maildir-root-dir")] @@ -65,40 +65,31 @@ pub struct MaildirConfigDef { } #[cfg(feature = "notmuch-backend")] -#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(remote = "NotmuchConfig")] pub struct NotmuchConfigDef { #[serde(rename = "notmuch-db-path")] pub db_path: PathBuf, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize)] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(remote = "Option")] +pub enum EmailTextPlainFormatOptionDef { + #[serde(with = "EmailTextPlainFormatDef")] + Some(EmailTextPlainFormat), + #[default] + None, +} + +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(remote = "EmailTextPlainFormat", rename_all = "snake_case")] -enum EmailTextPlainFormatDef { +pub enum EmailTextPlainFormatDef { Auto, Flowed, Fixed(usize), } -pub mod email_text_plain_format { - use himalaya_lib::EmailTextPlainFormat; - use serde::{Deserialize, Deserializer}; - - use super::EmailTextPlainFormatDef; - - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - struct Helper(#[serde(with = "EmailTextPlainFormatDef")] EmailTextPlainFormat); - - let helper = Option::deserialize(deserializer)?; - Ok(helper.map(|Helper(external)| external)) - } -} - -#[derive(Debug, Clone, Eq, PartialEq, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(remote = "EmailSender", tag = "sender", rename_all = "snake_case")] pub enum EmailSenderDef { None, @@ -108,36 +99,27 @@ pub enum EmailSenderDef { Sendmail(SendmailConfig), } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(remote = "SendmailConfig")] pub struct SendmailConfigDef { #[serde(rename = "sendmail-cmd")] cmd: String, } +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[serde(remote = "Option")] +pub enum EmailHooksOptionDef { + #[serde(with = "EmailHooksDef")] + Some(EmailHooks), + #[default] + None, +} + /// Represents the email hooks. Useful for doing extra email /// processing before or after sending it. -#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)] +#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(remote = "EmailHooks")] -struct EmailHooksDef { +pub struct EmailHooksDef { /// Represents the hook called just before sending an email. pub pre_send: Option, } - -pub mod email_hooks { - use himalaya_lib::EmailHooks; - use serde::{Deserialize, Deserializer}; - - use super::EmailHooksDef; - - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - #[derive(Deserialize)] - struct Helper(#[serde(with = "EmailHooksDef")] EmailHooks); - - let helper = Option::deserialize(deserializer)?; - Ok(helper.map(|Helper(external)| external)) - } -} diff --git a/src/config/wizard/imap.rs b/src/config/wizard/imap.rs new file mode 100644 index 0000000..74d93eb --- /dev/null +++ b/src/config/wizard/imap.rs @@ -0,0 +1,57 @@ +use super::{SECURITY_PROTOCOLS, THEME}; +use crate::account::{ + DeserializedAccountConfig, DeserializedBaseAccountConfig, DeserializedImapAccountConfig, +}; +use anyhow::Result; +use dialoguer::{Input, Select}; +use himalaya_lib::ImapConfig; + +#[cfg(feature = "imap-backend")] +pub(crate) fn configure(base: DeserializedBaseAccountConfig) -> Result { + // TODO: Validate by checking as valid URI + let mut backend = ImapConfig { + host: Input::with_theme(&*THEME) + .with_prompt("Enter the IMAP host:") + .default(format!("imap.{}", base.email.rsplit_once('@').unwrap().1)) + .interact()?, + ..Default::default() + }; + + let default_port = match Select::with_theme(&*THEME) + .with_prompt("Which security protocol do you want to use?") + .items(SECURITY_PROTOCOLS) + .default(0) + .interact_opt()? + { + Some(idx) if SECURITY_PROTOCOLS[idx] == "SSL/TLS" => { + backend.ssl = Some(true); + 993 + } + Some(idx) if SECURITY_PROTOCOLS[idx] == "STARTTLS" => { + backend.starttls = Some(true); + 143 + } + _ => 143, + }; + + backend.port = Input::with_theme(&*THEME) + .with_prompt("Enter the IMAP port:") + .validate_with(|input: &String| input.parse::().map(|_| ())) + .default(default_port.to_string()) + .interact() + .map(|input| input.parse::().unwrap())?; + + backend.login = Input::with_theme(&*THEME) + .with_prompt("Enter your IMAP login:") + .default(base.email.clone()) + .interact()?; + + backend.passwd_cmd = Input::with_theme(&*THEME) + .with_prompt("What shell command should we run to get your password?") + .default(format!("pass show {}", &base.email)) + .interact()?; + + Ok(DeserializedAccountConfig::Imap( + DeserializedImapAccountConfig { base, backend }, + )) +} diff --git a/src/config/wizard/maildir.rs b/src/config/wizard/maildir.rs new file mode 100644 index 0000000..8041791 --- /dev/null +++ b/src/config/wizard/maildir.rs @@ -0,0 +1,31 @@ +use super::THEME; +use crate::account::{ + DeserializedAccountConfig, DeserializedBaseAccountConfig, DeserializedMaildirAccountConfig, +}; +use anyhow::Result; +use dialoguer::Input; +use dirs::home_dir; +use himalaya_lib::MaildirConfig; + +#[cfg(feature = "maildir-backend")] +pub(crate) fn configure(base: DeserializedBaseAccountConfig) -> Result { + let input = if let Some(home) = home_dir() { + Input::with_theme(&*THEME) + .default(home.join("Mail").display().to_string()) + .with_prompt("Enter the path to your maildir") + .interact_text()? + } else { + Input::with_theme(&*THEME) + .with_prompt("Enter the path to your maildir") + .interact_text()? + }; + + Ok(DeserializedAccountConfig::Maildir( + DeserializedMaildirAccountConfig { + base, + backend: MaildirConfig { + root_dir: input.into(), + }, + }, + )) +} diff --git a/src/config/wizard/mod.rs b/src/config/wizard/mod.rs new file mode 100644 index 0000000..fcbe681 --- /dev/null +++ b/src/config/wizard/mod.rs @@ -0,0 +1,170 @@ +#[cfg(feature = "imap-backend")] +mod imap; +#[cfg(feature = "maildir-backend")] +mod maildir; +#[cfg(feature = "notmuch-backend")] +mod notmuch; +mod sendmail; +mod smtp; +mod validators; + +use super::DeserializedConfig; +use crate::account::{DeserializedAccountConfig, DeserializedBaseAccountConfig}; +use anyhow::{anyhow, Result}; +use console::style; +use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select}; +use log::trace; +use once_cell::sync::Lazy; +use std::{fs, process}; + +const BACKENDS: &[&str] = &[ + #[cfg(feature = "imap-backend")] + "IMAP", + #[cfg(feature = "maildir-backend")] + "Maildir", + #[cfg(feature = "notmuch-backend")] + "Notmuch", +]; + +const SENDERS: &[&str] = &["SMTP", "Sendmail"]; + +const SECURITY_PROTOCOLS: &[&str] = &["SSL/TLS", "STARTTLS", "None"]; + +// A wizard should have pretty colors 💅 +static THEME: Lazy = Lazy::new(ColorfulTheme::default); + +pub(crate) fn wizard() -> Result { + trace!(">> wizard"); + println!("Himalaya couldn't find an already existing configuration file."); + + match Confirm::new() + .with_prompt("Do you want to create one with the wizard?") + .default(true) + .report(false) + .interact_opt()? + { + Some(false) | None => process::exit(0), + _ => {} + } + + // Determine path to save to + let path = dirs::config_dir() + .map(|p| p.join("himalaya").join("config.toml")) + .ok_or_else(|| anyhow!("The wizard could not determine the config directory. Aborting"))?; + + let mut config = DeserializedConfig::default(); + + // Setup one or multiple accounts + println!("\n{}", style("First let's setup an account").underlined()); + while let Some(account_config) = configure_account()? { + let name: String = Input::with_theme(&*THEME) + .with_prompt("What would you like to name your account?") + .default("Personal".to_owned()) + .interact()?; + + config.accounts.insert(name, account_config); + + match Confirm::new() + .with_prompt("Setup another account?") + .default(false) + .report(false) + .interact_opt()? + { + Some(true) => println!("\n{}", style("Setting up another account").underlined()), + _ => break, + } + } + + // If one acounts is setup, make it the default. If multiple accounts are setup, decide which + // will be the default. If no accounts are setup, exit the process + let default = match config.accounts.len() { + 1 => Some(config.accounts.values_mut().next().unwrap()), + i if i > 1 => { + let accounts = config.accounts.clone(); + let accounts: Vec<&String> = accounts.keys().collect(); + + println!( + "\n{}", + style(format!("You've setup {} accounts", accounts.len())).underlined() + ); + match Select::with_theme(&*THEME) + .with_prompt("Which account would you like to set as your default?") + .items(&accounts) + .default(0) + .interact_opt()? + { + Some(i) => Some(config.accounts.get_mut(accounts[i]).unwrap()), + _ => process::exit(0), + } + } + _ => process::exit(0), + }; + + match default { + Some(DeserializedAccountConfig::None(default)) => default.default = Some(true), + #[cfg(feature = "imap-backend")] + Some(DeserializedAccountConfig::Imap(default)) => default.base.default = Some(true), + #[cfg(feature = "maildir-backend")] + Some(DeserializedAccountConfig::Maildir(default)) => default.base.default = Some(true), + #[cfg(feature = "notmuch-backend")] + Some(DeserializedAccountConfig::Notmuch(default)) => default.base.default = Some(true), + _ => {} + } + + // Serialize config to file + println!("\nWriting the configuration to {path:?}..."); + fs::create_dir_all(path.parent().unwrap())?; + fs::write(path, toml::to_vec(&config)?)?; + + trace!("<< wizard"); + Ok(config) +} + +fn configure_account() -> Result> { + let mut base = configure_base()?; + let sender = Select::with_theme(&*THEME) + .with_prompt("Which sender would you like use with your account?") + .items(SENDERS) + .default(0) + .interact_opt()?; + + base.email_sender = match sender { + Some(idx) if SENDERS[idx] == "SMTP" => smtp::configure(&base), + Some(idx) if SENDERS[idx] == "Sendmail" => sendmail::configure(), + _ => return Ok(None), + }?; + + let backend = Select::with_theme(&*THEME) + .with_prompt("Which backend would you like to configure your account for?") + .items(BACKENDS) + .default(0) + .interact_opt()?; + + match backend { + #[cfg(feature = "imap-backend")] + Some(idx) if BACKENDS[idx] == "IMAP" => Ok(Some(imap::configure(base)?)), + #[cfg(feature = "maildir-backend")] + Some(idx) if BACKENDS[idx] == "Maildir" => Ok(Some(maildir::configure(base)?)), + #[cfg(feature = "notmuch-backend")] + Some(idx) if BACKENDS[idx] == "Notmuch" => Ok(Some(notmuch::configure(base)?)), + _ => Ok(None), + } +} + +fn configure_base() -> Result { + let mut base_account_config = DeserializedBaseAccountConfig { + email: Input::with_theme(&*THEME) + .with_prompt("Enter your email:") + .validate_with(validators::EmailValidator) + .interact()?, + ..Default::default() + }; + + base_account_config.display_name = Some( + Input::with_theme(&*THEME) + .with_prompt("Enter display name:") + .interact()?, + ); + + Ok(base_account_config) +} diff --git a/src/config/wizard/notmuch.rs b/src/config/wizard/notmuch.rs new file mode 100644 index 0000000..15a78b1 --- /dev/null +++ b/src/config/wizard/notmuch.rs @@ -0,0 +1,25 @@ +use super::THEME; +use crate::account::{ + DeserializedAccountConfig, DeserializedBaseAccountConfig, DeserializedNotmuchAccountConfig, +}; +use anyhow::Result; +use dialoguer::Input; +use himalaya_lib::{NotmuchBackend, NotmuchConfig}; + +pub(crate) fn configure(base: DeserializedBaseAccountConfig) -> Result { + let db_path = match NotmuchBackend::get_default_db_path() { + Ok(db) => db, + _ => { + let input: String = Input::with_theme(&*THEME) + .with_prompt("Could not find a notmuch database. Enter path manually:") + .interact_text()?; + input.into() + } + }; + + let backend = NotmuchConfig { db_path }; + + Ok(DeserializedAccountConfig::Notmuch( + DeserializedNotmuchAccountConfig { base, backend }, + )) +} diff --git a/src/config/wizard/sendmail.rs b/src/config/wizard/sendmail.rs new file mode 100644 index 0000000..82ae1a6 --- /dev/null +++ b/src/config/wizard/sendmail.rs @@ -0,0 +1,13 @@ +use super::THEME; +use anyhow::Result; +use dialoguer::Input; +use himalaya_lib::{EmailSender, SendmailConfig}; + +pub(crate) fn configure() -> Result { + Ok(EmailSender::Sendmail(SendmailConfig { + cmd: Input::with_theme(&*THEME) + .with_prompt("Enter an external command to send a mail: ") + .default("/usr/bin/msmtp".to_owned()) + .interact()?, + })) +} diff --git a/src/config/wizard/smtp.rs b/src/config/wizard/smtp.rs new file mode 100644 index 0000000..3d0359d --- /dev/null +++ b/src/config/wizard/smtp.rs @@ -0,0 +1,51 @@ +use super::{SECURITY_PROTOCOLS, THEME}; +use crate::account::DeserializedBaseAccountConfig; +use anyhow::Result; +use dialoguer::{Input, Select}; +use himalaya_lib::{EmailSender, SmtpConfig}; + +pub(crate) fn configure(base: &DeserializedBaseAccountConfig) -> Result { + let mut smtp_config = SmtpConfig { + host: Input::with_theme(&*THEME) + .with_prompt("Enter the SMTP host: ") + .default(format!("smtp.{}", base.email.rsplit_once('@').unwrap().1)) + .interact()?, + ..Default::default() + }; + + let default_port = match Select::with_theme(&*THEME) + .with_prompt("Which security protocol do you want to use?") + .items(SECURITY_PROTOCOLS) + .default(0) + .interact_opt()? + { + Some(idx) if SECURITY_PROTOCOLS[idx] == "SSL/TLS" => { + smtp_config.ssl = Some(true); + 465 + } + Some(idx) if SECURITY_PROTOCOLS[idx] == "STARTTLS" => { + smtp_config.starttls = Some(true); + 587 + } + _ => 25, + }; + + smtp_config.port = Input::with_theme(&*THEME) + .with_prompt("Enter the SMTP port:") + .validate_with(|input: &String| input.parse::().map(|_| ())) + .default(default_port.to_string()) + .interact() + .map(|input| input.parse::().unwrap())?; + + smtp_config.login = Input::with_theme(&*THEME) + .with_prompt("Enter your SMTP login:") + .default(base.email.clone()) + .interact()?; + + smtp_config.passwd_cmd = Input::with_theme(&*THEME) + .with_prompt("What shell command should we run to get your password?") + .default(format!("pass show {}", &base.email)) + .interact()?; + + Ok(EmailSender::Smtp(smtp_config)) +} diff --git a/src/config/wizard/validators.rs b/src/config/wizard/validators.rs new file mode 100644 index 0000000..4a1c290 --- /dev/null +++ b/src/config/wizard/validators.rs @@ -0,0 +1,18 @@ +use anyhow::anyhow; +use dialoguer::Validator; +use email_address::EmailAddress; + +pub(crate) struct EmailValidator; + +impl Validator for EmailValidator { + type Err = anyhow::Error; + + fn validate(&mut self, input: &T) -> Result<(), Self::Err> { + let input = input.to_string(); + if EmailAddress::is_valid(&input) { + Ok(()) + } else { + Err(anyhow!("Invalid email address: {}", input)) + } + } +} diff --git a/src/domain/account/args.rs b/src/domain/account/args.rs index f258ed6..701552b 100644 --- a/src/domain/account/args.rs +++ b/src/domain/account/args.rs @@ -1,27 +1,43 @@ //! This module provides arguments related to the user account config. use anyhow::Result; -use clap::{App, Arg, ArgMatches, SubCommand}; +use clap::{Arg, ArgAction, ArgMatches, Command}; use log::info; use crate::ui::table; const ARG_ACCOUNT: &str = "account"; +const ARG_DRY_RUN: &str = "dry-run"; const CMD_ACCOUNTS: &str = "accounts"; +const CMD_LIST: &str = "list"; +const CMD_SYNC: &str = "sync"; + +type DryRun = bool; /// Represents the account commands. #[derive(Debug, PartialEq, Eq)] pub enum Cmd { /// Represents the list accounts command. List(table::args::MaxTableWidth), + /// Represents the sync account command. + Sync(DryRun), } /// Represents the account command matcher. pub fn matches(m: &ArgMatches) -> Result> { let cmd = if let Some(m) = m.subcommand_matches(CMD_ACCOUNTS) { - info!("accounts command matched"); - let max_table_width = table::args::parse_max_width(m); - Some(Cmd::List(max_table_width)) + if let Some(m) = m.subcommand_matches(CMD_SYNC) { + info!("sync account subcommand matched"); + let dry_run = parse_dry_run_arg(m); + Some(Cmd::Sync(dry_run)) + } else if let Some(m) = m.subcommand_matches(CMD_LIST) { + info!("list accounts subcommand matched"); + let max_table_width = table::args::parse_max_width(m); + Some(Cmd::List(max_table_width)) + } else { + info!("no account subcommand matched, falling back to subcommand list"); + Some(Cmd::List(None)) + } } else { None }; @@ -29,25 +45,50 @@ pub fn matches(m: &ArgMatches) -> Result> { Ok(cmd) } -/// Represents the account subcommands. -pub fn subcmds<'a>() -> Vec> { - vec![SubCommand::with_name(CMD_ACCOUNTS) - .aliases(&["account", "acc", "a"]) - .about("Lists accounts") - .arg(table::args::max_width())] +/// Represents the account subcommand. +pub fn subcmd() -> Command { + Command::new(CMD_ACCOUNTS) + .about("Manage accounts") + .subcommands([ + Command::new(CMD_LIST) + .about("List all accounts from the config file") + .arg(table::args::max_width()), + Command::new(CMD_SYNC) + .about("Synchronize the given account locally") + .arg(dry_run()), + ]) } /// Represents the user account name argument. This argument allows /// the user to select a different account than the default one. -pub fn arg<'a>() -> Arg<'a, 'a> { - Arg::with_name(ARG_ACCOUNT) +pub fn arg() -> Arg { + Arg::new(ARG_ACCOUNT) .long("account") - .short("a") - .help("Selects a specific account") + .short('a') + .help("Select a specific account by name") .value_name("STRING") } /// Represents the user account name argument parser. -pub fn parse_arg<'a>(matches: &'a ArgMatches<'a>) -> Option<&'a str> { - matches.value_of(ARG_ACCOUNT) +pub fn parse_arg(matches: &ArgMatches) -> Option<&str> { + matches.get_one::(ARG_ACCOUNT).map(String::as_str) +} + +/// Represents the user account sync dry run flag. This flag allows +/// the user to see the changes of a sync without applying them. +pub fn dry_run() -> Arg { + Arg::new(ARG_DRY_RUN) + .help("Do not apply changes of the synchronization") + .long_help( + "Do not apply changes of the synchronization. +Changes can be visualized with the RUST_LOG=trace environment variable.", + ) + .short('d') + .long("dry-run") + .action(ArgAction::SetTrue) +} + +/// Represents the user account sync dry run flag parser. +pub fn parse_dry_run_arg(m: &ArgMatches) -> bool { + m.get_flag(ARG_DRY_RUN) } diff --git a/src/domain/account/config.rs b/src/domain/account/config.rs index 671ff06..972ea79 100644 --- a/src/domain/account/config.rs +++ b/src/domain/account/config.rs @@ -14,13 +14,13 @@ use himalaya_lib::MaildirConfig; #[cfg(feature = "notmuch-backend")] use himalaya_lib::NotmuchConfig; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::{collections::HashMap, path::PathBuf}; use crate::config::{prelude::*, DeserializedConfig}; /// Represents all existing kind of account config. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(tag = "backend", rename_all = "snake_case")] pub enum DeserializedAccountConfig { None(DeserializedBaseAccountConfig), @@ -33,25 +33,30 @@ pub enum DeserializedAccountConfig { } impl DeserializedAccountConfig { - pub fn to_configs(&self, global_config: &DeserializedConfig) -> (AccountConfig, BackendConfig) { + pub fn to_configs( + &self, + name: String, + global_config: &DeserializedConfig, + ) -> (AccountConfig, BackendConfig) { match self { - DeserializedAccountConfig::None(config) => { - (config.to_account_config(global_config), BackendConfig::None) - } + DeserializedAccountConfig::None(config) => ( + config.to_account_config(name, global_config), + BackendConfig::None, + ), #[cfg(feature = "imap-backend")] DeserializedAccountConfig::Imap(config) => ( - config.base.to_account_config(global_config), - BackendConfig::Imap(&config.backend), + config.base.to_account_config(name, global_config), + BackendConfig::Imap(config.backend.clone()), ), #[cfg(feature = "maildir-backend")] DeserializedAccountConfig::Maildir(config) => ( - config.base.to_account_config(global_config), - BackendConfig::Maildir(&config.backend), + config.base.to_account_config(name, global_config), + BackendConfig::Maildir(config.backend.clone()), ), #[cfg(feature = "notmuch-backend")] DeserializedAccountConfig::Notmuch(config) => ( - config.base.to_account_config(global_config), - BackendConfig::Notmuch(&config.backend), + config.base.to_account_config(name, global_config), + BackendConfig::Notmuch(config.backend.clone()), ), } } @@ -69,7 +74,7 @@ impl DeserializedAccountConfig { } } -#[derive(Default, Debug, Clone, Eq, PartialEq, Deserialize)] +#[derive(Default, Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct DeserializedBaseAccountConfig { pub email: String, @@ -84,18 +89,25 @@ pub struct DeserializedBaseAccountConfig { pub email_listing_page_size: Option, pub email_reading_headers: Option>, - #[serde(default, with = "email_text_plain_format")] + #[serde(default, with = "EmailTextPlainFormatOptionDef", skip_serializing_if = "Option::is_none")] pub email_reading_format: Option, + pub email_reading_verify_cmd: Option, pub email_reading_decrypt_cmd: Option, + pub email_writing_headers: Option>, + pub email_writing_sign_cmd: Option, pub email_writing_encrypt_cmd: Option, #[serde(flatten, with = "EmailSenderDef")] pub email_sender: EmailSender, - #[serde(default, with = "email_hooks")] + #[serde(default, with = "EmailHooksOptionDef", skip_serializing_if = "Option::is_none")] pub email_hooks: Option, + + #[serde(default)] + pub sync: bool, + pub sync_dir: Option, } impl DeserializedBaseAccountConfig { - pub fn to_account_config(&self, config: &DeserializedConfig) -> AccountConfig { + pub fn to_account_config(&self, name: String, config: &DeserializedConfig) -> AccountConfig { let mut folder_aliases = config .folder_aliases .as_ref() @@ -109,6 +121,7 @@ impl DeserializedBaseAccountConfig { ); AccountConfig { + name, email: self.email.to_owned(), display_name: self .display_name @@ -148,6 +161,16 @@ impl DeserializedBaseAccountConfig { .map(ToOwned::to_owned) .or_else(|| config.email_reading_format.as_ref().map(ToOwned::to_owned)) .unwrap_or_default(), + email_reading_verify_cmd: self + .email_reading_verify_cmd + .as_ref() + .map(ToOwned::to_owned) + .or_else(|| { + config + .email_reading_verify_cmd + .as_ref() + .map(ToOwned::to_owned) + }), email_reading_decrypt_cmd: self .email_reading_decrypt_cmd .as_ref() @@ -158,6 +181,16 @@ impl DeserializedBaseAccountConfig { .as_ref() .map(ToOwned::to_owned) }), + email_writing_sign_cmd: self + .email_writing_sign_cmd + .as_ref() + .map(ToOwned::to_owned) + .or_else(|| { + config + .email_writing_sign_cmd + .as_ref() + .map(ToOwned::to_owned) + }), email_writing_encrypt_cmd: self .email_writing_encrypt_cmd .as_ref() @@ -168,6 +201,11 @@ impl DeserializedBaseAccountConfig { .as_ref() .map(ToOwned::to_owned) }), + email_writing_headers: self + .email_writing_headers + .as_ref() + .map(ToOwned::to_owned) + .or_else(|| config.email_writing_headers.as_ref().map(ToOwned::to_owned)), email_sender: self.email_sender.to_owned(), email_hooks: EmailHooks { pre_send: self @@ -183,11 +221,13 @@ impl DeserializedBaseAccountConfig { }) .unwrap_or_default(), }, + sync: self.sync, + sync_dir: self.sync_dir.clone(), } } } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[cfg(feature = "imap-backend")] pub struct DeserializedImapAccountConfig { #[serde(flatten)] @@ -196,7 +236,7 @@ pub struct DeserializedImapAccountConfig { pub backend: ImapConfig, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[cfg(feature = "maildir-backend")] pub struct DeserializedMaildirAccountConfig { #[serde(flatten)] @@ -205,7 +245,7 @@ pub struct DeserializedMaildirAccountConfig { pub backend: MaildirConfig, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[cfg(feature = "notmuch-backend")] pub struct DeserializedNotmuchAccountConfig { #[serde(flatten)] diff --git a/src/domain/account/handlers.rs b/src/domain/account/handlers.rs index b8a1c2f..cde1c80 100644 --- a/src/domain/account/handlers.rs +++ b/src/domain/account/handlers.rs @@ -3,7 +3,8 @@ //! This module gathers all account actions triggered by the CLI. use anyhow::Result; -use himalaya_lib::AccountConfig; +use himalaya_lib::{AccountConfig, Backend, BackendSyncBuilder, BackendSyncProgressEvent}; +use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use log::{info, trace}; use crate::{ @@ -19,7 +20,7 @@ pub fn list<'a, P: Printer>( deserialized_config: &DeserializedConfig, printer: &mut P, ) -> Result<()> { - info!(">> account list handler"); + info!("entering the list accounts handler"); let accounts: Accounts = deserialized_config.accounts.iter().into(); trace!("accounts: {:?}", accounts); @@ -36,6 +37,184 @@ pub fn list<'a, P: Printer>( Ok(()) } +/// Synchronizes the account defined using argument `-a|--account`. If +/// no account given, synchronizes the default one. +pub fn sync( + account_config: &AccountConfig, + printer: &mut P, + backend: &dyn Backend, + dry_run: bool, +) -> Result<()> { + info!("entering the sync accounts handler"); + trace!("dry run: {}", dry_run); + + let sync_builder = BackendSyncBuilder::new(account_config); + + if dry_run { + let report = sync_builder.dry_run(true).sync(backend)?; + let mut hunks_count = report.folders_patch.len(); + + if !report.folders_patch.is_empty() { + printer.print_log("Folders patch:")?; + for (hunk, _) in report.folders_patch { + printer.print_log(format!(" - {hunk}"))?; + } + printer.print_log("")?; + } + + if !report.envelopes_patch.is_empty() { + printer.print_log("Envelopes patch:")?; + for (hunk, _) in report.envelopes_patch { + hunks_count += 1; + printer.print_log(format!(" - {hunk}"))?; + } + printer.print_log("")?; + } + + printer.print(format!( + "Estimated patch length for account {} to be synchronized: {hunks_count}", + backend.name(), + ))?; + } else if printer.is_json() { + sync_builder.sync(backend)?; + printer.print(format!( + "Account {} successfully synchronized!", + backend.name() + ))?; + } else { + let multi = MultiProgress::new(); + let progress = multi.add( + ProgressBar::new(0).with_style( + ProgressStyle::with_template( + " {spinner:.dim} {msg:.dim}\n {wide_bar:.cyan/blue} {pos}/{len} ", + ) + .unwrap(), + ), + ); + + let report = sync_builder + .on_progress(|evt| { + use BackendSyncProgressEvent::*; + Ok(match evt { + GetLocalCachedFolders => { + progress.set_length(4); + progress.set_position(0); + progress.set_message("Getting local cached folders…"); + } + GetLocalFolders => { + progress.inc(1); + progress.set_message("Getting local maildir folders…"); + } + GetRemoteCachedFolders => { + progress.inc(1); + progress.set_message("Getting remote cached folders…"); + } + GetRemoteFolders => { + progress.inc(1); + progress.set_message("Getting remote folders…"); + } + BuildFoldersPatch => { + progress.inc(1); + progress.set_message("Building patch…"); + } + ProcessFoldersPatch(n) => { + progress.set_length(n as u64); + progress.set_position(0); + progress.set_message("Processing patch…"); + } + ProcessFolderHunk(msg) => { + progress.inc(1); + progress.set_message(msg + "…"); + } + StartEnvelopesSync(folder, n, len) => { + multi.println(format!("[{n:2}/{len}] {folder}")).unwrap(); + progress.reset(); + } + GetLocalCachedEnvelopes => { + progress.set_length(4); + progress.set_message("Getting local cached envelopes…"); + } + GetLocalEnvelopes => { + progress.inc(1); + progress.set_message("Getting local maildir envelopes…"); + } + GetRemoteCachedEnvelopes => { + progress.inc(1); + progress.set_message("Getting remote cached envelopes…"); + } + GetRemoteEnvelopes => { + progress.inc(1); + progress.set_message("Getting remote envelopes…"); + } + BuildEnvelopesPatch => { + progress.inc(1); + progress.set_message("Building patch…"); + } + ProcessEnvelopesPatch(n) => { + progress.set_length(n as u64); + progress.set_position(0); + progress.set_message("Processing patch…"); + } + ProcessEnvelopeHunk(msg) => { + progress.inc(1); + progress.set_message(msg + "…"); + } + }) + }) + .sync(backend)?; + + progress.finish_and_clear(); + + let folders_patch_err = report + .folders_patch + .iter() + .filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err))) + .collect::>(); + if !folders_patch_err.is_empty() { + printer.print_log("")?; + printer.print_log("Errors occured while applying the folders patch:")?; + folders_patch_err + .iter() + .try_for_each(|(hunk, err)| printer.print_log(format!(" - {hunk}: {err}")))?; + } + + if let Some(err) = report.folders_cache_patch.1 { + printer.print_log("")?; + printer.print_log(format!( + "Error occured while applying the folder cache patch: {err}" + ))?; + } + + let envelopes_patch_err = report + .envelopes_patch + .iter() + .filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err))) + .collect::>(); + if !envelopes_patch_err.is_empty() { + printer.print_log("")?; + printer.print_log("Errors occured while applying the envelopes patch:")?; + for (hunk, err) in folders_patch_err { + printer.print_log(format!(" - {hunk}: {err}"))?; + } + } + + if !report.envelopes_cache_patch.1.is_empty() { + printer.print_log("")?; + printer.print_log("Error occured while applying the envelopes cache patch:")?; + for err in report.envelopes_cache_patch.1 { + printer.print_log(format!(" - {err}"))?; + } + } + + printer.print(format!( + "Account {} successfully synchronized!", + backend.name() + ))?; + } + + Ok(()) +} + #[cfg(test)] mod tests { use himalaya_lib::{AccountConfig, ImapConfig}; @@ -101,13 +280,10 @@ mod tests { data.print_table(&mut self.writer, opts)?; Ok(()) } - fn print_str(&mut self, _data: T) -> Result<()> { + fn print_log(&mut self, _data: T) -> Result<()> { unimplemented!() } - fn print_struct( - &mut self, - _data: T, - ) -> Result<()> { + fn print(&mut self, _data: T) -> Result<()> { unimplemented!() } fn is_json(&self) -> bool { diff --git a/src/domain/email/args.rs b/src/domain/email/args.rs index ec2b643..8a3d5f8 100644 --- a/src/domain/email/args.rs +++ b/src/domain/email/args.rs @@ -1,18 +1,15 @@ -//! Module related to email CLI. +//! Email CLI module. //! -//! This module provides subcommands, arguments and a command matcher related to email. +//! This module contains the command matcher, the subcommands and the +//! arguments related to the email domain. use anyhow::Result; -use clap::{self, App, Arg, ArgMatches, SubCommand}; -use himalaya_lib::email::TplOverride; -use log::{debug, trace}; +use clap::{Arg, ArgAction, ArgMatches, Command}; -use crate::{email, flag, folder, tpl, ui::table}; +use crate::{flag, folder, tpl, ui::table}; -const ARG_ATTACHMENTS: &str = "attachment"; const ARG_CRITERIA: &str = "criterion"; -const ARG_ENCRYPT: &str = "encrypt"; -const ARG_HEADERS: &str = "header"; +const ARG_HEADERS: &str = "headers"; const ARG_ID: &str = "id"; const ARG_IDS: &str = "ids"; const ARG_MIME_TYPE: &str = "mime-type"; @@ -24,7 +21,7 @@ const ARG_REPLY_ALL: &str = "reply-all"; const ARG_SANITIZE: &str = "sanitize"; const CMD_ATTACHMENTS: &str = "attachments"; const CMD_COPY: &str = "copy"; -const CMD_DEL: &str = "delete"; +const CMD_DELETE: &str = "delete"; const CMD_FORWARD: &str = "forward"; const CMD_LIST: &str = "list"; const CMD_MOVE: &str = "move"; @@ -36,37 +33,35 @@ const CMD_SEND: &str = "send"; const CMD_SORT: &str = "sort"; const CMD_WRITE: &str = "write"; -type Criteria = String; -type Encrypt = bool; -type Folder<'a> = &'a str; -type Page = usize; -type PageSize = usize; -type Query = String; -type Sanitize = bool; -type Raw = bool; -type RawEmail<'a> = &'a str; -type TextMime<'a> = &'a str; - -pub(crate) type All = bool; -pub(crate) type Attachments<'a> = Vec<&'a str>; -pub(crate) type Headers<'a> = Vec<&'a str>; -pub(crate) type Id<'a> = &'a str; -pub(crate) type Ids<'a> = &'a str; +pub type All = bool; +pub type Criteria = String; +pub type Folder<'a> = &'a str; +pub type Headers<'a> = Vec<&'a str>; +pub type Id<'a> = &'a str; +pub type Ids<'a> = Vec<&'a str>; +pub type Page = usize; +pub type PageSize = usize; +pub type Query = String; +pub type Raw = bool; +pub type RawEmail = String; +pub type Sanitize = bool; +pub type TextMime<'a> = &'a str; /// Represents the email commands. #[derive(Debug, PartialEq, Eq)] pub enum Cmd<'a> { - Attachments(Id<'a>), - Copy(Id<'a>, Folder<'a>), + Attachments(Ids<'a>), + Copy(Ids<'a>, Folder<'a>), Delete(Ids<'a>), - Forward(Id<'a>, Attachments<'a>, Encrypt), + Flag(Option>), + Forward(Id<'a>, tpl::args::Headers<'a>, tpl::args::Body<'a>), List(table::args::MaxTableWidth, Option, Page), - Move(Id<'a>, Folder<'a>), - Read(Id<'a>, TextMime<'a>, Sanitize, Raw, Headers<'a>), - Reply(Id<'a>, All, Attachments<'a>, Encrypt), - Save(RawEmail<'a>), + Move(Ids<'a>, Folder<'a>), + Read(Ids<'a>, TextMime<'a>, Sanitize, Raw, Headers<'a>), + Reply(Id<'a>, All, tpl::args::Headers<'a>, tpl::args::Body<'a>), + Save(RawEmail), Search(Query, table::args::MaxTableWidth, Option, Page), - Send(RawEmail<'a>), + Send(RawEmail), Sort( Criteria, Query, @@ -74,74 +69,61 @@ pub enum Cmd<'a> { Option, Page, ), - Write(TplOverride<'a>, Attachments<'a>, Encrypt), - - Flag(Option>), Tpl(Option>), + Write(tpl::args::Headers<'a>, tpl::args::Body<'a>), } /// Email command matcher. pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { - trace!("matches: {:?}", m); - let cmd = if let Some(m) = m.subcommand_matches(CMD_ATTACHMENTS) { - debug!("attachments command matched"); - let id = parse_id_arg(m); - Cmd::Attachments(id) + let ids = parse_ids_arg(m); + Cmd::Attachments(ids) } else if let Some(m) = m.subcommand_matches(CMD_COPY) { - debug!("copy command matched"); - let id = parse_id_arg(m); + let ids = parse_ids_arg(m); let folder = folder::args::parse_target_arg(m); - Cmd::Copy(id, folder) - } else if let Some(m) = m.subcommand_matches(CMD_DEL) { - debug!("delete command matched"); + Cmd::Copy(ids, folder) + } else if let Some(m) = m.subcommand_matches(CMD_DELETE) { let ids = parse_ids_arg(m); Cmd::Delete(ids) + } else if let Some(m) = m.subcommand_matches(flag::args::CMD_FLAG) { + Cmd::Flag(flag::args::matches(m)?) } else if let Some(m) = m.subcommand_matches(CMD_FORWARD) { - debug!("forward command matched"); let id = parse_id_arg(m); - let attachments = parse_attachments_arg(m); - let encrypt = parse_encrypt_flag(m); - Cmd::Forward(id, attachments, encrypt) + let headers = tpl::args::parse_headers_arg(m); + let body = tpl::args::parse_body_arg(m); + Cmd::Forward(id, headers, body) } else if let Some(m) = m.subcommand_matches(CMD_LIST) { - debug!("list command matched"); let max_table_width = table::args::parse_max_width(m); let page_size = parse_page_size_arg(m); let page = parse_page_arg(m); Cmd::List(max_table_width, page_size, page) } else if let Some(m) = m.subcommand_matches(CMD_MOVE) { - debug!("move command matched"); - let id = parse_id_arg(m); + let ids = parse_ids_arg(m); let folder = folder::args::parse_target_arg(m); - Cmd::Move(id, folder) + Cmd::Move(ids, folder) } else if let Some(m) = m.subcommand_matches(CMD_READ) { - debug!("read command matched"); - let id = parse_id_arg(m); + let ids = parse_ids_arg(m); let mime = parse_mime_type_arg(m); let sanitize = parse_sanitize_flag(m); let raw = parse_raw_flag(m); let headers = parse_headers_arg(m); - Cmd::Read(id, mime, sanitize, raw, headers) + Cmd::Read(ids, mime, sanitize, raw, headers) } else if let Some(m) = m.subcommand_matches(CMD_REPLY) { - debug!("reply command matched"); let id = parse_id_arg(m); let all = parse_reply_all_flag(m); - let attachments = parse_attachments_arg(m); - let encrypt = parse_encrypt_flag(m); - Cmd::Reply(id, all, attachments, encrypt) + let headers = tpl::args::parse_headers_arg(m); + let body = tpl::args::parse_body_arg(m); + Cmd::Reply(id, all, headers, body) } else if let Some(m) = m.subcommand_matches(CMD_SAVE) { - debug!("save command matched"); let email = parse_raw_arg(m); Cmd::Save(email) } else if let Some(m) = m.subcommand_matches(CMD_SEARCH) { - debug!("search command matched"); let max_table_width = table::args::parse_max_width(m); let page_size = parse_page_size_arg(m); let page = parse_page_arg(m); let query = parse_query_arg(m); Cmd::Search(query, max_table_width, page_size, page) } else if let Some(m) = m.subcommand_matches(CMD_SORT) { - debug!("sort command matched"); let max_table_width = table::args::parse_max_width(m); let page_size = parse_page_size_arg(m); let page = parse_page_arg(m); @@ -149,21 +131,15 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { let query = parse_query_arg(m); Cmd::Sort(criteria, query, max_table_width, page_size, page) } else if let Some(m) = m.subcommand_matches(CMD_SEND) { - debug!("send command matched"); let email = parse_raw_arg(m); Cmd::Send(email) - } else if let Some(m) = m.subcommand_matches(CMD_WRITE) { - debug!("write command matched"); - let attachments = parse_attachments_arg(m); - let encrypt = parse_encrypt_flag(m); - let tpl = tpl::args::parse_override_arg(m); - Cmd::Write(tpl, attachments, encrypt) } else if let Some(m) = m.subcommand_matches(tpl::args::CMD_TPL) { Cmd::Tpl(tpl::args::matches(m)?) - } else if let Some(m) = m.subcommand_matches(flag::args::CMD_FLAG) { - Cmd::Flag(flag::args::matches(m)?) + } else if let Some(m) = m.subcommand_matches(CMD_WRITE) { + let headers = tpl::args::parse_headers_arg(m); + let body = tpl::args::parse_body_arg(m); + Cmd::Write(headers, body) } else { - debug!("default list command matched"); Cmd::List(None, None, 0) }; @@ -171,80 +147,74 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { } /// Represents the email subcommands. -pub fn subcmds<'a>() -> Vec> { +pub fn subcmds() -> Vec { vec![ flag::args::subcmds(), tpl::args::subcmds(), vec![ - SubCommand::with_name(CMD_ATTACHMENTS) - .aliases(&["attachment", "attach", "att", "at", "a"]) - .about("Downloads all attachments of the targeted email") - .arg(email::args::id_arg()), - SubCommand::with_name(CMD_LIST) - .aliases(&["lst", "l"]) - .about("Lists all emails") + Command::new(CMD_ATTACHMENTS) + .about("Downloads all emails attachments") + .arg(ids_arg()), + Command::new(CMD_LIST) + .alias("lst") + .about("List envelopes") .arg(page_size_arg()) .arg(page_arg()) .arg(table::args::max_width()), - SubCommand::with_name(CMD_SEARCH) - .aliases(&["s", "query", "q"]) - .about("Lists emails matching the given query") + Command::new(CMD_SEARCH) + .aliases(["query", "q"]) + .about("Filter envelopes matching the given query") .arg(page_size_arg()) .arg(page_arg()) .arg(table::args::max_width()) .arg(query_arg()), - SubCommand::with_name(CMD_SORT) - .about("Sorts emails by the given criteria and matching the given query") + Command::new(CMD_SORT) + .about("Sort envelopes by the given criteria and matching the given query") .arg(page_size_arg()) .arg(page_arg()) .arg(table::args::max_width()) .arg(criteria_arg()) .arg(query_arg()), - SubCommand::with_name(CMD_WRITE) - .about("Writes a new email") - .aliases(&["w", "new", "n"]) - .args(&tpl::args::args()) - .arg(attachments_arg()) - .arg(encrypt_flag()), - SubCommand::with_name(CMD_SEND) - .about("Sends a raw email") + Command::new(CMD_WRITE) + .about("Write a new email") + .aliases(["new", "n"]) + .args(tpl::args::args()), + Command::new(CMD_SEND) + .about("Send a raw email") .arg(raw_arg()), - SubCommand::with_name(CMD_SAVE) - .about("Saves a raw email") + Command::new(CMD_SAVE) + .about("Save a raw email") .arg(raw_arg()), - SubCommand::with_name(CMD_READ) - .about("Reads text bodies of an email") - .arg(id_arg()) + Command::new(CMD_READ) + .about("Read text bodies of emails") .arg(mime_type_arg()) .arg(sanitize_flag()) .arg(raw_flag()) - .arg(headers_arg()), - SubCommand::with_name(CMD_REPLY) - .aliases(&["rep", "r"]) - .about("Answers to an email") - .arg(id_arg()) + .arg(headers_arg()) + .arg(ids_arg()), + Command::new(CMD_REPLY) + .about("Answer to an email") .arg(reply_all_flag()) - .arg(attachments_arg()) - .arg(encrypt_flag()), - SubCommand::with_name(CMD_FORWARD) - .aliases(&["fwd", "f"]) - .about("Forwards an email") - .arg(id_arg()) - .arg(attachments_arg()) - .arg(encrypt_flag()), - SubCommand::with_name(CMD_COPY) - .aliases(&["cp", "c"]) - .about("Copies an email to the targeted folder") - .arg(id_arg()) - .arg(folder::args::target_arg()), - SubCommand::with_name(CMD_MOVE) - .aliases(&["mv"]) - .about("Moves an email to the targeted folder") - .arg(id_arg()) - .arg(folder::args::target_arg()), - SubCommand::with_name(CMD_DEL) - .aliases(&["del", "d", "remove", "rm"]) - .about("Deletes an email") + .args(tpl::args::args()) + .arg(id_arg()), + Command::new(CMD_FORWARD) + .aliases(["fwd", "f"]) + .about("Forward an email") + .args(tpl::args::args()) + .arg(id_arg()), + Command::new(CMD_COPY) + .alias("cp") + .about("Copy emails to the given folder") + .arg(folder::args::target_arg()) + .arg(ids_arg()), + Command::new(CMD_MOVE) + .alias("mv") + .about("Move emails to the given folder") + .arg(folder::args::target_arg()) + .arg(ids_arg()), + Command::new(CMD_DELETE) + .aliases(["remove", "rm"]) + .about("Delete emails") .arg(ids_arg()), ], ] @@ -252,29 +222,45 @@ pub fn subcmds<'a>() -> Vec> { } /// Represents the email id argument. -pub fn id_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name(ARG_ID) +pub fn id_arg() -> Arg { + Arg::new(ARG_ID) .help("Specifies the target email") .value_name("ID") .required(true) } /// Represents the email id argument parser. -pub fn parse_id_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str { - matches.value_of(ARG_ID).unwrap() +pub fn parse_id_arg(matches: &ArgMatches) -> &str { + matches.get_one::(ARG_ID).unwrap() +} + +/// Represents the email ids argument. +pub fn ids_arg() -> Arg { + Arg::new(ARG_IDS) + .help("Email ids") + .value_name("IDS") + .num_args(1..) + .required(true) +} + +/// Represents the email ids argument parser. +pub fn parse_ids_arg(matches: &ArgMatches) -> Vec<&str> { + matches + .get_many::(ARG_IDS) + .unwrap() + .map(String::as_str) + .collect() } /// Represents the email sort criteria argument. -pub fn criteria_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name(ARG_CRITERIA) - .long("criterion") - .short("c") +pub fn criteria_arg<'a>() -> Arg { + Arg::new(ARG_CRITERIA) .help("Email sorting preferences") + .long("criterion") + .short('c') .value_name("CRITERION:ORDER") - .takes_value(true) - .multiple(true) - .required(true) - .possible_values(&[ + .action(ArgAction::Append) + .value_parser([ "arrival", "arrival:asc", "arrival:desc", @@ -300,69 +286,59 @@ pub fn criteria_arg<'a>() -> Arg<'a, 'a> { } /// Represents the email sort criteria argument parser. -pub fn parse_criteria_arg<'a>(matches: &'a ArgMatches<'a>) -> String { +pub fn parse_criteria_arg(matches: &ArgMatches) -> String { matches - .values_of(ARG_CRITERIA) + .get_many::(ARG_CRITERIA) .unwrap_or_default() + .map(ToOwned::to_owned) .collect::>() .join(" ") } -/// Represents the email ids argument. -pub fn ids_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name(ARG_IDS) - .help("Specifies the target email(s)") - .long_help("Specifies a range of emails. The range follows the RFC3501 format.") - .value_name("IDS") - .required(true) -} - -/// Represents the email ids argument parser. -pub fn parse_ids_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str { - matches.value_of(email::args::ARG_IDS).unwrap() -} - /// Represents the email reply all argument. -pub fn reply_all_flag<'a>() -> Arg<'a, 'a> { - Arg::with_name(ARG_REPLY_ALL) +pub fn reply_all_flag() -> Arg { + Arg::new(ARG_REPLY_ALL) .help("Includes all recipients") - .short("A") .long("all") + .short('a') + .action(ArgAction::SetTrue) } /// Represents the email reply all argument parser. -pub fn parse_reply_all_flag<'a>(matches: &'a ArgMatches<'a>) -> bool { - matches.is_present(ARG_REPLY_ALL) +pub fn parse_reply_all_flag(matches: &ArgMatches) -> bool { + matches.get_flag(ARG_REPLY_ALL) } /// Represents the page size argument. -fn page_size_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name(ARG_PAGE_SIZE) +fn page_size_arg() -> Arg { + Arg::new(ARG_PAGE_SIZE) .help("Page size") - .short("s") - .long("size") + .long("page-size") + .short('s') .value_name("INT") } /// Represents the page size argument parser. -fn parse_page_size_arg<'a>(matches: &'a ArgMatches<'a>) -> Option { - matches.value_of(ARG_PAGE_SIZE).and_then(|s| s.parse().ok()) +fn parse_page_size_arg(matches: &ArgMatches) -> Option { + matches + .get_one::(ARG_PAGE_SIZE) + .and_then(|s| s.parse().ok()) } /// Represents the page argument. -fn page_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name(ARG_PAGE) +fn page_arg() -> Arg { + Arg::new(ARG_PAGE) .help("Page number") - .short("p") + .short('p') .long("page") .value_name("INT") .default_value("1") } /// Represents the page argument parser. -fn parse_page_arg<'a>(matches: &'a ArgMatches<'a>) -> usize { +fn parse_page_arg(matches: &ArgMatches) -> usize { matches - .value_of(ARG_PAGE) + .get_one::(ARG_PAGE) .unwrap() .parse() .ok() @@ -370,120 +346,94 @@ fn parse_page_arg<'a>(matches: &'a ArgMatches<'a>) -> usize { .unwrap_or_default() } -/// Represents the email attachments argument. -pub fn attachments_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name(ARG_ATTACHMENTS) - .help("Adds attachment to the email") - .short("a") - .long("attachment") - .value_name("PATH") - .multiple(true) -} - -/// Represents the email attachments argument parser. -pub fn parse_attachments_arg<'a>(matches: &'a ArgMatches<'a>) -> Vec<&'a str> { - matches - .values_of(ARG_ATTACHMENTS) - .unwrap_or_default() - .collect() -} - /// Represents the email headers argument. -pub fn headers_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name(ARG_HEADERS) +pub fn headers_arg() -> Arg { + Arg::new(ARG_HEADERS) .help("Shows additional headers with the email") - .short("h") .long("header") + .short('H') .value_name("STRING") - .multiple(true) + .action(ArgAction::Append) } /// Represents the email headers argument parser. -pub fn parse_headers_arg<'a>(matches: &'a ArgMatches<'a>) -> Vec<&'a str> { - matches.values_of(ARG_HEADERS).unwrap_or_default().collect() +pub fn parse_headers_arg(m: &ArgMatches) -> Vec<&str> { + m.get_many::(ARG_HEADERS) + .unwrap_or_default() + .map(String::as_str) + .collect::>() } /// Represents the sanitize flag. -pub fn sanitize_flag<'a>() -> Arg<'a, 'a> { - Arg::with_name(ARG_SANITIZE) +pub fn sanitize_flag() -> Arg { + Arg::new(ARG_SANITIZE) .help("Sanitizes text bodies") .long("sanitize") - .short("s") + .short('s') + .action(ArgAction::SetTrue) } /// Represents the raw flag. -pub fn raw_flag<'a>() -> Arg<'a, 'a> { - Arg::with_name(ARG_RAW) +pub fn raw_flag() -> Arg { + Arg::new(ARG_RAW) .help("Returns raw version of email") .long("raw") - .short("r") + .short('r') + .action(ArgAction::SetTrue) } /// Represents the sanitize flag parser. -pub fn parse_sanitize_flag<'a>(matches: &'a ArgMatches<'a>) -> bool { - matches.is_present(ARG_SANITIZE) +pub fn parse_sanitize_flag(m: &ArgMatches) -> bool { + m.get_flag(ARG_SANITIZE) } /// Represents the raw flag parser. -pub fn parse_raw_flag<'a>(matches: &'a ArgMatches<'a>) -> bool { - matches.is_present(ARG_RAW) +pub fn parse_raw_flag(m: &ArgMatches) -> bool { + m.get_flag(ARG_RAW) } /// Represents the email raw argument. -pub fn raw_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name(ARG_RAW).raw(true) +pub fn raw_arg() -> Arg { + Arg::new(ARG_RAW).raw(true) } /// Represents the email raw argument parser. -pub fn parse_raw_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str { - matches.value_of(ARG_RAW).unwrap_or_default() -} - -/// Represents the email encrypt flag. -pub fn encrypt_flag<'a>() -> Arg<'a, 'a> { - Arg::with_name(ARG_ENCRYPT) - .help("Encrypts the email") - .short("e") - .long("encrypt") -} - -/// Represents the email encrypt flag parser. -pub fn parse_encrypt_flag<'a>(matches: &'a ArgMatches<'a>) -> bool { - matches.is_present(ARG_ENCRYPT) +pub fn parse_raw_arg(m: &ArgMatches) -> String { + m.get_one::(ARG_RAW).cloned().unwrap_or_default() } /// Represents the email MIME type argument. -pub fn mime_type_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name(ARG_MIME_TYPE) +pub fn mime_type_arg() -> Arg { + Arg::new(ARG_MIME_TYPE) .help("MIME type to use") - .short("t") + .short('t') .long("mime-type") .value_name("MIME") - .possible_values(&["plain", "html"]) + .value_parser(["plain", "html"]) .default_value("plain") } /// Represents the email MIME type argument parser. -pub fn parse_mime_type_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str { - matches.value_of(ARG_MIME_TYPE).unwrap() +pub fn parse_mime_type_arg(matches: &ArgMatches) -> &str { + matches.get_one::(ARG_MIME_TYPE).unwrap() } /// Represents the email query argument. -pub fn query_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name(ARG_QUERY) +pub fn query_arg() -> Arg { + Arg::new(ARG_QUERY) .long_help("The query system depends on the backend, see the wiki for more details") .value_name("QUERY") - .multiple(true) + .num_args(1..) .required(true) } /// Represents the email query argument parser. -pub fn parse_query_arg<'a>(matches: &'a ArgMatches<'a>) -> String { +pub fn parse_query_arg(matches: &ArgMatches) -> String { matches - .values_of(ARG_QUERY) + .get_many::(ARG_QUERY) .unwrap_or_default() .fold((false, vec![]), |(escape, mut cmds), cmd| { - match (cmd, escape) { + match (cmd.as_str(), escape) { // Next command is an arg and needs to be escaped ("subject", _) | ("body", _) | ("text", _) => { cmds.push(cmd.to_string()); diff --git a/src/domain/email/handlers.rs b/src/domain/email/handlers.rs index e4be121..3261a02 100644 --- a/src/domain/email/handlers.rs +++ b/src/domain/email/handlers.rs @@ -1,123 +1,138 @@ -//! Module related to message handling. -//! -//! This module gathers all message commands. - -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use atty::Stream; use himalaya_lib::{ - AccountConfig, Backend, Email, Part, Parts, PartsReaderOptions, Sender, TextPlainPart, - TplOverride, + AccountConfig, Backend, Email, Flag, Flags, Sender, ShowTextPartsStrategy, Tpl, TplBuilder, }; -use log::{debug, info, trace}; -use mailparse::addrparse; +use log::{debug, trace}; use std::{ - borrow::Cow, fs, io::{self, BufRead}, }; use url::Url; +use uuid::Uuid; use crate::{ printer::{PrintTableOpts, Printer}, ui::editor, }; -/// Downloads all message attachments to the user account downloads directory. -pub fn attachments<'a, P: Printer, B: Backend<'a> + ?Sized>( - seq: &str, - mbox: &str, +pub fn attachments( config: &AccountConfig, printer: &mut P, backend: &mut B, + folder: &str, + ids: Vec<&str>, ) -> Result<()> { - let attachments = backend.email_get(mbox, seq)?.attachments(); - let attachments_len = attachments.len(); + let folder = config.folder_alias(folder)?; + let emails = backend.get_emails(&folder, ids.clone())?; + let mut index = 0; - if attachments_len == 0 { - return printer.print_struct(format!("No attachment found for message {}", seq)); + let mut emails_count = 0; + let mut attachments_count = 0; + + for email in emails.to_vec() { + let id = ids.get(index).unwrap(); + let attachments = email.attachments()?; + + index = index + 1; + + if attachments.is_empty() { + printer.print_log(format!("No attachment found for email #{}", id))?; + continue; + } else { + emails_count = emails_count + 1; + } + + printer.print_log(format!( + "{} attachment(s) found for email #{}…", + attachments.len(), + id + ))?; + + for attachment in attachments { + let filename = attachment + .filename + .unwrap_or_else(|| Uuid::new_v4().to_string()); + let filepath = config.get_download_file_path(&filename)?; + printer.print_log(format!("Downloading {:?}…", filepath))?; + fs::write(&filepath, &attachment.body).context("cannot download attachment")?; + attachments_count = attachments_count + 1; + } } - printer.print_str(format!( - "{} attachment(s) found for message {}", - attachments_len, seq - ))?; - - for attachment in attachments { - let file_path = config.get_download_file_path(&attachment.filename)?; - printer.print_str(format!("Downloading {:?}…", file_path))?; - fs::write(&file_path, &attachment.content) - .context(format!("cannot download attachment {:?}", file_path))?; + match attachments_count { + 0 => printer.print("No attachment found!"), + 1 => printer.print("Downloaded 1 attachment!"), + n => printer.print(format!( + "Downloaded {} attachment(s) from {} email(s)!", + n, emails_count, + )), } - - printer.print_struct("Done!") } -/// Copy a message from a folder to another. -pub fn copy<'a, P: Printer, B: Backend<'a> + ?Sized>( - seq: &str, - mbox_src: &str, - mbox_dst: &str, +pub fn copy( + config: &AccountConfig, printer: &mut P, backend: &mut B, + from_folder: &str, + to_folder: &str, + ids: Vec<&str>, ) -> Result<()> { - backend.email_copy(mbox_src, mbox_dst, seq)?; - printer.print_struct(format!( - "Message {} successfully copied to folder {}", - seq, mbox_dst - )) + let from_folder = config.folder_alias(from_folder)?; + let to_folder = config.folder_alias(to_folder)?; + backend.copy_emails(&from_folder, &to_folder, ids)?; + printer.print("Email(s) successfully copied!") } -/// Delete messages matching the given sequence range. -pub fn delete<'a, P: Printer, B: Backend<'a> + ?Sized>( - seq: &str, - mbox: &str, +pub fn delete( + config: &AccountConfig, printer: &mut P, backend: &mut B, + folder: &str, + ids: Vec<&str>, ) -> Result<()> { - backend.email_delete(mbox, seq)?; - printer.print_struct(format!("Message(s) {} successfully deleted", seq)) + let folder = config.folder_alias(folder)?; + backend.delete_emails(&folder, ids)?; + printer.print("Email(s) successfully deleted!") } -/// Forward the given message UID from the selected folder. -pub fn forward<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( - seq: &str, - attachments_paths: Vec<&str>, - encrypt: bool, - mbox: &str, +pub fn forward( config: &AccountConfig, printer: &mut P, backend: &mut B, sender: &mut S, + folder: &str, + id: &str, + headers: Option>, + body: Option<&str>, ) -> Result<()> { - let msg = backend - .email_get(mbox, seq)? - .into_forward(config)? - .add_attachments(attachments_paths)? - .encrypt(encrypt); - editor::edit_email_with_editor( - msg, - TplOverride::default(), - config, - printer, - backend, - sender, - )?; + let folder = config.folder_alias(folder)?; + let tpl = backend + .get_emails(&folder, vec![id])? + .first() + .ok_or_else(|| anyhow!("cannot find email {}", id))? + .to_forward_tpl_builder(config)? + .set_some_raw_headers(headers) + .some_text_plain_part(body) + .build(); + trace!("initial template: {}", *tpl); + editor::edit_tpl_with_editor(config, printer, backend, sender, tpl)?; Ok(()) } -/// List paginated messages from the selected folder. -pub fn list<'a, P: Printer, B: Backend<'a> + ?Sized>( - max_width: Option, - page_size: Option, - page: usize, - mbox: &str, +pub fn list( config: &AccountConfig, printer: &mut P, backend: &mut B, + folder: &str, + max_width: Option, + page_size: Option, + page: usize, ) -> Result<()> { + let folder = config.folder_alias(folder)?; let page_size = page_size.unwrap_or(config.email_listing_page_size()); debug!("page size: {}", page_size); - let msgs = backend.envelope_list(mbox, page_size, page)?; + let msgs = backend.list_envelopes(&folder, page_size, page)?; trace!("envelopes: {:?}", msgs); printer.print_table( Box::new(msgs), @@ -131,242 +146,125 @@ pub fn list<'a, P: Printer, B: Backend<'a> + ?Sized>( /// Parses and edits a message from a [mailto] URL string. /// /// [mailto]: https://en.wikipedia.org/wiki/Mailto -pub fn mailto<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( - url: &Url, +pub fn mailto( config: &AccountConfig, printer: &mut P, backend: &mut B, sender: &mut S, + url: &Url, ) -> Result<()> { - info!("entering mailto command handler"); - - let to = addrparse(url.path())?; - let mut cc = Vec::new(); - let mut bcc = Vec::new(); - let mut subject = Cow::default(); - let mut body = Cow::default(); + let mut tpl = TplBuilder::default().to(url.path()); for (key, val) in url.query_pairs() { - match key.as_bytes() { - b"cc" => { - cc.push(val.to_string()); - } - b"bcc" => { - bcc.push(val.to_string()); - } - b"subject" => { - subject = val; - } - b"body" => { - body = val; - } + match key.to_lowercase().as_bytes() { + b"cc" => tpl = tpl.cc(val), + b"bcc" => tpl = tpl.bcc(val), + b"subject" => tpl = tpl.subject(val), + b"body" => tpl = tpl.text_plain_part(val.as_bytes()), _ => (), } } - let msg = Email { - from: Some(vec![config.address()?].into()), - to: if to.is_empty() { None } else { Some(to) }, - cc: if cc.is_empty() { - None - } else { - Some(addrparse(&cc.join(","))?) - }, - bcc: if bcc.is_empty() { - None - } else { - Some(addrparse(&bcc.join(","))?) - }, - subject: subject.into(), - parts: Parts(vec![Part::TextPlain(TextPlainPart { - content: body.into(), - })]), - ..Email::default() - }; - trace!("message: {:?}", msg); - - editor::edit_email_with_editor( - msg, - TplOverride::default(), - config, - printer, - backend, - sender, - )?; - Ok(()) + editor::edit_tpl_with_editor(config, printer, backend, sender, tpl.build()) } -/// Move a message from a folder to another. -pub fn move_<'a, P: Printer, B: Backend<'a> + ?Sized>( - seq: &str, - mbox_src: &str, - mbox_dst: &str, +pub fn move_( + config: &AccountConfig, printer: &mut P, backend: &mut B, + from_folder: &str, + to_folder: &str, + ids: Vec<&str>, ) -> Result<()> { - backend.email_move(mbox_src, mbox_dst, seq)?; - printer.print_struct(format!( - r#"Message {} successfully moved to folder "{}""#, - seq, mbox_dst - )) + let from_folder = config.folder_alias(from_folder)?; + let to_folder = config.folder_alias(to_folder)?; + backend.move_emails(&from_folder, &to_folder, ids)?; + printer.print("Email(s) successfully moved!") } -/// Read a message by its sequence number. -pub fn read<'a, P: Printer, B: Backend<'a> + ?Sized>( - seq: &str, +pub fn read( + config: &AccountConfig, + printer: &mut P, + backend: &mut B, + folder: &str, + ids: Vec<&str>, text_mime: &str, sanitize: bool, raw: bool, headers: Vec<&str>, - mbox: &str, +) -> Result<()> { + let folder = config.folder_alias(folder)?; + let emails = backend.get_emails(&folder, ids)?; + + let mut glue = ""; + let mut bodies = String::default(); + + for email in emails.to_vec() { + bodies.push_str(glue); + + if raw { + // emails do not always have valid utf8, uses "lossy" to + // display what can be displayed + bodies.push_str(&String::from_utf8_lossy(email.raw()?).into_owned()); + } else { + let tpl = email + .to_read_tpl_builder(config)? + .show_headers(config.email_reading_headers()) + .show_headers(&headers) + .show_text_parts_only(true) + .use_show_text_parts_strategy(if text_mime == "plain" { + ShowTextPartsStrategy::PlainOtherwiseHtml + } else { + ShowTextPartsStrategy::HtmlOtherwisePlain + }) + .sanitize_text_parts(sanitize) + .build(); + + bodies.push_str(&>::into(tpl)); + } + + glue = "\n\n"; + } + + printer.print(bodies) +} + +pub fn reply( config: &AccountConfig, printer: &mut P, backend: &mut B, -) -> Result<()> { - let msg = backend.email_get(mbox, seq)?; - - printer.print_struct(if raw { - // Emails do not always have valid utf8. Using "lossy" to - // display what we can. - String::from_utf8_lossy(&msg.raw).into_owned() - } else { - msg.to_readable( - config, - PartsReaderOptions { - plain_first: text_mime == "plain", - sanitize, - }, - headers, - )? - }) -} - -/// Reply to the given message UID. -pub fn reply<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( - seq: &str, + sender: &mut S, + folder: &str, + id: &str, all: bool, - attachments_paths: Vec<&str>, - encrypt: bool, - mbox: &str, - config: &AccountConfig, - printer: &mut P, - backend: &mut B, - sender: &mut S, + headers: Option>, + body: Option<&str>, ) -> Result<()> { - let msg = backend - .email_get(mbox, seq)? - .into_reply(all, config)? - .add_attachments(attachments_paths)? - .encrypt(encrypt); - editor::edit_email_with_editor( - msg, - TplOverride::default(), - config, - printer, - backend, - sender, - )?; - backend.flags_add(mbox, seq, "replied")?; + let folder = config.folder_alias(folder)?; + let tpl = backend + .get_emails(&folder, vec![id])? + .first() + .ok_or_else(|| anyhow!("cannot find email {}", id))? + .to_reply_tpl_builder(config, all)? + .set_some_raw_headers(headers) + .some_text_plain_part(body) + .build(); + trace!("initial template: {}", *tpl); + editor::edit_tpl_with_editor(config, printer, backend, sender, tpl)?; + backend.add_flags(&folder, vec![id], &Flags::from_iter([Flag::Answered]))?; Ok(()) } -/// Saves a raw message to the targetted folder. -pub fn save<'a, P: Printer, B: Backend<'a> + ?Sized>( - mbox: &str, - raw_msg: &str, +pub fn save( + config: &AccountConfig, printer: &mut P, backend: &mut B, + folder: &str, + raw_email: String, ) -> Result<()> { - debug!("folder: {}", mbox); - + let folder = config.folder_alias(folder)?; let is_tty = atty::is(Stream::Stdin); - debug!("is tty: {}", is_tty); let is_json = printer.is_json(); - debug!("is json: {}", is_json); - - let raw_msg = if is_tty || is_json { - raw_msg.replace("\r", "").replace("\n", "\r\n") - } else { - io::stdin() - .lock() - .lines() - .filter_map(Result::ok) - .collect::>() - .join("\r\n") - }; - backend.email_add(mbox, raw_msg.as_bytes(), "seen")?; - Ok(()) -} - -/// Paginate messages from the selected folder matching the specified -/// query. -pub fn search<'a, P: Printer, B: Backend<'a> + ?Sized>( - query: String, - max_width: Option, - page_size: Option, - page: usize, - mbox: &str, - config: &AccountConfig, - printer: &mut P, - backend: &mut B, -) -> Result<()> { - let page_size = page_size.unwrap_or(config.email_listing_page_size()); - debug!("page size: {}", page_size); - let msgs = backend.envelope_search(mbox, &query, "", page_size, page)?; - trace!("messages: {:#?}", msgs); - printer.print_table( - Box::new(msgs), - PrintTableOpts { - format: &config.email_reading_format, - max_width, - }, - ) -} - -/// Paginates messages from the selected folder matching the specified -/// query, sorted by the given criteria. -pub fn sort<'a, P: Printer, B: Backend<'a> + ?Sized>( - sort: String, - query: String, - max_width: Option, - page_size: Option, - page: usize, - mbox: &str, - config: &AccountConfig, - printer: &mut P, - backend: &mut B, -) -> Result<()> { - let page_size = page_size.unwrap_or(config.email_listing_page_size()); - debug!("page size: {}", page_size); - let msgs = backend.envelope_search(mbox, &query, &sort, page_size, page)?; - trace!("envelopes: {:#?}", msgs); - printer.print_table( - Box::new(msgs), - PrintTableOpts { - format: &config.email_reading_format, - max_width, - }, - ) -} - -/// Send a raw message. -pub fn send<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( - raw_email: &str, - config: &AccountConfig, - printer: &mut P, - backend: &mut B, - sender: &mut S, -) -> Result<()> { - info!("entering send message handler"); - - let is_tty = atty::is(Stream::Stdin); - debug!("is tty: {}", is_tty); - let is_json = printer.is_json(); - debug!("is json: {}", is_json); - - let sent_folder = config.folder_alias("sent")?; - debug!("sent folder: {:?}", sent_folder); - let raw_email = if is_tty || is_json { raw_email.replace("\r", "").replace("\n", "\r\n") } else { @@ -377,26 +275,92 @@ pub fn send<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( .collect::>() .join("\r\n") }; - trace!("raw message: {:?}", raw_email); - let email = Email::from_tpl(&raw_email)?; - sender.send(&email)?; - backend.email_add(&sent_folder, raw_email.as_bytes(), "seen")?; + backend.add_email(&folder, raw_email.as_bytes(), &Flags::default())?; Ok(()) } -/// Compose a new message. -pub fn write<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( - tpl: TplOverride, - attachments_paths: Vec<&str>, - encrypt: bool, +pub fn search( + config: &AccountConfig, + printer: &mut P, + backend: &mut B, + folder: &str, + query: String, + max_width: Option, + page_size: Option, + page: usize, +) -> Result<()> { + let folder = config.folder_alias(folder)?; + let page_size = page_size.unwrap_or(config.email_listing_page_size()); + let envelopes = backend.search_envelopes(&folder, &query, "", page_size, page)?; + let opts = PrintTableOpts { + format: &config.email_reading_format, + max_width, + }; + + printer.print_table(Box::new(envelopes), opts) +} + +pub fn sort( + config: &AccountConfig, + printer: &mut P, + backend: &mut B, + folder: &str, + sort: String, + query: String, + max_width: Option, + page_size: Option, + page: usize, +) -> Result<()> { + let folder = config.folder_alias(folder)?; + let page_size = page_size.unwrap_or(config.email_listing_page_size()); + let envelopes = backend.search_envelopes(&folder, &query, &sort, page_size, page)?; + let opts = PrintTableOpts { + format: &config.email_reading_format, + max_width, + }; + + printer.print_table(Box::new(envelopes), opts) +} + +pub fn send( config: &AccountConfig, printer: &mut P, backend: &mut B, sender: &mut S, + raw_email: String, ) -> Result<()> { - let email = Email::default() - .add_attachments(attachments_paths)? - .encrypt(encrypt); - editor::edit_email_with_editor(email, tpl, config, printer, backend, sender)?; + let folder = config.folder_alias("sent")?; + let is_tty = atty::is(Stream::Stdin); + let is_json = printer.is_json(); + let raw_email = if is_tty || is_json { + raw_email.replace("\r", "").replace("\n", "\r\n") + } else { + io::stdin() + .lock() + .lines() + .filter_map(Result::ok) + .collect::>() + .join("\r\n") + }; + trace!("raw email: {:?}", raw_email); + sender.send(raw_email.as_bytes())?; + backend.add_email(&folder, raw_email.as_bytes(), &Flags::default())?; + Ok(()) +} + +pub fn write( + config: &AccountConfig, + printer: &mut P, + backend: &mut B, + sender: &mut S, + headers: Option>, + body: Option<&str>, +) -> Result<()> { + let tpl = Email::new_tpl_builder(config)? + .set_some_raw_headers(headers) + .some_text_plain_part(body) + .build(); + trace!("initial template: {}", *tpl); + editor::edit_tpl_with_editor(config, printer, backend, sender, tpl)?; Ok(()) } diff --git a/src/domain/envelope/envelope.rs b/src/domain/envelope/envelope.rs index bd694d8..a62254f 100644 --- a/src/domain/envelope/envelope.rs +++ b/src/domain/envelope/envelope.rs @@ -8,7 +8,7 @@ impl Table for Envelope { .cell(Cell::new("ID").bold().underline().white()) .cell(Cell::new("FLAGS").bold().underline().white()) .cell(Cell::new("SUBJECT").shrinkable().bold().underline().white()) - .cell(Cell::new("SENDER").bold().underline().white()) + .cell(Cell::new("FROM").bold().underline().white()) .cell(Cell::new("DATE").bold().underline().white()) } @@ -17,8 +17,12 @@ impl Table for Envelope { let flags = self.flags.to_symbols_string(); let unseen = !self.flags.contains(&Flag::Seen); let subject = &self.subject; - let sender = &self.sender; - let date = self.date.as_deref().unwrap_or_default(); + let sender = if let Some(name) = &self.from.name { + name + } else { + &self.from.addr + }; + let date = self.date.format("%d/%m/%Y %H:%M").to_string(); Row::new() .cell(Cell::new(id).bold_if(unseen).red()) diff --git a/src/domain/flag/args.rs b/src/domain/flag/args.rs index 2533dc9..8c476f8 100644 --- a/src/domain/flag/args.rs +++ b/src/domain/flag/args.rs @@ -1,10 +1,11 @@ //! Email flag CLI module. //! -//! This module provides subcommands, arguments and a command matcher -//! related to the email flag domain. +//! This module contains the command matcher, the subcommands and the +//! arguments related to the email flag domain. use anyhow::Result; -use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand}; +use clap::{Arg, ArgMatches, Command}; +use himalaya_lib::{Flag, Flags}; use log::{debug, info}; use crate::email; @@ -12,38 +13,36 @@ use crate::email; const ARG_FLAGS: &str = "flag"; const CMD_ADD: &str = "add"; -const CMD_DEL: &str = "remove"; +const CMD_REMOVE: &str = "remove"; const CMD_SET: &str = "set"; -pub(crate) const CMD_FLAG: &str = "flag"; - -type Flags = String; +pub(crate) const CMD_FLAG: &str = "flags"; /// Represents the flag commands. #[derive(Debug, PartialEq, Eq)] pub enum Cmd<'a> { Add(email::args::Ids<'a>, Flags), + Remove(email::args::Ids<'a>, Flags), Set(email::args::Ids<'a>, Flags), - Del(email::args::Ids<'a>, Flags), } /// Represents the flag command matcher. pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { let cmd = if let Some(m) = m.subcommand_matches(CMD_ADD) { - debug!("add subcommand matched"); + debug!("add flags command matched"); let ids = email::args::parse_ids_arg(m); - let flags: String = parse_flags_arg(m); + let flags = parse_flags_arg(m); Some(Cmd::Add(ids, flags)) + } else if let Some(m) = m.subcommand_matches(CMD_REMOVE) { + info!("remove flags command matched"); + let ids = email::args::parse_ids_arg(m); + let flags = parse_flags_arg(m); + Some(Cmd::Remove(ids, flags)) } else if let Some(m) = m.subcommand_matches(CMD_SET) { - debug!("set subcommand matched"); + debug!("set flags command matched"); let ids = email::args::parse_ids_arg(m); - let flags: String = parse_flags_arg(m); + let flags = parse_flags_arg(m); Some(Cmd::Set(ids, flags)) - } else if let Some(m) = m.subcommand_matches(CMD_DEL) { - info!("remove subcommand matched"); - let ids = email::args::parse_ids_arg(m); - let flags: String = parse_flags_arg(m); - Some(Cmd::Del(ids, flags)) } else { None }; @@ -52,48 +51,51 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { } /// Represents the flag subcommands. -pub fn subcmds<'a>() -> Vec> { - vec![SubCommand::with_name(CMD_FLAG) - .aliases(&["flags", "flg"]) +pub fn subcmds<'a>() -> Vec { + vec![Command::new(CMD_FLAG) .about("Handles email flags") - .setting(AppSettings::SubcommandRequiredElseHelp) + .subcommand_required(true) + .arg_required_else_help(true) .subcommand( - SubCommand::with_name(CMD_ADD) - .aliases(&["a"]) - .about("Adds email flags") + Command::new(CMD_ADD) + .about("Adds flags to an email") .arg(email::args::ids_arg()) .arg(flags_arg()), ) .subcommand( - SubCommand::with_name(CMD_SET) - .aliases(&["s", "change", "c"]) - .about("Sets email flags") + Command::new(CMD_REMOVE) + .aliases(["delete", "del", "d"]) + .about("Removes flags from an email") .arg(email::args::ids_arg()) .arg(flags_arg()), ) .subcommand( - SubCommand::with_name(CMD_DEL) - .aliases(&["rem", "rm", "r", "delete", "del", "d"]) - .about("Removes email flags") + Command::new(CMD_SET) + .aliases(["change", "c"]) + .about("Sets flags of an email") .arg(email::args::ids_arg()) .arg(flags_arg()), )] } /// Represents the flags argument. -pub fn flags_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name(ARG_FLAGS) - .long_help("Flags are case-insensitive, and they do not need to be prefixed with `\\`.") - .value_name("FLAGS…") - .multiple(true) +pub fn flags_arg() -> Arg { + Arg::new(ARG_FLAGS) + .value_name("FLAGS") + .help("The flags") + .long_help("The list of flags. It can be one of: seen, answered, flagged, deleted, draft, recent. Other flags are considered custom.") + .num_args(1..) .required(true) + .last(true) } /// Represents the flags argument parser. -pub fn parse_flags_arg<'a>(matches: &'a ArgMatches<'a>) -> String { - matches - .values_of(ARG_FLAGS) - .unwrap_or_default() - .collect::>() - .join(" ") +pub fn parse_flags_arg(matches: &ArgMatches) -> Flags { + Flags::from_iter( + matches + .get_many::(ARG_FLAGS) + .unwrap_or_default() + .map(String::as_str) + .map(Flag::from), + ) } diff --git a/src/domain/flag/handlers.rs b/src/domain/flag/handlers.rs index 8a7675d..1f3eb66 100644 --- a/src/domain/flag/handlers.rs +++ b/src/domain/flag/handlers.rs @@ -1,56 +1,37 @@ -//! Message flag handling module. -//! -//! This module gathers all flag actions triggered by the CLI. - use anyhow::Result; -use himalaya_lib::backend::Backend; +use himalaya_lib::{Backend, Flags}; use crate::printer::Printer; -/// Adds flags to all messages matching the given sequence range. -/// Flags are case-insensitive, and they do not need to be prefixed with `\`. -pub fn add<'a, P: Printer, B: Backend<'a> + ?Sized>( - seq_range: &str, - flags: &str, - mbox: &str, +pub fn add( printer: &mut P, backend: &mut B, + folder: &str, + ids: Vec<&str>, + flags: &Flags, ) -> Result<()> { - backend.flags_add(mbox, seq_range, flags)?; - printer.print_struct(format!( - "Flag(s) {:?} successfully added to message(s) {:?}", - flags, seq_range - )) + backend.add_flags(folder, ids, flags)?; + printer.print("Flag(s) successfully added!") } -/// Removes flags from all messages matching the given sequence range. -/// Flags are case-insensitive, and they do not need to be prefixed with `\`. -pub fn remove<'a, P: Printer, B: Backend<'a> + ?Sized>( - seq_range: &str, - flags: &str, - mbox: &str, +pub fn set( printer: &mut P, backend: &mut B, + folder: &str, + ids: Vec<&str>, + flags: &Flags, ) -> Result<()> { - backend.flags_delete(mbox, seq_range, flags)?; - printer.print_struct(format!( - "Flag(s) {:?} successfully removed from message(s) {:?}", - flags, seq_range - )) + backend.set_flags(folder, ids, flags)?; + printer.print("Flag(s) successfully set!") } -/// Replaces flags of all messages matching the given sequence range. -/// Flags are case-insensitive, and they do not need to be prefixed with `\`. -pub fn set<'a, P: Printer, B: Backend<'a> + ?Sized>( - seq_range: &str, - flags: &str, - mbox: &str, +pub fn remove( printer: &mut P, backend: &mut B, + folder: &str, + ids: Vec<&str>, + flags: &Flags, ) -> Result<()> { - backend.flags_set(mbox, seq_range, flags)?; - printer.print_struct(format!( - "Flag(s) {:?} successfully set for message(s) {:?}", - flags, seq_range - )) + backend.remove_flags(folder, ids, flags)?; + printer.print("Flag(s) successfully removed!") } diff --git a/src/domain/folder/args.rs b/src/domain/folder/args.rs index 2ed77f7..97f00fe 100644 --- a/src/domain/folder/args.rs +++ b/src/domain/folder/args.rs @@ -4,7 +4,7 @@ //! related to the folder domain. use anyhow::Result; -use clap::{self, App, Arg, ArgMatches, SubCommand}; +use clap::{self, Arg, ArgMatches, Command}; use log::debug; use crate::ui::table; @@ -32,122 +32,108 @@ pub fn matches(m: &ArgMatches) -> Result> { Ok(cmd) } -/// Represents folder subcommands. -pub fn subcmds<'a>() -> Vec> { - vec![SubCommand::with_name(CMD_FOLDERS) - .aliases(&[ - "folder", - "fold", - "fo", - "mailboxes", - "mailbox", - "mboxes", - "mbox", - "mb", - "m", - ]) +/// Represents the folder subcommand. +pub fn subcmd() -> Command { + Command::new(CMD_FOLDERS) .about("Lists folders") - .arg(table::args::max_width())] + .arg(table::args::max_width()) } /// Represents the source folder argument. -pub fn source_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name(ARG_SOURCE) - .short("f") +pub fn source_arg() -> Arg { + Arg::new(ARG_SOURCE) .long("folder") + .short('f') .help("Specifies the source folder") .value_name("SOURCE") .default_value("inbox") } /// Represents the source folder argument parser. -pub fn parse_source_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str { - matches.value_of(ARG_SOURCE).unwrap() +pub fn parse_source_arg(matches: &ArgMatches) -> &str { + matches.get_one::(ARG_SOURCE).unwrap().as_str() } /// Represents the target folder argument. -pub fn target_arg<'a>() -> Arg<'a, 'a> { - Arg::with_name(ARG_TARGET) +pub fn target_arg() -> Arg { + Arg::new(ARG_TARGET) .help("Specifies the target folder") .value_name("TARGET") .required(true) } /// Represents the target folder argument parser. -pub fn parse_target_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str { - matches.value_of(ARG_TARGET).unwrap() +pub fn parse_target_arg(matches: &ArgMatches) -> &str { + matches.get_one::(ARG_TARGET).unwrap().as_str() } #[cfg(test)] mod tests { - use clap::{App, ErrorKind}; + use clap::{error::ErrorKind, Command}; use super::*; #[test] fn it_should_match_cmds() { - let arg = App::new("himalaya") - .subcommands(subcmds()) + let arg = Command::new("himalaya") + .subcommand(subcmd()) .get_matches_from(&["himalaya", "folders"]); assert_eq!(Some(Cmd::List(None)), matches(&arg).unwrap()); - let arg = App::new("himalaya") - .subcommands(subcmds()) + let arg = Command::new("himalaya") + .subcommand(subcmd()) .get_matches_from(&["himalaya", "folders", "--max-width", "20"]); assert_eq!(Some(Cmd::List(Some(20))), matches(&arg).unwrap()); } - #[test] - fn it_should_match_aliases() { - macro_rules! get_matches_from { - ($alias:expr) => { - App::new("himalaya") - .subcommands(subcmds()) - .get_matches_from(&["himalaya", $alias]) - .subcommand_name() - }; - } - - assert_eq!(Some("folders"), get_matches_from!["folders"]); - assert_eq!(Some("folders"), get_matches_from!["folder"]); - assert_eq!(Some("folders"), get_matches_from!["fold"]); - assert_eq!(Some("folders"), get_matches_from!["fo"]); - } - #[test] fn it_should_match_source_arg() { macro_rules! get_matches_from { ($($arg:expr),*) => { - App::new("himalaya") + Command::new("himalaya") .arg(source_arg()) .get_matches_from(&["himalaya", $($arg,)*]) }; } let app = get_matches_from![]; - assert_eq!(Some("inbox"), app.value_of("source")); + assert_eq!( + Some("inbox"), + app.get_one::(ARG_SOURCE).map(String::as_str) + ); let app = get_matches_from!["-f", "SOURCE"]; - assert_eq!(Some("SOURCE"), app.value_of("source")); + assert_eq!( + Some("SOURCE"), + app.get_one::(ARG_SOURCE).map(String::as_str) + ); let app = get_matches_from!["--folder", "SOURCE"]; - assert_eq!(Some("SOURCE"), app.value_of("source")); + assert_eq!( + Some("SOURCE"), + app.get_one::(ARG_SOURCE).map(String::as_str) + ); } #[test] fn it_should_match_target_arg() { macro_rules! get_matches_from { ($($arg:expr),*) => { - App::new("himalaya") + Command::new("himalaya") .arg(target_arg()) - .get_matches_from_safe(&["himalaya", $($arg,)*]) + .try_get_matches_from_mut(&["himalaya", $($arg,)*]) }; } let app = get_matches_from![]; - assert_eq!(ErrorKind::MissingRequiredArgument, app.unwrap_err().kind); + assert_eq!(ErrorKind::MissingRequiredArgument, app.unwrap_err().kind()); let app = get_matches_from!["TARGET"]; - assert_eq!(Some("TARGET"), app.unwrap().value_of("target")); + assert_eq!( + Some("TARGET"), + app.unwrap() + .get_one::(ARG_TARGET) + .map(String::as_str) + ); } } diff --git a/src/domain/folder/handlers.rs b/src/domain/folder/handlers.rs index c5d932c..fb70f9f 100644 --- a/src/domain/folder/handlers.rs +++ b/src/domain/folder/handlers.rs @@ -4,19 +4,16 @@ use anyhow::Result; use himalaya_lib::{AccountConfig, Backend}; -use log::trace; use crate::printer::{PrintTableOpts, Printer}; -/// Lists all folders. -pub fn list<'a, P: Printer, B: Backend<'a> + ?Sized>( +pub fn list( max_width: Option, config: &AccountConfig, printer: &mut P, backend: &mut B, ) -> Result<()> { - let folders = backend.folder_list()?; - trace!("folders: {:?}", folders); + let folders = backend.list_folders()?; printer.print_table( // TODO: remove Box Box::new(folders), @@ -29,8 +26,10 @@ pub fn list<'a, P: Printer, B: Backend<'a> + ?Sized>( #[cfg(test)] mod tests { - use himalaya_lib::{backend, AccountConfig, Backend, Email, Envelopes, Folder, Folders}; - use std::{fmt::Debug, io}; + use himalaya_lib::{ + backend, AccountConfig, Backend, Emails, Envelope, Envelopes, Flags, Folder, Folders, + }; + use std::{any::Any, fmt::Debug, io}; use termcolor::ColorSpec; use crate::printer::{Print, PrintTable, WriteColor}; @@ -87,10 +86,10 @@ mod tests { data.print_table(&mut self.writer, opts)?; Ok(()) } - fn print_str(&mut self, _data: T) -> anyhow::Result<()> { + fn print_log(&mut self, _data: T) -> anyhow::Result<()> { unimplemented!() } - fn print_struct( + fn print( &mut self, _data: T, ) -> anyhow::Result<()> { @@ -103,12 +102,15 @@ mod tests { struct TestBackend; - impl<'a> Backend<'a> for TestBackend { - fn folder_add(&mut self, _: &str) -> backend::Result<()> { + impl Backend for TestBackend { + fn name(&self) -> String { unimplemented!(); } - fn folder_list(&mut self) -> backend::Result { - Ok(Folders(vec![ + fn add_folder(&self, _: &str) -> backend::Result<()> { + unimplemented!(); + } + fn list_folders(&self) -> backend::Result { + Ok(Folders::from_iter([ Folder { delim: "/".into(), name: "INBOX".into(), @@ -121,14 +123,23 @@ mod tests { }, ])) } - fn folder_delete(&mut self, _: &str) -> backend::Result<()> { + fn purge_folder(&self, _: &str) -> backend::Result<()> { unimplemented!(); } - fn envelope_list(&mut self, _: &str, _: usize, _: usize) -> backend::Result { + fn delete_folder(&self, _: &str) -> backend::Result<()> { + unimplemented!(); + } + fn get_envelope(&self, _: &str, _: &str) -> backend::Result { + unimplemented!(); + } + fn get_envelope_internal(&self, _: &str, _: &str) -> backend::Result { + unimplemented!(); + } + fn list_envelopes(&self, _: &str, _: usize, _: usize) -> backend::Result { unimplemented!() } - fn envelope_search( - &mut self, + fn search_envelopes( + &self, _: &str, _: &str, _: &str, @@ -137,31 +148,63 @@ mod tests { ) -> backend::Result { unimplemented!() } - fn email_add(&mut self, _: &str, _: &[u8], _: &str) -> backend::Result { + fn add_email(&self, _: &str, _: &[u8], _: &Flags) -> backend::Result { unimplemented!() } - fn email_get(&mut self, _: &str, _: &str) -> backend::Result { + fn add_email_internal(&self, _: &str, _: &[u8], _: &Flags) -> backend::Result { unimplemented!() } - fn email_copy(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { + fn get_emails(&self, _: &str, _: Vec<&str>) -> backend::Result { unimplemented!() } - fn email_move(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { + fn preview_emails(&self, _: &str, _: Vec<&str>) -> backend::Result { unimplemented!() } - fn email_delete(&mut self, _: &str, _: &str) -> backend::Result<()> { + fn get_emails_internal(&self, _: &str, _: Vec<&str>) -> backend::Result { unimplemented!() } - fn flags_add(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { + fn copy_emails(&self, _: &str, _: &str, _: Vec<&str>) -> backend::Result<()> { unimplemented!() } - fn flags_set(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { + fn copy_emails_internal(&self, _: &str, _: &str, _: Vec<&str>) -> backend::Result<()> { unimplemented!() } - fn flags_delete(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { + fn move_emails(&self, _: &str, _: &str, _: Vec<&str>) -> backend::Result<()> { unimplemented!() } - fn as_any(&self) -> &(dyn std::any::Any + 'a) { + fn move_emails_internal(&self, _: &str, _: &str, _: Vec<&str>) -> backend::Result<()> { + unimplemented!() + } + fn delete_emails(&self, _: &str, _: Vec<&str>) -> backend::Result<()> { + unimplemented!() + } + fn delete_emails_internal(&self, _: &str, _: Vec<&str>) -> backend::Result<()> { + unimplemented!() + } + fn add_flags(&self, _: &str, _: Vec<&str>, _: &Flags) -> backend::Result<()> { + unimplemented!() + } + fn add_flags_internal(&self, _: &str, _: Vec<&str>, _: &Flags) -> backend::Result<()> { + unimplemented!() + } + fn set_flags(&self, _: &str, _: Vec<&str>, _: &Flags) -> backend::Result<()> { + unimplemented!() + } + fn set_flags_internal(&self, _: &str, _: Vec<&str>, _: &Flags) -> backend::Result<()> { + unimplemented!() + } + fn remove_flags(&self, _: &str, _: Vec<&str>, _: &Flags) -> backend::Result<()> { + unimplemented!() + } + fn remove_flags_internal( + &self, + _: &str, + _: Vec<&str>, + _: &Flags, + ) -> backend::Result<()> { + unimplemented!() + } + fn as_any(&'static self) -> &(dyn Any) { self } } diff --git a/src/domain/imap/args.rs b/src/domain/imap/args.rs index abfd932..2dcf556 100644 --- a/src/domain/imap/args.rs +++ b/src/domain/imap/args.rs @@ -3,64 +3,60 @@ //! This module provides subcommands and a command matcher related to IMAP. use anyhow::Result; -use clap::{App, ArgMatches}; -use log::{debug, info}; +use clap::{value_parser, Arg, ArgMatches, Command}; +use log::debug; + +const ARG_KEEPALIVE: &str = "keepalive"; +const CMD_NOTIFY: &str = "notify"; +const CMD_WATCH: &str = "watch"; type Keepalive = u64; /// IMAP commands. -pub enum Command { +pub enum Cmd { /// Start the IMAP notify mode with the give keepalive duration. Notify(Keepalive), - /// Start the IMAP watch mode with the give keepalive duration. Watch(Keepalive), } /// IMAP command matcher. -pub fn matches(m: &ArgMatches) -> Result> { - info!("entering imap command matcher"); - - if let Some(m) = m.subcommand_matches("notify") { - info!("notify command matched"); - let keepalive = clap::value_t_or_exit!(m.value_of("keepalive"), u64); +pub fn matches(m: &ArgMatches) -> Result> { + if let Some(m) = m.subcommand_matches(CMD_NOTIFY) { + let keepalive = m.get_one::(ARG_KEEPALIVE).unwrap(); debug!("keepalive: {}", keepalive); - return Ok(Some(Command::Notify(keepalive))); + return Ok(Some(Cmd::Notify(*keepalive))); } - if let Some(m) = m.subcommand_matches("watch") { - info!("watch command matched"); - let keepalive = clap::value_t_or_exit!(m.value_of("keepalive"), u64); + if let Some(m) = m.subcommand_matches(CMD_WATCH) { + let keepalive = m.get_one::(ARG_KEEPALIVE).unwrap(); debug!("keepalive: {}", keepalive); - return Ok(Some(Command::Watch(keepalive))); + return Ok(Some(Cmd::Watch(*keepalive))); } Ok(None) } /// IMAP subcommands. -pub fn subcmds<'a>() -> Vec> { +pub fn subcmds<'a>() -> Vec { vec![ - clap::SubCommand::with_name("notify") + Command::new(CMD_NOTIFY) .about("Notifies when new messages arrive in the given folder") - .aliases(&["idle"]) - .arg( - clap::Arg::with_name("keepalive") - .help("Specifies the keepalive duration") - .short("k") - .long("keepalive") - .value_name("SECS") - .default_value("500"), - ), - clap::SubCommand::with_name("watch") + .alias("idle") + .arg(keepalive_arg()), + Command::new(CMD_WATCH) .about("Watches IMAP server changes") - .arg( - clap::Arg::with_name("keepalive") - .help("Specifies the keepalive duration") - .short("k") - .long("keepalive") - .value_name("SECS") - .default_value("500"), - ), + .arg(keepalive_arg()), ] } + +/// Represents the keepalive argument. +pub fn keepalive_arg() -> Arg { + Arg::new(ARG_KEEPALIVE) + .help("Specifies the keepalive duration.") + .long("keepalive") + .short('k') + .value_name("SECS") + .default_value("500") + .value_parser(value_parser!(u64)) +} diff --git a/src/domain/imap/handlers.rs b/src/domain/imap/handlers.rs index fe05c8f..3302700 100644 --- a/src/domain/imap/handlers.rs +++ b/src/domain/imap/handlers.rs @@ -5,10 +5,10 @@ use anyhow::{Context, Result}; use himalaya_lib::ImapBackend; -pub fn notify(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> { - imap.notify(keepalive, mbox).context("cannot imap notify") +pub fn notify(imap: &ImapBackend, folder: &str, keepalive: u64) -> Result<()> { + imap.notify(keepalive, folder).context("cannot imap notify") } -pub fn watch(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> { - imap.watch(keepalive, mbox).context("cannot imap watch") +pub fn watch(imap: &ImapBackend, folder: &str, keepalive: u64) -> Result<()> { + imap.watch(keepalive, folder).context("cannot imap watch") } diff --git a/src/domain/tpl/args.rs b/src/domain/tpl/args.rs index f09423a..50e17f5 100644 --- a/src/domain/tpl/args.rs +++ b/src/domain/tpl/args.rs @@ -4,68 +4,58 @@ //! related to email templating. use anyhow::Result; -use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand}; -use himalaya_lib::email::TplOverride; -use log::debug; +use clap::{Arg, ArgAction, ArgMatches, Command}; use crate::email; -const ARG_BCC: &str = "bcc"; const ARG_BODY: &str = "body"; -const ARG_CC: &str = "cc"; -const ARG_FROM: &str = "from"; -const ARG_HEADERS: &str = "header"; -const ARG_SIGNATURE: &str = "signature"; -const ARG_SUBJECT: &str = "subject"; -const ARG_TO: &str = "to"; +const ARG_HEADERS: &str = "headers"; const ARG_TPL: &str = "template"; const CMD_FORWARD: &str = "forward"; -const CMD_NEW: &str = "new"; const CMD_REPLY: &str = "reply"; const CMD_SAVE: &str = "save"; const CMD_SEND: &str = "send"; +const CMD_WRITE: &str = "write"; -pub(crate) const CMD_TPL: &str = "template"; +pub const CMD_TPL: &str = "template"; -type Tpl<'a> = &'a str; +pub type RawTpl = String; +pub type Headers<'a> = Option>; +pub type Body<'a> = Option<&'a str>; /// Represents the template commands. #[derive(Debug, PartialEq, Eq)] pub enum Cmd<'a> { - Forward(email::args::Id<'a>, TplOverride<'a>), - New(TplOverride<'a>), - Reply(email::args::Id<'a>, email::args::All, TplOverride<'a>), - Save(email::args::Attachments<'a>, Tpl<'a>), - Send(email::args::Attachments<'a>, Tpl<'a>), + Forward(email::args::Id<'a>, Headers<'a>, Body<'a>), + Write(Headers<'a>, Body<'a>), + Reply(email::args::Id<'a>, email::args::All, Headers<'a>, Body<'a>), + Save(RawTpl), + Send(RawTpl), } /// Represents the template command matcher. pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { let cmd = if let Some(m) = m.subcommand_matches(CMD_FORWARD) { - debug!("forward subcommand matched"); let id = email::args::parse_id_arg(m); - let tpl = parse_override_arg(m); - Some(Cmd::Forward(id, tpl)) - } else if let Some(m) = m.subcommand_matches(CMD_NEW) { - debug!("new subcommand matched"); - let tpl = parse_override_arg(m); - Some(Cmd::New(tpl)) + let headers = parse_headers_arg(m); + let body = parse_body_arg(m); + Some(Cmd::Forward(id, headers, body)) } else if let Some(m) = m.subcommand_matches(CMD_REPLY) { - debug!("reply subcommand matched"); let id = email::args::parse_id_arg(m); let all = email::args::parse_reply_all_flag(m); - let tpl = parse_override_arg(m); - Some(Cmd::Reply(id, all, tpl)) + let headers = parse_headers_arg(m); + let body = parse_body_arg(m); + Some(Cmd::Reply(id, all, headers, body)) } else if let Some(m) = m.subcommand_matches(CMD_SAVE) { - debug!("save subcommand matched"); - let attachments = email::args::parse_attachments_arg(m); - let tpl = parse_raw_arg(m); - Some(Cmd::Save(attachments, tpl)) + let raw_tpl = parse_raw_arg(m); + Some(Cmd::Save(raw_tpl)) } else if let Some(m) = m.subcommand_matches(CMD_SEND) { - debug!("send subcommand matched"); - let attachments = email::args::parse_attachments_arg(m); - let tpl = parse_raw_arg(m); - Some(Cmd::Send(attachments, tpl)) + let raw_tpl = parse_raw_arg(m); + Some(Cmd::Send(raw_tpl)) + } else if let Some(m) = m.subcommand_matches(CMD_WRITE) { + let headers = parse_headers_arg(m); + let body = parse_body_arg(m); + Some(Cmd::Write(headers, body)) } else { None }; @@ -74,112 +64,76 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { } /// Represents the template subcommands. -pub fn subcmds<'a>() -> Vec> { - vec![SubCommand::with_name(CMD_TPL) - .aliases(&["tpl"]) +pub fn subcmds<'a>() -> Vec { + vec![Command::new(CMD_TPL) + .alias("tpl") .about("Handles email templates") - .setting(AppSettings::SubcommandRequiredElseHelp) + .subcommand_required(true) + .arg_required_else_help(true) .subcommand( - SubCommand::with_name(CMD_NEW) - .aliases(&["n"]) - .about("Generates a template for a new email") + Command::new(CMD_FORWARD) + .alias("fwd") + .about("Generates a template for forwarding an email") + .arg(email::args::id_arg()) .args(&args()), ) .subcommand( - SubCommand::with_name(CMD_REPLY) - .aliases(&["rep", "re", "r"]) + Command::new(CMD_REPLY) .about("Generates a template for replying to an email") .arg(email::args::id_arg()) .arg(email::args::reply_all_flag()) .args(&args()), ) .subcommand( - SubCommand::with_name(CMD_FORWARD) - .aliases(&["fwd", "fw", "f"]) - .about("Generates a template for forwarding an email") - .arg(email::args::id_arg()) + Command::new(CMD_SAVE) + .about("Compiles the template into a valid email then saves it") + .arg(Arg::new(ARG_TPL).raw(true)), + ) + .subcommand( + Command::new(CMD_SEND) + .about("Compiles the template into a valid email then sends it") + .arg(Arg::new(ARG_TPL).raw(true)), + ) + .subcommand( + Command::new(CMD_WRITE) + .aliases(["new", "n"]) + .about("Generates a template for writing a new email") .args(&args()), - ) - .subcommand( - SubCommand::with_name(CMD_SAVE) - .about("Saves an email based on the given template") - .arg(&email::args::attachments_arg()) - .arg(Arg::with_name(ARG_TPL).raw(true)), - ) - .subcommand( - SubCommand::with_name(CMD_SEND) - .about("Sends an email based on the given template") - .arg(&email::args::attachments_arg()) - .arg(Arg::with_name(ARG_TPL).raw(true)), )] } /// Represents the template arguments. -pub fn args<'a>() -> Vec> { +pub fn args() -> Vec { vec![ - Arg::with_name(ARG_SUBJECT) - .help("Overrides the Subject header") - .short("s") - .long("subject") - .value_name("STRING"), - Arg::with_name(ARG_FROM) - .help("Overrides the From header") - .short("f") - .long("from") - .value_name("ADDR") - .multiple(true), - Arg::with_name(ARG_TO) - .help("Overrides the To header") - .short("t") - .long("to") - .value_name("ADDR") - .multiple(true), - Arg::with_name(ARG_CC) - .help("Overrides the Cc header") - .short("c") - .long("cc") - .value_name("ADDR") - .multiple(true), - Arg::with_name(ARG_BCC) - .help("Overrides the Bcc header") - .short("b") - .long("bcc") - .value_name("ADDR") - .multiple(true), - Arg::with_name(ARG_HEADERS) + Arg::new(ARG_HEADERS) .help("Overrides a specific header") - .short("h") + .short('H') .long("header") .value_name("KEY:VAL") - .multiple(true), - Arg::with_name(ARG_BODY) + .action(ArgAction::Append), + Arg::new(ARG_BODY) .help("Overrides the body") - .short("B") + .short('B') .long("body") .value_name("STRING"), - Arg::with_name(ARG_SIGNATURE) - .help("Overrides the signature") - .short("S") - .long("signature") - .value_name("STRING"), ] } -/// Represents the template override argument parser. -pub fn parse_override_arg<'a>(matches: &'a ArgMatches<'a>) -> TplOverride { - TplOverride { - subject: matches.value_of(ARG_SUBJECT), - from: matches.values_of(ARG_FROM).map(Iterator::collect), - to: matches.values_of(ARG_TO).map(Iterator::collect), - cc: matches.values_of(ARG_CC).map(Iterator::collect), - bcc: matches.values_of(ARG_BCC).map(Iterator::collect), - headers: matches.values_of(ARG_HEADERS).map(Iterator::collect), - body: matches.value_of(ARG_BODY), - signature: matches.value_of(ARG_SIGNATURE), - } +/// Represents the template headers argument parser. +pub fn parse_headers_arg(m: &ArgMatches) -> Headers<'_> { + m.get_many(ARG_HEADERS) + .map(|h| h.map(String::as_str).collect::>()) +} + +/// Represents the template body argument parser. +pub fn parse_body_arg(matches: &ArgMatches) -> Body<'_> { + matches.get_one::(ARG_BODY).map(String::as_str) } /// Represents the raw template argument parser. -pub fn parse_raw_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str { - matches.value_of(ARG_TPL).unwrap_or_default() +pub fn parse_raw_arg(matches: &ArgMatches) -> RawTpl { + matches + .get_one::(ARG_TPL) + .cloned() + .unwrap_or_default() } diff --git a/src/domain/tpl/handlers.rs b/src/domain/tpl/handlers.rs index 0c2f0a7..51920e2 100644 --- a/src/domain/tpl/handlers.rs +++ b/src/domain/tpl/handlers.rs @@ -1,103 +1,119 @@ -//! Module related to message template handling. -//! -//! This module gathers all message template commands. - -use anyhow::Result; +use anyhow::{anyhow, Result}; use atty::Stream; -use himalaya_lib::{AccountConfig, Backend, Email, Sender, TplOverride}; -use std::io::{self, BufRead}; +use himalaya_lib::{AccountConfig, Backend, CompilerBuilder, Email, Flags, Sender, Tpl}; +use std::io::{stdin, BufRead}; use crate::printer::Printer; -/// Generate a new message template. -pub fn new<'a, P: Printer>( - opts: TplOverride<'a>, - config: &'a AccountConfig, - printer: &'a mut P, +pub fn forward( + config: &AccountConfig, + printer: &mut P, + backend: &mut B, + folder: &str, + id: &str, + headers: Option>, + body: Option<&str>, ) -> Result<()> { - let tpl = Email::default().to_tpl(opts, config)?; - printer.print_struct(tpl) + let tpl = backend + .get_emails(folder, vec![id])? + .first() + .ok_or_else(|| anyhow!("cannot find email {}", id))? + .to_forward_tpl_builder(config)? + .set_some_raw_headers(headers) + .some_text_plain_part(body) + .build(); + + printer.print(>::into(tpl)) } -/// Generate a reply message template. -pub fn reply<'a, P: Printer, B: Backend<'a> + ?Sized>( - seq: &str, +pub fn reply( + config: &AccountConfig, + printer: &mut P, + backend: &mut B, + folder: &str, + id: &str, all: bool, - opts: TplOverride<'_>, - mbox: &str, - config: &AccountConfig, - printer: &mut P, - backend: &mut B, + headers: Option>, + body: Option<&str>, ) -> Result<()> { let tpl = backend - .email_get(mbox, seq)? - .into_reply(all, config)? - .to_tpl(opts, config)?; - printer.print_struct(tpl) + .get_emails(folder, vec![id])? + .first() + .ok_or_else(|| anyhow!("cannot find email {}", id))? + .to_reply_tpl_builder(config, all)? + .set_some_raw_headers(headers) + .some_text_plain_part(body) + .build(); + + printer.print(>::into(tpl)) } -/// Generate a forward message template. -pub fn forward<'a, P: Printer, B: Backend<'a> + ?Sized>( - seq: &str, - opts: TplOverride<'_>, - mbox: &str, +pub fn save( config: &AccountConfig, printer: &mut P, backend: &mut B, + folder: &str, + tpl: String, ) -> Result<()> { - let tpl = backend - .email_get(mbox, seq)? - .into_forward(config)? - .to_tpl(opts, config)?; - printer.print_struct(tpl) -} - -/// Saves a message based on a template. -pub fn save<'a, P: Printer, B: Backend<'a> + ?Sized>( - mbox: &str, - config: &AccountConfig, - attachments_paths: Vec<&str>, - tpl: &str, - printer: &mut P, - backend: &mut B, -) -> Result<()> { - let tpl = if atty::is(Stream::Stdin) || printer.is_json() { + let email = Tpl::from(if atty::is(Stream::Stdin) || printer.is_json() { tpl.replace("\r", "") } else { - io::stdin() + stdin() .lock() .lines() .filter_map(Result::ok) .collect::>() .join("\n") - }; - let email = Email::from_tpl(&tpl)?.add_attachments(attachments_paths)?; - let raw_email = email.into_sendable(config)?.formatted(); - backend.email_add(mbox, &raw_email, "seen")?; - printer.print_struct("Template successfully saved") + }) + .compile( + CompilerBuilder::default() + .some_pgp_sign_cmd(config.email_writing_sign_cmd.as_ref()) + .some_pgp_encrypt_cmd(config.email_writing_encrypt_cmd.as_ref()), + )?; + + backend.add_email(folder, &email, &Flags::default())?; + printer.print("Template successfully saved!") } -/// Sends a message based on a template. -pub fn send<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( - mbox: &str, - attachments_paths: Vec<&str>, - tpl: &str, +pub fn send( + config: &AccountConfig, printer: &mut P, backend: &mut B, sender: &mut S, + folder: &str, + tpl: String, ) -> Result<()> { - let tpl = if atty::is(Stream::Stdin) || printer.is_json() { + let email = Tpl::from(if atty::is(Stream::Stdin) || printer.is_json() { tpl.replace("\r", "") } else { - io::stdin() + stdin() .lock() .lines() .filter_map(Result::ok) .collect::>() .join("\n") - }; - let email = Email::from_tpl(&tpl)?.add_attachments(attachments_paths)?; - let sent_msg = sender.send(&email)?; - backend.email_add(mbox, &sent_msg, "seen")?; - printer.print_struct("Template successfully sent") + }) + .compile( + CompilerBuilder::default() + .some_pgp_sign_cmd(config.email_writing_sign_cmd.as_ref()) + .some_pgp_encrypt_cmd(config.email_writing_encrypt_cmd.as_ref()), + )?; + sender.send(&email)?; + backend.add_email(folder, &email, &Flags::default())?; + printer.print("Template successfully sent!")?; + Ok(()) +} + +pub fn write<'a, P: Printer>( + config: &'a AccountConfig, + printer: &'a mut P, + headers: Option>, + body: Option<&str>, +) -> Result<()> { + let tpl = Email::new_tpl_builder(config)? + .set_some_raw_headers(headers) + .some_text_plain_part(body) + .build(); + + printer.print(>::into(tpl)) } diff --git a/src/lib.rs b/src/lib.rs index a840ea1..4ba6146 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ +pub mod cache; pub mod compl; pub mod config; pub mod domain; +pub mod man; pub mod output; pub mod printer; pub mod ui; diff --git a/src/main.rs b/src/main.rs index e9a54bf..b9de5bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,12 @@ -use anyhow::{Context, Result}; -use std::env; +use anyhow::Result; +use clap::Command; +use std::{borrow::Cow, env}; use url::Url; use himalaya::{ - account, compl, + account, cache, compl, config::{self, DeserializedConfig}, - email, flag, folder, - output::{self, OutputFmt}, + email, flag, folder, man, output, printer::StdoutPrinter, tpl, }; @@ -15,19 +15,22 @@ use himalaya_lib::{BackendBuilder, BackendConfig, ImapBackend, SenderBuilder}; #[cfg(feature = "imap-backend")] use himalaya::imap; -fn create_app<'a>() -> clap::App<'a, 'a> { - let app = clap::App::new(env!("CARGO_PKG_NAME")) +fn create_app() -> Command { + let app = Command::new(env!("CARGO_PKG_NAME")) .version(env!("CARGO_PKG_VERSION")) .about(env!("CARGO_PKG_DESCRIPTION")) .author(env!("CARGO_PKG_AUTHORS")) - .global_setting(clap::AppSettings::GlobalVersion) - .arg(&config::args::arg()) - .arg(&account::args::arg()) - .args(&output::args::args()) + .propagate_version(true) + .infer_subcommands(true) + .arg(config::args::arg()) + .arg(account::args::arg()) + .arg(cache::args::arg()) + .args(output::args::args()) .arg(folder::args::source_arg()) - .subcommands(compl::args::subcmds()) - .subcommands(account::args::subcmds()) - .subcommands(folder::args::subcmds()) + .subcommand(compl::args::subcmd()) + .subcommand(man::args::subcmd()) + .subcommand(account::args::subcmd()) + .subcommand(folder::args::subcmd()) .subcommands(email::args::subcmds()); #[cfg(feature = "imap-backend")] @@ -41,73 +44,99 @@ fn main() -> Result<()> { let default_env_filter = env_logger::DEFAULT_FILTER_ENV; env_logger::init_from_env(env_logger::Env::default().filter_or(default_env_filter, "off")); - // Check mailto command BEFORE app initialization. + // checks mailto command before app initialization let raw_args: Vec = env::args().collect(); if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { let url = Url::parse(&raw_args[1])?; let config = DeserializedConfig::from_opt_path(None)?; let (account_config, backend_config) = config.to_configs(None)?; - let mut backend = BackendBuilder::build(&account_config, &backend_config)?; + let mut backend = BackendBuilder::new().build(&account_config, &backend_config)?; let mut sender = SenderBuilder::build(&account_config)?; - let mut printer = StdoutPrinter::from_fmt(OutputFmt::Plain); + let mut printer = StdoutPrinter::default(); return email::handlers::mailto( - &url, &account_config, &mut printer, backend.as_mut(), sender.as_mut(), + &url, ); } let app = create_app(); let m = app.get_matches(); - // Check completion command BEFORE entities and services initialization. - // Related issue: https://github.com/soywod/himalaya/issues/115. + // checks completion command before configs + // https://github.com/soywod/himalaya/issues/115 match compl::args::matches(&m)? { - Some(compl::args::Command::Generate(shell)) => { + Some(compl::args::Cmd::Generate(shell)) => { return compl::handlers::generate(create_app(), shell); } _ => (), } - // Init entities and services. + // checks completion command before configs + // https://github.com/soywod/himalaya/issues/115 + match man::args::matches(&m)? { + Some(man::args::Cmd::GenerateAll(dir)) => { + return man::handlers::generate(dir, create_app()); + } + _ => (), + } + + // inits config let config = DeserializedConfig::from_opt_path(config::args::parse_arg(&m))?; let (account_config, backend_config) = config.to_configs(account::args::parse_arg(&m))?; let folder = account_config.folder_alias(folder::args::parse_source_arg(&m))?; - // Check IMAP commands. + // checks IMAP commands #[cfg(feature = "imap-backend")] - if let BackendConfig::Imap(imap_config) = backend_config { - // FIXME: find a way to downcast `backend` instead. - let mut imap = ImapBackend::new(&account_config, imap_config); + if let BackendConfig::Imap(imap_config) = &backend_config { + // FIXME: find a way to downcast `backend` instead of + // recreating an instance. match imap::args::matches(&m)? { - Some(imap::args::Command::Notify(keepalive)) => { - return imap::handlers::notify(keepalive, &folder, &mut imap); + Some(imap::args::Cmd::Notify(keepalive)) => { + let imap = + ImapBackend::new(Cow::Borrowed(&account_config), Cow::Borrowed(&imap_config))?; + return imap::handlers::notify(&imap, &folder, keepalive); } - Some(imap::args::Command::Watch(keepalive)) => { - return imap::handlers::watch(keepalive, &folder, &mut imap); + Some(imap::args::Cmd::Watch(keepalive)) => { + let imap = + ImapBackend::new(Cow::Borrowed(&account_config), Cow::Borrowed(&imap_config))?; + return imap::handlers::watch(&imap, &folder, keepalive); } _ => (), } } - let mut backend = BackendBuilder::build(&account_config, &backend_config)?; + // inits services let mut sender = SenderBuilder::build(&account_config)?; - let mut printer = StdoutPrinter::from_opt_str(m.value_of("output"))?; + let mut printer = StdoutPrinter::try_from(&m)?; + let disable_cache = cache::args::parse_disable_cache_flag(&m); - // Check account commands. + // checks account commands match account::args::matches(&m)? { Some(account::args::Cmd::List(max_width)) => { return account::handlers::list(max_width, &account_config, &config, &mut printer); } + Some(account::args::Cmd::Sync(dry_run)) => { + let backend = BackendBuilder::new() + .sessions_pool_size(16) + .disable_cache(true) + .build(&account_config, &backend_config)?; + account::handlers::sync(&account_config, &mut printer, backend.as_ref(), dry_run)?; + backend.close()?; + return Ok(()); + } _ => (), } - // Check folder commands. + // checks folder commands match folder::args::matches(&m)? { Some(folder::args::Cmd::List(max_width)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; return folder::handlers::list( max_width, &account_config, @@ -118,202 +147,270 @@ fn main() -> Result<()> { _ => (), } - // Check message commands. + // checks email commands match email::args::matches(&m)? { - Some(email::args::Cmd::Attachments(seq)) => { + Some(email::args::Cmd::Attachments(ids)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; return email::handlers::attachments( - seq, - &folder, &account_config, &mut printer, backend.as_mut(), + &folder, + ids, ); } - Some(email::args::Cmd::Copy(seq, mbox_dst)) => { - return email::handlers::copy(seq, &folder, mbox_dst, &mut printer, backend.as_mut()); - } - Some(email::args::Cmd::Delete(seq)) => { - return email::handlers::delete(seq, &folder, &mut printer, backend.as_mut()); - } - Some(email::args::Cmd::Forward(seq, attachment_paths, encrypt)) => { - return email::handlers::forward( - seq, - attachment_paths, - encrypt, + Some(email::args::Cmd::Copy(ids, to_folder)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; + return email::handlers::copy( + &account_config, + &mut printer, + backend.as_mut(), &folder, + to_folder, + ids, + ); + } + Some(email::args::Cmd::Delete(ids)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; + return email::handlers::delete( + &account_config, + &mut printer, + backend.as_mut(), + &folder, + ids, + ); + } + Some(email::args::Cmd::Forward(id, headers, body)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; + return email::handlers::forward( &account_config, &mut printer, backend.as_mut(), sender.as_mut(), + &folder, + id, + headers, + body, ); } Some(email::args::Cmd::List(max_width, page_size, page)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; return email::handlers::list( - max_width, - page_size, - page, - &folder, &account_config, &mut printer, backend.as_mut(), + &folder, + max_width, + page_size, + page, ); } - Some(email::args::Cmd::Move(seq, mbox_dst)) => { - return email::handlers::move_(seq, &folder, mbox_dst, &mut printer, backend.as_mut()); + Some(email::args::Cmd::Move(ids, to_folder)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; + return email::handlers::move_( + &account_config, + &mut printer, + backend.as_mut(), + &folder, + to_folder, + ids, + ); } - Some(email::args::Cmd::Read(seq, text_mime, sanitize, raw, headers)) => { + Some(email::args::Cmd::Read(ids, text_mime, sanitize, raw, headers)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; return email::handlers::read( - seq, + &account_config, + &mut printer, + backend.as_mut(), + &folder, + ids, text_mime, sanitize, raw, headers, - &folder, - &account_config, - &mut printer, - backend.as_mut(), ); } - Some(email::args::Cmd::Reply(seq, all, attachment_paths, encrypt)) => { + Some(email::args::Cmd::Reply(id, all, headers, body)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; return email::handlers::reply( - seq, - all, - attachment_paths, - encrypt, - &folder, &account_config, &mut printer, backend.as_mut(), sender.as_mut(), + &folder, + id, + all, + headers, + body, ); } - Some(email::args::Cmd::Save(raw_msg)) => { - return email::handlers::save(&folder, raw_msg, &mut printer, backend.as_mut()); + Some(email::args::Cmd::Save(raw_email)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; + return email::handlers::save( + &account_config, + &mut printer, + backend.as_mut(), + &folder, + raw_email, + ); } Some(email::args::Cmd::Search(query, max_width, page_size, page)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; return email::handlers::search( + &account_config, + &mut printer, + backend.as_mut(), + &folder, query, max_width, page_size, page, - &folder, - &account_config, - &mut printer, - backend.as_mut(), ); } Some(email::args::Cmd::Sort(criteria, query, max_width, page_size, page)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; return email::handlers::sort( + &account_config, + &mut printer, + backend.as_mut(), + &folder, criteria, query, max_width, page_size, page, - &folder, - &account_config, - &mut printer, - backend.as_mut(), ); } - Some(email::args::Cmd::Send(raw_msg)) => { + Some(email::args::Cmd::Send(raw_email)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; return email::handlers::send( - raw_msg, - &account_config, - &mut printer, - backend.as_mut(), - sender.as_mut(), - ); - } - Some(email::args::Cmd::Write(tpl, atts, encrypt)) => { - return email::handlers::write( - tpl, - atts, - encrypt, &account_config, &mut printer, backend.as_mut(), sender.as_mut(), + raw_email, ); } Some(email::args::Cmd::Flag(m)) => match m { - Some(flag::args::Cmd::Set(seq_range, ref flags)) => { - return flag::handlers::set( - seq_range, - flags, - &folder, - &mut printer, - backend.as_mut(), - ); + Some(flag::args::Cmd::Set(ids, ref flags)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; + return flag::handlers::set(&mut printer, backend.as_mut(), &folder, ids, flags); } - Some(flag::args::Cmd::Add(seq_range, ref flags)) => { - return flag::handlers::add( - seq_range, - flags, - &folder, - &mut printer, - backend.as_mut(), - ); + Some(flag::args::Cmd::Add(ids, ref flags)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; + return flag::handlers::add(&mut printer, backend.as_mut(), &folder, ids, flags); } - Some(flag::args::Cmd::Del(seq_range, ref flags)) => { - return flag::handlers::remove( - seq_range, - flags, - &folder, - &mut printer, - backend.as_mut(), - ); + Some(flag::args::Cmd::Remove(ids, ref flags)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; + return flag::handlers::remove(&mut printer, backend.as_mut(), &folder, ids, flags); } _ => (), }, Some(email::args::Cmd::Tpl(m)) => match m { - Some(tpl::args::Cmd::New(tpl)) => { - return tpl::handlers::new(tpl, &account_config, &mut printer); - } - Some(tpl::args::Cmd::Reply(seq, all, tpl)) => { - return tpl::handlers::reply( - seq, - all, - tpl, - &folder, - &account_config, - &mut printer, - backend.as_mut(), - ); - } - Some(tpl::args::Cmd::Forward(seq, tpl)) => { + Some(tpl::args::Cmd::Forward(id, headers, body)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; return tpl::handlers::forward( - seq, - tpl, - &folder, &account_config, &mut printer, backend.as_mut(), + &folder, + id, + headers, + body, ); } - Some(tpl::args::Cmd::Save(atts, tpl)) => { + Some(tpl::args::Cmd::Write(headers, body)) => { + return tpl::handlers::write(&account_config, &mut printer, headers, body); + } + Some(tpl::args::Cmd::Reply(id, all, headers, body)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; + return tpl::handlers::reply( + &account_config, + &mut printer, + backend.as_mut(), + &folder, + id, + all, + headers, + body, + ); + } + Some(tpl::args::Cmd::Save(tpl)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; return tpl::handlers::save( - &folder, &account_config, - atts, - tpl, &mut printer, backend.as_mut(), + &folder, + tpl, ); } - Some(tpl::args::Cmd::Send(atts, tpl)) => { + Some(tpl::args::Cmd::Send(tpl)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; return tpl::handlers::send( - &folder, - atts, - tpl, + &account_config, &mut printer, backend.as_mut(), sender.as_mut(), + &folder, + tpl, ); } _ => (), }, + Some(email::args::Cmd::Write(headers, body)) => { + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; + return email::handlers::write( + &account_config, + &mut printer, + backend.as_mut(), + sender.as_mut(), + headers, + body, + ); + } _ => (), } - backend.as_mut().disconnect().context("cannot disconnect") + Ok(()) } diff --git a/src/man/args.rs b/src/man/args.rs new file mode 100644 index 0000000..6361c47 --- /dev/null +++ b/src/man/args.rs @@ -0,0 +1,40 @@ +//! Module related to man CLI. +//! +//! This module provides subcommands and a command matcher related to +//! man. + +use anyhow::Result; +use clap::{Arg, ArgMatches, Command}; +use log::debug; + +const ARG_DIR: &str = "dir"; +const CMD_MAN: &str = "man"; + +/// Man commands. +pub enum Cmd<'a> { + /// Generates all man pages to the specified directory. + GenerateAll(&'a str), +} + +/// Man command matcher. +pub fn matches(m: &ArgMatches) -> Result> { + if let Some(m) = m.subcommand_matches(CMD_MAN) { + let dir = m.get_one::(ARG_DIR).map(String::as_str).unwrap(); + debug!("directory: {}", dir); + return Ok(Some(Cmd::GenerateAll(dir))); + }; + + Ok(None) +} + +/// Man subcommands. +pub fn subcmd() -> Command { + Command::new(CMD_MAN) + .about("Generates all man pages to the specified directory.") + .arg( + Arg::new(ARG_DIR) + .help("Directory where to generate man files") + .long_help("Represents the directory where all man files of all commands and subcommands should be generated in.") + .required(true), + ) +} diff --git a/src/man/handlers.rs b/src/man/handlers.rs new file mode 100644 index 0000000..bb0e7b6 --- /dev/null +++ b/src/man/handlers.rs @@ -0,0 +1,29 @@ +//! Module related to man handling. +//! +//! This module gathers all man commands. + +use anyhow::Result; +use clap::Command; +use clap_mangen::Man; +use std::{fs, path::PathBuf}; + +/// Generates all man pages of all subcommands in the given directory. +pub fn generate(dir: &str, cmd: Command) -> Result<()> { + let mut buffer = Vec::new(); + let cmd_name = cmd.get_name().to_string(); + let subcmds = cmd.get_subcommands().cloned().collect::>(); + Man::new(cmd).render(&mut buffer)?; + fs::write(PathBuf::from(dir).join(format!("{}.1", cmd_name)), buffer)?; + + for subcmd in subcmds { + let mut buffer = Vec::new(); + let subcmd_name = subcmd.get_name().to_string(); + Man::new(subcmd).render(&mut buffer)?; + fs::write( + PathBuf::from(dir).join(format!("{}-{}.1", cmd_name, subcmd_name)), + buffer, + )?; + } + + Ok(()) +} diff --git a/src/man/mod.rs b/src/man/mod.rs new file mode 100644 index 0000000..b0b957b --- /dev/null +++ b/src/man/mod.rs @@ -0,0 +1,2 @@ +pub mod args; +pub mod handlers; diff --git a/src/output/args.rs b/src/output/args.rs index 84417b8..aa5776a 100644 --- a/src/output/args.rs +++ b/src/output/args.rs @@ -4,23 +4,43 @@ use clap::Arg; +pub(crate) const ARG_COLOR: &str = "color"; +pub(crate) const ARG_OUTPUT: &str = "output"; + /// Output arguments. -pub fn args<'a>() -> Vec> { +pub fn args() -> Vec { vec![ - Arg::with_name("output") - .long("output") - .short("o") + Arg::new(ARG_OUTPUT) .help("Defines the output format") + .long("output") + .short('o') .value_name("FMT") - .possible_values(&["plain", "json"]) + .value_parser(["plain", "json"]) .default_value("plain"), - Arg::with_name("log-level") - .long("log-level") - .alias("log") - .short("l") - .help("Defines the logs level") - .value_name("LEVEL") - .possible_values(&["error", "warn", "info", "debug", "trace"]) - .default_value("info"), + Arg::new(ARG_COLOR) + .help("Controls when to use colors.") + .long_help( + " +This flag controls when to use colors. The default setting is 'auto', which +means himalaya will try to guess when to use colors. For example, if himalaya is +printing to a terminal, then it will use colors, but if it is redirected to a +file or a pipe, then it will suppress color output. himalaya will suppress color +output in some other circumstances as well. For example, if the TERM +environment variable is not set or set to 'dumb', then himalaya will not use +colors. + +The possible values for this flag are: + +never Colors will never be used. +auto The default. himalaya tries to be smart. +always Colors will always be used regardless of where output is sent. +ansi Like 'always', but emits ANSI escapes (even in a Windows console). +", + ) + .long("color") + .short('C') + .value_parser(["never", "auto", "always", "ansi"]) + .default_value("auto") + .value_name("WHEN"), ] } diff --git a/src/output/output.rs b/src/output/output.rs index c0efbf8..9b1d19b 100644 --- a/src/output/output.rs +++ b/src/output/output.rs @@ -1,31 +1,30 @@ use anyhow::{anyhow, Error, Result}; -use std::{convert::TryFrom, fmt}; +use atty::Stream; +use serde::Serialize; +use std::{fmt, str::FromStr}; +use termcolor::ColorChoice; /// Represents the available output formats. -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum OutputFmt { Plain, Json, } -impl From<&str> for OutputFmt { - fn from(fmt: &str) -> Self { - match fmt { - slice if slice.eq_ignore_ascii_case("json") => Self::Json, - _ => Self::Plain, - } +impl Default for OutputFmt { + fn default() -> Self { + Self::Plain } } -impl TryFrom> for OutputFmt { - type Error = Error; +impl FromStr for OutputFmt { + type Err = Error; - fn try_from(fmt: Option<&str>) -> Result { + fn from_str(fmt: &str) -> Result { match fmt { - Some(fmt) if fmt.eq_ignore_ascii_case("json") => Ok(Self::Json), - Some(fmt) if fmt.eq_ignore_ascii_case("plain") => Ok(Self::Plain), - None => Ok(Self::Plain), - Some(fmt) => Err(anyhow!(r#"cannot parse output format "{}""#, fmt)), + fmt if fmt.eq_ignore_ascii_case("json") => Ok(Self::Json), + fmt if fmt.eq_ignore_ascii_case("plain") => Ok(Self::Plain), + unknown => Err(anyhow!("cannot parse output format {}", unknown)), } } } @@ -39,3 +38,76 @@ impl fmt::Display for OutputFmt { write!(f, "{}", fmt) } } + +/// Defines a struct-wrapper to provide a JSON output. +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] +pub struct OutputJson { + response: T, +} + +impl OutputJson { + pub fn new(response: T) -> Self { + Self { response } + } +} + +/// Represent the available color configs. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ColorFmt { + Never, + Always, + Ansi, + Auto, +} + +impl Default for ColorFmt { + fn default() -> Self { + Self::Auto + } +} + +impl FromStr for ColorFmt { + type Err = Error; + + fn from_str(fmt: &str) -> Result { + match fmt { + fmt if fmt.eq_ignore_ascii_case("never") => Ok(Self::Never), + fmt if fmt.eq_ignore_ascii_case("always") => Ok(Self::Always), + fmt if fmt.eq_ignore_ascii_case("ansi") => Ok(Self::Ansi), + fmt if fmt.eq_ignore_ascii_case("auto") => Ok(Self::Auto), + unknown => Err(anyhow!("cannot parse color format {}", unknown)), + } + } +} + +impl From for ColorChoice { + fn from(fmt: ColorFmt) -> Self { + match fmt { + ColorFmt::Never => Self::Never, + ColorFmt::Always => Self::Always, + ColorFmt::Ansi => Self::AlwaysAnsi, + ColorFmt::Auto => { + if atty::is(Stream::Stdout) { + // Otherwise let's `termcolor` decide by + // inspecting the environment. From the [doc]: + // + // * If `NO_COLOR` is set to any value, then + // colors will be suppressed. + // + // * If `TERM` is set to dumb, then colors will be + // suppressed. + // + // * In non-Windows environments, if `TERM` is not + // set, then colors will be suppressed. + // + // [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection + Self::Auto + } else { + // Colors should be deactivated if the terminal is + // not a tty. + Self::Never + } + } + } + } +} diff --git a/src/printer/print.rs b/src/printer/print.rs index 54bbde7..7f03a5f 100644 --- a/src/printer/print.rs +++ b/src/printer/print.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Result}; +use himalaya_lib::Tpl; use crate::printer::WriteColor; @@ -19,3 +20,10 @@ impl Print for String { Ok(writer.reset()?) } } + +impl Print for Tpl { + fn print(&self, writer: &mut dyn WriteColor) -> Result<()> { + self.as_str().print(writer)?; + Ok(writer.reset()?) + } +} diff --git a/src/printer/printer.rs b/src/printer/printer.rs index 5a86a67..872176d 100644 --- a/src/printer/printer.rs +++ b/src/printer/printer.rs @@ -1,16 +1,16 @@ -use anyhow::{Context, Result}; -use atty::Stream; +use anyhow::{Context, Error, Result}; +use clap::ArgMatches; use std::fmt::{self, Debug}; -use termcolor::{ColorChoice, StandardStream}; +use termcolor::StandardStream; use crate::{ - output::OutputFmt, + output::{args, ColorFmt, OutputFmt}, printer::{Print, PrintTable, PrintTableOpts, WriteColor}, }; pub trait Printer { - fn print_str(&mut self, data: T) -> Result<()>; - fn print_struct(&mut self, data: T) -> Result<()>; + fn print(&mut self, data: T) -> Result<()>; + fn print_log(&mut self, data: T) -> Result<()>; fn print_table( &mut self, data: Box, @@ -24,41 +24,30 @@ pub struct StdoutPrinter { pub fmt: OutputFmt, } -impl StdoutPrinter { - pub fn from_fmt(fmt: OutputFmt) -> Self { - let writer = StandardStream::stdout(if atty::isnt(Stream::Stdin) { - // Colors should be deactivated if the terminal is not a tty. - ColorChoice::Never - } else { - // Otherwise let's `termcolor` decide by inspecting the environment. From the [doc]: - // - If `NO_COLOR` is set to any value, then colors will be suppressed. - // - If `TERM` is set to dumb, then colors will be suppressed. - // - In non-Windows environments, if `TERM` is not set, then colors will be suppressed. - // - // [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection - ColorChoice::Auto - }); - let writer = Box::new(writer); - Self { writer, fmt } +impl Default for StdoutPrinter { + fn default() -> Self { + let fmt = OutputFmt::default(); + let writer = Box::new(StandardStream::stdout(ColorFmt::default().into())); + Self { fmt, writer } } +} - pub fn from_opt_str(s: Option<&str>) -> Result { - Ok(Self { - fmt: OutputFmt::try_from(s)?, - ..Self::from_fmt(OutputFmt::Plain) - }) +impl StdoutPrinter { + pub fn new(fmt: OutputFmt, color: ColorFmt) -> Self { + let writer = Box::new(StandardStream::stdout(color.into())); + Self { fmt, writer } } } impl Printer for StdoutPrinter { - fn print_str(&mut self, data: T) -> Result<()> { + fn print_log(&mut self, data: T) -> Result<()> { match self.fmt { OutputFmt::Plain => data.print(self.writer.as_mut()), OutputFmt::Json => Ok(()), } } - fn print_struct(&mut self, data: T) -> Result<()> { + fn print(&mut self, data: T) -> Result<()> { match self.fmt { OutputFmt::Plain => data.print(self.writer.as_mut()), OutputFmt::Json => serde_json::to_writer(self.writer.as_mut(), &data) @@ -86,3 +75,29 @@ impl Printer for StdoutPrinter { self.fmt == OutputFmt::Json } } + +impl From for StdoutPrinter { + fn from(fmt: OutputFmt) -> Self { + Self::new(fmt, ColorFmt::Auto) + } +} + +impl TryFrom<&ArgMatches> for StdoutPrinter { + type Error = Error; + + fn try_from(m: &ArgMatches) -> Result { + let fmt: OutputFmt = m + .get_one::(args::ARG_OUTPUT) + .map(String::as_str) + .unwrap() + .parse()?; + + let color: ColorFmt = m + .get_one::(args::ARG_COLOR) + .map(String::as_str) + .unwrap() + .parse()?; + + Ok(Self::new(fmt, color)) + } +} diff --git a/src/ui/editor.rs b/src/ui/editor.rs index e78ac4c..e8da1eb 100644 --- a/src/ui/editor.rs +++ b/src/ui/editor.rs @@ -1,9 +1,9 @@ use anyhow::{Context, Result}; use himalaya_lib::{ - email::{local_draft_path, remove_local_draft, Email, TplOverride}, - AccountConfig, Backend, Sender, + email::{local_draft_path, remove_local_draft}, + AccountConfig, Backend, CompilerBuilder, Flag, Flags, Sender, Tpl, }; -use log::{debug, info}; +use log::debug; use std::{env, fs, process::Command}; use crate::{ @@ -11,7 +11,7 @@ use crate::{ ui::choice::{self, PostEditChoice, PreEditChoice}, }; -pub fn open_with_tpl(tpl: String) -> Result { +pub fn open_with_tpl(tpl: Tpl) -> Result { let path = local_draft_path(); debug!("create draft"); @@ -27,48 +27,34 @@ pub fn open_with_tpl(tpl: String) -> Result { let content = fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?; - Ok(content) + Ok(Tpl::from(content)) } -pub fn open_with_draft() -> Result { +pub fn open_with_local_draft() -> Result { let path = local_draft_path(); - let tpl = + let content = fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?; - open_with_tpl(tpl) + open_with_tpl(Tpl::from(content)) } -fn _edit_email_with_editor( - email: &Email, - tpl: TplOverride, - config: &AccountConfig, -) -> Result { - let tpl = email.to_tpl(tpl, config)?; - let tpl = open_with_tpl(tpl)?; - Email::from_tpl(&tpl).context("cannot parse email from template") -} - -pub fn edit_email_with_editor<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( - mut email: Email, - tpl: TplOverride, +pub fn edit_tpl_with_editor( config: &AccountConfig, printer: &mut P, backend: &mut B, sender: &mut S, + mut tpl: Tpl, ) -> Result<()> { - info!("start editing with editor"); - let draft = local_draft_path(); if draft.exists() { loop { match choice::pre_edit() { Ok(choice) => match choice { PreEditChoice::Edit => { - let tpl = open_with_draft()?; - email.merge_with(Email::from_tpl(&tpl)?); + tpl = open_with_local_draft()?; break; } PreEditChoice::Discard => { - email.merge_with(_edit_email_with_editor(&email, tpl.clone(), config)?); + tpl = open_with_tpl(tpl)?; break; } PreEditChoice::Quit => return Ok(()), @@ -80,35 +66,44 @@ pub fn edit_email_with_editor<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender } } } else { - email.merge_with(_edit_email_with_editor(&email, tpl.clone(), config)?); + tpl = open_with_tpl(tpl)?; } loop { match choice::post_edit() { Ok(PostEditChoice::Send) => { - printer.print_str("Sending email…")?; - let sent_email: Vec = sender.send(&email)?; - let sent_folder = config.folder_alias("sent")?; - printer.print_str(format!("Adding email to the {:?} folder…", sent_folder))?; - backend.email_add(&sent_folder, &sent_email, "seen")?; + printer.print_log("Sending email…")?; + let email = tpl.compile( + CompilerBuilder::default() + .some_pgp_sign_cmd(config.email_writing_sign_cmd.as_ref()) + .some_pgp_encrypt_cmd(config.email_writing_encrypt_cmd.as_ref()), + )?; + sender.send(&email)?; + let sent_folder = config.sent_folder_alias()?; + printer.print_log(format!("Adding email to the {} folder…", sent_folder))?; + backend.add_email(&sent_folder, &email, &Flags::default())?; remove_local_draft()?; - printer.print_struct("Done!")?; + printer.print("Done!")?; break; } Ok(PostEditChoice::Edit) => { - email.merge_with(_edit_email_with_editor(&email, tpl.clone(), config)?); + tpl = open_with_tpl(tpl)?; continue; } Ok(PostEditChoice::LocalDraft) => { - printer.print_struct("Email successfully saved locally")?; + printer.print("Email successfully saved locally")?; break; } Ok(PostEditChoice::RemoteDraft) => { - let tpl = email.to_tpl(TplOverride::default(), config)?; let draft_folder = config.folder_alias("drafts")?; - backend.email_add(&draft_folder, tpl.as_bytes(), "seen draft")?; + let email = tpl.compile( + CompilerBuilder::default() + .some_pgp_sign_cmd(config.email_writing_sign_cmd.as_ref()) + .some_pgp_encrypt_cmd(config.email_writing_encrypt_cmd.as_ref()), + )?; + backend.add_email(&draft_folder, &email, &Flags::from_iter([Flag::Draft]))?; remove_local_draft()?; - printer.print_struct(format!("Email successfully saved to {}", draft_folder))?; + printer.print(format!("Email successfully saved to {}", draft_folder))?; break; } Ok(PostEditChoice::Discard) => { diff --git a/src/ui/table/args.rs b/src/ui/table/args.rs index e9da52b..e29a8f0 100644 --- a/src/ui/table/args.rs +++ b/src/ui/table/args.rs @@ -5,17 +5,17 @@ const ARG_MAX_TABLE_WIDTH: &str = "max-table-width"; pub(crate) type MaxTableWidth = Option; /// Represents the max table width argument. -pub fn max_width<'a>() -> Arg<'a, 'a> { - Arg::with_name(ARG_MAX_TABLE_WIDTH) +pub fn max_width() -> Arg { + Arg::new(ARG_MAX_TABLE_WIDTH) .help("Defines a maximum width for the table") - .short("w") .long("max-width") + .short('w') .value_name("INT") } /// Represents the max table width argument parser. -pub fn parse_max_width<'a>(matches: &'a ArgMatches<'a>) -> Option { +pub fn parse_max_width(matches: &ArgMatches) -> Option { matches - .value_of(ARG_MAX_TABLE_WIDTH) - .and_then(|width| width.parse::().ok()) + .get_one::(ARG_MAX_TABLE_WIDTH) + .and_then(|s| s.parse().ok()) } diff --git a/src/ui/table/table.rs b/src/ui/table/table.rs index 9090e92..de2dbde 100644 --- a/src/ui/table/table.rs +++ b/src/ui/table/table.rs @@ -183,7 +183,7 @@ where table[0].0.iter().map(|cell| cell.unicode_width()).collect(); table.extend( items - .iter() + .into_iter() .map(|item| { let row = item.row(); row.0.iter().enumerate().for_each(|(i, cell)| {