Merge pull request #447 from soywod/develop

Release v0.7.2
This commit is contained in:
Clément DOUIN 2023-05-01 00:25:23 +02:00 committed by GitHub
commit f66679318d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 1405 additions and 652 deletions

View file

@ -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 }}

View file

@ -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

150
.github/workflows/release.yml vendored Normal file
View file

@ -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}

View file

@ -3,36 +3,22 @@ name: tests
on: on:
pull_request: pull_request:
push: push:
branches:
- master
jobs: jobs:
tests: tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Install libnotmuch
run: sudo apt-get install -y libnotmuch-dev
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Start GreenMail testing server - name: Install Nix
run: | uses: cachix/install-nix-action@v20
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
with: with:
toolchain: stable nix_path: nixpkgs=channel:nixos-unstable
- name: Run tests extra_nix_config: |
uses: actions-rs/cargo@v1 experimental-features = nix-command flakes
- uses: cachix/cachix-action@v12
with: with:
command: test name: soywod
args: --all-features authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Test
run: nix run .#test

View file

@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [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 ## [0.7.1] - 2023-02-14
### Added ### Added
@ -17,7 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Changed the location of the - Changed the location of the
[documentation](https://pimalaya.org/himalaya/docs/). [documentation](https://pimalaya.org/himalaya/).
### Fixed ### Fixed
@ -498,7 +548,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Password from command [#22] - Password from command [#22]
- Set up README [#20] - Set up README [#20]
[Unreleased]: https://github.com/soywod/himalaya/compare/v0.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.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.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 [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 [#356]: https://github.com/soywod/himalaya/issues/356
[#419]: https://github.com/soywod/himalaya/issues/419 [#419]: https://github.com/soywod/himalaya/issues/419
[#430]: https://github.com/soywod/himalaya/issues/430 [#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

374
Cargo.lock generated
View file

@ -94,12 +94,24 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813"
[[package]] [[package]]
name = "bufstream" name = "bufstream"
version = "0.1.4" version = "0.1.4"
@ -136,7 +148,7 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18e9079d1a12a2cc2bffb5db039c43661836ead4082120d5844f02555aca2d46" checksum = "18e9079d1a12a2cc2bffb5db039c43661836ead4082120d5844f02555aca2d46"
dependencies = [ dependencies = [
"base64", "base64 0.13.1",
"encoding_rs", "encoding_rs",
] ]
@ -180,7 +192,7 @@ version = "4.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76" checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"clap_lex", "clap_lex",
"is-terminal", "is-terminal",
"strsim 0.10.0", "strsim 0.10.0",
@ -221,7 +233,7 @@ version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
] ]
[[package]] [[package]]
@ -500,11 +512,11 @@ checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
[[package]] [[package]]
name = "email-encoding" name = "email-encoding"
version = "0.1.3" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34dd14c63662e0206599796cd5e1ad0268ab2b9d19b868d6050d688eba2bbf98" checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75"
dependencies = [ dependencies = [
"base64", "base64 0.21.0",
"memchr 2.5.0", "memchr 2.5.0",
] ]
@ -765,10 +777,11 @@ dependencies = [
[[package]] [[package]]
name = "himalaya" name = "himalaya"
version = "0.7.1" version = "0.7.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"atty", "atty",
"chrono",
"clap", "clap",
"clap_complete", "clap_complete",
"clap_mangen", "clap_mangen",
@ -778,10 +791,10 @@ dependencies = [
"email_address", "email_address",
"env_logger", "env_logger",
"erased-serde", "erased-serde",
"himalaya-lib",
"indicatif", "indicatif",
"log", "log",
"once_cell", "once_cell",
"pimalaya-email",
"serde", "serde",
"serde_json", "serde_json",
"shellexpand", "shellexpand",
@ -794,42 +807,6 @@ dependencies = [
"uuid", "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]] [[package]]
name = "hostname" name = "hostname"
version = "0.3.1" version = "0.3.1"
@ -906,17 +883,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 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]] [[package]]
name = "idna" name = "idna"
version = "0.3.0" version = "0.3.0"
@ -933,7 +899,7 @@ version = "3.0.0-alpha.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c2ff52273d9cd791687b4510d8a0047277e985a348e411c94fe84e193e7a76" checksum = "b1c2ff52273d9cd791687b4510d8a0047277e985a348e411c94fe84e193e7a76"
dependencies = [ dependencies = [
"base64", "base64 0.13.1",
"bufstream", "bufstream",
"chrono", "chrono",
"imap-proto", "imap-proto",
@ -942,6 +908,7 @@ dependencies = [
"nom 7.1.1", "nom 7.1.1",
"ouroboros", "ouroboros",
"regex", "regex",
"rustls-connector",
] ]
[[package]] [[package]]
@ -1029,25 +996,29 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "lettre" name = "lettre"
version = "0.10.1" version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eabca5e0b4d0e98e7f2243fb5b7520b6af2b65d8f87bcc86f2c75185a6ff243" checksum = "dd84a055407850bcf4791baa77cb4818d37cbb79ad4e60b9b659727b920d2c65"
dependencies = [ dependencies = [
"base64", "base64 0.21.0",
"email-encoding", "email-encoding",
"email_address", "email_address",
"fastrand", "fastrand",
"futures-util", "futures-util",
"hostname", "hostname",
"httpdate", "httpdate",
"idna 0.2.3", "idna",
"mime", "mime",
"native-tls", "native-tls",
"nom 7.1.1", "nom 7.1.1",
"once_cell", "once_cell",
"quoted_printable", "quoted_printable",
"rustls",
"rustls-pemfile",
"serde", "serde",
"socket2", "socket2",
"tokio",
"webpki-roots",
] ]
[[package]] [[package]]
@ -1058,9 +1029,9 @@ checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
[[package]] [[package]]
name = "libsqlite3-sys" name = "libsqlite3-sys"
version = "0.25.2" version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa" checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326"
dependencies = [ dependencies = [
"cc", "cc",
"pkg-config", "pkg-config",
@ -1116,6 +1087,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" 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]] [[package]]
name = "maildir" name = "maildir"
version = "0.6.3" version = "0.6.3"
@ -1164,12 +1144,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]]
name = "matches"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]] [[package]]
name = "md5" name = "md5"
version = "0.7.0" version = "0.7.0"
@ -1238,6 +1212,18 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 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]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.11" version = "0.2.11"
@ -1281,6 +1267,15 @@ dependencies = [
"minimal-lexical", "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]] [[package]]
name = "notmuch" name = "notmuch"
version = "0.8.0" version = "0.8.0"
@ -1338,7 +1333,7 @@ version = "0.10.43"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "020433887e44c27ff16365eaa2d380547a94544ad509aff6eb5b6e3e0b27b376" checksum = "020433887e44c27ff16365eaa2d380547a94544ad509aff6eb5b6e3e0b27b376"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"cfg-if 1.0.0", "cfg-if 1.0.0",
"foreign-types", "foreign-types",
"libc", "libc",
@ -1364,6 +1359,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 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]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.78" version = "0.9.78"
@ -1373,6 +1377,7 @@ dependencies = [
"autocfg", "autocfg",
"cc", "cc",
"libc", "libc",
"openssl-src",
"pkg-config", "pkg-config",
"vcpkg", "vcpkg",
] ]
@ -1507,6 +1512,46 @@ dependencies = [
"siphasher", "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]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.9" version = "0.2.9"
@ -1702,7 +1747,7 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
] ]
[[package]] [[package]]
@ -1748,7 +1793,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11347d014ae34e1d367aaf9191c37bf071b161e2e1c09c8559c7717e87030e11" checksum = "11347d014ae34e1d367aaf9191c37bf071b161e2e1c09c8559c7717e87030e11"
dependencies = [ dependencies = [
"base64", "base64 0.13.1",
"charset", "charset",
"chumsky 0.8.0", "chumsky 0.8.0",
"memchr 2.5.0", "memchr 2.5.0",
@ -1756,6 +1801,21 @@ dependencies = [
"thiserror", "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]] [[package]]
name = "roff" name = "roff"
version = "0.2.1" version = "0.2.1"
@ -1764,11 +1824,11 @@ checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316"
[[package]] [[package]]
name = "rusqlite" name = "rusqlite"
version = "0.28.0" version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a" checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
dependencies = [ dependencies = [
"bitflags", "bitflags 2.2.1",
"fallible-iterator", "fallible-iterator",
"fallible-streaming-iterator", "fallible-streaming-iterator",
"hashlink", "hashlink",
@ -1782,7 +1842,7 @@ version = "0.36.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588" checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"errno", "errno",
"io-lifetimes", "io-lifetimes",
"libc", "libc",
@ -1790,6 +1850,51 @@ dependencies = [
"windows-sys 0.42.0", "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]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.11" version = "1.0.11"
@ -1818,13 +1923,23 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" 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]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.7.0" version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"core-foundation", "core-foundation",
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@ -1843,18 +1958,18 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.148" version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc" checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.148" version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c" checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1872,6 +1987,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_spanned"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "shellexpand" name = "shellexpand"
version = "2.1.2" version = "2.1.2"
@ -1912,6 +2036,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]] [[package]]
name = "stacker" name = "stacker"
version = "0.1.15" version = "0.1.15"
@ -2074,12 +2204,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "toml" name = "tokio"
version = "0.5.9" version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"serde", "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]] [[package]]
@ -2122,6 +2291,12 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]] [[package]]
name = "url" name = "url"
version = "2.3.1" version = "2.3.1"
@ -2129,7 +2304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna 0.3.0", "idna",
"percent-encoding", "percent-encoding",
] ]
@ -2151,7 +2326,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e326365261fc2761f0809dfb6032810534a0427bbd8f0edf546f6afeef89f5d" checksum = "5e326365261fc2761f0809dfb6032810534a0427bbd8f0edf546f6afeef89f5d"
dependencies = [ dependencies = [
"base64", "base64 0.13.1",
"encoding_rs", "encoding_rs",
"regex", "regex",
] ]
@ -2249,6 +2424,35 @@ version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" 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]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View file

@ -1,25 +1,36 @@
[package] [package]
name = "himalaya" name = "himalaya"
description = "Command-line interface for email management." description = "CLI to manage your emails."
version = "0.7.1" version = "0.7.2"
authors = ["soywod <clement.douin@posteo.net>"] authors = ["soywod <clement.douin@posteo.net>"]
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
categories = ["command-line-interface", "command-line-utilities", "email"] categories = ["command-line-interface", "command-line-utilities", "email"]
keywords = ["cli", "mail", "email", "client", "imap"] keywords = ["cli", "mail", "email", "client", "imap"]
homepage = "https://github.com/soywod/himalaya" homepage = "https://pimalaya.org/himalaya/"
documentation = "https://github.com/soywod/himalaya/wiki" documentation = "https://pimalaya.org/himalaya/"
repository = "https://github.com/soywod/himalaya" repository = "https://github.com/soywod/himalaya"
[package.metadata.deb] [package.metadata.docs.rs]
priority = "optional" all-features = true
section = "mail"
[features] [features]
imap-backend = ["himalaya-lib/imap-backend"] default = ["rustls-tls", "rustls-native-certs", "imap-backend", "smtp-sender"]
smtp-sender = ["himalaya-lib/smtp-sender"]
notmuch-backend = ["himalaya-lib/notmuch-backend"] # rustls
default = ["imap-backend", "smtp-sender"] 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] [dev-dependencies]
tempfile = "3.3" tempfile = "3.3"
@ -27,6 +38,7 @@ tempfile = "3.3"
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
atty = "0.2" atty = "0.2"
chrono = "0.4.23"
clap = "4.0" clap = "4.0"
clap_complete = "4.0" clap_complete = "4.0"
clap_mangen = "0.2" clap_mangen = "0.2"
@ -36,7 +48,7 @@ dialoguer = "0.10.2"
email_address = "0.2.4" email_address = "0.2.4"
env_logger = "0.8" env_logger = "0.8"
erased-serde = "0.3" erased-serde = "0.3"
himalaya-lib = "0.6.0" pimalaya-email = "0.7.1"
indicatif = "0.17" indicatif = "0.17"
log = "0.4" log = "0.4"
once_cell = "1.16.0" once_cell = "1.16.0"
@ -45,7 +57,7 @@ serde_json = "1.0"
shellexpand = "2.1" shellexpand = "2.1"
termcolor = "1.1" termcolor = "1.1"
terminal_size = "0.1" terminal_size = "0.1"
toml = "0.5" toml = "0.7.2"
unicode-width = "0.1" unicode-width = "0.1"
url = "2.2" url = "2.2"
uuid = { version = "0.8", features = ["v4"] } uuid = { version = "0.8", features = ["v4"] }

