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:
pull_request:
push:
branches:
- master
jobs:
tests:
runs-on: ubuntu-latest
steps:
- name: Install libnotmuch
run: sudo apt-get install -y libnotmuch-dev
- name: Checkout code
uses: actions/checkout@v2
- name: Start GreenMail testing server
run: |
docker run \
--rm \
-d \
-e GREENMAIL_OPTS='-Dgreenmail.setup.test.all -Dgreenmail.hostname=0.0.0.0 -Dgreenmail.auth.disabled -Dgreenmail.verbose' \
-p 3025:3025 \
-p 3110:3110 \
-p 3143:3143 \
-p 3465:3465 \
-p 3993:3993 \
-p 3995:3995 \
greenmail/standalone:1.6.11
- name: Install rust
uses: actions-rs/toolchain@v1
uses: actions/checkout@v2
- name: Install Nix
uses: cachix/install-nix-action@v20
with:
toolchain: stable
- name: Run tests
uses: actions-rs/cargo@v1
nix_path: nixpkgs=channel:nixos-unstable
extra_nix_config: |
experimental-features = nix-command flakes
- uses: cachix/cachix-action@v12
with:
command: test
args: --all-features
name: soywod
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Test
run: nix run .#test

View file

@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.7.2] - 2023-05-01
### Added
- Added `create` and `delete` folder commands [sourcehut#54].
- Added generated completions and man pages to releases
[sourcehut#43].
- Added new account config option `sync-folders-strategy` which allows
to choose a folders synchronization strategy [sourcehut#59]:
- `sync-folders-strategy = "all"`: synchronize all existing folders
for the current account
- `sync-folders-strategy.include = ["folder1", "folder2", …]`:
synchronize only the given folders for the current account
- `sync-folders-strategy.exclude = ["folder1", "folder2", …]`:
synchronizes all folders except the given ones for the current
account
Also added new `account sync` arguments that override the account
config option:
- `-A|--all-folders`: include all folders to the synchronization.
- `-F|--include-folder`: include given folders to the
synchronization. They can be repeated `-F folder1 folder2` or `-F
folder1 -F folder2`.
- `-x|--exclude-folder`: exclude given folders from the
synchronization. They can be repeated `-x folder1 folder2` or `-x
folder1 -F folder2`.
- Added cargo features `native-tls` (default), `rustls-tls` and
`rustls-native-certs`.
### Changed
- Made global options truly global, which means they can be used
everywhere (not only *before* commands but also *after*)
[sourcehut#60].
- Replaced reply all `-a` argument with `-A` because it conflicted
with the global option `-a|--account`.
- Replaced `himalaya-lib` by `pimalaya-email`.
- Renamed feature `vendored` to `native-tls-vendored`.
- Removed the `develop` branch, all the development is now done on the
`master` branch.
### Fixed
- Fixed config deserialization issue with `email-hooks` and
`email-reading-format`.
- Fixed flags case sensitivity.
## [0.7.1] - 2023-02-14
### Added
@ -17,7 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Changed the location of the
[documentation](https://pimalaya.org/himalaya/docs/).
[documentation](https://pimalaya.org/himalaya/).
### Fixed
@ -498,7 +548,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Password from command [#22]
- Set up README [#20]
[Unreleased]: https://github.com/soywod/himalaya/compare/v0.7.1...develop
[Unreleased]: https://github.com/soywod/himalaya/compare/v0.7.2...develop
[0.7.2]: https://github.com/soywod/himalaya/compare/v0.7.1...v0.7.2
[0.7.1]: https://github.com/soywod/himalaya/compare/v0.7.0...v0.7.1
[0.7.0]: https://github.com/soywod/himalaya/compare/v0.6.1...v0.7.0
[0.6.1]: https://github.com/soywod/himalaya/compare/v0.6.0...v0.6.1
@ -664,3 +715,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#356]: https://github.com/soywod/himalaya/issues/356
[#419]: https://github.com/soywod/himalaya/issues/419
[#430]: https://github.com/soywod/himalaya/issues/430
[sourcehut#43]: https://todo.sr.ht/~soywod/pimalaya/43
[sourcehut#54]: https://todo.sr.ht/~soywod/pimalaya/54
[sourcehut#59]: https://todo.sr.ht/~soywod/pimalaya/59
[sourcehut#60]: https://todo.sr.ht/~soywod/pimalaya/60

374
Cargo.lock generated
View file

@ -94,12 +94,24 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24a6904aef64d73cf10ab17ebace7befb918b82164785cb89907993be7f83813"
[[package]]
name = "bufstream"
version = "0.1.4"
@ -136,7 +148,7 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18e9079d1a12a2cc2bffb5db039c43661836ead4082120d5844f02555aca2d46"
dependencies = [
"base64",
"base64 0.13.1",
"encoding_rs",
]
@ -180,7 +192,7 @@ version = "4.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"clap_lex",
"is-terminal",
"strsim 0.10.0",
@ -221,7 +233,7 @@ version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
dependencies = [
"bitflags",
"bitflags 1.3.2",
]
[[package]]
@ -500,11 +512,11 @@ checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
[[package]]
name = "email-encoding"
version = "0.1.3"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34dd14c63662e0206599796cd5e1ad0268ab2b9d19b868d6050d688eba2bbf98"
checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75"
dependencies = [
"base64",
"base64 0.21.0",
"memchr 2.5.0",
]
@ -765,10 +777,11 @@ dependencies = [
[[package]]
name = "himalaya"
version = "0.7.1"
version = "0.7.2"
dependencies = [
"anyhow",
"atty",
"chrono",
"clap",
"clap_complete",
"clap_mangen",
@ -778,10 +791,10 @@ dependencies = [
"email_address",
"env_logger",
"erased-serde",
"himalaya-lib",
"indicatif",
"log",
"once_cell",
"pimalaya-email",
"serde",
"serde_json",
"shellexpand",
@ -794,42 +807,6 @@ dependencies = [
"uuid",
]
[[package]]
name = "himalaya-lib"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6aa84cdd1cec7bd25e319f0decd7d6ec5d765fb983da7a0dea10d797f7e73a8"
dependencies = [
"ammonia",
"chrono",
"convert_case",
"dirs",
"html-escape",
"imap",
"imap-proto",
"lettre",
"log",
"maildir",
"mailparse",
"md5",
"mime-msg-builder",
"native-tls",
"notmuch",
"ouroboros",
"proc-lock",
"rayon",
"regex",
"rfc2047-decoder",
"rusqlite",
"serde",
"shellexpand",
"thiserror",
"tree_magic",
"urlencoding",
"utf7-imap",
"uuid",
]
[[package]]
name = "hostname"
version = "0.3.1"
@ -906,17 +883,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "idna"
version = "0.3.0"
@ -933,7 +899,7 @@ version = "3.0.0-alpha.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c2ff52273d9cd791687b4510d8a0047277e985a348e411c94fe84e193e7a76"
dependencies = [
"base64",
"base64 0.13.1",
"bufstream",
"chrono",
"imap-proto",
@ -942,6 +908,7 @@ dependencies = [
"nom 7.1.1",
"ouroboros",
"regex",
"rustls-connector",
]
[[package]]
@ -1029,25 +996,29 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lettre"
version = "0.10.1"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eabca5e0b4d0e98e7f2243fb5b7520b6af2b65d8f87bcc86f2c75185a6ff243"
checksum = "dd84a055407850bcf4791baa77cb4818d37cbb79ad4e60b9b659727b920d2c65"
dependencies = [
"base64",
"base64 0.21.0",
"email-encoding",
"email_address",
"fastrand",
"futures-util",
"hostname",
"httpdate",
"idna 0.2.3",
"idna",
"mime",
"native-tls",
"nom 7.1.1",
"once_cell",
"quoted_printable",
"rustls",
"rustls-pemfile",
"serde",
"socket2",
"tokio",
"webpki-roots",
]
[[package]]
@ -1058,9 +1029,9 @@ checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
[[package]]
name = "libsqlite3-sys"
version = "0.25.2"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa"
checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326"
dependencies = [
"cc",
"pkg-config",
@ -1116,6 +1087,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "mail-parser"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2e03aa1d18528b45d0e79e46790f38cfeece6cce3af17a85912677272f36cd"
dependencies = [
"encoding_rs",
]
[[package]]
name = "maildir"
version = "0.6.3"
@ -1164,12 +1144,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]]
name = "matches"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "md5"
version = "0.7.0"
@ -1238,6 +1212,18 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "mio"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.42.0",
]
[[package]]
name = "native-tls"
version = "0.2.11"
@ -1281,6 +1267,15 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nom8"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8"
dependencies = [
"memchr 2.5.0",
]
[[package]]
name = "notmuch"
version = "0.8.0"
@ -1338,7 +1333,7 @@ version = "0.10.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "020433887e44c27ff16365eaa2d380547a94544ad509aff6eb5b6e3e0b27b376"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"cfg-if 1.0.0",
"foreign-types",
"libc",
@ -1364,6 +1359,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "111.25.0+1.1.1t"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3173cd3626c43e3854b1b727422a276e568d9ec5fe8cec197822cf52cfb743d6"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.78"
@ -1373,6 +1377,7 @@ dependencies = [
"autocfg",
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
@ -1507,6 +1512,46 @@ dependencies = [
"siphasher",
]
[[package]]
name = "pimalaya-email"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab7e9b0747644aecff24024c88348146bd74bba78fa5ecca10e7e5938d6873a"
dependencies = [
"ammonia",
"chrono",
"convert_case",
"dirs",
"html-escape",
"imap",
"imap-proto",
"lettre",
"log",
"mail-parser",
"maildir",
"mailparse",
"md5",
"mime-msg-builder",
"native-tls",
"notmuch",
"once_cell",
"ouroboros",
"proc-lock",
"rayon",
"regex",
"rfc2047-decoder",
"rusqlite",
"rustls",
"rustls-native-certs",
"shellexpand",
"thiserror",
"tree_magic",
"urlencoding",
"utf7-imap",
"uuid",
"webpki-roots",
]
[[package]]
name = "pin-project-lite"
version = "0.2.9"
@ -1702,7 +1747,7 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags",
"bitflags 1.3.2",
]
[[package]]
@ -1748,7 +1793,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11347d014ae34e1d367aaf9191c37bf071b161e2e1c09c8559c7717e87030e11"
dependencies = [
"base64",
"base64 0.13.1",
"charset",
"chumsky 0.8.0",
"memchr 2.5.0",
@ -1756,6 +1801,21 @@ dependencies = [
"thiserror",
]
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin",
"untrusted",
"web-sys",
"winapi",
]
[[package]]
name = "roff"
version = "0.2.1"
@ -1764,11 +1824,11 @@ checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316"
[[package]]
name = "rusqlite"
version = "0.28.0"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a"
checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
dependencies = [
"bitflags",
"bitflags 2.2.1",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
@ -1782,7 +1842,7 @@ version = "0.36.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"errno",
"io-lifetimes",
"libc",
@ -1790,6 +1850,51 @@ dependencies = [
"windows-sys 0.42.0",
]
[[package]]
name = "rustls"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f"
dependencies = [
"log",
"ring",
"sct",
"webpki",
]
[[package]]
name = "rustls-connector"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c6a18f8d10f71bce9bca6eaeb80429460e652f3bcf0381f0c5f8954abf7b3b8"
dependencies = [
"log",
"rustls",
"rustls-native-certs",
"webpki",
]
[[package]]
name = "rustls-native-certs"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
dependencies = [
"base64 0.21.0",
]
[[package]]
name = "ryu"
version = "1.0.11"
@ -1818,13 +1923,23 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
[[package]]
name = "sct"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "security-framework"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"core-foundation",
"core-foundation-sys",
"libc",
@ -1843,18 +1958,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.148"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc"
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.148"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c"
checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
dependencies = [
"proc-macro2",
"quote",
@ -1872,6 +1987,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4"
dependencies = [
"serde",
]
[[package]]
name = "shellexpand"
version = "2.1.2"
@ -1912,6 +2036,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "stacker"
version = "0.1.15"
@ -2074,12 +2204,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "toml"
version = "0.5.9"
name = "tokio"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af"
dependencies = [
"autocfg",
"libc",
"mio",
"pin-project-lite",
"socket2",
"windows-sys 0.42.0",
]
[[package]]
name = "toml"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6a7712b49e1775fb9a7b998de6635b299237f48b404dde71704f2e0e7f37e5"
dependencies = [
"indexmap",
"nom8",
"serde",
"serde_spanned",
"toml_datetime",
]
[[package]]
@ -2122,6 +2291,12 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "url"
version = "2.3.1"
@ -2129,7 +2304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
dependencies = [
"form_urlencoded",
"idna 0.3.0",
"idna",
"percent-encoding",
]
@ -2151,7 +2326,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e326365261fc2761f0809dfb6032810534a0427bbd8f0edf546f6afeef89f5d"
dependencies = [
"base64",
"base64 0.13.1",
"encoding_rs",
"regex",
]
@ -2249,6 +2424,35 @@ version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
[[package]]
name = "web-sys"
version = "0.3.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "webpki"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87"
dependencies = [
"webpki",
]
[[package]]
name = "winapi"
version = "0.3.9"

View file

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

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 is a CLI based on the
[himalaya-lib](https://git.sr.ht/~soywod/himalaya-lib) that allows you
to manipulate your emails using commands in your console.
https://pimalaya.org/himalaya/
CLI to manage your emails, based on the
[pimalaya-email](https://sr.ht/~soywod/pimalaya/) library.
![image](https://user-images.githubusercontent.com/10437171/138774902-7b9de5a3-93eb-44b0-8cfb-6d2e11e3b1aa.png)
@ -25,19 +26,19 @@ production before the `v1.0.0`.*
- JSON output
- …
[Folder listing]: https://pimalaya.org/himalaya/docs/cli/usage/folders/list.html
[Envelopes listing]: https://pimalaya.org/himalaya/docs/cli/usage/envelopes/list.html
[searching]: https://pimalaya.org/himalaya/docs/cli/usage/envelopes/search.html
[sorting]: https://pimalaya.org/himalaya/docs/cli/usage/envelopes/sort.html
[Email composition]: https://pimalaya.org/himalaya/docs/cli/usage/emails/write.html
[copy]: https://pimalaya.org/himalaya/docs/cli/usage/emails/copy.html
[move]: https://pimalaya.org/himalaya/docs/cli/usage/emails/move.html
[delete]: https://pimalaya.org/himalaya/docs/cli/usage/emails/delete.html
[Multi-accounting]: https://pimalaya.org/himalaya/docs/cli/configuration.html
[Account listing]: https://pimalaya.org/himalaya/docs/cli/usage/accounts/list.html
[Account synchronization]: https://pimalaya.org/himalaya/docs/cli/usage/accounts/synchronize.html
[real-time notifications]: https://pimalaya.org/himalaya/docs/cli/usage/notifications.html
[Completions]: https://pimalaya.org/himalaya/docs/cli/tips/completion.html
[Folder listing]: https://pimalaya.org/himalaya/cli/usage/folders/list.html
[Envelopes listing]: https://pimalaya.org/himalaya/cli/usage/envelopes/list.html
[searching]: https://pimalaya.org/himalaya/cli/usage/envelopes/search.html
[sorting]: https://pimalaya.org/himalaya/cli/usage/envelopes/sort.html
[Email composition]: https://pimalaya.org/himalaya/cli/usage/emails/write.html
[copy]: https://pimalaya.org/himalaya/cli/usage/emails/copy.html
[move]: https://pimalaya.org/himalaya/cli/usage/emails/move.html
[delete]: https://pimalaya.org/himalaya/cli/usage/emails/delete.html
[Multi-accounting]: https://pimalaya.org/himalaya/cli/configuration.html
[Account listing]: https://pimalaya.org/himalaya/cli/usage/accounts/list.html
[Account synchronization]: https://pimalaya.org/himalaya/cli/usage/accounts/synchronize.html
[real-time notifications]: https://pimalaya.org/himalaya/cli/usage/notifications.html
[Completions]: https://pimalaya.org/himalaya/cli/tips/completion.html
## Installation
@ -60,6 +61,9 @@ $ yay -S himalaya-git
# Homebrew
$ brew install himalaya
# Scoop
$ scoop install himalaya
# Cargo
$ cargo install himalaya
@ -68,7 +72,7 @@ $ nix-env -i himalaya
```
*See the
[documentation](https://pimalaya.org/himalaya/docs/cli/installation.html)
[documentation](https://pimalaya.org/himalaya/cli/installation.html)
for other installation methods.*
</td>
@ -78,11 +82,12 @@ for other installation methods.*
## Configuration
Please read the
[documentation](https://pimalaya.org/himalaya/docs/cli/configuration.html).
[documentation](https://pimalaya.org/himalaya/cli/configuration.html).
## Contributing
If you find a **bug**, please send an email at
If you find a **bug** that [does not exist
yet](https://todo.sr.ht/~soywod/pimalaya), please send an email at
[~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht).
If you have a **question**, please send an email at

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

171
flake.nix
View file

@ -1,65 +1,140 @@
{
description = "Command-line interface for email management.";
description = "CLI to manage your emails.";
inputs = {
utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
naersk.url = "github:nix-community/naersk";
flake-compat = {
url = "github:edolstra/flake-compat";
flake = false;
nixpkgs.url = "github:nixos/nixpkgs/nixos-22.11";
flake-utils.url = "github:numtide/flake-utils";
gitignore = {
url = "github:hercules-ci/gitignore.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
naersk = {
url = "github:nix-community/naersk";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, utils, rust-overlay, naersk, ... }:
utils.lib.eachDefaultSystem
(system:
outputs = { self, nixpkgs, flake-utils, gitignore, fenix, naersk }:
let
inherit (gitignore.lib) gitignoreSource;
mkToolchain = buildPlatform:
fenix.packages.${buildPlatform}.minimal.toolchain;
mkToolchainWithTarget = buildPlatform: targetPlatform:
with fenix.packages.${buildPlatform}; combine [
stable.rustc
stable.cargo
targets.${targetPlatform}.stable.rust-std
];
mkDevShells = buildPlatform:
let
name = "himalaya";
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs { inherit system overlays; };
pkgs = import nixpkgs { system = buildPlatform; };
rust-toolchain = fenix.packages.${buildPlatform}.fromToolchainFile {
file = ./rust-toolchain.toml;
sha256 = "eMJethw5ZLrJHmoN2/l0bIyQjoTX1NsvalWSscTixpI=";
};
in
rec {
# nix build
defaultPackage = packages.${name};
packages = {
${name} = naersk.lib.${system}.buildPackage {
pname = name;
root = ./.;
nativeBuildInputs = with pkgs; [ openssl.dev pkg-config ];
overrideMain = _: {
postInstall = ''
mkdir -p $out/share/applications/
cp assets/himalaya.desktop $out/share/applications/
'';
};
};
};
# nix run
defaultApp = apps.${name};
apps.${name} = utils.lib.mkApp {
inherit name;
drv = packages.${name};
};
# nix develop
devShell = pkgs.mkShell {
inputsFrom = builtins.attrValues self.packages.${system};
nativeBuildInputs = with pkgs; [
# Nix LSP + formatter
{
default = pkgs.mkShell {
buildInputs = with pkgs; [
# Nix env
rnix-lsp
nixpkgs-fmt
# Rust env
(rust-bin.fromRustupToolchainFile ./rust-toolchain.toml)
cargo-watch
rust-analyzer
rust-toolchain
# Notmuch
# notmuch
notmuch
];
};
}
);
};
mkPackage = pkgs: buildPlatform: targetPlatform: package:
let
toolchain =
if isNull targetPlatform
then mkToolchain buildPlatform
else mkToolchainWithTarget buildPlatform targetPlatform;
naersk' = naersk.lib.${buildPlatform}.override {
cargo = toolchain;
rustc = toolchain;
};
package' = {
name = "himalaya";
src = gitignoreSource ./.;
overrideMain = _: {
postInstall = ''
mkdir -p $out/share/applications/
cp assets/himalaya.desktop $out/share/applications/
'';
};
} // pkgs.lib.optionalAttrs (!isNull targetPlatform) {
CARGO_BUILD_TARGET = targetPlatform;
} // package;
in
naersk'.buildPackage package';
mkPackages = buildPlatform:
let
pkgs = import nixpkgs { system = buildPlatform; };
mkPackageWithTarget = mkPackage pkgs buildPlatform;
defaultPackage = mkPackage pkgs buildPlatform null { };
in
{
default = defaultPackage;
linux = defaultPackage;
macos = defaultPackage;
musl = mkPackageWithTarget "x86_64-unknown-linux-musl" (with pkgs.pkgsStatic; {
CARGO_BUILD_RUSTFLAGS = "-C target-feature=+crt-static";
SQLITE3_STATIC = 1;
SQLITE3_LIB_DIR = "${sqlite.out}/lib";
hardeningDisable = [ "all" ];
});
# FIXME: package does not build, assembler messages: unknown
# pseudo-op…
windows = mkPackageWithTarget "x86_64-pc-windows-gnu" {
strictDeps = true;
depsBuildBuild = with pkgs.pkgsCross.mingwW64; [
stdenv.cc
windows.pthreads
];
};
};
mkApp = drv: flake-utils.lib.mkApp {
inherit drv;
name = "himalaya";
};
mkApps = buildPlatform: {
default = mkApp self.packages.${buildPlatform}.default;
linux = mkApp self.packages.${buildPlatform}.linux;
macos = mkApp self.packages.${buildPlatform}.macos;
musl = mkApp self.packages.${buildPlatform}.musl;
windows =
let
pkgs = import nixpkgs { system = buildPlatform; };
wine = pkgs.wine.override { wineBuild = "wine64"; };
himalaya = self.packages.${buildPlatform}.windows;
app = pkgs.writeShellScriptBin "himalaya" ''
export WINEPREFIX="$(mktemp -d)"
${wine}/bin/wine64 ${himalaya}/bin/himalaya.exe $@
'';
in
mkApp app;
};
in
flake-utils.lib.eachDefaultSystem (system: {
devShells = mkDevShells system;
packages = mkPackages system;
apps = mkApps system;
});
}

View file

@ -1,2 +1,11 @@
[toolchain]
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.
pub fn arg() -> Arg {
Arg::new(ARG_DISABLE_CACHE)
.long("disable-cache")
.help("Disable any sort of cache")
.long_help(
"Disable any sort of cache. The action depends on
the command it applies on.",
)
.long("disable-cache")
.global(true)
.action(ArgAction::SetTrue)
}

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@ use crate::account::{
use anyhow::Result;
use dialoguer::Input;
use dirs::home_dir;
use himalaya_lib::MaildirConfig;
use pimalaya_email::MaildirConfig;
pub(crate) fn configure(base: DeserializedBaseAccountConfig) -> Result<DeserializedAccountConfig> {
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);
pub(crate) fn wizard() -> Result<DeserializedConfig> {
trace!(">> wizard");
println!("Himalaya couldn't find an already existing configuration file.");
match Confirm::new()
@ -111,7 +110,7 @@ pub(crate) fn wizard() -> Result<DeserializedConfig> {
// Serialize config to file
println!("\nWriting the configuration to {path:?}...");
fs::create_dir_all(path.parent().unwrap())?;
fs::write(path, toml::to_vec(&config)?)?;
fs::write(path, toml::to_string(&config)?)?;
trace!("<< wizard");
Ok(config)

View file

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

View file

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

View file

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

View file

@ -3,8 +3,10 @@
use anyhow::Result;
use clap::{Arg, ArgAction, ArgMatches, Command};
use log::info;
use pimalaya_email::folder::sync::Strategy as SyncFoldersStrategy;
use std::collections::HashSet;
use crate::ui::table;
use crate::{folder, ui::table};
const ARG_ACCOUNT: &str = "account";
const ARG_DRY_RUN: &str = "dry-run";
@ -20,7 +22,7 @@ pub enum Cmd {
/// Represents the list accounts command.
List(table::args::MaxTableWidth),
/// Represents the sync account command.
Sync(DryRun),
Sync(Option<SyncFoldersStrategy>, DryRun),
}
/// 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) {
info!("sync account subcommand matched");
let dry_run = parse_dry_run_arg(m);
Some(Cmd::Sync(dry_run))
let include = folder::args::parse_include_arg(m);
let exclude = folder::args::parse_exclude_arg(m);
let folders_strategy = if let Some(folder) = folder::args::parse_source_arg(m) {
Some(SyncFoldersStrategy::Include(HashSet::from_iter([
folder.to_owned()
])))
} else if !include.is_empty() {
Some(SyncFoldersStrategy::Include(include.to_owned()))
} else if !exclude.is_empty() {
Some(SyncFoldersStrategy::Exclude(exclude))
} else if folder::args::parse_all_arg(m) {
Some(SyncFoldersStrategy::All)
} else {
None
};
Some(Cmd::Sync(folders_strategy, dry_run))
} else if let Some(m) = m.subcommand_matches(CMD_LIST) {
info!("list accounts subcommand matched");
let max_table_width = table::args::parse_max_width(m);
@ -55,6 +72,13 @@ pub fn subcmd() -> Command {
.arg(table::args::max_width()),
Command::new(CMD_SYNC)
.about("Synchronize the given account locally")
.arg(folder::args::all_arg("Synchronize all folders"))
.arg(folder::args::include_arg(
"Synchronize only the given folders",
))
.arg(folder::args::exclude_arg(
"Synchronize all folders except the given ones",
))
.arg(dry_run()),
])
}
@ -63,9 +87,10 @@ pub fn subcmd() -> Command {
/// the user to select a different account than the default one.
pub fn arg() -> Arg {
Arg::new(ARG_ACCOUNT)
.help("Set the account")
.long("account")
.short('a')
.help("Select a specific account by name")
.global(true)
.value_name("STRING")
}

View file

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

View file

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

View file

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

View file

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

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 {
fn head() -> Row {
@ -14,15 +53,29 @@ impl Table for Envelope {
fn row(&self) -> Row {
let id = self.id.to_string();
let flags = self.flags.to_symbols_string();
let unseen = !self.flags.contains(&Flag::Seen);
let flags = {
let mut flags = String::new();
flags.push_str(if !unseen { " " } else { "" });
flags.push_str(if self.flags.contains(&Flag::Answered) {
""
} else {
" "
});
flags.push_str(if self.flags.contains(&Flag::Flagged) {
""
} else {
" "
});
flags
};
let subject = &self.subject;
let sender = if let Some(name) = &self.from.name {
name
} else {
&self.from.addr
};
let date = self.date.format("%d/%m/%Y %H:%M").to_string();
let date = self.date.to_rfc3339();
Row::new()
.cell(Cell::new(id).bold_if(unseen).red())

View file

@ -1,11 +1,32 @@
use std::ops;
use anyhow::Result;
use himalaya_lib::Envelopes;
use serde::Serialize;
use crate::{
printer::{PrintTable, PrintTableOpts, WriteColor},
ui::Table,
Envelope,
};
/// Represents the list of envelopes.
#[derive(Clone, Debug, Default, Serialize)]
pub struct Envelopes(Vec<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 {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writer)?;

View file

@ -5,8 +5,8 @@
use anyhow::Result;
use clap::{Arg, ArgMatches, Command};
use himalaya_lib::{Flag, Flags};
use log::{debug, info};
use pimalaya_email::{Flag, Flags};
use crate::email;
@ -83,7 +83,11 @@ pub fn flags_arg() -> Arg {
Arg::new(ARG_FLAGS)
.value_name("FLAGS")
.help("The flags")
.long_help("The list of flags. It can be one of: seen, answered, flagged, deleted, draft, recent. Other flags are considered custom.")
.long_help(
"The list of flags.
It can be one of: seen, answered, flagged, deleted, or draft.
Other flags are considered custom.",
)
.num_args(1..)
.required(true)
.last(true)

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 himalaya_lib::{Backend, Flags};
use pimalaya_email::{Backend, Flags};
use crate::printer::Printer;

View file

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

View file

@ -1,7 +1,24 @@
use himalaya_lib::folder::Folder;
use serde::Serialize;
use crate::ui::{Cell, Row, Table};
#[derive(Clone, Debug, Default, Serialize)]
pub struct Folder {
pub delim: String,
pub name: String,
pub desc: String,
}
impl From<&pimalaya_email::Folder> for Folder {
fn from(folder: &pimalaya_email::Folder) -> Self {
Folder {
delim: folder.delim.clone(),
name: folder.name.clone(),
desc: folder.desc.clone(),
}
}
}
impl Table for Folder {
fn head() -> Row {
Row::new()

View file

@ -1,11 +1,31 @@
use std::ops;
use anyhow::Result;
use himalaya_lib::folder::Folders;
use serde::Serialize;
use crate::{
printer::{PrintTable, PrintTableOpts, WriteColor},
ui::Table,
Folder,
};
#[derive(Clone, Debug, Default, Serialize)]
pub struct Folders(Vec<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 {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writer)?;

View file

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

View file

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

View file

@ -1,6 +1,6 @@
use anyhow::{anyhow, Result};
use atty::Stream;
use himalaya_lib::{AccountConfig, Backend, CompilerBuilder, Email, Flags, Sender, Tpl};
use pimalaya_email::{AccountConfig, Backend, CompilerBuilder, Email, Flags, Sender, Tpl};
use std::io::{stdin, BufRead};
use crate::printer::Printer;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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