diff --git a/.github/workflows/pre-releases.yml b/.github/workflows/pre-releases.yml deleted file mode 100644 index eef49be8..00000000 --- a/.github/workflows/pre-releases.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Pre-releases - -on: - workflow_dispatch: - push: - branch: master - -jobs: - build: - uses: pimalaya/nix/.github/workflows/pre-releases.yml@master - secrets: inherit - with: - project: himalaya diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index f94de2b8..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: release - -on: - push: - tags: - - v* - -jobs: - create-release: - runs-on: ubuntu-latest - outputs: - upload_url: ${{ steps.create-release.outputs.upload_url }} - steps: - - name: Create release - id: create-release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: ${{ github.ref }} - draft: false - prerelease: false - - deploy-releases: - runs-on: ${{ matrix.os }} - needs: create-release - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-latest - host: x86_64-linux - target: x86_64-linux - - os: ubuntu-latest - host: x86_64-linux - target: aarch64-linux - - os: ubuntu-latest - host: x86_64-linux - target: x86_64-windows - - os: macos-13 - host: x86_64-darwin - target: x86_64-darwin - - os: macos-14 - host: aarch64-darwin - target: aarch64-darwin - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Install Nix - uses: cachix/install-nix-action@v27 - with: - nix_path: nixpkgs=channel:nixos-24.05 - extra_nix_config: | - experimental-features = nix-command flakes - - uses: cachix/cachix-action@v15 - with: - name: soywod - authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' - extraPullNames: nix-community - - name: Build release archive - run: | - nix build -L --expr " - (builtins.getFlake \"git+file://${PWD}?shallow=1&rev=$(git rev-parse HEAD)\") - .outputs.packages.${{ matrix.host }}.${{ matrix.target }}.overrideAttrs { - GIT_DESCRIBE = \"$(git describe)\"; - }" - cp result/himalaya* . - - name: Upload tgz release archive - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: himalaya.tgz - asset_name: himalaya.${{ matrix.target }}.tgz - asset_content_type: application/gzip - - name: Upload zip release archive - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: himalaya.zip - asset_name: himalaya.${{ matrix.target }}.zip - asset_content_type: application/zip diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml new file mode 100644 index 00000000..d2d02426 --- /dev/null +++ b/.github/workflows/releases.yml @@ -0,0 +1,15 @@ +name: Releases + +on: + push: + tags: + - v* + branches: + - master + +jobs: + release: + uses: pimalaya/nix/.github/workflows/releases.yml@master + secrets: inherit + with: + project: himalaya diff --git a/CHANGELOG.md b/CHANGELOG.md index a537eda5..e7d0d888 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.0] - 2024-12-09 + +The Himalaya CLI scope has changed. It does not include anymore the synchronization, nor the envelope watching. These scopes have moved to dedicated projects: + +- [Neverest CLI](https://github.com/pimalaya/neverest), CLI to synchronize, backup and restore emails +- [Mirador CLI](https://github.com/pimalaya/mirador), CLI to watch mailbox changes + +Due to the long time difference with the previous `v1.0.0-beta.4` release, this changelog may be incomplete. The simplest way to upgrade is to reconfigure Himalaya CLI from scratch, using the wizard or the [`config.sample.toml`](./config.sample.toml). + +Himalaya CLI will now try to adopt the [conventional commits specification](https://github.com/conventional-commits/conventionalcommits.org). Tools like [`git-cliff`](https://git-cliff.org/) may help us generating more accurate changelogs in the future. + ### Added +- Added `message edit` command to edit a message. To edit on place (replace a message), use `--on-place`. - Added `account.list.table.preset` global config option, `accounts..folder.list.table.preset` and `accounts..envelope.list.table.preset` account config options. These options customize the shape of tables, see examples at [`comfy_table::presets`](https://docs.rs/comfy-table/latest/comfy_table/presets/index.html). Defaults to `"|| |-||| "`, which corresponds to [`comfy_table::presets::ASCII_MARKDOWN`](https://docs.rs/comfy-table/latest/comfy_table/presets/constant.ASCII_MARKDOWN.html). @@ -30,6 +42,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Refactored the `account configure` command: this command stands now for creating or editing account configurations from the wizard. The command requires the `wizard` cargo feature. +- Improved the `account doctor` command: it now checks the state of the config, and the new `--fix` argument allows you to configure keyring, OAuth 2.0 etc. - Improved long version `--version`. [#496] - Improved error messages when missing cargo features. For example, if a TOML configuration uses the IMAP backend without the `imap` cargo features, the error `missing "imap" feature` is displayed. [#20](https://github.com/pimalaya/core/issues/20) - Normalized enum-based configurations, using the [internally tagged representation](https://serde.rs/enum-representations.html#internally-tagged) `type =`. It should reduce issues due to misconfiguration, and improve othe error messages. Yet it is not perfect, see [#802](https://github.com/toml-rs/toml/issues/802): @@ -86,12 +100,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 message.send.backend.auth.method = "xoauth2" ``` +- Moved IMAP and SMTP `encryption` to `encryption.type`. + + This change prepares the config to accept different TLS providers with their options. The `true` and `false` variant have been removed as well: + + ```toml + # before + backend.encryption = "none" # or false + backend.encryption = "start-tls" + message.send.backend.encryption = "tls" # or true + + # after + backend.encryption.type = "none" + backend.encryption.type = "start-tls" + message.send.backend.encryption.type = "tls" + ``` + ### Fixed - Fixed pre-release archives issue. [#492] - Fixed mailto parsing issue. [core#10] - Fixed `Answered` flag not set when replying to a message. [#508] +### Removed + +- Removed systemd service from `assets/` folder, as Himalaya CLI scope does not include synchronization nor watching anymore. + ## [1.0.0-beta.4] - 2024-04-16 ### Added @@ -856,7 +890,10 @@ Few major concepts changed: - Password from command - Set up README -[Unreleased]: https://github.com/soywod/himalaya/compare/v1.0.0-beta.2...HEAD +[Unreleased]: https://github.com/soywod/himalaya/compare/v1.0.0...HEAD +[1.0.0]: https://github.com/soywod/himalaya/compare/v1.0.0-beta.4...v1.0.0 +[1.0.0-beta.4]: https://github.com/soywod/himalaya/compare/v1.0.0-beta.3...v1.0.0-beta.4 +[1.0.0-beta.3]: https://github.com/soywod/himalaya/compare/v1.0.0-beta.2...v1.0.0-beta.3 [1.0.0-beta.2]: https://github.com/soywod/himalaya/compare/v1.0.0-beta...v1.0.0-beta.2 [1.0.0-beta]: https://github.com/soywod/himalaya/compare/v0.9.0...v1.0.0-beta [0.9.0]: https://github.com/soywod/himalaya/compare/v0.8.4...v0.9.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5976d5ca..4ad0fa0f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,15 +17,6 @@ $ cargo build $ cargo run --feature pgp-gpg -- envelope list ``` -## Contributing +## Commit style -Himalaya CLI supports open-source, hence the choice of using [SourceHut](https://sourcehut.org/) for managing the project. The only reason why the source code is hosted on GitHub is to build releases for all major platforms (using GitHub Actions). Don't worry, contributing on SourceHut is not a big deal: you just need to send emails! You don't need to create any account. Here a small comparison guide with GitHub: - -The equivalent of **GitHub Discussions** are: - -- The [Matrix](https://matrix.org/) chat room [#pimalaya.himalaya](https://matrix.to/#/#pimalaya.himalaya:matrix.org) -- The SourceHut mailing list. You can consult existing messages [here](https://lists.sr.ht/~soywod/pimalaya). You can "open a new discussion" by sending an email at [~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht). You can also [subscribe](mailto:~soywod/pimalaya+subscribe@lists.sr.ht) and [unsubscribe](mailto:~soywod/pimalaya+unsubscribe@lists.sr.ht) to the mailing list, so you can receive a copy of all discussions. - -The equivalent of **GitHub Issues** is the SourceHut bug tracker. You can consult existing bugs [here](https://todo.sr.ht/~soywod/pimalaya), and you can "open a new issue" by sending an email at [~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht). - -The equivalent of **GitHub Pull requests** is the SourceHut mailing list. You can "open a new pull request" by sending an email containing a git patch at [~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht). The simplest way to send a patch is to use [git send-email](https://git-scm.com/docs/git-send-email), follow [this guide](https://git-send-email.io/) to configure git properly. +Starting from the `v1.0.0`, Himalaya CLI tries to adopt the [conventional commits specification](https://github.com/conventional-commits/conventionalcommits.org). diff --git a/Cargo.lock b/Cargo.lock index 201b38f9..0eedf354 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,9 +78,9 @@ checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" [[package]] name = "allocator-api2" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-tzdata" @@ -261,7 +261,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -296,7 +296,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -432,7 +432,7 @@ checksum = "e0af050e27e5d57aa14975f97fe47a134c46a390f91819f23a625319a7111bfa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -509,9 +509,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" +checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" dependencies = [ "jobserver", "libc", @@ -557,9 +557,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -594,9 +594,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.21" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -604,9 +604,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.21" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -633,14 +633,14 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] name = "clap_lex" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clap_mangen" @@ -917,7 +917,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -1143,7 +1143,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -1219,8 +1219,9 @@ dependencies = [ [[package]] name = "email-lib" -version = "0.26.0" -source = "git+https://github.com/pimalaya/core#e83f761fcb5fcd73659e933648d40dcca573da44" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8625e58d8fd41ac4f497ead618664d8ab7b86291cffbbf6feb2457c52561e721" dependencies = [ "async-trait", "chrono", @@ -1231,12 +1232,12 @@ dependencies = [ "hickory-resolver", "http-lib", "imap-client", - "imap-next", "keyring-lib", "mail-builder", "mail-parser", "mail-send", "maildirs", + "mime_guess", "mml-lib", "notify", "notmuch", @@ -1255,11 +1256,12 @@ dependencies = [ "shellexpand-utils", "thiserror 1.0.69", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls 0.26.1", "tracing", "tree_magic_mini", "urlencoding", "utf7-imap", + "uuid 1.11.0", ] [[package]] @@ -1269,7 +1271,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f24a09fd651027f8764f8a12c12358715cb9bab622ab3125ede3dd6ae047c95" dependencies = [ "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -1302,7 +1304,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -1323,7 +1325,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -1355,9 +1357,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" dependencies = [ "event-listener", "pin-project-lite", @@ -1375,9 +1377,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "ff" @@ -1538,7 +1540,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -1716,12 +1718,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "hermit-abi" version = "0.4.0" @@ -1797,6 +1793,7 @@ dependencies = [ "himalaya", "mml-lib", "once_cell", + "open", "pimalaya-tui", "secret-lib", "serde", @@ -1806,7 +1803,7 @@ dependencies = [ "toml", "tracing", "url", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -1840,9 +1837,9 @@ dependencies = [ [[package]] name = "http" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" dependencies = [ "bytes", "fnv", @@ -1852,7 +1849,8 @@ dependencies = [ [[package]] name = "http-lib" version = "0.1.0" -source = "git+https://github.com/pimalaya/core#e83f761fcb5fcd73659e933648d40dcca573da44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "994cbd23c90551cb5821d1c9d9b1e41383f338b31fc122671edc7d1695a61338" dependencies = [ "thiserror 1.0.69", "tokio", @@ -2004,7 +2002,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -2055,17 +2053,16 @@ dependencies = [ [[package]] name = "imap-client" -version = "0.1.5" -source = "git+https://github.com/pimalaya/imap-client#bca4048458585a4c3ed8e0fc71b82f8835ab8286" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "072d1848cdf0d9b2e1632cea3d22a0f935a610e7a9ea14e7c8b37e85fa131495" dependencies = [ - "futures-util", "imap-next", - "rustls-platform-verifier", - "start-tls", - "thiserror 2.0.3", + "rip-starttls", + "rustls-platform-verifier 0.4.0", + "thiserror 2.0.6", "tokio", - "tokio-rustls 0.26.0", - "tokio-util", + "tokio-rustls 0.26.1", "tracing", ] @@ -2085,12 +2082,15 @@ dependencies = [ [[package]] name = "imap-next" -version = "0.3.0" -source = "git+https://github.com/soywod/imap-next?branch=jakoschiko_futures-stream-without-split#5addf10140ce81e906f114ca2fc767b1c69428a4" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d85520e742d9e8d9edbf9df9e0876f560ed08650db8f9de562bc7cd46b9b43" dependencies = [ - "futures-util", + "bytes", "imap-codec", - "thiserror 1.0.69", + "thiserror 2.0.6", + "tokio", + "tokio-rustls 0.26.1", "tracing", ] @@ -2116,9 +2116,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -2198,6 +2198,25 @@ version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -2241,10 +2260,11 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -2277,7 +2297,8 @@ dependencies = [ [[package]] name = "keyring-lib" version = "1.0.2" -source = "git+https://github.com/pimalaya/core#e83f761fcb5fcd73659e933648d40dcca573da44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56921558c465f33d51c6047b86b76764cd2a86b69b99653b43ba1f9a32965218" dependencies = [ "keyring", "once_cell", @@ -2318,9 +2339,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.167" +version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "libdbus-sys" @@ -2466,7 +2487,7 @@ dependencies = [ "rustls-pki-types", "smtp-proto", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls 0.26.1", "webpki-roots", ] @@ -2536,6 +2557,22 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2574,11 +2611,10 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi 0.3.9", "libc", "wasi", "windows-sys 0.52.0", @@ -2586,8 +2622,9 @@ dependencies = [ [[package]] name = "mml-lib" -version = "1.1.0" -source = "git+https://github.com/pimalaya/core#e83f761fcb5fcd73659e933648d40dcca573da44" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e7cb0490a34b47734055fd39a6df4349fcaf9ee425208e48172e443936c590" dependencies = [ "async-recursion", "chumsky", @@ -2739,7 +2776,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -2785,8 +2822,9 @@ dependencies = [ [[package]] name = "oauth-lib" -version = "1.0.0" -source = "git+https://github.com/pimalaya/core#e83f761fcb5fcd73659e933648d40dcca573da44" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a367f5fbc5e9d1971e3dae045c7d6c8b1f9984691d2c1624882b40057f92a" dependencies = [ "http-lib", "oauth2", @@ -2829,6 +2867,17 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "open" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ecd52f0b8d15c40ce4820aa251ed5de032e5d91fab27f7db2f40d42a8bdf69c" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "openssl-probe" version = "0.1.5" @@ -2979,6 +3028,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -3059,7 +3114,8 @@ dependencies = [ [[package]] name = "pgp-lib" version = "1.0.0" -source = "git+https://github.com/pimalaya/core#e83f761fcb5fcd73659e933648d40dcca573da44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b1a64cb843daaa31d7109d77871ba6fcaa25be5b488c97eeb28fc378e5fed7" dependencies = [ "async-recursion", "futures", @@ -3076,8 +3132,9 @@ dependencies = [ [[package]] name = "pimalaya-tui" -version = "0.1.0" -source = "git+https://github.com/pimalaya/tui#7acc0db96b4a131a679928cc069b8e89dbc5dc43" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "921add1c8f4ca3ad7f5ff3ea6f839ae08466ad39090627914db73ab8473bc4c7" dependencies = [ "async-trait", "clap", @@ -3092,6 +3149,7 @@ dependencies = [ "md5", "mml-lib", "oauth-lib", + "once_cell", "petgraph", "process-lib", "secret-lib", @@ -3100,7 +3158,7 @@ dependencies = [ "serde_json", "shellexpand-utils", "sled", - "thiserror 2.0.3", + "thiserror 2.0.6", "tokio", "toml", "toml_edit", @@ -3167,7 +3225,7 @@ checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.4.0", + "hermit-abi", "pin-project-lite", "rustix", "tracing", @@ -3237,7 +3295,8 @@ dependencies = [ [[package]] name = "process-lib" version = "1.0.0" -source = "git+https://github.com/pimalaya/core#e83f761fcb5fcd73659e933648d40dcca573da44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb47ed33aeaf6b32cecbbde6f56dde6c8740f2dac4a146179cc82f797918c46" dependencies = [ "serde", "thiserror 1.0.69", @@ -3435,6 +3494,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rip-starttls" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edf1f62c7965137bde00221c63e20e511bb4d576e2ddf9bdf57e1d03042ba112" +dependencies = [ + "tokio", + "tracing", +] + [[package]] name = "ripemd" version = "0.1.3" @@ -3487,15 +3556,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.41" +version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3583,6 +3652,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c7dc240fec5517e6c4eab3310438636cfe6391dfc345ba013109909a90d136" +dependencies = [ + "core-foundation 0.9.4", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.19", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki 0.102.8", + "security-framework 2.11.1", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.52.0", +] + [[package]] name = "rustls-platform-verifier-android" version = "0.1.1" @@ -3673,7 +3763,8 @@ dependencies = [ [[package]] name = "secret-lib" version = "1.0.0" -source = "git+https://github.com/pimalaya/core#e83f761fcb5fcd73659e933648d40dcca573da44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba34b385def61154faed219ac86f80ad8c72877a278208bac2ecdd85d68f962f" dependencies = [ "keyring-lib", "process-lib", @@ -3782,7 +3873,7 @@ checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -3815,7 +3906,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -4005,15 +4096,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "start-tls" -version = "0.1.0" -source = "git+https://github.com/pimalaya/core#e83f761fcb5fcd73659e933648d40dcca573da44" -dependencies = [ - "futures-util", - "tracing", -] - [[package]] name = "static_assertions" version = "1.1.0" @@ -4054,7 +4136,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -4076,9 +4158,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.89" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -4093,7 +4175,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -4130,9 +4212,9 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" dependencies = [ "rustix", "windows-sys 0.59.0", @@ -4149,11 +4231,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.3" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" dependencies = [ - "thiserror-impl 2.0.3", + "thiserror-impl 2.0.6", ] [[package]] @@ -4164,18 +4246,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] name = "thiserror-impl" -version = "2.0.3" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -4215,14 +4297,14 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.41.1" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes", "libc", - "mio 1.0.2", + "mio 1.0.3", "pin-project-lite", "signal-hook-registry", "socket2", @@ -4239,7 +4321,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -4254,26 +4336,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ "rustls 0.23.19", - "rustls-pki-types", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "pin-project-lite", "tokio", ] @@ -4330,7 +4397,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -4345,9 +4412,9 @@ dependencies = [ [[package]] name = "tracing-error" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" dependencies = [ "tracing", "tracing-subscriber", @@ -4366,9 +4433,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", @@ -4421,6 +4488,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "unicase" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" + [[package]] name = "unicode-bidi" version = "0.3.17" @@ -4481,7 +4554,7 @@ dependencies = [ "rustls 0.23.19", "rustls-pemfile 2.2.0", "rustls-pki-types", - "rustls-platform-verifier", + "rustls-platform-verifier 0.3.4", "ureq-proto", "utf-8", "webpki-roots", @@ -4561,6 +4634,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", +] + [[package]] name = "valuable" version = "0.1.0" @@ -4603,9 +4685,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", @@ -4614,24 +4696,23 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4639,22 +4720,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + +[[package]] +name = "webpki-root-certs" +version = "0.26.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd5da49bdf1f30054cfe0b8ce2958b8fbeb67c4d82c8967a598af481bef255c" +dependencies = [ + "rustls-pki-types", +] [[package]] name = "webpki-roots" @@ -4933,9 +5023,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.23" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af310deaae937e48a26602b730250b4949e125f468f11e6990be3e5304ddd96f" +checksum = "ea8b391c9a790b496184c29f7f93b9ed5b16abb306c05415b68bcc16e4d06432" [[package]] name = "yansi" @@ -4963,7 +5053,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", "synstructure", ] @@ -5021,7 +5111,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", "zvariant_utils", ] @@ -5054,7 +5144,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -5074,7 +5164,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", "synstructure", ] @@ -5095,7 +5185,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -5117,7 +5207,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] [[package]] @@ -5142,7 +5232,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", "zvariant_utils", ] @@ -5154,5 +5244,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.90", ] diff --git a/Cargo.toml b/Cargo.toml index ace5ecbe..818f1161 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ pgp-gpg = ["email-lib/pgp-gpg", "mml-lib/pgp-gpg", "pimalaya-tui/pgp-gpg"] pgp-native = ["email-lib/pgp-native", "mml-lib/pgp-native", "pimalaya-tui/pgp-native"] [build-dependencies] -pimalaya-tui = { version = "=0.1", default-features = false, features = ["build-envs"] } +pimalaya-tui = { version = "0.2", default-features = false, features = ["build-envs"] } [dev-dependencies] himalaya = { path = ".", features = ["notmuch", "keyring", "oauth2", "pgp-gpg", "pgp-native"] } @@ -41,10 +41,11 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] } clap_complete = "4.4" clap_mangen = "0.2" color-eyre = "0.6" -email-lib = { version = "=0.26", default-features = false, features = ["tokio-rustls", "derive", "thread"] } +email-lib = { version = "0.26", default-features = false, features = ["tokio-rustls", "derive", "thread"] } mml-lib = { version = "1", default-features = false, features = ["compiler", "interpreter", "derive"] } once_cell = "1.16" -pimalaya-tui = { version = "=0.1", default-features = false, features = ["email", "path", "cli", "himalaya", "tracing", "sled"] } +open = "5.3" +pimalaya-tui = { version = "0.2", default-features = false, features = ["rustls", "email", "path", "cli", "himalaya", "tracing", "sled"] } secret-lib = { version = "1", default-features = false, features = ["tokio", "rustls", "command", "derive"] } serde = { version = "1", features = ["derive"] } serde_json = "1" @@ -54,12 +55,3 @@ toml = "0.8" tracing = "0.1" url = "2.2" uuid = { version = "0.8", features = ["v4"] } - -[patch.crates-io] -email-lib = { git = "https://github.com/pimalaya/core" } -keyring-lib = { git = "https://github.com/pimalaya/core" } -mml-lib = { git = "https://github.com/pimalaya/core" } -oauth-lib = { git = "https://github.com/pimalaya/core" } -pimalaya-tui = { git = "https://github.com/pimalaya/tui" } -process-lib = { git = "https://github.com/pimalaya/core" } -secret-lib = { git = "https://github.com/pimalaya/core" } diff --git a/README.md b/README.md index 88a30b62..b2ea757f 100644 --- a/README.md +++ b/README.md @@ -17,49 +17,31 @@ $ himalaya envelope list --account posteo --folder Archives.FOSS --page 2 ## Features -- Multi-accounting -- Interactive configuration via **wizard** (requires `wizard` feature) -- Mailbox, envelope, message and flag management +- Multi-accounting configuration: + - interactive via **wizard** (requires `wizard` feature) + - manual via **TOML**-based configuration file (see [`./config.sample.toml`](./config.sample.toml)) - Message composition based on `$EDITOR` - **IMAP** backend (requires `imap` feature) - **Maildir** backend (requires `maildir` feature) - **Notmuch** backend (requires `notmuch` feature) - **SMTP** backend (requires `smtp` feature) - **Sendmail** backend (requires `sendmail` feature) -- Global system **keyring** for managing secrets (requires `keyring` feature) -- **OAuth 2.0** authorization (requires `oauth2` feature) +- Global system **keyring** for secret management (requires `keyring` feature) +- **OAuth 2.0** authorization flow (requires `oauth2` feature) - **JSON** output via `--output json` - **PGP** encryption: - via shell commands (requires `pgp-commands` feature) - via [GPG](https://www.gnupg.org/) bindings (requires `pgp-gpg` feature) - via native implementation (requires `pgp-native` feature) -*Himalaya CLI is written in [Rust](https://www.rust-lang.org/), and relies on [cargo features](https://doc.rust-lang.org/cargo/reference/features.html) to enable or disable functionalities. Default features can be found in the `features` section of the [`Cargo.toml`](https://github.com/pimalaya/himalaya/blob/master/Cargo.toml#L18).* +*Himalaya CLI is written in [Rust](https://www.rust-lang.org/), and relies on [cargo features](https://doc.rust-lang.org/cargo/reference/features.html) to enable or disable functionalities. Default features can be found in the `features` section of the [`Cargo.toml`](./Cargo.toml#L18), or on [docs.rs](https://docs.rs/crate/himalaya/latest/features).* ## Installation -*The `v1.0.0` is currently being tested on the `master` branch, and is the prefered version to use. Previous versions (including GitHub beta releases and repositories published versions) are not recommended.* - -### Pre-built binary - -Himalaya CLI `v1.0.0` can be installed with a pre-built binary. Find the latest [`pre-releases`](https://github.com/pimalaya/himalaya/actions/workflows/pre-releases.yml) GitHub workflow and look for the *Artifacts* section. You should find a pre-built binary matching your OS. - -### Cargo (git) - -Himalaya CLI `v1.0.0` can also be installed with [cargo](https://doc.rust-lang.org/cargo/): - -```bash -$ cargo install --frozen --force --git https://github.com/pimalaya/himalaya.git -``` - -### Other outdated methods - -These installation methods should not be used until the `v1.0.0` is finally released, as they are all (temporarily) outdated: -
Pre-built binary - Himalaya CLI can be installed with a prebuilt binary: + Himalaya CLI can be installed with the installer: ```bash # As root: @@ -71,7 +53,9 @@ These installation methods should not be used until the `v1.0.0` is finally rele These commands install the latest binary from the GitHub [releases](https://github.com/pimalaya/himalaya/releases) section. - *Binaries are built with [default](https://github.com/pimalaya/himalaya/blob/master/Cargo.toml#L18) cargo features. If you want to enable or disable a feature, please use another installation method.* + If you want a more up-to-date version than the latest release, check out the [`releases`](https://github.com/pimalaya/himalaya/actions/workflows/releases.yml) GitHub workflow and look for the *Artifacts* section. You should find a pre-built binary matching your OS. These pre-built binaries are built from the `master` branch. + + *Such binaries are built with the default cargo features. If you want to enable or disable a feature, please use another installation method.*
@@ -89,7 +73,7 @@ These installation methods should not be used until the `v1.0.0` is finally rele You can also use the git repository for a more up-to-date (but less stable) version: ```bash - $ cargo install --git https://github.com/pimalaya/himalaya.git himalaya + $ cargo install --frozen --force --git https://github.com/pimalaya/himalaya.git ```
@@ -215,6 +199,8 @@ These installation methods should not be used until the `v1.0.0` is finally rele Just run `himalaya`, the wizard will help you to configure your default account. +Accounts can be (re)configured via the wizard using the command `himalaya account configure `. + You can also manually edit your own configuration, from scratch: - Copy the content of the documented [`./config.sample.toml`](./config.sample.toml) @@ -237,7 +223,7 @@ You can also manually edit your own configuration, from scratch: backend.type = "imap" backend.host = "127.0.0.1" backend.port = 1143 - backend.encryption = false + backend.encryption.type = "none" backend.login = "example@proton.me" backend.auth.type = "password" backend.auth.raw = "*****" @@ -245,7 +231,7 @@ You can also manually edit your own configuration, from scratch: message.send.backend.type = "smtp" message.send.backend.host = "127.0.0.1" message.send.backend.port = 1025 - message.send.backend.encryption = false + message.send.backend.encryption.type = "none" message.send.backend.login = "example@proton.me" message.send.backend.auth.type = "password" message.send.backend.auth.raw = "*****" @@ -387,7 +373,7 @@ You can also manually edit your own configuration, from scratch: message.send.backend.type = "smtp" message.send.backend.host = "smtp-mail.outlook.com" message.send.backend.port = 587 - message.send.backend.encryption = "start-tls" + message.send.backend.encryption.type = "start-tls" message.send.backend.login = "example@outlook.com" message.send.backend.auth.type = "password" message.send.backend.auth.raw = "*****" @@ -474,7 +460,7 @@ You can also manually edit your own configuration, from scratch: message.send.backend.type = "smtp" message.send.backend.host = "smtp.mail.me.com" message.send.backend.port = 587 - message.send.backend.encryption = "start-tls" + message.send.backend.encryption.type = "start-tls" message.send.backend.login = "johnappleseed@icloud.com" message.send.backend.auth.type = "password" message.send.backend.auth.raw = "*****" diff --git a/assets/himalaya-watch@.service b/assets/himalaya-watch@.service deleted file mode 100644 index dd7e88e0..00000000 --- a/assets/himalaya-watch@.service +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -Description=Email client Himalaya CLI envelopes watcher service -After=network.target - -[Service] -Type=exec -ExecStart=%install_dir%/himalaya envelopes watch --account %i -ExecSearchPath=/bin -Restart=always -RestartSec=10 - -[Install] -WantedBy=default.target diff --git a/config.sample.toml b/config.sample.toml index 0b16aa54..45037ce8 100644 --- a/config.sample.toml +++ b/config.sample.toml @@ -1,5 +1,5 @@ ################################################################################ -#### Global configuration ###################################################### +###[ Global configuration ]##################################################### ################################################################################ # Default display name for all accounts. It is used to build the full @@ -11,11 +11,11 @@ display-name = "Example" # bottom of all messages. It can be a path or a string. Supports TOML # multilines. # -# signature = "/path/to/signature/file" -# signature = """ -# Thanks you, -# Regards -# """ +#signature = "/path/to/signature/file" +#signature = """ +# Thanks you, +# Regards +#""" signature = "Regards,\n" # Default signature delimiter for all accounts. It delimits the end of @@ -27,10 +27,10 @@ signature-delim = "-- \n" # for downloading attachments. Defaults to the system temporary # directory. # -downloads-dir = "~/downloads" +downloads-dir = "~/Downloads" -# Customizes the charset used to build the table. Defaults to markdown -# table style. +# Customizes the charset used to build the accounts listing +# table. Defaults to markdown table style. # # See . # @@ -52,9 +52,11 @@ account.list.table.backends-color = "blue" account.list.table.default-color = "black" ################################################################################ -#### Account configuration ##################################################### +###[ Account configuration ]#################################################### ################################################################################ +# The account name should be unique. +# [accounts.example] # Defaultness of the account. The current account will be used by @@ -88,11 +90,10 @@ signature-delim = "-- \n" # Downloads directory path. It is mostly used for downloading # attachments. Defaults to the system temporary directory. +# downloads-dir = "~/downloads" -######################################## -#### Folder configuration ############## -######################################## + # Defines aliases for your mailboxes. There are 4 special aliases used # by the tool: inbox, sent, drafts and trash. Other aliases can be @@ -123,9 +124,7 @@ folder.list.table.name-color = "blue" # folder.list.table.desc-color = "green" -######################################## -#### Envelope configuration ############ -######################################## + # Customizes the number of envelopes to show by page. # @@ -194,9 +193,7 @@ envelope.list.table.sender-color = "blue" # envelope.list.table.date-color = "yellow" -######################################## -#### Message configuration ############# -######################################## + # Defines headers to show at the top of messages when reading them. # @@ -233,9 +230,7 @@ message.send.pre-hook = "process-markdown.sh" #message.delete.style = "flag" message.delete.style = "folder" -######################################## -#### Template configuration ############ -######################################## + # Defines how and where the signature should be displayed when writing # a new message. @@ -287,18 +282,14 @@ template.forward.signature-style = "inlined" # template.forward.quote-headline = "-------- Forwarded Message --------\n" -######################################## -#### GPG-based PGP configuration ####### -######################################## + # Enables PGP using GPG bindings. It requires the GPG lib to be # installed on the system, and the `pgp-gpg` cargo feature on. # #pgp.type = "gpg" -######################################## -#### Command-based PGP configuration ### -######################################## + # Enables PGP using shell commands. A PGP client needs to be installed # on the system, like gpg. It also requires the `pgp-commands` cargo @@ -334,9 +325,7 @@ template.forward.quote-headline = "-------- Forwarded Message --------\n" # #pgp.verify-cmd = "gpg --verify --quiet" -######################################## -#### Native PGP configuration ########## -######################################## + # Enables the native Rust implementation of PGP. It requires the # `pgp-native` cargo feature. @@ -363,9 +352,7 @@ template.forward.quote-headline = "-------- Forwarded Message --------\n" # #pgp.key-servers = ["hkps://keys.openpgp.org", "hkps://keys.mailvelope.com"] -######################################## -#### IMAP configuration ################ -######################################## + # Defines the IMAP backend as the default one for all features. # @@ -382,9 +369,9 @@ backend.port = 993 # IMAP server encryption. # -#backend.encryption = "none" # or false -#backend.encryption = "start-tls" -backend.encryption = "tls" # or true +#backend.encryption.type = "none" +#backend.encryption.type = "start-tls" +backend.encryption.type = "tls" # IMAP server login. # @@ -488,9 +475,7 @@ backend.auth.cmd = "pass show example-imap" # #backend.auth.redirect-port = 9999 -######################################## -#### Maildir configuration ############# -######################################## + # Defines the Maildir backend as the default one for all features. # @@ -507,9 +492,7 @@ backend.auth.cmd = "pass show example-imap" # #backend.maildirpp = false -######################################## -#### Notmuch configuration ############# -######################################## + # Defines the Notmuch backend as the default one for all features. # @@ -533,9 +516,7 @@ backend.auth.cmd = "pass show example-imap" # #backend.profile = "example" -######################################## -#### SMTP configuration ################ -######################################## + # Defines the SMTP backend for the message sending feature. # @@ -553,9 +534,9 @@ message.send.backend.port = 587 # SMTP server encryption. # -#message.send.backend.encryption = "none" # or false -#message.send.backend.encryption = "start-tls" -message.send.backend.encryption = "tls" # or true +#message.send.backend.encryption.type = "none" +#message.send.backend.encryption.type = "start-tls" +message.send.backend.encryption.type = "tls" # SMTP server login. # @@ -659,9 +640,7 @@ message.send.backend.auth.cmd = "pass show example-smtp" # #message.send.backend.auth.redirect-port = 9999 -######################################## -#### Sendmail configuration ############ -######################################## + # Defines the Sendmail backend for the message sending feature. # diff --git a/install.sh b/install.sh index 11d8c19a..cecbf8b6 100644 --- a/install.sh +++ b/install.sh @@ -9,7 +9,7 @@ die() { DESTDIR="${DESTDIR:-}" PREFIX="${PREFIX:-"$DESTDIR/usr/local"}" -RELEASES_URL="https://github.com/soywod/himalaya/releases" +RELEASES_URL="https://github.com/pimalaya/himalaya/releases" binary=himalaya system=$(uname -s | tr [:upper:] [:lower:]) @@ -23,14 +23,17 @@ case $system in linux|freebsd) case $machine in x86_64) target=x86_64-linux;; + x86|i386|i686) target=i686-linux;; arm64|aarch64) target=aarch64-linux;; + armv6l) target=armv6l-linux;; + armv7l) target=armv7l-linux;; *) die "Unsupported machine $machine for system $system";; esac;; darwin) case $machine in - x86_64) target=x86_64-macos;; - arm64|aarch64) target=aarch64-macos;; + x86_64) target=x86_64-darwin;; + arm64|aarch64) target=aarch64-darwin;; *) die "Unsupported machine $machine for system $system";; esac;; diff --git a/src/account/command/check_up.rs b/src/account/command/check_up.rs deleted file mode 100644 index 0b0c6921..00000000 --- a/src/account/command/check_up.rs +++ /dev/null @@ -1,111 +0,0 @@ -use std::sync::Arc; - -use clap::Parser; -use color_eyre::Result; -#[cfg(feature = "imap")] -use email::imap::ImapContextBuilder; -#[cfg(feature = "maildir")] -use email::maildir::MaildirContextBuilder; -#[cfg(feature = "notmuch")] -use email::notmuch::NotmuchContextBuilder; -#[cfg(feature = "sendmail")] -use email::sendmail::SendmailContextBuilder; -#[cfg(feature = "smtp")] -use email::smtp::SmtpContextBuilder; -use email::{backend::BackendBuilder, config::Config}; -use pimalaya_tui::{ - himalaya::config::{Backend, SendingBackend}, - terminal::{cli::printer::Printer, config::TomlConfig as _}, -}; -use tracing::info; - -use crate::{account::arg::name::OptionalAccountNameArg, config::TomlConfig}; - -/// Check up the given account. -/// -/// This command performs a checkup of the given account. It checks if -/// the configuration is valid, if backend can be created and if -/// sessions work as expected. -#[derive(Debug, Parser)] -pub struct AccountCheckUpCommand { - #[command(flatten)] - pub account: OptionalAccountNameArg, -} - -impl AccountCheckUpCommand { - pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { - info!("executing check up account command"); - - printer.log("Checking configuration integrity…\n")?; - - let (toml_account_config, account_config) = config - .clone() - .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { - c.account(name).ok() - })?; - let account_config = Arc::new(account_config); - - match toml_account_config.backend { - #[cfg(feature = "maildir")] - Some(Backend::Maildir(mdir_config)) => { - printer.log("Checking Maildir integrity…\n")?; - - let ctx = MaildirContextBuilder::new(account_config.clone(), Arc::new(mdir_config)); - BackendBuilder::new(account_config.clone(), ctx) - .check_up() - .await?; - } - #[cfg(feature = "imap")] - Some(Backend::Imap(imap_config)) => { - printer.log("Checking IMAP integrity…\n")?; - - let ctx = ImapContextBuilder::new(account_config.clone(), Arc::new(imap_config)) - .with_pool_size(1); - BackendBuilder::new(account_config.clone(), ctx) - .check_up() - .await?; - } - #[cfg(feature = "notmuch")] - Some(Backend::Notmuch(notmuch_config)) => { - printer.log("Checking Notmuch integrity…\n")?; - - let ctx = - NotmuchContextBuilder::new(account_config.clone(), Arc::new(notmuch_config)); - BackendBuilder::new(account_config.clone(), ctx) - .check_up() - .await?; - } - _ => (), - } - - let sending_backend = toml_account_config - .message - .and_then(|msg| msg.send) - .and_then(|send| send.backend); - - match sending_backend { - #[cfg(feature = "smtp")] - Some(SendingBackend::Smtp(smtp_config)) => { - printer.log("Checking SMTP integrity…\n")?; - - let ctx = SmtpContextBuilder::new(account_config.clone(), Arc::new(smtp_config)); - BackendBuilder::new(account_config.clone(), ctx) - .check_up() - .await?; - } - #[cfg(feature = "sendmail")] - Some(SendingBackend::Sendmail(sendmail_config)) => { - printer.log("Checking Sendmail integrity…\n")?; - - let ctx = - SendmailContextBuilder::new(account_config.clone(), Arc::new(sendmail_config)); - BackendBuilder::new(account_config.clone(), ctx) - .check_up() - .await?; - } - _ => (), - } - - printer.out("Checkup successfully completed!\n") - } -} diff --git a/src/account/command/configure.rs b/src/account/command/configure.rs index 9faa147c..be2c9390 100644 --- a/src/account/command/configure.rs +++ b/src/account/command/configure.rs @@ -1,130 +1,52 @@ +use std::path::PathBuf; + use clap::Parser; use color_eyre::Result; -#[cfg(feature = "imap")] -use email::imap::config::ImapAuthConfig; -#[cfg(feature = "smtp")] -use email::smtp::config::SmtpAuthConfig; -#[cfg(any( - feature = "imap", - feature = "smtp", - feature = "pgp-gpg", - feature = "pgp-commands", - feature = "pgp-native", -))] -use pimalaya_tui::terminal::prompt; -use pimalaya_tui::terminal::{cli::printer::Printer, config::TomlConfig as _}; -use tracing::info; -#[cfg(any(feature = "imap", feature = "smtp"))] -use tracing::{debug, warn}; use crate::{account::arg::name::AccountNameArg, config::TomlConfig}; -/// Configure an account. +/// Configure the given account. /// -/// This command is mostly used to define or reset passwords managed -/// by your global keyring. If you do not use the keyring system, you -/// can skip this command. +/// This command allows you to configure an existing account or to +/// create a new one, using the wizard. The `wizard` cargo feature is +/// required. #[derive(Debug, Parser)] pub struct AccountConfigureCommand { #[command(flatten)] pub account: AccountNameArg, - - /// Reset keyring passwords. - /// - /// This argument will force passwords to be prompted again, then - /// saved to your global keyring. - #[arg(long, short)] - pub reset: bool, } impl AccountConfigureCommand { - pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { - info!("executing configure account command"); - - let account = &self.account.name; - let (_, toml_account_config) = config.to_toml_account_config(Some(account))?; + #[cfg(feature = "wizard")] + pub async fn execute( + self, + mut config: TomlConfig, + config_path: Option<&PathBuf>, + ) -> Result<()> { + use pimalaya_tui::{himalaya::wizard, terminal::config::TomlConfig as _}; + use tracing::info; - if self.reset { - #[cfg(feature = "imap")] - { - let reset = match toml_account_config.imap_auth_config() { - Some(ImapAuthConfig::Password(config)) => config.reset().await, - #[cfg(feature = "oauth2")] - Some(ImapAuthConfig::OAuth2(config)) => config.reset().await, - _ => Ok(()), - }; + info!("executing account configure command"); - if let Err(err) = reset { - warn!("error while resetting imap secrets: {err}"); - debug!("error while resetting imap secrets: {err:?}"); - } - } + let path = match config_path { + Some(path) => path.clone(), + None => TomlConfig::default_path()?, + }; - #[cfg(feature = "smtp")] - { - let reset = match toml_account_config.smtp_auth_config() { - Some(SmtpAuthConfig::Password(config)) => config.reset().await, - #[cfg(feature = "oauth2")] - Some(SmtpAuthConfig::OAuth2(config)) => config.reset().await, - _ => Ok(()), - }; + let account_name = Some(self.account.name.as_str()); - if let Err(err) = reset { - warn!("error while resetting smtp secrets: {err}"); - debug!("error while resetting smtp secrets: {err:?}"); - } - } + let account_config = config + .accounts + .remove(&self.account.name) + .unwrap_or_default(); - #[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))] - if let Some(config) = &toml_account_config.pgp { - config.reset().await?; - } - } + wizard::edit(path, config, account_name, account_config).await?; - #[cfg(feature = "imap")] - match toml_account_config.imap_auth_config() { - Some(ImapAuthConfig::Password(config)) => { - config - .configure(|| Ok(prompt::password("IMAP password")?)) - .await - } - #[cfg(feature = "oauth2")] - Some(ImapAuthConfig::OAuth2(config)) => { - config - .configure(|| Ok(prompt::secret("IMAP OAuth 2.0 client secret")?)) - .await - } - _ => Ok(()), - }?; - - #[cfg(feature = "smtp")] - match toml_account_config.smtp_auth_config() { - Some(SmtpAuthConfig::Password(config)) => { - config - .configure(|| Ok(prompt::password("SMTP password")?)) - .await - } - #[cfg(feature = "oauth2")] - Some(SmtpAuthConfig::OAuth2(config)) => { - config - .configure(|| Ok(prompt::secret("SMTP OAuth 2.0 client secret")?)) - .await - } - _ => Ok(()), - }?; - - #[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))] - if let Some(config) = &toml_account_config.pgp { - config - .configure(&toml_account_config.email, || { - Ok(prompt::password("PGP secret key password")?) - }) - .await?; - } + Ok(()) + } - printer.out(format!( - "Account {account} successfully {}configured!\n", - if self.reset { "re" } else { "" } - )) + #[cfg(not(feature = "wizard"))] + pub async fn execute(self, _: TomlConfig, _: Option<&PathBuf>) -> Result<()> { + color_eyre::eyre::bail!("This command requires the `wizard` cargo feature to work"); } } diff --git a/src/account/command/doctor.rs b/src/account/command/doctor.rs new file mode 100644 index 00000000..0a807182 --- /dev/null +++ b/src/account/command/doctor.rs @@ -0,0 +1,233 @@ +use std::{ + io::{stdout, Write}, + sync::Arc, +}; + +use clap::Parser; +use color_eyre::{Result, Section}; +#[cfg(all(feature = "keyring", feature = "imap"))] +use email::imap::config::ImapAuthConfig; +#[cfg(feature = "imap")] +use email::imap::ImapContextBuilder; +#[cfg(feature = "maildir")] +use email::maildir::MaildirContextBuilder; +#[cfg(feature = "notmuch")] +use email::notmuch::NotmuchContextBuilder; +#[cfg(feature = "sendmail")] +use email::sendmail::SendmailContextBuilder; +#[cfg(all(feature = "keyring", feature = "smtp"))] +use email::smtp::config::SmtpAuthConfig; +#[cfg(feature = "smtp")] +use email::smtp::SmtpContextBuilder; +use email::{backend::BackendBuilder, config::Config}; +#[cfg(feature = "keyring")] +use pimalaya_tui::terminal::prompt; +use pimalaya_tui::{ + himalaya::config::{Backend, SendingBackend}, + terminal::config::TomlConfig as _, +}; + +use crate::{account::arg::name::OptionalAccountNameArg, config::TomlConfig}; + +/// Diagnose and fix the given account. +/// +/// This command diagnoses the given account and can even try to fix +/// it. It mostly checks if the configuration is valid, if backends +/// can be instanciated and if sessions work as expected. +#[derive(Debug, Parser)] +pub struct AccountDoctorCommand { + #[command(flatten)] + pub account: OptionalAccountNameArg, + + /// Try to fix the given account. + /// + /// This argument can be used to (re)configure keyring entries for + /// example. + #[arg(long, short)] + pub fix: bool, +} + +impl AccountDoctorCommand { + pub async fn execute(self, config: &TomlConfig) -> Result<()> { + let mut stdout = stdout(); + + if let Some(name) = self.account.name.as_ref() { + print!("Checking TOML configuration integrity for account {name}… "); + } else { + print!("Checking TOML configuration integrity for default account… "); + } + + stdout.flush()?; + + let (toml_account_config, account_config) = config + .clone() + .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { + c.account(name).ok() + })?; + let account_config = Arc::new(account_config); + + println!("OK"); + + #[cfg(feature = "keyring")] + if self.fix { + if prompt::bool("Would you like to reset existing keyring entries?", false)? { + print!("Resetting keyring entries… "); + stdout.flush()?; + + #[cfg(feature = "imap")] + match toml_account_config.imap_auth_config() { + Some(ImapAuthConfig::Password(config)) => config.reset().await?, + #[cfg(feature = "oauth2")] + Some(ImapAuthConfig::OAuth2(config)) => config.reset().await?, + _ => (), + } + + #[cfg(feature = "smtp")] + match toml_account_config.smtp_auth_config() { + Some(SmtpAuthConfig::Password(config)) => config.reset().await?, + #[cfg(feature = "oauth2")] + Some(SmtpAuthConfig::OAuth2(config)) => config.reset().await?, + _ => (), + } + + #[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))] + if let Some(config) = &toml_account_config.pgp { + config.reset().await?; + } + + println!("OK"); + } + + #[cfg(feature = "imap")] + match toml_account_config.imap_auth_config() { + Some(ImapAuthConfig::Password(config)) => { + config + .configure(|| Ok(prompt::password("IMAP password")?)) + .await?; + } + #[cfg(feature = "oauth2")] + Some(ImapAuthConfig::OAuth2(config)) => { + config + .configure(|| Ok(prompt::secret("IMAP OAuth 2.0 client secret")?)) + .await?; + } + _ => (), + }; + + #[cfg(feature = "smtp")] + match toml_account_config.smtp_auth_config() { + Some(SmtpAuthConfig::Password(config)) => { + config + .configure(|| Ok(prompt::password("SMTP password")?)) + .await?; + } + #[cfg(feature = "oauth2")] + Some(SmtpAuthConfig::OAuth2(config)) => { + config + .configure(|| Ok(prompt::secret("SMTP OAuth 2.0 client secret")?)) + .await?; + } + _ => (), + }; + + #[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))] + if let Some(config) = &toml_account_config.pgp { + config + .configure(&toml_account_config.email, || { + Ok(prompt::password("PGP secret key password")?) + }) + .await?; + } + } + + match toml_account_config.backend { + #[cfg(feature = "maildir")] + Some(Backend::Maildir(mdir_config)) => { + print!("Checking Maildir integrity… "); + stdout.flush()?; + + let ctx = MaildirContextBuilder::new(account_config.clone(), Arc::new(mdir_config)); + BackendBuilder::new(account_config.clone(), ctx) + .check_up() + .await?; + + println!("OK"); + } + #[cfg(feature = "imap")] + Some(Backend::Imap(imap_config)) => { + print!("Checking IMAP integrity… "); + stdout.flush()?; + + let ctx = ImapContextBuilder::new(account_config.clone(), Arc::new(imap_config)) + .with_pool_size(1); + let res = BackendBuilder::new(account_config.clone(), ctx) + .check_up() + .await; + + if self.fix { + res?; + } else { + res.note("Run with --fix to (re)configure your account.")?; + } + + println!("OK"); + } + #[cfg(feature = "notmuch")] + Some(Backend::Notmuch(notmuch_config)) => { + print!("Checking Notmuch integrity… "); + stdout.flush()?; + + let ctx = + NotmuchContextBuilder::new(account_config.clone(), Arc::new(notmuch_config)); + BackendBuilder::new(account_config.clone(), ctx) + .check_up() + .await?; + + println!("OK"); + } + _ => (), + } + + let sending_backend = toml_account_config + .message + .and_then(|msg| msg.send) + .and_then(|send| send.backend); + + match sending_backend { + #[cfg(feature = "smtp")] + Some(SendingBackend::Smtp(smtp_config)) => { + print!("Checking SMTP integrity… "); + stdout.flush()?; + + let ctx = SmtpContextBuilder::new(account_config.clone(), Arc::new(smtp_config)); + let res = BackendBuilder::new(account_config.clone(), ctx) + .check_up() + .await; + + if self.fix { + res?; + } else { + res.note("Run with --fix to (re)configure your account.")?; + } + + println!("OK"); + } + #[cfg(feature = "sendmail")] + Some(SendingBackend::Sendmail(sendmail_config)) => { + print!("Checking Sendmail integrity… "); + stdout.flush()?; + + let ctx = + SendmailContextBuilder::new(account_config.clone(), Arc::new(sendmail_config)); + BackendBuilder::new(account_config.clone(), ctx) + .check_up() + .await?; + + println!("OK"); + } + _ => (), + } + + Ok(()) + } +} diff --git a/src/account/command/list.rs b/src/account/command/list.rs index a387729f..2117c707 100644 --- a/src/account/command/list.rs +++ b/src/account/command/list.rs @@ -8,18 +8,19 @@ use tracing::info; use crate::config::TomlConfig; -/// List all accounts. +/// List all existing accounts. /// -/// This command lists all accounts defined in your TOML configuration -/// file. +/// This command lists all the accounts defined in your TOML +/// configuration file. #[derive(Debug, Parser)] pub struct AccountListCommand { /// The maximum width the table should not exceed. /// /// This argument will force the table not to exceed the given - /// width in pixels. Columns may shrink with ellipsis in order to + /// width, in pixels. Columns may shrink with ellipsis in order to /// fit the width. - #[arg(long, short = 'w', name = "table_max_width", value_name = "PIXELS")] + #[arg(long = "max-width", short = 'w')] + #[arg(name = "table_max_width", value_name = "PIXELS")] pub table_max_width: Option, } @@ -35,7 +36,6 @@ impl AccountListCommand { .with_some_backends_color(config.account_list_table_backends_color()) .with_some_default_color(config.account_list_table_default_color()); - printer.out(table)?; - Ok(()) + printer.out(table) } } diff --git a/src/account/command/mod.rs b/src/account/command/mod.rs index 444a4563..469afc4f 100644 --- a/src/account/command/mod.rs +++ b/src/account/command/mod.rs @@ -1,7 +1,9 @@ -mod check_up; mod configure; +mod doctor; mod list; +use std::path::PathBuf; + use clap::Subcommand; use color_eyre::Result; use pimalaya_tui::terminal::cli::printer::Printer; @@ -9,33 +11,31 @@ use pimalaya_tui::terminal::cli::printer::Printer; use crate::config::TomlConfig; use self::{ - check_up::AccountCheckUpCommand, configure::AccountConfigureCommand, list::AccountListCommand, + configure::AccountConfigureCommand, doctor::AccountDoctorCommand, list::AccountListCommand, }; -/// Manage accounts. +/// Configure, list and diagnose your accounts. /// -/// An account is a set of settings, identified by an account -/// name. Settings are directly taken from your TOML configuration -/// file. This subcommand allows you to manage them. +/// An account is a group of settings, identified by a unique +/// name. This subcommand allows you to manage your accounts. #[derive(Debug, Subcommand)] pub enum AccountSubcommand { - #[command(alias = "checkup")] - CheckUp(AccountCheckUpCommand), - - #[command(alias = "cfg")] Configure(AccountConfigureCommand), - - #[command(alias = "lst")] + Doctor(AccountDoctorCommand), List(AccountListCommand), } impl AccountSubcommand { - #[allow(unused)] - pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + pub async fn execute( + self, + printer: &mut impl Printer, + config: TomlConfig, + config_path: Option<&PathBuf>, + ) -> Result<()> { match self { - Self::CheckUp(cmd) => cmd.execute(printer, config).await, - Self::Configure(cmd) => cmd.execute(printer, config).await, - Self::List(cmd) => cmd.execute(printer, config).await, + Self::Configure(cmd) => cmd.execute(config, config_path).await, + Self::Doctor(cmd) => cmd.execute(&config).await, + Self::List(cmd) => cmd.execute(printer, &config).await, } } } diff --git a/src/cli.rs b/src/cli.rs index 2e362863..e3af1288 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -123,7 +123,7 @@ impl HimalayaCommand { match self { Self::Account(cmd) => { let config = TomlConfig::from_paths_or_default(config_paths).await?; - cmd.execute(printer, &config).await + cmd.execute(printer, config, config_paths.first()).await } Self::Folder(cmd) => { let config = TomlConfig::from_paths_or_default(config_paths).await?; diff --git a/src/completion/command.rs b/src/completion/command.rs index 11bc5421..152a2ae7 100644 --- a/src/completion/command.rs +++ b/src/completion/command.rs @@ -1,12 +1,13 @@ +use std::io; + use clap::{value_parser, CommandFactory, Parser}; use clap_complete::Shell; use color_eyre::Result; -use std::io; use tracing::info; use crate::cli::Cli; -/// Print completion script for a shell to stdout. +/// Print completion script for the given shell to stdout. /// /// This command allows you to generate completion script for a given /// shell. The script is printed to the standard output. If you want diff --git a/src/email/envelope/command/list.rs b/src/email/envelope/command/list.rs index d28179f8..a81c836e 100644 --- a/src/email/envelope/command/list.rs +++ b/src/email/envelope/command/list.rs @@ -18,12 +18,12 @@ use crate::{ folder::arg::name::FolderNameOptionalFlag, }; -/// List all envelopes. +/// Search and sort envelopes as a list. /// -/// This command allows you to list all envelopes included in the -/// given folder. +/// This command allows you to list envelopes included in the given +/// folder, matching the given query. #[derive(Debug, Parser)] -pub struct ListEnvelopesCommand { +pub struct EnvelopeListCommand { #[command(flatten)] pub folder: FolderNameOptionalFlag, @@ -123,7 +123,7 @@ pub struct ListEnvelopesCommand { pub query: Option>, } -impl Default for ListEnvelopesCommand { +impl Default for EnvelopeListCommand { fn default() -> Self { Self { folder: Default::default(), @@ -136,7 +136,7 @@ impl Default for ListEnvelopesCommand { } } -impl ListEnvelopesCommand { +impl EnvelopeListCommand { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { info!("executing list envelopes command"); @@ -213,7 +213,6 @@ impl ListEnvelopesCommand { .with_some_sender_color(toml_account_config.envelope_list_table_sender_color()) .with_some_date_color(toml_account_config.envelope_list_table_date_color()); - printer.out(table)?; - Ok(()) + printer.out(table) } } diff --git a/src/email/envelope/command/mod.rs b/src/email/envelope/command/mod.rs index d0b170ca..6966e592 100644 --- a/src/email/envelope/command/mod.rs +++ b/src/email/envelope/command/mod.rs @@ -7,9 +7,9 @@ use pimalaya_tui::terminal::cli::printer::Printer; use crate::config::TomlConfig; -use self::{list::ListEnvelopesCommand, thread::ThreadEnvelopesCommand}; +use self::{list::EnvelopeListCommand, thread::EnvelopeThreadCommand}; -/// Manage envelopes. +/// List, search and sort your envelopes. /// /// An envelope is a small representation of a message. It contains an /// identifier (given by the backend), some flags as well as few @@ -18,10 +18,10 @@ use self::{list::ListEnvelopesCommand, thread::ThreadEnvelopesCommand}; #[derive(Debug, Subcommand)] pub enum EnvelopeSubcommand { #[command(alias = "lst")] - List(ListEnvelopesCommand), + List(EnvelopeListCommand), #[command()] - Thread(ThreadEnvelopesCommand), + Thread(EnvelopeThreadCommand), } impl EnvelopeSubcommand { diff --git a/src/email/envelope/command/thread.rs b/src/email/envelope/command/thread.rs index bba515b7..189a3024 100644 --- a/src/email/envelope/command/thread.rs +++ b/src/email/envelope/command/thread.rs @@ -17,12 +17,12 @@ use crate::{ folder::arg::name::FolderNameOptionalFlag, }; -/// Thread all envelopes. +/// Search and sort envelopes as a thread. /// -/// This command allows you to thread all envelopes included in the -/// given folder. +/// This command allows you to thread envelopes included in the given +/// folder, matching the given query. #[derive(Debug, Parser)] -pub struct ThreadEnvelopesCommand { +pub struct EnvelopeThreadCommand { #[command(flatten)] pub folder: FolderNameOptionalFlag, @@ -33,11 +33,14 @@ pub struct ThreadEnvelopesCommand { #[arg(long, short)] pub id: Option, + /// The list envelopes filter and sort query. + /// + /// See `envelope list --help` for more information. #[arg(allow_hyphen_values = true, trailing_var_arg = true)] pub query: Option>, } -impl ThreadEnvelopesCommand { +impl EnvelopeThreadCommand { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { info!("executing thread envelopes command"); @@ -102,9 +105,7 @@ impl ThreadEnvelopesCommand { let tree = EnvelopesTree::new(account_config, envelopes); - printer.out(tree)?; - - Ok(()) + printer.out(tree) } } diff --git a/src/email/envelope/flag/command/add.rs b/src/email/envelope/flag/command/add.rs index 9b4e0797..c54b416a 100644 --- a/src/email/envelope/flag/command/add.rs +++ b/src/email/envelope/flag/command/add.rs @@ -16,7 +16,7 @@ use crate::{ folder::arg::name::FolderNameOptionalFlag, }; -/// Add flag(s) to an envelope. +/// Add flag(s) to the given envelope. /// /// This command allows you to attach the given flag(s) to the given /// envelope(s). diff --git a/src/email/envelope/flag/command/mod.rs b/src/email/envelope/flag/command/mod.rs index b1bbfc9e..106b9bb3 100644 --- a/src/email/envelope/flag/command/mod.rs +++ b/src/email/envelope/flag/command/mod.rs @@ -10,12 +10,11 @@ use crate::config::TomlConfig; use self::{add::FlagAddCommand, remove::FlagRemoveCommand, set::FlagSetCommand}; -/// Manage flags. +/// Add, change and remove your envelopes flags. /// /// A flag is a tag associated to an envelope. Existing flags are /// seen, answered, flagged, deleted, draft. Other flags are -/// considered custom, which are not always supported (the -/// synchronization does not take care of them yet). +/// considered custom, which are not always supported. #[derive(Debug, Subcommand)] pub enum FlagSubcommand { #[command(arg_required_else_help = true)] diff --git a/src/email/envelope/flag/command/remove.rs b/src/email/envelope/flag/command/remove.rs index 70ce9ad3..1d73a7b9 100644 --- a/src/email/envelope/flag/command/remove.rs +++ b/src/email/envelope/flag/command/remove.rs @@ -16,7 +16,7 @@ use crate::{ folder::arg::name::FolderNameOptionalFlag, }; -/// Remove flag(s) from an envelope. +/// Remove flag(s) from a given envelope. /// /// This command allows you to remove the given flag(s) from the given /// envelope(s). diff --git a/src/email/envelope/flag/command/set.rs b/src/email/envelope/flag/command/set.rs index 647419cb..df473d71 100644 --- a/src/email/envelope/flag/command/set.rs +++ b/src/email/envelope/flag/command/set.rs @@ -16,7 +16,7 @@ use crate::{ folder::arg::name::FolderNameOptionalFlag, }; -/// Replace flag(s) of an envelope. +/// Replace flag(s) of a given envelope. /// /// This command allows you to replace existing flags of the given /// envelope(s) with the given flag(s). diff --git a/src/email/message/attachment/command/download.rs b/src/email/message/attachment/command/download.rs index 43c75275..65ffc83f 100644 --- a/src/email/message/attachment/command/download.rs +++ b/src/email/message/attachment/command/download.rs @@ -14,7 +14,7 @@ use crate::{ folder::arg::name::FolderNameOptionalFlag, }; -/// Download all attachments for the given message. +/// Download all attachments found in the given message. /// /// This command allows you to download all attachments found for the /// given message to your downloads directory. @@ -69,14 +69,14 @@ impl AttachmentDownloadCommand { let attachments = email.attachments()?; if attachments.is_empty() { - printer.log(format!("No attachment found for message {id}!"))?; + printer.log(format!("No attachment found for message {id}!\n"))?; continue; } else { emails_count += 1; } printer.log(format!( - "{} attachment(s) found for message {id}!", + "{} attachment(s) found for message {id}!\n", attachments.len() ))?; @@ -86,7 +86,7 @@ impl AttachmentDownloadCommand { .unwrap_or_else(|| Uuid::new_v4().to_string()) .into(); let filepath = account_config.get_download_file_path(&filename)?; - printer.log(format!("Downloading {:?}…", filepath))?; + printer.log(format!("Downloading {:?}…\n", filepath))?; fs::write(&filepath, &attachment.body) .with_context(|| format!("cannot save attachment at {filepath:?}"))?; attachments_count += 1; @@ -94,10 +94,10 @@ impl AttachmentDownloadCommand { } match attachments_count { - 0 => printer.out("No attachment found!"), - 1 => printer.out("Downloaded 1 attachment!"), + 0 => printer.out("No attachment found!\n"), + 1 => printer.out("Downloaded 1 attachment!\n"), n => printer.out(format!( - "Downloaded {} attachment(s) from {} messages(s)!", + "Downloaded {} attachment(s) from {} messages(s)!\n", n, emails_count, )), } diff --git a/src/email/message/attachment/command/mod.rs b/src/email/message/attachment/command/mod.rs index fe3577de..e661fe5f 100644 --- a/src/email/message/attachment/command/mod.rs +++ b/src/email/message/attachment/command/mod.rs @@ -8,14 +8,14 @@ use crate::config::TomlConfig; use self::download::AttachmentDownloadCommand; -/// Manage attachments. +/// Download your message attachments. /// /// A message body can be composed of multiple MIME parts. An /// attachment is the representation of a binary part of a message /// body. #[derive(Debug, Subcommand)] pub enum AttachmentSubcommand { - #[command(arg_required_else_help = true)] + #[command(arg_required_else_help = true, alias = "dl")] Download(AttachmentDownloadCommand), } diff --git a/src/email/message/command/copy.rs b/src/email/message/command/copy.rs index 7fc46c55..4f01e160 100644 --- a/src/email/message/command/copy.rs +++ b/src/email/message/command/copy.rs @@ -16,7 +16,8 @@ use crate::{ folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg}, }; -/// Copy a message from a source folder to a target folder. +/// Copy the message associated to the given envelope id(s) to the +/// given target folder. #[derive(Debug, Parser)] pub struct MessageCopyCommand { #[command(flatten)] diff --git a/src/email/message/command/delete.rs b/src/email/message/command/delete.rs index 82d5aec8..598dd3eb 100644 --- a/src/email/message/command/delete.rs +++ b/src/email/message/command/delete.rs @@ -14,7 +14,7 @@ use crate::{ folder::arg::name::FolderNameOptionalFlag, }; -/// Mark as deleted a message from a folder. +/// Mark as deleted the message associated to the given envelope id(s). /// /// This command does not really delete the message: if the given /// folder points to the trash folder, it adds the "deleted" flag to diff --git a/src/email/message/command/edit.rs b/src/email/message/command/edit.rs new file mode 100644 index 00000000..05292327 --- /dev/null +++ b/src/email/message/command/edit.rs @@ -0,0 +1,103 @@ +use std::sync::Arc; + +use clap::Parser; +use color_eyre::{eyre::eyre, Result}; +use email::{backend::feature::BackendFeatureSource, config::Config}; +use pimalaya_tui::{ + himalaya::{backend::BackendBuilder, editor}, + terminal::{cli::printer::Printer, config::TomlConfig as _}, +}; +use tracing::info; + +use crate::{ + account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg, + folder::arg::name::FolderNameOptionalFlag, +}; + +/// Edit the message associated to the given envelope id. +/// +/// This command allows you to edit the given message using the +/// editor defined in your environment variable $EDITOR. When the +/// edition process finishes, you can choose between saving or sending +/// the final message. +#[derive(Debug, Parser)] +pub struct MessageEditCommand { + #[command(flatten)] + pub folder: FolderNameOptionalFlag, + + #[command(flatten)] + pub envelope: EnvelopeIdArg, + + /// List of headers that should be visible at the top of the + /// message. + /// + /// If a given header is not found in the message, it will not be + /// visible. If no header is given, defaults to the one set up in + /// your TOML configuration file. + #[arg(long = "header", short = 'H', value_name = "NAME")] + pub headers: Vec, + + /// Edit the message on place. + /// + /// If set, the original message being edited will be removed at + /// the end of the command. Useful when you need, for example, to + /// edit a draft, send it then remove it from the Drafts folder. + #[arg(long, short = 'p')] + pub on_place: bool, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl MessageEditCommand { + pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { + info!("executing edit message command"); + + let folder = &self.folder.name; + + let (toml_account_config, account_config) = config + .clone() + .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { + c.account(name).ok() + })?; + + let account_config = Arc::new(account_config); + + let backend = BackendBuilder::new( + Arc::new(toml_account_config), + account_config.clone(), + |builder| { + builder + .without_features() + .with_add_message(BackendFeatureSource::Context) + .with_send_message(BackendFeatureSource::Context) + .with_delete_messages(BackendFeatureSource::Context) + }, + ) + .build() + .await?; + + let id = self.envelope.id; + let tpl = backend + .get_messages(folder, &[id]) + .await? + .first() + .ok_or(eyre!("cannot find message"))? + .to_read_tpl(&account_config, |mut tpl| { + if !self.headers.is_empty() { + tpl = tpl.with_show_only_headers(&self.headers); + } + + tpl + }) + .await?; + + editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await?; + + if self.on_place { + backend.delete_messages(folder, &[id]).await?; + } + + Ok(()) + } +} diff --git a/src/email/message/command/export.rs b/src/email/message/command/export.rs new file mode 100644 index 00000000..3ae331a4 --- /dev/null +++ b/src/email/message/command/export.rs @@ -0,0 +1,155 @@ +use std::{ + env::temp_dir, + fs, + io::{stdout, Write}, + path::PathBuf, + sync::Arc, +}; + +use clap::Parser; +use color_eyre::{eyre::eyre, Result}; +use email::{backend::feature::BackendFeatureSource, config::Config}; +use pimalaya_tui::{himalaya::backend::BackendBuilder, terminal::config::TomlConfig as _}; +use tracing::info; + +use crate::{ + account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg, + folder::arg::name::FolderNameOptionalFlag, +}; + +/// Export the message associated to the given envelope id. +/// +/// This command allows you to export a message. A message can be +/// fully exported in one single file, or exported in multiple files +/// (one per MIME part found in the message). This is useful, for +/// example, to read a HTML message. +#[derive(Debug, Parser)] +pub struct MessageExportCommand { + #[command(flatten)] + pub folder: FolderNameOptionalFlag, + + #[command(flatten)] + pub envelope: EnvelopeIdArg, + + /// Export the full raw message as one unique .eml file. + /// + /// The raw message represents the headers and the body as it is + /// on the backend, unedited: not decoded nor decrypted. This is + /// useful for debugging faulty messages, but also for + /// saving/sending/transfering messages. + #[arg(long, short = 'F')] + pub full: bool, + + /// Try to open the exported message, when applicable. + /// + /// This argument only works with full message export, or when + /// HTML or plain text is present in the export. + #[arg(long, short = 'O')] + pub open: bool, + + /// Where the message should be exported to. + /// + /// The destination should point to a valid directory. If `--full` + /// is given, it can also point to a .eml file. + #[arg(long, short, alias = "dest")] + pub destination: Option, + + #[command(flatten)] + pub account: AccountNameFlag, +} + +impl MessageExportCommand { + pub async fn execute(self, config: &TomlConfig) -> Result<()> { + info!("executing export message command"); + + let folder = &self.folder.name; + let id = &self.envelope.id; + + let (toml_account_config, account_config) = config + .clone() + .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { + c.account(name).ok() + })?; + + let account_config = Arc::new(account_config); + + let backend = BackendBuilder::new( + Arc::new(toml_account_config), + account_config.clone(), + |builder| { + builder + .without_features() + .with_get_messages(BackendFeatureSource::Context) + }, + ) + .without_sending_backend() + .build() + .await?; + + let msgs = backend.get_messages(folder, &[*id]).await?; + let msg = msgs.first().ok_or(eyre!("cannot find message {id}"))?; + + if self.full { + let bytes = msg.raw()?; + + match self.destination { + Some(mut dest) if dest.is_dir() => { + dest.push(format!("{id}.eml")); + fs::write(&dest, bytes)?; + let dest = dest.display(); + println!("Message {id} successfully exported at {dest}!"); + } + Some(dest) => { + fs::write(&dest, bytes)?; + let dest = dest.display(); + println!("Message {id} successfully exported at {dest}!"); + } + None => { + stdout().write_all(bytes)?; + } + }; + } else { + let dest = match self.destination { + Some(dest) if dest.is_dir() => { + let dest = msg.download_parts(&dest)?; + let d = dest.display(); + println!("Message {id} successfully exported in {d}!"); + dest + } + Some(dest) if dest.is_file() => { + let dest = dest.parent().unwrap_or(&dest); + let dest = msg.download_parts(&dest)?; + let d = dest.display(); + println!("Message {id} successfully exported in {d}!"); + dest + } + Some(dest) => { + return Err(eyre!("Destination {} does not exist!", dest.display())); + } + None => { + let dest = temp_dir(); + let dest = msg.download_parts(&dest)?; + let d = dest.display(); + println!("Message {id} successfully exported in {d}!"); + dest + } + }; + + if self.open { + let index_html = dest.join("index.html"); + if index_html.exists() { + return Ok(open::that(index_html)?); + } + + let plain_txt = dest.join("plain.txt"); + if plain_txt.exists() { + return Ok(open::that(plain_txt)?); + } + + println!("--open was passed but nothing to open, ignoring"); + } + } + + Ok(()) + } +} diff --git a/src/email/message/command/forward.rs b/src/email/message/command/forward.rs index c6fc7315..3de9c53b 100644 --- a/src/email/message/command/forward.rs +++ b/src/email/message/command/forward.rs @@ -17,7 +17,7 @@ use crate::{ message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs}, }; -/// Forward a message. +/// Forward the message associated to the given envelope id. /// /// This command allows you to forward the given message using the /// editor defined in your environment variable $EDITOR. When the @@ -65,7 +65,6 @@ impl MessageForwardCommand { .with_send_message(BackendFeatureSource::Context) }, ) - .without_sending_backend() .build() .await?; diff --git a/src/email/message/command/mailto.rs b/src/email/message/command/mailto.rs index 118a7c37..32dca8d4 100644 --- a/src/email/message/command/mailto.rs +++ b/src/email/message/command/mailto.rs @@ -12,7 +12,7 @@ use url::Url; use crate::{account::arg::name::AccountNameFlag, config::TomlConfig}; -/// Parse and edit a message from a mailto URL string. +/// Parse and edit a message from the given mailto URL string. /// /// This command allows you to edit a message from the mailto format /// using the editor defined in your environment variable diff --git a/src/email/message/command/mod.rs b/src/email/message/command/mod.rs index fa8cdff3..5ab1ba8b 100644 --- a/src/email/message/command/mod.rs +++ b/src/email/message/command/mod.rs @@ -1,5 +1,7 @@ pub mod copy; pub mod delete; +pub mod edit; +pub mod export; pub mod forward; pub mod mailto; pub mod r#move; @@ -17,13 +19,14 @@ use pimalaya_tui::terminal::cli::printer::Printer; use crate::config::TomlConfig; use self::{ - copy::MessageCopyCommand, delete::MessageDeleteCommand, forward::MessageForwardCommand, - mailto::MessageMailtoCommand, r#move::MessageMoveCommand, read::MessageReadCommand, - reply::MessageReplyCommand, save::MessageSaveCommand, send::MessageSendCommand, - thread::MessageThreadCommand, write::MessageWriteCommand, + copy::MessageCopyCommand, delete::MessageDeleteCommand, edit::MessageEditCommand, + export::MessageExportCommand, forward::MessageForwardCommand, mailto::MessageMailtoCommand, + r#move::MessageMoveCommand, read::MessageReadCommand, reply::MessageReplyCommand, + save::MessageSaveCommand, send::MessageSendCommand, thread::MessageThreadCommand, + write::MessageWriteCommand, }; -/// Manage messages. +/// Read, write, send, copy, move and delete your messages. /// /// A message is the content of an email. It is composed of headers /// (located at the top of the message) and a body (located at the @@ -34,19 +37,22 @@ pub enum MessageSubcommand { #[command(arg_required_else_help = true)] Read(MessageReadCommand), + #[command(arg_required_else_help = true)] + Export(MessageExportCommand), + #[command(arg_required_else_help = true)] Thread(MessageThreadCommand), #[command(aliases = ["add", "create", "new", "compose"])] Write(MessageWriteCommand), - #[command()] Reply(MessageReplyCommand), #[command(aliases = ["fwd", "fd"])] Forward(MessageForwardCommand), - #[command()] + Edit(MessageEditCommand), + Mailto(MessageMailtoCommand), Save(MessageSaveCommand), @@ -71,10 +77,12 @@ impl MessageSubcommand { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { match self { Self::Read(cmd) => cmd.execute(printer, config).await, + Self::Export(cmd) => cmd.execute(config).await, Self::Thread(cmd) => cmd.execute(printer, config).await, Self::Write(cmd) => cmd.execute(printer, config).await, Self::Reply(cmd) => cmd.execute(printer, config).await, Self::Forward(cmd) => cmd.execute(printer, config).await, + Self::Edit(cmd) => cmd.execute(printer, config).await, Self::Mailto(cmd) => cmd.execute(printer, config).await, Self::Save(cmd) => cmd.execute(printer, config).await, Self::Send(cmd) => cmd.execute(printer, config).await, diff --git a/src/email/message/command/move.rs b/src/email/message/command/move.rs index 3557a360..7e4ef1f8 100644 --- a/src/email/message/command/move.rs +++ b/src/email/message/command/move.rs @@ -17,7 +17,8 @@ use crate::{ folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg}, }; -/// Move a message from a source folder to a target folder. +/// Move the message associated to the given envelope id(s) to the +/// given target folder. #[derive(Debug, Parser)] pub struct MessageMoveCommand { #[command(flatten)] diff --git a/src/email/message/command/read.rs b/src/email/message/command/read.rs index 8284bb71..33ece677 100644 --- a/src/email/message/command/read.rs +++ b/src/email/message/command/read.rs @@ -3,7 +3,6 @@ use std::sync::Arc; use clap::Parser; use color_eyre::Result; use email::{backend::feature::BackendFeatureSource, config::Config}; -use mml::message::FilterParts; use pimalaya_tui::{ himalaya::backend::BackendBuilder, terminal::{cli::printer::Printer, config::TomlConfig as _}, @@ -16,11 +15,12 @@ use crate::{ folder::arg::name::FolderNameOptionalFlag, }; -/// Read a message. +/// Read a human-friendly version of the message associated to the +/// given envelope id(s). /// /// This command allows you to read a message. When reading a message, /// the "seen" flag is automatically applied to the corresponding -/// envelope. To prevent this behaviour, use the --preview flag. +/// envelope. To prevent this behaviour, use the "--preview" flag. #[derive(Debug, Parser)] pub struct MessageReadCommand { #[command(flatten)] @@ -34,31 +34,10 @@ pub struct MessageReadCommand { #[arg(long, short)] pub preview: bool, - /// Read the raw version of the given message. - /// - /// The raw message represents the headers and the body as it is - /// on the backend, unedited: not decoded nor decrypted. This is - /// useful for debugging faulty messages, but also for - /// saving/sending/transfering messages. - #[arg(long, short)] - #[arg(conflicts_with = "no_headers")] - #[arg(conflicts_with = "headers")] - pub raw: bool, - - /// Read only body of text/html parts. - /// - /// This argument is useful when you need to read the HTML version - /// of a message. Combined with --no-headers, you can write it to - /// a .html file and open it with your favourite browser. - #[arg(long)] - #[arg(conflicts_with = "raw")] - pub html: bool, - /// Read only the body of the message. /// /// All headers will be removed from the message. #[arg(long)] - #[arg(conflicts_with = "raw")] #[arg(conflicts_with = "headers")] pub no_headers: bool, @@ -69,7 +48,6 @@ pub struct MessageReadCommand { /// visible. If no header is given, defaults to the one set up in /// your TOML configuration file. #[arg(long = "header", short = 'H', value_name = "NAME")] - #[arg(conflicts_with = "raw")] #[arg(conflicts_with = "no_headers")] pub headers: Vec, @@ -99,6 +77,7 @@ impl MessageReadCommand { builder .without_features() .with_get_messages(BackendFeatureSource::Context) + .with_peek_messages(BackendFeatureSource::Context) }, ) .without_sending_backend() @@ -117,28 +96,18 @@ impl MessageReadCommand { for email in emails.to_vec() { bodies.push_str(glue); - if self.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()?)); - } else { - let tpl = email - .to_read_tpl(&account_config, |mut tpl| { - if self.no_headers { - tpl = tpl.with_hide_all_headers(); - } else if !self.headers.is_empty() { - tpl = tpl.with_show_only_headers(&self.headers); - } - - if self.html { - tpl = tpl.with_filter_parts(FilterParts::Only("text/html".into())); - } - - tpl - }) - .await?; - bodies.push_str(&tpl); - } + let tpl = email + .to_read_tpl(&account_config, |mut tpl| { + if self.no_headers { + tpl = tpl.with_hide_all_headers(); + } else if !self.headers.is_empty() { + tpl = tpl.with_show_only_headers(&self.headers); + } + + tpl + }) + .await?; + bodies.push_str(&tpl); glue = "\n\n"; } diff --git a/src/email/message/command/reply.rs b/src/email/message/command/reply.rs index 4dd1b2cd..803848ea 100644 --- a/src/email/message/command/reply.rs +++ b/src/email/message/command/reply.rs @@ -17,7 +17,7 @@ use crate::{ message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs, reply::MessageReplyAllArg}, }; -/// Reply to a message. +/// Reply to the message associated to the given envelope id. /// /// This command allows you to reply to the given message using the /// editor defined in your environment variable $EDITOR. When the diff --git a/src/email/message/command/save.rs b/src/email/message/command/save.rs index 2dcd3153..3c601540 100644 --- a/src/email/message/command/save.rs +++ b/src/email/message/command/save.rs @@ -16,7 +16,7 @@ use crate::{ folder::arg::name::FolderNameOptionalFlag, message::arg::MessageRawArg, }; -/// Save a message to a folder. +/// Save the given raw message to the given folder. /// /// This command allows you to add a raw message to the given folder. #[derive(Debug, Parser)] diff --git a/src/email/message/command/send.rs b/src/email/message/command/send.rs index f6b58e76..c2ad3fc9 100644 --- a/src/email/message/command/send.rs +++ b/src/email/message/command/send.rs @@ -13,7 +13,7 @@ use tracing::info; use crate::{account::arg::name::AccountNameFlag, config::TomlConfig, message::arg::MessageRawArg}; -/// Send a message. +/// Send the given raw message. /// /// This command allows you to send a raw message and to save a copy /// to your send folder. diff --git a/src/email/message/command/thread.rs b/src/email/message/command/thread.rs index e5ecb751..aa16db21 100644 --- a/src/email/message/command/thread.rs +++ b/src/email/message/command/thread.rs @@ -3,7 +3,6 @@ use std::sync::Arc; use clap::Parser; use color_eyre::Result; use email::{backend::feature::BackendFeatureSource, config::Config}; -use mml::message::FilterParts; use pimalaya_tui::{ himalaya::backend::BackendBuilder, terminal::{cli::printer::Printer, config::TomlConfig as _}, @@ -17,7 +16,8 @@ use crate::{ folder::arg::name::FolderNameOptionalFlag, }; -/// Thread a message. +/// Read human-friendly version of messages associated to the +/// given envelope id's thread. /// /// This command allows you to thread a message. When threading a message, /// the "seen" flag is automatically applied to the corresponding @@ -35,31 +35,10 @@ pub struct MessageThreadCommand { #[arg(long, short)] pub preview: bool, - /// Thread the raw version of the given message. - /// - /// The raw message represents the headers and the body as it is - /// on the backend, unedited: not decoded nor decrypted. This is - /// useful for debugging faulty messages, but also for - /// saving/sending/transfering messages. - #[arg(long, short)] - #[arg(conflicts_with = "no_headers")] - #[arg(conflicts_with = "headers")] - pub raw: bool, - - /// Thread only body of text/html parts. - /// - /// This argument is useful when you need to thread the HTML version - /// of a message. Combined with --no-headers, you can write it to - /// a .html file and open it with your favourite browser. - #[arg(long)] - #[arg(conflicts_with = "raw")] - pub html: bool, - /// Thread only the body of the message. /// /// All headers will be removed from the message. #[arg(long)] - #[arg(conflicts_with = "raw")] #[arg(conflicts_with = "headers")] pub no_headers: bool, @@ -70,7 +49,6 @@ pub struct MessageThreadCommand { /// visible. If no header is given, defaults to the one set up in /// your TOML configuration file. #[arg(long = "header", short = 'H', value_name = "NAME")] - #[arg(conflicts_with = "raw")] #[arg(conflicts_with = "no_headers")] pub headers: Vec, @@ -100,6 +78,7 @@ impl MessageThreadCommand { builder .without_features() .with_get_messages(BackendFeatureSource::Context) + .with_peek_messages(BackendFeatureSource::Context) .with_thread_envelopes(BackendFeatureSource::Context) }, ) @@ -130,29 +109,19 @@ impl MessageThreadCommand { bodies.push_str(glue); bodies.push_str(&format!("-------- Message {} --------\n\n", ids[i + 1])); - if self.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()?)); - } else { - let tpl = email - .to_read_tpl(&account_config, |mut tpl| { - if self.no_headers { - tpl = tpl.with_hide_all_headers(); - } else if !self.headers.is_empty() { - tpl = tpl.with_show_only_headers(&self.headers); - } - - if self.html { - tpl = tpl.with_filter_parts(FilterParts::Only("text/html".into())); - } - - tpl - }) - .await?; - bodies.push_str(&tpl); - } + let tpl = email + .to_read_tpl(&account_config, |mut tpl| { + if self.no_headers { + tpl = tpl.with_hide_all_headers(); + } else if !self.headers.is_empty() { + tpl = tpl.with_show_only_headers(&self.headers); + } + + tpl + }) + .await?; + bodies.push_str(&tpl); glue = "\n\n"; } diff --git a/src/email/message/command/write.rs b/src/email/message/command/write.rs index cbba61a6..02b4a5cb 100644 --- a/src/email/message/command/write.rs +++ b/src/email/message/command/write.rs @@ -18,7 +18,7 @@ use crate::{ message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs}, }; -/// Write a new message. +/// Compose a new message, from scratch. /// /// This command allows you to write a new message using the editor /// defined in your environment variable $EDITOR. When the edition diff --git a/src/email/message/template/command/mod.rs b/src/email/message/template/command/mod.rs index e9c98a92..542303b0 100644 --- a/src/email/message/template/command/mod.rs +++ b/src/email/message/template/command/mod.rs @@ -15,15 +15,14 @@ use self::{ send::TemplateSendCommand, write::TemplateWriteCommand, }; -/// Manage templates. +/// Generate, save and send message templates. /// /// A template is an editable version of a message (headers + /// body). It uses a specific language called MML that allows you to /// attach file or encrypt content. This subcommand allows you manage /// them. /// -/// You can learn more about MML at -/// . +/// Learn more about MML at: . #[derive(Debug, Subcommand)] pub enum TemplateSubcommand { #[command(aliases = ["add", "create", "new", "compose"])] diff --git a/src/folder/command/add.rs b/src/folder/command/add.rs index 73b64921..9eeab2a9 100644 --- a/src/folder/command/add.rs +++ b/src/folder/command/add.rs @@ -16,12 +16,12 @@ use crate::{ account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg, }; -/// Create a new folder. +/// Create the given folder. /// /// This command allows you to create a new folder using the given /// name. #[derive(Debug, Parser)] -pub struct AddFolderCommand { +pub struct FolderAddCommand { #[command(flatten)] pub folder: FolderNameArg, @@ -29,7 +29,7 @@ pub struct AddFolderCommand { pub account: AccountNameFlag, } -impl AddFolderCommand { +impl FolderAddCommand { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { info!("executing create folder command"); diff --git a/src/folder/command/delete.rs b/src/folder/command/delete.rs index 1f334357..dccde3bd 100644 --- a/src/folder/command/delete.rs +++ b/src/folder/command/delete.rs @@ -16,7 +16,7 @@ use crate::{ account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg, }; -/// Delete a folder. +/// Delete the given folder. /// /// All emails from the given folder are definitely deleted. The /// folder is also deleted after execution of the command. diff --git a/src/folder/command/expunge.rs b/src/folder/command/expunge.rs index d215d67e..f095e4ae 100644 --- a/src/folder/command/expunge.rs +++ b/src/folder/command/expunge.rs @@ -15,7 +15,7 @@ use crate::{ account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg, }; -/// Expunge a folder. +/// Expunge the given folder. /// /// The concept of expunging is similar to the IMAP one: it definitely /// deletes emails from the given folder that contain the "deleted" diff --git a/src/folder/command/list.rs b/src/folder/command/list.rs index 31ce9367..187013d4 100644 --- a/src/folder/command/list.rs +++ b/src/folder/command/list.rs @@ -28,9 +28,10 @@ pub struct FolderListCommand { /// The maximum width the table should not exceed. /// /// This argument will force the table not to exceed the given - /// width in pixels. Columns may shrink with ellipsis in order to + /// width, in pixels. Columns may shrink with ellipsis in order to /// fit the width. - #[arg(long, short = 'w', name = "table_max_width", value_name = "PIXELS")] + #[arg(long = "max-width", short = 'w')] + #[arg(name = "table_max_width", value_name = "PIXELS")] pub table_max_width: Option, } diff --git a/src/folder/command/mod.rs b/src/folder/command/mod.rs index 1eb92af0..0d8e1d73 100644 --- a/src/folder/command/mod.rs +++ b/src/folder/command/mod.rs @@ -11,18 +11,18 @@ use pimalaya_tui::terminal::cli::printer::Printer; use crate::config::TomlConfig; use self::{ - add::AddFolderCommand, delete::FolderDeleteCommand, expunge::FolderExpungeCommand, + add::FolderAddCommand, delete::FolderDeleteCommand, expunge::FolderExpungeCommand, list::FolderListCommand, purge::FolderPurgeCommand, }; -/// Manage folders. +/// Create, list and purge your folders (as known as mailboxes). /// -/// A folder (as known as mailbox, or directory) contains one or more -/// emails. This subcommand allows you to manage them. +/// A folder (as known as mailbox, or directory) is a messages +/// container. This subcommand allows you to manage them. #[derive(Debug, Subcommand)] pub enum FolderSubcommand { #[command(visible_alias = "create", alias = "new")] - Add(AddFolderCommand), + Add(FolderAddCommand), #[command(alias = "lst")] List(FolderListCommand), diff --git a/src/folder/command/purge.rs b/src/folder/command/purge.rs index 859a3a25..91bf046d 100644 --- a/src/folder/command/purge.rs +++ b/src/folder/command/purge.rs @@ -13,7 +13,7 @@ use crate::{ account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg, }; -/// Purge a folder. +/// Purge the given folder. /// /// All emails from the given folder are definitely deleted. The /// purged folder will remain empty after execution of the command. diff --git a/src/main.rs b/src/main.rs index db542311..dd9ae5ec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use clap::Parser; use color_eyre::Result; use himalaya::{ - cli::Cli, config::TomlConfig, envelope::command::list::ListEnvelopesCommand, + cli::Cli, config::TomlConfig, envelope::command::list::EnvelopeListCommand, message::command::mailto::MessageMailtoCommand, }; use pimalaya_tui::terminal::{ @@ -37,7 +37,7 @@ async fn main() -> Result<()> { Some(cmd) => cmd.execute(&mut printer, cli.config_paths.as_ref()).await, None => { let config = TomlConfig::from_paths_or_default(cli.config_paths.as_ref()).await?; - ListEnvelopesCommand::default() + EnvelopeListCommand::default() .execute(&mut printer, &config) .await } diff --git a/src/manual/command.rs b/src/manual/command.rs index bf334b8e..66a59f3e 100644 --- a/src/manual/command.rs +++ b/src/manual/command.rs @@ -1,14 +1,15 @@ +use std::{fs, path::PathBuf}; + use clap::{CommandFactory, Parser}; use clap_mangen::Man; use color_eyre::Result; use pimalaya_tui::terminal::cli::printer::Printer; use shellexpand_utils::{canonicalize, expand}; -use std::{fs, path::PathBuf}; use tracing::info; use crate::cli::Cli; -/// Generate manual pages to a directory. +/// Generate manual pages to the given directory. /// /// This command allows you to generate manual pages (following the /// man page format) to the given directory. If the directory does not @@ -34,7 +35,7 @@ impl ManualGenerateCommand { Man::new(cmd).render(&mut buffer)?; fs::create_dir_all(&self.dir)?; - printer.log(format!("Generating man page for command {cmd_name}…"))?; + printer.log(format!("Generating man page for command {cmd_name}…\n"))?; fs::write(self.dir.join(format!("{}.1", cmd_name)), buffer)?; for subcmd in subcmds { @@ -43,7 +44,9 @@ impl ManualGenerateCommand { let mut buffer = Vec::new(); Man::new(subcmd).render(&mut buffer)?; - printer.log(format!("Generating man page for subcommand {subcmd_name}…"))?; + printer.log(format!( + "Generating man page for subcommand {subcmd_name}…\n" + ))?; fs::write( self.dir.join(format!("{}-{}.1", cmd_name, subcmd_name)), buffer, @@ -51,8 +54,8 @@ impl ManualGenerateCommand { } printer.log(format!( - "{subcmds_len} man page(s) successfully generated in {:?}!", - self.dir + "{subcmds_len} man page(s) successfully generated in {}!\n", + self.dir.display() ))?; Ok(())