View file

@ -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 [![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 https://pimalaya.org/himalaya/
[himalaya-lib](https://git.sr.ht/~soywod/himalaya-lib) that allows you
to manipulate your emails using commands in your console. 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) ![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 - JSON output
- … - …
[Folder listing]: https://pimalaya.org/himalaya/docs/cli/usage/folders/list.html [Folder listing]: https://pimalaya.org/himalaya/cli/usage/folders/list.html
[Envelopes listing]: https://pimalaya.org/himalaya/docs/cli/usage/envelopes/list.html [Envelopes listing]: https://pimalaya.org/himalaya/cli/usage/envelopes/list.html
[searching]: https://pimalaya.org/himalaya/docs/cli/usage/envelopes/search.html [searching]: https://pimalaya.org/himalaya/cli/usage/envelopes/search.html
[sorting]: https://pimalaya.org/himalaya/docs/cli/usage/envelopes/sort.html [sorting]: https://pimalaya.org/himalaya/cli/usage/envelopes/sort.html
[Email composition]: https://pimalaya.org/himalaya/docs/cli/usage/emails/write.html [Email composition]: https://pimalaya.org/himalaya/cli/usage/emails/write.html
[copy]: https://pimalaya.org/himalaya/docs/cli/usage/emails/copy.html [copy]: https://pimalaya.org/himalaya/cli/usage/emails/copy.html
[move]: https://pimalaya.org/himalaya/docs/cli/usage/emails/move.html [move]: https://pimalaya.org/himalaya/cli/usage/emails/move.html
[delete]: https://pimalaya.org/himalaya/docs/cli/usage/emails/delete.html [delete]: https://pimalaya.org/himalaya/cli/usage/emails/delete.html
[Multi-accounting]: https://pimalaya.org/himalaya/docs/cli/configuration.html [Multi-accounting]: https://pimalaya.org/himalaya/cli/configuration.html
[Account listing]: https://pimalaya.org/himalaya/docs/cli/usage/accounts/list.html [Account listing]: https://pimalaya.org/himalaya/cli/usage/accounts/list.html
[Account synchronization]: https://pimalaya.org/himalaya/docs/cli/usage/accounts/synchronize.html [Account synchronization]: https://pimalaya.org/himalaya/cli/usage/accounts/synchronize.html
[real-time notifications]: https://pimalaya.org/himalaya/docs/cli/usage/notifications.html [real-time notifications]: https://pimalaya.org/himalaya/cli/usage/notifications.html
[Completions]: https://pimalaya.org/himalaya/docs/cli/tips/completion.html [Completions]: https://pimalaya.org/himalaya/cli/tips/completion.html
## Installation ## Installation
@ -60,6 +61,9 @@ $ yay -S himalaya-git
# Homebrew # Homebrew
$ brew install himalaya $ brew install himalaya
# Scoop
$ scoop install himalaya
# Cargo # Cargo
$ cargo install himalaya $ cargo install himalaya
@ -68,7 +72,7 @@ $ nix-env -i himalaya
``` ```
*See the *See the
[documentation](https://pimalaya.org/himalaya/docs/cli/installation.html) [documentation](https://pimalaya.org/himalaya/cli/installation.html)
for other installation methods.* for other installation methods.*
</td> </td>
@ -78,11 +82,12 @@ for other installation methods.*
## Configuration ## Configuration
Please read the Please read the
[documentation](https://pimalaya.org/himalaya/docs/cli/configuration.html). [documentation](https://pimalaya.org/himalaya/cli/configuration.html).
## Contributing ## 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). [~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht).
If you have a **question**, please send an email at If you have a **question**, please send an email at

52
config.sample.toml Normal file
View file

@ -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 <recipient>"
[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 = """📫 "<sender>" "<subject>""""
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

View file

@ -1,28 +1,36 @@
{ {
"nodes": { "nodes": {
"flake-compat": { "fenix": {
"flake": false, "inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": { "locked": {
"lastModified": 1673956053, "lastModified": 1682835640,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", "narHash": "sha256-rAYEOd4nZFLjDlrF9KNlcopPKNVtr1svSXcEValVRMY=",
"owner": "edolstra", "owner": "nix-community",
"repo": "flake-compat", "repo": "fenix",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", "rev": "006b429d3c493f4c5b1743a94f71ad961c7693ab",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "edolstra", "owner": "nix-community",
"repo": "flake-compat", "repo": "fenix",
"type": "github" "type": "github"
} }
}, },
"flake-utils": { "flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": { "locked": {
"lastModified": 1659877975, "lastModified": 1681202837,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", "rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -31,16 +39,38 @@
"type": "github" "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": { "naersk": {
"inputs": { "inputs": {
"nixpkgs": "nixpkgs" "nixpkgs": [
"nixpkgs"
]
}, },
"locked": { "locked": {
"lastModified": 1671096816, "lastModified": 1679567394,
"narHash": "sha256-ezQCsNgmpUHdZANDCILm3RvtO1xH8uujk/+EqNvzIOg=", "narHash": "sha256-ZvLuzPeARDLiQUt6zSZFGOs+HZmE+3g4QURc8mkBsfM=",
"owner": "nix-community", "owner": "nix-community",
"repo": "naersk", "repo": "naersk",
"rev": "d998160d6a076cfe8f9741e56aeec7e267e3e114", "rev": "88cd22380154a2c36799fe8098888f0f59861a15",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -51,88 +81,58 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1675698036, "lastModified": 1682669017,
"narHash": "sha256-BgsQkQewdlQi8gapJN4phpxkI/FCE/2sORBaFcYbp/A=", "narHash": "sha256-Vi+p4y3wnl0/4gcwTdmCO398kKlDaUrNROtf3GOD2NY=",
"owner": "NixOS", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "1046c7b92e908a1202c0f1ba3fc21d19e1cf1b62", "rev": "7449971a3ecf857b4a554cf79b1d9dcc1a4647d8",
"type": "github" "type": "github"
}, },
"original": { "original": {
"id": "nixpkgs", "owner": "nixos",
"type": "indirect" "ref": "nixos-22.11",
}
},
"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",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
}, },
"root": { "root": {
"inputs": { "inputs": {
"flake-compat": "flake-compat", "fenix": "fenix",
"naersk": "naersk",
"nixpkgs": "nixpkgs_2",
"rust-overlay": "rust-overlay",
"utils": "utils"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_3" "gitignore": "gitignore",
}, "naersk": "naersk",
"nixpkgs": "nixpkgs"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": { "locked": {
"lastModified": 1675823425, "lastModified": 1682792082,
"narHash": "sha256-o/uLXQdq3OrRAv4BZVVY0VmhMmQBLWw6Y4o+p6ZiaR4=", "narHash": "sha256-1nuP2rqipsdB8IJ3N5ws3FQm4dX3mKIueIrCUSu1bWw=",
"owner": "oxalica", "owner": "rust-lang",
"repo": "rust-overlay", "repo": "rust-analyzer",
"rev": "02e1abbdcbc2d516193ff8a7add71f44cd976ba0", "rev": "7bcb4c2ef23e151a639ff918fbb8ab9d521eabb9",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "oxalica", "owner": "rust-lang",
"repo": "rust-overlay", "ref": "nightly",
"repo": "rust-analyzer",
"type": "github" "type": "github"
} }
}, },
"utils": { "systems": {
"locked": { "locked": {
"lastModified": 1667395993, "lastModified": 1681028828,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "numtide", "owner": "nix-systems",
"repo": "flake-utils", "repo": "default",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "numtide", "owner": "nix-systems",
"repo": "flake-utils", "repo": "default",
"type": "github" "type": "github"
} }
} }

171
flake.nix
View file

@ -1,65 +1,140 @@
{ {
description = "Command-line interface for email management."; description = "CLI to manage your emails.";
inputs = { inputs = {
utils.url = "github:numtide/flake-utils"; nixpkgs.url = "github:nixos/nixpkgs/nixos-22.11";
rust-overlay.url = "github:oxalica/rust-overlay"; flake-utils.url = "github:numtide/flake-utils";
naersk.url = "github:nix-community/naersk"; gitignore = {
flake-compat = { url = "github:hercules-ci/gitignore.nix";
url = "github:edolstra/flake-compat"; inputs.nixpkgs.follows = "nixpkgs";
flake = false; };
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, ... }: outputs = { self, nixpkgs, flake-utils, gitignore, fenix, naersk }:
utils.lib.eachDefaultSystem let
(system: 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 let
name = "himalaya"; pkgs = import nixpkgs { system = buildPlatform; };
overlays = [ (import rust-overlay) ]; rust-toolchain = fenix.packages.${buildPlatform}.fromToolchainFile {
pkgs = import nixpkgs { inherit system overlays; }; file = ./rust-toolchain.toml;
sha256 = "eMJethw5ZLrJHmoN2/l0bIyQjoTX1NsvalWSscTixpI=";
};
in in
rec { {
# nix build default = pkgs.mkShell {
defaultPackage = packages.${name}; buildInputs = with pkgs; [
packages = { # Nix env
${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
rnix-lsp rnix-lsp
nixpkgs-fmt nixpkgs-fmt
# Rust env # Rust env
(rust-bin.fromRustupToolchainFile ./rust-toolchain.toml) rust-toolchain
cargo-watch
rust-analyzer
# Notmuch # 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;
});
} }

View file

@ -1,2 +1,11 @@
[toolchain] [toolchain]
channel = "stable" channel = "stable"
profile = "default"
components = [
"cargo",
"clippy",
"rust-analyzer",
"rust-std",
"rustc",
"rustfmt",
]

7
src/cache/args.rs vendored
View file

@ -8,8 +8,13 @@ const ARG_DISABLE_CACHE: &str = "disable-cache";
/// the user to disable any sort of cache. /// the user to disable any sort of cache.
pub fn arg() -> Arg { pub fn arg() -> Arg {
Arg::new(ARG_DISABLE_CACHE) Arg::new(ARG_DISABLE_CACHE)
.long("disable-cache")
.help("Disable any sort of 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) .action(ArgAction::SetTrue)
} }

View file

@ -8,9 +8,10 @@ const ARG_CONFIG: &str = "config";
/// user to customize the config file path. /// user to customize the config file path.
pub fn arg() -> Arg { pub fn arg() -> Arg {
Arg::new(ARG_CONFIG) Arg::new(ARG_CONFIG)
.help("Set a custom configuration file path")
.long("config") .long("config")
.short('c') .short('c')
.help("Forces a specific config file path") .global(true)
.value_name("PATH") .value_name("PATH")
} }

View file

@ -5,8 +5,8 @@
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use dirs::{config_dir, home_dir}; use dirs::{config_dir, home_dir};
use himalaya_lib::{AccountConfig, BackendConfig, EmailHooks, EmailTextPlainFormat};
use log::{debug, trace}; use log::{debug, trace};
use pimalaya_email::{AccountConfig, BackendConfig, EmailHooks, EmailTextPlainFormat};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fs, path::PathBuf}; use std::{collections::HashMap, fs, path::PathBuf};
use toml; use toml;
@ -17,7 +17,7 @@ use crate::{
}; };
/// Represents the user config file. /// 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")] #[serde(rename_all = "kebab-case")]
pub struct DeserializedConfig { pub struct DeserializedConfig {
#[serde(alias = "name")] #[serde(alias = "name")]
@ -31,12 +31,8 @@ pub struct DeserializedConfig {
pub email_listing_page_size: Option<usize>, pub email_listing_page_size: Option<usize>,
pub email_reading_headers: Option<Vec<String>>, pub email_reading_headers: Option<Vec<String>>,
#[serde( #[serde(default, with = "EmailTextPlainFormatDef")]
default, pub email_reading_format: EmailTextPlainFormat,
with = "EmailTextPlainFormatOptionDef",
skip_serializing_if = "Option::is_none"
)]
pub email_reading_format: Option<EmailTextPlainFormat>,
pub email_reading_verify_cmd: Option<String>, pub email_reading_verify_cmd: Option<String>,
pub email_reading_decrypt_cmd: Option<String>, pub email_reading_decrypt_cmd: Option<String>,
pub email_writing_headers: Option<Vec<String>>, pub email_writing_headers: Option<Vec<String>>,
@ -44,10 +40,10 @@ pub struct DeserializedConfig {
pub email_writing_encrypt_cmd: Option<String>, pub email_writing_encrypt_cmd: Option<String>,
#[serde( #[serde(
default, default,
with = "EmailHooksOptionDef", with = "EmailHooksDef",
skip_serializing_if = "Option::is_none" skip_serializing_if = "EmailHooks::is_empty"
)] )]
pub email_hooks: Option<EmailHooks>, pub email_hooks: EmailHooks,
#[serde(flatten)] #[serde(flatten)]
pub accounts: HashMap<String, DeserializedAccountConfig>, pub accounts: HashMap<String, DeserializedAccountConfig>,
@ -123,12 +119,12 @@ impl DeserializedConfig {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use himalaya_lib::{EmailSender, MaildirConfig, SendmailConfig, SmtpConfig}; use pimalaya_email::{EmailSender, MaildirConfig, SendmailConfig, SmtpConfig};
#[cfg(feature = "imap-backend")] #[cfg(feature = "imap-backend")]
use himalaya_lib::ImapConfig; use pimalaya_email::ImapConfig;
#[cfg(feature = "notmuch-backend")] #[cfg(feature = "notmuch-backend")]
use himalaya_lib::NotmuchConfig; use pimalaya_email::NotmuchConfig;
use std::io::Write; use std::io::Write;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
@ -163,10 +159,11 @@ mod tests {
fn account_missing_backend_field() { fn account_missing_backend_field() {
let config = make_config("[account]"); let config = make_config("[account]");
assert_eq!( assert!(config
config.unwrap_err().root_cause().to_string(), .unwrap_err()
"missing field `backend` at line 1 column 1" .root_cause()
); .to_string()
.contains("missing field `backend`"));
} }
#[test] #[test]
@ -180,7 +177,7 @@ mod tests {
.unwrap_err() .unwrap_err()
.root_cause() .root_cause()
.to_string() .to_string()
.starts_with("unknown variant `bad`")); .contains("unknown variant `bad`"));
} }
#[test] #[test]
@ -190,10 +187,11 @@ mod tests {
backend = \"none\"", backend = \"none\"",
); );
assert_eq!( assert!(config
config.unwrap_err().root_cause().to_string(), .unwrap_err()
"missing field `email` at line 1 column 1" .root_cause()
); .to_string()
.contains("missing field `email`"));
} }
#[test] #[test]
@ -205,10 +203,11 @@ mod tests {
backend = \"imap\"", backend = \"imap\"",
); );
assert_eq!( assert!(config
config.unwrap_err().root_cause().to_string(), .unwrap_err()
"missing field `imap-host` at line 1 column 1" .root_cause()
); .to_string()
.contains("missing field `imap-host`"));
} }
#[test] #[test]
@ -221,10 +220,11 @@ mod tests {
imap-host = \"localhost\"", imap-host = \"localhost\"",
); );
assert_eq!( assert!(config
config.unwrap_err().root_cause().to_string(), .unwrap_err()
"missing field `imap-port` at line 1 column 1" .root_cause()
); .to_string()
.contains("missing field `imap-port`"));
} }
#[test] #[test]
@ -238,10 +238,11 @@ mod tests {
imap-port = 993", imap-port = 993",
); );
assert_eq!( assert!(config
config.unwrap_err().root_cause().to_string(), .unwrap_err()
"missing field `imap-login` at line 1 column 1" .root_cause()
); .to_string()
.contains("missing field `imap-login`"));
} }
#[test] #[test]
@ -256,10 +257,11 @@ mod tests {
imap-login = \"login\"", imap-login = \"login\"",
); );
assert_eq!( assert!(config
config.unwrap_err().root_cause().to_string(), .unwrap_err()
"missing field `imap-passwd-cmd` at line 1 column 1" .root_cause()
); .to_string()
.contains("missing field `imap-passwd-cmd`"));
} }
#[test] #[test]
@ -271,10 +273,11 @@ mod tests {
backend = \"maildir\"", backend = \"maildir\"",
); );
assert_eq!( assert!(config
config.unwrap_err().root_cause().to_string(), .unwrap_err()
"missing field `maildir-root-dir` at line 1 column 1" .root_cause()
); .to_string()
.contains("missing field `maildir-root-dir`"));
} }
#[cfg(feature = "notmuch-backend")] #[cfg(feature = "notmuch-backend")]
@ -287,10 +290,11 @@ mod tests {
backend = \"notmuch\"", backend = \"notmuch\"",
); );
assert_eq!( assert!(config
config.unwrap_err().root_cause().to_string(), .unwrap_err()
"missing field `notmuch-db-path` at line 1 column 1" .root_cause()
); .to_string()
.contains("missing field `notmuch-db-path`"));
} }
#[test] #[test]
@ -301,10 +305,11 @@ mod tests {
backend = \"none\"", backend = \"none\"",
); );
assert_eq!( assert!(config
config.unwrap_err().root_cause().to_string(), .unwrap_err()
"missing field `sender` at line 1 column 1" .root_cause()
); .to_string()
.contains("missing field `sender`"));
} }
#[test] #[test]
@ -316,10 +321,11 @@ mod tests {
sender = \"bad\"", sender = \"bad\"",
); );
assert_eq!( assert!(config
config.unwrap_err().root_cause().to_string(), .unwrap_err()
"unknown variant `bad`, expected one of `none`, `smtp`, `sendmail` at line 1 column 1", .root_cause()
); .to_string()
.contains("unknown variant `bad`, expected one of `none`, `smtp`, `sendmail`"),);
} }
#[test] #[test]
@ -331,10 +337,11 @@ mod tests {
sender = \"smtp\"", sender = \"smtp\"",
); );
assert_eq!( assert!(config
config.unwrap_err().root_cause().to_string(), .unwrap_err()
"missing field `smtp-host` at line 1 column 1" .root_cause()
); .to_string()
.contains("missing field `smtp-host`"));
} }
#[test] #[test]
@ -347,10 +354,11 @@ mod tests {
smtp-host = \"localhost\"", smtp-host = \"localhost\"",
); );
assert_eq!( assert!(config
config.unwrap_err().root_cause().to_string(), .unwrap_err()
"missing field `smtp-port` at line 1 column 1" .root_cause()
); .to_string()
.contains("missing field `smtp-port`"));
} }
#[test] #[test]
@ -364,10 +372,11 @@ mod tests {
smtp-port = 25", smtp-port = 25",
); );
assert_eq!( assert!(config
config.unwrap_err().root_cause().to_string(), .unwrap_err()
"missing field `smtp-login` at line 1 column 1" .root_cause()
); .to_string()
.contains("missing field `smtp-login`"));
} }
#[test] #[test]
@ -382,10 +391,11 @@ mod tests {
smtp-login = \"login\"", smtp-login = \"login\"",
); );
assert_eq!( assert!(config
config.unwrap_err().root_cause().to_string(), .unwrap_err()
"missing field `smtp-passwd-cmd` at line 1 column 1" .root_cause()
); .to_string()
.contains("missing field `smtp-passwd-cmd`"));
} }
#[test] #[test]
@ -397,10 +407,11 @@ mod tests {
sender = \"sendmail\"", sender = \"sendmail\"",
); );
assert_eq!( assert!(config
config.unwrap_err().root_cause().to_string(), .unwrap_err()
"missing field `sendmail-cmd` at line 1 column 1" .root_cause()
); .to_string()
.contains("missing field `sendmail-cmd`"));
} }
#[test] #[test]

