diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml deleted file mode 100644 index 1e3d1f4..0000000 --- a/.github/workflows/deployment.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: deployment - -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_linux_macos_windows_github: - runs-on: ${{ matrix.os }} - needs: create_release - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-latest - os_name: linux - - os: macos-latest - os_name: macos - - os: windows-latest - os_name: windows - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Install rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - name: Check project - uses: actions-rs/cargo@v1 - with: - command: check - - name: Builds release - uses: actions-rs/cargo@v1 - with: - command: build - args: --release - - name: Compress executable (unix) - if: matrix.os_name == 'linux' || matrix.os_name == 'macos' - run: tar czf himalaya.tar.gz -C target/release himalaya - - name: Compress executable (windows) - if: matrix.os_name == 'windows' - run: tar czf himalaya.tar.gz -C target/release himalaya.exe - - name: Upload release asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create_release.outputs.upload_url }} - asset_path: himalaya.tar.gz - asset_name: himalaya-${{ matrix.os_name }}.tar.gz - asset_content_type: application/gzip - deploy_musl_github: - runs-on: ubuntu-latest - needs: create_release - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Build release - run: | - docker run -v "${PWD}:/volume" --rm -t clux/muslrust:stable cargo build --release - - name: Compress executable - run: tar czf himalaya.tar.gz -C target/x86_64-unknown-linux-musl/release himalaya - - name: Upload release asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ needs.create_release.outputs.upload_url }} - asset_path: himalaya.tar.gz - asset_name: himalaya-musl.tar.gz - asset_content_type: application/gzip - deploy_crates: - runs-on: ubuntu-latest - needs: create_release - environment: deployment - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Install rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - uses: katyo/publish-crates@v1 - with: - registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml deleted file mode 100644 index 46069ef..0000000 --- a/.github/workflows/nix.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: nix - -on: - pull_request: - push: - branches: - - master - -jobs: - nix-build: - runs-on: ubuntu-latest - steps: - - name: Checkouts code - uses: actions/checkout@v3 - - - name: Caches Nix store - uses: actions/cache@v3 - id: nix-cache - with: - path: /tmp/nix-cache - key: nix-${{ hashFiles('**/flake.*') }} - - - name: Installs Nix - uses: cachix/install-nix-action@v18 - with: - extra_nix_config: | - experimental-features = nix-command flakes - - - name: Imports Nix store cache - if: ${{ steps.nix-cache.outputs.cache-hit == 'true' }} - run: nix-store --import < /tmp/nix-cache - - - name: Builds the project - run: nix build - - - name: Exports Nix store cache - if: ${{ steps.nix-cache.outputs.cache-hit != 'true' }} - run: nix-store --export $(find /nix/store -maxdepth 1 -name '*-*') > /tmp/nix-cache diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3004c9c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,150 @@ +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_github: + runs-on: ${{ matrix.os }} + needs: create_release + strategy: + fail-fast: false + matrix: + include: + - target: linux + os: ubuntu-latest + - target: macos + os: macos-latest + - target: musl + os: ubuntu-latest + # TODO: put back when nix package .#windows is fixed + # - target: windows + # os: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Install Nix + uses: cachix/install-nix-action@v20 + with: + nix_path: nixpkgs=channel:nixos-unstable + extra_nix_config: | + experimental-features = nix-command flakes + - uses: cachix/cachix-action@v12 + with: + name: soywod + authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' + - name: Build release + run: nix build .#${{ matrix.target }} + - name: Compress executable + run: | + mkdir -p {man,completions} + cp result/bin/himalaya* . + nix run .#${{ matrix.target }} man ./man + nix run .#${{ matrix.target }} completion bash > ./completions/himalaya.bash + nix run .#${{ matrix.target }} completion elvish > ./completions/himalaya.elvish + nix run .#${{ matrix.target }} completion fish > ./completions/himalaya.fish + nix run .#${{ matrix.target }} completion powershell > ./completions/himalaya.powershell + nix run .#${{ matrix.target }} completion zsh > ./completions/himalaya.zsh + tar -czf himalaya.tar.gz himalaya* man completions + zip -r himalaya.zip himalaya* man completions + - name: Upload tar.gz release asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_path: himalaya.tar.gz + asset_name: himalaya-${{ matrix.target }}.tar.gz + asset_content_type: application/gzip + - name: Upload zip release asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_path: himalaya.zip + asset_name: himalaya-${{ matrix.target }}.zip + asset_content_type: application/zip + + # TODO: remove me when nix package .#windows is fixed + deploy_windows_github: + runs-on: windows-latest + needs: create_release + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Install rust + uses: actions-rs/toolchain@v1 + with: + command: check + - name: Builds release + uses: actions-rs/cargo@v1 + with: + command: build + args: --release + - name: Compress executable + run: | + mkdir man + mkdir completions + copy target/release/himalaya.exe . + ./himalaya.exe man ./man + ./himalaya.exe completion bash > ./completions/himalaya.bash + ./himalaya.exe completion elvish > ./completions/himalaya.elvish + ./himalaya.exe completion fish > ./completions/himalaya.fish + ./himalaya.exe completion powershell > ./completions/himalaya.powershell + ./himalaya.exe completion zsh > ./completions/himalaya.zsh + tar -czf himalaya.tar.gz himalaya.exe man completions + zip -r himalaya.zip himalaya.exe man completions + - name: Upload tar.gz release asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_path: himalaya.tar.gz + asset_name: himalaya-windows.tar.gz + asset_content_type: application/gzip + - name: Upload zip release asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.upload_url }} + asset_path: himalaya.zip + asset_name: himalaya-windows.zip + asset_content_type: application/zip + + deploy_crates: + runs-on: ubuntu-latest + needs: create_release + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Install Nix + uses: cachix/install-nix-action@v20 + with: + nix_path: nixpkgs=channel:nixos-unstable + extra_nix_config: | + experimental-features = nix-command flakes + - name: Publish library to crates.io + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: nix develop -c cargo publish --no-verify --token ${CARGO_REGISTRY_TOKEN} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f315921..d346b05 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,36 +3,22 @@ name: tests on: pull_request: push: - branches: - - master jobs: tests: runs-on: ubuntu-latest steps: - - name: Install libnotmuch - run: sudo apt-get install -y libnotmuch-dev - name: Checkout code - uses: actions/checkout@v2 - - name: Start GreenMail testing server - run: | - docker run \ - --rm \ - -d \ - -e GREENMAIL_OPTS='-Dgreenmail.setup.test.all -Dgreenmail.hostname=0.0.0.0 -Dgreenmail.auth.disabled -Dgreenmail.verbose' \ - -p 3025:3025 \ - -p 3110:3110 \ - -p 3143:3143 \ - -p 3465:3465 \ - -p 3993:3993 \ - -p 3995:3995 \ - greenmail/standalone:1.6.11 - - name: Install rust - uses: actions-rs/toolchain@v1 + uses: actions/checkout@v2 + - name: Install Nix + uses: cachix/install-nix-action@v20 with: - toolchain: stable - - name: Run tests - uses: actions-rs/cargo@v1 + nix_path: nixpkgs=channel:nixos-unstable + extra_nix_config: | + experimental-features = nix-command flakes + - uses: cachix/cachix-action@v12 with: - command: test - args: --all-features + name: soywod + authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' + - name: Test + run: nix run .#test diff --git a/CHANGELOG.md b/CHANGELOG.md index 891d90d..48147cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.7.2] - 2023-05-01 + +### Added + +- Added `create` and `delete` folder commands [sourcehut#54]. +- Added generated completions and man pages to releases + [sourcehut#43]. +- Added new account config option `sync-folders-strategy` which allows + to choose a folders synchronization strategy [sourcehut#59]: + + - `sync-folders-strategy = "all"`: synchronize all existing folders + for the current account + - `sync-folders-strategy.include = ["folder1", "folder2", …]`: + synchronize only the given folders for the current account + - `sync-folders-strategy.exclude = ["folder1", "folder2", …]`: + synchronizes all folders except the given ones for the current + account + + Also added new `account sync` arguments that override the account + config option: + + - `-A|--all-folders`: include all folders to the synchronization. + - `-F|--include-folder`: include given folders to the + synchronization. They can be repeated `-F folder1 folder2` or `-F + folder1 -F folder2`. + - `-x|--exclude-folder`: exclude given folders from the + synchronization. They can be repeated `-x folder1 folder2` or `-x + folder1 -F folder2`. + +- Added cargo features `native-tls` (default), `rustls-tls` and + `rustls-native-certs`. + +### Changed + +- Made global options truly global, which means they can be used + everywhere (not only *before* commands but also *after*) + [sourcehut#60]. +- Replaced reply all `-a` argument with `-A` because it conflicted + with the global option `-a|--account`. +- Replaced `himalaya-lib` by `pimalaya-email`. +- Renamed feature `vendored` to `native-tls-vendored`. +- Removed the `develop` branch, all the development is now done on the + `master` branch. + +### Fixed + +- Fixed config deserialization issue with `email-hooks` and + `email-reading-format`. +- Fixed flags case sensitivity. + ## [0.7.1] - 2023-02-14 ### Added @@ -17,7 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Changed the location of the - [documentation](https://pimalaya.org/himalaya/docs/). + [documentation](https://pimalaya.org/himalaya/). ### Fixed @@ -498,7 +548,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Password from command [#22] - Set up README [#20] -[Unreleased]: https://github.com/soywod/himalaya/compare/v0.7.1...develop +[Unreleased]: https://github.com/soywod/himalaya/compare/v0.7.2...develop +[0.7.2]: https://github.com/soywod/himalaya/compare/v0.7.1...v0.7.2 [0.7.1]: https://github.com/soywod/himalaya/compare/v0.7.0...v0.7.1 [0.7.0]: https://github.com/soywod/himalaya/compare/v0.6.1...v0.7.0 [0.6.1]: https://github.com/soywod/himalaya/compare/v0.6.0...v0.6.1 @@ -664,3 +715,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#356]: https://github.com/soywod/himalaya/issues/356 [#419]: https://github.com/soywod/himalaya/issues/419 [#430]: https://github.com/soywod/himalaya/issues/430 + +[sourcehut#43]: https://todo.sr.ht/~soywod/pimalaya/43 +[sourcehut#54]: https://todo.sr.ht/~soywod/pimalaya/54 +[sourcehut#59]: https://todo.sr.ht/~soywod/pimalaya/59 +[sourcehut#60]: https://todo.sr.ht/~soywod/pimalaya/60 diff --git a/Cargo.lock b/Cargo.lock index ded212d..4b40a4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,12 +94,24 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813" + [[package]] name = "bufstream" version = "0.1.4" @@ -136,7 +148,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18e9079d1a12a2cc2bffb5db039c43661836ead4082120d5844f02555aca2d46" dependencies = [ - "base64", + "base64 0.13.1", "encoding_rs", ] @@ -180,7 +192,7 @@ version = "4.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76" dependencies = [ - "bitflags", + "bitflags 1.3.2", "clap_lex", "is-terminal", "strsim 0.10.0", @@ -221,7 +233,7 @@ version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -500,11 +512,11 @@ checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" [[package]] name = "email-encoding" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34dd14c63662e0206599796cd5e1ad0268ab2b9d19b868d6050d688eba2bbf98" +checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75" dependencies = [ - "base64", + "base64 0.21.0", "memchr 2.5.0", ] @@ -765,10 +777,11 @@ dependencies = [ [[package]] name = "himalaya" -version = "0.7.1" +version = "0.7.2" dependencies = [ "anyhow", "atty", + "chrono", "clap", "clap_complete", "clap_mangen", @@ -778,10 +791,10 @@ dependencies = [ "email_address", "env_logger", "erased-serde", - "himalaya-lib", "indicatif", "log", "once_cell", + "pimalaya-email", "serde", "serde_json", "shellexpand", @@ -794,42 +807,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "himalaya-lib" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6aa84cdd1cec7bd25e319f0decd7d6ec5d765fb983da7a0dea10d797f7e73a8" -dependencies = [ - "ammonia", - "chrono", - "convert_case", - "dirs", - "html-escape", - "imap", - "imap-proto", - "lettre", - "log", - "maildir", - "mailparse", - "md5", - "mime-msg-builder", - "native-tls", - "notmuch", - "ouroboros", - "proc-lock", - "rayon", - "regex", - "rfc2047-decoder", - "rusqlite", - "serde", - "shellexpand", - "thiserror", - "tree_magic", - "urlencoding", - "utf7-imap", - "uuid", -] - [[package]] name = "hostname" version = "0.3.1" @@ -906,17 +883,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "idna" version = "0.3.0" @@ -933,7 +899,7 @@ version = "3.0.0-alpha.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1c2ff52273d9cd791687b4510d8a0047277e985a348e411c94fe84e193e7a76" dependencies = [ - "base64", + "base64 0.13.1", "bufstream", "chrono", "imap-proto", @@ -942,6 +908,7 @@ dependencies = [ "nom 7.1.1", "ouroboros", "regex", + "rustls-connector", ] [[package]] @@ -1029,25 +996,29 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lettre" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eabca5e0b4d0e98e7f2243fb5b7520b6af2b65d8f87bcc86f2c75185a6ff243" +checksum = "dd84a055407850bcf4791baa77cb4818d37cbb79ad4e60b9b659727b920d2c65" dependencies = [ - "base64", + "base64 0.21.0", "email-encoding", "email_address", "fastrand", "futures-util", "hostname", "httpdate", - "idna 0.2.3", + "idna", "mime", "native-tls", "nom 7.1.1", "once_cell", "quoted_printable", + "rustls", + "rustls-pemfile", "serde", "socket2", + "tokio", + "webpki-roots", ] [[package]] @@ -1058,9 +1029,9 @@ checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" [[package]] name = "libsqlite3-sys" -version = "0.25.2" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa" +checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" dependencies = [ "cc", "pkg-config", @@ -1116,6 +1087,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mail-parser" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2e03aa1d18528b45d0e79e46790f38cfeece6cce3af17a85912677272f36cd" +dependencies = [ + "encoding_rs", +] + [[package]] name = "maildir" version = "0.6.3" @@ -1164,12 +1144,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - [[package]] name = "md5" version = "0.7.0" @@ -1238,6 +1212,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "mio" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.42.0", +] + [[package]] name = "native-tls" version = "0.2.11" @@ -1281,6 +1267,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom8" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" +dependencies = [ + "memchr 2.5.0", +] + [[package]] name = "notmuch" version = "0.8.0" @@ -1338,7 +1333,7 @@ version = "0.10.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "020433887e44c27ff16365eaa2d380547a94544ad509aff6eb5b6e3e0b27b376" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if 1.0.0", "foreign-types", "libc", @@ -1364,6 +1359,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-src" +version = "111.25.0+1.1.1t" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3173cd3626c43e3854b1b727422a276e568d9ec5fe8cec197822cf52cfb743d6" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.78" @@ -1373,6 +1377,7 @@ dependencies = [ "autocfg", "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] @@ -1507,6 +1512,46 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pimalaya-email" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab7e9b0747644aecff24024c88348146bd74bba78fa5ecca10e7e5938d6873a" +dependencies = [ + "ammonia", + "chrono", + "convert_case", + "dirs", + "html-escape", + "imap", + "imap-proto", + "lettre", + "log", + "mail-parser", + "maildir", + "mailparse", + "md5", + "mime-msg-builder", + "native-tls", + "notmuch", + "once_cell", + "ouroboros", + "proc-lock", + "rayon", + "regex", + "rfc2047-decoder", + "rusqlite", + "rustls", + "rustls-native-certs", + "shellexpand", + "thiserror", + "tree_magic", + "urlencoding", + "utf7-imap", + "uuid", + "webpki-roots", +] + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -1702,7 +1747,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -1748,7 +1793,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11347d014ae34e1d367aaf9191c37bf071b161e2e1c09c8559c7717e87030e11" dependencies = [ - "base64", + "base64 0.13.1", "charset", "chumsky 0.8.0", "memchr 2.5.0", @@ -1756,6 +1801,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + [[package]] name = "roff" version = "0.2.1" @@ -1764,11 +1824,11 @@ checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" [[package]] name = "rusqlite" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a" +checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ - "bitflags", + "bitflags 2.2.1", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -1782,7 +1842,7 @@ version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", @@ -1790,6 +1850,51 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "rustls" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-connector" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6a18f8d10f71bce9bca6eaeb80429460e652f3bcf0381f0c5f8954abf7b3b8" +dependencies = [ + "log", + "rustls", + "rustls-native-certs", + "webpki", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +dependencies = [ + "base64 0.21.0", +] + [[package]] name = "ryu" version = "1.0.11" @@ -1818,13 +1923,23 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "security-framework" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -1843,18 +1958,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.148" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.148" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", @@ -1872,6 +1987,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +dependencies = [ + "serde", +] + [[package]] name = "shellexpand" version = "2.1.2" @@ -1912,6 +2036,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "stacker" version = "0.1.15" @@ -2074,12 +2204,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] -name = "toml" -version = "0.5.9" +name = "tokio" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" +dependencies = [ + "autocfg", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.42.0", +] + +[[package]] +name = "toml" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6" dependencies = [ "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6a7712b49e1775fb9a7b998de6635b299237f48b404dde71704f2e0e7f37e5" +dependencies = [ + "indexmap", + "nom8", + "serde", + "serde_spanned", + "toml_datetime", ] [[package]] @@ -2122,6 +2291,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.3.1" @@ -2129,7 +2304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", - "idna 0.3.0", + "idna", "percent-encoding", ] @@ -2151,7 +2326,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e326365261fc2761f0809dfb6032810534a0427bbd8f0edf546f6afeef89f5d" dependencies = [ - "base64", + "base64 0.13.1", "encoding_rs", "regex", ] @@ -2249,6 +2424,35 @@ version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +[[package]] +name = "web-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index bd282e4..6f5197e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,25 +1,36 @@ [package] name = "himalaya" -description = "Command-line interface for email management." -version = "0.7.1" +description = "CLI to manage your emails." +version = "0.7.2" authors = ["soywod "] edition = "2021" license = "MIT" categories = ["command-line-interface", "command-line-utilities", "email"] keywords = ["cli", "mail", "email", "client", "imap"] -homepage = "https://github.com/soywod/himalaya" -documentation = "https://github.com/soywod/himalaya/wiki" +homepage = "https://pimalaya.org/himalaya/" +documentation = "https://pimalaya.org/himalaya/" repository = "https://github.com/soywod/himalaya" -[package.metadata.deb] -priority = "optional" -section = "mail" +[package.metadata.docs.rs] +all-features = true [features] -imap-backend = ["himalaya-lib/imap-backend"] -smtp-sender = ["himalaya-lib/smtp-sender"] -notmuch-backend = ["himalaya-lib/notmuch-backend"] -default = ["imap-backend", "smtp-sender"] +default = ["rustls-tls", "rustls-native-certs", "imap-backend", "smtp-sender"] + +# rustls +rustls-tls = ["pimalaya-email/rustls-tls"] +rustls-native-certs = ["pimalaya-email/rustls-native-certs"] + +# native tls +native-tls = ["pimalaya-email/native-tls"] +native-tls-vendored = ["pimalaya-email/native-tls-vendored"] + +# backends +imap-backend = ["pimalaya-email/imap-backend"] +notmuch-backend = ["pimalaya-email/notmuch-backend"] + +# senders +smtp-sender = ["pimalaya-email/smtp-sender"] [dev-dependencies] tempfile = "3.3" @@ -27,6 +38,7 @@ tempfile = "3.3" [dependencies] anyhow = "1.0" atty = "0.2" +chrono = "0.4.23" clap = "4.0" clap_complete = "4.0" clap_mangen = "0.2" @@ -36,7 +48,7 @@ dialoguer = "0.10.2" email_address = "0.2.4" env_logger = "0.8" erased-serde = "0.3" -himalaya-lib = "0.6.0" +pimalaya-email = "0.7.1" indicatif = "0.17" log = "0.4" once_cell = "1.16.0" @@ -45,7 +57,7 @@ serde_json = "1.0" shellexpand = "2.1" termcolor = "1.1" terminal_size = "0.1" -toml = "0.5" +toml = "0.7.2" unicode-width = "0.1" url = "2.2" uuid = { version = "0.8", features = ["v4"] } diff --git a/README.md b/README.md index ebc9690..15ff35c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # 📫 Himalaya [![GitHub release](https://img.shields.io/github/v/release/soywod/himalaya?color=success)](https://github.com/soywod/himalaya/releases/latest) [![Matrix](https://img.shields.io/matrix/pimalaya.himalaya:matrix.org?color=success&label=chat)](https://matrix.to/#/#pimalaya.himalaya:matrix.org) -Himalaya is a CLI based on the -[himalaya-lib](https://git.sr.ht/~soywod/himalaya-lib) that allows you -to manipulate your emails using commands in your console. +https://pimalaya.org/himalaya/ + +CLI to manage your emails, based on the +[pimalaya-email](https://sr.ht/~soywod/pimalaya/) library. ![image](https://user-images.githubusercontent.com/10437171/138774902-7b9de5a3-93eb-44b0-8cfb-6d2e11e3b1aa.png) @@ -25,19 +26,19 @@ production before the `v1.0.0`.* - JSON output - … -[Folder listing]: https://pimalaya.org/himalaya/docs/cli/usage/folders/list.html -[Envelopes listing]: https://pimalaya.org/himalaya/docs/cli/usage/envelopes/list.html -[searching]: https://pimalaya.org/himalaya/docs/cli/usage/envelopes/search.html -[sorting]: https://pimalaya.org/himalaya/docs/cli/usage/envelopes/sort.html -[Email composition]: https://pimalaya.org/himalaya/docs/cli/usage/emails/write.html -[copy]: https://pimalaya.org/himalaya/docs/cli/usage/emails/copy.html -[move]: https://pimalaya.org/himalaya/docs/cli/usage/emails/move.html -[delete]: https://pimalaya.org/himalaya/docs/cli/usage/emails/delete.html -[Multi-accounting]: https://pimalaya.org/himalaya/docs/cli/configuration.html -[Account listing]: https://pimalaya.org/himalaya/docs/cli/usage/accounts/list.html -[Account synchronization]: https://pimalaya.org/himalaya/docs/cli/usage/accounts/synchronize.html -[real-time notifications]: https://pimalaya.org/himalaya/docs/cli/usage/notifications.html -[Completions]: https://pimalaya.org/himalaya/docs/cli/tips/completion.html +[Folder listing]: https://pimalaya.org/himalaya/cli/usage/folders/list.html +[Envelopes listing]: https://pimalaya.org/himalaya/cli/usage/envelopes/list.html +[searching]: https://pimalaya.org/himalaya/cli/usage/envelopes/search.html +[sorting]: https://pimalaya.org/himalaya/cli/usage/envelopes/sort.html +[Email composition]: https://pimalaya.org/himalaya/cli/usage/emails/write.html +[copy]: https://pimalaya.org/himalaya/cli/usage/emails/copy.html +[move]: https://pimalaya.org/himalaya/cli/usage/emails/move.html +[delete]: https://pimalaya.org/himalaya/cli/usage/emails/delete.html +[Multi-accounting]: https://pimalaya.org/himalaya/cli/configuration.html +[Account listing]: https://pimalaya.org/himalaya/cli/usage/accounts/list.html +[Account synchronization]: https://pimalaya.org/himalaya/cli/usage/accounts/synchronize.html +[real-time notifications]: https://pimalaya.org/himalaya/cli/usage/notifications.html +[Completions]: https://pimalaya.org/himalaya/cli/tips/completion.html ## Installation @@ -60,6 +61,9 @@ $ yay -S himalaya-git # Homebrew $ brew install himalaya +# Scoop +$ scoop install himalaya + # Cargo $ cargo install himalaya @@ -68,7 +72,7 @@ $ nix-env -i himalaya ``` *See the -[documentation](https://pimalaya.org/himalaya/docs/cli/installation.html) +[documentation](https://pimalaya.org/himalaya/cli/installation.html) for other installation methods.* @@ -78,11 +82,12 @@ for other installation methods.* ## Configuration Please read the -[documentation](https://pimalaya.org/himalaya/docs/cli/configuration.html). +[documentation](https://pimalaya.org/himalaya/cli/configuration.html). ## Contributing -If you find a **bug**, please send an email at +If you find a **bug** that [does not exist +yet](https://todo.sr.ht/~soywod/pimalaya), please send an email at [~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht). If you have a **question**, please send an email at diff --git a/config.sample.toml b/config.sample.toml new file mode 100644 index 0000000..8870953 --- /dev/null +++ b/config.sample.toml @@ -0,0 +1,52 @@ +display-name = "Display NAME" +signature-delim = "~~" +signature = "~/.signature" +downloads-dir = "~/downloads" +folder-listing-page-size = 12 +email-listing-page-size = 12 +email-reading-headers = ["From", "To"] +email-reading-verify-cmd = "gpg --verify -q" +email-reading-decrypt-cmd = "gpg -dq" +email-writing-sign-cmd = "gpg -o - -saq" +email-writing-encrypt-cmd = "gpg -o - -eqar " + +[example] +default = true +display-name = "Display NAME (gmail)" +email = "display.name@gmail.local" + +backend = "imap" +imap-host = "imap.gmail.com" +imap-login = "display.name@gmail.local" +imap-passwd-cmd = "pass show gmail" +imap-port = 993 +imap-ssl = true +imap-starttls = false +imap-notify-cmd = """📫 "" """"" +imap-notify-query = "NOT SEEN" +imap-watch-cmds = ["echo \"received server changes!\""] + +sender = "smtp" +smtp-host = "smtp.gmail.com" +smtp-login = "display.name@gmail.local" +smtp-passwd-cmd = "pass show piana/gmail" +smtp-port = 465 +smtp-ssl = true +smtp-starttls = false + +sync = true +sync-dir = "/tmp/sync/gmail" +sync-folders-strategy.only = ["INBOX"] + +[example.folder-aliases] +inbox = "INBOX" +drafts = "[Gmail]/Drafts" +sent = "[Gmail]/Sent Mail" +trash = "[Gmail]/Trash" + +[example.email-hooks] +pre-send = "echo $1" + +[example.email-reading-format] +type = "fixed" +width = 64 diff --git a/flake.lock b/flake.lock index 95c6151..a344611 100644 --- a/flake.lock +++ b/flake.lock @@ -1,28 +1,36 @@ { "nodes": { - "flake-compat": { - "flake": false, + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, "locked": { - "lastModified": 1673956053, - "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "lastModified": 1682835640, + "narHash": "sha256-rAYEOd4nZFLjDlrF9KNlcopPKNVtr1svSXcEValVRMY=", + "owner": "nix-community", + "repo": "fenix", + "rev": "006b429d3c493f4c5b1743a94f71ad961c7693ab", "type": "github" }, "original": { - "owner": "edolstra", - "repo": "flake-compat", + "owner": "nix-community", + "repo": "fenix", "type": "github" } }, "flake-utils": { + "inputs": { + "systems": "systems" + }, "locked": { - "lastModified": 1659877975, - "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "lastModified": 1681202837, + "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", "owner": "numtide", "repo": "flake-utils", - "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "rev": "cfacdce06f30d2b68473a46042957675eebb3401", "type": "github" }, "original": { @@ -31,16 +39,38 @@ "type": "github" } }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1660459072, + "narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "a20de23b925fd8264fd7fad6454652e142fd7f73", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, "naersk": { "inputs": { - "nixpkgs": "nixpkgs" + "nixpkgs": [ + "nixpkgs" + ] }, "locked": { - "lastModified": 1671096816, - "narHash": "sha256-ezQCsNgmpUHdZANDCILm3RvtO1xH8uujk/+EqNvzIOg=", + "lastModified": 1679567394, + "narHash": "sha256-ZvLuzPeARDLiQUt6zSZFGOs+HZmE+3g4QURc8mkBsfM=", "owner": "nix-community", "repo": "naersk", - "rev": "d998160d6a076cfe8f9741e56aeec7e267e3e114", + "rev": "88cd22380154a2c36799fe8098888f0f59861a15", "type": "github" }, "original": { @@ -51,88 +81,58 @@ }, "nixpkgs": { "locked": { - "lastModified": 1675698036, - "narHash": "sha256-BgsQkQewdlQi8gapJN4phpxkI/FCE/2sORBaFcYbp/A=", - "owner": "NixOS", + "lastModified": 1682669017, + "narHash": "sha256-Vi+p4y3wnl0/4gcwTdmCO398kKlDaUrNROtf3GOD2NY=", + "owner": "nixos", "repo": "nixpkgs", - "rev": "1046c7b92e908a1202c0f1ba3fc21d19e1cf1b62", + "rev": "7449971a3ecf857b4a554cf79b1d9dcc1a4647d8", "type": "github" }, "original": { - "id": "nixpkgs", - "type": "indirect" - } - }, - "nixpkgs_2": { - "locked": { - "lastModified": 1664356419, - "narHash": "sha256-PD0hM9YWp2lepAJk7edh8g1VtzJip5rals1fpoQUlY0=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "46e8398474ac3b1b7bb198bf9097fc213bbf59b1", - "type": "github" - }, - "original": { - "id": "nixpkgs", - "type": "indirect" - } - }, - "nixpkgs_3": { - "locked": { - "lastModified": 1665296151, - "narHash": "sha256-uOB0oxqxN9K7XGF1hcnY+PQnlQJ+3bP2vCn/+Ru/bbc=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "14ccaaedd95a488dd7ae142757884d8e125b3363", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", + "owner": "nixos", + "ref": "nixos-22.11", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { - "flake-compat": "flake-compat", - "naersk": "naersk", - "nixpkgs": "nixpkgs_2", - "rust-overlay": "rust-overlay", - "utils": "utils" - } - }, - "rust-overlay": { - "inputs": { + "fenix": "fenix", "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs_3" - }, + "gitignore": "gitignore", + "naersk": "naersk", + "nixpkgs": "nixpkgs" + } + }, + "rust-analyzer-src": { + "flake": false, "locked": { - "lastModified": 1675823425, - "narHash": "sha256-o/uLXQdq3OrRAv4BZVVY0VmhMmQBLWw6Y4o+p6ZiaR4=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "02e1abbdcbc2d516193ff8a7add71f44cd976ba0", + "lastModified": 1682792082, + "narHash": "sha256-1nuP2rqipsdB8IJ3N5ws3FQm4dX3mKIueIrCUSu1bWw=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "7bcb4c2ef23e151a639ff918fbb8ab9d521eabb9", "type": "github" }, "original": { - "owner": "oxalica", - "repo": "rust-overlay", + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", "type": "github" } }, - "utils": { + "systems": { "locked": { - "lastModified": 1667395993, - "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "type": "github" }, "original": { - "owner": "numtide", - "repo": "flake-utils", + "owner": "nix-systems", + "repo": "default", "type": "github" } } diff --git a/flake.nix b/flake.nix index b8baf74..34bc322 100644 --- a/flake.nix +++ b/flake.nix @@ -1,65 +1,140 @@ { - description = "Command-line interface for email management."; + description = "CLI to manage your emails."; inputs = { - utils.url = "github:numtide/flake-utils"; - rust-overlay.url = "github:oxalica/rust-overlay"; - naersk.url = "github:nix-community/naersk"; - flake-compat = { - url = "github:edolstra/flake-compat"; - flake = false; + nixpkgs.url = "github:nixos/nixpkgs/nixos-22.11"; + flake-utils.url = "github:numtide/flake-utils"; + gitignore = { + url = "github:hercules-ci/gitignore.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + fenix = { + url = "github:nix-community/fenix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + naersk = { + url = "github:nix-community/naersk"; + inputs.nixpkgs.follows = "nixpkgs"; }; }; - outputs = { self, nixpkgs, utils, rust-overlay, naersk, ... }: - utils.lib.eachDefaultSystem - (system: + outputs = { self, nixpkgs, flake-utils, gitignore, fenix, naersk }: + let + inherit (gitignore.lib) gitignoreSource; + + mkToolchain = buildPlatform: + fenix.packages.${buildPlatform}.minimal.toolchain; + + mkToolchainWithTarget = buildPlatform: targetPlatform: + with fenix.packages.${buildPlatform}; combine [ + stable.rustc + stable.cargo + targets.${targetPlatform}.stable.rust-std + ]; + + mkDevShells = buildPlatform: let - name = "himalaya"; - overlays = [ (import rust-overlay) ]; - pkgs = import nixpkgs { inherit system overlays; }; + pkgs = import nixpkgs { system = buildPlatform; }; + rust-toolchain = fenix.packages.${buildPlatform}.fromToolchainFile { + file = ./rust-toolchain.toml; + sha256 = "eMJethw5ZLrJHmoN2/l0bIyQjoTX1NsvalWSscTixpI="; + }; in - rec { - # nix build - defaultPackage = packages.${name}; - packages = { - ${name} = naersk.lib.${system}.buildPackage { - pname = name; - root = ./.; - nativeBuildInputs = with pkgs; [ openssl.dev pkg-config ]; - overrideMain = _: { - postInstall = '' - mkdir -p $out/share/applications/ - cp assets/himalaya.desktop $out/share/applications/ - ''; - }; - }; - }; - - # nix run - defaultApp = apps.${name}; - apps.${name} = utils.lib.mkApp { - inherit name; - drv = packages.${name}; - }; - - # nix develop - devShell = pkgs.mkShell { - inputsFrom = builtins.attrValues self.packages.${system}; - nativeBuildInputs = with pkgs; [ - # Nix LSP + formatter + { + default = pkgs.mkShell { + buildInputs = with pkgs; [ + # Nix env rnix-lsp nixpkgs-fmt # Rust env - (rust-bin.fromRustupToolchainFile ./rust-toolchain.toml) - cargo-watch - rust-analyzer + rust-toolchain - # Notmuch + # notmuch notmuch ]; }; - } - ); + }; + + mkPackage = pkgs: buildPlatform: targetPlatform: package: + let + toolchain = + if isNull targetPlatform + then mkToolchain buildPlatform + else mkToolchainWithTarget buildPlatform targetPlatform; + naersk' = naersk.lib.${buildPlatform}.override { + cargo = toolchain; + rustc = toolchain; + }; + package' = { + name = "himalaya"; + src = gitignoreSource ./.; + overrideMain = _: { + postInstall = '' + mkdir -p $out/share/applications/ + cp assets/himalaya.desktop $out/share/applications/ + ''; + }; + } // pkgs.lib.optionalAttrs (!isNull targetPlatform) { + CARGO_BUILD_TARGET = targetPlatform; + } // package; + in + naersk'.buildPackage package'; + + mkPackages = buildPlatform: + let + pkgs = import nixpkgs { system = buildPlatform; }; + mkPackageWithTarget = mkPackage pkgs buildPlatform; + defaultPackage = mkPackage pkgs buildPlatform null { }; + in + { + default = defaultPackage; + linux = defaultPackage; + macos = defaultPackage; + musl = mkPackageWithTarget "x86_64-unknown-linux-musl" (with pkgs.pkgsStatic; { + CARGO_BUILD_RUSTFLAGS = "-C target-feature=+crt-static"; + SQLITE3_STATIC = 1; + SQLITE3_LIB_DIR = "${sqlite.out}/lib"; + hardeningDisable = [ "all" ]; + }); + # FIXME: package does not build, assembler messages: unknown + # pseudo-op… + windows = mkPackageWithTarget "x86_64-pc-windows-gnu" { + strictDeps = true; + depsBuildBuild = with pkgs.pkgsCross.mingwW64; [ + stdenv.cc + windows.pthreads + ]; + }; + }; + + mkApp = drv: flake-utils.lib.mkApp { + inherit drv; + name = "himalaya"; + }; + + mkApps = buildPlatform: { + default = mkApp self.packages.${buildPlatform}.default; + linux = mkApp self.packages.${buildPlatform}.linux; + macos = mkApp self.packages.${buildPlatform}.macos; + musl = mkApp self.packages.${buildPlatform}.musl; + windows = + let + pkgs = import nixpkgs { system = buildPlatform; }; + wine = pkgs.wine.override { wineBuild = "wine64"; }; + himalaya = self.packages.${buildPlatform}.windows; + app = pkgs.writeShellScriptBin "himalaya" '' + export WINEPREFIX="$(mktemp -d)" + ${wine}/bin/wine64 ${himalaya}/bin/himalaya.exe $@ + ''; + in + mkApp app; + }; + + in + flake-utils.lib.eachDefaultSystem (system: { + devShells = mkDevShells system; + packages = mkPackages system; + apps = mkApps system; + }); } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 292fe49..aff8cc2 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,11 @@ [toolchain] channel = "stable" +profile = "default" +components = [ + "cargo", + "clippy", + "rust-analyzer", + "rust-std", + "rustc", + "rustfmt", +] diff --git a/src/cache/args.rs b/src/cache/args.rs index 9da7989..cd40a09 100644 --- a/src/cache/args.rs +++ b/src/cache/args.rs @@ -8,8 +8,13 @@ const ARG_DISABLE_CACHE: &str = "disable-cache"; /// the user to disable any sort of cache. pub fn arg() -> Arg { Arg::new(ARG_DISABLE_CACHE) - .long("disable-cache") .help("Disable any sort of cache") + .long_help( + "Disable any sort of cache. The action depends on +the command it applies on.", + ) + .long("disable-cache") + .global(true) .action(ArgAction::SetTrue) } diff --git a/src/config/args.rs b/src/config/args.rs index 20ba201..ec425f5 100644 --- a/src/config/args.rs +++ b/src/config/args.rs @@ -8,9 +8,10 @@ const ARG_CONFIG: &str = "config"; /// user to customize the config file path. pub fn arg() -> Arg { Arg::new(ARG_CONFIG) + .help("Set a custom configuration file path") .long("config") .short('c') - .help("Forces a specific config file path") + .global(true) .value_name("PATH") } diff --git a/src/config/config.rs b/src/config/config.rs index dbb96d3..caeded6 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -5,8 +5,8 @@ use anyhow::{anyhow, Context, Result}; use dirs::{config_dir, home_dir}; -use himalaya_lib::{AccountConfig, BackendConfig, EmailHooks, EmailTextPlainFormat}; use log::{debug, trace}; +use pimalaya_email::{AccountConfig, BackendConfig, EmailHooks, EmailTextPlainFormat}; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fs, path::PathBuf}; use toml; @@ -17,7 +17,7 @@ use crate::{ }; /// Represents the user config file. -#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct DeserializedConfig { #[serde(alias = "name")] @@ -31,12 +31,8 @@ pub struct DeserializedConfig { pub email_listing_page_size: Option, pub email_reading_headers: Option>, - #[serde( - default, - with = "EmailTextPlainFormatOptionDef", - skip_serializing_if = "Option::is_none" - )] - pub email_reading_format: Option, + #[serde(default, with = "EmailTextPlainFormatDef")] + pub email_reading_format: EmailTextPlainFormat, pub email_reading_verify_cmd: Option, pub email_reading_decrypt_cmd: Option, pub email_writing_headers: Option>, @@ -44,10 +40,10 @@ pub struct DeserializedConfig { pub email_writing_encrypt_cmd: Option, #[serde( default, - with = "EmailHooksOptionDef", - skip_serializing_if = "Option::is_none" + with = "EmailHooksDef", + skip_serializing_if = "EmailHooks::is_empty" )] - pub email_hooks: Option, + pub email_hooks: EmailHooks, #[serde(flatten)] pub accounts: HashMap, @@ -123,12 +119,12 @@ impl DeserializedConfig { #[cfg(test)] mod tests { - use himalaya_lib::{EmailSender, MaildirConfig, SendmailConfig, SmtpConfig}; + use pimalaya_email::{EmailSender, MaildirConfig, SendmailConfig, SmtpConfig}; #[cfg(feature = "imap-backend")] - use himalaya_lib::ImapConfig; + use pimalaya_email::ImapConfig; #[cfg(feature = "notmuch-backend")] - use himalaya_lib::NotmuchConfig; + use pimalaya_email::NotmuchConfig; use std::io::Write; use tempfile::NamedTempFile; @@ -163,10 +159,11 @@ mod tests { fn account_missing_backend_field() { let config = make_config("[account]"); - assert_eq!( - config.unwrap_err().root_cause().to_string(), - "missing field `backend` at line 1 column 1" - ); + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `backend`")); } #[test] @@ -180,7 +177,7 @@ mod tests { .unwrap_err() .root_cause() .to_string() - .starts_with("unknown variant `bad`")); + .contains("unknown variant `bad`")); } #[test] @@ -190,10 +187,11 @@ mod tests { backend = \"none\"", ); - assert_eq!( - config.unwrap_err().root_cause().to_string(), - "missing field `email` at line 1 column 1" - ); + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `email`")); } #[test] @@ -205,10 +203,11 @@ mod tests { backend = \"imap\"", ); - assert_eq!( - config.unwrap_err().root_cause().to_string(), - "missing field `imap-host` at line 1 column 1" - ); + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `imap-host`")); } #[test] @@ -221,10 +220,11 @@ mod tests { imap-host = \"localhost\"", ); - assert_eq!( - config.unwrap_err().root_cause().to_string(), - "missing field `imap-port` at line 1 column 1" - ); + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `imap-port`")); } #[test] @@ -238,10 +238,11 @@ mod tests { imap-port = 993", ); - assert_eq!( - config.unwrap_err().root_cause().to_string(), - "missing field `imap-login` at line 1 column 1" - ); + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `imap-login`")); } #[test] @@ -256,10 +257,11 @@ mod tests { imap-login = \"login\"", ); - assert_eq!( - config.unwrap_err().root_cause().to_string(), - "missing field `imap-passwd-cmd` at line 1 column 1" - ); + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `imap-passwd-cmd`")); } #[test] @@ -271,10 +273,11 @@ mod tests { backend = \"maildir\"", ); - assert_eq!( - config.unwrap_err().root_cause().to_string(), - "missing field `maildir-root-dir` at line 1 column 1" - ); + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `maildir-root-dir`")); } #[cfg(feature = "notmuch-backend")] @@ -287,10 +290,11 @@ mod tests { backend = \"notmuch\"", ); - assert_eq!( - config.unwrap_err().root_cause().to_string(), - "missing field `notmuch-db-path` at line 1 column 1" - ); + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `notmuch-db-path`")); } #[test] @@ -301,10 +305,11 @@ mod tests { backend = \"none\"", ); - assert_eq!( - config.unwrap_err().root_cause().to_string(), - "missing field `sender` at line 1 column 1" - ); + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `sender`")); } #[test] @@ -316,10 +321,11 @@ mod tests { sender = \"bad\"", ); - assert_eq!( - config.unwrap_err().root_cause().to_string(), - "unknown variant `bad`, expected one of `none`, `smtp`, `sendmail` at line 1 column 1", - ); + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("unknown variant `bad`, expected one of `none`, `smtp`, `sendmail`"),); } #[test] @@ -331,10 +337,11 @@ mod tests { sender = \"smtp\"", ); - assert_eq!( - config.unwrap_err().root_cause().to_string(), - "missing field `smtp-host` at line 1 column 1" - ); + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `smtp-host`")); } #[test] @@ -347,10 +354,11 @@ mod tests { smtp-host = \"localhost\"", ); - assert_eq!( - config.unwrap_err().root_cause().to_string(), - "missing field `smtp-port` at line 1 column 1" - ); + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `smtp-port`")); } #[test] @@ -364,10 +372,11 @@ mod tests { smtp-port = 25", ); - assert_eq!( - config.unwrap_err().root_cause().to_string(), - "missing field `smtp-login` at line 1 column 1" - ); + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `smtp-login`")); } #[test] @@ -382,10 +391,11 @@ mod tests { smtp-login = \"login\"", ); - assert_eq!( - config.unwrap_err().root_cause().to_string(), - "missing field `smtp-passwd-cmd` at line 1 column 1" - ); + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `smtp-passwd-cmd`")); } #[test] @@ -397,10 +407,11 @@ mod tests { sender = \"sendmail\"", ); - assert_eq!( - config.unwrap_err().root_cause().to_string(), - "missing field `sendmail-cmd` at line 1 column 1" - ); + assert!(config + .unwrap_err() + .root_cause() + .to_string() + .contains("missing field `sendmail-cmd`")); } #[test] diff --git a/src/config/prelude.rs b/src/config/prelude.rs index 9f30f97..d121f28 100644 --- a/src/config/prelude.rs +++ b/src/config/prelude.rs @@ -1,17 +1,17 @@ -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; - -use himalaya_lib::{ - EmailHooks, EmailSender, EmailTextPlainFormat, MaildirConfig, SendmailConfig, SmtpConfig, +use pimalaya_email::{ + folder::sync::Strategy as SyncFoldersStrategy, EmailHooks, EmailSender, EmailTextPlainFormat, + MaildirConfig, SendmailConfig, SmtpConfig, }; +use serde::{Deserialize, Serialize}; +use std::{collections::HashSet, path::PathBuf}; #[cfg(feature = "imap-backend")] -use himalaya_lib::ImapConfig; +use pimalaya_email::ImapConfig; #[cfg(feature = "notmuch-backend")] -use himalaya_lib::NotmuchConfig; +use pimalaya_email::NotmuchConfig; -#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] #[serde(remote = "SmtpConfig")] struct SmtpConfigDef { #[serde(rename = "smtp-host")] @@ -31,7 +31,7 @@ struct SmtpConfigDef { } #[cfg(feature = "imap-backend")] -#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] #[serde(remote = "ImapConfig")] pub struct ImapConfigDef { #[serde(rename = "imap-host")] @@ -56,41 +56,39 @@ pub struct ImapConfigDef { pub watch_cmds: Option>, } -#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] -#[serde(remote = "MaildirConfig")] +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +#[serde(remote = "MaildirConfig", rename_all = "kebab-case")] pub struct MaildirConfigDef { #[serde(rename = "maildir-root-dir")] pub root_dir: PathBuf, } #[cfg(feature = "notmuch-backend")] -#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] -#[serde(remote = "NotmuchConfig")] +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +#[serde(remote = "NotmuchConfig", rename_all = "kebab-case")] pub struct NotmuchConfigDef { #[serde(rename = "notmuch-db-path")] pub db_path: PathBuf, } -#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] -#[serde(remote = "Option")] -pub enum EmailTextPlainFormatOptionDef { - #[serde(with = "EmailTextPlainFormatDef")] - Some(EmailTextPlainFormat), - #[default] - None, -} - -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] -#[serde(remote = "EmailTextPlainFormat", rename_all = "snake_case")] +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +#[serde( + remote = "EmailTextPlainFormat", + tag = "type", + content = "width", + rename_all = "kebab-case" +)] pub enum EmailTextPlainFormatDef { + #[default] Auto, Flowed, Fixed(usize), } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] -#[serde(remote = "EmailSender", tag = "sender", rename_all = "snake_case")] +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +#[serde(remote = "EmailSender", tag = "sender", rename_all = "kebab-case")] pub enum EmailSenderDef { + #[default] None, #[serde(with = "SmtpConfigDef")] Smtp(SmtpConfig), @@ -98,27 +96,30 @@ pub enum EmailSenderDef { Sendmail(SendmailConfig), } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] -#[serde(remote = "SendmailConfig")] +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +#[serde(remote = "SendmailConfig", rename_all = "kebab-case")] pub struct SendmailConfigDef { #[serde(rename = "sendmail-cmd")] cmd: String, } -#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] -#[serde(remote = "Option")] -pub enum EmailHooksOptionDef { - #[serde(with = "EmailHooksDef")] - Some(EmailHooks), - #[default] - None, -} - /// Represents the email hooks. Useful for doing extra email /// processing before or after sending it. -#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] -#[serde(remote = "EmailHooks")] +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +#[serde(remote = "EmailHooks", rename_all = "kebab-case")] pub struct EmailHooksDef { /// Represents the hook called just before sending an email. pub pre_send: Option, } + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] +#[serde(remote = "SyncFoldersStrategy", rename_all = "kebab-case")] +pub enum SyncFoldersStrategyDef { + #[default] + All, + #[serde(alias = "only")] + Include(HashSet), + #[serde(alias = "except")] + #[serde(alias = "ignore")] + Exclude(HashSet), +} diff --git a/src/config/wizard/imap.rs b/src/config/wizard/imap.rs index 74d93eb..33f4b8e 100644 --- a/src/config/wizard/imap.rs +++ b/src/config/wizard/imap.rs @@ -4,7 +4,7 @@ use crate::account::{ }; use anyhow::Result; use dialoguer::{Input, Select}; -use himalaya_lib::ImapConfig; +use pimalaya_email::ImapConfig; #[cfg(feature = "imap-backend")] pub(crate) fn configure(base: DeserializedBaseAccountConfig) -> Result { diff --git a/src/config/wizard/maildir.rs b/src/config/wizard/maildir.rs index 2670301..037deb2 100644 --- a/src/config/wizard/maildir.rs +++ b/src/config/wizard/maildir.rs @@ -5,7 +5,7 @@ use crate::account::{ use anyhow::Result; use dialoguer::Input; use dirs::home_dir; -use himalaya_lib::MaildirConfig; +use pimalaya_email::MaildirConfig; pub(crate) fn configure(base: DeserializedBaseAccountConfig) -> Result { let input = if let Some(home) = home_dir() { diff --git a/src/config/wizard/mod.rs b/src/config/wizard/mod.rs index 2282af8..cc58608 100644 --- a/src/config/wizard/mod.rs +++ b/src/config/wizard/mod.rs @@ -32,7 +32,6 @@ const SECURITY_PROTOCOLS: &[&str] = &["SSL/TLS", "STARTTLS", "None"]; static THEME: Lazy = Lazy::new(ColorfulTheme::default); pub(crate) fn wizard() -> Result { - trace!(">> wizard"); println!("Himalaya couldn't find an already existing configuration file."); match Confirm::new() @@ -111,7 +110,7 @@ pub(crate) fn wizard() -> Result { // Serialize config to file println!("\nWriting the configuration to {path:?}..."); fs::create_dir_all(path.parent().unwrap())?; - fs::write(path, toml::to_vec(&config)?)?; + fs::write(path, toml::to_string(&config)?)?; trace!("<< wizard"); Ok(config) diff --git a/src/config/wizard/notmuch.rs b/src/config/wizard/notmuch.rs index 15a78b1..df9cc6f 100644 --- a/src/config/wizard/notmuch.rs +++ b/src/config/wizard/notmuch.rs @@ -4,7 +4,7 @@ use crate::account::{ }; use anyhow::Result; use dialoguer::Input; -use himalaya_lib::{NotmuchBackend, NotmuchConfig}; +use pimalaya_email::{NotmuchBackend, NotmuchConfig}; pub(crate) fn configure(base: DeserializedBaseAccountConfig) -> Result { let db_path = match NotmuchBackend::get_default_db_path() { diff --git a/src/config/wizard/sendmail.rs b/src/config/wizard/sendmail.rs index 82ae1a6..734fe69 100644 --- a/src/config/wizard/sendmail.rs +++ b/src/config/wizard/sendmail.rs @@ -1,7 +1,7 @@ use super::THEME; use anyhow::Result; use dialoguer::Input; -use himalaya_lib::{EmailSender, SendmailConfig}; +use pimalaya_email::{EmailSender, SendmailConfig}; pub(crate) fn configure() -> Result { Ok(EmailSender::Sendmail(SendmailConfig { diff --git a/src/config/wizard/smtp.rs b/src/config/wizard/smtp.rs index 3d0359d..197973f 100644 --- a/src/config/wizard/smtp.rs +++ b/src/config/wizard/smtp.rs @@ -2,7 +2,7 @@ use super::{SECURITY_PROTOCOLS, THEME}; use crate::account::DeserializedBaseAccountConfig; use anyhow::Result; use dialoguer::{Input, Select}; -use himalaya_lib::{EmailSender, SmtpConfig}; +use pimalaya_email::{EmailSender, SmtpConfig}; pub(crate) fn configure(base: &DeserializedBaseAccountConfig) -> Result { let mut smtp_config = SmtpConfig { diff --git a/src/domain/account/args.rs b/src/domain/account/args.rs index 701552b..02233da 100644 --- a/src/domain/account/args.rs +++ b/src/domain/account/args.rs @@ -3,8 +3,10 @@ use anyhow::Result; use clap::{Arg, ArgAction, ArgMatches, Command}; use log::info; +use pimalaya_email::folder::sync::Strategy as SyncFoldersStrategy; +use std::collections::HashSet; -use crate::ui::table; +use crate::{folder, ui::table}; const ARG_ACCOUNT: &str = "account"; const ARG_DRY_RUN: &str = "dry-run"; @@ -20,7 +22,7 @@ pub enum Cmd { /// Represents the list accounts command. List(table::args::MaxTableWidth), /// Represents the sync account command. - Sync(DryRun), + Sync(Option, DryRun), } /// Represents the account command matcher. @@ -29,7 +31,22 @@ pub fn matches(m: &ArgMatches) -> Result> { if let Some(m) = m.subcommand_matches(CMD_SYNC) { info!("sync account subcommand matched"); let dry_run = parse_dry_run_arg(m); - Some(Cmd::Sync(dry_run)) + let include = folder::args::parse_include_arg(m); + let exclude = folder::args::parse_exclude_arg(m); + let folders_strategy = if let Some(folder) = folder::args::parse_source_arg(m) { + Some(SyncFoldersStrategy::Include(HashSet::from_iter([ + folder.to_owned() + ]))) + } else if !include.is_empty() { + Some(SyncFoldersStrategy::Include(include.to_owned())) + } else if !exclude.is_empty() { + Some(SyncFoldersStrategy::Exclude(exclude)) + } else if folder::args::parse_all_arg(m) { + Some(SyncFoldersStrategy::All) + } else { + None + }; + Some(Cmd::Sync(folders_strategy, dry_run)) } else if let Some(m) = m.subcommand_matches(CMD_LIST) { info!("list accounts subcommand matched"); let max_table_width = table::args::parse_max_width(m); @@ -55,6 +72,13 @@ pub fn subcmd() -> Command { .arg(table::args::max_width()), Command::new(CMD_SYNC) .about("Synchronize the given account locally") + .arg(folder::args::all_arg("Synchronize all folders")) + .arg(folder::args::include_arg( + "Synchronize only the given folders", + )) + .arg(folder::args::exclude_arg( + "Synchronize all folders except the given ones", + )) .arg(dry_run()), ]) } @@ -63,9 +87,10 @@ pub fn subcmd() -> Command { /// the user to select a different account than the default one. pub fn arg() -> Arg { Arg::new(ARG_ACCOUNT) + .help("Set the account") .long("account") .short('a') - .help("Select a specific account by name") + .global(true) .value_name("STRING") } diff --git a/src/domain/account/config.rs b/src/domain/account/config.rs index fa76224..4fd3d01 100644 --- a/src/domain/account/config.rs +++ b/src/domain/account/config.rs @@ -3,24 +3,24 @@ //! This module contains the raw deserialized representation of an //! account in the accounts section of the user configuration file. -use himalaya_lib::{ - AccountConfig, BackendConfig, EmailHooks, EmailSender, EmailTextPlainFormat, MaildirConfig, +use pimalaya_email::{ + folder::sync::Strategy as SyncFoldersStrategy, AccountConfig, BackendConfig, EmailHooks, + EmailSender, EmailTextPlainFormat, MaildirConfig, }; - -#[cfg(feature = "imap-backend")] -use himalaya_lib::ImapConfig; - -#[cfg(feature = "notmuch-backend")] -use himalaya_lib::NotmuchConfig; - use serde::{Deserialize, Serialize}; use std::{collections::HashMap, path::PathBuf}; +#[cfg(feature = "imap-backend")] +use pimalaya_email::ImapConfig; + +#[cfg(feature = "notmuch-backend")] +use pimalaya_email::NotmuchConfig; + use crate::config::{prelude::*, DeserializedConfig}; /// Represents all existing kind of account config. -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] -#[serde(tag = "backend", rename_all = "snake_case")] +#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[serde(tag = "backend", rename_all = "kebab-case")] pub enum DeserializedAccountConfig { None(DeserializedBaseAccountConfig), Maildir(DeserializedMaildirAccountConfig), @@ -70,7 +70,7 @@ impl DeserializedAccountConfig { } } -#[derive(Default, Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct DeserializedBaseAccountConfig { pub email: String, @@ -85,12 +85,8 @@ pub struct DeserializedBaseAccountConfig { pub email_listing_page_size: Option, pub email_reading_headers: Option>, - #[serde( - default, - with = "EmailTextPlainFormatOptionDef", - skip_serializing_if = "Option::is_none" - )] - pub email_reading_format: Option, + #[serde(default, with = "EmailTextPlainFormatDef")] + pub email_reading_format: EmailTextPlainFormat, pub email_reading_verify_cmd: Option, pub email_reading_decrypt_cmd: Option, pub email_writing_headers: Option>, @@ -100,14 +96,16 @@ pub struct DeserializedBaseAccountConfig { pub email_sender: EmailSender, #[serde( default, - with = "EmailHooksOptionDef", - skip_serializing_if = "Option::is_none" + with = "EmailHooksDef", + skip_serializing_if = "EmailHooks::is_empty" )] - pub email_hooks: Option, + pub email_hooks: EmailHooks, #[serde(default)] pub sync: bool, pub sync_dir: Option, + #[serde(default, with = "SyncFoldersStrategyDef")] + pub sync_folders_strategy: SyncFoldersStrategy, } impl DeserializedBaseAccountConfig { @@ -159,12 +157,7 @@ impl DeserializedBaseAccountConfig { .as_ref() .map(ToOwned::to_owned) .or_else(|| config.email_reading_headers.as_ref().map(ToOwned::to_owned)), - email_reading_format: self - .email_reading_format - .as_ref() - .map(ToOwned::to_owned) - .or_else(|| config.email_reading_format.as_ref().map(ToOwned::to_owned)) - .unwrap_or_default(), + email_reading_format: self.email_reading_format.clone(), email_reading_verify_cmd: self .email_reading_verify_cmd .as_ref() @@ -212,26 +205,16 @@ impl DeserializedBaseAccountConfig { .or_else(|| config.email_writing_headers.as_ref().map(ToOwned::to_owned)), email_sender: self.email_sender.to_owned(), email_hooks: EmailHooks { - pre_send: self - .email_hooks - .as_ref() - .map(ToOwned::to_owned) - .map(|hook| hook.pre_send) - .or_else(|| { - config - .email_hooks - .as_ref() - .map(|hook| hook.pre_send.as_ref().map(ToOwned::to_owned)) - }) - .unwrap_or_default(), + pre_send: self.email_hooks.pre_send.clone(), }, sync: self.sync, sync_dir: self.sync_dir.clone(), + sync_folders_strategy: self.sync_folders_strategy.clone(), } } } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] #[cfg(feature = "imap-backend")] pub struct DeserializedImapAccountConfig { #[serde(flatten)] @@ -240,7 +223,7 @@ pub struct DeserializedImapAccountConfig { pub backend: ImapConfig, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] pub struct DeserializedMaildirAccountConfig { #[serde(flatten)] pub base: DeserializedBaseAccountConfig, @@ -248,7 +231,7 @@ pub struct DeserializedMaildirAccountConfig { pub backend: MaildirConfig, } -#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] #[cfg(feature = "notmuch-backend")] pub struct DeserializedNotmuchAccountConfig { #[serde(flatten)] diff --git a/src/domain/account/handlers.rs b/src/domain/account/handlers.rs index 0f5aeca..a0e4e33 100644 --- a/src/domain/account/handlers.rs +++ b/src/domain/account/handlers.rs @@ -3,9 +3,12 @@ //! This module gathers all account actions triggered by the CLI. use anyhow::Result; -use himalaya_lib::{AccountConfig, Backend, BackendSyncBuilder, BackendSyncProgressEvent}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use log::{info, trace}; +use pimalaya_email::{ + folder::sync::Strategy as SyncFoldersStrategy, AccountConfig, Backend, BackendSyncBuilder, + BackendSyncProgressEvent, +}; use crate::{ config::DeserializedConfig, @@ -43,15 +46,17 @@ pub fn sync( account_config: &AccountConfig, printer: &mut P, backend: &dyn Backend, - folder: &Option, + folders_strategy: Option, dry_run: bool, ) -> Result<()> { info!("entering the sync accounts handler"); - trace!("dry run: {}", dry_run); + trace!("dry run: {dry_run}"); + trace!("folders strategy: {folders_strategy:#?}"); let mut sync_builder = BackendSyncBuilder::new(account_config); - if let Some(folder) = folder { - sync_builder = sync_builder.only_folder(folder); + + if let Some(strategy) = folders_strategy { + sync_builder = sync_builder.folders_strategy(strategy); } if dry_run { @@ -221,7 +226,7 @@ pub fn sync( #[cfg(test)] mod tests { - use himalaya_lib::{AccountConfig, ImapConfig}; + use pimalaya_email::{AccountConfig, ImapConfig}; use std::{collections::HashMap, fmt::Debug, io}; use termcolor::ColorSpec; diff --git a/src/domain/email/args.rs b/src/domain/email/args.rs index 8a3d5f8..ab4f207 100644 --- a/src/domain/email/args.rs +++ b/src/domain/email/args.rs @@ -298,9 +298,9 @@ pub fn parse_criteria_arg(matches: &ArgMatches) -> String { /// Represents the email reply all argument. pub fn reply_all_flag() -> Arg { Arg::new(ARG_REPLY_ALL) - .help("Includes all recipients") + .help("Include all recipients") .long("all") - .short('a') + .short('A') .action(ArgAction::SetTrue) } diff --git a/src/domain/email/handlers.rs b/src/domain/email/handlers.rs index 3261a02..6ddca41 100644 --- a/src/domain/email/handlers.rs +++ b/src/domain/email/handlers.rs @@ -1,9 +1,9 @@ use anyhow::{anyhow, Context, Result}; use atty::Stream; -use himalaya_lib::{ +use log::{debug, trace}; +use pimalaya_email::{ AccountConfig, Backend, Email, Flag, Flags, Sender, ShowTextPartsStrategy, Tpl, TplBuilder, }; -use log::{debug, trace}; use std::{ fs, io::{self, BufRead}, @@ -14,6 +14,7 @@ use uuid::Uuid; use crate::{ printer::{PrintTableOpts, Printer}, ui::editor, + Envelopes, }; pub fn attachments( @@ -132,10 +133,10 @@ pub fn list( let folder = config.folder_alias(folder)?; let page_size = page_size.unwrap_or(config.email_listing_page_size()); debug!("page size: {}", page_size); - let msgs = backend.list_envelopes(&folder, page_size, page)?; - trace!("envelopes: {:?}", msgs); + let envelopes: Envelopes = backend.list_envelopes(&folder, page_size, page)?.into(); + trace!("envelopes: {:?}", envelopes); printer.print_table( - Box::new(msgs), + Box::new(envelopes), PrintTableOpts { format: &config.email_reading_format, max_width, @@ -291,7 +292,9 @@ pub fn search( ) -> Result<()> { let folder = config.folder_alias(folder)?; let page_size = page_size.unwrap_or(config.email_listing_page_size()); - let envelopes = backend.search_envelopes(&folder, &query, "", page_size, page)?; + let envelopes: Envelopes = backend + .search_envelopes(&folder, &query, "", page_size, page)? + .into(); let opts = PrintTableOpts { format: &config.email_reading_format, max_width, @@ -313,7 +316,9 @@ pub fn sort( ) -> Result<()> { let folder = config.folder_alias(folder)?; let page_size = page_size.unwrap_or(config.email_listing_page_size()); - let envelopes = backend.search_envelopes(&folder, &query, &sort, page_size, page)?; + let envelopes: Envelopes = backend + .search_envelopes(&folder, &query, &sort, page_size, page)? + .into(); let opts = PrintTableOpts { format: &config.email_reading_format, max_width, @@ -329,7 +334,7 @@ pub fn send( sender: &mut S, raw_email: String, ) -> Result<()> { - let folder = config.folder_alias("sent")?; + let folder = config.sent_folder_alias()?; let is_tty = atty::is(Stream::Stdin); let is_json = printer.is_json(); let raw_email = if is_tty || is_json { @@ -344,7 +349,11 @@ pub fn send( }; trace!("raw email: {:?}", raw_email); sender.send(raw_email.as_bytes())?; - backend.add_email(&folder, raw_email.as_bytes(), &Flags::default())?; + backend.add_email( + &folder, + raw_email.as_bytes(), + &Flags::from_iter([Flag::Seen]), + )?; Ok(()) } diff --git a/src/domain/envelope/envelope.rs b/src/domain/envelope/envelope.rs index a62254f..d58c499 100644 --- a/src/domain/envelope/envelope.rs +++ b/src/domain/envelope/envelope.rs @@ -1,6 +1,45 @@ -use himalaya_lib::{Envelope, Flag}; +use chrono::{DateTime, Local}; +use serde::{Serialize, Serializer}; -use crate::ui::{Cell, Row, Table}; +use crate::{ + ui::{Cell, Row, Table}, + Flag, Flags, +}; + +fn date(date: &DateTime, s: S) -> Result { + s.serialize_str(&date.to_rfc3339()) +} + +#[derive(Clone, Debug, Default, Serialize)] +pub struct Mailbox { + pub name: Option, + pub addr: String, +} + +#[derive(Clone, Debug, Default, Serialize)] +pub struct Envelope { + pub id: String, + pub flags: Flags, + pub subject: String, + pub from: Mailbox, + #[serde(serialize_with = "date")] + pub date: DateTime, +} + +impl From<&pimalaya_email::Envelope> for Envelope { + fn from(envelope: &pimalaya_email::Envelope) -> Self { + Envelope { + id: envelope.id.clone(), + flags: envelope.flags.clone().into(), + subject: envelope.subject.clone(), + from: Mailbox { + name: envelope.from.name.clone(), + addr: envelope.from.addr.clone(), + }, + date: envelope.date.clone(), + } + } +} impl Table for Envelope { fn head() -> Row { @@ -14,15 +53,29 @@ impl Table for Envelope { fn row(&self) -> Row { let id = self.id.to_string(); - let flags = self.flags.to_symbols_string(); let unseen = !self.flags.contains(&Flag::Seen); + let flags = { + let mut flags = String::new(); + flags.push_str(if !unseen { " " } else { "✷" }); + flags.push_str(if self.flags.contains(&Flag::Answered) { + "↵" + } else { + " " + }); + flags.push_str(if self.flags.contains(&Flag::Flagged) { + "⚑" + } else { + " " + }); + flags + }; let subject = &self.subject; let sender = if let Some(name) = &self.from.name { name } else { &self.from.addr }; - let date = self.date.format("%d/%m/%Y %H:%M").to_string(); + let date = self.date.to_rfc3339(); Row::new() .cell(Cell::new(id).bold_if(unseen).red()) diff --git a/src/domain/envelope/envelopes.rs b/src/domain/envelope/envelopes.rs index 1bbaa31..3efe2d3 100644 --- a/src/domain/envelope/envelopes.rs +++ b/src/domain/envelope/envelopes.rs @@ -1,11 +1,32 @@ +use std::ops; + use anyhow::Result; -use himalaya_lib::Envelopes; +use serde::Serialize; use crate::{ printer::{PrintTable, PrintTableOpts, WriteColor}, ui::Table, + Envelope, }; +/// Represents the list of envelopes. +#[derive(Clone, Debug, Default, Serialize)] +pub struct Envelopes(Vec); + +impl ops::Deref for Envelopes { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for Envelopes { + fn from(envelopes: pimalaya_email::Envelopes) -> Self { + Envelopes(envelopes.iter().map(Envelope::from).collect()) + } +} + impl PrintTable for Envelopes { fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { writeln!(writer)?; diff --git a/src/domain/flag/args.rs b/src/domain/flag/args.rs index 8c476f8..32fe191 100644 --- a/src/domain/flag/args.rs +++ b/src/domain/flag/args.rs @@ -5,8 +5,8 @@ use anyhow::Result; use clap::{Arg, ArgMatches, Command}; -use himalaya_lib::{Flag, Flags}; use log::{debug, info}; +use pimalaya_email::{Flag, Flags}; use crate::email; @@ -83,7 +83,11 @@ pub fn flags_arg() -> Arg { Arg::new(ARG_FLAGS) .value_name("FLAGS") .help("The flags") - .long_help("The list of flags. It can be one of: seen, answered, flagged, deleted, draft, recent. Other flags are considered custom.") + .long_help( + "The list of flags. +It can be one of: seen, answered, flagged, deleted, or draft. +Other flags are considered custom.", + ) .num_args(1..) .required(true) .last(true) diff --git a/src/domain/flag/flag.rs b/src/domain/flag/flag.rs new file mode 100644 index 0000000..daea47c --- /dev/null +++ b/src/domain/flag/flag.rs @@ -0,0 +1,25 @@ +use serde::Serialize; + +/// Represents the flag variants. +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)] +pub enum Flag { + Seen, + Answered, + Flagged, + Deleted, + Draft, + Custom(String), +} + +impl From<&pimalaya_email::Flag> for Flag { + fn from(flag: &pimalaya_email::Flag) -> Self { + match flag { + pimalaya_email::Flag::Seen => Flag::Seen, + pimalaya_email::Flag::Answered => Flag::Answered, + pimalaya_email::Flag::Flagged => Flag::Flagged, + pimalaya_email::Flag::Deleted => Flag::Deleted, + pimalaya_email::Flag::Draft => Flag::Draft, + pimalaya_email::Flag::Custom(flag) => Flag::Custom(flag.clone()), + } + } +} diff --git a/src/domain/flag/flags.rs b/src/domain/flag/flags.rs new file mode 100644 index 0000000..534cb97 --- /dev/null +++ b/src/domain/flag/flags.rs @@ -0,0 +1,21 @@ +use serde::Serialize; +use std::{collections::HashSet, ops}; + +use crate::Flag; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)] +pub struct Flags(pub HashSet); + +impl ops::Deref for Flags { + type Target = HashSet; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for Flags { + fn from(flags: pimalaya_email::Flags) -> Self { + Flags(flags.iter().map(Flag::from).collect()) + } +} diff --git a/src/domain/flag/handlers.rs b/src/domain/flag/handlers.rs index 1f3eb66..0e184f7 100644 --- a/src/domain/flag/handlers.rs +++ b/src/domain/flag/handlers.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use himalaya_lib::{Backend, Flags}; +use pimalaya_email::{Backend, Flags}; use crate::printer::Printer; diff --git a/src/domain/flag/mod.rs b/src/domain/flag/mod.rs index b0b957b..5d92934 100644 --- a/src/domain/flag/mod.rs +++ b/src/domain/flag/mod.rs @@ -1,2 +1,8 @@ pub mod args; pub mod handlers; + +pub mod flag; +pub use flag::*; + +pub mod flags; +pub use flags::*; diff --git a/src/domain/folder/args.rs b/src/domain/folder/args.rs index 4ddf949..f83b28f 100644 --- a/src/domain/folder/args.rs +++ b/src/domain/folder/args.rs @@ -3,14 +3,21 @@ //! This module provides subcommands, arguments and a command matcher //! related to the folder domain. +use std::collections::HashSet; + use anyhow::Result; -use clap::{self, Arg, ArgMatches, Command}; +use clap::{self, Arg, ArgAction, ArgMatches, Command}; use log::{debug, info}; use crate::ui::table; +const ARG_ALL: &str = "all"; +const ARG_EXCLUDE: &str = "exclude"; +const ARG_INCLUDE: &str = "include"; const ARG_SOURCE: &str = "source"; const ARG_TARGET: &str = "target"; +const CMD_CREATE: &str = "create"; +const CMD_DELETE: &str = "delete"; const CMD_EXPUNGE: &str = "expunge"; const CMD_FOLDERS: &str = "folders"; const CMD_LIST: &str = "list"; @@ -18,8 +25,10 @@ const CMD_LIST: &str = "list"; /// Represents the folder commands. #[derive(Debug, PartialEq, Eq)] pub enum Cmd { + Create, List(table::args::MaxTableWidth), Expunge, + Delete, } /// Represents the folder command matcher. @@ -28,10 +37,16 @@ pub fn matches(m: &ArgMatches) -> Result> { if let Some(_) = m.subcommand_matches(CMD_EXPUNGE) { info!("expunge folder subcommand matched"); Some(Cmd::Expunge) + } else if let Some(_) = m.subcommand_matches(CMD_CREATE) { + debug!("create folder command matched"); + Some(Cmd::Create) } else if let Some(m) = m.subcommand_matches(CMD_LIST) { debug!("list folders command matched"); let max_table_width = table::args::parse_max_width(m); Some(Cmd::List(max_table_width)) + } else if let Some(_) = m.subcommand_matches(CMD_DELETE) { + debug!("delete folder command matched"); + Some(Cmd::Delete) } else { info!("no folder subcommand matched, falling back to subcommand list"); Some(Cmd::List(None)) @@ -49,18 +64,25 @@ pub fn subcmd() -> Command { .about("Manage folders") .subcommands([ Command::new(CMD_EXPUNGE).about("Delete emails marked for deletion"), + Command::new(CMD_CREATE) + .aliases(["add", "new"]) + .about("Create a new folder"), Command::new(CMD_LIST) .about("List folders") .arg(table::args::max_width()), + Command::new(CMD_DELETE) + .aliases(["remove", "rm"]) + .about("Delete a folder with all its emails"), ]) } /// Represents the source folder argument. pub fn source_arg() -> Arg { Arg::new(ARG_SOURCE) + .help("Set the source folder") .long("folder") .short('f') - .help("Specifies the source folder") + .global(true) .value_name("SOURCE") } @@ -69,6 +91,70 @@ pub fn parse_source_arg(matches: &ArgMatches) -> Option<&str> { matches.get_one::(ARG_SOURCE).map(String::as_str) } +/// Represents the all folders argument. +pub fn all_arg(help: &'static str) -> Arg { + Arg::new(ARG_ALL) + .help(help) + .long("all-folders") + .alias("all") + .short('A') + .action(ArgAction::SetTrue) + .conflicts_with(ARG_SOURCE) + .conflicts_with(ARG_INCLUDE) + .conflicts_with(ARG_EXCLUDE) +} + +/// Represents the all folders argument parser. +pub fn parse_all_arg(m: &ArgMatches) -> bool { + m.get_flag(ARG_ALL) +} + +/// Represents the folders to include argument. +pub fn include_arg(help: &'static str) -> Arg { + Arg::new(ARG_INCLUDE) + .help(help) + .long("include-folder") + .alias("only") + .short('F') + .value_name("FOLDER") + .num_args(1..) + .action(ArgAction::Append) + .conflicts_with(ARG_SOURCE) + .conflicts_with(ARG_ALL) + .conflicts_with(ARG_EXCLUDE) +} + +/// Represents the folders to include argument parser. +pub fn parse_include_arg(m: &ArgMatches) -> HashSet { + m.get_many::(ARG_INCLUDE) + .unwrap_or_default() + .map(ToOwned::to_owned) + .collect() +} + +/// Represents the folders to exclude argument. +pub fn exclude_arg(help: &'static str) -> Arg { + Arg::new(ARG_EXCLUDE) + .help(help) + .long("exclude-folder") + .alias("except") + .short('x') + .value_name("FOLDER") + .num_args(1..) + .action(ArgAction::Append) + .conflicts_with(ARG_SOURCE) + .conflicts_with(ARG_ALL) + .conflicts_with(ARG_INCLUDE) +} + +/// Represents the folders to exclude argument parser. +pub fn parse_exclude_arg(m: &ArgMatches) -> HashSet { + m.get_many::(ARG_EXCLUDE) + .unwrap_or_default() + .map(ToOwned::to_owned) + .collect() +} + /// Represents the target folder argument. pub fn target_arg() -> Arg { Arg::new(ARG_TARGET) diff --git a/src/domain/folder/folder.rs b/src/domain/folder/folder.rs index ba0d931..0aebace 100644 --- a/src/domain/folder/folder.rs +++ b/src/domain/folder/folder.rs @@ -1,7 +1,24 @@ -use himalaya_lib::folder::Folder; +use serde::Serialize; use crate::ui::{Cell, Row, Table}; +#[derive(Clone, Debug, Default, Serialize)] +pub struct Folder { + pub delim: String, + pub name: String, + pub desc: String, +} + +impl From<&pimalaya_email::Folder> for Folder { + fn from(folder: &pimalaya_email::Folder) -> Self { + Folder { + delim: folder.delim.clone(), + name: folder.name.clone(), + desc: folder.desc.clone(), + } + } +} + impl Table for Folder { fn head() -> Row { Row::new() diff --git a/src/domain/folder/folders.rs b/src/domain/folder/folders.rs index db34dad..d3335ae 100644 --- a/src/domain/folder/folders.rs +++ b/src/domain/folder/folders.rs @@ -1,11 +1,31 @@ +use std::ops; + use anyhow::Result; -use himalaya_lib::folder::Folders; +use serde::Serialize; use crate::{ printer::{PrintTable, PrintTableOpts, WriteColor}, ui::Table, + Folder, }; +#[derive(Clone, Debug, Default, Serialize)] +pub struct Folders(Vec); + +impl ops::Deref for Folders { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for Folders { + fn from(folders: pimalaya_email::Folders) -> Self { + Folders(folders.iter().map(Folder::from).collect()) + } +} + impl PrintTable for Folders { fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { writeln!(writer)?; diff --git a/src/domain/folder/handlers.rs b/src/domain/folder/handlers.rs index 85c1516..2e2a384 100644 --- a/src/domain/folder/handlers.rs +++ b/src/domain/folder/handlers.rs @@ -3,26 +3,31 @@ //! This module gathers all folder actions triggered by the CLI. use anyhow::Result; -use himalaya_lib::{AccountConfig, Backend}; +use dialoguer::Confirm; +use pimalaya_email::{AccountConfig, Backend}; +use std::process; -use crate::printer::{PrintTableOpts, Printer}; +use crate::{ + printer::{PrintTableOpts, Printer}, + Folders, +}; pub fn expunge( - folder: &str, printer: &mut P, backend: &mut B, + folder: &str, ) -> Result<()> { backend.expunge_folder(folder)?; printer.print(format!("Folder {folder} successfully expunged!")) } pub fn list( - max_width: Option, config: &AccountConfig, printer: &mut P, backend: &mut B, + max_width: Option, ) -> Result<()> { - let folders = backend.list_folders()?; + let folders: Folders = backend.list_folders()?.into(); printer.print_table( // TODO: remove Box Box::new(folders), @@ -33,9 +38,36 @@ pub fn list( ) } +pub fn create( + printer: &mut P, + backend: &mut B, + folder: &str, +) -> Result<()> { + backend.add_folder(folder)?; + printer.print("Folder successfully created!") +} + +pub fn delete( + printer: &mut P, + backend: &mut B, + folder: &str, +) -> Result<()> { + if let Some(false) | None = Confirm::new() + .with_prompt(format!("Confirm deletion of folder {folder}?")) + .default(false) + .report(false) + .interact_opt()? + { + process::exit(0); + }; + + backend.delete_folder(folder)?; + printer.print("Folder successfully deleted!") +} + #[cfg(test)] mod tests { - use himalaya_lib::{ + use pimalaya_email::{ backend, AccountConfig, Backend, Emails, Envelope, Envelopes, Flags, Folder, Folders, }; use std::{any::Any, fmt::Debug, io}; diff --git a/src/domain/imap/handlers.rs b/src/domain/imap/handlers.rs index 3302700..d664d30 100644 --- a/src/domain/imap/handlers.rs +++ b/src/domain/imap/handlers.rs @@ -3,7 +3,7 @@ //! This module gathers all IMAP handlers triggered by the CLI. use anyhow::{Context, Result}; -use himalaya_lib::ImapBackend; +use pimalaya_email::ImapBackend; pub fn notify(imap: &ImapBackend, folder: &str, keepalive: u64) -> Result<()> { imap.notify(keepalive, folder).context("cannot imap notify") diff --git a/src/domain/tpl/handlers.rs b/src/domain/tpl/handlers.rs index 51920e2..9f09b3b 100644 --- a/src/domain/tpl/handlers.rs +++ b/src/domain/tpl/handlers.rs @@ -1,6 +1,6 @@ use anyhow::{anyhow, Result}; use atty::Stream; -use himalaya_lib::{AccountConfig, Backend, CompilerBuilder, Email, Flags, Sender, Tpl}; +use pimalaya_email::{AccountConfig, Backend, CompilerBuilder, Email, Flags, Sender, Tpl}; use std::io::{stdin, BufRead}; use crate::printer::Printer; diff --git a/src/main.rs b/src/main.rs index 797af9b..37669fe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{anyhow, Context, Result}; use clap::Command; use std::{borrow::Cow, env}; use url::Url; @@ -10,7 +10,7 @@ use himalaya::{ printer::StdoutPrinter, tpl, }; -use himalaya_lib::{ +use pimalaya_email::{ BackendBuilder, BackendConfig, ImapBackend, SenderBuilder, DEFAULT_INBOX_FOLDER, }; @@ -77,8 +77,7 @@ fn main() -> Result<()> { _ => (), } - // checks completion command before configs - // https://github.com/soywod/himalaya/issues/115 + // also checks man command before configs match man::args::matches(&m)? { Some(man::args::Cmd::GenerateAll(dir)) => { return man::handlers::generate(dir, create_app()); @@ -123,11 +122,7 @@ fn main() -> Result<()> { Some(account::args::Cmd::List(max_width)) => { return account::handlers::list(max_width, &account_config, &config, &mut printer); } - Some(account::args::Cmd::Sync(dry_run)) => { - let folder = match folder { - Some(folder) => Some(account_config.folder_alias(folder)?), - None => None, - }; + Some(account::args::Cmd::Sync(folders_strategy, dry_run)) => { let backend = BackendBuilder::new() .sessions_pool_size(8) .disable_cache(true) @@ -136,7 +131,7 @@ fn main() -> Result<()> { &account_config, &mut printer, backend.as_ref(), - &folder, + folders_strategy, dry_run, )?; backend.close()?; @@ -147,24 +142,44 @@ fn main() -> Result<()> { // checks folder commands match folder::args::matches(&m)? { - Some(folder::args::Cmd::Expunge) => { - let folder = account_config.folder_alias(folder.unwrap_or(DEFAULT_INBOX_FOLDER))?; + Some(folder::args::Cmd::Create) => { + let folder = folder + .ok_or_else(|| anyhow!("the folder argument is missing")) + .context("cannot create folder")?; + let folder = account_config.folder_alias(folder)?; let mut backend = BackendBuilder::new() .disable_cache(disable_cache) .build(&account_config, &backend_config)?; - return folder::handlers::expunge(&folder, &mut printer, backend.as_mut()); + return folder::handlers::create(&mut printer, backend.as_mut(), &folder); } Some(folder::args::Cmd::List(max_width)) => { let mut backend = BackendBuilder::new() .disable_cache(disable_cache) .build(&account_config, &backend_config)?; return folder::handlers::list( - max_width, &account_config, &mut printer, backend.as_mut(), + max_width, ); } + Some(folder::args::Cmd::Expunge) => { + let folder = account_config.folder_alias(folder.unwrap_or(DEFAULT_INBOX_FOLDER))?; + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; + return folder::handlers::expunge(&mut printer, backend.as_mut(), &folder); + } + Some(folder::args::Cmd::Delete) => { + let folder = folder + .ok_or_else(|| anyhow!("the folder argument is missing")) + .context("cannot delete folder")?; + let folder = account_config.folder_alias(folder)?; + let mut backend = BackendBuilder::new() + .disable_cache(disable_cache) + .build(&account_config, &backend_config)?; + return folder::handlers::delete(&mut printer, backend.as_mut(), &folder); + } _ => (), } diff --git a/src/man/args.rs b/src/man/args.rs index 6361c47..a85f953 100644 --- a/src/man/args.rs +++ b/src/man/args.rs @@ -30,11 +30,14 @@ pub fn matches(m: &ArgMatches) -> Result> { /// Man subcommands. pub fn subcmd() -> Command { Command::new(CMD_MAN) - .about("Generates all man pages to the specified directory.") + .about("Generate all man pages to the given directory") .arg( Arg::new(ARG_DIR) - .help("Directory where to generate man files") - .long_help("Represents the directory where all man files of all commands and subcommands should be generated in.") + .help("Directory to generate man files in") + .long_help( + "Represents the directory where all man files of +all commands and subcommands should be generated in.", + ) .required(true), ) } diff --git a/src/output/args.rs b/src/output/args.rs index aa5776a..d5a98a1 100644 --- a/src/output/args.rs +++ b/src/output/args.rs @@ -11,22 +11,23 @@ pub(crate) const ARG_OUTPUT: &str = "output"; pub fn args() -> Vec { vec![ Arg::new(ARG_OUTPUT) - .help("Defines the output format") + .help("Set the output format") .long("output") .short('o') + .global(true) .value_name("FMT") .value_parser(["plain", "json"]) .default_value("plain"), Arg::new(ARG_COLOR) - .help("Controls when to use colors.") + .help("Control when to use colors.") .long_help( - " -This flag controls when to use colors. The default setting is 'auto', which -means himalaya will try to guess when to use colors. For example, if himalaya is -printing to a terminal, then it will use colors, but if it is redirected to a -file or a pipe, then it will suppress color output. himalaya will suppress color -output in some other circumstances as well. For example, if the TERM -environment variable is not set or set to 'dumb', then himalaya will not use + "This flag controls when to use colors. The default +setting is 'auto', which means himalaya will try to guess when to use +colors. For example, if himalaya is printing to a terminal, then it +will use colors, but if it is redirected to a file or a pipe, then it +will suppress color output. himalaya will suppress color output in +some other circumstances as well. For example, if the TERM environment +variable is not set or set to 'dumb', then himalaya will not use colors. The possible values for this flag are: @@ -34,11 +35,11 @@ The possible values for this flag are: never Colors will never be used. auto The default. himalaya tries to be smart. always Colors will always be used regardless of where output is sent. -ansi Like 'always', but emits ANSI escapes (even in a Windows console). -", +ansi Like 'always', but emits ANSI escapes (even in a Windows console).", ) .long("color") .short('C') + .global(true) .value_parser(["never", "auto", "always", "ansi"]) .default_value("auto") .value_name("WHEN"), diff --git a/src/printer/print.rs b/src/printer/print.rs index 7f03a5f..680fb11 100644 --- a/src/printer/print.rs +++ b/src/printer/print.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use himalaya_lib::Tpl; +use pimalaya_email::Tpl; use crate::printer::WriteColor; diff --git a/src/printer/print_table.rs b/src/printer/print_table.rs index bf6d75f..a2f847a 100644 --- a/src/printer/print_table.rs +++ b/src/printer/print_table.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use himalaya_lib::EmailTextPlainFormat; +use pimalaya_email::EmailTextPlainFormat; use std::io; use termcolor::{self, StandardStream}; diff --git a/src/ui/editor.rs b/src/ui/editor.rs index e8da1eb..87ca7b1 100644 --- a/src/ui/editor.rs +++ b/src/ui/editor.rs @@ -1,9 +1,9 @@ use anyhow::{Context, Result}; -use himalaya_lib::{ +use log::debug; +use pimalaya_email::{ email::{local_draft_path, remove_local_draft}, AccountConfig, Backend, CompilerBuilder, Flag, Flags, Sender, Tpl, }; -use log::debug; use std::{env, fs, process::Command}; use crate::{ @@ -81,7 +81,7 @@ pub fn edit_tpl_with_editor sender.send(&email)?; let sent_folder = config.sent_folder_alias()?; printer.print_log(format!("Adding email to the {} folder…", sent_folder))?; - backend.add_email(&sent_folder, &email, &Flags::default())?; + backend.add_email(&sent_folder, &email, &Flags::from_iter([Flag::Seen]))?; remove_local_draft()?; printer.print("Done!")?; break; @@ -101,7 +101,11 @@ pub fn edit_tpl_with_editor .some_pgp_sign_cmd(config.email_writing_sign_cmd.as_ref()) .some_pgp_encrypt_cmd(config.email_writing_encrypt_cmd.as_ref()), )?; - backend.add_email(&draft_folder, &email, &Flags::from_iter([Flag::Draft]))?; + backend.add_email( + &draft_folder, + &email, + &Flags::from_iter([Flag::Seen, Flag::Draft]), + )?; remove_local_draft()?; printer.print(format!("Email successfully saved to {}", draft_folder))?; break; diff --git a/src/ui/table/table.rs b/src/ui/table/table.rs index de2dbde..4c43540 100644 --- a/src/ui/table/table.rs +++ b/src/ui/table/table.rs @@ -5,8 +5,8 @@ //! [builder design pattern]: https://refactoring.guru/design-patterns/builder use anyhow::{Context, Result}; -use himalaya_lib::EmailTextPlainFormat; use log::trace; +use pimalaya_email::EmailTextPlainFormat; use termcolor::{Color, ColorSpec}; use terminal_size; use unicode_width::UnicodeWidthStr;