View file

@ -1,17 +1,17 @@
use serde::{Deserialize, Serialize}; use pimalaya_email::{
use std::path::PathBuf; folder::sync::Strategy as SyncFoldersStrategy, EmailHooks, EmailSender, EmailTextPlainFormat,
MaildirConfig, SendmailConfig, SmtpConfig,
use himalaya_lib::{
EmailHooks, EmailSender, EmailTextPlainFormat, MaildirConfig, SendmailConfig, SmtpConfig,
}; };
use serde::{Deserialize, Serialize};
use std::{collections::HashSet, path::PathBuf};
#[cfg(feature = "imap-backend")] #[cfg(feature = "imap-backend")]
use himalaya_lib::ImapConfig; use pimalaya_email::ImapConfig;
#[cfg(feature = "notmuch-backend")] #[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")] #[serde(remote = "SmtpConfig")]
struct SmtpConfigDef { struct SmtpConfigDef {
#[serde(rename = "smtp-host")] #[serde(rename = "smtp-host")]
@ -31,7 +31,7 @@ struct SmtpConfigDef {
} }
#[cfg(feature = "imap-backend")] #[cfg(feature = "imap-backend")]
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "ImapConfig")] #[serde(remote = "ImapConfig")]
pub struct ImapConfigDef { pub struct ImapConfigDef {
#[serde(rename = "imap-host")] #[serde(rename = "imap-host")]
@ -56,41 +56,39 @@ pub struct ImapConfigDef {
pub watch_cmds: Option<Vec<String>>, pub watch_cmds: Option<Vec<String>>,
} }
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "MaildirConfig")] #[serde(remote = "MaildirConfig", rename_all = "kebab-case")]
pub struct MaildirConfigDef { pub struct MaildirConfigDef {
#[serde(rename = "maildir-root-dir")] #[serde(rename = "maildir-root-dir")]
pub root_dir: PathBuf, pub root_dir: PathBuf,
} }
#[cfg(feature = "notmuch-backend")] #[cfg(feature = "notmuch-backend")]
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "NotmuchConfig")] #[serde(remote = "NotmuchConfig", rename_all = "kebab-case")]
pub struct NotmuchConfigDef { pub struct NotmuchConfigDef {
#[serde(rename = "notmuch-db-path")] #[serde(rename = "notmuch-db-path")]
pub db_path: PathBuf, pub db_path: PathBuf,
} }
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "Option<EmailTextPlainFormat>")] #[serde(
pub enum EmailTextPlainFormatOptionDef { remote = "EmailTextPlainFormat",
#[serde(with = "EmailTextPlainFormatDef")] tag = "type",
Some(EmailTextPlainFormat), content = "width",
#[default] rename_all = "kebab-case"
None, )]
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "EmailTextPlainFormat", rename_all = "snake_case")]
pub enum EmailTextPlainFormatDef { pub enum EmailTextPlainFormatDef {
#[default]
Auto, Auto,
Flowed, Flowed,
Fixed(usize), Fixed(usize),
} }
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "EmailSender", tag = "sender", rename_all = "snake_case")] #[serde(remote = "EmailSender", tag = "sender", rename_all = "kebab-case")]
pub enum EmailSenderDef { pub enum EmailSenderDef {
#[default]
None, None,
#[serde(with = "SmtpConfigDef")] #[serde(with = "SmtpConfigDef")]
Smtp(SmtpConfig), Smtp(SmtpConfig),
@ -98,27 +96,30 @@ pub enum EmailSenderDef {
Sendmail(SendmailConfig), Sendmail(SendmailConfig),
} }
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "SendmailConfig")] #[serde(remote = "SendmailConfig", rename_all = "kebab-case")]
pub struct SendmailConfigDef { pub struct SendmailConfigDef {
#[serde(rename = "sendmail-cmd")] #[serde(rename = "sendmail-cmd")]
cmd: String, cmd: String,
} }
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "Option<EmailHooks>")]
pub enum EmailHooksOptionDef {
#[serde(with = "EmailHooksDef")]
Some(EmailHooks),
#[default]
None,
}
/// Represents the email hooks. Useful for doing extra email /// Represents the email hooks. Useful for doing extra email
/// processing before or after sending it. /// processing before or after sending it.
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "EmailHooks")] #[serde(remote = "EmailHooks", rename_all = "kebab-case")]
pub struct EmailHooksDef { pub struct EmailHooksDef {
/// Represents the hook called just before sending an email. /// Represents the hook called just before sending an email.
pub pre_send: Option<String>, pub pre_send: Option<String>,
} }
#[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<String>),
#[serde(alias = "except")]
#[serde(alias = "ignore")]
Exclude(HashSet<String>),
}

View file

@ -4,7 +4,7 @@ use crate::account::{
}; };
use anyhow::Result; use anyhow::Result;
use dialoguer::{Input, Select}; use dialoguer::{Input, Select};
use himalaya_lib::ImapConfig; use pimalaya_email::ImapConfig;
#[cfg(feature = "imap-backend")] #[cfg(feature = "imap-backend")]
pub(crate) fn configure(base: DeserializedBaseAccountConfig) -> Result<DeserializedAccountConfig> { pub(crate) fn configure(base: DeserializedBaseAccountConfig) -> Result<DeserializedAccountConfig> {

View file

@ -5,7 +5,7 @@ use crate::account::{
use anyhow::Result; use anyhow::Result;
use dialoguer::Input; use dialoguer::Input;
use dirs::home_dir; use dirs::home_dir;
use himalaya_lib::MaildirConfig; use pimalaya_email::MaildirConfig;
pub(crate) fn configure(base: DeserializedBaseAccountConfig) -> Result<DeserializedAccountConfig> { pub(crate) fn configure(base: DeserializedBaseAccountConfig) -> Result<DeserializedAccountConfig> {
let input = if let Some(home) = home_dir() { let input = if let Some(home) = home_dir() {

View file

@ -32,7 +32,6 @@ const SECURITY_PROTOCOLS: &[&str] = &["SSL/TLS", "STARTTLS", "None"];
static THEME: Lazy<ColorfulTheme> = Lazy::new(ColorfulTheme::default); static THEME: Lazy<ColorfulTheme> = Lazy::new(ColorfulTheme::default);
pub(crate) fn wizard() -> Result<DeserializedConfig> { pub(crate) fn wizard() -> Result<DeserializedConfig> {
trace!(">> wizard");
println!("Himalaya couldn't find an already existing configuration file."); println!("Himalaya couldn't find an already existing configuration file.");
match Confirm::new() match Confirm::new()
@ -111,7 +110,7 @@ pub(crate) fn wizard() -> Result<DeserializedConfig> {
// Serialize config to file // Serialize config to file
println!("\nWriting the configuration to {path:?}..."); println!("\nWriting the configuration to {path:?}...");
fs::create_dir_all(path.parent().unwrap())?; fs::create_dir_all(path.parent().unwrap())?;
fs::write(path, toml::to_vec(&config)?)?; fs::write(path, toml::to_string(&config)?)?;
trace!("<< wizard"); trace!("<< wizard");
Ok(config) Ok(config)

View file

@ -4,7 +4,7 @@ use crate::account::{
}; };
use anyhow::Result; use anyhow::Result;
use dialoguer::Input; use dialoguer::Input;
use himalaya_lib::{NotmuchBackend, NotmuchConfig}; use pimalaya_email::{NotmuchBackend, NotmuchConfig};
pub(crate) fn configure(base: DeserializedBaseAccountConfig) -> Result<DeserializedAccountConfig> { pub(crate) fn configure(base: DeserializedBaseAccountConfig) -> Result<DeserializedAccountConfig> {
let db_path = match NotmuchBackend::get_default_db_path() { let db_path = match NotmuchBackend::get_default_db_path() {

View file

@ -1,7 +1,7 @@
use super::THEME; use super::THEME;
use anyhow::Result; use anyhow::Result;
use dialoguer::Input; use dialoguer::Input;
use himalaya_lib::{EmailSender, SendmailConfig}; use pimalaya_email::{EmailSender, SendmailConfig};
pub(crate) fn configure() -> Result<EmailSender> { pub(crate) fn configure() -> Result<EmailSender> {
Ok(EmailSender::Sendmail(SendmailConfig { Ok(EmailSender::Sendmail(SendmailConfig {

View file

@ -2,7 +2,7 @@ use super::{SECURITY_PROTOCOLS, THEME};
use crate::account::DeserializedBaseAccountConfig; use crate::account::DeserializedBaseAccountConfig;
use anyhow::Result; use anyhow::Result;
use dialoguer::{Input, Select}; use dialoguer::{Input, Select};
use himalaya_lib::{EmailSender, SmtpConfig}; use pimalaya_email::{EmailSender, SmtpConfig};
pub(crate) fn configure(base: &DeserializedBaseAccountConfig) -> Result<EmailSender> { pub(crate) fn configure(base: &DeserializedBaseAccountConfig) -> Result<EmailSender> {
let mut smtp_config = SmtpConfig { let mut smtp_config = SmtpConfig {

View file

@ -3,8 +3,10 @@
use anyhow::Result; use anyhow::Result;
use clap::{Arg, ArgAction, ArgMatches, Command}; use clap::{Arg, ArgAction, ArgMatches, Command};
use log::info; 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_ACCOUNT: &str = "account";
const ARG_DRY_RUN: &str = "dry-run"; const ARG_DRY_RUN: &str = "dry-run";
@ -20,7 +22,7 @@ pub enum Cmd {
/// Represents the list accounts command. /// Represents the list accounts command.
List(table::args::MaxTableWidth), List(table::args::MaxTableWidth),
/// Represents the sync account command. /// Represents the sync account command.
Sync(DryRun), Sync(Option<SyncFoldersStrategy>, DryRun),
} }
/// Represents the account command matcher. /// Represents the account command matcher.
@ -29,7 +31,22 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
if let Some(m) = m.subcommand_matches(CMD_SYNC) { if let Some(m) = m.subcommand_matches(CMD_SYNC) {
info!("sync account subcommand matched"); info!("sync account subcommand matched");
let dry_run = parse_dry_run_arg(m); 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) { } else if let Some(m) = m.subcommand_matches(CMD_LIST) {
info!("list accounts subcommand matched"); info!("list accounts subcommand matched");
let max_table_width = table::args::parse_max_width(m); let max_table_width = table::args::parse_max_width(m);
@ -55,6 +72,13 @@ pub fn subcmd() -> Command {
.arg(table::args::max_width()), .arg(table::args::max_width()),
Command::new(CMD_SYNC) Command::new(CMD_SYNC)
.about("Synchronize the given account locally") .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()), .arg(dry_run()),
]) ])
} }
@ -63,9 +87,10 @@ pub fn subcmd() -> Command {
/// the user to select a different account than the default one. /// the user to select a different account than the default one.
pub fn arg() -> Arg { pub fn arg() -> Arg {
Arg::new(ARG_ACCOUNT) Arg::new(ARG_ACCOUNT)
.help("Set the account")
.long("account") .long("account")
.short('a') .short('a')
.help("Select a specific account by name") .global(true)
.value_name("STRING") .value_name("STRING")
} }

View file

@ -3,24 +3,24 @@
//! This module contains the raw deserialized representation of an //! This module contains the raw deserialized representation of an
//! account in the accounts section of the user configuration file. //! account in the accounts section of the user configuration file.
use himalaya_lib::{ use pimalaya_email::{
AccountConfig, BackendConfig, EmailHooks, EmailSender, EmailTextPlainFormat, MaildirConfig, 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 serde::{Deserialize, Serialize};
use std::{collections::HashMap, path::PathBuf}; 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}; use crate::config::{prelude::*, DeserializedConfig};
/// Represents all existing kind of account config. /// Represents all existing kind of account config.
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(tag = "backend", rename_all = "snake_case")] #[serde(tag = "backend", rename_all = "kebab-case")]
pub enum DeserializedAccountConfig { pub enum DeserializedAccountConfig {
None(DeserializedBaseAccountConfig), None(DeserializedBaseAccountConfig),
Maildir(DeserializedMaildirAccountConfig), 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")] #[serde(rename_all = "kebab-case")]
pub struct DeserializedBaseAccountConfig { pub struct DeserializedBaseAccountConfig {
pub email: String, pub email: String,
@ -85,12 +85,8 @@ pub struct DeserializedBaseAccountConfig {
pub email_listing_page_size: Option<usize>, pub email_listing_page_size: Option<usize>,
pub email_reading_headers: Option<Vec<String>>, pub email_reading_headers: Option<Vec<String>>,
#[serde( #[serde(default, with = "EmailTextPlainFormatDef")]
default, pub email_reading_format: EmailTextPlainFormat,
with = "EmailTextPlainFormatOptionDef",
skip_serializing_if = "Option::is_none"
)]
pub email_reading_format: Option<EmailTextPlainFormat>,
pub email_reading_verify_cmd: Option<String>, pub email_reading_verify_cmd: Option<String>,
pub email_reading_decrypt_cmd: Option<String>, pub email_reading_decrypt_cmd: Option<String>,
pub email_writing_headers: Option<Vec<String>>, pub email_writing_headers: Option<Vec<String>>,
@ -100,14 +96,16 @@ pub struct DeserializedBaseAccountConfig {
pub email_sender: EmailSender, pub email_sender: EmailSender,
#[serde( #[serde(
default, default,
with = "EmailHooksOptionDef", with = "EmailHooksDef",
skip_serializing_if = "Option::is_none" skip_serializing_if = "EmailHooks::is_empty"
)] )]
pub email_hooks: Option<EmailHooks>, pub email_hooks: EmailHooks,
#[serde(default)] #[serde(default)]
pub sync: bool, pub sync: bool,
pub sync_dir: Option<PathBuf>, pub sync_dir: Option<PathBuf>,
#[serde(default, with = "SyncFoldersStrategyDef")]
pub sync_folders_strategy: SyncFoldersStrategy,
} }
impl DeserializedBaseAccountConfig { impl DeserializedBaseAccountConfig {
@ -159,12 +157,7 @@ impl DeserializedBaseAccountConfig {
.as_ref() .as_ref()
.map(ToOwned::to_owned) .map(ToOwned::to_owned)
.or_else(|| config.email_reading_headers.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: self.email_reading_format.clone(),
.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_verify_cmd: self email_reading_verify_cmd: self
.email_reading_verify_cmd .email_reading_verify_cmd
.as_ref() .as_ref()
@ -212,26 +205,16 @@ impl DeserializedBaseAccountConfig {
.or_else(|| config.email_writing_headers.as_ref().map(ToOwned::to_owned)), .or_else(|| config.email_writing_headers.as_ref().map(ToOwned::to_owned)),
email_sender: self.email_sender.to_owned(), email_sender: self.email_sender.to_owned(),
email_hooks: EmailHooks { email_hooks: EmailHooks {
pre_send: self pre_send: self.email_hooks.pre_send.clone(),
.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(),
}, },
sync: self.sync, sync: self.sync,
sync_dir: self.sync_dir.clone(), 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")] #[cfg(feature = "imap-backend")]
pub struct DeserializedImapAccountConfig { pub struct DeserializedImapAccountConfig {
#[serde(flatten)] #[serde(flatten)]
@ -240,7 +223,7 @@ pub struct DeserializedImapAccountConfig {
pub backend: ImapConfig, pub backend: ImapConfig,
} }
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct DeserializedMaildirAccountConfig { pub struct DeserializedMaildirAccountConfig {
#[serde(flatten)] #[serde(flatten)]
pub base: DeserializedBaseAccountConfig, pub base: DeserializedBaseAccountConfig,
@ -248,7 +231,7 @@ pub struct DeserializedMaildirAccountConfig {
pub backend: MaildirConfig, pub backend: MaildirConfig,
} }
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[cfg(feature = "notmuch-backend")] #[cfg(feature = "notmuch-backend")]
pub struct DeserializedNotmuchAccountConfig { pub struct DeserializedNotmuchAccountConfig {
#[serde(flatten)] #[serde(flatten)]

View file

@ -3,9 +3,12 @@
//! This module gathers all account actions triggered by the CLI. //! This module gathers all account actions triggered by the CLI.
use anyhow::Result; use anyhow::Result;
use himalaya_lib::{AccountConfig, Backend, BackendSyncBuilder, BackendSyncProgressEvent};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use log::{info, trace}; use log::{info, trace};
use pimalaya_email::{
folder::sync::Strategy as SyncFoldersStrategy, AccountConfig, Backend, BackendSyncBuilder,
BackendSyncProgressEvent,
};
use crate::{ use crate::{
config::DeserializedConfig, config::DeserializedConfig,
@ -43,15 +46,17 @@ pub fn sync<P: Printer>(
account_config: &AccountConfig, account_config: &AccountConfig,
printer: &mut P, printer: &mut P,
backend: &dyn Backend, backend: &dyn Backend,
folder: &Option<String>, folders_strategy: Option<SyncFoldersStrategy>,
dry_run: bool, dry_run: bool,
) -> Result<()> { ) -> Result<()> {
info!("entering the sync accounts handler"); 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); 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 { if dry_run {
@ -221,7 +226,7 @@ pub fn sync<P: Printer>(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use himalaya_lib::{AccountConfig, ImapConfig}; use pimalaya_email::{AccountConfig, ImapConfig};
use std::{collections::HashMap, fmt::Debug, io}; use std::{collections::HashMap, fmt::Debug, io};
use termcolor::ColorSpec; use termcolor::ColorSpec;

View file

@ -298,9 +298,9 @@ pub fn parse_criteria_arg(matches: &ArgMatches) -> String {
/// Represents the email reply all argument. /// Represents the email reply all argument.
pub fn reply_all_flag() -> Arg { pub fn reply_all_flag() -> Arg {
Arg::new(ARG_REPLY_ALL) Arg::new(ARG_REPLY_ALL)
.help("Includes all recipients") .help("Include all recipients")
.long("all") .long("all")
.short('a') .short('A')
.action(ArgAction::SetTrue) .action(ArgAction::SetTrue)
} }

View file

@ -1,9 +1,9 @@
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use atty::Stream; use atty::Stream;
use himalaya_lib::{ use log::{debug, trace};
use pimalaya_email::{
AccountConfig, Backend, Email, Flag, Flags, Sender, ShowTextPartsStrategy, Tpl, TplBuilder, AccountConfig, Backend, Email, Flag, Flags, Sender, ShowTextPartsStrategy, Tpl, TplBuilder,
}; };
use log::{debug, trace};
use std::{ use std::{
fs, fs,
io::{self, BufRead}, io::{self, BufRead},
@ -14,6 +14,7 @@ use uuid::Uuid;
use crate::{ use crate::{
printer::{PrintTableOpts, Printer}, printer::{PrintTableOpts, Printer},
ui::editor, ui::editor,
Envelopes,
}; };
pub fn attachments<P: Printer, B: Backend + ?Sized>( pub fn attachments<P: Printer, B: Backend + ?Sized>(
@ -132,10 +133,10 @@ pub fn list<P: Printer, B: Backend + ?Sized>(
let folder = config.folder_alias(folder)?; let folder = config.folder_alias(folder)?;
let page_size = page_size.unwrap_or(config.email_listing_page_size()); let page_size = page_size.unwrap_or(config.email_listing_page_size());
debug!("page size: {}", page_size); debug!("page size: {}", page_size);
let msgs = backend.list_envelopes(&folder, page_size, page)?; let envelopes: Envelopes = backend.list_envelopes(&folder, page_size, page)?.into();
trace!("envelopes: {:?}", msgs); trace!("envelopes: {:?}", envelopes);
printer.print_table( printer.print_table(
Box::new(msgs), Box::new(envelopes),
PrintTableOpts { PrintTableOpts {
format: &config.email_reading_format, format: &config.email_reading_format,
max_width, max_width,
@ -291,7 +292,9 @@ pub fn search<P: Printer, B: Backend + ?Sized>(
) -> Result<()> { ) -> Result<()> {
let folder = config.folder_alias(folder)?; let folder = config.folder_alias(folder)?;
let page_size = page_size.unwrap_or(config.email_listing_page_size()); 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 { let opts = PrintTableOpts {
format: &config.email_reading_format, format: &config.email_reading_format,
max_width, max_width,
@ -313,7 +316,9 @@ pub fn sort<P: Printer, B: Backend + ?Sized>(
) -> Result<()> { ) -> Result<()> {
let folder = config.folder_alias(folder)?; let folder = config.folder_alias(folder)?;
let page_size = page_size.unwrap_or(config.email_listing_page_size()); 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 { let opts = PrintTableOpts {
format: &config.email_reading_format, format: &config.email_reading_format,
max_width, max_width,
@ -329,7 +334,7 @@ pub fn send<P: Printer, B: Backend + ?Sized, S: Sender + ?Sized>(
sender: &mut S, sender: &mut S,
raw_email: String, raw_email: String,
) -> Result<()> { ) -> Result<()> {
let folder = config.folder_alias("sent")?; let folder = config.sent_folder_alias()?;
let is_tty = atty::is(Stream::Stdin); let is_tty = atty::is(Stream::Stdin);
let is_json = printer.is_json(); let is_json = printer.is_json();
let raw_email = if is_tty || is_json { let raw_email = if is_tty || is_json {
@ -344,7 +349,11 @@ pub fn send<P: Printer, B: Backend + ?Sized, S: Sender + ?Sized>(
}; };
trace!("raw email: {:?}", raw_email); trace!("raw email: {:?}", raw_email);
sender.send(raw_email.as_bytes())?; 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(()) Ok(())
} }

View file

@ -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<S: Serializer>(date: &DateTime<Local>, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&date.to_rfc3339())
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct Mailbox {
pub name: Option<String>,
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<Local>,
}
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 { impl Table for Envelope {
fn head() -> Row { fn head() -> Row {
@ -14,15 +53,29 @@ impl Table for Envelope {
fn row(&self) -> Row { fn row(&self) -> Row {
let id = self.id.to_string(); let id = self.id.to_string();
let flags = self.flags.to_symbols_string();
let unseen = !self.flags.contains(&Flag::Seen); 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 subject = &self.subject;
let sender = if let Some(name) = &self.from.name { let sender = if let Some(name) = &self.from.name {
name name
} else { } else {
&self.from.addr &self.from.addr
}; };
let date = self.date.format("%d/%m/%Y %H:%M").to_string(); let date = self.date.to_rfc3339();
Row::new() Row::new()
.cell(Cell::new(id).bold_if(unseen).red()) .cell(Cell::new(id).bold_if(unseen).red())

View file

@ -1,11 +1,32 @@
use std::ops;
use anyhow::Result; use anyhow::Result;
use himalaya_lib::Envelopes; use serde::Serialize;
use crate::{ use crate::{
printer::{PrintTable, PrintTableOpts, WriteColor}, printer::{PrintTable, PrintTableOpts, WriteColor},
ui::Table, ui::Table,
Envelope,
}; };
/// Represents the list of envelopes.
#[derive(Clone, Debug, Default, Serialize)]
pub struct Envelopes(Vec<Envelope>);
impl ops::Deref for Envelopes {
type Target = Vec<Envelope>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<pimalaya_email::Envelopes> for Envelopes {
fn from(envelopes: pimalaya_email::Envelopes) -> Self {
Envelopes(envelopes.iter().map(Envelope::from).collect())
}
}
impl PrintTable for Envelopes { impl PrintTable for Envelopes {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writer)?; writeln!(writer)?;

View file

@ -5,8 +5,8 @@
use anyhow::Result; use anyhow::Result;
use clap::{Arg, ArgMatches, Command}; use clap::{Arg, ArgMatches, Command};
use himalaya_lib::{Flag, Flags};
use log::{debug, info}; use log::{debug, info};
use pimalaya_email::{Flag, Flags};
use crate::email; use crate::email;
@ -83,7 +83,11 @@ pub fn flags_arg() -> Arg {
Arg::new(ARG_FLAGS) Arg::new(ARG_FLAGS)
.value_name("FLAGS") .value_name("FLAGS")
.help("The 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..) .num_args(1..)
.required(true) .required(true)
.last(true) .last(true)

25
src/domain/flag/flag.rs Normal file
View file

@ -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()),
}
}
}

21
src/domain/flag/flags.rs Normal file
View file

@ -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<Flag>);
impl ops::Deref for Flags {
type Target = HashSet<Flag>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<pimalaya_email::Flags> for Flags {
fn from(flags: pimalaya_email::Flags) -> Self {
Flags(flags.iter().map(Flag::from).collect())
}
}

View file

@ -1,5 +1,5 @@
use anyhow::Result; use anyhow::Result;
use himalaya_lib::{Backend, Flags}; use pimalaya_email::{Backend, Flags};
use crate::printer::Printer; use crate::printer::Printer;

View file

@ -1,2 +1,8 @@
pub mod args; pub mod args;
pub mod handlers; pub mod handlers;
pub mod flag;
pub use flag::*;
pub mod flags;
pub use flags::*;

View file

@ -3,14 +3,21 @@
//! This module provides subcommands, arguments and a command matcher //! This module provides subcommands, arguments and a command matcher
//! related to the folder domain. //! related to the folder domain.
use std::collections::HashSet;
use anyhow::Result; use anyhow::Result;
use clap::{self, Arg, ArgMatches, Command}; use clap::{self, Arg, ArgAction, ArgMatches, Command};
use log::{debug, info}; use log::{debug, info};
use crate::ui::table; 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_SOURCE: &str = "source";
const ARG_TARGET: &str = "target"; const ARG_TARGET: &str = "target";
const CMD_CREATE: &str = "create";
const CMD_DELETE: &str = "delete";
const CMD_EXPUNGE: &str = "expunge"; const CMD_EXPUNGE: &str = "expunge";
const CMD_FOLDERS: &str = "folders"; const CMD_FOLDERS: &str = "folders";
const CMD_LIST: &str = "list"; const CMD_LIST: &str = "list";
@ -18,8 +25,10 @@ const CMD_LIST: &str = "list";
/// Represents the folder commands. /// Represents the folder commands.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum Cmd { pub enum Cmd {
Create,
List(table::args::MaxTableWidth), List(table::args::MaxTableWidth),
Expunge, Expunge,
Delete,
} }
/// Represents the folder command matcher. /// Represents the folder command matcher.
@ -28,10 +37,16 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
if let Some(_) = m.subcommand_matches(CMD_EXPUNGE) { if let Some(_) = m.subcommand_matches(CMD_EXPUNGE) {
info!("expunge folder subcommand matched"); info!("expunge folder subcommand matched");
Some(Cmd::Expunge) 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) { } else if let Some(m) = m.subcommand_matches(CMD_LIST) {
debug!("list folders command matched"); debug!("list folders command matched");
let max_table_width = table::args::parse_max_width(m); let max_table_width = table::args::parse_max_width(m);
Some(Cmd::List(max_table_width)) Some(Cmd::List(max_table_width))
} else if let Some(_) = m.subcommand_matches(CMD_DELETE) {
debug!("delete folder command matched");
Some(Cmd::Delete)
} else { } else {
info!("no folder subcommand matched, falling back to subcommand list"); info!("no folder subcommand matched, falling back to subcommand list");
Some(Cmd::List(None)) Some(Cmd::List(None))
@ -49,18 +64,25 @@ pub fn subcmd() -> Command {
.about("Manage folders") .about("Manage folders")
.subcommands([ .subcommands([
Command::new(CMD_EXPUNGE).about("Delete emails marked for deletion"), 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) Command::new(CMD_LIST)
.about("List folders") .about("List folders")
.arg(table::args::max_width()), .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. /// Represents the source folder argument.
pub fn source_arg() -> Arg { pub fn source_arg() -> Arg {
Arg::new(ARG_SOURCE) Arg::new(ARG_SOURCE)
.help("Set the source folder")
.long("folder") .long("folder")
.short('f') .short('f')
.help("Specifies the source folder") .global(true)
.value_name("SOURCE") .value_name("SOURCE")
} }
@ -69,6 +91,70 @@ pub fn parse_source_arg(matches: &ArgMatches) -> Option<&str> {
matches.get_one::<String>(ARG_SOURCE).map(String::as_str) matches.get_one::<String>(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<String> {
m.get_many::<String>(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<String> {
m.get_many::<String>(ARG_EXCLUDE)
.unwrap_or_default()
.map(ToOwned::to_owned)
.collect()
}
/// Represents the target folder argument. /// Represents the target folder argument.
pub fn target_arg() -> Arg { pub fn target_arg() -> Arg {
Arg::new(ARG_TARGET) Arg::new(ARG_TARGET)

View file

@ -1,7 +1,24 @@
use himalaya_lib::folder::Folder; use serde::Serialize;
use crate::ui::{Cell, Row, Table}; 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 { impl Table for Folder {
fn head() -> Row { fn head() -> Row {
Row::new() Row::new()

View file

@ -1,11 +1,31 @@
use std::ops;
use anyhow::Result; use anyhow::Result;
use himalaya_lib::folder::Folders; use serde::Serialize;
use crate::{ use crate::{
printer::{PrintTable, PrintTableOpts, WriteColor}, printer::{PrintTable, PrintTableOpts, WriteColor},
ui::Table, ui::Table,
Folder,
}; };
#[derive(Clone, Debug, Default, Serialize)]
pub struct Folders(Vec<Folder>);
impl ops::Deref for Folders {
type Target = Vec<Folder>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<pimalaya_email::Folders> for Folders {
fn from(folders: pimalaya_email::Folders) -> Self {
Folders(folders.iter().map(Folder::from).collect())
}
}
impl PrintTable for Folders { impl PrintTable for Folders {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writer)?; writeln!(writer)?;

View file

@ -3,26 +3,31 @@
//! This module gathers all folder actions triggered by the CLI. //! This module gathers all folder actions triggered by the CLI.
use anyhow::Result; 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<P: Printer, B: Backend + ?Sized>( pub fn expunge<P: Printer, B: Backend + ?Sized>(
folder: &str,
printer: &mut P, printer: &mut P,
backend: &mut B, backend: &mut B,
folder: &str,
) -> Result<()> { ) -> Result<()> {
backend.expunge_folder(folder)?; backend.expunge_folder(folder)?;
printer.print(format!("Folder {folder} successfully expunged!")) printer.print(format!("Folder {folder} successfully expunged!"))
} }
pub fn list<P: Printer, B: Backend + ?Sized>( pub fn list<P: Printer, B: Backend + ?Sized>(
max_width: Option<usize>,
config: &AccountConfig, config: &AccountConfig,
printer: &mut P, printer: &mut P,
backend: &mut B, backend: &mut B,
max_width: Option<usize>,
) -> Result<()> { ) -> Result<()> {
let folders = backend.list_folders()?; let folders: Folders = backend.list_folders()?.into();
printer.print_table( printer.print_table(
// TODO: remove Box // TODO: remove Box
Box::new(folders), Box::new(folders),
@ -33,9 +38,36 @@ pub fn list<P: Printer, B: Backend + ?Sized>(
) )
} }
pub fn create<P: Printer, B: Backend + ?Sized>(
printer: &mut P,
backend: &mut B,
folder: &str,
) -> Result<()> {
backend.add_folder(folder)?;
printer.print("Folder successfully created!")
}
pub fn delete<P: Printer, B: Backend + ?Sized>(
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)] #[cfg(test)]
mod tests { mod tests {
use himalaya_lib::{ use pimalaya_email::{
backend, AccountConfig, Backend, Emails, Envelope, Envelopes, Flags, Folder, Folders, backend, AccountConfig, Backend, Emails, Envelope, Envelopes, Flags, Folder, Folders,
}; };
use std::{any::Any, fmt::Debug, io}; use std::{any::Any, fmt::Debug, io};

View file

@ -3,7 +3,7 @@
//! This module gathers all IMAP handlers triggered by the CLI. //! This module gathers all IMAP handlers triggered by the CLI.
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use himalaya_lib::ImapBackend; use pimalaya_email::ImapBackend;
pub fn notify(imap: &ImapBackend, folder: &str, keepalive: u64) -> Result<()> { pub fn notify(imap: &ImapBackend, folder: &str, keepalive: u64) -> Result<()> {
imap.notify(keepalive, folder).context("cannot imap notify") imap.notify(keepalive, folder).context("cannot imap notify")

View file

@ -1,6 +1,6 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use atty::Stream; 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 std::io::{stdin, BufRead};
use crate::printer::Printer; use crate::printer::Printer;

View file

@ -1,4 +1,4 @@
use anyhow::Result; use anyhow::{anyhow, Context, Result};
use clap::Command; use clap::Command;
use std::{borrow::Cow, env}; use std::{borrow::Cow, env};
use url::Url; use url::Url;
@ -10,7 +10,7 @@ use himalaya::{
printer::StdoutPrinter, printer::StdoutPrinter,
tpl, tpl,
}; };
use himalaya_lib::{ use pimalaya_email::{
BackendBuilder, BackendConfig, ImapBackend, SenderBuilder, DEFAULT_INBOX_FOLDER, BackendBuilder, BackendConfig, ImapBackend, SenderBuilder, DEFAULT_INBOX_FOLDER,
}; };
@ -77,8 +77,7 @@ fn main() -> Result<()> {
_ => (), _ => (),
} }
// checks completion command before configs // also checks man command before configs
// https://github.com/soywod/himalaya/issues/115
match man::args::matches(&m)? { match man::args::matches(&m)? {
Some(man::args::Cmd::GenerateAll(dir)) => { Some(man::args::Cmd::GenerateAll(dir)) => {
return man::handlers::generate(dir, create_app()); return man::handlers::generate(dir, create_app());
@ -123,11 +122,7 @@ fn main() -> Result<()> {
Some(account::args::Cmd::List(max_width)) => { Some(account::args::Cmd::List(max_width)) => {
return account::handlers::list(max_width, &account_config, &config, &mut printer); return account::handlers::list(max_width, &account_config, &config, &mut printer);
} }
Some(account::args::Cmd::Sync(dry_run)) => { Some(account::args::Cmd::Sync(folders_strategy, dry_run)) => {
let folder = match folder {
Some(folder) => Some(account_config.folder_alias(folder)?),
None => None,
};
let backend = BackendBuilder::new() let backend = BackendBuilder::new()
.sessions_pool_size(8) .sessions_pool_size(8)
.disable_cache(true) .disable_cache(true)
@ -136,7 +131,7 @@ fn main() -> Result<()> {
&account_config, &account_config,
&mut printer, &mut printer,
backend.as_ref(), backend.as_ref(),
&folder, folders_strategy,
dry_run, dry_run,
)?; )?;
backend.close()?; backend.close()?;
@ -147,24 +142,44 @@ fn main() -> Result<()> {
// checks folder commands // checks folder commands
match folder::args::matches(&m)? { match folder::args::matches(&m)? {
Some(folder::args::Cmd::Expunge) => { Some(folder::args::Cmd::Create) => {
let folder = account_config.folder_alias(folder.unwrap_or(DEFAULT_INBOX_FOLDER))?; 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() let mut backend = BackendBuilder::new()
.disable_cache(disable_cache) .disable_cache(disable_cache)
.build(&account_config, &backend_config)?; .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)) => { Some(folder::args::Cmd::List(max_width)) => {
let mut backend = BackendBuilder::new() let mut backend = BackendBuilder::new()
.disable_cache(disable_cache) .disable_cache(disable_cache)
.build(&account_config, &backend_config)?; .build(&account_config, &backend_config)?;
return folder::handlers::list( return folder::handlers::list(
max_width,
&account_config, &account_config,
&mut printer, &mut printer,
backend.as_mut(), 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);
}
_ => (), _ => (),
} }

View file

@ -30,11 +30,14 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
/// Man subcommands. /// Man subcommands.
pub fn subcmd() -> Command { pub fn subcmd() -> Command {
Command::new(CMD_MAN) Command::new(CMD_MAN)
.about("Generates all man pages to the specified directory.") .about("Generate all man pages to the given directory")
.arg( .arg(
Arg::new(ARG_DIR) Arg::new(ARG_DIR)
.help("Directory where to generate man files") .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.") .long_help(
"Represents the directory where all man files of
all commands and subcommands should be generated in.",
)
.required(true), .required(true),
) )
} }

View file

@ -11,22 +11,23 @@ pub(crate) const ARG_OUTPUT: &str = "output";
pub fn args() -> Vec<Arg> { pub fn args() -> Vec<Arg> {
vec![ vec![
Arg::new(ARG_OUTPUT) Arg::new(ARG_OUTPUT)
.help("Defines the output format") .help("Set the output format")
.long("output") .long("output")
.short('o') .short('o')
.global(true)
.value_name("FMT") .value_name("FMT")
.value_parser(["plain", "json"]) .value_parser(["plain", "json"])
.default_value("plain"), .default_value("plain"),
Arg::new(ARG_COLOR) Arg::new(ARG_COLOR)
.help("Controls when to use colors.") .help("Control when to use colors.")
.long_help( .long_help(
" "This flag controls when to use colors. The default
This flag controls when to use colors. The default setting is 'auto', which setting is 'auto', which means himalaya will try to guess when to use
means himalaya will try to guess when to use colors. For example, if himalaya is colors. For example, if himalaya is printing to a terminal, then it
printing to a terminal, then it will use colors, but if it is redirected to a will use colors, but if it is redirected to a file or a pipe, then it
file or a pipe, then it will suppress color output. himalaya will suppress color will suppress color output. himalaya will suppress color output in
output in some other circumstances as well. For example, if the TERM some other circumstances as well. For example, if the TERM environment
environment variable is not set or set to 'dumb', then himalaya will not use variable is not set or set to 'dumb', then himalaya will not use
colors. colors.
The possible values for this flag are: The possible values for this flag are:
@ -34,11 +35,11 @@ The possible values for this flag are:
never Colors will never be used. never Colors will never be used.
auto The default. himalaya tries to be smart. auto The default. himalaya tries to be smart.
always Colors will always be used regardless of where output is sent. 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") .long("color")
.short('C') .short('C')
.global(true)
.value_parser(["never", "auto", "always", "ansi"]) .value_parser(["never", "auto", "always", "ansi"])
.default_value("auto") .default_value("auto")
.value_name("WHEN"), .value_name("WHEN"),

View file

@ -1,5 +1,5 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use himalaya_lib::Tpl; use pimalaya_email::Tpl;
use crate::printer::WriteColor; use crate::printer::WriteColor;

View file

@ -1,5 +1,5 @@
use anyhow::Result; use anyhow::Result;
use himalaya_lib::EmailTextPlainFormat; use pimalaya_email::EmailTextPlainFormat;
use std::io; use std::io;
use termcolor::{self, StandardStream}; use termcolor::{self, StandardStream};

View file

@ -1,9 +1,9 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use himalaya_lib::{ use log::debug;
use pimalaya_email::{
email::{local_draft_path, remove_local_draft}, email::{local_draft_path, remove_local_draft},
AccountConfig, Backend, CompilerBuilder, Flag, Flags, Sender, Tpl, AccountConfig, Backend, CompilerBuilder, Flag, Flags, Sender, Tpl,
}; };
use log::debug;
use std::{env, fs, process::Command}; use std::{env, fs, process::Command};
use crate::{ use crate::{
@ -81,7 +81,7 @@ pub fn edit_tpl_with_editor<P: Printer, B: Backend + ?Sized, S: Sender + ?Sized>
sender.send(&email)?; sender.send(&email)?;
let sent_folder = config.sent_folder_alias()?; let sent_folder = config.sent_folder_alias()?;
printer.print_log(format!("Adding email to the {} folder…", sent_folder))?; 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()?; remove_local_draft()?;
printer.print("Done!")?; printer.print("Done!")?;
break; break;
@ -101,7 +101,11 @@ pub fn edit_tpl_with_editor<P: Printer, B: Backend + ?Sized, S: Sender + ?Sized>
.some_pgp_sign_cmd(config.email_writing_sign_cmd.as_ref()) .some_pgp_sign_cmd(config.email_writing_sign_cmd.as_ref())
.some_pgp_encrypt_cmd(config.email_writing_encrypt_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()?; remove_local_draft()?;
printer.print(format!("Email successfully saved to {}", draft_folder))?; printer.print(format!("Email successfully saved to {}", draft_folder))?;
break; break;

View file

@ -5,8 +5,8 @@
//! [builder design pattern]: https://refactoring.guru/design-patterns/builder //! [builder design pattern]: https://refactoring.guru/design-patterns/builder
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use himalaya_lib::EmailTextPlainFormat;
use log::trace; use log::trace;
use pimalaya_email::EmailTextPlainFormat;
use termcolor::{Color, ColorSpec}; use termcolor::{Color, ColorSpec};
use terminal_size; use terminal_size;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;